From b287e15a67621cb9634fd8dd257770a91f126e48 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 09:24:55 -0400 Subject: [PATCH 1/8] ci: retire the orphaned coverage badge pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #502 dropped the Go Coverage badge from the README but left everything that fed it running: the non-gating `badge` job kept firing on every main push, holding ci.yml's only `contents: write`, publishing coverage-go.json to the orphan `badges` branch for a badge no page rendered. Retire rather than restore (#509). Removes the `badge` job, its two producer steps in `coverage`, scripts/ci/publish-badge.sh, and the `cov badge` subcommand. The coverage GATE is untouched — make cov, threshold.total, per-suite minima, and the Code Quality PR comments all still run; only the published badge surface is gone. ci.yml now declares no `contents: write` in any job. Also fixes a latent break this surfaced: `timing`'s needs still listed `badge`, which is a workflow-level error once the job is gone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .github/workflows/README.md | 26 +++++++------- .github/workflows/ci.yml | 61 ++------------------------------ .testcoverage.yml | 8 ++--- AGENTS.md | 2 +- CHANGELOG.md | 6 ++++ scripts/ci/publish-badge.sh | 54 ---------------------------- scripts/cov/main.go | 70 +------------------------------------ 7 files changed, 28 insertions(+), 199 deletions(-) delete mode 100755 scripts/ci/publish-badge.sh diff --git a/.github/workflows/README.md b/.github/workflows/README.md index cc5c4d62..2c59e74b 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -24,7 +24,6 @@ graph TB unit -. "coverage-unit (poll)" .-> coverage integration -. "coverage-integration (poll)" .-> coverage e2e -. "coverage-e2e (poll)" .-> coverage - coverage --> badge["badge (main) — non-gating"] title["title (PRs)"] --> ci["CI (aggregator — sole required check)"] lint["lint"] --> ci coverage --> ci @@ -55,14 +54,12 @@ Break one of these knowingly or not at all. Go suites) and event-filtered jobs (title on pushes, deploys on PRs) never orphan the required check, and adding/renaming jobs never requires a ruleset edit. Consequence: every job that must gate merges - **must be in the aggregator's `needs` list**. Three jobs are deliberately - non-gating and excluded: `timing` (advisory wall-clock table), + **must be in the aggregator's `needs` list**. Two jobs are deliberately + non-gating and excluded: `timing` (advisory wall-clock table) and `docs-preview` (the convenience Cloudflare preview deploy — `docs-build` already validates the build and *is* a need, so only the build gates; the preview deploy reports its own "Docs preview" check but, slow or - failed, never delays or reds `CI`), and `badge` (publishes the README - coverage badge to the `badges` branch on main pushes — a badge push must - never block a merge; it reports its own "Coverage badge" status). + failed, never delays or reds `CI`). 2. **A dedicated `coverage` job applies the consolidated gate, polling — not `needs`-ing — the suites.** Each suite (`unit`, `integration`, @@ -151,15 +148,18 @@ gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)): public-preview feature can never red `CI`. Fork PRs skip (no `code-quality` token, per GitHub's own guard). Renders only once the repo's *Settings → Security → Code quality* is enabled. -- **README badge** — on main pushes the job emits a shields.io endpoint JSON - for the merged Go total (`cov badge`, the exact gated number); the separate - non-gating `badge` job publishes it to the orphan `badges` branch, which the - README badge reads over `raw.githubusercontent.com` (public-repo only). The - `badge` job is the sole holder of `contents:write` and runs only on trusted - main, so a push to `badges` can't be influenced by PR code (invariant 5). +- **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). + A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an + orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) + dropped the badge from the README and the pipeline ran on for weeks + publishing to nothing, so the job, `scripts/ci/publish-badge.sh`, the + `cov badge` subcommand, and the `badges` branch are gone. **Consequence + worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the + `badge` job was its only holder. Restoring a badge means restoring that + permission, so weigh it against a `contents: read` alternative first. SDK (TS) coverage is gated by `make cov` but not yet published — extend with a -`language: javascript` upload step and a second badge JSON when wanted. +`language: javascript` upload step when wanted. ## Merge queue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c0b05f9..43f8233a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -393,27 +393,9 @@ jobs: with: file: tmp/coverage/go-coverage.xml language: go - # ── Go coverage badge data (main only) ───────────────────────────── - # Emit the shields.io endpoint JSON for the merged Go total (the exact - # gated number) and hand it to the `badge` job, the sole holder of - # contents:write, which publishes it to the `badges` branch. - - name: Generate Go coverage badge - if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }} - run: | - mkdir -p tmp/coverage/badge - go run ./scripts/cov badge > tmp/coverage/badge/coverage-go.json - - name: Upload badge data - if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: go-coverage-badge - path: tmp/coverage/badge - if-no-files-found: error - retention-days: 3 - overwrite: true # Coverage in the job-summary panel — the at-a-glance per-run number - # (the README badge + Code Quality PR comment are the published - # surfaces; this stays for the full per-package func table). + # (the Code Quality PR comment is the published surface; this stays + # for the full per-package func table). - name: Coverage summary if: always() run: | @@ -426,42 +408,6 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" fi - # ── Coverage badge publish (main only, non-gating) ───────────────── - # Publishes the Go merged-total coverage to the orphan `badges` branch as - # a shields.io endpoint JSON, which the README badge reads over - # raw.githubusercontent.com (#133). The ONLY job holding contents:write, - # and it runs solely on main pushes off a trusted-main checkout — it never - # executes PR code (trust-domain invariant). Non-gating: deliberately NOT - # in the `CI` aggregator's needs (a badge push must never block a merge) — - # it reports its own "Coverage badge" status. The number is produced by - # the coverage job (`cov badge`, the exact gated value) and passed here as - # the go-coverage-badge artifact, so this job needs no Go toolchain. - badge: - name: Coverage badge - needs: [changes, coverage] - if: >- - github.event_name == 'push' && github.ref == 'refs/heads/main' && - needs.changes.outputs.code == 'true' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write # push the badge JSON to the `badges` branch - actions: read # download the go-coverage-badge artifact from this run - steps: - # persist-credentials stays at its default (true): this job's whole - # purpose is to push, and it runs only on trusted main. publish-badge.sh - # fetches the badges branch itself, so a shallow checkout is enough. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - - name: Download badge data - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: go-coverage-badge - path: tmp/coverage/badge - - name: Publish badge to the badges branch - run: scripts/ci/publish-badge.sh tmp/coverage/badge/coverage-go.json coverage-go.json - # ── Docs deploys ───────────────────────────────────────────────────── # Both deploy jobs check out the DEFAULT branch (or, on push, the pushed # main commit itself) and install wrangler from that trusted lockfile — @@ -655,7 +601,7 @@ jobs: # deploy (see its job), non-gating by design, so it neither delays nor # reds the required check — its own "Docs preview" status reports it. The # `docs-build` it depends on IS here, so a broken docs BUILD still gates. - # `timing` and `badge` (both non-gating) are the only other jobs excluded. + # `timing` (non-gating) is the only other job excluded. ci: name: CI needs: @@ -705,7 +651,6 @@ jobs: integration, e2e, coverage, - badge, docs-preview, docs-deploy, ] diff --git a/.testcoverage.yml b/.testcoverage.yml index 669d65be..7cecc7a5 100644 --- a/.testcoverage.yml +++ b/.testcoverage.yml @@ -2,10 +2,10 @@ # behind `make cov` / `make test-`) which renders profiles and # gates each suite against the numbers below. Schema is compatible with # `vladopajic/go-test-coverage`'s config format, but we don't run that -# action anymore. Publishing (#133) lives in CI's `coverage` job — a -# README badge from the merged Go total (`cov badge`) and PR comments -# via GitHub Code Quality; see .github/workflows/README.md "Coverage -# publishing". This file is the gate; publishing reads from it. +# action anymore. Publishing (#133) lives in CI's `coverage` job — PR +# comments via GitHub Code Quality; see .github/workflows/README.md +# "Coverage publishing". (The README coverage badge this also fed was +# retired in #509.) This file is the gate; publishing reads from it. # Merged-suites profile produced by `scripts/cov merge` at the end of # `make ci`. Contains unit + integration + e2e coverage stitched diff --git a/AGENTS.md b/AGENTS.md index 9967534a..d79cb184 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ Tooling notes (the non-obvious bits `make help` won't tell you): - **Policy helpers**: Use `policy.NewMemoryStore(p)` for in-memory policy testing without NATS. - **Pipes helpers**: Use `pipes.NewMemoryStore(queries...)` for in-memory pipes testing without NATS. - **Response assertions**: Use `testutil.AssertJSONResponse(t, rec, status, expected)` and `testutil.AssertJSONContains(t, rec, status, substring)`. -- **Coverage target**: 80% project-wide (CI enforces `threshold.total` in `.testcoverage.yml` against the merged unit + integration + e2e profile). Per-suite minima also enforced: unit 80%, integration 20%, e2e 60%, sdk 50%. Aim for 80%+ on new code. Coverage is published as a README badge (Go merged-total) and as PR comments via GitHub Code Quality — see `.github/workflows/README.md` "Coverage publishing"; the gate is unchanged. +- **Coverage target**: 80% project-wide (CI enforces `threshold.total` in `.testcoverage.yml` against the merged unit + integration + e2e profile). Per-suite minima also enforced: unit 80%, integration 20%, e2e 60%, sdk 50%. Aim for 80%+ on new code. Coverage is published as PR comments via GitHub Code Quality — see `.github/workflows/README.md` "Coverage publishing"; the gate is unchanged. - **Every new function should have corresponding test cases.** Run `make lint` and `make test` before considering work complete. - **E2E tests via SDK**: The TypeScript SDK is the primary E2E test harness. Tests in `tests/e2e/sdk/` exercise the full pipeline (ingest → ClickHouse → query) and simultaneously validate backend behavior and SDK correctness. Use `make test-e2e` to run. Add new E2E scenarios as `tests/e2e/sdk/*.test.ts` files using helpers from `tests/e2e/sdk/helpers.ts`. - **Per-suite table isolation**: Each e2e test file owns its own ClickHouse tables — `clicks_` / `events_` / `users_`, generated from `tests/e2e/sdk/tables.ts` and created by `setup.ts`. A new test file must (1) add its suite name to `SUITES` in `tables.ts` and (2) get its names via `const T = suiteTables("")`, then reference `T.clicks` etc. — never a bare `clicks`. This makes cross-file *data* contamination structurally impossible. Files still run **sequentially** (`vitest.config.ts` `maxWorkers: 1`): running them in parallel is blocked by shared *global policy* state (several files read-modify-write the single policy document; `streaming.test.ts` flips the global `default_role`), so policy-mutating tests snapshot the full policy and restore it. Dropping `maxWorkers: 1` is a deferred follow-up tracked in #214 (per-table policy storage; see `docs/src/content/docs/ingest-pipeline.md` § Deferred). diff --git a/CHANGELOG.md b/CHANGELOG.md index 713b72c7..b47595ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Docs-site analytics for search, code copies, 404s, docs section, and live-demo connectivity** (`docs/src/components/DocsTracking.astro` (new), `docs/src/components/{PostHog,Footer,LiveDemo}.astro`): the site tracked its own CTAs but nothing a reader did on the way to one, so the questions that decide what to write next — what people search for and *don't* find, which snippets get copied, which dead links keep getting followed — had no data behind them. `docs_search` fires a second after the query settles rather than once per keystroke, carrying `query` and `result_count` read off Pagefind's own results message (the rendered list is capped at its page size, so counting the DOM would under-report); `result_count: 0` is the event worth having. `code_copied` (`page`, `language`) watches Expressive Code's copy buttons from the document rather than re-binding every code block on every navigation — the hero's install chip is not an EC block and keeps its own `hero_install_copied`. `docs_404` (`path`, `referrer`) turns broken inbound links into a list instead of a hunch. A `doc_section` property (the first path segment, `home` for `/`) puts every event in a docs area without each tracker carrying its own copy; it's stamped at capture time by a `before_send` hook in `posthog.init()` rather than `register()`, because a queued `register()` replays only after init has already captured the first hard-load `$pageview` — which would then carry the previous visit's persisted value — and `history_change` navigations update the URL before capture fires, so reading `location` in the hook is always current. `live_demo_connected` fires once per mount when the hero's SSE feed comes up rather than on its first row — named for what it measures (the demo backend answered), since a quiet minute on the repo is not a disengaged reader. The three site-wide trackers share one new `DocsTracking.astro` rendered from the footer (like `MermaidZoom` / `ScrollHints`) and delegate from `document`, since Pagefind, Expressive Code, and the 404 route all own their own markup — some of it created after page load. +### Removed + +- **The README coverage badge and its whole publishing pipeline** (`.github/workflows/ci.yml`, `scripts/ci/publish-badge.sh` (deleted), `scripts/cov/main.go`, `.github/workflows/README.md`, `.testcoverage.yml`, `AGENTS.md`): [#502](https://github.com/Wave-RF/WaveHouse/pull/502) rewrote the README badge row and dropped the Go Coverage badge, but nothing removed what fed it — so for weeks the non-gating `badge` job kept running on every main push, holding `ci.yml`'s only `contents: write`, publishing `coverage-go.json` to the orphan `badges` branch for a badge no page rendered. Retired rather than restored ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)): the `badge` job, its two producer steps in `coverage` (`cov badge` + the `go-coverage-badge` artifact), `scripts/ci/publish-badge.sh`, the `cov badge` subcommand (with `badgeData`/`badgeColor`), and the `badges` branch itself. **The gate is untouched** — `make cov`, `.testcoverage.yml`'s `threshold.total` and per-suite minima, and the GitHub Code Quality PR comments (the other half of [#133](https://github.com/Wave-RF/WaveHouse/issues/133)) all still run; only the published badge surface is gone. The security consequence is the reason to prefer retiring over restoring: **`ci.yml` now declares no `contents: write` in any job**, so the workflow that executes PR-authored code can no longer write to the repository under any path. Also fixed in passing: `timing`'s `needs` still listed `badge` (a dangling `needs` is a workflow-level error once the job is gone), and the "sole holder of `contents:write`" comments were repo-wide claims that were only ever true within `ci.yml` — `release.yml` and `publish-npm.yml` hold it too. + +- **The Go Report Card badge stays gone** (no code change — [#502](https://github.com/Wave-RF/WaveHouse/pull/502) already removed the only reference): recorded here because [#509](https://github.com/Wave-RF/WaveHouse/issues/509) tracked it as collateral to restore. It isn't coming back — goreportcard.com is deprecated upstream, and the signal it graded (gofmt, vet, lint, misspell) is gated far more strictly by `make lint-go`/`golangci-lint` in CI. + ## [0.1.0] - 2026-08-19 The first public release. Everything below shipped in it — the sections are grouped the way Keep a Changelog asks for, but since there is no previous release to compare against, a reader upgrading from nothing can treat the whole file as "Added". The date is the intended cut date; correct it if tagging slips, and move anything merged in between up from `## Unreleased`. diff --git a/scripts/ci/publish-badge.sh b/scripts/ci/publish-badge.sh deleted file mode 100755 index d82e8d5a..00000000 --- a/scripts/ci/publish-badge.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Publish a shields.io endpoint JSON to the orphan `badges` branch, which the -# README coverage badge reads over raw.githubusercontent.com (unrestricted now -# the repo is public — #133). -# -# Runs ONLY in the main-push `badge` job — the sole holder of contents:write — -# off a trusted-main checkout, so a push to this branch can never be influenced -# by PR code (.github/workflows/README.md, invariant 5). -# -# `badges` is a detached, single-purpose store with no shared history to -# protect: we fast-forward a tiny commit onto it (creating it as an orphan the -# first time), and no-op when the number hasn't changed. The workflow-level -# concurrency group serializes main-push runs, so the push never races itself. -# -# Usage: scripts/ci/publish-badge.sh -# Requires a checkout with push credentials (actions/checkout default). - -set -euo pipefail - -src="${1:?usage: publish-badge.sh }" -dest="${2:?usage: publish-badge.sh }" -branch="badges" - -[ -f "$src" ] || { echo "::error::badge source not found: $src" >&2; exit 1; } - -# A linked worktree keeps the job's main checkout untouched. -work="$(mktemp -d)" -trap 'git worktree remove --force "$work" 2>/dev/null || true' EXIT - -if git fetch --depth=1 origin "$branch" 2>/dev/null; then - git worktree add --force "$work" "origin/$branch" - git -C "$work" checkout -B "$branch" -else - echo "badges branch not found — creating it." - git worktree add --force --detach "$work" - git -C "$work" checkout --orphan "$branch" - git -C "$work" rm -rf . >/dev/null 2>&1 || true -fi - -mkdir -p "$work/$(dirname "$dest")" -cp "$src" "$work/$dest" -git -C "$work" add "$dest" - -if git -C "$work" diff --cached --quiet; then - echo "Coverage badge unchanged — nothing to publish." - exit 0 -fi - -git -C "$work" \ - -c user.name="github-actions[bot]" \ - -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ - commit -q -m "chore(badge): update coverage badge" -git -C "$work" push origin "$branch" -echo "Published $dest to the $branch branch." diff --git a/scripts/cov/main.go b/scripts/cov/main.go index f4250bfa..dc1c6424 100644 --- a/scripts/cov/main.go +++ b/scripts/cov/main.go @@ -21,10 +21,6 @@ // cov threshold Print the configured threshold for // (or "total"). Used by the SDK pipeline to // pass into vitest's --coverage.thresholds. -// cov badge Emit the shields.io endpoint JSON for the merged -// Go-total coverage (the number threshold.total -// gates) to stdout — CI publishes it to the -// `badges` branch for the README coverage badge. // // All thresholds come from .testcoverage.yml — `threshold.total` is the // canonical merged-coverage gate (the same field go-test-coverage reads @@ -34,7 +30,6 @@ package main import ( "context" - "encoding/json" "fmt" "os" "os/exec" @@ -147,10 +142,6 @@ func main() { if err := report(cfg); err != nil { fatal("%v", err) } - case "badge": - if err := badge(cfg); err != nil { - fatal("%v", err) - } case "threshold": if len(os.Args) < 3 { usage() @@ -162,7 +153,7 @@ func main() { } func usage() { - fmt.Fprintln(os.Stderr, "usage: cov render | merge | ts-merge | merge-all | report | badge | threshold ") + fmt.Fprintln(os.Stderr, "usage: cov render | merge | ts-merge | merge-all | report | threshold ") os.Exit(2) } @@ -623,65 +614,6 @@ func printReport(rows []reportRow) { fmt.Println() } -// badgeData is the shields.io endpoint schema (https://shields.io/endpoint). -// The README's coverage badge is an pointing at img.shields.io/endpoint -// whose url= is this JSON, published to the `badges` branch by CI. Emitting it -// here means the badge always shows the exact number `make cov` gated. -type badgeData struct { - SchemaVersion int `json:"schemaVersion"` - Label string `json:"label"` - Message string `json:"message"` - Color string `json:"color"` -} - -// badge writes the shields.io endpoint JSON for the merged Go-total coverage -// (global excludes only — the same number threshold.total gates) to stdout, -// and nothing else, so the caller can redirect it straight to a file. It reads -// the profile `make cov`/`cov report` already rendered to tmp/coverage/total. -func badge(c *config) error { - profile := filepath.Join(root, "total", "coverage.txt") - if _, err := os.Stat(profile); err != nil { - return fmt.Errorf("no merged Go profile at %s — run `make cov` first", profile) - } - _, total, covered, err := parseCoverage(profile, c, c.excludesFor("")) - if err != nil { - return err - } - msg, color := "unknown", "lightgrey" - if total > 0 { - pct := float64(covered) * 100.0 / float64(total) - msg = fmt.Sprintf("%.1f%%", pct) - color = badgeColor(pct, c.Threshold.Total) - } - out, err := json.Marshal(badgeData{SchemaVersion: 1, Label: "coverage", Message: msg, Color: color}) - if err != nil { - return err - } - fmt.Println(string(out)) - return nil -} - -// badgeColor maps a coverage percentage to a shields.io color anchored on the -// configured gate: at/above the gate reads green, warming through yellow to -// red below it (so the badge color tracks the same line the build enforces). -func badgeColor(pct float64, gate int) string { - g := float64(gate) - switch { - case pct >= g+10: - return "brightgreen" - case pct >= g: - return "green" - case pct >= g-10: - return "yellowgreen" - case pct >= g-20: - return "yellow" - case pct >= g-30: - return "orange" - default: - return "red" - } -} - // formatPctBare is formatPct without the trailing % ("85.1"), or "n/a". func formatPctBare(covered, total int) string { if total == 0 { From d1e022c9568e0dbbbdd7c3a62deb43e6f4d1515e Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 10:09:29 -0400 Subject: [PATCH 2/8] ci: address review findings on the badge retirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four documentation-accuracy fixes from the pre-push code and docs reviewers, both of which independently flagged the first two: - The `badges` branch was described in the past tense as already gone. It isn't, and it can't be until this merges — while the `badge` job still exists on main, the next code push would recreate it. Both the workflows README and the CHANGELOG now state that sequencing. - "for weeks" was wrong: #502 merged 2026-08-20, six days ago. - ci.yml's header claimed docs-preview was the only job holding a write scope, undercounting coverage's `code-quality: write` — which it holds while executing the PR tree. Pre-existing, but exactly the class of stale permission claim this PR set out to correct. - The "restoring a badge means restoring contents: write" note asserted an implication and then offered the alternative that disproves it. A shields endpoint hosted outside the repo needs no write scope at all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .github/workflows/README.md | 16 ++++++++++------ .github/workflows/ci.yml | 6 ++++-- CHANGELOG.md | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 2c59e74b..1bd4293f 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -151,12 +151,16 @@ gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)): - **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) - dropped the badge from the README and the pipeline ran on for weeks - publishing to nothing, so the job, `scripts/ci/publish-badge.sh`, the - `cov badge` subcommand, and the `badges` branch are gone. **Consequence - worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the - `badge` job was its only holder. Restoring a badge means restoring that - permission, so weigh it against a `contents: read` alternative first. + dropped the badge from the README, and for the six days until this landed + the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, + and the `cov badge` subcommand are gone. The orphan `badges` branch is + deleted separately once this lands: while the job still exists on main, the + next code push would just recreate it. **Consequence worth keeping**: + `ci.yml` now declares no `contents: write` anywhere — the `badge` job was + its only holder. Restoring the badge *in this shape* — publishing into the + repo — means restoring `contents: write` to a workflow that executes + PR-authored code. Prefer an endpoint hosted outside the repo, so a badge + costs no write scope at all. SDK (TS) coverage is gated by `make cov` but not yet published — extend with a `language: javascript` upload step when wanted. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43f8233a..9ec1ec51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,10 @@ on: # Manual re-run escape hatch — `gh workflow run CI --ref `. workflow_dispatch: -# Default-deny; each job below redeclares exactly what it needs. Only the -# docs-preview job (sticky PR comment) holds a write scope. +# Default-deny; each job below redeclares exactly what it needs. Two jobs +# hold a write scope: docs-preview (sticky PR comment — issues, +# pull-requests) and coverage (code-quality, for the PR coverage comment). +# No job holds contents: write. permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index b47595ba..186deb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Removed -- **The README coverage badge and its whole publishing pipeline** (`.github/workflows/ci.yml`, `scripts/ci/publish-badge.sh` (deleted), `scripts/cov/main.go`, `.github/workflows/README.md`, `.testcoverage.yml`, `AGENTS.md`): [#502](https://github.com/Wave-RF/WaveHouse/pull/502) rewrote the README badge row and dropped the Go Coverage badge, but nothing removed what fed it — so for weeks the non-gating `badge` job kept running on every main push, holding `ci.yml`'s only `contents: write`, publishing `coverage-go.json` to the orphan `badges` branch for a badge no page rendered. Retired rather than restored ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)): the `badge` job, its two producer steps in `coverage` (`cov badge` + the `go-coverage-badge` artifact), `scripts/ci/publish-badge.sh`, the `cov badge` subcommand (with `badgeData`/`badgeColor`), and the `badges` branch itself. **The gate is untouched** — `make cov`, `.testcoverage.yml`'s `threshold.total` and per-suite minima, and the GitHub Code Quality PR comments (the other half of [#133](https://github.com/Wave-RF/WaveHouse/issues/133)) all still run; only the published badge surface is gone. The security consequence is the reason to prefer retiring over restoring: **`ci.yml` now declares no `contents: write` in any job**, so the workflow that executes PR-authored code can no longer write to the repository under any path. Also fixed in passing: `timing`'s `needs` still listed `badge` (a dangling `needs` is a workflow-level error once the job is gone), and the "sole holder of `contents:write`" comments were repo-wide claims that were only ever true within `ci.yml` — `release.yml` and `publish-npm.yml` hold it too. +- **The README coverage badge and its whole publishing pipeline** (`.github/workflows/ci.yml`, `scripts/ci/publish-badge.sh` (deleted), `scripts/cov/main.go`, `.github/workflows/README.md`, `.testcoverage.yml`, `AGENTS.md`): [#502](https://github.com/Wave-RF/WaveHouse/pull/502) rewrote the README badge row and dropped the Go Coverage badge, but nothing removed what fed it — so for the six days until this landed the non-gating `badge` job kept running on every main push, holding `ci.yml`'s only `contents: write`, publishing `coverage-go.json` to the orphan `badges` branch for a badge no page rendered. Retired rather than restored ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)): the `badge` job, its two producer steps in `coverage` (`cov badge` + the `go-coverage-badge` artifact), `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand (with `badgeData`/`badgeColor`). The orphan `badges` branch is deleted separately once this lands — while the job still exists on main, the next code push would recreate it. **The gate is untouched** — `make cov`, `.testcoverage.yml`'s `threshold.total` and per-suite minima, and the GitHub Code Quality PR comments (the other half of [#133](https://github.com/Wave-RF/WaveHouse/issues/133)) all still run; only the published badge surface is gone. The security consequence is the reason to prefer retiring over restoring: **`ci.yml` now declares no `contents: write` in any job**, so the workflow that executes PR-authored code can no longer write to the repository under any path. Also fixed in passing: `timing`'s `needs` still listed `badge` (a dangling `needs` is a workflow-level error once the job is gone), and two permission comments were wrong: the "sole holder of `contents:write`" claims were repo-wide statements only ever true within `ci.yml` (`release.yml` and `publish-npm.yml` hold it too), and the workflow header claimed `docs-preview` was the only job with a write scope, undercounting `coverage`'s `code-quality: write` — which it holds while executing the PR tree. - **The Go Report Card badge stays gone** (no code change — [#502](https://github.com/Wave-RF/WaveHouse/pull/502) already removed the only reference): recorded here because [#509](https://github.com/Wave-RF/WaveHouse/issues/509) tracked it as collateral to restore. It isn't coming back — goreportcard.com is deprecated upstream, and the signal it graded (gofmt, vet, lint, misspell) is gated far more strictly by `make lint-go`/`golangci-lint` in CI. From 0cf7470692338a635ba6b3686e81cc1ea5ff6d9e Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 11:03:18 -0400 Subject: [PATCH 3/8] docs: unwrap the badge-retirement bullet to one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit flagged the new bullet in .github/workflows/README.md as hard-wrapped, and it was right. I initially pushed back because .github/.markdownlint.json sets "WH001": false — but that exclusion contradicts CONTRIBUTING.md, which promises the rule is enforced "everywhere", and AGENTS.md, which states it with no carve-out. The config is the thing that's wrong, not the finding. This commit fixes only the prose this PR introduced, so the change stays scoped to the badge retirement. Removing both WH001 exclusions (.github/ and .claude/) and reflowing the 53 hard-wrapped paragraphs they were hiding (292 WH001 violation lines, since the rule reports one per line) is tracked in #521. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .github/workflows/README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 1bd4293f..e89ff4ca 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -148,19 +148,7 @@ gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)): public-preview feature can never red `CI`. Fork PRs skip (no `code-quality` token, per GitHub's own guard). Renders only once the repo's *Settings → Security → Code quality* is enabled. -- **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). - A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an - orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) - dropped the badge from the README, and for the six days until this landed - the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, - and the `cov badge` subcommand are gone. The orphan `badges` branch is - deleted separately once this lands: while the job still exists on main, the - next code push would just recreate it. **Consequence worth keeping**: - `ci.yml` now declares no `contents: write` anywhere — the `badge` job was - its only holder. Restoring the badge *in this shape* — publishing into the - repo — means restoring `contents: write` to a workflow that executes - PR-authored code. Prefer an endpoint hosted outside the repo, so a badge - costs no write scope at all. +- **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) dropped the badge from the README, and for the six days until this landed the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand are gone. The orphan `badges` branch is deleted separately once this lands: while the job still exists on main, the next code push would just recreate it. **Consequence worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the `badge` job was its only holder. Restoring the badge *in this shape* — publishing into the repo — means restoring `contents: write` to a workflow that executes PR-authored code. Prefer an endpoint hosted outside the repo, so a badge costs no write scope at all. SDK (TS) coverage is gated by `make cov` but not yet published — extend with a `language: javascript` upload step when wanted. From fe5e19efdb50860bdf69fe1825a1483b356fa5a9 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 12:21:02 -0400 Subject: [PATCH 4/8] build(lint): enforce WH001 everywhere, drop vendored skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two path-scoped configs — .github/.markdownlint.json and .claude/.markdownlint.json — held nothing but "WH001": false, silently switching the no-hard-wrapped-prose rule off for CI docs and agent prompts ever since #489 introduced it. CONTRIBUTING.md tells contributors it is enforced everywhere, while AGENTS.md and the .markdownlint-cli2.jsonc header wrote up the carve-out — three descriptions of one rule, disagreeing. That cost a round trip on #520: a reviewer correctly flagged a hard-wrapped bullet, an agent pointed at "WH001": false for that path and pushed back, and the reviewer recorded a learning never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction. Both configs deleted; the 51 paragraphs they hid are joined (41 in .github/workflows/README.md, 10 in pm-triage/references/routine.md), mechanical joins only. The vendored PostHog skill goes too — 9 files, ~1,456 lines, job finished in #277, nothing references it, and it was the only file that would have needed a special-case exclusion, so removing it is what lets WH001 apply with no exception at all. Review then found four more things the exclusion had hidden: WH001 skips lines indented 4+ spaces as code, so three nested bullets were invisible to the autofix (unwrapped by hand) and the "a list item is joined as a unit" claim was wrong for nested items; docs-prose.sh named two lockstep copies of its denylist when there are three, missing the gating subagent's own prompt; the config header overstated WH002's scope; and the job graph omitted docs-deploy's suite needs edges. Closes #521 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .claude/.markdownlint.json | 4 - .../.posthog-wizard | 0 .../SKILL.md | 57 -- .../references/EXAMPLE.md | 809 ------------------ .../references/astro.md | 165 ---- .../references/basic-integration-1.0-begin.md | 56 -- .../references/basic-integration-1.1-edit.md | 36 - .../basic-integration-1.2-revise.md | 22 - .../basic-integration-1.3-conclude.md | 40 - .../references/identify-users.md | 271 ------ .../skills/pm-triage/references/routine.md | 39 +- .github/.markdownlint.json | 4 - .github/prompts/docs-review.md | 2 +- .github/workflows/README.md | 348 ++------ .markdownlint-cli2.jsonc | 11 +- AGENTS.md | 6 +- CHANGELOG.md | 4 + docs/src/content/docs/claude-code.md | 2 - docs/src/content/docs/development.md | 2 +- scripts/docs-prose.sh | 7 +- 20 files changed, 96 insertions(+), 1789 deletions(-) delete mode 100644 .claude/.markdownlint.json delete mode 100644 .claude/skills/integration-astro-view-transitions/.posthog-wizard delete mode 100644 .claude/skills/integration-astro-view-transitions/SKILL.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/EXAMPLE.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/astro.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md delete mode 100644 .claude/skills/integration-astro-view-transitions/references/identify-users.md delete mode 100644 .github/.markdownlint.json diff --git a/.claude/.markdownlint.json b/.claude/.markdownlint.json deleted file mode 100644 index 56df99ac..00000000 --- a/.claude/.markdownlint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../.markdownlint.json", - "WH001": false -} diff --git a/.claude/skills/integration-astro-view-transitions/.posthog-wizard b/.claude/skills/integration-astro-view-transitions/.posthog-wizard deleted file mode 100644 index e69de29b..00000000 diff --git a/.claude/skills/integration-astro-view-transitions/SKILL.md b/.claude/skills/integration-astro-view-transitions/SKILL.md deleted file mode 100644 index 9defe9ce..00000000 --- a/.claude/skills/integration-astro-view-transitions/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: integration-astro-view-transitions -description: PostHog integration for Astro with ClientRouter view transitions -metadata: - author: PostHog - version: 1.21.1 ---- - -# PostHog integration for Astro (View Transitions) - -This skill helps you add PostHog analytics to Astro (View Transitions) applications. - -## Workflow - -Follow these steps in order to complete the integration: - -1. `basic-integration-1.0-begin.md` - PostHog Setup - Begin ← **Start here** -2. `basic-integration-1.1-edit.md` - PostHog Setup - Edit -3. `basic-integration-1.2-revise.md` - PostHog Setup - Revise -4. `basic-integration-1.3-conclude.md` - PostHog Setup - Conclusion - -## Reference files - -- `references/EXAMPLE.md` - Astro (View Transitions) example project code -- `references/astro.md` - Astro - docs -- `references/identify-users.md` - Identify users - docs -- `references/basic-integration-1.0-begin.md` - PostHog setup - begin -- `references/basic-integration-1.1-edit.md` - PostHog setup - edit -- `references/basic-integration-1.2-revise.md` - PostHog setup - revise -- `references/basic-integration-1.3-conclude.md` - PostHog setup - conclusion - -The example project shows the target implementation pattern. Consult the documentation for API details. - -## Key principles - -- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. -- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. -- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. - -## Framework guidelines - -- Always use the is:inline directive on PostHog script tags to prevent Astro from processing them and causing TypeScript errors -- Use PUBLIC_prefix for client-side environment variables in Astro (e.g., PUBLIC_POSTHOG_PROJECT_TOKEN) -- Create a posthog.astro component in src/components/ for reusable initialization across pages -- Import the PostHog component in a Layout and wrap all pages with that layout -- Wrap PostHog initialization with a window.__posthog_initialized guard to prevent stack overflow during soft navigation -- Set capture_pageview option to 'history_change' for automatic pageview tracking during soft navigation -- Use the astro page-load event instead of just DOMContentLoaded to re-run scripts after soft navigation -- When a reverse proxy is configured, both /static/*AND /array/* must route to the assets origin (us-assets.i.posthog.com or eu-assets.i.posthog.com). - -## Identifying users - -Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. - -## Error tracking - -Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/.claude/skills/integration-astro-view-transitions/references/EXAMPLE.md b/.claude/skills/integration-astro-view-transitions/references/EXAMPLE.md deleted file mode 100644 index 56c5a23f..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/EXAMPLE.md +++ /dev/null @@ -1,809 +0,0 @@ -# PostHog Astro (View Transitions) Example Project - -Repository: -Path: basics/astro-view-transitions - ---- - -## README.md - -### PostHog Astro View Transitions Example - -This is an [Astro](https://astro.build/) example demonstrating PostHog integration with [View Transitions](https://docs.astro.build/en/guides/view-transitions/) (ClientRouter) for SPA-like navigation. - -It uses the PostHog web snippet with special handling to prevent stack overflow errors during soft navigation, and shows how to: - -- Initialize PostHog with an initialization guard for View Transitions -- Track pageviews automatically during soft navigation -- Identify users after login -- Track custom events from pages -- Capture errors via `posthog.captureException()` -- Reset PostHog state on logout - -## Features - -- **View Transitions**: Smooth client-side navigation with `` -- **Product analytics**: Track login and burrito consideration events -- **Automatic pageview tracking**: Uses `capture_pageview: 'history_change'` for soft navigation -- **Session replay**: Enabled via PostHog snippet configuration -- **Error tracking**: Manual error capture sent to PostHog -- **Simple auth flow**: Demo login using localStorage - -## Getting started - -### 1. Install dependencies - -```bash -npm install -# or -pnpm install -``` - -### 2. Configure environment variables - -Create a `.env` file in the project root: - -```bash -PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token -PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -``` - -Get your PostHog project token from your project settings in PostHog. - -### 3. Run the development server - -```bash -npm run dev -# or -pnpm dev -``` - -Open `http://localhost:4321` in your browser. - -## Project structure - -```text -src/ - components/ - posthog.astro # PostHog snippet WITH initialization guard - Header.astro # Navigation + logout, uses astro:page-load event - layouts/ - PostHogLayout.astro # Root layout with and PostHog - lib/ - auth.ts # Auth utilities (localStorage-based) - pages/ - index.astro # Login form, identifies user + captures 'user_logged_in' - burrito.astro # Burrito consideration demo, captures 'burrito_considered' - profile.astro # Profile + error tracking demo - styles/ - global.css # Global styles + view transition animations -``` - -## Key integration points - -### PostHog initialization with View Transitions (`src/components/posthog.astro`) - -When using Astro's View Transitions (ClientRouter), you **must** wrap the PostHog initialization with a guard to prevent stack overflow errors: - -```astro - -``` - -Without this guard, ClientRouter's soft navigation can re-execute the inline script during page transitions, causing a stack overflow error. - -The `capture_pageview: 'history_change'` option ensures pageviews are tracked automatically as users navigate between pages. - -### Layout with ClientRouter (`src/layouts/PostHogLayout.astro`) - -The layout includes Astro's ClientRouter for smooth page transitions: - -```astro ---- -import { ClientRouter } from 'astro:transitions'; -import PostHog from '../components/posthog.astro'; ---- - - - - - - ... - -``` - -### Handling View Transitions in scripts - -When using View Transitions, you need to set up event listeners after each page navigation: - -```javascript -function setupPage() { - // Your setup code here -} - -// Run on initial page load -document.addEventListener("DOMContentLoaded", setupPage); - -// Run after view transitions complete (for soft navigation) -document.addEventListener("astro:page-load", setupPage); -``` - -### User identification (`src/pages/index.astro`) - -After a successful "login", the app identifies the user and captures a login event: - -```javascript -window.posthog?.identify(username); -window.posthog?.capture("user_logged_in"); -``` - -### Event tracking (`src/pages/burrito.astro`) - -The burrito page tracks a custom event when a user "considers" the burrito: - -```javascript -window.posthog?.capture("burrito_considered", { - total_considerations: newCount, - username: currentUser, -}); -``` - -### Logout and session reset (`src/components/Header.astro`) - -On logout, both the local auth state and PostHog state are cleared: - -```javascript -window.posthog?.capture("user_logged_out"); -localStorage.removeItem("currentUser"); -window.posthog?.reset(); -``` - -## Scripts - -```bash -# Run dev server -npm run dev - -# Build for production -npm run build - -# Preview production build -npm run preview -``` - -## Learn more - -- [PostHog documentation](https://posthog.com/docs) -- [PostHog Astro guide](https://posthog.com/docs/libraries/astro) -- [Astro View Transitions](https://docs.astro.build/en/guides/view-transitions/) -- [Astro documentation](https://docs.astro.build/) - ---- - -## .env.example - -```example -PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token_here -PUBLIC_POSTHOG_HOST=https://us.i.posthog.com - -``` - ---- - -## astro.config.mjs - -```mjs -import { defineConfig } from "astro/config"; - -export default defineConfig({}); - -``` - ---- - -## src/components/Header.astro - -```astro ---- -// Header component with navigation and logout functionality -// Works with View Transitions by using data-astro-reload for logout ---- -
-
- -
- - Not logged in - -
-
-
- - - - - -``` - ---- - -## src/components/posthog.astro - -```astro ---- -// PostHog analytics snippet with View Transitions support -// Uses is:inline to prevent Astro from processing the script -// Includes initialization guard to prevent stack overflow with ClientRouter ---- - - -``` - ---- - -## src/layouts/PostHogLayout.astro - -```astro ---- -import { ClientRouter } from 'astro:transitions'; -import PostHog from '../components/posthog.astro'; -import Header from '../components/Header.astro'; -import '../styles/global.css'; - -interface Props { - title: string; -} - -const { title } = Astro.props; ---- - - - - - - - - {title} - - - - -
-
- -
- - - -``` - ---- - -## src/lib/auth.ts - -```ts -// Client-side auth utilities for localStorage-based authentication - -export interface User { - username: string; - burritoConsiderations: number; -} - -export function getCurrentUser(): User | null { - if (typeof window === "undefined") return null; - - const username = localStorage.getItem("currentUser"); - if (!username) return null; - - const considerations = parseInt( - localStorage.getItem("burritoConsiderations") || "0", - 10, - ); - - return { - username, - burritoConsiderations: considerations, - }; -} - -export function login(username: string, password: string): boolean { - if (!username || !password) return false; - - localStorage.setItem("currentUser", username); - // Initialize burrito considerations if not set - if (!localStorage.getItem("burritoConsiderations")) { - localStorage.setItem("burritoConsiderations", "0"); - } - - return true; -} - -export function logout(): void { - localStorage.removeItem("currentUser"); - localStorage.removeItem("burritoConsiderations"); -} - -export function incrementBurritoConsiderations(): number { - const current = parseInt( - localStorage.getItem("burritoConsiderations") || "0", - 10, - ); - const newCount = current + 1; - localStorage.setItem("burritoConsiderations", newCount.toString()); - return newCount; -} - -``` - ---- - -## src/pages/burrito.astro - -```astro ---- -import PostHogLayout from '../layouts/PostHogLayout.astro'; ---- - -
-

Burrito consideration zone

-

Take a moment to truly consider the potential of burritos.

- -
- - - -
- -
-

Consideration stats

-

Total considerations: 0

-
-
-
- - - -``` - ---- - -## src/pages/index.astro - -```astro ---- -import PostHogLayout from '../layouts/PostHogLayout.astro'; ---- - -
- - -
-

Welcome to Burrito Consideration App

-

Please sign in to begin your burrito journey

- -
-
- - -
- -
- - -
- - - - -
- -

- Note: This is a demo app. Use any username and password to sign in. -

-
-
-
- - - -``` - ---- - -## src/pages/profile.astro - -```astro ---- -import PostHogLayout from '../layouts/PostHogLayout.astro'; ---- - -
-

User Profile

- -
-

Your Information

-

Username:

-

Burrito Considerations: 0

-
- -
-

Your Burrito Journey

-

-
- -
-

Error Tracking Demo

-

Click the button below to trigger a test error and send it to PostHog:

- - -
-
-
- - - -``` - ---- diff --git a/.claude/skills/integration-astro-view-transitions/references/astro.md b/.claude/skills/integration-astro-view-transitions/references/astro.md deleted file mode 100644 index a9a2ba14..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/astro.md +++ /dev/null @@ -1,165 +0,0 @@ -# Astro - Docs - -PostHog makes it easy to get data about traffic and usage of your [Astro](https://astro.build/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more. - -This guide walks you through integrating PostHog into your Astro app using the [JavaScript Web SDK](/docs/libraries/js.md). - -## Beta: integration via LLM - -Install PostHog for Astro in seconds with our wizard by running this prompt with [LLM coding agents](/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal. - -`npx @posthog/wizard@latest` - -[Learn more](/wizard.md) - -Or, to integrate manually, continue with the rest of this guide. - -## Installation - -In your `src/components` folder, create a `posthog.astro` file: - -Terminal - -PostHog AI - -```bash -cd ./src/components -# or 'cd ./src && mkdir components && cd ./components' if your components folder doesnt exist -touch posthog.astro -``` - -In this file, add your `Web snippet` which you can find in [your project settings](https://us.posthog.com/settings/project#snippet). Be sure to include the `is:inline` directive [to prevent Astro from processing it](https://docs.astro.build/en/guides/client-side-scripts/#opting-out-of-processing), or you will get Typescript and build errors that property 'posthog' does not exist on type 'Window & typeof globalThis'. - -posthog.astro - -PostHog AI - -```javascript - -``` - -### Using with Astro's view transitions (ClientRouter) - -If you've opted in to Astro's `` component for client-side navigation, you'll need to add an initialization guard to prevent PostHog from running multiple times during page transitions. - -Update your `posthog.astro` file to wrap the snippet with a check: - -posthog.astro - -PostHog AI - -```javascript ---- -// src/components/posthog.astro ---- - -``` - -Without this guard, `ClientRouter`'s soft navigation can re-execute the inline script during page transitions, causing a stack overflow error. The `capture_pageview: 'history_change'` option ensures pageviews are tracked automatically as users navigate. - -The next step is to a create a [Layout](https://docs.astro.build/en/core-concepts/layouts/) where we will use `posthog.astro`. Create a new file `PostHogLayout.astro` in your `src/layouts` folder: - -Terminal - -PostHog AI - -```bash -cd .. && cd .. # move back to your base directory if you're still in src/components/posthog.astro -cd ./src/layouts -# or 'cd ./src && mkdir layouts && cd ./layouts' if your layouts folder doesn't exist yet -touch PostHogLayout.astro -``` - -Add the following code to `PostHogLayout.astro`: - -PostHogLayout.astro - -PostHog AI - -```javascript ---- -import PostHog from '../components/posthog.astro' ---- - - - -``` - -Lastly, update `index.astro` to wrap your existing app components with the new Layout: - -index.astro - -PostHog AI - -```javascript ---- -import PostHogLayout from '../layouts/PostHogLayout.astro'; ---- - - - -``` - -## Identifying users - -> **Identifying users is required.** Call `posthog.identify('your-user-id')` after login to link events to a known user. This is what connects frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), and [error tracking](/docs/error-tracking.md) to the same person — and lets backend events link back too. -> -> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. - -Set up a reverse proxy (recommended) - -We recommend [setting up a reverse proxy](/docs/advanced/proxy.md), so that events are less likely to be intercepted by tracking blockers. - -We have our [own managed reverse proxy service](/docs/advanced/proxy/managed-reverse-proxy.md), which is free for all PostHog Cloud users, routes through our infrastructure, and makes setting up your proxy easy. - -If you don't want to use our managed service then there are several other options for creating a reverse proxy, including using [Cloudflare](/docs/advanced/proxy/cloudflare.md), [AWS Cloudfront](/docs/advanced/proxy/cloudfront.md), and [Vercel](/docs/advanced/proxy/vercel.md). - -Grouping products in one project (recommended) - -If you have multiple customer-facing products (e.g. a marketing website + mobile app + web app), it's best to install PostHog on them all and [group them in one project](/docs/settings/projects.md). - -This makes it possible to track users across their entire journey (e.g. from visiting your marketing website to signing up for your product), or how they use your product across multiple platforms. - -Add IPs to Firewall/WAF allowlists (recommended) - -For certain features like [heatmaps](/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site. - -**EU**: `3.75.65.221`, `18.197.246.42`, `3.120.223.253` - -**US**: `44.205.89.55`, `52.4.194.122`, `44.208.188.173` - -These are public, stable IPs used by PostHog services (e.g., Celery tasks for snapshots). - -## Next steps - -For any technical questions for how to integrate specific PostHog features into Astro (such as analytics, feature flags, A/B testing, surveys, etc.), have a look at our [JavaScript Web SDK docs](/docs/libraries/js/features.md). - -Alternatively, the following tutorials can help you get started: - -- [How to set up Astro analytics, feature flags, and more](/tutorials/astro-analytics.md) -- [How to set up A/B tests in Astro](/tutorials/astro-ab-tests.md) -- [How to set up surveys in Astro](/tutorials/astro-surveys.md) - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better diff --git a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md b/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md deleted file mode 100644 index a315b4e2..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.0-begin.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: PostHog Setup - Begin -description: Start the event tracking setup process by analyzing the project and creating an event tracking plan ---- - -We're making an event tracking plan for this project. - -This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. - -## Task list - -As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. - -It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. - -Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. - -Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. - -From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. - -Look for opportunities to track client-side events. - -**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: - -- Payment/checkout completion -- Webhook handlers -- Authentication endpoints - -Do not skip server-side events - they capture actions that cannot be tracked client-side. - -Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. - -Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. - -As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. - -## Status - -Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: - -[STATUS] Checking project structure. - -Status to report in this phase: - -- Checking project structure -- Verifying PostHog dependencies -- Generating events based on project - -## Abort statuses - -If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. - ---- - -**Upon completion, continue with:** [basic-integration-1.1-edit.md](basic-integration-1.1-edit.md) diff --git a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md b/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md deleted file mode 100644 index b5d6b49c..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.1-edit.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: PostHog Setup - Edit -description: Implement PostHog event tracking in the identified files, following best practices and the example project ---- - -For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. - -Use environment variables for PostHog keys. Do not hardcode PostHog keys. - -If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. - -For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. - -Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. - -Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. - -It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. - -You should also add PostHog exception capture error tracking to these files where relevant. - -Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. - -Remember the documentation and example project resources you were provided at the beginning. Read them now. - -## Status - -Status to report in this phase: - -- Inserting PostHog capture code -- A status message for each file whose edits you are planning, including a high level summary of changes -- A status message for each file you have edited - ---- - -**Upon completion, continue with:** [basic-integration-1.2-revise.md](basic-integration-1.2-revise.md) diff --git a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md b/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md deleted file mode 100644 index 285ee734..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.2-revise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: PostHog Setup - Revise -description: Review and fix any errors in the PostHog integration implementation ---- - -Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. - -Ensure that any components created were actually used. - -Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. - -## Status - -Status to report in this phase: - -- Finding and correcting errors -- Report details of any errors you fix -- Linting, building and prettying - ---- - -**Upon completion, continue with:** [basic-integration-1.3-conclude.md](basic-integration-1.3-conclude.md) diff --git a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md b/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md deleted file mode 100644 index 2bd36227..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/basic-integration-1.3-conclude.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: PostHog Setup - Conclusion -description: Review and fix any errors in the PostHog integration implementation ---- - -Use the PostHog MCP to create a new dashboard named "Analytics basics (wizard)" based on the events created here. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. - -Search for a file called `.posthog-events.json` and read it for available events. - -Do not spawn subagents. - -Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, along with a list of links for the dashboard and insights created. Follow this format: - - -# PostHog post-wizard report - -The wizard has completed a deep integration of your project. [Detailed summary of changes] - -[table of events/descriptions/files] - -## Next steps - -We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: - -[links] - -### Agent skill - -We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. - - - -Upon completion, remove .posthog-events.json. - -## Status - -Status to report in this phase: - -- Configured dashboard: [insert PostHog dashboard URL] -- Created setup report: [insert full local file path] diff --git a/.claude/skills/integration-astro-view-transitions/references/identify-users.md b/.claude/skills/integration-astro-view-transitions/references/identify-users.md deleted file mode 100644 index 13defe51..00000000 --- a/.claude/skills/integration-astro-view-transitions/references/identify-users.md +++ /dev/null @@ -1,271 +0,0 @@ -# Identify users - Docs - -Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. - -This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. - -However, in the frontend of a [web](/docs/libraries/js/features.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/features.md#capturing-anonymous-events). - -To link events to specific users, call `identify`: - -PostHog AI - -## Web - -```javascript -posthog.identify( - 'distinct_id', // Replace 'distinct_id' with your user's unique identifier - { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties -); -``` - -## Android - -```kotlin -PostHog.identify( - distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier - // optional: set additional person properties - userProperties = mapOf( - "name" to "Max Hedgehog", - "email" to "max@hedgehogmail.com" - ) -) -``` - -## iOS - -```swift -PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier - userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties -``` - -## React Native - -```jsx -posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier - email: 'max@hedgehogmail.com', // optional: set additional person properties - name: 'Max Hedgehog' -}) -``` - -## Dart - -```dart -await Posthog().identify( - userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier - userProperties: { - email: "max@hedgehogmail.com", // optional: set additional person properties - name: "Max Hedgehog" -}); -``` - -Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. - -Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. - -## How identify works - -When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. - -Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. - -By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. - -Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. - -This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. - -Using identify in the backend - -Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. - -## Best practices when using `identify` - -### 1\. Call `identify` as soon as you're able to - -In your frontend, you should call `identify` as soon as you're able to. - -Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. - -This ensures that events sent during your users' sessions are correctly associated with them. - -You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. - -If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. - -### 2\. Use unique strings for distinct IDs - -If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: - -- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. -- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. - -PostHog also has built-in protections to stop the most common distinct ID mistakes. - -### 3\. Reset after logout - -If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. - -This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. - -**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** - -You can do that like so: - -PostHog AI - -### Web - -```javascript -posthog.reset() -``` - -### iOS - -```swift -PostHogSDK.shared.reset() -``` - -### Android - -```kotlin -PostHog.reset() -``` - -### React Native - -```jsx -posthog.reset() -``` - -### Dart - -```dart -Posthog().reset() -``` - -If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: - -Web - -PostHog AI - -```javascript -posthog.reset(true) -``` - -### 4\. Person profiles and properties - -You'll notice that one of the parameters in the `identify` method is a `properties` object. - -This enables you to set [person properties](/docs/product-analytics/person-properties.md). - -Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. - -Person properties can also be set being adding a `$set` property to a event `capture` call. - -See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. - -### 5\. Use deep links between platforms - -We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able-to), typically when a user signs up or logs in. - -This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: - -- Onboarding and signup flows before authentication. -- Unauthenticated web pages redirecting to authenticated mobile apps. -- Authenticated web apps prompting an app download. - -In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. - -1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. -2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. -3. When the user is redirected to the app, parse the deep link and handle the following cases: - -- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/features.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. -- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/features.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. - -As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. - -Here's an example implementation for handling deep links from web to mobile: - -PostHog AI - -#### iOS - -```swift -import PostHog -class DeepLinkIdentityManager { - static let shared = DeepLinkIdentityManager() - // MARK: - Deep Link Received - func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { - guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? - .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { - return - } - if isAuthenticatedOnMobile { - // The mobile app already knows the current user. - // Alias the incoming web distinct ID to that user. - PostHogSDK.shared.alias(webDistinctId) - } else { - // Reuse the web distinct ID until login on mobile. - PostHogSDK.shared.identify(webDistinctId) - } - } - // MARK: - Login/Signup - func handleLogin(canonicalUserId: String) { - // Switch from the web distinct ID (or a mobile anon ID) - // to your canonical user ID. - PostHogSDK.shared.identify(canonicalUserId) - // Set user properties, track signup event, etc. - } - func handleLogout() { - PostHogSDK.shared.reset() - } -} -``` - -#### Android - -```kotlin -import android.net.Uri -import com.posthog.PostHog -object DeepLinkIdentityManager { - // Deep Link Received - fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { - val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return - if (isAuthenticatedOnMobile) { - // The mobile app already knows the current user. - // Alias the incoming web distinct ID to that user. - PostHog.alias(webDistinctId) - } else { - // Reuse the web distinct ID until login on mobile. - PostHog.identify(webDistinctId) - } - } - // Login/Signup - fun handleLogin(canonicalUserId: String) { - // Switch from the web distinct ID (or a mobile anon ID) - // to your canonical user ID. - PostHog.identify(canonicalUserId) - // Set user properties, track signup event, etc. - } - fun handleLogout() { - PostHog.reset() - } -} -``` - -## Further reading - -- [Identifying users docs](/docs/product-analytics/identify.md) -- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) -- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) - -### Community questions - -Ask a question - -### Was this page useful? - -HelpfulCould be better diff --git a/.claude/skills/pm-triage/references/routine.md b/.claude/skills/pm-triage/references/routine.md index 0794c738..81718752 100644 --- a/.claude/skills/pm-triage/references/routine.md +++ b/.claude/skills/pm-triage/references/routine.md @@ -1,30 +1,18 @@ # Running `/pm-triage all` as a local routine -The scheduled form of pm-triage: a daily local pass that reconciles status, re-checks the -backlog, and sweeps new TODOs — applying safe changes itself and surfacing risky ones. Scope -`all`, default run-mode `safe-auto`. Runs on your Mac (only when it's on — that's fine), set up -as a routine in the **Claude desktop app**. It is **not** a cloud routine (this repo's gates, -`gh` auth, and `make ci` deps don't exist in the cloud) and **not** a launchd / `claude -p` job. +The scheduled form of pm-triage: a daily local pass that reconciles status, re-checks the backlog, and sweeps new TODOs — applying safe changes itself and surfacing risky ones. Scope `all`, default run-mode `safe-auto`. Runs on your Mac (only when it's on — that's fine), set up as a routine in the **Claude desktop app**. It is **not** a cloud routine (this repo's gates, `gh` auth, and `make ci` deps don't exist in the cloud) and **not** a launchd / `claude -p` job. ## State: a local-only orphan branch -State lives on an **orphan branch `pm-triage-state`** — shares no history with main (its own -thing), checked out in its own worktree at `
/.worktrees/pm-triage-state`, **never pushed -by default** (private; teammates can't see an unpushed branch). `scripts/state.sh` manages it. -Why this over the alternatives: +State lives on an **orphan branch `pm-triage-state`** — shares no history with main (its own thing), checked out in its own worktree at `
/.worktrees/pm-triage-state`, **never pushed by default** (private; teammates can't see an unpushed branch). `scripts/state.sh` manages it. Why this over the alternatives: - **vs. a GitHub issue:** no API round-trips, no notifications, no teammate comments — the logic stays private. - **vs. `.git/pm-triage/`:** a real branch is legible and idiomatic, and it's versioned. - **vs. a plain gitignored dir:** untracked files are *per-worktree*, so a `.claude/pm-triage/` would fork across your 7 worktrees. The orphan worktree is one registered path, identical from everywhere. -You get a **versioned audit trail** — each meaningful run commits, so `git -C log` -shows when it changed what — and an easy **future sync** path: flip on `git push -u origin -pm-triage-state` to share across machines (left OFF for now). +You get a **versioned audit trail** — each meaningful run commits, so `git -C log` shows when it changed what — and an easy **future sync** path: flip on `git push -u origin pm-triage-state` to share across machines (left OFF for now). -**No branch-switching, ever.** The skill never `git checkout`s between main and the state -branch in one tree (that swaps your whole working dir and breaks on uncommitted work). It -navigates by **path**: file I/O in the state worktree, and code/TODO reads against `origin/main` -by ref (`git fetch`, then `git grep` / `git diff ..origin/main`). +**No branch-switching, ever.** The skill never `git checkout`s between main and the state branch in one tree (that swaps your whole working dir and breaks on uncommitted work). It navigates by **path**: file I/O in the state worktree, and code/TODO reads against `origin/main` by ref (`git fetch`, then `git grep` / `git diff ..origin/main`). Files in the state worktree: @@ -36,8 +24,7 @@ Files in the state worktree: ## safe-auto is the default (every invocation) -The safe tier just happens — no proposal step — whether you run it by hand or it fires on -schedule. Only the risky tier flexes: +The safe tier just happens — no proposal step — whether you run it by hand or it fires on schedule. Only the risky tier flexes: - **You're watching** (manual `/pm-triage …`): it presents risky items and asks you right there. - **Unattended** (the routine): it appends them to `pending.md` for you to review later. @@ -50,22 +37,17 @@ schedule. Only the risky tier flexes: ## Cadence -Daily, weekday mornings. In the routine's schedule pick an off-`:00` minute (e.g. 9:07) so -you're not landing on the same instant as everyone else. +Daily, weekday mornings. In the routine's schedule pick an off-`:00` minute (e.g. 9:07) so you're not landing on the same instant as everyone else. ## Set it up in the Claude desktop app -1. Open the WaveHouse repo as a local project in the desktop app, so the routine has the repo, - your keychain'd `gh`, and the `.claude/` skill. +1. Open the WaveHouse repo as a local project in the desktop app, so the routine has the repo, your keychain'd `gh`, and the `.claude/` skill. 2. Create a routine / scheduled task on a weekday-morning schedule and paste the prompt below. -3. **On the first run, verify:** it read the skill (`SKILL.md`), `gh auth status` is good, and it - created the state worktree. Approve the `gh` / `git` / file tools it needs — or pre-grant them: +3. **On the first run, verify:** it read the skill (`SKILL.md`), `gh auth status` is good, and it created the state worktree. Approve the `gh` / `git` / file tools it needs — or pre-grant them: ### Permissions (so an unattended run doesn't stop on a prompt) -Pre-grant the reads/writes in **`.claude/settings.local.json`** (gitignored, per-user — *not* -the shared `settings.json`). The shared `deny` list still blocks the dangerous ops, and `deny` -beats `allow`: +Pre-grant the reads/writes in **`.claude/settings.local.json`** (gitignored, per-user — *not* the shared `settings.json`). The shared `deny` list still blocks the dangerous ops, and `deny` beats `allow`: ```jsonc // .claude/settings.local.json (gitignored — personal) @@ -85,8 +67,7 @@ beats `allow`: } ``` -(Rules are prefix matches on the exact command string; the script rules assume the -`bash .claude/skills/pm-triage/scripts/.sh …` form the prompt uses.) +(Rules are prefix matches on the exact command string; the script rules assume the `bash .claude/skills/pm-triage/scripts/.sh …` form the prompt uses.) ## Dry run first (writes nothing) diff --git a/.github/.markdownlint.json b/.github/.markdownlint.json deleted file mode 100644 index 56df99ac..00000000 --- a/.github/.markdownlint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../.markdownlint.json", - "WH001": false -} diff --git a/.github/prompts/docs-review.md b/.github/prompts/docs-review.md index c274c7c8..1784e025 100644 --- a/.github/prompts/docs-review.md +++ b/.github/prompts/docs-review.md @@ -1,6 +1,6 @@ You are reviewing the **documentation** of the WaveHouse project — the prose itself, and whether it kept up with the code; not the code's correctness. Read AGENTS.md at the repo root first: §Documentation Sync maps each code area to the docs that describe it, §SDK Sync covers the client, and the architecture/config context tells you what the docs *should* say. -**Scope** is the canonical docs-prose set resolved by `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, `PERF-CLAIMS-REVIEW.md`, and `docs/posthog-setup-report.md` (frozen wizard artifact). That is the Astro Starlight site under `docs/src/content/docs/` (`.md` + `index.mdx`) **plus** the user-facing governance docs — `README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md` — and any doc added later (new files are covered automatically). `CODE_OF_CONDUCT.md` and `SUPPORT.md` are mostly boilerplate: only deep-review them when they changed or when a material change elsewhere warrants it. +**Scope** is the canonical docs-prose set resolved by `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, and `PERF-CLAIMS-REVIEW.md`. That is the Astro Starlight site under `docs/src/content/docs/` (`.md` + `index.mdx`) **plus** the user-facing governance docs — `README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md` — and any doc added later (new files are covered automatically). `CODE_OF_CONDUCT.md` and `SUPPORT.md` are mostly boilerplate: only deep-review them when they changed or when a material change elsewhere warrants it. This review **complements** the deterministic layers that already run — do **not** duplicate them: diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e89ff4ca..ba441978 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,15 +1,6 @@ # CI architecture -How `ci.yml` is shaped and why. This is the canonical reference — the -workflow file's comments only explain what's local to a step, and -[development.md](../../docs/src/content/docs/development.md) carries the -contributor-facing summary. Wall-clock for a full PR run: **~3m15s push → -all green** (e2e ~170s is the long pole; the `coverage` job overlaps its -setup with the suites and merges within ~10s of e2e finishing, then the -~4s aggregator. Reliable now that the variable Cloudflare `docs-preview` -deploy is non-gating — only `docs-build` gates. Was 5m54s before the -2026-06 reshape, and ~4m when the -coverage job still serialized its setup via `needs`). +How `ci.yml` is shaped and why. This is the canonical reference — the workflow file's comments only explain what's local to a step, and [development.md](../../docs/src/content/docs/development.md) carries the contributor-facing summary. Wall-clock for a full PR run: **~3m15s push → all green** (e2e ~170s is the long pole; the `coverage` job overlaps its setup with the suites and merges within ~10s of e2e finishing, then the ~4s aggregator. Reliable now that the variable Cloudflare `docs-preview` deploy is non-gating — only `docs-build` gates. Was 5m54s before the 2026-06 reshape, and ~4m when the coverage job still serialized its setup via `needs`). ## The graph @@ -35,148 +26,47 @@ graph TB changes --> deploy lint --> deploy docsbuild --> deploy + unit --> deploy + integration --> deploy + e2e --> deploy coverage --> deploy ``` -Solid arrows are `needs` edges. Dotted arrows are **artifact polls** -(invariant 2): the `coverage` job starts off `changes` alone and polls -for the suites' fragments rather than `needs`-ing the suites, so its setup -overlaps them. +Solid arrows are `needs` edges. Dotted arrows are **artifact polls** (invariant 2): the `coverage` job starts off `changes` alone and polls for the suites' fragments rather than `needs`-ing the suites, so its setup overlaps them. ## Design invariants Break one of these knowingly or not at all. -1. **The aggregator job named `CI` is the only required status check.** - The `main branch protection` ruleset requires `CI` and nothing else. - The aggregator fails on any `failure`/`cancelled` need and treats - `skipped` as passing — so path-filtered jobs (docs-only PRs skip the - Go suites) and event-filtered jobs (title on pushes, deploys on PRs) - never orphan the required check, and adding/renaming jobs never - requires a ruleset edit. Consequence: every job that must gate merges - **must be in the aggregator's `needs` list**. Two jobs are deliberately - non-gating and excluded: `timing` (advisory wall-clock table) and - `docs-preview` (the convenience Cloudflare preview deploy — `docs-build` - already validates the build and *is* a need, so only the build gates; - the preview deploy reports its own "Docs preview" check but, slow or - failed, never delays or reds `CI`). - -2. **A dedicated `coverage` job applies the consolidated gate, polling — - not `needs`-ing — the suites.** Each suite (`unit`, `integration`, - `e2e`) runs with `COV_DEFER=1` and uploads a `coverage-` - fragment; the `coverage` job runs `make cov` (merge + every threshold - gate) over all three — exactly like local `make ci`'s final step. - Keeping it a separate job (not folded into e2e's tail) decouples the - gate result from the e2e suite's pass/fail. Crucially it is - `needs: changes` **only, not the suites**: a `needs` edge is a - *scheduling* barrier — GitHub won't pick up a runner, check out, restore - caches, or `pnpm install` until the needed jobs finish — so needing the - suites would serialize this job's ~50s of setup onto the critical path - after the last suite, for nothing (the setup doesn't depend on their - results). Instead it starts at run creation, runs its setup in parallel - with the suites, and blocks only at the merge by polling for the three - fragments with [`scripts/ci/wait-artifact.sh`](../../scripts/ci/wait-artifact.sh) - (fails fast if a producer concluded without producing). Tail on the - critical path: ~10s, not ~50s. **The aggregator and `docs-deploy` must - keep `coverage` *and* every suite in their `needs`** — the suites - directly (a suite failure must red the gate even though `coverage` - no longer needs them), and `coverage` (else a coverage-gate failure - wouldn't block merge or a prod deploy). - -3. **e2e builds its own inputs and mirrors local `make test-e2e`.** It - compiles the SDK dist + cover binary itself (`make -j test-e2e`, warm - per-suffix cache) rather than waiting on a builder job, and runs the - suite exactly as a developer does — one orchestrator, one ClickHouse - testcontainer, sequential files. The ClickHouse image pulls in the - background while caches restore (also in the integration job). - -4. **One change classifier, split into a pure core + a CI wrapper.** The - pure allowlist — file list on stdin ⇒ `code`/`docs` — lives in - [`scripts/classify-paths.sh`](../../scripts/classify-paths.sh), - dependency-free and unit-tested by - [`scripts/classify-paths.test.sh`](../../scripts/classify-paths.test.sh) - (`make test-classify-paths`, a `verify` leaf) so the allowlists can't - silently regress. The `changes` job runs the thin wrapper - [`scripts/ci/classify-changes.sh`](../../scripts/ci/classify-changes.sh), - which adds the CI-only policy (API file-list fetch + fail-closed: - pushes, dispatches, API hiccups ⇒ `code=true`) on top. Keeping the core - pure means the local git hooks can share it (`git diff --name-only | - scripts/classify-paths.sh`). The `code`/`docs` outputs gate the suites - and docs jobs — gate on these, never on workflow-level `paths:` - filters, which would orphan the required check (invariant 1). - -5. **Trust domains.** Jobs that can reach deploy secrets (`docs-preview`, - `docs-deploy`) check out **trusted `main`** and execute only files - resolved from it — wrangler, the worker source, `wrangler.jsonc`, and - any `scripts/ci/*.sh` they call ([#305](https://github.com/Wave-RF/WaveHouse/issues/305)). - The only PR-derived input they touch is the static `docs-dist` - artifact, consumed as data. Inline `run:` blocks in those jobs are - acceptable (the workflow file itself is the reviewed surface); - PR-tree *files* are not. Everything else (suites, lint, docs-build) - runs the PR tree with no secrets beyond a read-mostly `GITHUB_TOKEN`. - Fork PRs: secrets are absent and `docs-preview` skips itself. - -6. **`ci.yml`'s caches are owned end-to-end by `setup-env`** - ([.github/actions/setup-env](../actions/setup-env/action.yml)): each - cache is a nested `actions/cache` step that restores inline and saves - automatically at job end on an exact-key miss. No save steps in - `ci.yml`. Trade-offs accepted: failed jobs don't save (restore-keys - cushion the next run), and concurrent same-key misses produce benign - "already exists" warnings. **Two cache steps live outside it**, both in - `publish-dev.yml`, because that workflow doesn't use `setup-env` at all - (it runs GoReleaser, not the test suites): a bare `actions/cache` owning - the release build cache, and an `actions/cache/restore` that *reads* - `gomod-v1` and owns nothing. Those two are the only `actions/cache*` - steps outside the composite — keep it that way. A workflow that needs - the shared module tree reads it restore-only; writing it belongs to the - `ci.yml` jobs that run a full `go mod download`. +1. **The aggregator job named `CI` is the only required status check.** The `main branch protection` ruleset requires `CI` and nothing else. The aggregator fails on any `failure`/`cancelled` need and treats `skipped` as passing — so path-filtered jobs (docs-only PRs skip the Go suites) and event-filtered jobs (title on pushes, deploys on PRs) never orphan the required check, and adding/renaming jobs never requires a ruleset edit. Consequence: every job that must gate merges **must be in the aggregator's `needs` list**. Two jobs are deliberately non-gating and excluded: `timing` (advisory wall-clock table) and `docs-preview` (the convenience Cloudflare preview deploy — `docs-build` already validates the build and *is* a need, so only the build gates; the preview deploy reports its own "Docs preview" check but, slow or failed, never delays or reds `CI`). + +2. **A dedicated `coverage` job applies the consolidated gate, polling — not `needs`-ing — the suites.** Each suite (`unit`, `integration`, `e2e`) runs with `COV_DEFER=1` and uploads a `coverage-` fragment; the `coverage` job runs `make cov` (merge + every threshold gate) over all three — exactly like local `make ci`'s final step. Keeping it a separate job (not folded into e2e's tail) decouples the gate result from the e2e suite's pass/fail. Crucially it is `needs: changes` **only, not the suites**: a `needs` edge is a *scheduling* barrier — GitHub won't pick up a runner, check out, restore caches, or `pnpm install` until the needed jobs finish — so needing the suites would serialize this job's ~50s of setup onto the critical path after the last suite, for nothing (the setup doesn't depend on their results). Instead it starts at run creation, runs its setup in parallel with the suites, and blocks only at the merge by polling for the three fragments with [`scripts/ci/wait-artifact.sh`](../../scripts/ci/wait-artifact.sh) (fails fast if a producer concluded without producing). Tail on the critical path: ~10s, not ~50s. **The aggregator and `docs-deploy` must keep `coverage` *and* every suite in their `needs`** — the suites directly (a suite failure must red the gate even though `coverage` no longer needs them), and `coverage` (else a coverage-gate failure wouldn't block merge or a prod deploy). + +3. **e2e builds its own inputs and mirrors local `make test-e2e`.** It compiles the SDK dist + cover binary itself (`make -j test-e2e`, warm per-suffix cache) rather than waiting on a builder job, and runs the suite exactly as a developer does — one orchestrator, one ClickHouse testcontainer, sequential files. The ClickHouse image pulls in the background while caches restore (also in the integration job). + +4. **One change classifier, split into a pure core + a CI wrapper.** The pure allowlist — file list on stdin ⇒ `code`/`docs` — lives in [`scripts/classify-paths.sh`](../../scripts/classify-paths.sh), dependency-free and unit-tested by [`scripts/classify-paths.test.sh`](../../scripts/classify-paths.test.sh) (`make test-classify-paths`, a `verify` leaf) so the allowlists can't silently regress. The `changes` job runs the thin wrapper [`scripts/ci/classify-changes.sh`](../../scripts/ci/classify-changes.sh), which adds the CI-only policy (API file-list fetch + fail-closed: pushes, dispatches, API hiccups ⇒ `code=true`) on top. Keeping the core pure means the local git hooks can share it (`git diff --name-only | scripts/classify-paths.sh`). The `code`/`docs` outputs gate the suites and docs jobs — gate on these, never on workflow-level `paths:` filters, which would orphan the required check (invariant 1). + +5. **Trust domains.** Jobs that can reach deploy secrets (`docs-preview`, `docs-deploy`) check out **trusted `main`** and execute only files resolved from it — wrangler, the worker source, `wrangler.jsonc`, and any `scripts/ci/*.sh` they call ([#305](https://github.com/Wave-RF/WaveHouse/issues/305)). The only PR-derived input they touch is the static `docs-dist` artifact, consumed as data. Inline `run:` blocks in those jobs are acceptable (the workflow file itself is the reviewed surface); PR-tree *files* are not. Everything else (suites, lint, docs-build) runs the PR tree with no secrets beyond a read-mostly `GITHUB_TOKEN`. Fork PRs: secrets are absent and `docs-preview` skips itself. + +6. **`ci.yml`'s caches are owned end-to-end by `setup-env`** ([.github/actions/setup-env](../actions/setup-env/action.yml)): each cache is a nested `actions/cache` step that restores inline and saves automatically at job end on an exact-key miss. No save steps in `ci.yml`. Trade-offs accepted: failed jobs don't save (restore-keys cushion the next run), and concurrent same-key misses produce benign "already exists" warnings. **Two cache steps live outside it**, both in `publish-dev.yml`, because that workflow doesn't use `setup-env` at all (it runs GoReleaser, not the test suites): a bare `actions/cache` owning the release build cache, and an `actions/cache/restore` that *reads* `gomod-v1` and owns nothing. Those two are the only `actions/cache*` steps outside the composite — keep it that way. A workflow that needs the shared module tree reads it restore-only; writing it belongs to the `ci.yml` jobs that run a full `go mod download`. ## Coverage publishing -The `coverage` job both **gates** (`make cov` against the floors in -`.testcoverage.yml`) and **publishes** — independent concerns, and only the -gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)): - -- **Per-run job summary** — the merged per-package func table on every run's - Summary page. -- **PR comment (GitHub Code Quality)** — on same-repo PRs and main pushes the - job converts the merged Go profile to Cobertura (`go tool gocover-cobertura`, - `-ignore-dirs` mirroring `.testcoverage.yml`'s global excludes so the % tracks - the merged-total gate) and uploads it via `actions/upload-code-coverage`; the - `github-code-quality[bot]` posts the aggregate + per-file diff-vs-main - comment. **Non-gating**: the upload is `continue-on-error`, so this - public-preview feature can never red `CI`. Fork PRs skip (no `code-quality` - token, per GitHub's own guard). Renders only once the repo's *Settings → - Security → Code quality* is enabled. +The `coverage` job both **gates** (`make cov` against the floors in `.testcoverage.yml`) and **publishes** — independent concerns, and only the gate blocks merges ([#133](https://github.com/Wave-RF/WaveHouse/issues/133)): + +- **Per-run job summary** — the merged per-package func table on every run's Summary page. +- **PR comment (GitHub Code Quality)** — on same-repo PRs and main pushes the job converts the merged Go profile to Cobertura (`go tool gocover-cobertura`, `-ignore-dirs` mirroring `.testcoverage.yml`'s global excludes so the % tracks the merged-total gate) and uploads it via `actions/upload-code-coverage`; the `github-code-quality[bot]` posts the aggregate + per-file diff-vs-main comment. **Non-gating**: the upload is `continue-on-error`, so this public-preview feature can never red `CI`. Fork PRs skip (no `code-quality` token, per GitHub's own guard). Renders only once the repo's *Settings → Security → Code quality* is enabled. - **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) dropped the badge from the README, and for the six days until this landed the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand are gone. The orphan `badges` branch is deleted separately once this lands: while the job still exists on main, the next code push would just recreate it. **Consequence worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the `badge` job was its only holder. Restoring the badge *in this shape* — publishing into the repo — means restoring `contents: write` to a workflow that executes PR-authored code. Prefer an endpoint hosted outside the repo, so a badge costs no write scope at all. -SDK (TS) coverage is gated by `make cov` but not yet published — extend with a -`language: javascript` upload step when wanted. +SDK (TS) coverage is gated by `make cov` but not yet published — extend with a `language: javascript` upload step when wanted. ## Merge queue -PRs land through a **merge queue**: "Merge when ready" enqueues the PR, -GitHub builds a merge-group ref (current main ⊕ the PRs ahead ⊕ this -PR), runs the required `CI` check against **that**, and fast-forwards -main only on green. This is the integration gate — it catches semantic -conflicts with a main that advanced after the PR's own run, replaces -the old "require branches to be up to date" rule (PRs no longer show -"out of date", and nobody clicks Update-branch), and never touches the -PR branch itself (so `require_last_push_approval` is never reset by it). - -How a `merge_group` run flows through the DAG: the classifier treats it -like a push (full suite — the queue never skips code checks; `docs` -still gates docs-build), while `title` (already validated on the PR), -`docs-preview` (PR-scoped), and `docs-deploy` (push-scoped) sit out and -the aggregator counts their skips as passes, exactly like any other -event-filtered run. **Removing the `merge_group:` trigger from ci.yml -would hang every queued PR** — no trigger means the required `CI` check -never reports on the merge group, and entries bounce out only after the -60-minute check timeout. - -Queue settings live in the `main branch protection` ruleset's -`merge_queue` rule: squash merges, land-as-ready (`min_entries_to_merge: -1`, no batching wait), up to 5 speculative builds. +PRs land through a **merge queue**: "Merge when ready" enqueues the PR, GitHub builds a merge-group ref (current main ⊕ the PRs ahead ⊕ this PR), runs the required `CI` check against **that**, and fast-forwards main only on green. This is the integration gate — it catches semantic conflicts with a main that advanced after the PR's own run, replaces the old "require branches to be up to date" rule (PRs no longer show "out of date", and nobody clicks Update-branch), and never touches the PR branch itself (so `require_last_push_approval` is never reset by it). + +How a `merge_group` run flows through the DAG: the classifier treats it like a push (full suite — the queue never skips code checks; `docs` still gates docs-build), while `title` (already validated on the PR), `docs-preview` (PR-scoped), and `docs-deploy` (push-scoped) sit out and the aggregator counts their skips as passes, exactly like any other event-filtered run. **Removing the `merge_group:` trigger from ci.yml would hang every queued PR** — no trigger means the required `CI` check never reports on the merge group, and entries bounce out only after the 60-minute check timeout. + +Queue settings live in the `main branch protection` ruleset's `merge_queue` rule: squash merges, land-as-ready (`min_entries_to_merge: 1`, no batching wait), up to 5 speculative builds. ## Cache inventory @@ -192,62 +82,21 @@ Queue settings live in the `main branch protection` ruleset's | Go modules (release read) | `gomod-v1--` | nobody — **restore-only** | `publish-dev` reads `ci.yml`'s shared entry from `main`'s scope via `actions/cache/restore`, so its cross-compile isn't slowed by a cold module tree. No post-step save, so 0 GB of budget and no risk of a partial write to the shared key. | | CodeQL DB + deps | `codeql-dependencies-*`, `codeql-overlay-base-database-*` | GHAS default setup | **Not ours** — minted by GitHub's default CodeQL setup, not by any workflow in this repo, and not configurable here. ~0.4 GB. Listed so the budget arithmetic below is honest. | -Deliberately **not** cached: `actions/setup-go`'s bundled cache -(`cache: false` in `publish-dev.yml`, `release.yml` and -`goreleaser-validate.yml`) — for different reasons per job. - -It stores `~/go/pkg/mod` **and** `~/.cache/go-build` under one entry -(~1 GB stored), keyed on the root `go.mod` — setup-go hashed `go.sum` -through v6.2.0 and `go.mod` from v6.3.0, see -[actions/setup-go#705](https://github.com/actions/setup-go/pull/705) — so -roughly half of it re-stores the module tree `gomod-v1` already keeps once. -`publish-dev.yml` opts out of that entry and caches the half that pays for -itself on its own key (`gobuild-v3--go-release-`, -~0.5 GB): its GoReleaser step takes 36–246 s warm versus 401–446 s cold, so -dropping the build objects outright would cost roughly 2.5–7 minutes on -every push to main (mean delta ≈4.8 min across those runs). Those timings were measured with setup-go's bundled entry, which -also held `~/go/pkg/mod` — so `publish-dev` additionally *restores* (never -saves) `gomod-v1` from `main`'s scope, keeping the module tree warm too. -Without that restore the job would re-download ~112 MB per push and land -above the warm range this table quotes. - -`release.yml` keeps the plain opt-out — no re-cache. After this change -nothing mints a `setup-go-*` key at all, so turning its bundled cache back -on would be a cold miss *and* a fresh ~1 GB save rather than a hit. What is -warm is `publish-dev`'s `gobuild-v3--go-release-` entry, which a tag run -could restore from the default branch's scope — but a tagged release is rare -and not latency-sensitive, so it isn't worth a hand-rolled restore step. - -Re-enabling the bundled cache there would be strictly negative, not merely -unhelpful: cache writes are scoped to the ref that made them, so a save -from `refs/tags/v1.0.0` can never be read by `refs/tags/v1.0.1`, by `main`, -or by a PR — only by a re-run of that same tag. It would be a ~1 GB entry -per release that nothing but a retry can ever read. If release wall-clock -ever does matter, the lever is `actions/cache/restore` on -`publish-dev`'s key: restore-only, so it reads `main`'s warm entry and never -writes a tag-scoped one. - -`goreleaser-validate.yml` opts out on its own grounds: its `--single-target` -snapshot is fast enough that the post-step save costs more than a cold -`go mod download`. - -Key-versioning policy: bump the `v` prefix whenever the cache's -expected *contents* change shape — saves only fire on an exact-key miss, -so without a bump the old entry exact-hits forever and the new content -is never captured. Keep the old prefixes as transitional restore-keys, -then delete them once main has saved the new version. - -**Exception — a rotation that *narrows* `path:` carries no transitional -restore-key.** The old archive still contains the paths you just removed, -so restoring it would re-materialize exactly the content the rotation was -meant to stop storing (and, for `~/go/pkg/mod`, extract 0444 module files -over an already-restored tree). Drop the old prefix and purge the stale -entries instead — they hold budget the new keys need. `gobuild-v3` is the -worked example: it kept only its own same-suffix prefix. - -Purge **after** the rotation is on `main`, not before — until then `main` -still restores the old keys, so an early delete just forces a cold -repopulate of caches you are about to abandon: +Deliberately **not** cached: `actions/setup-go`'s bundled cache (`cache: false` in `publish-dev.yml`, `release.yml` and `goreleaser-validate.yml`) — for different reasons per job. + +It stores `~/go/pkg/mod` **and** `~/.cache/go-build` under one entry (~1 GB stored), keyed on the root `go.mod` — setup-go hashed `go.sum` through v6.2.0 and `go.mod` from v6.3.0, see [actions/setup-go#705](https://github.com/actions/setup-go/pull/705) — so roughly half of it re-stores the module tree `gomod-v1` already keeps once. `publish-dev.yml` opts out of that entry and caches the half that pays for itself on its own key (`gobuild-v3--go-release-`, ~0.5 GB): its GoReleaser step takes 36–246 s warm versus 401–446 s cold, so dropping the build objects outright would cost roughly 2.5–7 minutes on every push to main (mean delta ≈4.8 min across those runs). Those timings were measured with setup-go's bundled entry, which also held `~/go/pkg/mod` — so `publish-dev` additionally *restores* (never saves) `gomod-v1` from `main`'s scope, keeping the module tree warm too. Without that restore the job would re-download ~112 MB per push and land above the warm range this table quotes. + +`release.yml` keeps the plain opt-out — no re-cache. After this change nothing mints a `setup-go-*` key at all, so turning its bundled cache back on would be a cold miss *and* a fresh ~1 GB save rather than a hit. What is warm is `publish-dev`'s `gobuild-v3--go-release-` entry, which a tag run could restore from the default branch's scope — but a tagged release is rare and not latency-sensitive, so it isn't worth a hand-rolled restore step. + +Re-enabling the bundled cache there would be strictly negative, not merely unhelpful: cache writes are scoped to the ref that made them, so a save from `refs/tags/v1.0.0` can never be read by `refs/tags/v1.0.1`, by `main`, or by a PR — only by a re-run of that same tag. It would be a ~1 GB entry per release that nothing but a retry can ever read. If release wall-clock ever does matter, the lever is `actions/cache/restore` on `publish-dev`'s key: restore-only, so it reads `main`'s warm entry and never writes a tag-scoped one. + +`goreleaser-validate.yml` opts out on its own grounds: its `--single-target` snapshot is fast enough that the post-step save costs more than a cold `go mod download`. + +Key-versioning policy: bump the `v` prefix whenever the cache's expected *contents* change shape — saves only fire on an exact-key miss, so without a bump the old entry exact-hits forever and the new content is never captured. Keep the old prefixes as transitional restore-keys, then delete them once main has saved the new version. + +**Exception — a rotation that *narrows* `path:` carries no transitional restore-key.** The old archive still contains the paths you just removed, so restoring it would re-materialize exactly the content the rotation was meant to stop storing (and, for `~/go/pkg/mod`, extract 0444 module files over an already-restored tree). Drop the old prefix and purge the stale entries instead — they hold budget the new keys need. `gobuild-v3` is the worked example: it kept only its own same-suffix prefix. + +Purge **after** the rotation is on `main`, not before — until then `main` still restores the old keys, so an early delete just forces a cold repopulate of caches you are about to abandon: ```bash gh api repos/Wave-RF/WaveHouse/actions/caches --paginate \ @@ -255,25 +104,11 @@ gh api repos/Wave-RF/WaveHouse/actions/caches --paginate \ | xargs -I{} gh api -X DELETE repos/Wave-RF/WaveHouse/actions/caches/{} ``` -Include every family the rotation orphans, not just the renamed one — e.g. -turning on `cache: false` strands that job's `setup-go-*` entry too. - -**Sizing policy — the repo cache budget is 10 GB, hard.** Past it GitHub -LRU-evicts, so warm entries disappear mid-run and builds silently get -slower. Budget for **two live generations**: a `go.mod`/`go.sum` or -lockfile bump mints a whole new set while the previous one is still warm, -so the steady state is ~2× a single generation. That is why `~/go/pkg/mod` -is cached **once** (`gomod-v1`) rather than folded into each suffixed -build cache — doing the latter stored the module tree five times over, -five entries of ~0.9-1.2 GB each, ~5.2 GB per generation, and #438's 24-module -bump pushed the repo to 10.53 GB -([#443](https://github.com/Wave-RF/WaveHouse/issues/443)). - -Steady state after the split is roughly 5 GB of the 10 — two generations -of `gomod-v1` + the five `gobuild-v3` flavors + the release build cache, -plus the node-side caches and CodeQL. Before adding a cache or widening an -existing `path:`, check the current footprint and confirm two generations -still fit: +Include every family the rotation orphans, not just the renamed one — e.g. turning on `cache: false` strands that job's `setup-go-*` entry too. + +**Sizing policy — the repo cache budget is 10 GB, hard.** Past it GitHub LRU-evicts, so warm entries disappear mid-run and builds silently get slower. Budget for **two live generations**: a `go.mod`/`go.sum` or lockfile bump mints a whole new set while the previous one is still warm, so the steady state is ~2× a single generation. That is why `~/go/pkg/mod` is cached **once** (`gomod-v1`) rather than folded into each suffixed build cache — doing the latter stored the module tree five times over, five entries of ~0.9-1.2 GB each, ~5.2 GB per generation, and #438's 24-module bump pushed the repo to 10.53 GB ([#443](https://github.com/Wave-RF/WaveHouse/issues/443)). + +Steady state after the split is roughly 5 GB of the 10 — two generations of `gomod-v1` + the five `gobuild-v3` flavors + the release build cache, plus the node-side caches and CodeQL. Before adding a cache or widening an existing `path:`, check the current footprint and confirm two generations still fit: ```bash gh api repos/Wave-RF/WaveHouse/actions/cache/usage \ @@ -282,13 +117,11 @@ gh api repos/Wave-RF/WaveHouse/actions/caches --paginate \ -q '.actions_caches[]|"\(.size_in_bytes)\t\(.key)"' | sort -rn | head ``` -Never add a per-job copy of content that is a pure function of a lockfile -— key it once, unsuffixed, and let every job share it. +Never add a per-job copy of content that is a pure function of a lockfile — key it once, unsuffixed, and let every job share it. ## Timing (steady state, full pipeline) -The non-gating **Timing summary** job writes a per-job wall-clock table -to every run's Summary page. Reference shape: +The non-gating **Timing summary** job writes a per-job wall-clock table to every run's Summary page. Reference shape: | Job | Starts | Duration | |---|---:|---:| @@ -301,78 +134,27 @@ to every run's Summary page. Reference shape: ## Deferred optimizations -Designed and measured during the 2026-06 reshape, then backed out to -keep CI simple and in parity with local `make test-e2e`. If the e2e -suite's wall-clock becomes a problem again, start here: - -- **e2e sharding** (the big one, ~60s): N concurrent orchestrators in - one runner, each with its own ClickHouse + server — file-level - parallelism is impossible *within* one server (shared global policy - state, [#214](https://github.com/Wave-RF/WaveHouse/issues/214)), but - isolated stacks dissolve the constraint with zero test changes. - Measured green at 3 shards: suite wall 100s → ~40s (floor = the - slowest file, `ingest.test.ts` at 36s), CPU contention negligible on - the 4-core runners. Needs: per-shard scratch paths + vitest file - filters in the orchestrator, a shard-map driver script, per-shard TS - coverage dirs nyc-merged back into `ts-e2e/` (Go covdata can share - one GOCOVERDIR — covcounters are pid-stamped). See PR #312's history - (commit `ed1db1c`) for a working implementation. -- **No-`needs` e2e** (~5s): e2e classifies the change set itself and - starts at run creation; requires a second classifier run plus an - aggregator cross-check so drift fails closed. Also in `ed1db1c`. -- **docs-preview artifact-poll** (~0s on today's critical path): preview - does its trusted-main setup in parallel with docs-build and polls for - `docs-dist`. Only worth it if e2e drops under ~150s again. +Designed and measured during the 2026-06 reshape, then backed out to keep CI simple and in parity with local `make test-e2e`. If the e2e suite's wall-clock becomes a problem again, start here: + +- **e2e sharding** (the big one, ~60s): N concurrent orchestrators in one runner, each with its own ClickHouse + server — file-level parallelism is impossible *within* one server (shared global policy state, [#214](https://github.com/Wave-RF/WaveHouse/issues/214)), but isolated stacks dissolve the constraint with zero test changes. Measured green at 3 shards: suite wall 100s → ~40s (floor = the slowest file, `ingest.test.ts` at 36s), CPU contention negligible on the 4-core runners. Needs: per-shard scratch paths + vitest file filters in the orchestrator, a shard-map driver script, per-shard TS coverage dirs nyc-merged back into `ts-e2e/` (Go covdata can share one GOCOVERDIR — covcounters are pid-stamped). See PR #312's history (commit `ed1db1c`) for a working implementation. +- **No-`needs` e2e** (~5s): e2e classifies the change set itself and starts at run creation; requires a second classifier run plus an aggregator cross-check so drift fails closed. Also in `ed1db1c`. +- **docs-preview artifact-poll** (~0s on today's critical path): preview does its trusted-main setup in parallel with docs-build and polls for `docs-dist`. Only worth it if e2e drops under ~150s again. ## Adding a job -1. Pick the gate: must it block merges? Add it to **both** the - aggregator's and `timing`'s `needs` lists. Advisory-only? Mirror - `timing` (`continue-on-error: true`, not in the aggregator's needs). -2. Gate on the change set via `needs: changes` + `if:` on its outputs — - never with workflow-level `paths` filters (they'd orphan the required - check, invariant 1). +1. Pick the gate: must it block merges? Add it to **both** the aggregator's and `timing`'s `needs` lists. Advisory-only? Mirror `timing` (`continue-on-error: true`, not in the aggregator's needs). +2. Gate on the change set via `needs: changes` + `if:` on its outputs — never with workflow-level `paths` filters (they'd orphan the required check, invariant 1). 3. Use `setup-env`. Three rules come with it: - - **Pass a `go-cache-suffix` if the job compiles Go** — a fresh one for - new flags, an existing flavor's if it compiles identically. Never - empty: the resulting `gobuild-v3--go-` restore-key prefix-matches - every flavor's entry (the cross-flavor restore the split exists to - avoid) and mints an extra build entry against the sizing policy above. - `setup-env` fails the job outright whenever `go` is true — which is - the **default** — and no suffix is passed, so a new job can't inherit - the empty default silently. A job that doesn't build Go passes - `go: "false"` instead, as the docs jobs do. - - **Make sure the job's make target reaches `go-mod-download`.** Every - `ci.yml` Go job races to save the shared unsuffixed `gomod-v1`, so a - job that only fetches the modules it happens to import can store a - partial tree that then exact-hits for everyone until the next - rotation. This is why `cov` carries the prerequisite. - - **Never add cache save steps to `ci.yml`** (invariant 6). A workflow - outside it that needs a cache hand-rolls one, as `publish-dev.yml` - does. -4. Need a build product / data from another job? Upload it as an artifact - there, then either `needs` the producer + `download-artifact` (simple, - but serializes this job's setup behind the producer), or — when this - job has its own setup to overlap and sits on the critical path — start - it off `changes` and poll with - [`scripts/ci/wait-artifact.sh`](../../scripts/ci/wait-artifact.sh) (see - the `coverage` job). The poll holds a runner idle while waiting; worth - it to keep setup off the critical path. -5. Declare least-privilege `permissions:` on the job; the workflow - default is `contents: read`. -6. Nontrivial logic goes in `scripts/ci/*.sh` (shellcheck-gated via - `make lint-sh`), not inline YAML — except in the trusted-main deploy - jobs (invariant 5) where inline is the point. -7. Run `make lint-gha` (actionlint) before pushing; it's part of - `make verify`. + - **Pass a `go-cache-suffix` if the job compiles Go** — a fresh one for new flags, an existing flavor's if it compiles identically. Never empty: the resulting `gobuild-v3--go-` restore-key prefix-matches every flavor's entry (the cross-flavor restore the split exists to avoid) and mints an extra build entry against the sizing policy above. `setup-env` fails the job outright whenever `go` is true — which is the **default** — and no suffix is passed, so a new job can't inherit the empty default silently. A job that doesn't build Go passes `go: "false"` instead, as the docs jobs do. + - **Make sure the job's make target reaches `go-mod-download`.** Every `ci.yml` Go job races to save the shared unsuffixed `gomod-v1`, so a job that only fetches the modules it happens to import can store a partial tree that then exact-hits for everyone until the next rotation. This is why `cov` carries the prerequisite. + - **Never add cache save steps to `ci.yml`** (invariant 6). A workflow outside it that needs a cache hand-rolls one, as `publish-dev.yml` does. +4. Need a build product / data from another job? Upload it as an artifact there, then either `needs` the producer + `download-artifact` (simple, but serializes this job's setup behind the producer), or — when this job has its own setup to overlap and sits on the critical path — start it off `changes` and poll with [`scripts/ci/wait-artifact.sh`](../../scripts/ci/wait-artifact.sh) (see the `coverage` job). The poll holds a runner idle while waiting; worth it to keep setup off the critical path. +5. Declare least-privilege `permissions:` on the job; the workflow default is `contents: read`. +6. Nontrivial logic goes in `scripts/ci/*.sh` (shellcheck-gated via `make lint-sh`), not inline YAML — except in the trusted-main deploy jobs (invariant 5) where inline is the point. +7. Run `make lint-gha` (actionlint) before pushing; it's part of `make verify`. ## Debugging a slow or red run -- Start at the run's **Summary** page: the Timing table says where the - wall-clock went; the coverage table comes from the `coverage` job. -- `coverage` red in "Wait for coverage fragments" means a suite failed or - was cancelled before uploading its fragment — the aggregator is already - red from that suite. Fix that job first; it's not a `coverage` bug. -- Re-run failed jobs is safe everywhere: fragments/dist artifacts - persist per-run, `download-artifact` finds them instantly, and the - sticky preview comment updates in place. +- Start at the run's **Summary** page: the Timing table says where the wall-clock went; the coverage table comes from the `coverage` job. +- `coverage` red in "Wait for coverage fragments" means a suite failed or was cancelled before uploading its fragment — the aggregator is already red from that suite. Fix that job first; it's not a `coverage` bug. +- Re-run failed jobs is safe everywhere: fragments/dist artifacts persist per-run, `download-artifact` finds them instantly, and the sticky preview comment updates in place. diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 5ae734e7..aac43c54 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -32,9 +32,14 @@ // repairs WH001 in .md and only WH002 in .mdx: // WH001 prose paragraphs must not be hard-wrapped (autofix: joins them) // WH002 MDX fence adjacent to a JSX tag needs a blank line (fixed in phase 1) - // Both are enabled in .markdownlint.json. WH001 is turned off for CI docs and - // agent prompts by .github/.markdownlint.json and .claude/.markdownlint.json, - // and applies everywhere else — a narrower exclusion than scripts/docs-prose.sh. + // Both are enabled in .markdownlint.json with no path-scoped carve-out of any + // kind — WH001 over every tracked Markdown file, exactly what CONTRIBUTING.md + // tells contributors; WH002 over every .mdx (it returns early on .md, where + // CommonMark has no JSX to sit beside a fence). Two subtree .markdownlint.json files used to switch WH001 off + // under .github/ and .claude/; #521 deleted them, and the last file that + // would have needed a special case (a vendored PostHog skill) was deleted + // with them. Don't reintroduce a rule-level override here or in a subtree + // config: silently contradicting CONTRIBUTING.md is what #521 was fixing. // // markdownlint-cli2 globs with dot:true, so `**/*.md` descends into hidden // dirs — including .worktrees/, where this repo nests git worktrees. Honor diff --git a/AGENTS.md b/AGENTS.md index d79cb184..81c2b5f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -294,7 +294,7 @@ Then run the reviewers relevant to the PR's diff (the same set from `scripts/pre Documentation *prose* — accuracy against the code, runnable examples, clarity, completeness — **and code↔docs sync** (code that changed but whose docs didn't) are reviewed by the **`docs-reviewer`** subagent, not the code-focused `pre-push-reviewer`. The canonical rubric is `.github/prompts/docs-review.md`. It complements the deterministic prose tools — misspell, markdownlint, starlight-links-validator — reviewing only what they can't, and it never edits docs or posts PR comments. -**Scope** is the canonical docs-prose set from `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, `PERF-CLAIMS-REVIEW.md`, `docs/posthog-setup-report.md`. So it covers the Starlight site under `docs/src/content/` **and** the governance docs (`README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — new docs are picked up automatically. `CODE_OF_CONDUCT.md`/`SUPPORT.md` are deep-reviewed only on change or material suspicion. +**Scope** is the canonical docs-prose set from `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, `PERF-CLAIMS-REVIEW.md`. So it covers the Starlight site under `docs/src/content/` **and** the governance docs (`README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — new docs are picked up automatically. `CODE_OF_CONDUCT.md`/`SUPPORT.md` are deep-reviewed only on change or material suspicion. **It is a hard pre-push gate**, run in parallel with the other pre-push reviewers (see §Pre-push self-review). Invoked with the **default (branch) scope** it emits a `VERDICT:` line; on `ship_it` the `review-marker.sh` SubagentStop hook writes `tmp/docs-reviewer-passed-`, which the push gate requires — unconditionally, on every PR-branch push (even code-only ones). Run it via **`/docs-review`**; with **no arg** that's the gating review (branch scope), while an explicit **path/glob** or **`all`** is **advisory** (no `VERDICT:`, no marker) for ad-hoc audits. The whole dev team runs Claude Code and this command is tracked in-repo, so everyone runs it themselves; there is intentionally **no PR/cloud path** for docs review. @@ -323,7 +323,7 @@ Before finishing a task, grep for the identifiers you touched (field names, env ### Markdown authoring rules -- **Never hard-wrap prose. One paragraph is one line.** No wrapping at 72/80 columns, no "semantic linefeeds" splitting a paragraph at sentence boundaries. Wrapped prose makes every later edit rewrap the whole block, so a one-word change lands as a five-line diff. Enforced by WH001 (`scripts/markdownlint-rules/no-hard-wrapped-prose.mjs`), which autofixes. Tables (with or without leading pipes), code, headings, setext underlines, blockquotes, JSX, `$$` display math, multi-line MDX `import`/`export`, and `:::` aside delimiters are left alone; a list item is joined as a unit, marker line included; an aside's *body* is joined but its delimiters are not. +- **Never hard-wrap prose. One paragraph is one line.** No wrapping at 72/80 columns, no "semantic linefeeds" splitting a paragraph at sentence boundaries. Wrapped prose makes every later edit rewrap the whole block, so a one-word change lands as a five-line diff. Enforced by WH001 (`scripts/markdownlint-rules/no-hard-wrapped-prose.mjs`), which autofixes. Tables (with or without leading pipes), code, headings, setext underlines, blockquotes, JSX, `$$` display math, multi-line MDX `import`/`export`, and `:::` aside delimiters are left alone; a list item is joined as a unit, marker line included; an aside's *body* is joined but its delimiters are not. Note the four-space rule cuts both ways: any line indented four or more spaces is read as an indented code block and skipped, so a *nested* list item is never joined and must be unwrapped by hand. - **In MDX, leave a blank line between a JSX tag and a code fence.** MDX itself renders the glued form correctly — verified by compiling both shapes with the same `@mdx-js/mdx` Astro uses. The blank line is what keeps *markdownlint* agreeing with it: markdownlint parses CommonMark, where `` opens an HTML block that runs to the next blank line, so a glued fence is not a code block to any generic rule and `markdownlint --fix` will reformat the code inside it: ````mdx @@ -339,7 +339,7 @@ Before finishing a task, grep for the identifiers you touched (field names, env Enforced by WH002. **`.mdx` is never auto-fixed by the generic markdownlint rules** — `make fix` scopes that pass to `**/*.md`, because where markdownlint's CommonMark parse and MDX disagree a generic autofix rewrites the inside of a code block. MDX gets exactly one *structural* fixer, `scripts/fix-mdx-fences.mjs`, which only ever inserts a blank line beside a JSX tag (misspell still corrects spelling there — its curated list needs no parse). So `make lint` reports MDX problems but `make fix` will not silently repair them — including WH001 wrapping, which you must unwrap by hand in `.mdx`. You can't reach MDX with a bare `markdownlint-cli2 --fix` either — the config globs `.md` only, and the `.mdx` glob lives on `lint:md` — so that hazard is closed by construction rather than by this instruction. - **Editors see WH001 in `.md` only.** The markdownlint extension reads `.markdownlint-cli2.jsonc`, `customRules` included, so no `.vscode` setting is needed (`markdownlint.customRules` is deprecated in favor of that file). But it activates on the `markdown` language ID, and `.mdx` is not associated with it — so WH002 never squiggles in the editor, and WH001 squiggles only in `.md`. Don't "fix" that with a `files.associations` entry: it would enable `source.fixAll.markdownlint` on `.mdx`, running exactly the generic fixers that must never see MDX. `make fix` and the agent hook are the MDX path. - **These fix themselves as you write.** `.claude/hooks/markdown-on-save.sh` (PostToolUse, sibling of `gofumpt-on-save.sh`) runs the MDX fence pass on `.mdx`, markdownlint `--fix` on `.md`, and misspell on both, so an agent's output is corrected in the same pass rather than costing a lint failure and a manual cleanup. It only sees `Edit`/`Write`/`MultiEdit` — a file written through a Bash heredoc bypasses it, so run `make fix` after doing that. -- **WH001 is off under `.github/` and `.claude/`** (CI docs and agent prompts) via their own `.markdownlint.json`. It applies everywhere else, `AGENTS.md` and `CHANGELOG.md` included — so this is a narrower exclusion than `scripts/docs-prose.sh`, which also skips those two. +- **WH001 applies to every tracked Markdown file, with no carve-out** — `AGENTS.md`, `CHANGELOG.md`, `.github/` CI docs and `.claude/` agent prompts included. Path-scoped `.markdownlint.json` files under `.github/` and `.claude/` used to switch it off there; [#521](https://github.com/Wave-RF/WaveHouse/issues/521) deleted them, because a config that silently contradicts `CONTRIBUTING.md` teaches a reviewer reading the config the wrong invariant. Don't reintroduce one — if a file genuinely can't satisfy WH001, say why in a `markdownlint-disable` comment in the file itself, where the next reader sees it. This makes WH001 *broader* than `scripts/docs-prose.sh`, which still skips `.github/` and `.claude/`: mechanical style is cheap to enforce everywhere, a prose review is not. ### Authoring docs-site pages diff --git a/CHANGELOG.md b/CHANGELOG.md index 186deb0b..c31d0ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Docs-site analytics for search, code copies, 404s, docs section, and live-demo connectivity** (`docs/src/components/DocsTracking.astro` (new), `docs/src/components/{PostHog,Footer,LiveDemo}.astro`): the site tracked its own CTAs but nothing a reader did on the way to one, so the questions that decide what to write next — what people search for and *don't* find, which snippets get copied, which dead links keep getting followed — had no data behind them. `docs_search` fires a second after the query settles rather than once per keystroke, carrying `query` and `result_count` read off Pagefind's own results message (the rendered list is capped at its page size, so counting the DOM would under-report); `result_count: 0` is the event worth having. `code_copied` (`page`, `language`) watches Expressive Code's copy buttons from the document rather than re-binding every code block on every navigation — the hero's install chip is not an EC block and keeps its own `hero_install_copied`. `docs_404` (`path`, `referrer`) turns broken inbound links into a list instead of a hunch. A `doc_section` property (the first path segment, `home` for `/`) puts every event in a docs area without each tracker carrying its own copy; it's stamped at capture time by a `before_send` hook in `posthog.init()` rather than `register()`, because a queued `register()` replays only after init has already captured the first hard-load `$pageview` — which would then carry the previous visit's persisted value — and `history_change` navigations update the URL before capture fires, so reading `location` in the hook is always current. `live_demo_connected` fires once per mount when the hero's SSE feed comes up rather than on its first row — named for what it measures (the demo backend answered), since a quiet minute on the repo is not a disengaged reader. The three site-wide trackers share one new `DocsTracking.astro` rendered from the footer (like `MermaidZoom` / `ScrollHints`) and delegate from `document`, since Pagefind, Expressive Code, and the 404 route all own their own markup — some of it created after page load. +### Changed + +- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. + ### Removed - **The README coverage badge and its whole publishing pipeline** (`.github/workflows/ci.yml`, `scripts/ci/publish-badge.sh` (deleted), `scripts/cov/main.go`, `.github/workflows/README.md`, `.testcoverage.yml`, `AGENTS.md`): [#502](https://github.com/Wave-RF/WaveHouse/pull/502) rewrote the README badge row and dropped the Go Coverage badge, but nothing removed what fed it — so for the six days until this landed the non-gating `badge` job kept running on every main push, holding `ci.yml`'s only `contents: write`, publishing `coverage-go.json` to the orphan `badges` branch for a badge no page rendered. Retired rather than restored ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)): the `badge` job, its two producer steps in `coverage` (`cov badge` + the `go-coverage-badge` artifact), `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand (with `badgeData`/`badgeColor`). The orphan `badges` branch is deleted separately once this lands — while the job still exists on main, the next code push would recreate it. **The gate is untouched** — `make cov`, `.testcoverage.yml`'s `threshold.total` and per-suite minima, and the GitHub Code Quality PR comments (the other half of [#133](https://github.com/Wave-RF/WaveHouse/issues/133)) all still run; only the published badge surface is gone. The security consequence is the reason to prefer retiring over restoring: **`ci.yml` now declares no `contents: write` in any job**, so the workflow that executes PR-authored code can no longer write to the repository under any path. Also fixed in passing: `timing`'s `needs` still listed `badge` (a dangling `needs` is a workflow-level error once the job is gone), and two permission comments were wrong: the "sole holder of `contents:write`" claims were repo-wide statements only ever true within `ci.yml` (`release.yml` and `publish-npm.yml` hold it too), and the workflow header claimed `docs-preview` was the only job with a write scope, undercounting `coverage`'s `code-quality: write` — which it holds while executing the PR tree. diff --git a/docs/src/content/docs/claude-code.md b/docs/src/content/docs/claude-code.md index 59abc2d0..ee77bf36 100644 --- a/docs/src/content/docs/claude-code.md +++ b/docs/src/content/docs/claude-code.md @@ -60,7 +60,6 @@ Both markers are tree-keyed so commit-then-push works without a re-run when the | `.claude/skills/pr-sync-with-main/SKILL.md` | "Fix this stale PR" workflow — merge origin/main, never rebase or force-push | | `.claude/skills/pr-review-locally/SKILL.md` | "Review PR locally" workflow — `wt switch pr:` + the relevant reviewers (code, docs, …) in parallel, no PR comments | | `.claude/skills/pm-triage/SKILL.md` | PM-review workflow — triage feedback / backlog / code TODOs and reconcile issue & PR status into well-scoped, tracked Task Board (project #7) issues; invoked as `/pm-triage` | -| `.claude/skills/integration-astro-view-transitions/` | Vendored PostHog-authored skill (installed by the PostHog wizard, v1.21.1) — reference patterns for the docs-site analytics: web snippet, ClientRouter view-transitions guard, user identify | | `.claude/settings.local.json` | **Your personal overrides** — gitignored; put model choice, status line, allow lists, etc. here | | `.config/wt.toml` | Worktrunk project hooks (post-start, pre-merge, pre-remove) | @@ -96,7 +95,6 @@ Skills load automatically into Claude's context when conversation patterns match | `pr-sync-with-main` | When a PR shows "out-of-date with base branch", or a user asks to "fix the PR" / "sync with main". Documents the merge-not-rebase procedure and the WaveHouse-specific reason long-lived branches need it. | | `pr-review-locally` | When a user asks to "review PR ", "audit PR ", "look at PR " — pulls the PR down via `wt switch pr:` (or `gh pr checkout`), runs the relevant reviewers (code, docs, …) in parallel in fresh context, surfaces their combined findings without commenting on the PR. | | `pm-triage` | When a user asks to triage dogfooding feedback, re-prioritize the backlog (P0–P3), sweep code TODOs, or check that work is tracked — runs a PM-style review against the Task Board (project #7) and proposes well-scoped issues. Invoked as `/pm-triage`. | -| `integration-astro-view-transitions` | PostHog work on the Astro docs site (snippet patterns, view-transitions guard, identify). Vendored PostHog-authored content — wizard-installed, lightly edited only for markdownlint; the live setup (relay host, committed fallback) is documented in `PostHog.astro` itself and the CHANGELOG. | To add a skill: create `.claude/skills//SKILL.md` with frontmatter `name` + `description` and the workflow body. Description quality matters — that's what Claude matches against to load the skill. diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index e3f765db..c0b09178 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -429,7 +429,7 @@ Formatting (**gofumpt** — strict superset of gofmt — and **goimports** impor `make lint` also covers documentation: **markdownlint** owns Markdown *and* MDX style (rules in `.markdownlint.json`; file selection in `.markdownlint-cli2.jsonc` for `.md`, plus the `**/*.mdx` glob on `lint:md` in `package.json`) and **misspell** owns spelling. Two rules are repo-local, in `scripts/markdownlint-rules/`: -- **WH001 / no-hard-wrapped-prose** — a paragraph must be one line. Hard-wrapping at 72/80 columns turns a one-word edit into a five-line diff. Autofixes. Tables (leading pipes optional), code, headings, setext underlines, blockquotes, JSX, `$$` display math, and multi-line MDX `import`/`export` are left alone; a list item is joined as a unit; an aside's body is joined but its `:::` delimiters are not. +- **WH001 / no-hard-wrapped-prose** — a paragraph must be one line. Hard-wrapping at 72/80 columns turns a one-word edit into a five-line diff. Autofixes. Tables (leading pipes optional), code, headings, setext underlines, blockquotes, JSX, `$$` display math, and multi-line MDX `import`/`export` are left alone; a list item is joined as a unit; an aside's body is joined but its `:::` delimiters are not. Note the four-space rule cuts both ways: any line indented four or more spaces is read as an indented code block and skipped, so a *nested* list item is never joined and must be unwrapped by hand. - **WH002 / mdx-fence-needs-blank-line** — an MDX code fence directly against a JSX tag (`` immediately followed by a fence). MDX renders that fine; the problem is that markdownlint parses CommonMark, where the tag opens an HTML block that runs to the next blank line — so the fence is not a code block to any generic rule, and `markdownlint --fix` will happily reformat the code inside it. The blank line is what keeps the two parsers agreeing. Run `make fix` to apply them. **The generic markdownlint fixers run over `.md` only.** markdownlint parses CommonMark and MDX does not, and where the two disagree an autofix rewrites the inside of a code block — de-indenting YAML comments it reads as headings, autolinking bare URLs. Reporting that disagreement is useful, so `make lint` still checks `.mdx`; acting on it is not. MDX therefore gets exactly one *structural* fixer, `scripts/fix-mdx-fences.mjs`, which only ever inserts a blank line beside a JSX tag. (misspell still auto-corrects spelling in `.mdx` — its corrections come from a curated list and don't depend on parsing the document.) diff --git a/scripts/docs-prose.sh b/scripts/docs-prose.sh index 86684758..52c5db71 100755 --- a/scripts/docs-prose.sh +++ b/scripts/docs-prose.sh @@ -12,7 +12,10 @@ # DENYLIST model: every tracked *.md / *.mdx file IS docs prose UNLESS it # matches an exclusion below — so newly-added docs are covered automatically # (there is no allowlist to keep in sync). Keep the exclusions in lockstep -# with AGENTS.md §"Docs review" and .github/prompts/docs-review.md. +# with the THREE places that restate them: AGENTS.md §"Docs review", +# .github/prompts/docs-review.md, and .claude/agents/docs-reviewer.md (the +# gating subagent's own system prompt — easy to miss, and stale there means +# the reviewer scopes itself wrongly). # # Excluded (NOT user-facing prose): # .claude/** , .github/** agent prompts, PR/issue templates, tooling @@ -21,7 +24,6 @@ # these AS truth to check the other docs) # *.draft.md , *.old.md drafts / archives # PERF-CLAIMS-REVIEW.md internal review artifact -# docs/posthog-setup-report.md frozen wizard-generated setup artifact # # CODE_OF_CONDUCT.md and SUPPORT.md ARE docs prose (they appear in `all`), but # the docs-reviewer only deep-reviews them when they changed or when a material @@ -42,7 +44,6 @@ is_docs_prose() { case "$1" in .claude/*|.github/*) return 1 ;; CHANGELOG.md|AGENTS.md|CLAUDE.md|PERF-CLAIMS-REVIEW.md) return 1 ;; - docs/posthog-setup-report.md) return 1 ;; *.draft.md|*.old.md) return 1 ;; *.md|*.mdx) return 0 ;; *) return 1 ;; From b9112bf9e87d76a15a7bf92716fa848d849e5537 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 13:47:54 -0400 Subject: [PATCH 5/8] build(lint): drop the PERF-CLAIMS-REVIEW.md denylist entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlike docs/posthog-setup-report.md — a real file deleted in #502 that left a stale reference behind — PERF-CLAIMS-REVIEW.md was never tracked at all (`git log --all` finds nothing) and isn't gitignored, so the entry guarded a document that has never existed in this repo. The denylist's other general cases are patterns (*.draft.md, *.old.md) that already cover a one-off review write-up. A literal filename for a hypothetical file, restated in four places, is the outlier — and this PR is about not leaving speculative claims lying around in comments. Removed from all four lockstep locations, per the header rule the previous commit corrected. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .claude/agents/docs-reviewer.md | 2 +- .github/prompts/docs-review.md | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 2 +- scripts/docs-prose.sh | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.claude/agents/docs-reviewer.md b/.claude/agents/docs-reviewer.md index 0899489c..6e852297 100644 --- a/.claude/agents/docs-reviewer.md +++ b/.claude/agents/docs-reviewer.md @@ -20,7 +20,7 @@ Read `.github/prompts/docs-review.md` first; it is the canonical docs-review rub ## What counts as docs prose (scope) -The canonical set is resolved by `scripts/docs-prose.sh` — a **denylist**: every tracked `*.md`/`*.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`, `*.old.md`, `PERF-CLAIMS-REVIEW.md`. That is the Astro Starlight site under `docs/src/content/` **plus** the user-facing governance docs (`README.md`, `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — and any doc added later, automatically. +The canonical set is resolved by `scripts/docs-prose.sh` — a **denylist**: every tracked `*.md`/`*.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`, `*.old.md`. That is the Astro Starlight site under `docs/src/content/` **plus** the user-facing governance docs (`README.md`, `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — and any doc added later, automatically. - `scripts/docs-prose.sh all` — every docs-prose file (the full reading list). - `scripts/docs-prose.sh changed` — the docs-prose files changed on this branch (`main...HEAD`). diff --git a/.github/prompts/docs-review.md b/.github/prompts/docs-review.md index 1784e025..a6251092 100644 --- a/.github/prompts/docs-review.md +++ b/.github/prompts/docs-review.md @@ -1,6 +1,6 @@ You are reviewing the **documentation** of the WaveHouse project — the prose itself, and whether it kept up with the code; not the code's correctness. Read AGENTS.md at the repo root first: §Documentation Sync maps each code area to the docs that describe it, §SDK Sync covers the client, and the architecture/config context tells you what the docs *should* say. -**Scope** is the canonical docs-prose set resolved by `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, and `PERF-CLAIMS-REVIEW.md`. That is the Astro Starlight site under `docs/src/content/docs/` (`.md` + `index.mdx`) **plus** the user-facing governance docs — `README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md` — and any doc added later (new files are covered automatically). `CODE_OF_CONDUCT.md` and `SUPPORT.md` are mostly boilerplate: only deep-review them when they changed or when a material change elsewhere warrants it. +**Scope** is the canonical docs-prose set resolved by `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` file EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, and `*.draft.md`/`*.old.md`. That is the Astro Starlight site under `docs/src/content/docs/` (`.md` + `index.mdx`) **plus** the user-facing governance docs — `README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md` — and any doc added later (new files are covered automatically). `CODE_OF_CONDUCT.md` and `SUPPORT.md` are mostly boilerplate: only deep-review them when they changed or when a material change elsewhere warrants it. This review **complements** the deterministic layers that already run — do **not** duplicate them: diff --git a/AGENTS.md b/AGENTS.md index 81c2b5f2..fb387f2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -294,7 +294,7 @@ Then run the reviewers relevant to the PR's diff (the same set from `scripts/pre Documentation *prose* — accuracy against the code, runnable examples, clarity, completeness — **and code↔docs sync** (code that changed but whose docs didn't) are reviewed by the **`docs-reviewer`** subagent, not the code-focused `pre-push-reviewer`. The canonical rubric is `.github/prompts/docs-review.md`. It complements the deterministic prose tools — misspell, markdownlint, starlight-links-validator — reviewing only what they can't, and it never edits docs or posts PR comments. -**Scope** is the canonical docs-prose set from `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`, `PERF-CLAIMS-REVIEW.md`. So it covers the Starlight site under `docs/src/content/` **and** the governance docs (`README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — new docs are picked up automatically. `CODE_OF_CONDUCT.md`/`SUPPORT.md` are deep-reviewed only on change or material suspicion. +**Scope** is the canonical docs-prose set from `scripts/docs-prose.sh` — a *denylist*: every tracked `.md`/`.mdx` EXCEPT `.claude/**`, `.github/**`, `CHANGELOG.md`, `AGENTS.md`, `CLAUDE.md`, `*.draft.md`/`*.old.md`. So it covers the Starlight site under `docs/src/content/` **and** the governance docs (`README.md`, the SDK readme `clients/ts/README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`) — new docs are picked up automatically. `CODE_OF_CONDUCT.md`/`SUPPORT.md` are deep-reviewed only on change or material suspicion. **It is a hard pre-push gate**, run in parallel with the other pre-push reviewers (see §Pre-push self-review). Invoked with the **default (branch) scope** it emits a `VERDICT:` line; on `ship_it` the `review-marker.sh` SubagentStop hook writes `tmp/docs-reviewer-passed-`, which the push gate requires — unconditionally, on every PR-branch push (even code-only ones). Run it via **`/docs-review`**; with **no arg** that's the gating review (branch scope), while an explicit **path/glob** or **`all`** is **advisory** (no `VERDICT:`, no marker) for ad-hoc audits. The whole dev team runs Claude Code and this command is tracked in-repo, so everyone runs it themselves; there is intentionally **no PR/cloud path** for docs review. diff --git a/CHANGELOG.md b/CHANGELOG.md index c31d0ae4..4529f38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed -- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. +- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. The denylist also drops its `PERF-CLAIMS-REVIEW.md` entry — unlike the wizard artifact this one names a file that was **never tracked** at all, so it guarded a hypothetical; the list's other general cases are patterns (`*.draft.md`, `*.old.md`) that already cover a one-off review document, and a literal filename restated in four places is the outlier. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. ### Removed diff --git a/scripts/docs-prose.sh b/scripts/docs-prose.sh index 52c5db71..2e0fb3d9 100755 --- a/scripts/docs-prose.sh +++ b/scripts/docs-prose.sh @@ -23,7 +23,6 @@ # AGENTS.md , CLAUDE.md agent source-of-truth (the reviewer reads # these AS truth to check the other docs) # *.draft.md , *.old.md drafts / archives -# PERF-CLAIMS-REVIEW.md internal review artifact # # CODE_OF_CONDUCT.md and SUPPORT.md ARE docs prose (they appear in `all`), but # the docs-reviewer only deep-reviews them when they changed or when a material @@ -43,7 +42,7 @@ cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 1 is_docs_prose() { case "$1" in .claude/*|.github/*) return 1 ;; - CHANGELOG.md|AGENTS.md|CLAUDE.md|PERF-CLAIMS-REVIEW.md) return 1 ;; + CHANGELOG.md|AGENTS.md|CLAUDE.md) return 1 ;; *.draft.md|*.old.md) return 1 ;; *.md|*.mdx) return 0 ;; *) return 1 ;; From 193eb6534a7ca4269d271e3d57d8981efdb32345 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 13:59:14 -0400 Subject: [PATCH 6/8] docs: past-tense the badges-branch note, rewrap a config comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings. The workflows README still told readers the orphan `badges` branch "is deleted separately once this lands: while the job still exists on main, the next code push would just recreate it." Both clauses stopped being true when #520 merged and the branch was deleted — so the canonical CI reference was handing readers an open action item that is already done. Past-tensed, keeping the reason it had to be sequenced that way. The twin sentence in CHANGELOG.md is deliberately NOT changed: that entry records what #520 did and planned at the time, and Keep a Changelog entries aren't rewritten as reality moves. The living reference doc tracks current state; the changelog tracks history. Also rewrapped a 112-character comment line in .markdownlint-cli2.jsonc to match the ~80 of its neighbours — hand-wrapped prose maintenance cost, in the file that configures the rule against it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .github/workflows/README.md | 2 +- .markdownlint-cli2.jsonc | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index ba441978..c07db947 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -56,7 +56,7 @@ The `coverage` job both **gates** (`make cov` against the floors in `.testcovera - **Per-run job summary** — the merged per-package func table on every run's Summary page. - **PR comment (GitHub Code Quality)** — on same-repo PRs and main pushes the job converts the merged Go profile to Cobertura (`go tool gocover-cobertura`, `-ignore-dirs` mirroring `.testcoverage.yml`'s global excludes so the % tracks the merged-total gate) and uploads it via `actions/upload-code-coverage`; the `github-code-quality[bot]` posts the aggregate + per-file diff-vs-main comment. **Non-gating**: the upload is `continue-on-error`, so this public-preview feature can never red `CI`. Fork PRs skip (no `code-quality` token, per GitHub's own guard). Renders only once the repo's *Settings → Security → Code quality* is enabled. -- **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) dropped the badge from the README, and for the six days until this landed the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand are gone. The orphan `badges` branch is deleted separately once this lands: while the job still exists on main, the next code push would just recreate it. **Consequence worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the `badge` job was its only holder. Restoring the badge *in this shape* — publishing into the repo — means restoring `contents: write` to a workflow that executes PR-authored code. Prefer an endpoint hosted outside the repo, so a badge costs no write scope at all. +- **README badge — retired** ([#509](https://github.com/Wave-RF/WaveHouse/issues/509)). A `badge` job used to publish a shields.io endpoint JSON (`cov badge`) to an orphan `badges` branch for a README badge. [#502](https://github.com/Wave-RF/WaveHouse/pull/502) dropped the badge from the README, and for the six days until this landed the job kept publishing to nothing — so the job, `scripts/ci/publish-badge.sh`, and the `cov badge` subcommand are gone. The orphan `badges` branch was deleted once that landed — deleting it any earlier would have been futile, since the still-live job would have recreated it on the next code push. **Consequence worth keeping**: `ci.yml` now declares no `contents: write` anywhere — the `badge` job was its only holder. Restoring the badge *in this shape* — publishing into the repo — means restoring `contents: write` to a workflow that executes PR-authored code. Prefer an endpoint hosted outside the repo, so a badge costs no write scope at all. SDK (TS) coverage is gated by `make cov` but not yet published — extend with a `language: javascript` upload step when wanted. diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index aac43c54..6672f7c6 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -35,8 +35,9 @@ // Both are enabled in .markdownlint.json with no path-scoped carve-out of any // kind — WH001 over every tracked Markdown file, exactly what CONTRIBUTING.md // tells contributors; WH002 over every .mdx (it returns early on .md, where - // CommonMark has no JSX to sit beside a fence). Two subtree .markdownlint.json files used to switch WH001 off - // under .github/ and .claude/; #521 deleted them, and the last file that + // CommonMark has no JSX to sit beside a fence). Two subtree + // .markdownlint.json files used to switch WH001 off under .github/ and + // .claude/; #521 deleted them, and the last file that // would have needed a special case (a vendored PostHog skill) was deleted // with them. Don't reintroduce a rule-level override here or in a subtree // config: silently contradicting CONTRIBUTING.md is what #521 was fixing. From 1ee75cf971c90604974783be8a8de93b92dfbe8d Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 14:07:57 -0400 Subject: [PATCH 7/8] docs: point Grafana MCP at the obs stack that actually shipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude-code.md told readers that "when #121 lands a SigNoz dev stack with `make dev-obs`", Grafana MCP would become useful for trace/log inspection. Three ways wrong: - #121 closed as COMPLETED on 2026-05-24. - `dev-obs` exists nowhere in the tree — the real targets are obs-aspire, obs-grafana, and obs-front. - The project went the other way. deployment.md states outright that no heavy multi-node cluster like SigNoz is maintained for local dev, and deployments/signoz/ is gone. So the page deferred to future work that had already shipped under a different name, and handed readers a command that errors out. Now points at `make obs-grafana` and links the deployment section. Pre-existing rather than introduced here, but claude-code.md is in the docs-prose set and already edited by this PR — and stale prose that outlived its subject is precisely what this PR is about. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- CHANGELOG.md | 2 +- docs/src/content/docs/claude-code.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4529f38c..feaf40a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed -- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. The denylist also drops its `PERF-CLAIMS-REVIEW.md` entry — unlike the wizard artifact this one names a file that was **never tracked** at all, so it guarded a hypothetical; the list's other general cases are patterns (`*.draft.md`, `*.old.md`) that already cover a one-off review document, and a literal filename restated in four places is the outlier. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. +- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. The denylist also drops its `PERF-CLAIMS-REVIEW.md` entry — unlike the wizard artifact this one names a file that was **never tracked** at all, so it guarded a hypothetical; the list's other general cases are patterns (`*.draft.md`, `*.old.md`) that already cover a one-off review document, and a literal filename restated in four places is the outlier. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. Reviewing this branch's own docs then caught an unrelated stale promise in `docs/src/content/docs/claude-code.md`, fixed here since the file was already being edited: it told readers that "when [#121](https://github.com/Wave-RF/WaveHouse/issues/121) lands a SigNoz dev stack with `make dev-obs`" Grafana MCP would become useful — but #121 closed as completed in May, `dev-obs` exists nowhere in the tree (the real targets are `obs-aspire` / `obs-grafana` / `obs-front`), and the project deliberately went the other way, with `deployment.md` stating outright that no heavy multi-node cluster like SigNoz is maintained for local development. It now points at `make obs-grafana`, which has shipped. ### Removed diff --git a/docs/src/content/docs/claude-code.md b/docs/src/content/docs/claude-code.md index ee77bf36..e2b3d2e5 100644 --- a/docs/src/content/docs/claude-code.md +++ b/docs/src/content/docs/claude-code.md @@ -167,7 +167,7 @@ None of these are committed at project level — pick what you actually use. - **[Grafana MCP](https://github.com/grafana/mcp-grafana)** — for querying Prometheus / Loki / Tempo / Pyroscope from Claude when debugging observability work. Useful if you touch `internal/observability/`. - **ClickHouse MCP** (community) — direct schema introspection + query against your local `make dev` ClickHouse. Useful for `internal/discovery/` and ingest work, but `make deps-shell` (clickhouse-client REPL) is often enough. -When [issue #121](https://github.com/Wave-RF/WaveHouse/issues/121) lands a SigNoz dev stack with `make dev-obs`, Grafana MCP pointed at that dev environment will become a natural choice for trace / log inspection. +`make obs-grafana` brings up a local Grafana LGTM stack (see [Deployment → Local Observability Stack](/deployment#local-observability-stack)), so Grafana MCP pointed at it is already the natural choice for trace / log inspection — no heavyweight cluster to stand up first. ## Permission posture From 8c02808e81b183060011d04a90ae62945ad34353 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 26 Aug 2026 14:22:06 -0400 Subject: [PATCH 8/8] docs: finish the job-graph audit, split the changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings. The job graph still omitted two real `needs` edges — `changes --> ci` and `changes --> preview` — while the legend three lines below says flatly "Solid arrows are `needs` edges". Not a deliberate simplification either: `changes --> deploy` IS drawn, so the diagram contradicted itself. Adding both makes the edges into `ci` exactly the aggregator's nine `needs`. The earlier commit in this PR audited that diagram and stopped two edges short, which is the same defect class the PR exists to remove. The Grafana MCP fix also moves out of the WH001 entry into its own `### Fixed` bullet. That entry's headline is about WH001 applying everywhere; its last two sentences were about `make obs-grafana` and a closed issue, which a reader scanning for either would not find. The repo's convention is one thorough bullet per change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MB4oPy9yfUHSe8FjnDmeZH --- .github/workflows/README.md | 2 ++ CHANGELOG.md | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c07db947..575a8389 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -12,10 +12,12 @@ graph TB changes --> docsbuild["docs-build"] changes --> coverage["coverage — poll fragments → merge → gate"] docsbuild --> preview["docs-preview (PRs) — non-gating"] + changes --> preview unit -. "coverage-unit (poll)" .-> coverage integration -. "coverage-integration (poll)" .-> coverage e2e -. "coverage-e2e (poll)" .-> coverage title["title (PRs)"] --> ci["CI (aggregator — sole required check)"] + changes --> ci lint["lint"] --> ci coverage --> ci e2e --> ci diff --git a/CHANGELOG.md b/CHANGELOG.md index feaf40a5..d19c88c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed -- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. The denylist also drops its `PERF-CLAIMS-REVIEW.md` entry — unlike the wizard artifact this one names a file that was **never tracked** at all, so it guarded a hypothetical; the list's other general cases are patterns (`*.draft.md`, `*.old.md`) that already cover a one-off review document, and a literal filename restated in four places is the outlier. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. Reviewing this branch's own docs then caught an unrelated stale promise in `docs/src/content/docs/claude-code.md`, fixed here since the file was already being edited: it told readers that "when [#121](https://github.com/Wave-RF/WaveHouse/issues/121) lands a SigNoz dev stack with `make dev-obs`" Grafana MCP would become useful — but #121 closed as completed in May, `dev-obs` exists nowhere in the tree (the real targets are `obs-aspire` / `obs-grafana` / `obs-front`), and the project deliberately went the other way, with `deployment.md` stating outright that no heavy multi-node cluster like SigNoz is maintained for local development. It now points at `make obs-grafana`, which has shipped. +- **WH001 (no hard-wrapped prose) now applies to every tracked Markdown file, with no carve-out** (`.github/.markdownlint.json` (deleted), `.claude/.markdownlint.json` (deleted), `.claude/skills/integration-astro-view-transitions/` (deleted), `.markdownlint-cli2.jsonc`, `.github/workflows/README.md`, `.claude/skills/pm-triage/references/routine.md`, `AGENTS.md`, `scripts/docs-prose.sh`, `.github/prompts/docs-review.md`, `docs/src/content/docs/claude-code.md`, `docs/src/content/docs/development.md`, `.claude/agents/docs-reviewer.md`): two path-scoped configs had switched WH001 off under `.github/` and `.claude/` ever since [#489](https://github.com/Wave-RF/WaveHouse/pull/489) introduced the rule — baked in from the start rather than added in response to a discovered problem — which left the repo documenting the rule three ways and disagreeing with itself: `CONTRIBUTING.md` promises contributors `make lint` enforces it *everywhere*, while `AGENTS.md` and the `.markdownlint-cli2.jsonc` header wrote up the carve-out. Not theoretical: on [#520](https://github.com/Wave-RF/WaveHouse/pull/520) a reviewer correctly flagged a hard-wrapped bullet in `.github/workflows/README.md`, an agent pointed at `"WH001": false` for that path and pushed back, and the reviewer recorded a *learning* never to flag WH001 there — the wrong invariant, learned off the wrong side of the contradiction ([#521](https://github.com/Wave-RF/WaveHouse/issues/521)). Both configs are deleted — each held nothing but the override, so the root `.markdownlint.json` governs again — and the 51 hard-wrapped paragraphs they were hiding are joined: 41 in `.github/workflows/README.md` and 10 in `.claude/skills/pm-triage/references/routine.md`, mechanical joins with no wording changed and every fenced block, table row, and heading byte-identical either side of the reflow. Deleted with them: the wizard-installed PostHog skill at `.claude/skills/integration-astro-view-transitions/` — 9 files, ~1,456 lines, including an 809-line `EXAMPLE.md` copied wholesale from `PostHog/context-mill`. Its integration job finished in [#277](https://github.com/Wave-RF/WaveHouse/pull/277), nothing in the repo calls it, and the docs-site setup it once described is documented where it belongs — in `docs/src/components/PostHog.astro` and this file. Keeping unowned third-party prose in the tree means content that drifts silently on every upstream bump and that nobody here reviews; it was also the single file that would have needed a special-case lint exclusion, so removing it is what lets WH001 apply with **no exception at all** rather than one documented one. Its two inventory rows in `claude-code.md` go with it, as does the now-dead `docs/posthog-setup-report.md` entry in the `scripts/docs-prose.sh` denylist (the wizard's other artifact, deleted back in [#502](https://github.com/Wave-RF/WaveHouse/pull/502)) and the copies of that denylist in `AGENTS.md` and `.github/prompts/docs-review.md`, which the script's header requires be kept in lockstep. Review of the change then turned up four more things the exclusion had been hiding, all fixed here: **WH001 has a blind spot** — `no-hard-wrapped-prose.mjs` classifies any line indented four or more spaces as an indented code block, so a *nested* list item is never joined, which left three hard-wrapped bullets in `.github/workflows/README.md` §"Adding a job" that the autofix could not see (unwrapped by hand; they were the last hard-wrapped prose paragraphs in the repo) and made `AGENTS.md`'s and `development.md`'s "a list item is joined as a unit" wrong for nested items (both now state the four-space caveat); the `scripts/docs-prose.sh` header told readers to keep its denylist in lockstep with **two** sibling copies when there are **three** — the missed one being `.claude/agents/docs-reviewer.md`, the gating subagent's own system prompt, which had in fact been silently out of sync for the whole life of the `posthog-setup-report.md` exclusion; the `.markdownlint-cli2.jsonc` header's "applies to every tracked Markdown file" was exact for WH001 but not WH002, which returns early on anything that isn't `.mdx`; and the job-graph diagram omitted `docs-deploy`'s `needs` edges from `unit`, `integration`, and `e2e`, contradicting invariant 2 three lines below it. The denylist also drops its `PERF-CLAIMS-REVIEW.md` entry — unlike the wizard artifact this one names a file that was **never tracked** at all, so it guarded a hypothetical; the list's other general cases are patterns (`*.draft.md`, `*.old.md`) that already cover a one-off review document, and a literal filename restated in four places is the outlier. `scripts/docs-prose.sh all` still resolves the same 27-file prose set. ### Removed @@ -25,6 +25,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **The Go Report Card badge stays gone** (no code change — [#502](https://github.com/Wave-RF/WaveHouse/pull/502) already removed the only reference): recorded here because [#509](https://github.com/Wave-RF/WaveHouse/issues/509) tracked it as collateral to restore. It isn't coming back — goreportcard.com is deprecated upstream, and the signal it graded (gofmt, vet, lint, misspell) is gated far more strictly by `make lint-go`/`golangci-lint` in CI. +### Fixed + +- **The Claude Code page pointed at an observability stack that never shipped** (`docs/src/content/docs/claude-code.md`): the MCP-servers section told readers that "when [#121](https://github.com/Wave-RF/WaveHouse/issues/121) lands a SigNoz dev stack with `make dev-obs`", Grafana MCP would become a natural choice for trace / log inspection. Wrong three ways: #121 closed as *completed* on 2026-05-24, `make dev-obs` exists nowhere in the tree (the real targets are `obs-aspire` / `obs-grafana` / `obs-front`), and the project deliberately went the other way — `docs/src/content/docs/deployment.md` states outright that no heavy multi-node cluster like SigNoz is maintained for local development, and `deployments/signoz/` is gone. So the page deferred to future work that had already shipped under a different name, and handed readers a command that errors out. It now points at `make obs-grafana` (a single-container Grafana LGTM stack) and links the deployment section. Caught by the docs review on the WH001 change above rather than by anything in that change — the two share only the observation that prose outliving its subject is invisible until someone reads the whole file. + ## [0.1.0] - 2026-08-19 The first public release. Everything below shipped in it — the sections are grouped the way Keep a Changelog asks for, but since there is no previous release to compare against, a reader upgrading from nothing can treat the whole file as "Added". The date is the intended cut date; correct it if tagging slips, and move anything merged in between up from `## Unreleased`.