From eab5e2e7eb3164ab13f2b8bb92e7b83700404bf9 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 29 May 2026 11:16:17 +0000 Subject: [PATCH 01/20] Auto: hooks daemon regenerated CLAUDE.md handler guidance --- CLAUDE.md | 45 ++++++++++++++------------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a630470..1d3aafb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -452,14 +452,12 @@ The handlers listed below are active in this project. Read this section to avoid AskUserQuestion calls are only allowed when every `question` string begins with `ASKING BECAUSE:` (case-sensitive, leading whitespace OK). The convention mirrors the Stop handler's `STOPPING BECAUSE:` pattern — explicit declared intent gates the privilege of pausing the session. **Before asking, evaluate critically**: - - Tautological/rhetorical questions with one obvious answer ("Should I continue?", "Would you like me to proceed?") — do NOT ask. State the question and your assumed-correct answer in plain output text and proceed. The user is watching and will interrupt if the assumption is wrong. - Questions whose options reduce to **good vs. bad** are tautological — the answer is always the good option. Examples: best practice vs. bodge, increasing vs. decreasing code quality, delivering the requirement vs. not delivering it, fixing the failing test vs. leaving it broken, following project conventions vs. inventing your own. Do NOT ask; pick the good option and proceed. - Errors with a clear recovery path ("Should I fix the failing test?") — do NOT ask. Fix it. - Genuine choice questions where you cannot resolve the answer from context — these are the legitimate use case. Prefix every question text with `ASKING BECAUSE: ` so the daemon allows the call through. **Audit log pattern** (preferred for tautological questions): - ``` I would normally ask: . Assumed answer: . @@ -472,17 +470,17 @@ Proceeding on that basis; the user will interrupt if wrong. The following git commands are permanently blocked and will always be denied: -| Command | Reason | -| ------------------------ | ------------------------------------------------------------------------ | -| `git reset --hard` | Permanently destroys all uncommitted changes | -| `git clean -f` | Permanently deletes untracked files | -| `git checkout -- ` | Discards all local changes to that file | -| `git restore ` | Discards local changes (`--staged` is allowed) | -| `git stash drop` | Permanently destroys stashed changes | -| `git stash clear` | Permanently destroys all stashes | -| `git push --force` | Can overwrite remote history and destroy teammates' work | -| `git branch -D` | Force-deletes branch without checking if merged (lowercase `-d` is safe) | -| `git commit --amend` | Rewrites the previous commit — create a new commit instead | +| Command | Reason | +|---------|--------| +| `git reset --hard` | Permanently destroys all uncommitted changes | +| `git clean -f` | Permanently deletes untracked files | +| `git checkout -- ` | Discards all local changes to that file | +| `git restore ` | Discards local changes (`--staged` is allowed) | +| `git stash drop` | Permanently destroys stashed changes | +| `git stash clear` | Permanently destroys all stashes | +| `git push --force` | Can overwrite remote history and destroy teammates' work | +| `git branch -D` | Force-deletes branch without checking if merged (lowercase `-d` is safe) | +| `git commit --amend` | Rewrites the previous commit — create a new commit instead | If the user needs to run one of these, ask them to do it manually. Do not attempt to work around the block. @@ -493,18 +491,15 @@ If the user needs to run one of these, ask them to do it manually. Do not attemp `sed` is blocked because Claude gets sed syntax wrong and a single error can silently destroy hundreds of files with no recovery possible. **Blocked**: - - `sed -i` / `sed -e` (in-place file editing via Bash tool) - `grep -rl X | xargs sed -i` (mass file modification) - Shell scripts (`.sh`/`.bash`) written via Write tool that contain `sed` **Allowed** (read-only, no file modification): - - `cat file | sed 's/x/y/' | grep z` (pipeline transforming stdout only) - `sed` mentioned in commit messages, PR bodies, or `.md` documentation files **Use instead**: - - `Edit` tool — safe, atomic, verifiable - Parallel Haiku agents with `Edit` tool for bulk changes across many files: 1. Identify all files to update @@ -539,7 +534,6 @@ The working directory is `/workspace`. Prepend `/workspace/` to any relative pat Writing code that silently swallows errors is blocked. All errors must be handled explicitly. **Blocked patterns (examples)**: - - Python: bare `except` clauses with an empty body, catching and discarding all exceptions - Shell: redirecting stderr to `/dev/null` to silence failures, `|| true` to suppress non-zero exit codes - JavaScript/TypeScript: empty `catch` blocks that swallow exceptions @@ -554,7 +548,6 @@ Piping network content directly to a shell is blocked. It executes untrusted rem **Blocked**: `curl URL | bash`, `curl URL | sh`, `wget URL | bash`, `curl URL | sudo bash` **Safe alternative**: download first, inspect, then execute: - ``` curl -o /tmp/script.sh URL cat /tmp/script.sh # inspect @@ -566,7 +559,6 @@ bash /tmp/script.sh # execute if safe Writing code that contains security antipatterns is blocked across all supported languages. Fix the code to use safe patterns instead. **Blocked categories**: - - SQL injection: building queries via string concatenation (use parameterised queries) - Command injection: passing unvalidated input to subprocess (use argument lists) - Hardcoded credentials: API keys, passwords, tokens embedded in source code @@ -609,7 +601,6 @@ Worktrees are isolated branches. Cross-copying corrupts that isolation and can s **Why**: stashes get forgotten, lost, and block `git pull`. Use `git commit -m 'WIP: ...'` instead — WIP commits are acceptable. **Escape hatch** (when commit truly won't work): - ``` MUST_STASH_BECAUSE="explain why"; git stash ``` @@ -623,7 +614,6 @@ Configure via `handlers.pre_tool_use.git_stash.options.mode: warn` for advisory- **Blocked**: `chmod 777`, `chmod 666`, `chmod a+w`, `chmod o+w` **Use least-privilege permissions instead**: - - Executable scripts: `chmod 755` (owner rwx, group/other rx) - Regular files: `chmod 644` (owner rw, group/other r) - Private files: `chmod 600` (owner rw only) @@ -635,7 +625,6 @@ Direct `Write` or `Edit` to package manager lock files is blocked. Lock files ar **Blocked files**: `composer.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, `Package.resolved`, `Pipfile.lock`, and others. **Use package manager commands instead**: - - PHP: `composer install` / `composer require package` - Node: `npm install` / `yarn add package` - Ruby: `bundle install` / `bundle add gem` @@ -675,7 +664,6 @@ Even in a container running as root, `sudo` adds nothing — drop it and use a v Using `Grep` or `Bash` (grep/rg) to find class definitions, function signatures, or symbol references is blocked or redirected to LSP tools, which are faster and semantically accurate. **Prefer LSP tools for**: - - Finding where a class or function is defined → `goToDefinition` - Finding all usages of a symbol → `findReferences` - Getting type information or documentation → `hover` @@ -711,7 +699,6 @@ If using `--json`, include `comments` in the field list instead of adding `--com Writes to `src/data/articles.ts` that embed multi-line code directly inside `
...
` blocks are blocked. Articles must reference code via `{{SNIPPET:article-slug/filename.ext}}` placeholders. **Workflow**: - 1. Create the code file under `code-snippets//`. 2. Reference it from the article: `
{{SNIPPET:article-slug/example.php}}
`. 3. The build step (`scripts/generate-snippets.mjs`) auto-generates `src/data/snippets.ts` from those files. @@ -771,18 +758,15 @@ STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified. **Why**: The stop hook enforces intentional stops. Stopping without an explanation triggers an auto-block that asks you to explain or continue. **Alternatives**: - - `STOPPING BECAUSE: ` — stops cleanly with explanation - Continue working — no need to stop unless all work is genuinely complete **Do NOT**: - - Stop mid-task without explanation - Ask confirmation questions and then stop (the hook auto-continues those) - Use `AUTO-CONTINUE` unless you intend to keep working indefinitely **Before asking a question, evaluate it critically**: - - Tautological/rhetorical questions with obvious answers ("Should I continue?", "Would you like me to proceed?") — do NOT ask, just do it - Errors with a clear next step ("The test failed, should I fix it?") — do NOT ask, just fix it - Genuine choice questions where all options are valid ("Which of A, B, or C should we use?") — these deserve a response. Use `STOPPING BECAUSE: need user input` and ask your question @@ -790,7 +774,6 @@ STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified. **Recovering from a `tool_use_error` — do NOT stop silently**: Some tool errors require an explicit recovery action, not a halt. The most common shape: - - You call `Edit` or `Write` on a file you have not yet read. - Claude Code returns a `tool_use_error` (e.g. "File has not been read yet"). - The correct recovery is **Read the file, then retry Edit/Write** — **do not stop**. Stopping silently after a tool error triggers a Stop-hook re-entry loop and wastes a turn. @@ -805,9 +788,9 @@ Stop-time advisory that fires on language patterns signalling avoidance of work. **Avoid**: -- Dismissing issues as `pre-existing`, `out of scope`, `not our problem`, or `not relevant` to deflect work that is in fact yours. -- Premature-halt phrasing like `natural checkpoint`, `ready to continue on your cue`, `pausing here` mid-plan when there is more to do — finish the task rather than dressing up a halt. -- Speculative `should be fine` or `probably works` when verification is cheap (run the test, read the file). +- Dismissing issues as `pre-existing`, `out of scope`, `not our problem`, or `not relevant` to deflect work that is in fact yours. +- Premature-halt phrasing like `natural checkpoint`, `ready to continue on your cue`, `pausing here` mid-plan when there is more to do — finish the task rather than dressing up a halt. +- Speculative `should be fine` or `probably works` when verification is cheap (run the test, read the file). **Do**: acknowledge the issue, fix it, or — if it genuinely is out of scope — say so once with the specific reason and continue with the in-scope work. From 89aff2e5f4a9980c0f713d999890d2c0c1d28d12 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 09:28:48 +0000 Subject: [PATCH 02/20] Auto: hooks daemon regenerated CLAUDE.md handler guidance --- CLAUDE.md | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 225 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1d3aafb..3f57992 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -452,12 +452,14 @@ The handlers listed below are active in this project. Read this section to avoid AskUserQuestion calls are only allowed when every `question` string begins with `ASKING BECAUSE:` (case-sensitive, leading whitespace OK). The convention mirrors the Stop handler's `STOPPING BECAUSE:` pattern — explicit declared intent gates the privilege of pausing the session. **Before asking, evaluate critically**: + - Tautological/rhetorical questions with one obvious answer ("Should I continue?", "Would you like me to proceed?") — do NOT ask. State the question and your assumed-correct answer in plain output text and proceed. The user is watching and will interrupt if the assumption is wrong. - Questions whose options reduce to **good vs. bad** are tautological — the answer is always the good option. Examples: best practice vs. bodge, increasing vs. decreasing code quality, delivering the requirement vs. not delivering it, fixing the failing test vs. leaving it broken, following project conventions vs. inventing your own. Do NOT ask; pick the good option and proceed. - Errors with a clear recovery path ("Should I fix the failing test?") — do NOT ask. Fix it. - Genuine choice questions where you cannot resolve the answer from context — these are the legitimate use case. Prefix every question text with `ASKING BECAUSE: ` so the daemon allows the call through. **Audit log pattern** (preferred for tautological questions): + ``` I would normally ask: . Assumed answer: . @@ -470,17 +472,17 @@ Proceeding on that basis; the user will interrupt if wrong. The following git commands are permanently blocked and will always be denied: -| Command | Reason | -|---------|--------| -| `git reset --hard` | Permanently destroys all uncommitted changes | -| `git clean -f` | Permanently deletes untracked files | -| `git checkout -- ` | Discards all local changes to that file | -| `git restore ` | Discards local changes (`--staged` is allowed) | -| `git stash drop` | Permanently destroys stashed changes | -| `git stash clear` | Permanently destroys all stashes | -| `git push --force` | Can overwrite remote history and destroy teammates' work | -| `git branch -D` | Force-deletes branch without checking if merged (lowercase `-d` is safe) | -| `git commit --amend` | Rewrites the previous commit — create a new commit instead | +| Command | Reason | +| ------------------------ | ------------------------------------------------------------------------ | +| `git reset --hard` | Permanently destroys all uncommitted changes | +| `git clean -f` | Permanently deletes untracked files | +| `git checkout -- ` | Discards all local changes to that file | +| `git restore ` | Discards local changes (`--staged` is allowed) | +| `git stash drop` | Permanently destroys stashed changes | +| `git stash clear` | Permanently destroys all stashes | +| `git push --force` | Can overwrite remote history and destroy teammates' work | +| `git branch -D` | Force-deletes branch without checking if merged (lowercase `-d` is safe) | +| `git commit --amend` | Rewrites the previous commit — create a new commit instead | If the user needs to run one of these, ask them to do it manually. Do not attempt to work around the block. @@ -491,15 +493,18 @@ If the user needs to run one of these, ask them to do it manually. Do not attemp `sed` is blocked because Claude gets sed syntax wrong and a single error can silently destroy hundreds of files with no recovery possible. **Blocked**: + - `sed -i` / `sed -e` (in-place file editing via Bash tool) - `grep -rl X | xargs sed -i` (mass file modification) - Shell scripts (`.sh`/`.bash`) written via Write tool that contain `sed` **Allowed** (read-only, no file modification): + - `cat file | sed 's/x/y/' | grep z` (pipeline transforming stdout only) - `sed` mentioned in commit messages, PR bodies, or `.md` documentation files **Use instead**: + - `Edit` tool — safe, atomic, verifiable - Parallel Haiku agents with `Edit` tool for bulk changes across many files: 1. Identify all files to update @@ -534,6 +539,7 @@ The working directory is `/workspace`. Prepend `/workspace/` to any relative pat Writing code that silently swallows errors is blocked. All errors must be handled explicitly. **Blocked patterns (examples)**: + - Python: bare `except` clauses with an empty body, catching and discarding all exceptions - Shell: redirecting stderr to `/dev/null` to silence failures, `|| true` to suppress non-zero exit codes - JavaScript/TypeScript: empty `catch` blocks that swallow exceptions @@ -548,6 +554,7 @@ Piping network content directly to a shell is blocked. It executes untrusted rem **Blocked**: `curl URL | bash`, `curl URL | sh`, `wget URL | bash`, `curl URL | sudo bash` **Safe alternative**: download first, inspect, then execute: + ``` curl -o /tmp/script.sh URL cat /tmp/script.sh # inspect @@ -559,6 +566,7 @@ bash /tmp/script.sh # execute if safe Writing code that contains security antipatterns is blocked across all supported languages. Fix the code to use safe patterns instead. **Blocked categories**: + - SQL injection: building queries via string concatenation (use parameterised queries) - Command injection: passing unvalidated input to subprocess (use argument lists) - Hardcoded credentials: API keys, passwords, tokens embedded in source code @@ -594,6 +602,25 @@ Worktrees are isolated branches. Cross-copying corrupts that isolation and can s **Allowed**: operations within the same worktree branch. **To merge changes**: use `git merge` or `git cherry-pick` instead. +## root_recursion_guard — recursive scans rooted at / are blocked + +A recursive scanner whose path argument resolves to a catastrophic root location is blocked, because it walks the entire filesystem and can pin every CPU core for hours. + +**Blocked** (recursive scanner + dangerous root path): + +- `grep -r`/`-R`/`-rl`, `ugrep -r`, `rgrep`, `find`, `fd`/`fdfind`, `rg` +- pointed at `/`, `/proc`, `/sys`, `/home`, `/root`, `~`, `$HOME` + +**Allowed**: the same scanners scoped to the project — `rg -l "x" /workspace`, `grep -rl "x" "$CLAUDE_PROJECT_DIR"`, `grep -rl x src/`, `find . -name y`. Non-recursive `grep x /etc/hosts` is not affected. + +**Note**: `... | head` does NOT bound a `-l`/`-rl` scan — a producer that matches nothing never writes, so it never receives SIGPIPE and runs to completion across the whole disk. + +**Escape hatch** (rare legitimate whole-disk scan): + +``` +MUST_SCAN_ROOT_BECAUSE="explain why"; grep -rl x / +``` + ## git_stash — git stash is blocked by default `git stash`, `git stash push`, and `git stash save` are blocked. `git stash pop`, `git stash apply`, `git stash list`, and `git stash show` are always allowed. @@ -601,6 +628,7 @@ Worktrees are isolated branches. Cross-copying corrupts that isolation and can s **Why**: stashes get forgotten, lost, and block `git pull`. Use `git commit -m 'WIP: ...'` instead — WIP commits are acceptable. **Escape hatch** (when commit truly won't work): + ``` MUST_STASH_BECAUSE="explain why"; git stash ``` @@ -614,6 +642,7 @@ Configure via `handlers.pre_tool_use.git_stash.options.mode: warn` for advisory- **Blocked**: `chmod 777`, `chmod 666`, `chmod a+w`, `chmod o+w` **Use least-privilege permissions instead**: + - Executable scripts: `chmod 755` (owner rwx, group/other rx) - Regular files: `chmod 644` (owner rw, group/other r) - Private files: `chmod 600` (owner rw only) @@ -625,6 +654,7 @@ Direct `Write` or `Edit` to package manager lock files is blocked. Lock files ar **Blocked files**: `composer.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, `Package.resolved`, `Pipfile.lock`, and others. **Use package manager commands instead**: + - PHP: `composer install` / `composer require package` - Node: `npm install` / `yarn add package` - Ruby: `bundle install` / `bundle add gem` @@ -664,6 +694,7 @@ Even in a container running as root, `sudo` adds nothing — drop it and use a v Using `Grep` or `Bash` (grep/rg) to find class definitions, function signatures, or symbol references is blocked or redirected to LSP tools, which are faster and semantically accurate. **Prefer LSP tools for**: + - Finding where a class or function is defined → `goToDefinition` - Finding all usages of a symbol → `findReferences` - Getting type information or documentation → `hover` @@ -694,17 +725,96 @@ If using `--json`, include `comments` in the field list instead of adding `--com If using `--json`, include `comments` in the field list instead of adding `--comments`. +## plan_qa_commit_gate — cross-file plan checks at git commit + +Every `git commit` is checked against the STAGED tree's plan QA +invariants. In `commit_gate_mode: warn` (the rollout default) +violations appear as advisory context — read them and amend the +commit content BEFORE committing; in `block` mode they deny the +commit with a TODO list of what the commit must also contain. + +**The invariants**: + +- creating a plan folder ⇒ the SAME commit stages its README + index row (`index-at-birth`) and the number must come from the + git counter / mkplan.bash (`counter-sanity`, `no-new-collisions`) +- flipping a plan to Complete/Cancelled/Superseded ⇒ the SAME + commit contains the `git mv` into the archive dir AND the README + row + statistics update (`terminal-state-atomic`) +- every folder has a README row in the section matching its + location, and every row's link resolves + (`row-folder-bijection`, `stats-recount`) +- a commit claiming `Plan NNNNN` that stages src/tests/config + changes should also update that plan's PLAN.md + (`same-commit-plan-doc`); reference plans as `Plan NNNNN:` + (`plan-ref-format`) + +Check the staged tree any time without committing: +`$PYTHON -m claude_code_hooks_daemon.daemon.cli plan-qa --check-staged`. +Commits inside nested/vendor repos or foreign worktrees are exempt. + +## plan_qa_edit — PLAN.md writes are linted in real time + +Every Write/Edit of a `PLAN.md` under the plan directory is checked +against the plan QA edit-stage rules on the content the file WOULD +have. Block-level violations (in `edit_mode: block`) deny the tool +call with the exact remediation; fix the content and retry. + +**Rules that block new plan material**: + +- a parseable `**Status**:` line must exist (`status-line-present`) +- the status token must be one of: Not Started, In Progress, + Complete, Blocked, Cancelled, Superseded, Dormant + (`status-enum-and-date`) +- the header must not contradict the body — do not leave + `Not Started`/`In Progress` above an all-ticked task list or + "ALL DONE" prose; flip the status instead + (`header-body-coherence`) +- use the template task grammar `- [ ] ⬜ **Task N.N**:` — not + ad-hoc markers like `[✓]`/`[⏳]` (`task-grammar`) + +**Advisory rules**: missing Created/Owner/Priority headers on new +plans; a terminal status set while the folder is still in the plan +root (the same commit must `git mv` it to the archive dir and +update the README row); edits to archived plans; backticked +`src/...` paths that no longer exist. + +Grandfathered plans in `plan_workflow.qa.legacy_plan_allowlist` +only ever advise. Lint any file on demand: +`$PYTHON -m claude_code_hooks_daemon.daemon.cli plan-qa --lint `. + ## article-snippet-enforcer — articles must use the snippet system Writes to `src/data/articles.ts` that embed multi-line code directly inside `
...
` blocks are blocked. Articles must reference code via `{{SNIPPET:article-slug/filename.ext}}` placeholders. **Workflow**: + 1. Create the code file under `code-snippets//`. 2. Reference it from the article: `
{{SNIPPET:article-slug/example.php}}
`. 3. The build step (`scripts/generate-snippets.mjs`) auto-generates `src/data/snippets.ts` from those files. Short inline references like `exampleVar` are allowed. +## background_process_tracker — backgrounded processes are tracked + +A PostToolUse advisory that fires when a Bash call backgrounds a process (`run_in_background: true`, or a `&`/`nohup`/`setsid`/`disown` command). It records the command to `background-processes.jsonl` and injects rate-limited guidance. + +**The daemon never kills.** It surfaces runaways; you decide. + +When you background a long-lived process: + +- Create a non-durable recurring **watchdog cron** (CronCreate, durable:false) whose prompt runs `$PYTHON -m claude_code_hooks_daemon.daemon.cli harvest-background` and acts on any runaway — this covers the idle/compaction window a tool-call hook cannot. Do NOT wait for the cron; keep working. +- Check on demand: run `harvest-background` (exit 1 == runaways surfaced). +- Reap a runaway by its **process group**: `kill -- -` (not just the pid). +- Keep a wanted long task: note `KEEP_RUNNING_BECAUSE="reason"`. +- Delete the watchdog cron (CronDelete) when no backgrounded work remains. + +Advisory is rate-limited per session (default-on). Disable with `handlers.post_tool_use.background_process_tracker.enabled: false`. + +## git_hooks_executable_fixer — auto-fixes non-executable git hooks + +When a git command prints `hint: The '...' hook was ignored because it's not set as executable`, this handler automatically `chmod +x`s every non-`.sample` file in the repository's hooks directory (resolved via `git rev-parse --git-path hooks`, so worktrees and `core.hooksPath` are handled). Execute bits are added with least privilege (only where read is already granted). It never blocks the command and reports which hooks it fixed via advisory context. `.sample` files and already-executable hooks are left untouched. + ## markdown_table_formatter — markdown tables are auto-aligned After every `Write` or `Edit` of a `.md` or `.markdown` file, the content is re-formatted via `mdformat + mdformat-gfm` so that table pipes are aligned and column widths are consistent. The handler is non-terminal and advisory — it never blocks, it just rewrites the file on disk. @@ -722,6 +832,69 @@ After every `Write` or `Edit` of a `.md` or `.markdown` file, the content is re- $PYTHON -m claude_code_hooks_daemon.daemon.cli format-markdown ``` +## recovery_cron_advisor — failsafe recovery cron lifecycle advisory + +An advisory PostToolUse handler that fires across a plan's lifecycle and +injects guidance telling the agent to manage a non-durable hourly failsafe +recovery cron. + +### What it does + +Three lifecycle phases are detected from Write/Edit to `CLAUDE/Plan/-/PLAN.md` +(never from files inside `Completed/`) and from `mkplan.bash` Bash invocations: + +| Phase | Trigger | Guidance injected | +| -------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Creation** | New PLAN.md written, or `mkplan.bash` invoked | Create a non-durable hourly cron now (CronCreate, durable:false); record the ID in the plan; do NOT wait for the cron. | +| **Progress** | Edit to PLAN.md touching task-status icons (⬜/🔄/✅) or `## Notes & Updates` section | Confirm the recovery cron is still running (CronList); recreate if missing; keep working. | +| **Completion** | `**Status**: Complete[d]` written/edited | Plan complete — **warns first**: deleting now leaves the still-live session with no recovery coverage. Keep the cron if any further work may happen (it is non-durable and dies on session exit); `CronDelete` only when certain the session is finished. | + +Progress reminders are rate-limited per plan: the handler advises on the first +progress edit and then once every few progress edits for that plan, so it does +not spam context on every edit. Completion always advises (bypasses the interval). + +### CRITICAL: recovery cron is NOT a heartbeat + +The recovery cron is a **failsafe safety net**, not a pacing mechanism: + +- The agent **must never** wait for the cron between units of work. +- Work proceeds at **full speed** until an external factor (Claude API error, + rate limit, 5-hour usage limit, network failure) actually stalls it. +- The cron fires only while the REPL is idle; it cannot interrupt active work. +- Treating the cron as a heartbeat is an **own goal** — it would convert a + safety net into an artificial hourly throttle. + +### Canonical recovery-cron prompt + +Use this verbatim as the CronCreate prompt: + +``` +**FAILSAFE RECOVERY CHECK (automated hourly safety net — NOT a heartbeat).** +If your most recent work on the active plan/task was interrupted by an +*external* factor (Claude API error/overload, rate limit, 5-hour usage limit, +network failure) and is now resumable, resume it immediately and carry it to +completion. If you are blocked **only** on human input, do nothing and keep +waiting. If work is already proceeding normally, this is a **no-op** — do not +interrupt, restart, or duplicate anything in flight. Never treat this as a +heartbeat or pacing signal: between checks, continue at full speed until an +external factor actually stops you — waiting for the cron is an own goal. Do +NOT delete this cron merely because a tick finds nothing to resume: it is +non-durable and ends automatically when the session exits, and a still-live +session stays exposed to the next rate limit without it. Remove it (CronDelete) +only once the session is genuinely finished with no further work. +``` + +### Configuration + +This handler is **on by default** (opt-out). Disable with: + +```yaml +handlers: + post_tool_use: + recovery_cron_advisor: + enabled: false +``` + ## hook_registration_checker — hooks configuration policy On every new session this handler audits hook configuration across `.claude/settings.json` and `.claude/settings.local.json`. When it reports issues, fix them — do not ignore the warning. @@ -739,6 +912,40 @@ On every new session this handler audits hook configuration across `.claude/sett - **Missing hooks**: the daemon's installer writes the full set. If any are missing, re-run `install.py` or manually add the missing `{event_name}` entry pointing at `"$CLAUDE_PROJECT_DIR"/.claude/hooks/{bash-key}`. - **Duplicate hooks**: a hook registered in both files fires twice. Keep the `settings.json` entry, delete from `settings.local.json`. +## plan_qa_sweep — plan-tree drift report at session start + +At the start of each new session the plan directory is swept with the +plan QA check catalogue (index/folder bijection, number collisions, +statistics recount, archive structure, status-vs-location coherence, +staleness). Findings are injected once as advisory context — the +sweep never blocks. + +**When a drift report appears**: fix the listed findings (each names +its exact remediation) as part of your plan housekeeping, then +re-check with: + +``` +$PYTHON -m claude_code_hooks_daemon.daemon.cli plan-qa --sweep +``` + +The CLI exits 1 while findings remain (CI-able). Single-file lint: +`plan-qa --lint `; staged-commit check: `plan-qa --check-staged`. +Policy lives under `plan_workflow.qa` in `.claude/hooks-daemon.yaml` +(archive dir names, staleness window, legacy/collision allowlists). + +## project_handler_load_checker — project protection degraded alert + +At session start this handler reports any **project handlers** (`.claude/project-handlers/`) that FAILED to load in the running daemon. A skipped handler is a silently-disabled protection — the alert exists so you never assume a guardrail is active when it is not. + +### When you see `🚨 PROJECT PROTECTION DEGRADED 🚨` + +1. **Do not assume normal guardrails are in force.** The listed handlers are OFF for this session. +2. **Diagnose** each failure: `$PYTHON -m claude_code_hooks_daemon.daemon.cli validate-project-handlers` names the file, the missing method, and the daemon version that introduced it. +3. **Fix** the handler(s) — usually adding a required method stub (e.g. `get_claude_md`) that a daemon upgrade made mandatory. +4. **Restart the daemon** (`$PYTHON -m claude_code_hooks_daemon.daemon.cli restart`). The alert reflects the *running* daemon, so it clears only after a restart reloads the fixed handlers — fixing the file alone is not enough. + +The handler is silent when every project handler loads, so seeing this alert always means real action is required. + ## auto_approve_reads — gated on bypassPermissions mode Read-only tool permission requests (`Read`, `Glob`, `Grep`) are auto-approved **only** when Claude Code reports `permission_mode == "bypassPermissions"` (YOLO mode). @@ -758,15 +965,18 @@ STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified. **Why**: The stop hook enforces intentional stops. Stopping without an explanation triggers an auto-block that asks you to explain or continue. **Alternatives**: + - `STOPPING BECAUSE: ` — stops cleanly with explanation - Continue working — no need to stop unless all work is genuinely complete **Do NOT**: + - Stop mid-task without explanation - Ask confirmation questions and then stop (the hook auto-continues those) - Use `AUTO-CONTINUE` unless you intend to keep working indefinitely **Before asking a question, evaluate it critically**: + - Tautological/rhetorical questions with obvious answers ("Should I continue?", "Would you like me to proceed?") — do NOT ask, just do it - Errors with a clear next step ("The test failed, should I fix it?") — do NOT ask, just fix it - Genuine choice questions where all options are valid ("Which of A, B, or C should we use?") — these deserve a response. Use `STOPPING BECAUSE: need user input` and ask your question @@ -774,6 +984,7 @@ STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified. **Recovering from a `tool_use_error` — do NOT stop silently**: Some tool errors require an explicit recovery action, not a halt. The most common shape: + - You call `Edit` or `Write` on a file you have not yet read. - Claude Code returns a `tool_use_error` (e.g. "File has not been read yet"). - The correct recovery is **Read the file, then retry Edit/Write** — **do not stop**. Stopping silently after a tool error triggers a Stop-hook re-entry loop and wastes a turn. @@ -788,9 +999,9 @@ Stop-time advisory that fires on language patterns signalling avoidance of work. **Avoid**: -- Dismissing issues as `pre-existing`, `out of scope`, `not our problem`, or `not relevant` to deflect work that is in fact yours. -- Premature-halt phrasing like `natural checkpoint`, `ready to continue on your cue`, `pausing here` mid-plan when there is more to do — finish the task rather than dressing up a halt. -- Speculative `should be fine` or `probably works` when verification is cheap (run the test, read the file). +- Dismissing issues as `pre-existing`, `out of scope`, `not our problem`, or `not relevant` to deflect work that is in fact yours. +- Premature-halt phrasing like `natural checkpoint`, `ready to continue on your cue`, `pausing here` mid-plan when there is more to do — finish the task rather than dressing up a halt. +- Speculative `should be fine` or `probably works` when verification is cheap (run the test, read the file). **Do**: acknowledge the issue, fix it, or — if it genuinely is out of scope — say so once with the specific reason and continue with the in-scope work. From e51fd0a279e548af9cd3120d20a9279c354fb666 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 09:45:20 +0000 Subject: [PATCH 03/20] Plan 011: create ts-qa-ci TypeScript QA/CI harness plan Mirrors lts/php-qa-ci for TypeScript projects: orchestrated QA pipeline, component-driven-development ESLint rule tier, Claude Code integration tooling, dogfooded on lts-commerce-site before external rollout. Co-Authored-By: Claude Sonnet 5 --- .claude/init.sh | 41 ++++-- CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md | 171 +++++++++++++++++++++++ CLAUDE/Plan/README.md | 2 + 3 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md diff --git a/.claude/init.sh b/.claude/init.sh index 94bb652..06a590f 100755 --- a/.claude/init.sh +++ b/.claude/init.sh @@ -284,29 +284,38 @@ _resolve_python_cmd() { # # _get_hostname_suffix() - Get hostname-based suffix for runtime files # -# Uses HOSTNAME environment variable directly to isolate daemon runtime -# files across different environments (containers, machines). +# Resolves a STABLE hostname (in series): $HOSTNAME, then the `hostname` +# command (the OS hostname), then a constant. This MUST agree with the Python +# side (daemon/paths.py:_resolve_hostname_from_env, which uses +# socket.gethostname()) so the bash forwarder and the Python daemon compute the +# SAME socket/PID suffix. +# +# NEVER use a time-based hash here: $HOSTNAME is unset on macOS (zsh) and many +# minimal containers, and a time hash changes on every call — so start/status/ +# stop would each look for a different socket (the macOS daemon-unmanageable +# bug, Plan 00122 BUG 1). # # Returns: -# "-{sanitized-hostname}" or "-{time-hash}" if no hostname +# "-{sanitized-hostname}" # # Example: # HOSTNAME="laptop" -> "-laptop" # HOSTNAME="506355bfbc76" -> "-506355bfbc76" # HOSTNAME="My-Server" -> "-my-server" -# No HOSTNAME -> "-a1b2c3d4" (MD5 of timestamp) +# No HOSTNAME -> "-{os-hostname}" (e.g. "-work.local"), or "-localhost" # _get_hostname_suffix() { local hostname="${HOSTNAME:-}" - # No hostname? Use MD5 of current time for uniqueness + # No $HOSTNAME (macOS/zsh, minimal containers)? Use the OS hostname — the + # same value Python's socket.gethostname() returns — so both sides agree. + if [[ -z "$hostname" ]] && command -v hostname > /dev/null; then + hostname="$(hostname)" + fi + + # Last resort: a stable constant (matches paths.py _HOSTNAME_FALLBACK). if [[ -z "$hostname" ]]; then - local timestamp - timestamp=$(date +%s.%N) - local hash - hash=$(echo -n "$timestamp" | md5sum | cut -c1-8) - echo "-${hash}" - return 0 + hostname="localhost" fi # Sanitize hostname for filesystem safety: lowercase, no spaces @@ -377,7 +386,6 @@ _exec_bit_selfheal() { # Pattern: {project}/.claude/hooks-daemon/untracked/daemon.{sock|pid} # Container: {project}/.claude/hooks-daemon/untracked/daemon-{hash}.{sock|pid} # Must match Python paths module: claude_code_hooks_daemon.daemon.paths -_abs_project_path=$(realpath "$PROJECT_PATH") # Determine untracked directory path # Must match ProjectContext.daemon_untracked_dir() logic @@ -540,8 +548,13 @@ start_daemon() { return 1 fi - # Remove stale socket file - rm -f "$SOCKET_PATH" + # NOTE (Plan 00127): do NOT `rm -f "$SOCKET_PATH"` here. On the + # host+container shared-untracked path the socket may be owned by a LIVE + # incumbent daemon, and unconditionally deleting it would steal the socket + # before the python layer's liveness gate ever runs. Stale-socket cleanup is + # now the single responsibility of the python server, which probes socket + # liveness before unlinking (reuse on live, unlink on stale). We already + # short-circuit via is_daemon_running() above for the healthy-incumbent case. # Start daemon using CLI (proper daemonization) # CRITICAL: Pass --project-root and export env vars so the CLI uses the diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md new file mode 100644 index 0000000..c1d3b7f --- /dev/null +++ b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md @@ -0,0 +1,171 @@ +# Plan 011: ts-qa-ci — TypeScript QA/CI Harness Package + +**Status**: Not Started +**Created**: 2026-07-10 +**Owner**: Claude Code +**Priority**: High +**Type**: New Package / Tooling +**Related**: Plan 008 (ESLint Custom Rules Adoption — precursor/superseded-by this plan's CDD rule tier) + +## Overview + +`lts/php-qa-ci` (github.com/LongTermSupport/php-qa-ci) is a mature, single-devDependency QA/CI harness for PHP projects: it pulls in every quality tool transitively, encapsulates delivery (PHARs via PHIVE, an isolated Rector sub-project, replace-section tricks) so it never pollutes a consumer's own dependency graph, ships opinionated-but-overridable default configs via a cascade (defaults → platform → project), runs everything through one orchestrator binary (`vendor/bin/qa`) in a fail-fast phased pipeline, and includes first-class Claude Code integration (`deploy-skills.bash` pushes hooks/skills/agents into the consumer project). + +This plan creates `ts-qa-ci`, the equivalent harness for TypeScript/React projects, published as `@longtermsupport/ts-qa-ci` from a new GitHub repo `LongTermSupport/ts-qa-ci`. It is developed by first cloning `php-qa-ci` for reference (`./untracked/repos/php-qa-ci`, done) and scaffolding the new package in `./untracked/repos/ts-qa-ci` (gitignored working copy) before it is pushed to its own GitHub repo. Once the package exists, **this repo (lts-commerce-site) is the first and mandatory dogfooding consumer** — every rough edge is found and fixed here, and the pipeline is wired into this repo's GitHub Actions CI, before ts-qa-ci is considered ready to roll out to any other project. + +A first-class deliverable of this plan is a **Component-Driven Development (CDD) ESLint rule tier**: a set of custom rules that ban ad hoc raw HTML outside designated component files and ban ad hoc/arbitrary CSS class strings, forcing all styling through component variant props that internally resolve to a constrained, reviewable set of classes. + +This repo is explicitly **not** assumed to be a paragon of best practice going in — dogfooding is expected to surface real violations in the existing codebase, and fixing them is part of the plan, not a sign the plan is broken. + +## Goals + +1. **Extract transferable concepts from php-qa-ci** via structured research (phased fail-fast pipeline, hybrid tool-delivery encapsulation, config cascade, platform detection, per-tool override hooks, pre/post pipeline hooks, the arkitect-vs-stan "where does a rule belong" tiering philosophy, the managed-source pattern, Claude Code deployment scripts) and document which concepts transfer directly, which need TS-specific adaptation, and which don't apply. +2. **Select and group the TS/JS tool landscape** into the equivalent of php-qa-ci's four phases (code-modification, lint/validation, static analysis, testing) with concrete tool choices and justification for each. +3. **Design and scaffold the `ts-qa-ci` package architecture**: a single npm devDependency exposing a `ts-qa` orchestrator binary, phased pipeline execution, config cascade (defaults → framework/bundler detection [Vite/Next.js/generic] → project overrides), and a dependency-encapsulation strategy (evaluate: peerDependencies + internal pinning, a vendored/isolated tool-install step analogous to PHIVE, or npm's own overrides mechanism) so consumers get one line in `package.json`, not forty. +4. **Build a custom ESLint rule suite**: always-on core rules plus opt-in tiers, explicitly including a **Component-Driven Development (CDD) tier**: + - Ban ad hoc/raw HTML elements outside designated component definition files. + - Ban ad hoc/arbitrary CSS class strings (raw Tailwind utility soup, inline `className="..."` literals) in favour of a component variant-prop API that internally maps variants to a fixed, auditable set of classes. + - Enforce the variant API pattern itself (naming, typing, exhaustiveness) so variants are the only sanctioned styling surface. +5. **Write progressive-enhancement, high-quality documentation** mirroring php-qa-ci's `docs/` structure: pipeline architecture, configuration/override guide, per-tool docs, coding-standards guide, CI integration guide, and the CDD rules guide — each written so a reader gets value from the first paragraph and can go arbitrarily deep. +6. **Build Claude Code integration tooling**: a `deploy-skills`-equivalent script that pushes ts-qa-ci's skills/hooks/agents/config into a consumer project, mirroring php-qa-ci's approach. +7. **Dogfood on lts-commerce-site**: integrate ts-qa-ci into this repo, run the full pipeline, fix every violation it surfaces (both pipeline-config issues and genuine code-quality issues in this codebase), wire it into `.github/workflows/`, and iterate until CI is fully green and the maintainer considers it production-ready. +8. **Prepare for external rollout** only after dogfooding is clean — final polish pass, versioning/publish plan — as the closing phase of this plan. + +## Non-Goals + +- **Not building QA tools from scratch.** Same philosophy as php-qa-ci: orchestrate best-in-class existing tools (ESLint, Prettier or Biome, `tsc`, Vitest/Playwright, etc.), don't reinvent them. +- **Not full multi-framework support in v1.** Target generic TS + React/Vite (this repo's stack) as the fully-implemented platform. Architecture should leave room for Next.js/other-framework detection later, but that detection does not need to be built now. +- **Not publishing to the public npm registry as part of this plan.** Publishing is a deliberate, separate go/no-go decision after dogfooding proves the package out — this plan ends at "ready to publish," not "published." +- **Not a big-bang rewrite of this repo's existing ESLint config as a standalone exercise.** CDD rule adoption happens through dogfooding ts-qa-ci against this repo, which surfaces and fixes violations organically (same pattern as Plan 008). +- **Not deciding final CI secrets/publish credentials setup** (npm token provisioning, GitHub org settings) — flagged as a follow-up once rollout is greenlit. + +## Context & Background + +- Reference implementation: `LongTermSupport/php-qa-ci` (github.com/LongTermSupport/php-qa-ci), cloned locally to `./untracked/repos/php-qa-ci` for research. Composer package `lts/php-qa-ci`, orchestrator binary `bin/qa`, four-phase pipeline (code-mod → lint/validate → static analysis → test), hybrid PHAR/composer/isolated-subproject tool delivery, `qaConfig/` cascade, per-tool `qaConfig/tools/{tool}.inc.bash` overrides, `hookPre.bash`/`hookPost.bash`, PHPArkitect for structural rules vs PHPStan for semantic rules (explicit "never enforce the same convention in both engines" SSoT principle), always-on + opt-in PHPStan rule tiers, a "managed source" codegen mechanism, and `scripts/deploy-skills.bash` for Claude Code integration. +- This repo (`lts-commerce-site`) stack: React 18 + TypeScript (strict) + Vite 6 + Tailwind CSS v4 + React Router v7, SSG via custom prerender script. Existing tooling: ESLint flat config with 11 custom rules already (see Plan 008), Prettier, `tsc` type-checking as a build gate. See root `CLAUDE.md` for full architecture. +- **npm/GitHub scope decision** (resolved 2026-07-10, see Technical Decisions): package will be `@longtermsupport/ts-qa-ci` in a new repo `LongTermSupport/ts-qa-ci`. `@edmondscommerce` is confirmed owned by the maintainer and has genuine prior art (`@edmondscommerce/feqa`, a dormant 2019 "Frontend QA Pipeline" package) but was not chosen — see decision rationale. +- This plan explicitly treats lts-commerce-site as an imperfect dogfood target, not a reference implementation to be preserved as-is. + +## Tasks + +### Phase 1: Research — php-qa-ci Concept Extraction + +- [ ] ⬜ **Task 1.1**: Dispatch Sonnet research agents (dynamic workflow) over `./untracked/repos/php-qa-ci` to produce a structured concept-extraction report + - [ ] ⬜ Pipeline architecture & phasing philosophy (why 4 phases, why code-mod runs first, fail-fast design) + - [ ] ⬜ Tool delivery/encapsulation strategy (PHARs via PHIVE, isolated Rector sub-project, `replace` trick, `bin/` shims) and what has a realistic npm/Node equivalent + - [ ] ⬜ Config cascade & override system (`qaConfig/`, per-tool `.inc.bash` overrides, platform detection) + - [ ] ⬜ Hook system (`hookPre.bash`/`hookPost.bash`, per-tool override files) and its Claude Code hooks (`deploy-skills.bash`, hook list, migration-on-update behaviour) + - [ ] ⬜ Rule-tiering philosophy (PHPArkitect vs PHPStan "where does a rule belong," always-on vs opt-in rule tiers, SSoT-never-duplicate principle) — this directly informs the CDD ESLint tier design + - [ ] ⬜ Documentation structure and style (`docs/`, tool-specific docs, README structure) as the template for ts-qa-ci's docs + - [ ] ⬜ CI/GitHub Actions templates (`templates/github-actions/*.yml`, the autofix-then-gate pattern, branch protection setup script) +- [ ] ⬜ **Task 1.2**: Opus review pass on the concept-extraction report — confirm nothing load-bearing was missed before design work starts + +### Phase 2: Design — TS Tool Landscape & Package Architecture + +- [ ] ⬜ **Task 2.1**: Research and select the TS/JS tool for each pipeline phase, with rationale: + - [ ] ⬜ Phase 1 equivalent (code-modification): formatter (Prettier vs Biome) + codemod/upgrade tool (ts-migrate / Biome's own rules / manual) + - [ ] ⬜ Phase 2 equivalent (lint/validation): ESLint (flat config), `tsc --noEmit`, import/dependency validation (depcheck / knip), markdown link checking + - [ ] ⬜ Phase 3 equivalent (static analysis): `tsc` strict mode as the PHPStan analogue; evaluate whether a structural/architecture tool (dependency-cruiser, or a custom ESLint import-boundary tier) is the arkitect analogue + - [ ] ⬜ Phase 4 equivalent (testing): Vitest (unit) + Playwright (e2e/smoke), mutation testing analogue (Stryker Mutator, optional tier like Infection) + - [ ] ⬜ Decide the npm-package encapsulation strategy (peerDependencies vs internal pinning vs vendored binaries) and document the tradeoffs explicitly — this has no exact npm equivalent to PHIVE/PHAR and needs its own design +- [ ] ⬜ **Task 2.2**: Design the `ts-qa` orchestrator CLI: phases, `-t ` single-tool mode, `-p ` path scoping, config cascade resolution order, platform detection (generic vs Vite vs Next.js), exit-code/retry semantics +- [ ] ⬜ **Task 2.3**: Opus review of the tool-selection + architecture design before scaffolding starts + +### Phase 3: Build — Package Scaffold + +- [ ] ⬜ **Task 3.1**: Scaffold `ts-qa-ci` in `./untracked/repos/ts-qa-ci` — package.json (`@longtermsupport/ts-qa-ci`), orchestrator CLI, config defaults, directory layout mirroring php-qa-ci's clarity (`configDefaults/`, `bin/`, `docs/`, `.claude/`) +- [ ] ⬜ **Task 3.2**: Implement Phase 1–4 tool runners with the config cascade and per-tool override mechanism +- [ ] ⬜ **Task 3.3**: Implement the CDD ESLint rule tier + - [ ] ⬜ `no-ad-hoc-html` (or similarly named): bans raw HTML tags outside designated component files + - [ ] ⬜ `no-ad-hoc-classnames`: bans arbitrary/inline CSS class strings outside a component's internal variant-to-class mapping + - [ ] ⬜ Variant-API enforcement rule(s): every styleable component must expose typed variant props; internal class resolution is the only place raw classes may appear + - [ ] ⬜ Rule documentation (purpose, examples, escape hatches) for each, matching the granularity of Plan 008's rule docs +- [ ] ⬜ **Task 3.4**: Implement the Claude Code integration deploy script (skills/hooks/agents push into a consumer project) +- [ ] ⬜ **Task 3.5**: Write the docs set (`docs/pipeline.md`, `docs/configuration.md`, `docs/tools/*.md`, `docs/coding-standards.md`, `docs/cdd-rules.md`, `docs/github-actions.md`) — progressive enhancement style: short/skimmable at the top, full depth available below +- [ ] ⬜ **Task 3.6**: Push scaffolded package to new GitHub repo `LongTermSupport/ts-qa-ci` + +### Phase 4: Dogfood — Integrate into lts-commerce-site + +- [ ] ⬜ **Task 4.1**: `npm link` (or workspace/file: dependency) ts-qa-ci into this repo as a devDependency +- [ ] ⬜ **Task 4.2**: Run the full `ts-qa` pipeline against this repo; triage every failure into "pipeline/config bug in ts-qa-ci" vs "genuine code-quality issue in lts-commerce-site" +- [ ] ⬜ **Task 4.3**: Fix ts-qa-ci pipeline/config bugs surfaced by the real run (iterate with Phase 3 as needed) +- [ ] ⬜ **Task 4.4**: Fix genuine violations surfaced in this repo's code, including CDD violations (ad hoc HTML/CSS classes) — this is expected to be non-trivial given the current codebase was not built against these rules +- [ ] ⬜ **Task 4.5**: Wire `ts-qa` into `.github/workflows/ci.yml`, replacing/consolidating the existing separate format/lint/typecheck/build steps where appropriate +- [ ] ⬜ **Task 4.6**: Confirm CI green end-to-end on a real PR, not just local runs +- [ ] ⬜ **Task 4.7**: Deploy the Claude Code integration tooling into this repo and confirm it works (skills/hooks show up correctly, no conflicts with existing hooks-daemon setup) + +### Phase 5: Rollout Readiness + +- [ ] ⬜ **Task 5.1**: Retrospective on dogfooding — what broke, what was surprising, what's still rough +- [ ] ⬜ **Task 5.2**: Final documentation pass incorporating dogfooding lessons +- [ ] ⬜ **Task 5.3**: Decide and document the publish plan (npm publish access, versioning strategy, semver policy) — decision only, execution is a follow-on +- [ ] ⬜ **Task 5.4**: Update this plan's status to Complete and record the outcome + +## Dependencies + +- **Depends on**: Plan 008 (ESLint Custom Rules Adoption) — provides the existing 11-rule baseline the CDD tier extends, and prior art for how this repo evaluates/adapts external rule sets. +- **Blocks**: Nothing directly; future articles/portfolio content about ts-qa-ci depend on this plan reaching Phase 5. +- **Related**: `LongTermSupport/php-qa-ci` (reference implementation, external repo). + +## Technical Decisions + +### Decision 1: Package location — new standalone GitHub repo + +**Context**: php-qa-ci is a standalone repo/package that projects depend on, not a subdirectory of any one consumer. + +**Options Considered**: + +1. New standalone GitHub repo (`LongTermSupport/ts-qa-ci`) — matches the proven php-qa-ci model exactly. +2. Develop in `untracked/repos/ts-qa-ci` only, promote later. +3. Subdirectory package (`packages/ts-qa-ci/`) inside lts-commerce-site. + +**Decision**: Option 1, with the practical bootstrapping detail from option 2 folded in: scaffold in `./untracked/repos/ts-qa-ci` first (fast iteration, no repo-admin overhead while the shape is still changing), then push to the new `LongTermSupport/ts-qa-ci` GitHub repo once the initial scaffold is solid. Rejected option 3 because it conflates a reusable cross-project tool with a single portfolio-site codebase, and would make dogfooding (a package installed as a dependency) impossible to test honestly. + +**Date**: 2026-07-10 + +### Decision 2: npm scope and package name + +**Context**: Needed a real, ownership-confirmed npm scope. Checked three candidates against the npm registry API on 2026-07-10. + +**Findings**: + +- `@lts` — the scope exists on npm but its member list is hidden via the public API (`{}` returned); ownership by the maintainer could not be confirmed without authenticating. Given how generic "lts" is as a scope name, risky to assume. +- `@longtermsupport` — confirmed **unclaimed** (registry org-lookup returned 404). Exact match to the existing `LongTermSupport` GitHub org, which already hosts `php-qa-ci`. +- `@edmondscommerce` — confirmed **owned** by the maintainer (`registry.npmjs.org/-/org/edmondscommerce/user` → `{"edmondscommerce":"owner"}`). Has genuine prior art: `@edmondscommerce/feqa`, a "Frontend QA Pipeline" package published 2019, now dormant. + +**Decision**: `@longtermsupport/ts-qa-ci`, in a new `LongTermSupport/ts-qa-ci` GitHub repo. Chosen for exact consistency with the existing, already-established `php-qa-ci` branding and org, and because ownership is unambiguous (unclaimed scope registered fresh) versus the uncertain `@lts` status. `@edmondscommerce/feqa`'s existence is worth a look during Phase 1 research for any reusable ideas, but the new package supersedes it under the LTS/php-qa-ci lineage rather than reviving the edmondscommerce scope. + +**Date**: 2026-07-10 + +## Success Criteria + +- [ ] Concept-extraction report from Phase 1 exists and was reviewed by an Opus pass +- [ ] `ts-qa-ci` package scaffolded, pushed to `LongTermSupport/ts-qa-ci`, installable as a devDependency +- [ ] All four pipeline phases implemented with working default configs +- [ ] CDD ESLint tier implemented, documented, and enforced with zero violations in this repo +- [ ] Full documentation set written in progressive-enhancement style +- [ ] Claude Code integration deploy tooling implemented and verified working in this repo +- [ ] `ts-qa` pipeline wired into this repo's GitHub Actions CI and green on a real PR +- [ ] Rollout-readiness retrospective and publish plan documented + +## Risks & Mitigations + +| Risk | Impact | Probability | Mitigation | +| --------------------------------------------------------------------------------------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| No clean npm equivalent to PHIVE/PHAR encapsulation exists | Medium | High | Treat as an open design question in Phase 2, not an assumed solved problem; evaluate peerDependencies + strict version pinning as the pragmatic default | +| CDD rules surface a large volume of violations in this repo, stalling dogfooding | Medium | Medium | Expected and budgeted for in Phase 4; fix incrementally, consider `warn` level temporarily if volume is large (same pattern as Plan 008) | +| Scope creep into full multi-framework support before dogfooding proves the core | High | Medium | Non-Goals explicitly exclude this; architecture leaves room but generic + Vite/React is the only fully-implemented target | +| `@longtermsupport` npm scope registration turns out to have friction (e.g. requires paid org) | Low | Low | Confirmed unclaimed via registry API; verify actual registration mechanics early in Phase 3 rather than assuming | + +## Notes & Updates + +### 2026-07-10 - Plan Creation + +- Cloned `LongTermSupport/php-qa-ci` to `./untracked/repos/php-qa-ci` for reference research +- Resolved package location (new standalone repo, bootstrapped via `untracked/repos/` first) and npm scope (`@longtermsupport/ts-qa-ci`) via direct npm registry API checks — see Technical Decisions +- Next: dispatch Opus review of this plan's requirements/scope, then kick off the Phase 1 research workflow + +--- + +**Maintained by**: Joseph (LTS Commerce) +**Last Updated**: 2026-07-10 diff --git a/CLAUDE/Plan/README.md b/CLAUDE/Plan/README.md index 6df5932..7d23d26 100644 --- a/CLAUDE/Plan/README.md +++ b/CLAUDE/Plan/README.md @@ -3,6 +3,7 @@ This directory contains all project plans following the Planning Workflow (see `CLAUDE/PlanWorkflow.md`). ## Active Plans + - [001: React Migration & Claude Code Infrastructure Adoption](001-react-migration-adoption/PLAN.md) - 🔄 In Progress - **High Priority** - Migrate LTS Commerce site from EJS/Vite to React/TypeScript, apply skeleton from Plan 002 - [002: React Site Skeleton - Reusable Foundation](002-react-site-skeleton/PLAN.md) - 🔄 In Progress - **High Priority** - Create generic React/TypeScript skeleton repo with type-safe patterns, ESLint rules, minimal components, and Claude Code infrastructure - [003: Contact Form with Google Apps Script Backend](003-contact-form-google-apps-script/PLAN.md) - 📋 Planned - **High Priority** - Replace mailto: contact form with React Hook Form + Zod + Google Apps Script backend (honeypot, rate limiting, email) @@ -11,6 +12,7 @@ This directory contains all project plans following the Planning Workflow (see ` - [006: Testing Infrastructure](006-testing-infrastructure/PLAN.md) - 📋 Planned - **Medium Priority** - Set up Vitest + Playwright (smoke tests for all pages); required before Plan 001 Phase 8 - [007: Component Library Lift from EC Site](007-component-library-lift/PLAN.md) - 📋 Planned - **Medium Priority** - Adopt 6 UI components (BlurText, Typewriter, StatusBadge, ThreeColumnFeatures, MobileCarouselGrid, HighlightTypewriter) - [008: ESLint Custom Rules Adoption](008-eslint-custom-rules/PLAN.md) - 📋 Planned - **Medium-High Priority** - Cherry-pick 8 ESLint rules from EC site's 80+ (SEO enforcement, navigation, placeholder prevention) +- [011: ts-qa-ci — TypeScript QA/CI Harness Package](011-ts-qa-ci-package/PLAN.md) - 📋 Planned - **High Priority** - New `@longtermsupport/ts-qa-ci` package (TS analogue of `lts/php-qa-ci`): orchestrated QA pipeline, CDD ESLint rule tier, Claude Code integration, dogfooded on this repo first ## Completed Plans From 2956863374c8d32d1a9dfb53e46fcdd4d4862bf8 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 10:08:46 +0000 Subject: [PATCH 04/20] Plan 011: fold in pre-flight Opus review findings Adds untracked/ec-site as a parallel TS-native research target alongside php-qa-ci (it already implements most of this plan's CDD/orchestrator ambitions), corrects the CI baseline premise (build+deploy only today, no lint/format/test gate), scopes the CDD "no ad hoc HTML" rule to JSX only with articles.ts explicitly exempt, and brings variant-prop catalogue construction into Phase 4 scope rather than quietly descoping it. Co-Authored-By: Claude Sonnet 5 --- CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md | 198 ++++++++++++++++------- 1 file changed, 138 insertions(+), 60 deletions(-) diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md index c1d3b7f..33e7254 100644 --- a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md +++ b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md @@ -11,88 +11,130 @@ `lts/php-qa-ci` (github.com/LongTermSupport/php-qa-ci) is a mature, single-devDependency QA/CI harness for PHP projects: it pulls in every quality tool transitively, encapsulates delivery (PHARs via PHIVE, an isolated Rector sub-project, replace-section tricks) so it never pollutes a consumer's own dependency graph, ships opinionated-but-overridable default configs via a cascade (defaults → platform → project), runs everything through one orchestrator binary (`vendor/bin/qa`) in a fail-fast phased pipeline, and includes first-class Claude Code integration (`deploy-skills.bash` pushes hooks/skills/agents into the consumer project). -This plan creates `ts-qa-ci`, the equivalent harness for TypeScript/React projects, published as `@longtermsupport/ts-qa-ci` from a new GitHub repo `LongTermSupport/ts-qa-ci`. It is developed by first cloning `php-qa-ci` for reference (`./untracked/repos/php-qa-ci`, done) and scaffolding the new package in `./untracked/repos/ts-qa-ci` (gitignored working copy) before it is pushed to its own GitHub repo. Once the package exists, **this repo (lts-commerce-site) is the first and mandatory dogfooding consumer** — every rough edge is found and fixed here, and the pipeline is wired into this repo's GitHub Actions CI, before ts-qa-ci is considered ready to roll out to any other project. +This plan creates `ts-qa-ci`, the equivalent harness for TypeScript/React projects, published as `@longtermsupport/ts-qa-ci` from a new GitHub repo `LongTermSupport/ts-qa-ci`. It is developed by cloning **two** references for research, not one: `php-qa-ci` (`./untracked/repos/php-qa-ci`, done) for orchestration/delivery philosophy, and `./untracked/ec-site/` — a React+TS+Vite+Tailwind LongTermSupport site that **already implements most of this plan's ambitions in idiomatic TypeScript** (57 custom ESLint rules with per-rule docs, working Component-Driven-Development rules including a solved articles/raw-HTML collision, a `bin/qa`-equivalent `npm run llm:qa` orchestrator, meta-rules) — for the TS-native realisations. The new package is scaffolded in `./untracked/repos/ts-qa-ci` (gitignored working copy) before being pushed to its own GitHub repo. Once the package exists, **this repo (lts-commerce-site) is the first and mandatory dogfooding consumer** — every rough edge is found and fixed here, and the pipeline is wired into this repo's GitHub Actions CI, before ts-qa-ci is considered ready to roll out to any other project. A first-class deliverable of this plan is a **Component-Driven Development (CDD) ESLint rule tier**: a set of custom rules that ban ad hoc raw HTML outside designated component files and ban ad hoc/arbitrary CSS class strings, forcing all styling through component variant props that internally resolve to a constrained, reviewable set of classes. -This repo is explicitly **not** assumed to be a paragon of best practice going in — dogfooding is expected to surface real violations in the existing codebase, and fixing them is part of the plan, not a sign the plan is broken. +This repo is explicitly **not** assumed to be a paragon of best practice going in — dogfooding is expected to surface real violations in the existing codebase, and fixing them is part of the plan, not a sign the plan is broken. A pre-flight Opus review (2026-07-10) confirmed this and quantified the real baseline (see Context & Background): CI today enforces nothing but the build, there are zero test files despite Vitest being configured, and the CDD ambition collides with a 19.7k-line article-content file that no AST rule can see inside. ## Goals -1. **Extract transferable concepts from php-qa-ci** via structured research (phased fail-fast pipeline, hybrid tool-delivery encapsulation, config cascade, platform detection, per-tool override hooks, pre/post pipeline hooks, the arkitect-vs-stan "where does a rule belong" tiering philosophy, the managed-source pattern, Claude Code deployment scripts) and document which concepts transfer directly, which need TS-specific adaptation, and which don't apply. -2. **Select and group the TS/JS tool landscape** into the equivalent of php-qa-ci's four phases (code-modification, lint/validation, static analysis, testing) with concrete tool choices and justification for each. -3. **Design and scaffold the `ts-qa-ci` package architecture**: a single npm devDependency exposing a `ts-qa` orchestrator binary, phased pipeline execution, config cascade (defaults → framework/bundler detection [Vite/Next.js/generic] → project overrides), and a dependency-encapsulation strategy (evaluate: peerDependencies + internal pinning, a vendored/isolated tool-install step analogous to PHIVE, or npm's own overrides mechanism) so consumers get one line in `package.json`, not forty. +1. **Extract transferable concepts from BOTH references**, researched in parallel: + + - `php-qa-ci` for orchestration & delivery philosophy: phased fail-fast pipeline, the read-only/CI-write duality (mutating tools dry-run-and-fail in CI, auto-fix locally), hybrid tool-delivery encapsulation, config cascade, the arkitect-vs-stan "where does a rule belong" tiering philosophy, the "estate-wide checks must be pipeline-owned, not opt-in rules" lesson, the managed-source pattern, Claude Code deployment scripts. + - `ec-site` for TS-native realisations: catalogue its 57 custom ESLint rules (lift/adapt/drop), its working CDD rules (`no-html-in-pages`, `no-raw-block-html-in-articles` — a solved version of this plan's own articles-HTML collision), its `npm run llm:qa` orchestrator shape, its meta-rules, and the synchronous-ESLint-handler gotcha documented in its `eslint-rules/CLAUDE.md`. + + Document every extracted concept as transfers-directly / needs-TS-adaptation / doesn't-apply, and note whether ec-site already implements it. + +2. **Select and group the TS/JS tool landscape** into the equivalent of php-qa-ci's four phases (code-modification, lint/validation, static analysis, testing), pre-committing to the parts this repo and ec-site already force (ESLint flat config + Prettier + `tsc` + Vitest + Playwright — Biome is off the table because the CDD tier requires custom JS-authored ESLint rules) and reserving genuine research time for the parts that are actually open (dependency-cruiser vs. custom import-boundary rule as the arkitect analogue; Stryker as an optional mutation tier; knip vs. depcheck). + +3. **Design and scaffold the `ts-qa-ci` package architecture**: a single npm devDependency exposing a `ts-qa` orchestrator binary, phased pipeline execution with the read-only/CI-write duality as a first-class concept, config cascade (defaults → framework/bundler detection [Vite/Next.js/generic] → project overrides), and a dependency-encapsulation strategy. Note going in: npm's nested dependency model means the PHIVE/PHAR problem (Composer's flat graph forcing autoloader pollution) mostly doesn't exist in npm — the real open questions are narrower (peerDependencies vs. bundled deps for `typescript`/`eslint`/`vite`, and ESLint plugin delivery via exported flat-config rule objects, which ec-site already demonstrates). + 4. **Build a custom ESLint rule suite**: always-on core rules plus opt-in tiers, explicitly including a **Component-Driven Development (CDD) tier**: - - Ban ad hoc/raw HTML elements outside designated component definition files. + + - Ban ad hoc/raw HTML elements in `.tsx` JSX (component/page files) outside designated component definition files. Scoped to JSX only — `src/data/articles.ts`'s template-literal HTML content is a sanctioned, separately-governed string-HTML surface (an AST/JSX rule cannot see inside a string literal anyway; see Context & Background and ec-site's `no-raw-block-html-in-articles` for the precedent of treating article prose HTML as legitimately different from page/component HTML). - Ban ad hoc/arbitrary CSS class strings (raw Tailwind utility soup, inline `className="..."` literals) in favour of a component variant-prop API that internally maps variants to a fixed, auditable set of classes. - - Enforce the variant API pattern itself (naming, typing, exhaustiveness) so variants are the only sanctioned styling surface. + - Enforce the variant API pattern itself (naming, typing, exhaustiveness) so variants are the only sanctioned styling surface. This repo currently has **no** variant-prop infrastructure (0 `class-variance-authority` usage, 17 component files) to redirect violators to, unlike ec-site — building that catalogue is explicitly brought into this plan's scope (Phase 4) rather than quietly dropped, because it's necessary infrastructure for the CDD vision, not optional polish. + - Decide (Phase 2) whether ts-qa **owns and runs** its own ESLint config over a consumer, rather than merely publishing includable rule objects — the direct TS analogue of php-qa-ci's "estate-wide checks can't be opt-in rules" lesson (its SensitiveParameter check is an always-on pipeline tool, not an opt-in PHPStan rule, for exactly this reason). + 5. **Write progressive-enhancement, high-quality documentation** mirroring php-qa-ci's `docs/` structure: pipeline architecture, configuration/override guide, per-tool docs, coding-standards guide, CI integration guide, and the CDD rules guide — each written so a reader gets value from the first paragraph and can go arbitrarily deep. -6. **Build Claude Code integration tooling**: a `deploy-skills`-equivalent script that pushes ts-qa-ci's skills/hooks/agents/config into a consumer project, mirroring php-qa-ci's approach. -7. **Dogfood on lts-commerce-site**: integrate ts-qa-ci into this repo, run the full pipeline, fix every violation it surfaces (both pipeline-config issues and genuine code-quality issues in this codebase), wire it into `.github/workflows/`, and iterate until CI is fully green and the maintainer considers it production-ready. + +6. **Build Claude Code integration tooling**: a `deploy-skills`-equivalent script that pushes ts-qa-ci's skills/hooks/agents/config into a consumer project, mirroring php-qa-ci's approach, authored in Node (not bash) for cross-platform portability, with an explicit manual-deploy-vs-postinstall-auto-deploy decision (Phase 2) and compatibility check against this repo's existing hooks-daemon `.claude/settings.json` policy (Phase 4). + +7. **Dogfood on lts-commerce-site**: integrate ts-qa-ci into this repo, run the full pipeline, fix every violation it surfaces (both pipeline-config issues and genuine code-quality issues in this codebase), wire it into `.github/workflows/` **for the first time** (this repo's CI currently runs build+deploy only — nothing to "consolidate"), and iterate until CI is fully green and the maintainer considers it production-ready. + 8. **Prepare for external rollout** only after dogfooding is clean — final polish pass, versioning/publish plan — as the closing phase of this plan. ## Non-Goals -- **Not building QA tools from scratch.** Same philosophy as php-qa-ci: orchestrate best-in-class existing tools (ESLint, Prettier or Biome, `tsc`, Vitest/Playwright, etc.), don't reinvent them. +- **Not building QA tools from scratch.** Same philosophy as php-qa-ci: orchestrate best-in-class existing tools (ESLint, Prettier, `tsc`, Vitest/Playwright, etc.), don't reinvent them. +- **Not adopting Biome to replace ESLint.** The CDD tier requires custom JS-authored ESLint rules; Biome's plugin story (GritQL) cannot run them. Biome as an additional/alternative *formatter* alongside Prettier is a minor, deferrable optimisation, not something this plan needs to settle. - **Not full multi-framework support in v1.** Target generic TS + React/Vite (this repo's stack) as the fully-implemented platform. Architecture should leave room for Next.js/other-framework detection later, but that detection does not need to be built now. - **Not publishing to the public npm registry as part of this plan.** Publishing is a deliberate, separate go/no-go decision after dogfooding proves the package out — this plan ends at "ready to publish," not "published." +- **Not migrating `src/data/articles.ts` HTML content to JSX/MDX/components.** The CDD "no ad hoc HTML" rule targets `.tsx` JSX only; article prose HTML stays a sanctioned string-content surface. Policing that content is a separate, later, string-linter-or-MDX-migration plan, named here only to be explicitly out of scope. - **Not a big-bang rewrite of this repo's existing ESLint config as a standalone exercise.** CDD rule adoption happens through dogfooding ts-qa-ci against this repo, which surfaces and fixes violations organically (same pattern as Plan 008). +- **Not Windows-first.** Targets Linux CI (matches php-qa-ci and this repo's GitHub Actions runners); the Claude Code deploy tooling is authored in Node for portability, but Windows dev-machine support is not a requirement of this plan. - **Not deciding final CI secrets/publish credentials setup** (npm token provisioning, GitHub org settings) — flagged as a follow-up once rollout is greenlit. ## Context & Background -- Reference implementation: `LongTermSupport/php-qa-ci` (github.com/LongTermSupport/php-qa-ci), cloned locally to `./untracked/repos/php-qa-ci` for research. Composer package `lts/php-qa-ci`, orchestrator binary `bin/qa`, four-phase pipeline (code-mod → lint/validate → static analysis → test), hybrid PHAR/composer/isolated-subproject tool delivery, `qaConfig/` cascade, per-tool `qaConfig/tools/{tool}.inc.bash` overrides, `hookPre.bash`/`hookPost.bash`, PHPArkitect for structural rules vs PHPStan for semantic rules (explicit "never enforce the same convention in both engines" SSoT principle), always-on + opt-in PHPStan rule tiers, a "managed source" codegen mechanism, and `scripts/deploy-skills.bash` for Claude Code integration. -- This repo (`lts-commerce-site`) stack: React 18 + TypeScript (strict) + Vite 6 + Tailwind CSS v4 + React Router v7, SSG via custom prerender script. Existing tooling: ESLint flat config with 11 custom rules already (see Plan 008), Prettier, `tsc` type-checking as a build gate. See root `CLAUDE.md` for full architecture. +- Reference 1: `LongTermSupport/php-qa-ci` (github.com/LongTermSupport/php-qa-ci), cloned locally to `./untracked/repos/php-qa-ci`. Composer package `lts/php-qa-ci`, orchestrator binary `bin/qa`, four-phase pipeline (code-mod → lint/validate → static analysis → test), the `qaReadOnly`/`detectReadOnly` read-only-in-CI/auto-fix-locally duality governing every mutating tool, hybrid PHAR/composer/isolated-subproject tool delivery, `qaConfig/` cascade, per-tool `qaConfig/tools/{tool}.inc.bash` overrides, `hookPre.bash`/`hookPost.bash`, PHPArkitect for structural rules vs PHPStan for semantic rules (explicit "never enforce the same convention in both engines" SSoT principle), always-on + opt-in PHPStan rule tiers (SensitiveParameter coverage is deliberately an always-on pipeline tool, *not* an opt-in rule, because opt-in rules "can't be relied on estate-wide"), a "managed source" codegen mechanism, and `scripts/deploy-skills.bash` for Claude Code integration. +- Reference 2: `./untracked/ec-site/` (LongTermSupport org, React+TS+Vite+Tailwind — same stack as this repo). **Confirmed present**: 57 files in `eslint-rules/*.js` each with a paired `.md` doc; CDD rules already built, including `no-html-in-pages.js` (bans raw HTML in page files, ships a component-replacement fix prompt) and `no-raw-block-html-in-articles.js` (the solved version of this plan's own articles-HTML collision — distinguishes allowed prose HTML from block HTML that must become components); `eslint-plugin-tailwindcss` for class ordering/validation; a TS-native orchestrator (`scripts/llm-*.ts`: `llm-lint.ts`, `llm-fix.ts`, `llm-type-check.ts`, `llm-format-check.ts`, wired as `npm run llm:qa`, caching under `var/qa/`); meta-rules that lint the rules themselves. Its `eslint-rules/CLAUDE.md` documents that ESLint rule handlers must be synchronous (use `execSync`, not async) — a gotcha worth avoiding rediscovery of. ec-site is a private repo; lifting its rules into a package intended to be publishable needs the maintainer's explicit sign-off (see Technical Decisions). +- This repo (`lts-commerce-site`) stack: React 18 + TypeScript (strict) + Vite 6 + Tailwind CSS v4 + React Router v7, SSG via custom prerender script. Existing tooling: ESLint flat config with 11 custom rules already (see Plan 008), Prettier, `tsc` type-checking, Vitest configured. It already has its own `llm:lint` / `llm:type-check` / `llm:test` / `llm:qa` npm scripts mirroring ec-site's convention. See root `CLAUDE.md` for full architecture (note: CLAUDE.md's CI description is partly aspirational — see baseline facts below). +- **Real baseline measured 2026-07-10** (run directly, not from CLAUDE.md's description): + - `.github/workflows/ci.yml` runs **only** `npm run build` then deploy. There is no ESLint, Prettier, or test step in CI today. Phase 4 therefore **adds** quality gates to CI for the first time — it does not "consolidate" existing ones. + - `npm run lint` (ESLint): **5 pre-existing errors**, all SEO-metadata length violations in `Home.tsx` and `NotFound.tsx`, unrelated to this plan. + - `npm run format:check` (Prettier): **53 files under `src/`** have formatting drift (plus a large amount of noise outside `src/` — `.claude/`, vendored `untracked/` subprojects — that ts-qa-ci's own finder should exclude, mirroring php-qa-ci's `php_cs_finder.php` exclusions; not a real signal). + - `npm run test:run` (Vitest): **zero test files exist** ("No test files found", exit 1) despite Vitest being fully configured. Testing is Phase 4 (Testing) of php-qa-ci's pipeline and has no baseline to protect — every test ts-qa-ci's pipeline requires will be new. + - `src/data/articles.ts` is **19,714 lines** containing roughly **10,000+ raw HTML tags** — but as content inside JavaScript template-literal strings in a `.ts` data file, not JSX. An AST/JSX-based ESLint rule cannot see inside a string literal; this is why the CDD rule is explicitly scoped to `.tsx` JSX only (see Goals/Non-Goals). + - This repo has **zero** `class-variance-authority` usage and only 17 component files — there is no existing variant-prop catalogue for a `className` ban to redirect violators to. Building one is in scope for Phase 4, not assumed to pre-exist. - **npm/GitHub scope decision** (resolved 2026-07-10, see Technical Decisions): package will be `@longtermsupport/ts-qa-ci` in a new repo `LongTermSupport/ts-qa-ci`. `@edmondscommerce` is confirmed owned by the maintainer and has genuine prior art (`@edmondscommerce/feqa`, a dormant 2019 "Frontend QA Pipeline" package) but was not chosen — see decision rationale. +- **Pre-flight review**: an Opus agent reviewed this plan's first draft against both references and this repo's real state before Phase 1 research was dispatched; findings are folded into this version. Full critique retained at `untracked/plan-011-review.md` (gitignored, not part of the plan itself) for anyone who wants the raw reasoning. - This plan explicitly treats lts-commerce-site as an imperfect dogfood target, not a reference implementation to be preserved as-is. ## Tasks -### Phase 1: Research — php-qa-ci Concept Extraction - -- [ ] ⬜ **Task 1.1**: Dispatch Sonnet research agents (dynamic workflow) over `./untracked/repos/php-qa-ci` to produce a structured concept-extraction report - - [ ] ⬜ Pipeline architecture & phasing philosophy (why 4 phases, why code-mod runs first, fail-fast design) - - [ ] ⬜ Tool delivery/encapsulation strategy (PHARs via PHIVE, isolated Rector sub-project, `replace` trick, `bin/` shims) and what has a realistic npm/Node equivalent - - [ ] ⬜ Config cascade & override system (`qaConfig/`, per-tool `.inc.bash` overrides, platform detection) - - [ ] ⬜ Hook system (`hookPre.bash`/`hookPost.bash`, per-tool override files) and its Claude Code hooks (`deploy-skills.bash`, hook list, migration-on-update behaviour) - - [ ] ⬜ Rule-tiering philosophy (PHPArkitect vs PHPStan "where does a rule belong," always-on vs opt-in rule tiers, SSoT-never-duplicate principle) — this directly informs the CDD ESLint tier design - - [ ] ⬜ Documentation structure and style (`docs/`, tool-specific docs, README structure) as the template for ts-qa-ci's docs - - [ ] ⬜ CI/GitHub Actions templates (`templates/github-actions/*.yml`, the autofix-then-gate pattern, branch protection setup script) -- [ ] ⬜ **Task 1.2**: Opus review pass on the concept-extraction report — confirm nothing load-bearing was missed before design work starts +### Phase 1: Research — Concept Extraction from Both References + +- [ ] ⬜ **Task 1.1**: Dispatch Sonnet research agents (dynamic workflow), in parallel, over both references: + - `./untracked/repos/php-qa-ci` — orchestration & delivery philosophy: + - [ ] ⬜ Pipeline architecture & phasing philosophy (why 4 phases, why code-mod runs first, fail-fast design) + - [ ] ⬜ The read-only/CI-write duality (`qaReadOnly`/`detectReadOnly`): mutating tools dry-run-and-fail in CI, auto-fix locally — the single most load-bearing behaviour of Phase 1, and directly portable to Prettier `--check`/`--write` and ESLint `--fix`/not + - [ ] ⬜ Tool delivery/encapsulation strategy (PHARs via PHIVE, isolated Rector sub-project, `replace` trick, `bin/` shims) and what has a realistic npm/Node equivalent — go in already knowing npm's nested dependency model avoids most of the *problem* PHIVE/PHAR solves; focus research on the narrower peerDeps-vs-bundled and plugin-delivery questions instead of treating this as a from-scratch unknown + - [ ] ⬜ Config cascade & override system (`qaConfig/`, per-tool `.inc.bash` overrides, platform detection) + - [ ] ⬜ Hook system (`hookPre.bash`/`hookPost.bash`, per-tool override files) and its Claude Code hooks (`deploy-skills.bash`, hook list, migration-on-update behaviour, the `PHP_QA_CI_DISABLE_CONFIG_PUSH` opt-out for auto-deploy-on-install) + - [ ] ⬜ Rule-tiering philosophy (PHPArkitect vs PHPStan "where does a rule belong," always-on vs opt-in rule tiers, SSoT-never-duplicate principle) **and** the "estate-wide checks must be pipeline-owned, not opt-in rules" lesson (SensitiveParameter coverage) — both directly inform the CDD ESLint tier's design and delivery mechanism + - [ ] ⬜ Documentation structure and style (`docs/`, tool-specific docs, README structure) as the template for ts-qa-ci's docs + - [ ] ⬜ CI/GitHub Actions templates (`templates/github-actions/*.yml`, the autofix-then-gate pattern, branch protection setup script) + - `./untracked/ec-site/` — TS-native realisations: + - [ ] ⬜ Catalogue all 57 rules in `eslint-rules/*.js` (with their `.md` docs); classify each lift-as-is / adapt / drop for ts-qa-ci + - [ ] ⬜ Deep-dive the CDD rules specifically: `no-html-in-pages.js`, `no-raw-block-html-in-articles.js` (the solved articles-collision precedent), `enforce-width-standards.js`, `no-orphaned-grid-items.js`, `no-hard-coded-component-data.js`, `no-duplicate-section-ids.js`, and the `eslint-plugin-tailwindcss` integration + - [ ] ⬜ The `npm run llm:qa` orchestrator shape (`scripts/llm-*.ts`, `var/qa/` caching convention) as a concrete answer to the orchestrator-CLI design question + - [ ] ⬜ Meta-rules (rules that lint the rules) and the synchronous-handler gotcha in `eslint-rules/CLAUDE.md` + - [ ] ⬜ Note the licensing/provenance question for lifting private-repo rules into a publishable package (flag for Technical Decisions, do not resolve unilaterally) +- [ ] ⬜ **Task 1.2**: Opus review pass on the concept-extraction report — confirm nothing load-bearing was missed, and confirm the lift/adapt/drop classification of ec-site's 57 rules is sound, before design work starts ### Phase 2: Design — TS Tool Landscape & Package Architecture -- [ ] ⬜ **Task 2.1**: Research and select the TS/JS tool for each pipeline phase, with rationale: - - [ ] ⬜ Phase 1 equivalent (code-modification): formatter (Prettier vs Biome) + codemod/upgrade tool (ts-migrate / Biome's own rules / manual) - - [ ] ⬜ Phase 2 equivalent (lint/validation): ESLint (flat config), `tsc --noEmit`, import/dependency validation (depcheck / knip), markdown link checking - - [ ] ⬜ Phase 3 equivalent (static analysis): `tsc` strict mode as the PHPStan analogue; evaluate whether a structural/architecture tool (dependency-cruiser, or a custom ESLint import-boundary tier) is the arkitect analogue - - [ ] ⬜ Phase 4 equivalent (testing): Vitest (unit) + Playwright (e2e/smoke), mutation testing analogue (Stryker Mutator, optional tier like Infection) - - [ ] ⬜ Decide the npm-package encapsulation strategy (peerDependencies vs internal pinning vs vendored binaries) and document the tradeoffs explicitly — this has no exact npm equivalent to PHIVE/PHAR and needs its own design -- [ ] ⬜ **Task 2.2**: Design the `ts-qa` orchestrator CLI: phases, `-t ` single-tool mode, `-p ` path scoping, config cascade resolution order, platform detection (generic vs Vite vs Next.js), exit-code/retry semantics -- [ ] ⬜ **Task 2.3**: Opus review of the tool-selection + architecture design before scaffolding starts +- [ ] ⬜ **Task 2.1**: Confirm the pre-committed baseline stack (ESLint flat config + Prettier + `tsc` + Vitest + Playwright) against Phase 1 findings, and resolve the genuinely open tool choices: + - [ ] ⬜ Structural/architecture tool for the arkitect analogue: dependency-cruiser vs. a custom ESLint import-boundary rule tier + - [ ] ⬜ Optional mutation-testing tier (Stryker Mutator, matching Infection's optional-tier treatment) + - [ ] ⬜ Dead-code/unused-dependency tool: knip vs. depcheck + - [ ] ⬜ Markdown link checking (a direct `mdlinks` analogue) +- [ ] ⬜ **Task 2.2**: Design the `ts-qa` orchestrator CLI: phases, `-t ` single-tool mode, `-p ` path scoping, config cascade resolution order, platform detection (generic vs Vite vs Next.js), exit-code/retry semantics, and the read-only/CI-write duality as a built-in mode (not bolted on later) +- [ ] ⬜ **Task 2.3**: Decide the npm dependency-encapsulation strategy concretely: which tools are peerDependencies (must match consumer's own version — `typescript`, `eslint`, `vite`) vs. bundled `dependencies`; confirm ESLint plugin delivery via exported flat-config rule objects (ec-site's approach) +- [ ] ⬜ **Task 2.4**: Decide whether ts-qa **owns and runs** its own ESLint config over a consumer (estate-wide enforcement, php-qa-ci's SensitiveParameter pattern) vs. shipping includable rule sets a consumer must opt into — this determines whether the CDD tier can ever be guaranteed-on, not just available +- [ ] ⬜ **Task 2.5**: Decide the Claude Code integration deploy mechanism: manual `deploy-skills`-style script only, vs. npm `postinstall` auto-deploy (document the `--ignore-scripts`/CI-policy/dirty-tree caveats php-qa-ci's own README flags for its composer-plugin equivalent) — and design the opt-out flag either way +- [ ] ⬜ **Task 2.6**: Decide the non-publish CI install mechanism needed for Phase 4 (git dependency, GitHub Packages, or committed tarball) — required before Task 4.6 can be attempted, since `npm ci` in GitHub Actions cannot resolve an `npm link` or a gitignored `file:` path +- [ ] ⬜ **Task 2.7**: Opus review of the tool-selection + architecture design before scaffolding starts ### Phase 3: Build — Package Scaffold - [ ] ⬜ **Task 3.1**: Scaffold `ts-qa-ci` in `./untracked/repos/ts-qa-ci` — package.json (`@longtermsupport/ts-qa-ci`), orchestrator CLI, config defaults, directory layout mirroring php-qa-ci's clarity (`configDefaults/`, `bin/`, `docs/`, `.claude/`) -- [ ] ⬜ **Task 3.2**: Implement Phase 1–4 tool runners with the config cascade and per-tool override mechanism -- [ ] ⬜ **Task 3.3**: Implement the CDD ESLint rule tier - - [ ] ⬜ `no-ad-hoc-html` (or similarly named): bans raw HTML tags outside designated component files +- [ ] ⬜ **Task 3.2**: Implement Phase 1–4 tool runners with the config cascade, per-tool override mechanism, and the read-only/CI-write duality +- [ ] ⬜ **Task 3.3**: Implement the CDD ESLint rule tier — **iteratively, cross-checked against this repo's real code from the start** (do not treat this as a frozen deliverable to hand to Phase 4 unchanged; prototype early against this repo's actual JSX/className surface and adapt ec-site's already-tuned versions rather than re-deriving from zero) + - [ ] ⬜ `no-ad-hoc-html` (JSX-scoped, adapted from ec-site's `no-html-in-pages`): bans raw HTML tags in `.tsx` component/page files outside designated component definition files - [ ] ⬜ `no-ad-hoc-classnames`: bans arbitrary/inline CSS class strings outside a component's internal variant-to-class mapping - [ ] ⬜ Variant-API enforcement rule(s): every styleable component must expose typed variant props; internal class resolution is the only place raw classes may appear - - [ ] ⬜ Rule documentation (purpose, examples, escape hatches) for each, matching the granularity of Plan 008's rule docs -- [ ] ⬜ **Task 3.4**: Implement the Claude Code integration deploy script (skills/hooks/agents push into a consumer project) + - [ ] ⬜ Rule documentation (purpose, examples, escape hatches) for each, matching the granularity of Plan 008's and ec-site's rule docs +- [ ] ⬜ **Task 3.4**: Implement the Claude Code integration deploy script (Node-authored, per Decision) — skills/hooks/agents push into a consumer project - [ ] ⬜ **Task 3.5**: Write the docs set (`docs/pipeline.md`, `docs/configuration.md`, `docs/tools/*.md`, `docs/coding-standards.md`, `docs/cdd-rules.md`, `docs/github-actions.md`) — progressive enhancement style: short/skimmable at the top, full depth available below -- [ ] ⬜ **Task 3.6**: Push scaffolded package to new GitHub repo `LongTermSupport/ts-qa-ci` +- [ ] ⬜ **Task 3.6**: Push the scaffolded package to the new GitHub repo `LongTermSupport/ts-qa-ci` — timed to when Phase 4 actually needs a CI-installable ref (Task 2.6's mechanism), not as an earlier formality ### Phase 4: Dogfood — Integrate into lts-commerce-site -- [ ] ⬜ **Task 4.1**: `npm link` (or workspace/file: dependency) ts-qa-ci into this repo as a devDependency -- [ ] ⬜ **Task 4.2**: Run the full `ts-qa` pipeline against this repo; triage every failure into "pipeline/config bug in ts-qa-ci" vs "genuine code-quality issue in lts-commerce-site" +- [ ] ⬜ **Task 4.1**: Install ts-qa-ci into this repo via the Task 2.6 mechanism (not a bare `npm link`, which CI cannot reproduce) +- [ ] ⬜ **Task 4.2**: Run the full `ts-qa` pipeline against this repo; triage every failure into "pipeline/config bug in ts-qa-ci" vs "genuine code-quality issue in lts-commerce-site". Expect the full measured baseline (5 ESLint errors, 53 files of Prettier drift, zero existing tests) plus whatever the new rule tiers surface — this is a first-time gate, not a consolidation - [ ] ⬜ **Task 4.3**: Fix ts-qa-ci pipeline/config bugs surfaced by the real run (iterate with Phase 3 as needed) -- [ ] ⬜ **Task 4.4**: Fix genuine violations surfaced in this repo's code, including CDD violations (ad hoc HTML/CSS classes) — this is expected to be non-trivial given the current codebase was not built against these rules -- [ ] ⬜ **Task 4.5**: Wire `ts-qa` into `.github/workflows/ci.yml`, replacing/consolidating the existing separate format/lint/typecheck/build steps where appropriate -- [ ] ⬜ **Task 4.6**: Confirm CI green end-to-end on a real PR, not just local runs -- [ ] ⬜ **Task 4.7**: Deploy the Claude Code integration tooling into this repo and confirm it works (skills/hooks show up correctly, no conflicts with existing hooks-daemon setup) +- [ ] ⬜ **Task 4.4**: Fix the pre-existing baseline violations (5 ESLint errors, Prettier drift) — independent of and prior to CDD-specific work, so CDD triage isn't muddied by unrelated pre-existing issues +- [ ] ⬜ **Task 4.5**: CDD violation remediation, sub-phased given there is no pre-existing variant-prop catalogue to redirect fixes to: + - [ ] ⬜ Triage/categorise every `no-ad-hoc-html` and `no-ad-hoc-classnames` violation across `src/pages/**` and `src/components/**` (expect ~185 raw JSX tags and ~259 `className` literals per the measured baseline) — group by recurring pattern, not file-by-file + - [ ] ⬜ Build/extend the variant-prop component catalogue for the patterns that recur (this is genuinely new component work, not just rule-fixing — budget for it explicitly rather than treating it as incidental) + - [ ] ⬜ Migrate violations onto the catalogue + - [ ] ⬜ Re-run the CDD tier to confirm convergence +- [ ] ⬜ **Task 4.6**: Add lint + format-check + type-check + test gates to `.github/workflows/ci.yml` for the first time (there is nothing to "consolidate" — today's CI only builds and deploys), orchestrated via `ts-qa` +- [ ] ⬜ **Task 4.7**: Confirm CI green end-to-end on a real PR, not just local runs — requires Task 3.6/2.6 (a CI-resolvable install mechanism) to already be in place +- [ ] ⬜ **Task 4.8**: Deploy the Claude Code integration tooling into this repo and confirm it works (skills/hooks show up correctly, no conflicts with this repo's existing hooks-daemon setup — check `hook_registration_checker` policy compliance specifically) ### Phase 5: Rollout Readiness @@ -105,7 +147,7 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - **Depends on**: Plan 008 (ESLint Custom Rules Adoption) — provides the existing 11-rule baseline the CDD tier extends, and prior art for how this repo evaluates/adapts external rule sets. - **Blocks**: Nothing directly; future articles/portfolio content about ts-qa-ci depend on this plan reaching Phase 5. -- **Related**: `LongTermSupport/php-qa-ci` (reference implementation, external repo). +- **Related**: `LongTermSupport/php-qa-ci` (reference implementation, external repo); `untracked/ec-site` (TS-native reference implementation, private LongTermSupport repo — see Technical Decisions on provenance). ## Technical Decisions @@ -119,7 +161,7 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i 2. Develop in `untracked/repos/ts-qa-ci` only, promote later. 3. Subdirectory package (`packages/ts-qa-ci/`) inside lts-commerce-site. -**Decision**: Option 1, with the practical bootstrapping detail from option 2 folded in: scaffold in `./untracked/repos/ts-qa-ci` first (fast iteration, no repo-admin overhead while the shape is still changing), then push to the new `LongTermSupport/ts-qa-ci` GitHub repo once the initial scaffold is solid. Rejected option 3 because it conflates a reusable cross-project tool with a single portfolio-site codebase, and would make dogfooding (a package installed as a dependency) impossible to test honestly. +**Decision**: Option 1, with the practical bootstrapping detail from option 2 folded in: scaffold in `./untracked/repos/ts-qa-ci` first (fast iteration, no repo-admin overhead while the shape is still changing), then push to the new `LongTermSupport/ts-qa-ci` GitHub repo once the initial scaffold is solid **and Phase 4 actually needs a CI-installable reference** (see Task 3.6/2.6 — pushing earlier as a pure formality was identified as premature during pre-flight review). Rejected option 3 because it conflates a reusable cross-project tool with a single portfolio-site codebase, and would make dogfooding (a package installed as a dependency) impossible to test honestly. **Date**: 2026-07-10 @@ -131,31 +173,56 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - `@lts` — the scope exists on npm but its member list is hidden via the public API (`{}` returned); ownership by the maintainer could not be confirmed without authenticating. Given how generic "lts" is as a scope name, risky to assume. - `@longtermsupport` — confirmed **unclaimed** (registry org-lookup returned 404). Exact match to the existing `LongTermSupport` GitHub org, which already hosts `php-qa-ci`. -- `@edmondscommerce` — confirmed **owned** by the maintainer (`registry.npmjs.org/-/org/edmondscommerce/user` → `{"edmondscommerce":"owner"}`). Has genuine prior art: `@edmondscommerce/feqa`, a "Frontend QA Pipeline" package published 2019, now dormant. +- `@edmondscommerce` — confirmed **owned** by the maintainer (`registry.npmjs.org/-/org/edmondscommerce/user` → `{"edmondscommerce":"owner"}`). Has genuine prior art: `@edmondscommerce/feqa`, a "Frontend QA Pipeline" package published 2019, now dormant. Note: `ec-site` (the Phase 1 TS-native reference) *is* the edmondscommerce-branded codebase. -**Decision**: `@longtermsupport/ts-qa-ci`, in a new `LongTermSupport/ts-qa-ci` GitHub repo. Chosen for exact consistency with the existing, already-established `php-qa-ci` branding and org, and because ownership is unambiguous (unclaimed scope registered fresh) versus the uncertain `@lts` status. `@edmondscommerce/feqa`'s existence is worth a look during Phase 1 research for any reusable ideas, but the new package supersedes it under the LTS/php-qa-ci lineage rather than reviving the edmondscommerce scope. +**Decision**: `@longtermsupport/ts-qa-ci`, in a new `LongTermSupport/ts-qa-ci` GitHub repo. Chosen for exact consistency with the existing, already-established `php-qa-ci` branding and org, and because ownership is unambiguous (unclaimed scope registered fresh) versus the uncertain `@lts` status. `ec-site`/`@edmondscommerce/feqa` prior art is mined for ideas (Phase 1) but the new package supersedes it under the LTS/php-qa-ci lineage rather than reviving the edmondscommerce scope. + +**Date**: 2026-07-10 + +### Decision 3: CDD rule scope is JSX-only; `articles.ts` is explicitly exempt + +**Context**: Pre-flight review found the original "ban all ad hoc HTML" wording ambiguous to the point of being unbuildable: `src/data/articles.ts` is 19,714 lines of HTML content inside JavaScript template-literal strings, not JSX, and is already special-cased in the existing `eslint.config.js` for other rules "because it holds raw article data including HTML content strings." + +**Decision**: The CDD `no-ad-hoc-html` rule is an AST/JSX rule scoped to `.tsx` files under `src/pages/**` and `src/components/**`. Article prose HTML inside `articles.ts` template literals is a sanctioned, separately-governed surface — not policed by this rule, following the precedent of ec-site's `no-raw-block-html-in-articles`, which draws exactly this distinction. Success criteria and violation counts are defined against the JSX surface only. + +**Date**: 2026-07-10 + +### Decision 4: Building the variant-prop component catalogue is in scope, not descoped + +**Context**: `no-ad-hoc-classnames` and variant-API enforcement presuppose a catalogue of components with typed variant props to redirect violators to. This repo has none (0 `class-variance-authority` usage, 17 components). ec-site can enforce its equivalent rule because it already has that catalogue; this repo does not. + +**Options Considered**: + +1. Descope variant-API enforcement from v1 entirely; ship only `no-ad-hoc-html` + Tailwind-class hygiene (no catalogue needed). +2. Bring building/extending the variant-prop catalogue into this plan's Phase 4 scope explicitly. + +**Decision**: Option 2. The user's stated ambition for this plan centres the CDD variant-driven-styling vision explicitly ("components have variants, variants internally drive custom CSS classes") — descoping it to a future plan would hollow out the plan's central deliverable rather than deliver it. Phase 4 (Task 4.5) is sub-phased accordingly: triage violations by recurring pattern, build the catalogue for those patterns, migrate, then enforce. This is acknowledged as materially more work than a pure rule-authoring task, and is budgeted as its own sub-phase rather than folded silently into "fix violations." **Date**: 2026-07-10 ## Success Criteria -- [ ] Concept-extraction report from Phase 1 exists and was reviewed by an Opus pass -- [ ] `ts-qa-ci` package scaffolded, pushed to `LongTermSupport/ts-qa-ci`, installable as a devDependency -- [ ] All four pipeline phases implemented with working default configs -- [ ] CDD ESLint tier implemented, documented, and enforced with zero violations in this repo +- [ ] Concept-extraction report from Phase 1 exists (covering both php-qa-ci and ec-site) and was reviewed by an Opus pass +- [ ] `ts-qa-ci` package scaffolded, pushed to `LongTermSupport/ts-qa-ci`, installable via a CI-reproducible mechanism (not `npm link`) +- [ ] All four pipeline phases implemented with working default configs, including the read-only/CI-write duality +- [ ] CDD ESLint tier implemented, documented, and enforced with zero violations in `src/pages/**` and `src/components/**` `.tsx` files (articles.ts explicitly exempted per Decision 3) +- [ ] A variant-prop component catalogue exists in this repo sufficient to have migrated every triaged CDD violation onto it +- [ ] Pre-existing baseline (5 ESLint errors, Prettier drift, zero tests) resolved independent of CDD work - [ ] Full documentation set written in progressive-enhancement style -- [ ] Claude Code integration deploy tooling implemented and verified working in this repo -- [ ] `ts-qa` pipeline wired into this repo's GitHub Actions CI and green on a real PR +- [ ] Claude Code integration deploy tooling implemented and verified working in this repo, confirmed compatible with the existing hooks-daemon setup +- [ ] `ts-qa` pipeline wired into this repo's GitHub Actions CI (lint + format + type-check + test, added for the first time) and green on a real PR - [ ] Rollout-readiness retrospective and publish plan documented ## Risks & Mitigations -| Risk | Impact | Probability | Mitigation | -| --------------------------------------------------------------------------------------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| No clean npm equivalent to PHIVE/PHAR encapsulation exists | Medium | High | Treat as an open design question in Phase 2, not an assumed solved problem; evaluate peerDependencies + strict version pinning as the pragmatic default | -| CDD rules surface a large volume of violations in this repo, stalling dogfooding | Medium | Medium | Expected and budgeted for in Phase 4; fix incrementally, consider `warn` level temporarily if volume is large (same pattern as Plan 008) | -| Scope creep into full multi-framework support before dogfooding proves the core | High | Medium | Non-Goals explicitly exclude this; architecture leaves room but generic + Vite/React is the only fully-implemented target | -| `@longtermsupport` npm scope registration turns out to have friction (e.g. requires paid org) | Low | Low | Confirmed unclaimed via registry API; verify actual registration mechanics early in Phase 3 rather than assuming | +| Risk | Impact | Probability | Mitigation | +| ----------------------------------------------------------------------------------------------------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Building the variant-prop catalogue (Decision 4) turns out much larger than a QA-tooling plan should absorb | High | Medium | Sub-phased and explicitly budgeted in Task 4.5; if it balloons, the fallback is to split catalogue-building into its own plan and re-scope Decision 4 to option 1 rather than stall Phase 4 indefinitely | +| No CI-reproducible install mechanism is settled before Phase 4 needs one | Medium | Low | Task 2.6 makes this an explicit Phase 2 decision, not a Phase 4 surprise (was a hard blocker found only at plan-review time in the first draft) | +| Adding lint/format/type/test gates to CI for the first time surfaces more than the measured baseline | Medium | Medium | Baseline already measured directly (5 ESLint errors, 53 Prettier-drift files, 0 tests) rather than assumed; Task 4.4 handles it before CDD-specific work begins | +| Scope creep into full multi-framework support before dogfooding proves the core | High | Medium | Non-Goals explicitly exclude this; architecture leaves room but generic + Vite/React is the only fully-implemented target | +| Lifting ec-site's rules into a publishable package without the maintainer's sign-off | Low | Low | Flagged explicitly in Phase 1 (Task 1.1) as a provenance question to raise, not resolve unilaterally | +| `@longtermsupport` npm scope registration turns out to have friction (e.g. requires paid org) | Low | Low | Confirmed unclaimed via registry API; verify actual registration mechanics early in Phase 3 rather than assuming | ## Notes & Updates @@ -163,7 +230,18 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - Cloned `LongTermSupport/php-qa-ci` to `./untracked/repos/php-qa-ci` for reference research - Resolved package location (new standalone repo, bootstrapped via `untracked/repos/` first) and npm scope (`@longtermsupport/ts-qa-ci`) via direct npm registry API checks — see Technical Decisions -- Next: dispatch Opus review of this plan's requirements/scope, then kick off the Phase 1 research workflow + +### 2026-07-10 - Pre-flight Opus Review + +- Dispatched an Opus review of the first draft against both references and this repo's real state before committing to Phase 1 research spend +- Discovered `./untracked/ec-site/` as a first-class, previously-unreferenced TS-native implementation of most of this plan's ambitions (57 ESLint rules, working CDD rules, solved articles-HTML collision, TS orchestrator) — folded in as a parallel Phase 1 research target alongside php-qa-ci +- Corrected the CI premise: measured directly, `.github/workflows/ci.yml` runs build+deploy only, no lint/format/test gate exists today, despite CLAUDE.md describing one — Phase 4 adds gates, doesn't consolidate them +- Measured real baseline: 5 pre-existing ESLint errors, 53 files of Prettier drift under `src/`, zero test files despite Vitest being configured, ~10,000+ raw HTML tags in `articles.ts` (string content, invisible to JSX rules), 0 `class-variance-authority` usage / 17 components (no variant catalogue exists) +- Scoped the CDD "no ad hoc HTML" rule to `.tsx` JSX only, explicitly exempting `articles.ts` (Decision 3) +- Decided to bring variant-prop catalogue construction into Phase 4 scope rather than descope it (Decision 4), matching the user's explicit CDD ambition +- Added an explicit non-publish CI-install-mechanism decision point (Task 2.6) ahead of the point where it would otherwise block Task 4.7 +- Full raw critique retained at `untracked/plan-011-review.md` (gitignored) +- Next: kick off the Phase 1 research workflow against both references --- From 85b1e5d829925976e0d386be1dad95c4191c55d0 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 10:09:26 +0000 Subject: [PATCH 05/20] Plan 011: flip status to In Progress, begin Phase 1 research Co-Authored-By: Claude Sonnet 5 --- CLAUDE/Plan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE/Plan/README.md b/CLAUDE/Plan/README.md index 7d23d26..6470132 100644 --- a/CLAUDE/Plan/README.md +++ b/CLAUDE/Plan/README.md @@ -12,7 +12,7 @@ This directory contains all project plans following the Planning Workflow (see ` - [006: Testing Infrastructure](006-testing-infrastructure/PLAN.md) - 📋 Planned - **Medium Priority** - Set up Vitest + Playwright (smoke tests for all pages); required before Plan 001 Phase 8 - [007: Component Library Lift from EC Site](007-component-library-lift/PLAN.md) - 📋 Planned - **Medium Priority** - Adopt 6 UI components (BlurText, Typewriter, StatusBadge, ThreeColumnFeatures, MobileCarouselGrid, HighlightTypewriter) - [008: ESLint Custom Rules Adoption](008-eslint-custom-rules/PLAN.md) - 📋 Planned - **Medium-High Priority** - Cherry-pick 8 ESLint rules from EC site's 80+ (SEO enforcement, navigation, placeholder prevention) -- [011: ts-qa-ci — TypeScript QA/CI Harness Package](011-ts-qa-ci-package/PLAN.md) - 📋 Planned - **High Priority** - New `@longtermsupport/ts-qa-ci` package (TS analogue of `lts/php-qa-ci`): orchestrated QA pipeline, CDD ESLint rule tier, Claude Code integration, dogfooded on this repo first +- [011: ts-qa-ci — TypeScript QA/CI Harness Package](011-ts-qa-ci-package/PLAN.md) - 🔄 In Progress - **High Priority** - New `@longtermsupport/ts-qa-ci` package (TS analogue of `lts/php-qa-ci`): orchestrated QA pipeline, CDD ESLint rule tier, Claude Code integration, dogfooded on this repo first ## Completed Plans From 5490908366abfe7ea3fc8c2ef50c93e7ea17e0b7 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 10:24:33 +0000 Subject: [PATCH 06/20] Plan 011: commit missed status-flip to In Progress Follow-up to 85b1e5d, which flipped README.md's index entry but missed staging the PLAN.md file itself. Co-Authored-By: Claude Sonnet 5 --- CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md index 33e7254..0db9d12 100644 --- a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md +++ b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md @@ -1,6 +1,6 @@ # Plan 011: ts-qa-ci — TypeScript QA/CI Harness Package -**Status**: Not Started +**Status**: In Progress **Created**: 2026-07-10 **Owner**: Claude Code **Priority**: High From 46d3addcfe1ca33662ccf747b823a5e05ae4f1c2 Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 10:28:23 +0000 Subject: [PATCH 07/20] Plan 011: mark Phase 1 complete, add Decision 5 (ec-site licensing gate) Phase 1 research (12-agent workflow + Opus review) finished cleanly: concept-extraction report and review verdict READY FOR PHASE 2, both retained under untracked/ (gitignored). Folds in headline findings and adds a blocking Task 3.0 gate requiring the maintainer's sign-off before any ec-site code is lifted into the publishable package. Co-Authored-By: Claude Sonnet 5 --- CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md | 44 +++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md index 0db9d12..f3b7e8a 100644 --- a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md +++ b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md @@ -74,25 +74,10 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i ## Tasks -### Phase 1: Research — Concept Extraction from Both References - -- [ ] ⬜ **Task 1.1**: Dispatch Sonnet research agents (dynamic workflow), in parallel, over both references: - - `./untracked/repos/php-qa-ci` — orchestration & delivery philosophy: - - [ ] ⬜ Pipeline architecture & phasing philosophy (why 4 phases, why code-mod runs first, fail-fast design) - - [ ] ⬜ The read-only/CI-write duality (`qaReadOnly`/`detectReadOnly`): mutating tools dry-run-and-fail in CI, auto-fix locally — the single most load-bearing behaviour of Phase 1, and directly portable to Prettier `--check`/`--write` and ESLint `--fix`/not - - [ ] ⬜ Tool delivery/encapsulation strategy (PHARs via PHIVE, isolated Rector sub-project, `replace` trick, `bin/` shims) and what has a realistic npm/Node equivalent — go in already knowing npm's nested dependency model avoids most of the *problem* PHIVE/PHAR solves; focus research on the narrower peerDeps-vs-bundled and plugin-delivery questions instead of treating this as a from-scratch unknown - - [ ] ⬜ Config cascade & override system (`qaConfig/`, per-tool `.inc.bash` overrides, platform detection) - - [ ] ⬜ Hook system (`hookPre.bash`/`hookPost.bash`, per-tool override files) and its Claude Code hooks (`deploy-skills.bash`, hook list, migration-on-update behaviour, the `PHP_QA_CI_DISABLE_CONFIG_PUSH` opt-out for auto-deploy-on-install) - - [ ] ⬜ Rule-tiering philosophy (PHPArkitect vs PHPStan "where does a rule belong," always-on vs opt-in rule tiers, SSoT-never-duplicate principle) **and** the "estate-wide checks must be pipeline-owned, not opt-in rules" lesson (SensitiveParameter coverage) — both directly inform the CDD ESLint tier's design and delivery mechanism - - [ ] ⬜ Documentation structure and style (`docs/`, tool-specific docs, README structure) as the template for ts-qa-ci's docs - - [ ] ⬜ CI/GitHub Actions templates (`templates/github-actions/*.yml`, the autofix-then-gate pattern, branch protection setup script) - - `./untracked/ec-site/` — TS-native realisations: - - [ ] ⬜ Catalogue all 57 rules in `eslint-rules/*.js` (with their `.md` docs); classify each lift-as-is / adapt / drop for ts-qa-ci - - [ ] ⬜ Deep-dive the CDD rules specifically: `no-html-in-pages.js`, `no-raw-block-html-in-articles.js` (the solved articles-collision precedent), `enforce-width-standards.js`, `no-orphaned-grid-items.js`, `no-hard-coded-component-data.js`, `no-duplicate-section-ids.js`, and the `eslint-plugin-tailwindcss` integration - - [ ] ⬜ The `npm run llm:qa` orchestrator shape (`scripts/llm-*.ts`, `var/qa/` caching convention) as a concrete answer to the orchestrator-CLI design question - - [ ] ⬜ Meta-rules (rules that lint the rules) and the synchronous-handler gotcha in `eslint-rules/CLAUDE.md` - - [ ] ⬜ Note the licensing/provenance question for lifting private-repo rules into a publishable package (flag for Technical Decisions, do not resolve unilaterally) -- [ ] ⬜ **Task 1.2**: Opus review pass on the concept-extraction report — confirm nothing load-bearing was missed, and confirm the lift/adapt/drop classification of ec-site's 57 rules is sound, before design work starts +### Phase 1: Research — Concept Extraction from Both References ✅ Complete (2026-07-10) + +- [x] ✅ **Task 1.1**: Dispatched 12 parallel Sonnet research agents over both references; synthesized into `untracked/plan-011-phase1-concept-report.md` (391 lines, Parts A/B/C + appendix). Covered all planned bullets for both `php-qa-ci` (7 areas: pipeline phasing, read-only/CI duality, tool delivery, config cascade, hooks/Claude integration, rule-tiering, docs/CI templates) and `ec-site` (5 areas: full 54-rule catalogue lift/adapt/drop, CDD deep-dive, orchestrator/caching, meta-rules, licensing flag) +- [x] ✅ **Task 1.2**: Opus review pass complete — verdict **READY FOR PHASE 2**, 6/6 spot-checked claims verified true against source, only 2 cosmetic findings (rule-count framing, one file citation), zero load-bearing gaps. Review at `untracked/plan-011-phase1-review.md` ### Phase 2: Design — TS Tool Landscape & Package Architecture @@ -110,6 +95,7 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i ### Phase 3: Build — Package Scaffold +- [ ] ⬜ **Task 3.0 (gate)**: Obtain the maintainer's explicit sign-off on lifting ec-site code per Decision 5 before any other Phase 3 task begins — (1) confirms ec-site code may be relicensed/republished under `@longtermsupport`, (2) confirms the lift/adapt/drop audit boundary (generic infrastructure vs. brand-specific rules that must stay private) - [ ] ⬜ **Task 3.1**: Scaffold `ts-qa-ci` in `./untracked/repos/ts-qa-ci` — package.json (`@longtermsupport/ts-qa-ci`), orchestrator CLI, config defaults, directory layout mirroring php-qa-ci's clarity (`configDefaults/`, `bin/`, `docs/`, `.claude/`) - [ ] ⬜ **Task 3.2**: Implement Phase 1–4 tool runners with the config cascade, per-tool override mechanism, and the read-only/CI-write duality - [ ] ⬜ **Task 3.3**: Implement the CDD ESLint rule tier — **iteratively, cross-checked against this repo's real code from the start** (do not treat this as a frozen deliverable to hand to Phase 4 unchanged; prototype early against this repo's actual JSX/className surface and adapt ec-site's already-tuned versions rather than re-deriving from zero) @@ -200,6 +186,14 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i **Date**: 2026-07-10 +### Decision 5: ec-site code lift requires the maintainer's explicit sign-off — blocking precondition for Phase 3 + +**Context**: Phase 1 research (Task 1.1, see `untracked/plan-011-phase1-concept-report.md` §B.5) found that ec-site is a private (`"private": true`) LongTermSupport repo, while `ts-qa-ci` is intended to become a publishable `@longtermsupport`-scoped package. Lifting ec-site's rules, meta-rules, cache library, or orchestrator scripts — even adapted — changes their exposure from "private, internal to one client site" to "public, redistributable." Several rules explicitly marked `drop` in the B.1 catalogue are ec-site-brand-specific (`no-hardcoded-contact-details.js`, `enforce-contact-link-whitelist.js`, `validate-tech-logos.js`, `validate-case-studies-index.js`, among others), underscoring this isn't a hypothetical concern. + +**Decision**: Before any Phase 3 code lift begins (Task 3.1 onward), obtain the maintainer's explicit sign-off that (1) ec-site's code may be relicensed/republished under the `@longtermsupport` scope, and (2) rules to be ported are audited first to separate genuinely generic QA infrastructure (the cache library, meta-rule concept, orchestrator shape, and the TRANSFERS-DIRECTLY/lift-as-is rows in the B.1 catalogue) from ec-site-brand-specific rules that must stay private or be rewritten from scratch. This is treated as a blocking precondition, not a footnote — added as an explicit Phase 3 gate task. + +**Date**: 2026-07-10 + ## Success Criteria - [ ] Concept-extraction report from Phase 1 exists (covering both php-qa-ci and ec-site) and was reviewed by an Opus pass @@ -241,7 +235,17 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - Decided to bring variant-prop catalogue construction into Phase 4 scope rather than descope it (Decision 4), matching the user's explicit CDD ambition - Added an explicit non-publish CI-install-mechanism decision point (Task 2.6) ahead of the point where it would otherwise block Task 4.7 - Full raw critique retained at `untracked/plan-011-review.md` (gitignored) -- Next: kick off the Phase 1 research workflow against both references + +### 2026-07-10 - Phase 1 Research Complete + +- 12-agent research workflow completed cleanly (14/14 agents succeeded, 0 errors); concept-extraction report and Opus review both written to `untracked/` (gitignored, not part of the plan itself) +- **Headline findings**: the read-only/CI-write duality (`qaReadOnly`/`detectReadOnly`) is confirmed as php-qa-ci's single most load-bearing mechanism and maps cleanly to Prettier `--check`/`--write` + ESLint `--fix`/not; npm's nested dependency model means the PHIVE/PHAR encapsulation problem mostly doesn't exist (DOESNT-APPLY) — real Phase 2 work is narrower (peerDeps for `typescript`/`eslint`/`vite`, ESLint plugin delivery via exported flat-config objects, already proven by ec-site) +- **ec-site rule catalogue**: all 54 real rules (of 57 files — 3 are a POC/helper/test) classified: 8 lift-as-is, 25 adapt, 21 drop. Strong always-on core-tier candidates identified: `no-eslint-disable` (3 independent cross-project precedents), `no-duplicate-section-ids`, `no-placeholder`, `require-explicit-type-annotations`, `require-exported-component-types`, `ssr-safe-hooks`, `validate-lazy-imports`, the meta-rules +- **CDD findings sharpen Decision 3/4**: `no-html-in-pages.js` is the confirmed ancestor of `no-ad-hoc-html`, needs widening to cover `src/components/**` not just pages; `no-raw-block-html-in-articles.js`'s mechanism is DOESNT-APPLY here (ec-site's articles are JSX, ours are template-literal strings) but its *governing principle* substantiates Decision 3 as-is; ec-site's own CVA/variant infrastructure is a **single-component pilot** (only `button.tsx`), not a mature catalogue — sharpens and validates Decision 4's choice to budget catalogue-building as real Phase 4 work rather than assume one exists to copy +- **`validate-routes-have-pages.js` flagged unsafe**: uses `eval()` on extracted source text — must be rebuilt with AST-based extraction before any adaptation, not a straight port +- **Genuinely open gaps for Phase 2** (not resolved by Phase 1 research): knip vs depcheck, markdown-link-checker tool choice (Task 2.1 remainder), and the non-publish CI install mechanism (Task 2.6) — flagged explicitly rather than guessed at +- **Blocking precondition surfaced**: ec-site is a private repo; lifting its rules/cache-library/orchestrator into a publishable `@longtermsupport` package requires the maintainer's explicit sign-off before Phase 3 code lift begins (not resolved by this research, flagged for Technical Decisions) +- Next: Phase 2 design work — targeted research to close the two genuine gaps, then formalize Part C's recommendations into concrete specs, then Opus review (Task 2.7) --- From ac423ab78f7cdbf177de9dabdde184531b63aedb Mon Sep 17 00:00:00 2001 From: joseph Date: Fri, 10 Jul 2026 10:45:29 +0000 Subject: [PATCH 08/20] Plan 011: relocate research artifacts into tracked plan folder, complete Phase 2 Supporting docs (concept report, all Opus reviews, Phase 2 design) were written to gitignored untracked/ instead of the plan folder per PlanWorkflow.md's own convention (supporting analysis lives alongside PLAN.md) - moved and re-linked. Phase 2 (tool selection, orchestrator CLI spec, dependency/CDD/deploy/CI decisions) complete. First Opus review pass (Task 2.7) returned NEEDS REVISION on one blocking finding: the config cascade's wholesale first-match-wins semantics would let a consumer's own eslint.config.js silently drop the entire always-on CDD tier, defeating Decision 4's estate-wide guarantee. Fixed with a dedicated resolveEslintConfig() merge-not-replace resolver. Also resolved: testing tools as peerDependencies, markdown external-link checking descoped from v1, and Decision 6 (ts-qa-ci repo will be public, unblocking the git- dependency CI install mechanism). Phase 3 is gated on Task 3.0 (Decision 5: maintainer sign-off before any ec-site code is lifted). Co-Authored-By: Claude Sonnet 5 --- CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md | 49 ++- .../phase1-concept-report.md | 390 ++++++++++++++++++ .../phase1-opus-review.md | 64 +++ .../011-ts-qa-ci-package/phase2-design.md | 318 ++++++++++++++ .../phase2-opus-review.md | 58 +++ .../preflight-opus-review.md | 368 +++++++++++++++++ 6 files changed, 1229 insertions(+), 18 deletions(-) create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/phase1-concept-report.md create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/phase1-opus-review.md create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/phase2-design.md create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/phase2-opus-review.md create mode 100644 CLAUDE/Plan/011-ts-qa-ci-package/preflight-opus-review.md diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md index f3b7e8a..e79aa6a 100644 --- a/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md +++ b/CLAUDE/Plan/011-ts-qa-ci-package/PLAN.md @@ -69,29 +69,25 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - `src/data/articles.ts` is **19,714 lines** containing roughly **10,000+ raw HTML tags** — but as content inside JavaScript template-literal strings in a `.ts` data file, not JSX. An AST/JSX-based ESLint rule cannot see inside a string literal; this is why the CDD rule is explicitly scoped to `.tsx` JSX only (see Goals/Non-Goals). - This repo has **zero** `class-variance-authority` usage and only 17 component files — there is no existing variant-prop catalogue for a `className` ban to redirect violators to. Building one is in scope for Phase 4, not assumed to pre-exist. - **npm/GitHub scope decision** (resolved 2026-07-10, see Technical Decisions): package will be `@longtermsupport/ts-qa-ci` in a new repo `LongTermSupport/ts-qa-ci`. `@edmondscommerce` is confirmed owned by the maintainer and has genuine prior art (`@edmondscommerce/feqa`, a dormant 2019 "Frontend QA Pipeline" package) but was not chosen — see decision rationale. -- **Pre-flight review**: an Opus agent reviewed this plan's first draft against both references and this repo's real state before Phase 1 research was dispatched; findings are folded into this version. Full critique retained at `untracked/plan-011-review.md` (gitignored, not part of the plan itself) for anyone who wants the raw reasoning. +- **Pre-flight review**: an Opus agent reviewed this plan's first draft against both references and this repo's real state before Phase 1 research was dispatched; findings are folded into this version. Full critique retained at [`preflight-opus-review.md`](preflight-opus-review.md) for anyone who wants the raw reasoning. - This plan explicitly treats lts-commerce-site as an imperfect dogfood target, not a reference implementation to be preserved as-is. ## Tasks ### Phase 1: Research — Concept Extraction from Both References ✅ Complete (2026-07-10) -- [x] ✅ **Task 1.1**: Dispatched 12 parallel Sonnet research agents over both references; synthesized into `untracked/plan-011-phase1-concept-report.md` (391 lines, Parts A/B/C + appendix). Covered all planned bullets for both `php-qa-ci` (7 areas: pipeline phasing, read-only/CI duality, tool delivery, config cascade, hooks/Claude integration, rule-tiering, docs/CI templates) and `ec-site` (5 areas: full 54-rule catalogue lift/adapt/drop, CDD deep-dive, orchestrator/caching, meta-rules, licensing flag) -- [x] ✅ **Task 1.2**: Opus review pass complete — verdict **READY FOR PHASE 2**, 6/6 spot-checked claims verified true against source, only 2 cosmetic findings (rule-count framing, one file citation), zero load-bearing gaps. Review at `untracked/plan-011-phase1-review.md` +- [x] ✅ **Task 1.1**: Dispatched 12 parallel Sonnet research agents over both references; synthesized into [`phase1-concept-report.md`](phase1-concept-report.md) (391 lines, Parts A/B/C + appendix). Covered all planned bullets for both `php-qa-ci` (7 areas: pipeline phasing, read-only/CI duality, tool delivery, config cascade, hooks/Claude integration, rule-tiering, docs/CI templates) and `ec-site` (5 areas: full 54-rule catalogue lift/adapt/drop, CDD deep-dive, orchestrator/caching, meta-rules, licensing flag) +- [x] ✅ **Task 1.2**: Opus review pass complete — verdict **READY FOR PHASE 2**, 6/6 spot-checked claims verified true against source, only 2 cosmetic findings (rule-count framing, one file citation), zero load-bearing gaps. Review at [`phase1-opus-review.md`](phase1-opus-review.md) -### Phase 2: Design — TS Tool Landscape & Package Architecture +### Phase 2: Design — TS Tool Landscape & Package Architecture ✅ Complete (2026-07-10) -- [ ] ⬜ **Task 2.1**: Confirm the pre-committed baseline stack (ESLint flat config + Prettier + `tsc` + Vitest + Playwright) against Phase 1 findings, and resolve the genuinely open tool choices: - - [ ] ⬜ Structural/architecture tool for the arkitect analogue: dependency-cruiser vs. a custom ESLint import-boundary rule tier - - [ ] ⬜ Optional mutation-testing tier (Stryker Mutator, matching Infection's optional-tier treatment) - - [ ] ⬜ Dead-code/unused-dependency tool: knip vs. depcheck - - [ ] ⬜ Markdown link checking (a direct `mdlinks` analogue) -- [ ] ⬜ **Task 2.2**: Design the `ts-qa` orchestrator CLI: phases, `-t ` single-tool mode, `-p ` path scoping, config cascade resolution order, platform detection (generic vs Vite vs Next.js), exit-code/retry semantics, and the read-only/CI-write duality as a built-in mode (not bolted on later) -- [ ] ⬜ **Task 2.3**: Decide the npm dependency-encapsulation strategy concretely: which tools are peerDependencies (must match consumer's own version — `typescript`, `eslint`, `vite`) vs. bundled `dependencies`; confirm ESLint plugin delivery via exported flat-config rule objects (ec-site's approach) -- [ ] ⬜ **Task 2.4**: Decide whether ts-qa **owns and runs** its own ESLint config over a consumer (estate-wide enforcement, php-qa-ci's SensitiveParameter pattern) vs. shipping includable rule sets a consumer must opt into — this determines whether the CDD tier can ever be guaranteed-on, not just available -- [ ] ⬜ **Task 2.5**: Decide the Claude Code integration deploy mechanism: manual `deploy-skills`-style script only, vs. npm `postinstall` auto-deploy (document the `--ignore-scripts`/CI-policy/dirty-tree caveats php-qa-ci's own README flags for its composer-plugin equivalent) — and design the opt-out flag either way -- [ ] ⬜ **Task 2.6**: Decide the non-publish CI install mechanism needed for Phase 4 (git dependency, GitHub Packages, or committed tarball) — required before Task 4.6 can be attempted, since `npm ci` in GitHub Actions cannot resolve an `npm link` or a gitignored `file:` path -- [ ] ⬜ **Task 2.7**: Opus review of the tool-selection + architecture design before scaffolding starts +- [x] ✅ **Task 2.1**: Baseline stack confirmed (ESLint flat + Prettier + `tsc` + Vitest + Playwright); all open tool choices resolved: **dependency-cruiser** (arkitect analogue, own phase-3 step — not via ESLint plugin, per SSoT), **Stryker Mutator** (opt-in mutation tier), **knip** (depcheck is archived), **remark-validate-links** (relative/anchor markdown-link parity with `mdlinks`; external-HTTP checking explicitly descoped from v1). See [`phase2-design.md`](phase2-design.md) §1 +- [x] ✅ **Task 2.2**: `ts-qa` orchestrator CLI fully specified — command surface, phase/tool membership table, module boundaries, config cascade algorithm, read-only/CI-write duality as concrete env vars, CI-vs-interactive retry gate. See §2 +- [x] ✅ **Task 2.3**: Dependency encapsulation table complete — `typescript`/`eslint`/`vite`/`@types/node`/test runners as peerDependencies, self-contained tools bundled, CDD rules as plain exported flat-config objects (no `eslint-plugin-*` package). See §3 +- [x] ✅ **Task 2.4**: ts-qa **owns and runs its own ESLint config** — Tier A core/CDD rules are force-merged via a dedicated `resolveEslintConfig()` resolver (merge-not-replace), distinct from the ordinary wholesale-replace cascade every other config file uses, so the estate-wide guarantee can't be silently dropped by a project's own `eslint.config.js`. Full rule-to-tier assignment (Tier A always-on / B opt-in CDD / C opt-in framework-specific) in §4 +- [x] ✅ **Task 2.5**: Manual `deploy-skills`-style script (Node-authored), not npm `postinstall` auto-deploy — npm's weaker `--ignore-scripts`/pnpm-lifecycle-blocking guarantees vs. Composer's plugin model made this the clear call. Hooks-daemon-compatible deploy contract in §5 +- [x] ✅ **Task 2.6**: Non-publish CI install mechanism decided — npm git-dependency (`github:LongTermSupport/ts-qa-ci#`) with `dist/` committed to avoid the flaky `prepare`-script trigger; requires the repo to be public (Decision 6); committed-tarball is the documented fallback. See §6 +- [x] ✅ **Task 2.7**: Opus review (Task 2.7) returned **NEEDS REVISION** on first pass — one blocking finding (§2.4/§4 internal contradiction on how the always-on CDD guarantee survives the config cascade). Fixed via the dedicated `resolveEslintConfig()` merge-not-replace resolver; 4 of 5 non-blocking findings also resolved inline (testing-tool peerDep decision, markdown external-link v1 scope, repo-visibility Decision 6, a table-wording nit). Full review at [`phase2-opus-review.md`](phase2-opus-review.md), revised design at [`phase2-design.md`](phase2-design.md) ### Phase 3: Build — Package Scaffold @@ -188,12 +184,20 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i ### Decision 5: ec-site code lift requires the maintainer's explicit sign-off — blocking precondition for Phase 3 -**Context**: Phase 1 research (Task 1.1, see `untracked/plan-011-phase1-concept-report.md` §B.5) found that ec-site is a private (`"private": true`) LongTermSupport repo, while `ts-qa-ci` is intended to become a publishable `@longtermsupport`-scoped package. Lifting ec-site's rules, meta-rules, cache library, or orchestrator scripts — even adapted — changes their exposure from "private, internal to one client site" to "public, redistributable." Several rules explicitly marked `drop` in the B.1 catalogue are ec-site-brand-specific (`no-hardcoded-contact-details.js`, `enforce-contact-link-whitelist.js`, `validate-tech-logos.js`, `validate-case-studies-index.js`, among others), underscoring this isn't a hypothetical concern. +**Context**: Phase 1 research (Task 1.1, see [`phase1-concept-report.md`](phase1-concept-report.md) §B.5) found that ec-site is a private (`"private": true`) LongTermSupport repo, while `ts-qa-ci` is intended to become a publishable `@longtermsupport`-scoped package. Lifting ec-site's rules, meta-rules, cache library, or orchestrator scripts — even adapted — changes their exposure from "private, internal to one client site" to "public, redistributable." Several rules explicitly marked `drop` in the B.1 catalogue are ec-site-brand-specific (`no-hardcoded-contact-details.js`, `enforce-contact-link-whitelist.js`, `validate-tech-logos.js`, `validate-case-studies-index.js`, among others), underscoring this isn't a hypothetical concern. **Decision**: Before any Phase 3 code lift begins (Task 3.1 onward), obtain the maintainer's explicit sign-off that (1) ec-site's code may be relicensed/republished under the `@longtermsupport` scope, and (2) rules to be ported are audited first to separate genuinely generic QA infrastructure (the cache library, meta-rule concept, orchestrator shape, and the TRANSFERS-DIRECTLY/lift-as-is rows in the B.1 catalogue) from ec-site-brand-specific rules that must stay private or be rewritten from scratch. This is treated as a blocking precondition, not a footnote — added as an explicit Phase 3 gate task. **Date**: 2026-07-10 +### Decision 6: `LongTermSupport/ts-qa-ci` will be a public GitHub repository + +**Context**: Phase 2 design (Task 2.6, see [`phase2-design.md`](phase2-design.md) §6) found that the recommended CI install mechanism (an npm git-dependency pinned to a commit SHA) needs zero network/auth setup only if the repo is publicly clonable; a private repo would require the same cross-repo `GITHUB_TOKEN`/PAT friction the design explicitly rejects for GitHub Packages, for no confidentiality benefit. + +**Decision**: `LongTermSupport/ts-qa-ci` is public. This is source-code visibility only, not npm-registry publication — the Non-Goal "not publishing to the public npm registry as part of this plan" is unaffected; a public GitHub repo and a published npm package are independent axes. Confidentiality of ec-site-derived code is already handled separately and more precisely by Decision 5's sign-off/audit gate (which code may be lifted at all), not by repo visibility — a private repo would not have protected anything Decision 5 doesn't already protect, while adding real CI friction. + +**Date**: 2026-07-10 + ## Success Criteria - [ ] Concept-extraction report from Phase 1 exists (covering both php-qa-ci and ec-site) and was reviewed by an Opus pass @@ -234,11 +238,11 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - Scoped the CDD "no ad hoc HTML" rule to `.tsx` JSX only, explicitly exempting `articles.ts` (Decision 3) - Decided to bring variant-prop catalogue construction into Phase 4 scope rather than descope it (Decision 4), matching the user's explicit CDD ambition - Added an explicit non-publish CI-install-mechanism decision point (Task 2.6) ahead of the point where it would otherwise block Task 4.7 -- Full raw critique retained at `untracked/plan-011-review.md` (gitignored) +- Full raw critique retained at [`preflight-opus-review.md`](preflight-opus-review.md) ### 2026-07-10 - Phase 1 Research Complete -- 12-agent research workflow completed cleanly (14/14 agents succeeded, 0 errors); concept-extraction report and Opus review both written to `untracked/` (gitignored, not part of the plan itself) +- 12-agent research workflow completed cleanly (14/14 agents succeeded, 0 errors); concept-extraction report and Opus review both committed alongside this plan ([`phase1-concept-report.md`](phase1-concept-report.md), [`phase1-opus-review.md`](phase1-opus-review.md)) - **Headline findings**: the read-only/CI-write duality (`qaReadOnly`/`detectReadOnly`) is confirmed as php-qa-ci's single most load-bearing mechanism and maps cleanly to Prettier `--check`/`--write` + ESLint `--fix`/not; npm's nested dependency model means the PHIVE/PHAR encapsulation problem mostly doesn't exist (DOESNT-APPLY) — real Phase 2 work is narrower (peerDeps for `typescript`/`eslint`/`vite`, ESLint plugin delivery via exported flat-config objects, already proven by ec-site) - **ec-site rule catalogue**: all 54 real rules (of 57 files — 3 are a POC/helper/test) classified: 8 lift-as-is, 25 adapt, 21 drop. Strong always-on core-tier candidates identified: `no-eslint-disable` (3 independent cross-project precedents), `no-duplicate-section-ids`, `no-placeholder`, `require-explicit-type-annotations`, `require-exported-component-types`, `ssr-safe-hooks`, `validate-lazy-imports`, the meta-rules - **CDD findings sharpen Decision 3/4**: `no-html-in-pages.js` is the confirmed ancestor of `no-ad-hoc-html`, needs widening to cover `src/components/**` not just pages; `no-raw-block-html-in-articles.js`'s mechanism is DOESNT-APPLY here (ec-site's articles are JSX, ours are template-literal strings) but its *governing principle* substantiates Decision 3 as-is; ec-site's own CVA/variant infrastructure is a **single-component pilot** (only `button.tsx`), not a mature catalogue — sharpens and validates Decision 4's choice to budget catalogue-building as real Phase 4 work rather than assume one exists to copy @@ -247,6 +251,15 @@ This repo is explicitly **not** assumed to be a paragon of best practice going i - **Blocking precondition surfaced**: ec-site is a private repo; lifting its rules/cache-library/orchestrator into a publishable `@longtermsupport` package requires the maintainer's explicit sign-off before Phase 3 code lift begins (not resolved by this research, flagged for Technical Decisions) - Next: Phase 2 design work — targeted research to close the two genuine gaps, then formalize Part C's recommendations into concrete specs, then Opus review (Task 2.7) +### 2026-07-10 - Phase 2 Design Complete + +- 2 gap-research agents closed the remaining open questions (final tool choices, CI install mechanism), followed by a design-synthesis agent producing [`phase2-design.md`](phase2-design.md) and an Opus review producing [`phase2-opus-review.md`](phase2-opus-review.md) +- First review pass returned **NEEDS REVISION**: one blocking internal contradiction between §2.4 (config cascade is first-match-wins, wholesale replace) and §4 (the CDD always-on tier "cannot be silently omitted") — under the cascade as originally written, a consumer's own `eslint.config.js` could in fact silently drop the entire core/CDD tier, exactly the "opt-in rules can't be relied on estate-wide" failure Task 2.4 exists to prevent +- **Fixed** by adding a dedicated `resolveEslintConfig()` resolver with merge-not-replace semantics (Tier A core always spread first; a project's `tsQaConfig/eslint.config.js` can only add rules or apply the documented narrow opt-out, never replace the base) — the one deliberate exception to the plan's otherwise-uniform first-match-wins cascade, and explicitly named as such so it doesn't read as an inconsistency +- Also resolved while revising: testing tools (`vitest`/`@playwright/test`/`@stryker-mutator/core`) are peerDependencies, not bundled (orchestrate the consumer's own config, same reasoning as `typescript`); external-HTTP markdown-link checking is explicitly out of v1 scope (relative/anchor-only via `remark-validate-links`); **Decision 6** added — `LongTermSupport/ts-qa-ci` will be a public GitHub repo (source visibility only, independent of the npm-publish Non-Goal), unblocking the git-dependency CI install mechanism +- Rule-to-tier assignment now concrete: Tier A (always-on core) includes `no-eslint-disable`, `no-duplicate-section-ids`, `no-placeholder`, `require-explicit-type-annotations`, `require-exported-component-types`, `ssr-safe-hooks`, `validate-lazy-imports`, the meta-rules, and the CDD flagship `no-ad-hoc-html`; Tier B (opt-in CDD) holds `no-ad-hoc-classnames` and variant-API enforcement, gated on the Phase 4 catalogue; Tier C (opt-in framework-specific) holds the remaining 13 adapted rules +- Next: Phase 3 (build) — but **blocked on Task 3.0** (Decision 5's maintainer sign-off for ec-site code lift) before any scaffolding that touches ec-site-derived rules begins + --- **Maintained by**: Joseph (LTS Commerce) diff --git a/CLAUDE/Plan/011-ts-qa-ci-package/phase1-concept-report.md b/CLAUDE/Plan/011-ts-qa-ci-package/phase1-concept-report.md new file mode 100644 index 0000000..c6bf55d --- /dev/null +++ b/CLAUDE/Plan/011-ts-qa-ci-package/phase1-concept-report.md @@ -0,0 +1,390 @@ +# Plan 011 — Phase 1 Concept-Extraction Report + +**Task**: Plan 011 (`ts-qa-ci`), Task 1.1 (research synthesis) and input to Task 1.2 (Opus review pass). +**Sources**: 12 independently-researched sections covering `php-qa-ci` (`/workspace/untracked/repos/php-qa-ci`) and `ec-site` (`/workspace/untracked/ec-site`), merged, de-duplicated, and cross-referenced against this repo (`lts-commerce-site`, `/workspace`) and its `CLAUDE.md`/hooks-daemon configuration. +**Date**: 2026-07-10 + +## How to read this report + +Part A covers `php-qa-ci` — the mature PHP reference — organised around orchestration and delivery philosophy. Part B covers `ec-site` — the TS-native reference that already implements most of this plan's ambition — organised around its ESLint rule catalogue, CDD rules, orchestrator, and meta-tooling. Where two source sections covered the same ground from different angles (e.g. the read-only/CI duality was researched twice, at different depths; several ESLint rules appear in both a batch table and the CDD deep-dive), the fuller/more authoritative treatment is kept and the other is cross-referenced rather than repeated. Part C is the synthesis: for every genuinely open Phase 2 decision (Tasks 2.1–2.7), it states what both references imply and what is still unresolved. Every extracted concept is tagged **TRANSFERS-DIRECTLY**, **NEEDS-TS-ADAPTATION**, or **DOESNT-APPLY**, per the plan's own instruction (Goal 1). + +--- + +## Part A: `php-qa-ci` — Orchestration & Delivery Philosophy + +### A.1 Pipeline architecture and phasing philosophy + +`php-qa-ci`'s `bin/qa` runs four sequential phases, each a meta-tool invoked through the same `runTool()` dispatch as any leaf tool: + +1. **Code Modification** (Rector, PHP CS Fixer) — mutates the tree. +2. **Linting and Validation** (PSR-4, Composer checks, strict types, PHP lint, composer-require-checker, markdown links) — cheap, syntactic/structural. +3. **Static Analysis** (PHPStan, PHPArkitect) — expensive, whole-program type inference. +4. **Testing** (PHPUnit, Infection) — most expensive, only meaningful on type-checked code. + +`docs/pipeline.md` states the reasoning directly: *"There is no point running static analysis on code that has not yet been auto-fixed, and no point running tests if the code has syntax errors."* This is a monotonically increasing cost/precision ladder, each phase presupposing the previous succeeded — a textbook fail-fast design, not an arbitrary ordering. + +**Why mutation runs first**: if static analysis ran before Rector/CS-Fixer mutated the code, its verdict would describe code that no longer exists by commit time. Running mutation first guarantees every downstream phase analyses the code that will actually ship. + +**Composition mechanics worth carrying over regardless of language**: + +- A meta-tool (`allCodingStandardsTools`, etc.) is *just another tool* invoked via the same dispatcher as any leaf tool — phases are not a special orchestrator concept, they're ordinary composition. +- **Tool resolution cascade**: `runTool()` resolves each tool name through three tiers — `{project}/qaConfig/tools/{tool}.inc.bash` (project override) → `includes/{platform}/{tool}.inc.bash` (platform-specific) → `includes/generic/{tool}.inc.bash` (fallback) — exclusive, first-match-wins, no merging. A sibling `runNonPlatformTool()` skips the platform tier. (Config *files*, as opposed to tool *scripts*, get the analogous treatment via `configPath()` — see A.4.) +- **Single-tool bypass**: `qa -t ` calls `runTool` directly, skipping phase grouping entirely — useful for iterating on one check without paying for the whole pipeline. +- **Per-tool path-support allowlist**: an explicit `PATH_SUPPORTING_TOOLS` vs `NON_PATH_SUPPORTING_TOOLS` list rejects `-p ` against tools that can't honour it (e.g. PHPArkitect, whose paths live in its own config file). +- **One global lock per invocation** (not per-phase) and **two fixed extension points**, `hookPre.bash`/`hookPost.bash` (project-authored, sourced into the pipeline's own shell) — see A.5 for the Claude Code hook system this composes with. + +**Fail-fast retry design** (`tryAgainOrAbort()` in `functions.inc.bash`): every leaf tool wraps its invocation in a retry loop. In `CI=true` mode, any failure is immediately fatal. In interactive mode, a failure pauses and prompts "try again? (y/n)" — because a human may fix code in an editor while the terminal waits. A `hasBeenRestarted` flag drives an end-of-run warning: *"RAN WITH RETRIES … you should run the whole process again to be sure everything is fine"*, because a retried tool doesn't re-validate phases that already passed before the fix. Orthogonal to this is a **fail-fast vs aggregate** axis (`qaAggregate`): aggregate mode collects every failing tool in one pass instead of stopping at the first, and only makes sense for a read-only verification run (you can't meaningfully "collect all pending diffs a fixer would apply" while also letting it write). Every leaf tool also distinguishes **crash (exit >1, e.g. bad config/parse failure) from failure (exit 1, a normal rule violation)** — retrying a crash is pointless; retrying a failure after a human fixes something is the whole point of the loop. + +**Classification**: + +| Concept | Classification | +| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| 4-phase cost ladder (mutate → lint → static-analyse → test) | TRANSFERS-DIRECTLY | +| Mutation-first ordering | TRANSFERS-DIRECTLY | +| Meta-tool = sequence of guarded tool calls | TRANSFERS-DIRECTLY | +| Tool-resolution cascade (project → platform → generic) | NEEDS-TS-ADAPTATION (bash `source` has no JS equivalent; needs dynamic `import()`/plugin loader) | +| Single-tool `-t` bypass; per-tool path-support allowlist | TRANSFERS-DIRECTLY | +| Single global lock; `hookPre`/`hookPost` extension points | TRANSFERS-DIRECTLY | +| CI-vs-interactive retry gate; crash-vs-failure exit-code distinction; fail-fast/aggregate duality | TRANSFERS-DIRECTLY (pure orchestration logic) | +| `tryAgainOrAbort` mechanism itself | NEEDS-TS-ADAPTATION (bash `while`+`read` → Node readline/child_process) | +| `hasBeenRestarted` end-of-run warning | TRANSFERS-DIRECTLY (good UX detail, port verbatim) | +| Platform detection (Symfony/Laravel) | DOESNT-APPLY literally; the *resolution-tier pattern* is a plausible analogue for a future Vite/Next.js tier | + +ec-site's own `llm:qa` (`npm run llm:type-check && npm run llm:lint && npm run llm:test`) encodes an ordering intuition (type-check → lint → test) but is a flat `&&` chain: no code-mutation phase, no retry loop, no fail-fast/aggregate duality, no phase grouping. It is a partial, single-shot analogue of Phases 2–4 with Phase 1 (mutation) entirely absent — a gap this pipeline design should close. See B.3 for the orchestrator's full comparison. + +### A.2 The read-only/CI-write duality — the single most load-bearing mechanism + +There are **two independent booleans**, set by different signals, deliberately kept orthogonal: + +| Variable | Question it answers | Set by | +| ------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `CI` | May I prompt interactively / retry in a loop? | Explicit `CI=true`, **or** `CLAUDECODE=1` ("Claude Code environment detected — enabling CI mode"), **or** no TTY on stdin/stdout. | +| `qaReadOnly` | May the mutating tools (Rector, PHP CS Fixer) **write files**? | `QA_READONLY` env override, else `GITHUB_ACTIONS=true` → `true`, else `false`. **Claude Code / non-TTY is not in this list.** | + +The header comment in `functions.inc.bash` states the intent explicitly: these were *historically conflated* under `CI`, which made it impossible to both (a) run a real verification gate that fails-instead-of-applies, and (b) let a non-interactive Claude/local session actually apply fixes. Splitting them fixes both. Concretely, three regimes fall out: + +| Context | `CI` | `qaReadOnly` | Behaviour | +| ------------------------- | ----- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| GitHub Actions | true | true | Single dry-run pass; a pending diff **fails the gate** with remediation text; no retry (retrying a dry-run can't change the result). | +| Claude Code session | true | false | **Writable** — applies changes directly. `CI=true` only suppresses the interactive retry prompt. | +| Real interactive terminal | false | false | Writable **and** interactive — prompts `(y/n)` to retry on failure. | + +**Rector's implementation**: `qaReadOnly=true` appends `--dry-run`, runs once, and interprets exit codes itself (0 = clean, 2 = pending changes → `reportReadOnlyWouldModify`, anything else = genuine error). `qaReadOnly=false` runs in the retry loop until clean. + +**PHP CS Fixer's implementation** adds a third, always-fatal category that exit codes alone don't capture: it `grep`s its own text output for `"Files that were not fixed due to errors"` (a lint/parse failure) and exits 1 regardless of read-only/writable mode or numeric exit code — the exit-code taxonomy alone was insufficient. + +**Caution found in the same codebase**: not every mutating tool adopted this pattern — `phpStrictTypes.inc.bash` unconditionally does `read -p "Would you like to fix?"` with no `CI`/`qaReadOnly` gating at all, a latent hang risk in any non-TTY context. `ts-qa-ci` should apply the gate **uniformly to every mutating tool from the start**, not retrofit it tool-by-tool the way `php-qa-ci` evidently did. + +**A shared remediation-message helper** (`reportReadOnlyWouldModify`) prints a fixed template naming the human tool, the exact local command to run with `QA_READONLY=0`, and the git add/commit follow-up — worth porting as a single shared helper, parameterised per tool. + +**Mapping to Prettier/ESLint**: + +- **`CI` vs `qaReadOnly` split itself** — TRANSFERS-DIRECTLY. The env-var signals (`GITHUB_ACTIONS`, `CLAUDECODE`, TTY detection) are platform-namespaced, not language-namespaced; `ts-qa` should implement its own `detectReadOnly()`-equivalent as one reusable function. +- **Prettier `--check`/`--write`** — TRANSFERS-DIRECTLY at the concept level, but NEEDS-TS-ADAPTATION on exit-code disambiguation: Prettier does not cleanly separate "clean" / "pending diff" / "genuine parse error" by exit code the way PHP CS Fixer's verified codes (0/8) do. `ts-qa` needs to empirically verify (in the spirit of `php-qa-ci`'s "(verified)" comments) which Prettier versions give which codes/output shapes and encode that as a tested contract, likely with the same stdout-grep fallback PHP CS Fixer uses. +- **ESLint `--fix`/no-`--fix`/`--fix-dry-run`** — NEEDS-TS-ADAPTATION, and structurally harder than Prettier: ESLint mixes auto-fixable and report-only rules *within one invocation*, whereas `php-qa-ci` keeps mutating tools (Rector, CS-Fixer) and pure-validation tools (PHPStan, PHPArkitect) strictly separate by phase. `ts-qa-ci` must decide explicitly whether ESLint runs twice (fix-capable under the read-only gate, then report-only for whatever `--fix` couldn't resolve) or once with mode selected by `qaReadOnly`, using the same crash/failure/clean triage (0 = clean, 1 = problems found, 2 = fatal config/crash — never retry). +- **Prettier/ESLint rule-ownership overlap** — a new consideration with no PHP-side precedent: Rector and CS-Fixer never fight over the same lines; Prettier and ESLint can, unless `eslint-config-prettier` (or the flat-config equivalent) is wired in. `ts-qa`'s Phase 1 rule-jurisdiction split must be designed fresh. +- **`tsc --noEmit`** — TRANSFERS-DIRECTLY, and the duality DOESNT-APPLY to it: like PHPStan/PHPArkitect it is inherently non-mutating, so it belongs unconditionally outside the `qaReadOnly` branch. + +**Confirmed gap in this repo today**: `/workspace/package.json` has `format`/`format:check` (the write/read-only pair for Prettier exists) but **no `--fix` script for ESLint at all**, and `/workspace/.github/workflows/ci.yml` runs `npm ci` → `npm run build` → deploy only — it invokes neither `format:check`, `lint`, nor `type-check`. This directly contradicts `CLAUDE.md`'s "Deployment Process" section (which claims Prettier auto-formats and commits, and TypeScript/ESLint block deployment on failure). There is no husky/lint-staged/git-hook wiring either. **The mutate/verify duality has zero enforcement in this repo's CI today** — it is pure aspiration in the docs, and `php-qa-ci` is the only working precedent to port from; ec-site provides no working example of this specific mechanism either (see A.8/B.3). + +### A.3 Tool delivery and dependency encapsulation + +`php-qa-ci` solves a problem specific to **Composer's flat, single-version dependency graph**: every consuming project's tool version must resolve to one shared version tree-wide. Four mechanisms address this: + +1. **PHARs via PHIVE** (PHPStan, PHP CS Fixer, Infection, Composer Require Checker, PHPArkitect) — self-executing, GPG-verified, committed binaries in `vendor-phar/`, entirely bypassing Composer's resolver. +2. **Isolated Rector sub-project** (`tools/rector/composer.json`) — Rector itself depends on `phpstan/phpstan` internally; giving it its own composer.json/vendor/ prevents that transitive dependency leaking into the consumer's tree. +3. **The `replace: {"phpstan/phpstan": "*"}` trick** — tells Composer's resolver "this package satisfies any `phpstan/phpstan` constraint, don't actually install it," so PHPStan *extension* packages (which declare `require: phpstan/phpstan`) install cleanly without pulling in a conflicting real copy alongside the PHAR. +4. **`PhpStanGuardPlugin`** — a Composer plugin doing post-install runtime verification: errors if the consumer directly required `phpstan/phpstan` anyway (defeating the `replace` trick), and cross-checks the bundled PHAR's version against what installed extensions expect via `Semver::satisfies()`. + +**None of this transfers as a mechanism** — npm's **nested** dependency model has no single flat `vendor/` where every package must agree on one tool version, so the problem these four mechanisms solve is structurally absent. All four are **DOESNT-APPLY**. + +**What does transfer is a narrower, genuinely open npm-native question**: peerDependencies vs bundled dependencies. Tools that must be the *exact same instance* the consumer's own code is compiled/linted against (because they participate in the consumer's own module/type graph) should be `peerDependencies`: `typescript` (a second nested TS compiler instance produces confusing, version-mismatched diagnostics), `eslint` (custom rule/plugin APIs are version-specific; a nested ESLint would fight the consumer's own binary/config resolution), and `vite` if `ts-qa-ci` ships Vite plugins. Tools that run as independent, self-contained subprocess checks with no runtime coupling (markdown-link checker, standalone formatter) can safely be ordinary `dependencies` — npm's nesting means no consumer conflict is possible. This is **NEEDS-TS-ADAPTATION**: same underlying goal as the PHAR/replace/isolation cluster, achieved via npm's native `peerDependencies` mechanism instead. + +`PhpStanGuardPlugin`'s *diagnostic intent* (detect and warn on consumer-conflict) is worth keeping but not its mechanism: npm's own `peerDependencies` warning (native since npm 7) already covers most of this need, so `ts-qa-ci` mainly needs to pick the right dependency type rather than build a custom guard plugin. NEEDS-TS-ADAPTATION. + +**ESLint plugin/rule delivery is already solved, natively, in TS** — and ec-site is a working example: under ESLint's flat-config system, a "plugin" is just a plain JS object with a `rules` map. ec-site's `eslint.config.js` imports local rule modules directly (`import noPlaceholder from './eslint-rules/no-placeholder.js'`) and namespaces them under a `custom` plugin object — no `eslint-plugin-*`-named package, no legacy plugin-name resolution. `ts-qa-ci` should ship its rule set the same way: plain exported objects/arrays a consumer imports and spreads into their own `eslint.config.js`. **TRANSFERS-DIRECTLY as a design pattern, and ec-site already implements the TS-native version of it.** + +A narrow surviving case for a "pinned binary" concept (the PHIVE goal, not its mechanism): if `ts-qa-ci` wants a genuinely decoupled, reproducible tool version (e.g. a canonical CI-gate `tsc`/`eslint` version distinct from whatever the consumer has), npm's lockfile + registry integrity hashes already provide this natively via ordinary `dependencies` — no bespoke vendoring layer needed. DOESNT-APPLY as "needs a PHIVE-equivalent," the goal is pre-solved by the platform. + +### A.4 Configuration cascade and override system + +Two independent resolution primitives (not one unified engine), plus a third precedence mechanism: + +**`configPath()`** resolves a config *file*: `{project}/qaConfig/{file}` → `configDefaults/{platform}/{file}` → `configDefaults/generic/{file}`. Notable finding: `configDefaults/` on disk today contains **only** `generic/` — the platform tier is architecturally real but currently unpopulated for Symfony (the docs show a hypothetical `configDefaults/symfony/phpstan.neon` that doesn't exist as a file). + +**`runTool()`** resolves a tool *script*: `{project}/qaConfig/tools/{tool}.inc.bash` → `includes/{platform}/{tool}.inc.bash` → `includes/generic/{tool}.inc.bash`, exclusive first-match-wins (no merging). A real example of platform behaviour *composing with* (not just replacing) generic behaviour exists: `includes/symfony/setConfig.inc.bash` explicitly `source`s the generic script first, then adds Symfony-only variables — extension is opt-in and explicit, not automatic merging. + +**Bash `${var:-default}` precedence** for scalar/array config variables — a 2-step precedence (defaults set first, project config sourced after and unconditionally wins), not a file cascade. + +**Doc/code drift found and worth heeding as a caution**: `CLAUDE.md`'s "6-step cascade" narrative references a `configDefaults.inc.bash` file that does not exist anywhere in the repo (it's shorthand for scattered `${var:-default}` assignments), and `docs/platform-detection.md` documents Laravel detection that `detectPlatform()` never implements (only Symfony via `symfony.lock` presence, else generic). `php-qa-ci` itself only ships 2 real platforms despite documenting a third. + +**Classification**: + +| Concept | Classification | +| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dual resolution primitives (config-file cascade vs. tool-script cascade, as two functions) | TRANSFERS-DIRECTLY | +| 3-tier search order, first-match-wins, no merging | TRANSFERS-DIRECTLY | +| `${var:-default}` env precedence | NEEDS-TS-ADAPTATION — use an explicit merge (`{...builtinDefaults, ...projectConfig, ...envOverrides}`) with a deliberately chosen precedence order, avoiding the footgun where php-qa-ci's project file unconditionally clobbers a pre-exported env var | +| Platform detection via marker file | NEEDS-TS-ADAPTATION — check `vite.config.*`/`next.config.*` presence, or `package.json` deps, for Vite/Next.js/generic | +| Composition pattern (platform explicitly re-imports/extends generic) | TRANSFERS-DIRECTLY | +| Doc/code drift risk (documented-but-unimplemented platforms) | NEEDS-TS-ADAPTATION as a process caution — keep `detectPlatform()`-equivalent code tested against its own docs | +| Log rotation/archival per tool; single lock file per project | TRANSFERS-DIRECTLY | + +Neither this repo nor ec-site currently implements anything like this cascade — ec-site's QA tooling (`README-QA.md`, `Dockerfile.ts-qa`) is a narrowly-scoped, per-code-snippet Docker harness with a hardcoded single config and no override points; this repo's `llm:qa` chain is a fixed, bespoke sequence. This is genuinely new work for `ts-qa-ci`, modelled on `php-qa-ci`'s cascade with npm-native mechanics (filesystem `existsSync` probes + dynamic `import()`). + +### A.5 Hook system and Claude Code integration + +**`hookPre.bash`/`hookPost.bash`**: pre-hook runs after preflight config resolution but before any tool executes; post-hook runs only after every phase succeeds (unreached on any upstream failure, since the pipeline is fail-fast). Both are `source`d into the pipeline's own shell, inheriting all pipeline variables. NEEDS-TS-ADAPTATION: the *lifecycle* transfers directly, but bash's `source`-into-parent-shell trick has no JS equivalent — `ts-qa` should dynamically `import()` a project-supplied `tsQaConfig/hookPre.ts`/`hookPost.ts` module exporting a callback the orchestrator invokes with a typed context object (safer than blind `source` regardless). Neither this repo nor ec-site has an equivalent project-level lifecycle-hook extension point today. + +**Per-tool override files** (`runTool()`'s cascade, A.4) let a project *completely replace* a tool step, not just tweak parameters. TRANSFERS-DIRECTLY as policy (project override > platform default > generic default), NEEDS-TS-ADAPTATION as mechanism (dynamic import of a resolved `.ts` module instead of `source`). + +**`deploy-skills.bash`** (705 lines) is what pushes skills/agents/hooks into a consumer project on `composer install`/`update`, via a Composer plugin (`SkillsDeployPlugin`) subscribed to `POST_INSTALL_CMD`/`POST_UPDATE_CMD`. It: + +- Copies `.claude/skills/*`, `.claude/agents/*.md`, `.claude/hooks/*.py` (with clobber protection — full directory replace, then `chmod +x`). +- Installs a git pre-commit hook only if absent or already carrying its own signature comment (never clobbers a hand-authored hook). +- Idempotently merges hook registrations into `settings.json` (byte-diff before write, no gratuitous rewrites), with a rename-migration dict for renamed hook files across versions. +- **Critically**: when a hooks-daemon is detected, it actively **removes** all classic `php-qa-ci__*.py` hook files and strips their `settings.json` entries, on the theory the daemon now provides that functionality — a classic-to-daemon migration precedent directly relevant here, since this repo already runs the hooks-daemon exclusively (confirmed via this session's system prompt `` block). +- The shipped hook catalogue (`php-qa-ci__auto-continue.py`, `php-qa-ci__prevent-destructive-git.py` — both "recommended for all projects" — plus optional ones for git-stash discouragement, plan-time-estimate blocking, doc-content validation, markdown organisation) maps near-1:1 onto this project's own hooks-daemon handlers (`destructive_git`, `git_stash`, `plan_time_estimates`, `validate_instruction_content`, `markdown_organization`) — three independent LTS implementations converging on the same policy set. + +**`PHP_QA_CI_DISABLE_CONFIG_PUSH`** opt-out: read first thing in `deploySkills()`, accepts truthy strings (`true`/`1`/`yes`/`on`), and — notably — the plugin **proactively logs the opt-out instructions on every run**, even when not using it, so deploy operators can discover the flag. The documented rationale: auto-deploy on `composer install`/`update` can leave a dirty working tree on hosts where that's unwanted (CI runners, staging/prod deploys that aren't Claude Code sessions). + +**Classification and the npm-specific risk this surfaces**: the Composer-plugin-subscribes-to-lifecycle-event pattern is TRANSFERS-DIRECTLY as *precedent for the tradeoff*, NEEDS-TS-ADAPTATION as mechanism — npm's nearest primitive is `package.json`'s `"postinstall"` script, which is materially weaker than Composer's plugin model: `npm install --ignore-scripts` (or a persisted `.npmrc` setting) silently skips it with **no equivalent logged feedback**, and `pnpm` blocks *all* dependency lifecycle scripts by default since v7 as a supply-chain mitigation. Given a growing corporate/CI trend toward blanket `--ignore-scripts` policies, an npm `postinstall` auto-deploy has a materially weaker guarantee than `php-qa-ci`'s Composer-plugin equivalent, and any opt-out env var it self-checks is bypassed entirely by `--ignore-scripts`/pnpm defaults before the deploy script ever runs. This is decision-relevant for Task 2.5 — see Part C. + +### A.6 Rule-tiering philosophy + +Two lessons from `php-qa-ci`'s PHPArkitect/PHPStan split, both directly relevant to designing the CDD ESLint tier. + +**1. "Where does a rule belong" decision guide.** README states it plainly: *"Default to PHPArkitect for structural rules. Upgrade to a PHPStan rule only when you need finer-grained, method-level, or semantic detection that arkitect cannot express."* PHPArkitect reasons about a class's *identity* (kind, name, namespace, ancestry) — naming conventions, namespace layering, dependency direction. PHPStan reasons about *code* — method-level predicates, composite name-pattern logic, behavioural/semantic checks. **SSoT principle**: never enforce one convention in both engines — when a structural rule already exists in the semantic engine, *migrate* it to the structural engine and delete the duplicate, rather than running both (concrete precedent: the Interface/Enum/Trait suffix convention was migrated from a PHPStan rule to arkitect). + +ESLint has no two-engine split (a single AST pass can express both identity and behavioural checks), so the *engine* distinction doesn't transfer — but the underlying decision axis does: identity/structural predicate vs semantic/behavioural predicate is still the right question when deciding which ESLint rule *tier* a check belongs in. The SSoT "never duplicate across two enforcement points" principle transfers directly and is the more important half. + +**2. Always-on vs opt-in tiers, and the "estate-wide checks can't be opt-in" lesson.** `php-qa-ci` ships PHPStan rules in three tiers: always-on (`rules-default.neon`, wired automatically), opt-in generic (`rules-optional.neon`), opt-in Symfony (`rules-optional-symfony.neon`, consumer adds an `includes:` line). PHPArkitect has the identical 3-tier shape. But the **SensitiveParameter usage check is deliberately NOT a PHPStan rule at all** — it's a separate, always-on *pipeline tool* invoked unconditionally inside `bin/qa`, with the reasoning stated twice for emphasis: *"PHPStan rules are opt-in (a consumer must include this library's rules neon), so they cannot be relied on estate-wide."* The escape hatch is opt-**out** (`useSensitiveParameterCheck=0`), not opt-in — an asymmetry that flips the estate-wide coverage guarantee: opt-out (default-on, rare escape) vs opt-in (default-off, rare adoption) produce opposite guarantees for the same rule. + +**Classification**: TRANSFERS-DIRECTLY as design patterns (tiered rule sets with opt-in escalation; the SSoT principle; the opt-in-can't-be-estate-wide lesson), NEEDS-TS-ADAPTATION for mechanics — ESLint flat config has a native, arguably better-suited idiom than PHPStan's `.neon` includes: exported config arrays (`rulesDefault`, `rulesOptional`, `rulesOptionalCdd`) a consumer spreads into their own `eslint.config.js`. Neither this repo nor ec-site currently implements a tiered always-on/opt-in split for multiple consumers — ec-site's 57 rules are all registered flat in one config because ec-site is the sole consumer of its own rules; it has never had to solve the multi-consumer tiering problem. This is genuinely new work, directly informing Part C's CDD-tier recommendation. + +### A.7 Documentation structure and style + +`php-qa-ci` uses a two-tier docs system: a top-level `README.md` with a docs index linking out, a `docs/` folder of focused single-topic files, and a `docs/tools/` subfolder for one-file-per-tool deep dives. Observed conventions worth adopting verbatim: short declarative headers with no fluff intro; numbered pipeline phases with bold tool name + one-line purpose; **relative links to actual source files with line anchors** rather than restated interfaces (directly matching this repo's own `CLAUDE.md` documentation-standards rule: *"NEVER replicate actual interfaces in documentation… link to the source file"*); config cascade explained once, precisely, with a concrete worked example rather than an abstract description; every tool doc follows a fixed shape (what it is → default config location → override mechanism → bundled extensions/rules table → troubleshooting with runnable snippets); every "how to enable/opt out" instruction pairs a one-line rationale with a copy-pasteable snippet. `CLAUDE.md` itself is a separate, longer, implementation-detail-oriented doc distinct from `docs/` — internals for someone modifying the tool, vs consumer-facing usage docs. + +**Classification**: TRANSFERS-DIRECTLY — this structure is language-agnostic and directly informs Task 3.5 (docs set: `docs/pipeline.md`, `docs/configuration.md`, `docs/tools/*.md`, `docs/coding-standards.md`, `docs/cdd-rules.md`, `docs/github-actions.md`). Neither ec-site nor this repo currently has an equivalent `docs/` tree for QA tooling (only a single flat `README-QA.md`); adopting this shape for `ts-qa-ci` is new work, not a rename. + +### A.8 CI/GitHub Actions patterns: the inline-barrier autofix-then-gate pattern + +Two jobs in one workflow file, chained with `needs:`: `autofix` (PR-only, writes fixes, commits back) → `gate` (read-only verification, runs on the fixed tip). + +**Mechanics**: `autofix` (gated `if: github.event_name == 'pull_request'`) checks out the PR branch tip (`ref: github.head_ref`, `fetch-depth: 0`), runs the orchestrator in explicit write mode (`QA_READONLY=0`), and pushes back with the default `GITHUB_TOKEN` — skipping cleanly if the tree is clean. `gate` (`needs: autofix`, skipped if autofix failed) re-checks out the **same tip autofix just pushed to**, and runs the orchestrator in its normal CI-auto-detected read-only mode, letting mutating tools dry-run-and-fail on any pending diff rather than silently reapplying it. + +**The load-bearing insight — why no PAT or re-trigger is needed**: *"The gate validates the fixed tip in the same run. The autofix push uses the default `GITHUB_TOKEN`, which deliberately starts no new workflow run — so there is no loop, no cancellation, and nothing to wait for."* GitHub Actions' anti-recursion rule (a push made with the built-in token never re-triggers `on: push`) would force a PAT and risk an infinite-trigger loop under a naive "commit then wait for fresh CI" design; keeping both steps in one execution graph sidesteps this entirely. + +**Safety properties**: autofix write-back only runs on PRs, never on a push to the default branch; `permissions: contents: write` is scoped to only the job that needs it (workflow default is `contents: read`); an explicit env var neutralises unrelated tree-writing side effects (a Composer plugin's own config-push) before the read-only gate runs, so the gate doesn't false-fail on drift it didn't cause; `composer install --no-scripts` avoids framework post-install hooks needing runtime env absent on the runner. Contrasted explicitly against a weaker single-job template that commits fixes *at the end* of one run — meaning the validating tools ran against **unfixed** code, a materially weaker guarantee than fix-then-validate ordering. + +**Classification**: + +| Concept | Classification | +| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Two-job `autofix` → `gate` via `needs:`, single run, no PAT | TRANSFERS-DIRECTLY | +| Dynamic-default-branch guard; concurrency group + cancel-in-progress | TRANSFERS-DIRECTLY | +| Single orchestrator binary with a read/write env flag auto-detecting CI | NEEDS-TS-ADAPTATION — no such orchestrator exists in this repo yet; must be built | +| Mutating tools dry-run-and-fail vs silently apply | NEEDS-TS-ADAPTATION, but *simpler* in TS: Prettier's `--check`/ESLint's no-`--fix` default mode is already read-only by construction, unlike PHP CS Fixer/Rector which need explicit dry-run flag plumbing | +| Phase-ordered pipeline inside one orchestrator | NEEDS-TS-ADAPTATION — genuinely new build, not a rename of existing npm scripts | +| CI auto-detection (`GITHUB_ACTIONS=true` → force read-only) | NEEDS-TS-ADAPTATION | +| Neutralising unrelated tree-writing side effects before the gate | NEEDS-TS-ADAPTATION — principle transfers (e.g. this repo's own `scripts/generate-snippets.mjs` output must be committed or excluded before a gate check) | +| `composer install --no-scripts`; PHIVE/PHAR install; SSH deploy-key bundling | DOESNT-APPLY — no npm equivalent problem class | +| Dynamic PHP-version detection from `composer.json` | NEEDS-TS-ADAPTATION — `actions/setup-node@v4` with `node-version-file` from `.nvmrc`/`package.json` does this natively and more simply; worth noting this repo's own `ci.yml` currently **hardcodes** `node-version: '18'` rather than deriving it — an existing gap, not just a porting task | +| Deferring the test phase from the gate (DB/service dependency) | NEEDS-TS-ADAPTATION, likely doesn't apply — this repo's Vitest suite has no evident DB/service dependency, so the gate could include tests directly; decide explicitly rather than inherit the PHP template's assumption | +| Branch-protection-requires-gate-job; coverage/artifact upload | TRANSFERS-DIRECTLY (coverage: `vitest run --coverage` already exists as an npm script) | + +This repo's current CI (`checkout → setup-node@18(hardcoded) → npm ci → npm run build → deploy`) has **no gate job and no autofix job at all** — the entire inline-barrier pattern is net-new work for Task 4.6, not a migration of an existing but differently-shaped mechanism. ec-site provides no CI-workflow precedent either (its own `llm:qa` scripts are local/manual, not wired into any GitHub Actions job). + +--- + +## Part B: `ec-site` — TS-Native Realisations + +ec-site (`edmonds-commerce-site`, confirmed `"private": true` in `package.json`) is a React+TS+Vite+Tailwind LongTermSupport site — the same stack as this repo — that has independently built most of what this plan wants: 57 custom ESLint rules (with paired `.md` docs), working CDD rules including a solved articles/raw-HTML collision, a TS-native orchestrator with agent-oriented caching, and meta-rules that lint the rules. Because ec-site is private, **Part B.5 raises a licensing/provenance flag that is not resolved by this report** and must be signed off before any Phase 3 code lift. + +### B.1 Master ESLint rule catalogue — all rules covered across the research batches + +All 57 rules live in `/workspace/untracked/ec-site/eslint-rules/*.js`, each paired with a `.md` doc. Three independent research batches (18 rules each) plus the CDD deep-dive (6 rules, 5 of which overlap with the batches and are reconciled below) collectively covered 55 distinct named rules from the full 57. Verdicts below merge all batches; where the CDD deep-dive additionally examined a rule already in a batch table, both angles are folded into one row (noted). + +**Verdict key**: **lift-as-is** = usable near-verbatim; **adapt** = real portable concept, needs de-hardcoding of ec-site vocabulary/paths/business logic (or, in one case, safety hardening) before it's package-worthy; **drop** = ec-site business/content/brand logic with no reusable core. + +| # | Rule file | Purpose | Verdict | Classification | Key reasoning | +| --- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `enforce-contact-link-whitelist.js` | Restricts `ContactLink`/`tel:`/`mailto:` to Contact page + Footer | drop | DOESNT-APPLY | Hardcoded ec-site page-path whitelist; brand/content policy, not code quality | +| 2 | `enforce-dynamic-data-sources.js` | Bans hardcoded case-study counts/tech versions; forces sourcing from registries | drop | DOESNT-APPLY | Tied to ec-site's anti-fabricated-metrics content initiative; no generic project has a `CASE_STUDIES` registry to point at | +| 3 | `enforce-external-link-whitelist.js` | Restricts external links to research/technology pages + Footer | drop | DOESNT-APPLY | Same shape as #1; also **duplicates policy with #13** — a rule-sprawl anti-pattern to flag, not replicate | +| 4 | `enforce-tel-links.js` | Enforces E.164 `tel:` formatting | adapt (opt-in a11y tier) | NEEDS-TS-ADAPTATION | Real UX concern, but UK-specific defaults and most TS/React projects have zero `tel:` links — opt-in "marketing site" tier only | +| 5 | `enforce-width-standards.js` | Bans non-standard Tailwind max-width classes vs. two approved tokens | adapt (CDD pattern template) | NEEDS-TS-ADAPTATION | Design tokens/exceptions are ec-site's own; the *shape* (narrow single-token banned/allowed list + hardcoded per-file exception table) is Plan 011's own cited `no-ad-hoc-classnames` reference point. See B.2 | +| 6 | `no-aside-after-expandable-details.js` | Flags `