From 70473e8e1052c649e0222db797b09179c79da421 Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:39:24 +0200 Subject: [PATCH] B1-2: truth, entry points, and contributor path (#1412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(hooks): guard on the command the hook actually runs pre-commit probed one binary and invoked another. The protocol block guarded on `command -v go` and then ran `make protocol-verify`; the sqlc block guarded on `command -v sqlc` and ran `make sqlc-verify`. `make` is not on PATH on a stock Windows box, so a contributor with Go installed but no make had their commit rejected with pre-commit: FAIL: protocol constants are stale — run 'make protocol-generate' in Server/ and stage the result when nothing had been generated and nothing compared. The real cause was `make: not found`, and the advice the message gives fails the same way. Rather than add a `command -v make` guard, inline what the two Makefile targets reduce to — `sqlc generate` / `go run ./scripts/genprotocol` followed by `git diff --exit-code`. Same semantics, one less prerequisite, and it doubles as the make-free equivalent B1-2 asks for. The Makefile targets stay for anyone who prefers them. Also: the protocol block was the only one with no `else`, so a contributor without Go got no check and no notice. It now warns like its two siblings. And gofmt is a separate binary from go, so it is probed separately. Verified both directions with the hook body replayed verbatim: - Go present, make absent -> passes, no staleness claimed. - schema edited without regenerating -> fails, as it must. Refs RL-20 / L-14. * docs: state one branch and PR model Active documents contradicted each other head-on. README.md and docs/contributing.md said branch from `dev` and target `dev`; CLAUDE.md said branch from `main`, PR to `main`. That is R-02, and the 2026-08-19 audit had already recorded it as D-06 without it being resolved. `dev` is the answer, and the repository already behaves that way: B0 made `dev` PR-only with ten required checks enforced on admins, and #1409, #1410 and #1411 all landed there. `main` carries releases. docs/contributing.md becomes the single source of truth. It now states the model, what protection is actually applied, and the two consequences a contributor meets on their first PR — that a self-mergeable PR still cannot merge red, and that Docker and Tauri Full Build report as skipped against `dev` rather than failing. Everywhere else summarises and links here. - CLAUDE.md: corrected, with a link rather than a second copy. - CONTRIBUTING.md: new. GitHub's contributing-guidelines affordance only resolves the root, .github/ or docs/ — `docs/contributing.md` is not a path it finds, so the link never appeared on issues or PRs. - PULL_REQUEST_TEMPLATE.md: names the base branch, which it did not. - bughunt-run skill: reviewed the branch against `origin/main`, which is the wrong base once every PR targets `dev`. README.md already said `dev` and is left as the short summary it should be. Dated audits and the historical remediation plan keep their `main`-era wording — they are records. Refs R-02. * fix(hooks): pick the pre-push base from the nearest integration branch pre-push decided which side's gates to run from `git diff --name-only origin/main...HEAD`. That was right when everything targeted `main`. Once `dev` became the integration branch it stopped being right: a branch cut from `dev` diffed against `main` counts everything on `dev` and not yet on `main` as "changed". Measured on this branch: the old base reported 609 changed files, the new one reports 6. So in practice the hook was running the full server build matrix and the client typecheck plus eslint on every push, whatever the change touched — the file-based narrowing it exists for never engaged. Now it picks whichever of origin/dev, origin/main is nearest, by commits between merge-base and HEAD, skipping a candidate that scores 0. Verified: a branch cut from dev picks origin/dev (2 ahead); dev itself scores 0 against dev and picks origin/main (8 ahead), which is what a dev -> main release PR wants. With no candidate resolvable it falls back to the existing `__all__`, so an unfetched or shallow clone still runs everything. Refs RL-20 / L-14, R-02. * chore(node): one Node source of truth `.nvmrc` and all ten `actions/setup-node` pins said 24; five active documents and the repo's only `engines` block still said 20. A contributor following the docs installed a version CI does not run. Node 24 wins — it is what CI already runs. Every manifest now declares `engines`, and `engine-strict=true` turns a wrong major into a failed install rather than an `EBADENGINE` warning nobody reads. `>=24` rather than `^24` so a Node 26 box keeps working; `Client/.nvmrc` stays the human-facing pin and the docs point at it instead of restating a number. The `.npmrc` is per package root, not one at the top. npm reads the project `.npmrc` from the package directory and does not walk parents — verified with a throwaway package requiring node >=99: with only a parent `.npmrc` npm warned and exited 0; with one in the package directory it failed `notsup`. A single root file would have left `Client/`, the package that matters most, on warnings. Five docs, not the four previously identified — `docs/mcp-introspect.md` also said 20. And `docs/contributing.md` claimed "`.nvmrc` + CI both say Node 20", which was wrong about both. Verified both directions in all three package roots: Node 22 fails `notsup`; Node 24 installs clean and `npm ci` passes in Client/. Refs RL-17 / C-01, ENV-01. * docs: add the documentation landing page `docs/` had 24 top-level files and no index. The root README carried a flat list of 22 links that had drifted: six documents were reachable from nowhere at all — including both 2026-08-23 audits and the test audit — and two entries were labelled "latest" while newer unlinked audits existed. docs/README.md is the index RL-12 asked for. It groups by what a document *is*, because that is what decides whether to trust it: guidance tells you how to do something, reference describes a contract the code implements, audits are dated snapshots nobody updates, plans record intent. Every tracked file under docs/ now appears exactly once, and the audit table says plainly that audit-2026-08-19.md still claims "0 open findings" when the ledger has 38. The root README keeps a short curated list and defers to the index, rather than maintaining a second copy that drifts again. Two fixes while there: `docs/plans/` was linked as a bare directory, unlike its two sibling directory entries, and was annotated "each carries a verified status header" — which docs/plans/README.md:7-9 explicitly contradicts, since a plan's header is exactly the thing that drifts and the index is the authority. Verified: 78 relative links across the new and edited files resolve, and no tracked docs/ file is unreachable from the index. Refs RL-12 / R-06. * feat(scripts): root command facade Entry points existed only inside Server/ (a Makefile) and Client/ (npm scripts). Nothing at the root told a new contributor where to start, and the root package.json had three scripts, none of which built or tested anything. `npm run check` from the root now runs what CI gates on, and check:server / check:client / check:rust run one stack. scripts/run.mjs is dependency-free Node — the shape render-ledger.mjs already uses — so `npm run check` works before `npm install` has. Cross-platform by construction: every step is spawned with an explicit cwd and no shell, so there is nothing to quote and no `cd &&` to behave differently on Windows. npm and npx get their .cmd suffix there. No step shells out to make. The facade orchestrates; it is not a new required path. Each step prints the command and the directory before running it, and those are exactly the commands documented per-stack — so a server contributor can read the output and type them instead, and still never needs Node. Tools CI installs but a contributor may not have (golangci-lint, which has no wrapper in this repo at all; sqlc, pinned by Server/sqlc.version) are skipped with a printed reason rather than failing. Three corrections to the ci-check skill while aligning it: - `make sqlc-verify protocol-verify` replaced by what those targets reduce to, so the documented path does not require make either. - `cargo test` -> `cargo test --lib`, which is what ci.yml actually runs. - "NODE_OPTIONS=--no-experimental-webstorage is mandatory on Node 22+" was false. tests/setup.ts installs the shim, CI runs Node 24 without the flag, and the suite was measured passing without it — 192 files / 5257 tests, identical to the flagged run. Also documents the third RL-20 problem, which needed no code: core.hooksPath is exclusive, so `npm run hooks:install` silently disables any .git/hooks/post-commit — including the one `graphify hook install` writes, which CLAUDE.md tells agents to install. Nothing warned about that. Verified: check:client 5257/192 green, check:rust 123 tests + clippy green, --list prints every command, and the optional-tool skip path reports rather than fails. Refs RL-04 / L-04, RL-20 / L-14. * feat(ci): fail on a document that contradicts the findings ledger G-04's remaining half. The ledger is the source of truth for defect counts, but nothing stopped a planning document from stating a different number and nothing noticed when one did. `render-ledger.mjs --check` cannot help: it validates the JSON schema and returns before rendering, so it never reads FINDINGS.md and cannot see drift at all — and no workflow ran it anyway. scripts/check-doc-counts.mjs counts ledger statuses and compares them to what an allow-list of active documents claims, failing with file, line, claimed value and actual. Wired into ci.yml as a job with no npm ci, since the script imports nothing outside node:, and into the facade as `npm run check:docs` — first in `check`, so a contradicted count does not wait behind ten minutes of -race. The patterns are narrow on purpose. A first attempt matched any " " and flagged nineteen things, all false: "the 45 open P1 rows" (issue-register rows, not ledger findings), "All 8 findings F1-F8" (a different register), "G-05 **refuted**" (an identifier), `">=20"` and `CGO_ENABLED=0` (not counts at all). A check that cries wolf gets ignored, which is the failure G-04 already describes. So a number is only read as a claim in three shapes that cannot mean anything else: an enumeration of two or more " " pairs, a status table row in a table that totals itself, and " records/findings" where the ledger is named within three lines. Fifteen selftest assertions pin both directions, and the job runs them before it runs the check. It reads findings-ledger.json directly rather than importing render-ledger.mjs for `validate`/`render`: that module ends in a bare top-level `await main()` with no import.meta.main guard, so importing it rewrites FINDINGS.md as a side effect. Dated docs/audit-*.md are reported, never failed — they are snapshots nobody maintains. audit-2026-08-19.md does claim zero open findings against 38 open, so b0-baseline's "No plan was found claiming '0 open findings'" holds for docs/plans/ but not for docs/. Not included: a real FINDINGS.md render-drift check. That is RL-07 and belongs with the generated-artifact work, not here. Verified: 27 claims across 9 documents agree; corrupting one count in docs/plans/README.md fails the check naming that line, for both the status and the total. Refs G-04. --------- Co-authored-by: Claude --- .claude/skills/bughunt-run/SKILL.md | 10 +- .claude/skills/ci-check/SKILL.md | 31 ++++- .githooks/pre-commit | 23 +++- .githooks/pre-push | 29 +++- .github/PULL_REQUEST_TEMPLATE.md | 5 + .github/workflows/ci.yml | 24 ++++ .npmrc | 5 + CLAUDE.md | 4 +- CONTRIBUTING.md | 24 ++++ Client/.npmrc | 5 + Client/package.json | 4 + README.md | 47 ++++--- docs/README.md | 102 ++++++++++++++ docs/contributing.md | 83 ++++++++++-- docs/mcp-introspect.md | 2 +- docs/quick-start.md | 2 +- package.json | 13 ++ scripts/check-doc-counts.mjs | 200 ++++++++++++++++++++++++++++ scripts/run.mjs | 173 ++++++++++++++++++++++++ tools/mcp-introspect/.npmrc | 5 + tools/mcp-introspect/package.json | 3 +- 21 files changed, 740 insertions(+), 54 deletions(-) create mode 100644 .npmrc create mode 100644 CONTRIBUTING.md create mode 100644 Client/.npmrc create mode 100644 docs/README.md create mode 100644 scripts/check-doc-counts.mjs create mode 100644 scripts/run.mjs create mode 100644 tools/mcp-introspect/.npmrc diff --git a/.claude/skills/bughunt-run/SKILL.md b/.claude/skills/bughunt-run/SKILL.md index 7c441362..ec21638e 100644 --- a/.claude/skills/bughunt-run/SKILL.md +++ b/.claude/skills/bughunt-run/SKILL.md @@ -314,10 +314,12 @@ finding must be excised from history (amend + rebase onto the amended commit), not merely removed by a follow-up commit. Then review the branch against the merge-base — `git diff -origin/main...HEAD` (three-dot), never two-dot: a concurrent merge plus a -background fetch can move origin/main mid-run and turn the two-dot diff into -phantom deletions. If origin moved, confirm zero file overlap and a clean -`git merge-tree --write-tree origin/main HEAD` before opening the PR by +origin/dev...HEAD` (three-dot), never two-dot: a concurrent merge plus a +background fetch can move the base mid-run and turn the two-dot diff into +phantom deletions. `dev` is the integration branch every PR targets +(docs/contributing.md#branch-and-pr-model); use `origin/main` only for a +release PR cut from `dev`. If origin moved, confirm zero file overlap and a +clean `git merge-tree --write-tree origin/dev HEAD` before opening the PR by hand. The workflow never pushes and never opens a PR. diff --git a/.claude/skills/ci-check/SKILL.md b/.claude/skills/ci-check/SKILL.md index f7b7e5ad..3c95c278 100644 --- a/.claude/skills/ci-check/SKILL.md +++ b/.claude/skills/ci-check/SKILL.md @@ -9,6 +9,13 @@ description: Run the local mirror of OwnCord's CI gates before pushing. Use when Run only the sections your change touches. Server and client are independent. +From the repository root, `npm run check` runs all of it, and +`check:server` / `check:client` / `check:rust` run one stack. `node +scripts/run.mjs --list` prints the exact command each step runs and the +directory it runs in — the per-stack commands below are those commands, and +staying with them is fine. Nothing here needs `make`, and server work needs no +Node. + ## Server (from `Server/`) All four build-tag variants must compile — the tags gate whole files, so a @@ -20,7 +27,11 @@ go vet ./... go test -race ./... go test -tags deadlock -count=1 ./ws/ # deadlock detector; ws is where lock order actually varies golangci-lint run # CI pins v2.11.3 -make sqlc-verify protocol-verify # generated output must not be stale + +# Generated output must not be stale. These are what `make sqlc-verify` and +# `make protocol-verify` reduce to — make is not on PATH on a stock Windows box. +sqlc generate && git diff --exit-code db/dbgen +go run ./scripts/genprotocol && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts ``` Add `-tags wazero` to `go vet`/`go test` when you touched `plugin/`. @@ -37,20 +48,23 @@ still in progress. ## Client (from `Client/`) ```bash -NODE_OPTIONS=--no-experimental-webstorage npm test +npm test npm run typecheck npm run lint npm run format:check ``` -The `NODE_OPTIONS` flag is mandatory on Node 22+ — see the client CLAUDE.md. +`NODE_OPTIONS=--no-experimental-webstorage` used to be required here. It is not +any more: `tests/setup.ts` installs an in-memory `localStorage` shim, CI runs +Node 24 without the flag (`ci.yml`), and the full suite was measured passing +without it — 192 files / 5257 tests, identical to the flagged run. `npm audit --audit-level=high` and `knip` also run in CI but are advisory. ## Rust (from `Client/src-tauri/`) ```bash -cargo test +cargo test --lib # CI runs --lib; plain `cargo test` also builds the bin target cargo clippy --all-targets -- -D warnings ``` @@ -67,3 +81,12 @@ CI on PRs to `main` and pulls heavy system dependencies. server build variants plus tsc and eslint. `OWNCORD_PREPUSH_TESTS=1` adds server tests. Bypass with `--no-verify` or `OWNCORD_SKIP_HOOKS=1` — CI still enforces everything. + +**`core.hooksPath` is exclusive, not additive.** Once set, Git resolves every +hook against `.githooks/` and stops consulting `.git/hooks/` entirely. +`.githooks/` holds only `pre-commit` and `pre-push`, so running +`hooks:install` **silently disables any locally installed `post-commit`** — +including the one `graphify hook install` writes (`CLAUDE.md`). Nothing warns +you. If you want both, either re-install graphify's hook as +`.githooks/post-commit` (untracked, and it stays yours), or skip +`hooks:install` and run the checks through `npm run check` instead. diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 67afdae2..2f060f68 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -22,31 +22,40 @@ fail() { # ---------- Server (Go) ---------- go_staged=$(printf '%s\n' "$staged" | grep '^Server/.*\.go$' | grep -v '^Server/db/dbgen/') if [ -n "$go_staged" ]; then - if command -v go >/dev/null 2>&1; then + if command -v go >/dev/null 2>&1 && command -v gofmt >/dev/null 2>&1; then # shellcheck disable=SC2086 — repo paths contain no spaces unformatted=$(gofmt -l $go_staged) [ -n "$unformatted" ] && fail "gofmt needed (run gofmt -w on): $unformatted" (cd Server && go vet ./...) || fail "go vet" else - printf 'pre-commit: WARNING: go not installed; skipping Go checks.\n' >&2 + printf 'pre-commit: WARNING: go/gofmt not installed; skipping Go checks.\n' >&2 fi fi +# These two blocks inline what `make sqlc-verify` / `make protocol-verify` reduce +# to (Server/Makefile), rather than shelling out to make. `make` is not on PATH on +# a stock Windows box, and a guard on `go`/`sqlc` does not imply it: the old code +# probed one command and invoked another, so a contributor with Go but no make was +# told "protocol constants are stale" when nothing had been generated or compared. + # sqlc inputs changed -> regenerated db/dbgen must be part of the same commit. if printf '%s\n' "$staged" | grep -qE '^Server/(db/queries/|migrations/|sqlc\.yaml|sqlc\.version)'; then if command -v sqlc >/dev/null 2>&1; then - (cd Server && make sqlc-verify) \ - || fail "db/dbgen is stale — run 'make sqlc-generate' in Server/ and stage the result" + (cd Server && sqlc generate && git diff --exit-code db/dbgen) \ + || fail "db/dbgen is stale — run 'sqlc generate' in Server/ and stage the result" else - printf 'pre-commit: WARNING: sqlc not installed (make sqlc-install); CI will run sqlc-verify.\n' >&2 + printf 'pre-commit: WARNING: sqlc not installed; skipping the db/dbgen check. Install the version pinned in Server/sqlc.version. CI will run it.\n' >&2 fi fi # Protocol schema changed -> regenerated Go + TS constants must be in the same commit. if printf '%s\n' "$staged" | grep -qE '^(docs/protocol-schema\.json|Server/scripts/genprotocol/)'; then if command -v go >/dev/null 2>&1; then - (cd Server && make protocol-verify) \ - || fail "protocol constants are stale — run 'make protocol-generate' in Server/ and stage the result" + (cd Server && go run ./scripts/genprotocol \ + && git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts) \ + || fail "protocol constants are stale — run 'go run ./scripts/genprotocol' in Server/ and stage the result" + else + printf 'pre-commit: WARNING: go not installed; skipping the protocol-constants check. CI will run it.\n' >&2 fi fi diff --git a/.githooks/pre-push b/.githooks/pre-push index 90d06bf6..277f26a5 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -15,9 +15,34 @@ fail() { exit 1 } -# What changed relative to origin/main decides which side's gates run. +# What changed relative to this branch's base decides which side's gates run. +# +# The base is whichever of origin/dev, origin/main is NEAREST — the one with the +# fewest commits between its merge-base and HEAD. A feature branch cut from dev +# picks dev; dev itself scores 0 against dev (nothing to compare) and so picks +# main, which is right for a dev -> main release PR. Hardcoding origin/main got +# the first case wrong once dev became the integration branch: everything on dev +# and not yet on main counted as "changed", so both sides' gates ran every time. +# # Markdown/docs changes never trigger builds. -changed=$(git diff --name-only origin/main...HEAD 2>/dev/null) || changed="__all__" +base="" +best="" +for cand in origin/dev origin/main; do + git rev-parse --verify -q "$cand" >/dev/null 2>&1 || continue + mb=$(git merge-base "$cand" HEAD 2>/dev/null) || continue + n=$(git rev-list --count "$mb..HEAD" 2>/dev/null) || continue + [ "$n" -eq 0 ] && continue + if [ -z "$best" ] || [ "$n" -lt "$best" ]; then + base=$cand + best=$n + fi +done + +if [ -n "$base" ]; then + changed=$(git diff --name-only "$base...HEAD" 2>/dev/null) || changed="__all__" +else + changed="__all__" +fi [ "$changed" = "__all__" ] || changed=$(printf '%s\n' "$changed" | grep -v '\.md$') [ -z "$changed" ] && exit 0 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8d98d305..b17e22bc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,10 @@ # Pull Request + + ## Summary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83920885..957ce528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,30 @@ jobs: # and must stay green — never "fix" a failing test by editing its assertions. # ubuntu-latest for the same reason as client-check above: jsdom-only vitest # with no platform-conditional code under test. + # The automated half of G-04: a planning document that states a finding count + # the ledger contradicts fails here instead of quietly misleading a reader. + # Deliberately tiny — no npm ci, because the script imports nothing outside + # node:. It also runs its own selftest, since the whole check rests on + # patterns narrow enough not to cry wolf. + docs-consistency: + name: Docs & Ledger Consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24 + + - name: Self-test the count matcher + run: node scripts/check-doc-counts.mjs --selftest + + - name: Documents must agree with the findings ledger + run: node scripts/check-doc-counts.mjs + + - name: Ledger schema is valid + run: node .superpowers/render-ledger.mjs --check + client-tests: name: Client Unit Tests runs-on: ubuntu-latest diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..2e536932 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# engine-strict makes the engines block in package.json a hard error rather +# than an npm warning. npm reads the project .npmrc from the package +# directory and does not walk parent directories, so this file has to exist +# in every package root or the gate silently downgrades to a warning there. +engine-strict=true diff --git a/CLAUDE.md b/CLAUDE.md index 06ca4cbf..95e8bde0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,4 +60,6 @@ else under `.superpowers/` is per-session scratch and stays local. - Security issues go through GitHub Security Advisories, never public issues (`docs/security.md`). This repo is public — unfixed defects do not belong in commits, issues, or PR descriptions. -- Branch from `main`, PR to `main`, squash merge, conventional commit subjects. +- Branch from `dev` and PR to `dev` — `dev` is the integration branch and is + PR-only; `main` carries releases. Squash merge, conventional commit subjects. + Full model: [docs/contributing.md](docs/contributing.md#branch-and-pr-model). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..99b28710 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to OwnCord + +The full guide lives in **[docs/contributing.md](docs/contributing.md)** — +environment setup, the branch model, coding standards, and how to run the +checks CI runs. + +This file exists so GitHub can find it: the contributing-guidelines link that +appears on new issues and pull requests only resolves `CONTRIBUTING.md` at the +repository root, in `.github/`, or in `docs/`. + +Three things worth knowing before you open a pull request: + +- **Branch from `dev` and target `dev`.** `main` carries releases only. See + [Branch and PR model](docs/contributing.md#branch-and-pr-model). +- **Run the checks first.** `npm run check` from the repository root, or the + per-stack commands in [docs/contributing.md](docs/contributing.md). CI takes + about 15 minutes and enforces more than a plain build and test. +- **Report security issues privately**, through GitHub Security Advisories — + never a public issue or pull request. See [SECURITY.md](SECURITY.md) and + [docs/security.md](docs/security.md). + +New to the codebase? [docs/README.md](docs/README.md) indexes everything, and +[docs/architecture/](docs/architecture/README.md) explains how the server and +client fit together. diff --git a/Client/.npmrc b/Client/.npmrc new file mode 100644 index 00000000..2e536932 --- /dev/null +++ b/Client/.npmrc @@ -0,0 +1,5 @@ +# engine-strict makes the engines block in package.json a hard error rather +# than an npm warning. npm reads the project .npmrc from the package +# directory and does not walk parent directories, so this file has to exist +# in every package root or the gate silently downgrades to a warning there. +engine-strict=true diff --git a/Client/package.json b/Client/package.json index 84e098e2..fcd575c3 100644 --- a/Client/package.json +++ b/Client/package.json @@ -3,6 +3,10 @@ "private": true, "version": "1.2.0-alpha.3", "type": "module", + "engines": { + "node": ">=24", + "npm": ">=10" + }, "scripts": { "dev": "vite", "build": "tsc -p tsconfig.build.json && vite build", diff --git a/README.md b/README.md index 1f91503a..dbbdcf65 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Two main components: ### Prerequisites - Go 1.26+ -- Node.js 20+ +- Node.js 24+ (see `Client/.nvmrc`) - Rust stable (client builds) ### Build from source @@ -152,6 +152,17 @@ npm run tauri build ### Core verification commands +Everything CI gates on, from the repository root: + +```bash +npm run check # server + client + Rust +npm run check:server # or one stack at a time +node scripts/run.mjs --list # exactly what each task runs, and where +``` + +Or run the stacks directly — the facade is a convenience, not the only path, +and **server work needs no Node at all**: + ```bash # Server cd Server @@ -202,28 +213,22 @@ When rotating the server updater key, update [Server/updater/server_update_publi ## Docs Index -- [docs/quick-start.md](docs/quick-start.md) -- [docs/deployment.md](docs/deployment.md) -- [docs/livekit-setup.md](docs/livekit-setup.md) -- [docs/port-forwarding.md](docs/port-forwarding.md) -- [docs/tailscale.md](docs/tailscale.md) +**[docs/README.md](docs/README.md) is the complete index** — every document in +`docs/`, grouped by whether it is guidance, a reference contract, a dated audit, +or a plan. The most-used entries: + +- [docs/quick-start.md](docs/quick-start.md) — get a server running +- [docs/deployment.md](docs/deployment.md) — production deployment +- [docs/contributing.md](docs/contributing.md) — setup, branch model, how to run the checks CI runs +- [docs/security.md](docs/security.md) — reporting a vulnerability - [docs/architecture/](docs/architecture/README.md) — system blueprints (diagrams + flows) -- [docs/audit-2026-08-04-docs-and-coverage.md](docs/audit-2026-08-04-docs-and-coverage.md) — latest full audit (docs accuracy, UX flow coverage, test runs) -- [docs/audit-2026-08-04.md](docs/audit-2026-08-04.md) — latest security review -- [docs/audit-2026-07-19.md](docs/audit-2026-07-19.md) — architecture & spec-conformance audit -- [docs/api.md](docs/api.md) -- [docs/protocol.md](docs/protocol.md) -- [docs/schema.md](docs/schema.md) -- [docs/architecture/client.md](docs/architecture/client.md) — client architecture (replaces client-architecture.md) - [docs/architecture/ux/](docs/architecture/ux/README.md) — client UX specification (target-state flows, per-view states, event→reaction maps) -- [docs/server-configuration.md](docs/server-configuration.md) -- [docs/credential-storage.md](docs/credential-storage.md) -- [docs/mcp-introspect.md](docs/mcp-introspect.md) — dev-only MCP server for introspecting a running instance -- [docs/audit-test-coverage-2026-07-25.md](docs/audit-test-coverage-2026-07-25.md) — test-coverage audit -- [docs/audit-2026-04-07.md](docs/audit-2026-04-07.md) — first comprehensive audit -- [docs/plans/](docs/plans/) — design plans and decision records (each carries a verified status header) -- [docs/contributing.md](docs/contributing.md) -- [docs/security.md](docs/security.md) +- [docs/api.md](docs/api.md), [docs/protocol.md](docs/protocol.md), [docs/schema.md](docs/schema.md), [docs/server-configuration.md](docs/server-configuration.md) — reference contracts +- [docs/plans/README.md](docs/plans/README.md) — plan index; records each plan's state and is the authority over a plan's own header + +Audits are dated snapshots and are not maintained after the fact — read them as +history. [docs/README.md](docs/README.md#audits--dated-not-maintained) lists all +nine, newest first. ## Contributing diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..a48e6d30 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,102 @@ +# Documentation index + +Every document in `docs/` is listed here. If it is not on this page it is not +current guidance. + +Docs fall into four kinds, and the difference matters when you are deciding +whether to trust one: **guidance** tells you how to do something, +**reference** describes a contract the code actually implements, **audits** are +dated snapshots that were true when written and were never updated, and +**plans** record intent. Read an audit as history, not as status. + +## Start here + +| I want to… | Read | +| --- | --- | +| Run a server | [quick-start.md](quick-start.md) | +| Deploy for real | [deployment.md](deployment.md) | +| Contribute a change | [contributing.md](contributing.md) | +| Understand the system | [architecture/](architecture/README.md) | +| Report a vulnerability | [security.md](security.md) | + +## Guidance + +| Document | Covers | +| --- | --- | +| [quick-start.md](quick-start.md) | Getting a server running with the fewest steps. | +| [deployment.md](deployment.md) | Production deployment on Windows and Linux. | +| [contributing.md](contributing.md) | Environment setup, **the branch and PR model**, coding standards, how to run the checks CI runs. | +| [security.md](security.md) | How to report a vulnerability, and how findings are handled in public vs private. | +| [livekit-setup.md](livekit-setup.md) | Standing up the LiveKit SFU for voice and video. | +| [port-forwarding.md](port-forwarding.md) | Making a server reachable from outside the LAN. | +| [tailscale.md](tailscale.md) | Remote access without port forwarding. | +| [mcp-introspect.md](mcp-introspect.md) | Dev-only MCP server for introspecting a running instance. | + +## Reference + +These describe contracts the code implements. If one disagrees with the code, +the code is right and the document is a bug. + +| Document | Covers | +| --- | --- | +| [api.md](api.md) | REST API under `/api/v1`. | +| [protocol.md](protocol.md) | WebSocket protocol — frames, sequencing, reconnect. | +| [schema.md](schema.md) | SQLite schema and migrations. | +| [server-configuration.md](server-configuration.md) | Every server configuration option. | +| [credential-storage.md](credential-storage.md) | What the desktop client persists, and where. | +| [protocol-schema.json](protocol-schema.json) | **Generated-code source of truth.** `Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are generated from it — never hand-edit either. | + +## Architecture + +[architecture/README.md](architecture/README.md) indexes the blueprints and +carries the maintenance rule: each blueprint names its source-of-truth files, +and a PR touching those updates the blueprint in the same change. + +- [system-overview.md](architecture/system-overview.md), [server.md](architecture/server.md), [client.md](architecture/client.md) +- [data-model.md](architecture/data-model.md), [websocket.md](architecture/websocket.md), [voice-e2ee.md](architecture/voice-e2ee.md) +- [ux/](architecture/ux/README.md) — target-state UX spec, per-view states and event→reaction maps + +[client-architecture.md](client-architecture.md) is a redirect stub; the live +document is [architecture/client.md](architecture/client.md). + +## Audits — dated, not maintained + +Point-in-time snapshots. They are **not** updated as the code moves, and they +are deliberately left alone when paths change, so links from commit messages +keep resolving. Anything here may be stale; the ledger and the plan index carry +current status. + +| Audit | Scope | +| --- | --- | +| [audit-2026-08-23-repository-layout.md](audit-2026-08-23-repository-layout.md) | Repository layout and contributor experience (`RL-01`…`RL-22`). | +| [audit-2026-08-23-repository-health.md](audit-2026-08-23-repository-health.md) | Full repository health. | +| [audit-2026-08-19.md](audit-2026-08-19.md) | Repo health. **States "0 open findings" — untrue since; see the ledger.** | +| [audit-test-coverage-2026-08-19.md](audit-test-coverage-2026-08-19.md) | Test audit (`T-*`, a separate register from the `OC-*` ledger). | +| [audit-2026-08-04-docs-and-coverage.md](audit-2026-08-04-docs-and-coverage.md) | Documentation accuracy and UI/UX test coverage. | +| [audit-2026-08-04.md](audit-2026-08-04.md) | Security review. | +| [audit-test-coverage-2026-07-25.md](audit-test-coverage-2026-07-25.md) | Test-coverage audit. | +| [audit-2026-07-19.md](audit-2026-07-19.md) | Architecture and spec-conformance review. | +| [audit-2026-04-07.md](audit-2026-04-07.md) | First comprehensive audit. | + +## Plans + +[plans/README.md](plans/README.md) indexes every plan with a recorded state — +active, partially implemented, design-only, or shipped — and **is the authority +over a plan's own header**, which can drift. + +## Where status actually lives + +Do not read a defect count, or a "what works" claim, out of a document on this +page. Status has owners: + +| Concern | Source of truth | +| --- | --- | +| Defect status | `.superpowers/findings-ledger.json` (`FINDINGS.md` is rendered from it) | +| Security-sensitive defects | Private GitHub Security Advisories | +| Phase order and gates | [plans/repo-health-roadmap-2026-08-23.md](plans/repo-health-roadmap-2026-08-23.md) | +| Current measured baseline | [plans/b0-baseline-2026-08-25.md](plans/b0-baseline-2026-08-25.md) | +| Generated-code contracts | `CLAUDE.md`, "Generated code — never hand-edit" | + +A CI job checks that documents on this page do not contradict the ledger's +counts. Adding a count to a document means adding it to that check's allow-list +in `scripts/check-doc-counts.mjs`. diff --git a/docs/contributing.md b/docs/contributing.md index 3dc7e6a5..74802cbd 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,12 +13,39 @@ How to set up the development environment and contribute to OwnCord. | Linux ARM64 | ✅ | ✅ (CI only) | - **Go 1.26+** (server) -- **Node.js 20+** (client) +- **Node.js 24+** (client) — pinned in `Client/.nvmrc`; `engine-strict` makes a + wrong major a hard failure, not a warning - **Rust / Cargo** (Tauri client — not needed for server-only work) - **Docker + Compose v2** (optional — alternative to building the server locally) ### Available Commands +#### Root facade — one entry point + +From the repository root. These orchestrate the per-stack commands below; they +are a convenience, not a replacement. Nothing here needs `make`, and everything +works the same on Windows, macOS and Linux. + +| Command | Description | +|---------|-------------| +| `npm run bootstrap` | `npm ci` in all three package roots | +| `npm run check` | Everything CI gates on: server, client, Rust | +| `npm run check:server` | Server only — build variants, vet, race, deadlock, lint, generated-output drift | +| `npm run check:client` | Client only — typecheck, lint, format, unit + integration tests | +| `npm run check:rust` | Tauri backend — `cargo test --lib` and clippy | +| `npm run check:docs` | Fail if a watched document states a finding count the ledger contradicts | +| `npm run format` | Prettier over the client, `gofmt -w` over the server | +| `npm run generate` | Regenerate protocol constants and the sqlc query layer | +| `npm run release:preflight` | `check` plus a client production build | +| `node scripts/run.mjs --list` | Print the exact command every task runs, and where | + +Tools CI installs but you may not have — `golangci-lint`, `sqlc` — are skipped +with a printed reason rather than failing the run. + +**Working on the server only? You never need Node.** The facade prints each +command it runs and the directory it runs it in; those are the commands in the +next section, and using them directly is equally correct. + #### Server (Go) | Command | Description | @@ -103,6 +130,15 @@ npm run hooks:install # = git config core.hooksPath .githooks Bypass with `--no-verify` or `OWNCORD_SKIP_HOOKS=1` when needed — CI still enforces everything. +Neither hook needs `make`, and neither needs Node for the Go checks. + +**`core.hooksPath` is exclusive.** Once set, Git resolves every hook against +`.githooks/` and never looks in `.git/hooks/` again. `.githooks/` holds only +`pre-commit` and `pre-push`, so `hooks:install` silently disables any +`post-commit` you installed there — `graphify hook install` writes one. Nothing +warns you. Put it at `.githooks/post-commit` instead (untracked, so it stays +yours), or skip `hooks:install` and use `npm run check` before pushing. + ## Plugin Development Plugins are WASM modules loaded at runtime when the server is built with `-tags wazero`. @@ -121,10 +157,27 @@ functions is equally valid — TinyGo is just the example toolchain used by `exa --- -## Active Branches +## Branch and PR model -- `main` -- stable releases -- `dev` -- active development +This section is the single source of truth for the branch model. Everywhere +else -- the root `README.md`, `CLAUDE.md`, the PR template -- summarises it and +links here rather than restating it. + +- `dev` -- the integration branch. **All contributions target `dev`.** +- `main` -- releases only. `dev` is merged to `main` for a release, and release + tags are cut from `main`. + +`dev` is protected and PR-only: direct pushes are rejected, ten status checks +are required, `required_approving_review_count` is 0, and the rule is enforced +on admins. So a PR is self-mergeable once CI is green, but no commit reaches +`dev` without CI having run on it. Settings and rationale live in +[`docs/plans/b0-dev-branch-protection.sh`](plans/b0-dev-branch-protection.sh). + +Two consequences worth knowing before you open a PR: + +- The Docker and Tauri Full Build jobs are gated on `main` and report as + *skipped* on a PR into `dev`. That is expected, not a failure. +- Squash merge, and a conventional commit subject on the squashed commit. ## Branch Naming @@ -149,11 +202,15 @@ ci: add lint step to GitHub Actions ## Pull Request Process -1. Branch from `dev` (the active development branch) -2. PRs target `dev`; `dev` is merged to `main` for releases, which are cut from tagged commits on `main` -3. CI must pass (build + test + lint) +See [Branch and PR model](#branch-and-pr-model) above for what to branch from +and target. + +1. Branch from `dev` +2. Open the PR against `dev` +3. All ten required checks must pass -- `dev` is protected, so a red PR cannot + merge 4. Request code review -5. Squash merge preferred +5. Squash merge, conventional commit subject ## Testing @@ -189,7 +246,9 @@ closing audit findings 2026-04-07 #8 / DC-11): triaged in the workflow comment instead of blocking on unfixable pins), `govulncheck` for Go, `cargo audit` for Rust, and `knip` refuses unused client dependencies outright. -- **Version skew is pinned at the toolchain level** too: `.nvmrc` + CI both - say Node 20, `Server/sqlc.version` pins sqlc, Go pins via `go.mod` - (`GOTOOLCHAIN=auto`), and GitHub Actions are SHA-pinned with Dependabot - bumping the pins. +- **Version skew is pinned at the toolchain level** too: `Client/.nvmrc`, every + `actions/setup-node` in CI, and an `engines` block in all three + `package.json` files say Node 24 — with `engine-strict=true` in each + package's `.npmrc`, so a wrong major fails the install instead of warning. + `Server/sqlc.version` pins sqlc, Go pins via `go.mod` (`GOTOOLCHAIN=auto`), + and GitHub Actions are SHA-pinned with Dependabot bumping the pins. diff --git a/docs/mcp-introspect.md b/docs/mcp-introspect.md index 5723399e..b61d23c5 100644 --- a/docs/mcp-introspect.md +++ b/docs/mcp-introspect.md @@ -9,7 +9,7 @@ adds nothing to the server binary — it is a thin wrapper over OwnCord's existi the client's on-disk log. - **Code:** `tools/mcp-introspect/index.mjs` (one file, ~270 lines) -- **Runtime:** Node ≥ 20, ESM. One real dependency: `@modelcontextprotocol/sdk` (+ `zod`) +- **Runtime:** Node ≥ 24, ESM. One real dependency: `@modelcontextprotocol/sdk` (+ `zod`) - **Registration:** `/.mcp.json` (committed) and `.claude/settings.local.json` (local) --- diff --git a/docs/quick-start.md b/docs/quick-start.md index 75075792..17235f45 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -20,7 +20,7 @@ Get OwnCord running with the fewest possible steps. ## Prerequisites - Go 1.26+ (only if building server from source) -- Node.js 20+ and Rust (only if building client from source) +- Node.js 24+ and Rust (only if building client from source) - Docker + Compose v2 (Docker path only) - LiveKit (optional, required for voice/video) diff --git a/package.json b/package.json index e02ad0b3..18dec33b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,19 @@ { "private": true, + "engines": { + "node": ">=24", + "npm": ">=10" + }, "scripts": { + "bootstrap": "node scripts/run.mjs bootstrap", + "check": "node scripts/run.mjs check", + "check:server": "node scripts/run.mjs check:server", + "check:client": "node scripts/run.mjs check:client", + "check:rust": "node scripts/run.mjs check:rust", + "check:docs": "node scripts/run.mjs check:docs", + "format": "node scripts/run.mjs format", + "generate": "node scripts/run.mjs generate", + "release:preflight": "node scripts/run.mjs release:preflight", "changelog": "changelogen", "release": "changelogen --release", "hooks:install": "git config core.hooksPath .githooks" diff --git a/scripts/check-doc-counts.mjs b/scripts/check-doc-counts.mjs new file mode 100644 index 00000000..fd4ca60f --- /dev/null +++ b/scripts/check-doc-counts.mjs @@ -0,0 +1,200 @@ +#!/usr/bin/env node +// Fail when an active document states a finding count the ledger contradicts +// (the automated half of G-04). +// +// node scripts/check-doc-counts.mjs +// node scripts/check-doc-counts.mjs --selftest +// +// Scope, deliberately small: this counts ledger statuses and compares them to +// the numbers active documents assert. It is not a document-status framework, +// and it does not check that FINDINGS.md is in sync with the ledger — that is a +// different check (RL-07) with a different owner. +// +// It reads findings-ledger.json directly and does NOT import render-ledger.mjs. +// That module has no `import.meta.main` guard, so importing it to reuse +// `validate`/`render` runs `main()` and rewrites FINDINGS.md as a side effect. +// +// ── Why the patterns are narrow ────────────────────────────────────────────── +// "open" is overloaded in this repository. The issue register has 45 open P1 +// *rows*; a security scan closed 8 *findings* F1–F8; `G-05 **refuted**` puts a +// digit next to a status word. None of those are ledger counts, and a loose +// pattern flags all of them — a check that cries wolf gets ignored, which is +// the failure mode G-04 already describes. +// +// So a number is only read as a ledger claim in three unambiguous shapes: +// +// 1. An enumeration — two or more " " pairs on one line, e.g. +// "306 fixed / 38 open / 3 declined / 1 duplicate = 348". A lone +// "45 open" is never enough. +// 2. A status table row "| open | **38** |", but only in a table that also +// carries a "| Total | 348 |" row nearby. +// 3. " records" / " findings", but only where the ledger is named +// within the preceding few lines. +// +// Dated docs/audit-*.md are reported, never failed: they are point-in-time +// snapshots that are deliberately not maintained, and editing them is out of +// scope for the repository-layout work. audit-2026-08-19.md does claim zero +// open findings — true when written, false now, and left alone on purpose. + +import { readFileSync, existsSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +// Active documents that assert a count. Adding a count to a document means +// adding it here — an unlisted document is not checked. +const WATCHED = [ + 'docs/README.md', + 'docs/plans/README.md', + 'docs/plans/hp-0-scorecard-2026-08-25.md', + 'docs/plans/repo-health-issue-register-2026-08-23.md', + 'docs/plans/b0-baseline-2026-08-25.md', + 'docs/plans/b1-repository-foundation-2026-08-25.md', + '.superpowers/FINDINGS.md', + 'CLAUDE.md', + 'README.md', +] + +// Reported but never failed — dated snapshots, see the header. +const REPORT_ONLY = ['docs/audit-'] + +const STATUSES = ['open', 'fixed', 'declined', 'duplicate', 'refuted', 'blocked'] +const S = STATUSES.join('|') + +// Never let a digit that belongs to an identifier or comparison start a claim: +// G-05, >=20, CGO_ENABLED=0, version=1.2.0-alpha.3. +const LEAD = '(?/])' +const PAIR = new RegExp(`${LEAD}(\\d+)\\*{0,2}\\s+\\*{0,2}(${S})\\b`, 'gi') +const LEDGER_CONTEXT = /ledger|OC-\d|findings-ledger|FINDINGS\.md/i + +export function tally(ledger) { + const counts = Object.fromEntries(STATUSES.map((s) => [s, 0])) + for (const f of ledger.findings) if (f.status in counts) counts[f.status]++ + counts.total = ledger.findings.length + return counts +} + +export function claimsIn(text) { + const out = [] + const lines = text.split('\n') + + // Which lines sit in a status table that has a Total row within 10 lines? + const totalRowAt = new Set() + lines.forEach((l, i) => { + if (/^\|\s*\*{0,2}total\*{0,2}\s*\|\s*\*{0,2}\d+\*{0,2}\s*\|/i.test(l)) totalRowAt.add(i) + }) + const nearTotalRow = (i) => [...totalRowAt].some((t) => Math.abs(t - i) <= 10) + + lines.forEach((line, i) => { + const at = i + 1 + + // 1. Enumeration: two or more " " pairs on one line. + const pairs = [...line.matchAll(PAIR)] + if (pairs.length >= 2) { + for (const m of pairs) { + out.push({ line: at, kind: m[2].toLowerCase(), value: Number(m[1]), text: m[0].trim() }) + } + // "... = 348" closing an enumeration is the total. + const eq = line.match(/=\s*\*{0,2}(\d+)\*{0,2}/) + if (eq) out.push({ line: at, kind: 'total', value: Number(eq[1]), text: eq[0].trim() }) + } + + // 2. Status table row, only inside a table that totals itself. + const row = line.match(new RegExp(`^\\|\\s*\\*{0,2}(${S})\\*{0,2}\\s*\\|\\s*\\*{0,2}(\\d+)\\*{0,2}\\s*\\|`, 'i')) + if (row && nearTotalRow(i)) { + out.push({ line: at, kind: row[1].toLowerCase(), value: Number(row[2]), text: row[0].trim() }) + } + const totalRow = line.match(/^\|\s*\*{0,2}total\*{0,2}\s*\|\s*\*{0,2}(\d+)\*{0,2}\s*\|/i) + if (totalRow) out.push({ line: at, kind: 'total', value: Number(totalRow[1]), text: totalRow[0].trim() }) + + // 3. " records"/" findings", only near an explicit mention of the ledger. + const ctx = lines.slice(Math.max(0, i - 3), i + 1).join('\n') + if (LEDGER_CONTEXT.test(ctx)) { + for (const m of line.matchAll(new RegExp(`${LEAD}(\\d+)\\*{0,2}\\s+(?:records?|findings?)\\b`, 'gi'))) { + out.push({ line: at, kind: 'total', value: Number(m[1]), text: m[0].trim() }) + } + } + }) + return out +} + +function main() { + const ledgerPath = join(ROOT, '.superpowers/findings-ledger.json') + if (!existsSync(ledgerPath)) { + console.error(`missing ${ledgerPath}`) + process.exit(1) + } + const counts = tally(JSON.parse(readFileSync(ledgerPath, 'utf8'))) + console.log(`ledger: ${STATUSES.map((s) => `${counts[s]} ${s}`).join(' / ')} = ${counts.total}`) + + const failures = [] + const notes = [] + let claimCount = 0 + + for (const rel of WATCHED) { + const p = join(ROOT, rel) + if (!existsSync(p)) { + failures.push(`${rel}: watched file does not exist — fix the list in scripts/check-doc-counts.mjs`) + continue + } + for (const c of claimsIn(readFileSync(p, 'utf8'))) { + const actual = counts[c.kind] + if (actual === undefined) continue + claimCount++ + if (c.value === actual) continue + const entry = `${rel}:${c.line} claims "${c.text}" — ledger says ${c.kind} = ${actual}` + if (REPORT_ONLY.some((prefix) => rel.startsWith(prefix))) notes.push(entry) + else failures.push(entry) + } + } + + for (const n of notes) console.log(`NOTE ${n}`) + + if (failures.length) { + console.error(`\n${failures.length} document claim(s) contradict the ledger:\n`) + for (const f of failures) console.error(` ${f}`) + console.error( + '\nThe ledger is the source of truth. Update the document, or if the ledger is\n' + + 'wrong, fix .superpowers/findings-ledger.json and re-render FINDINGS.md.', + ) + process.exit(1) + } + console.log(`\n${claimCount} claim(s) across ${WATCHED.length} watched document(s) agree with the ledger.`) +} + +function selftest() { + let failed = 0 + const assert = (cond, msg) => { + console.log(`${cond ? 'PASS' : 'FAIL'} ${msg}`) + if (!cond) failed++ + } + const t = tally({ findings: [{ status: 'open' }, { status: 'open' }, { status: 'fixed' }] }) + assert(t.open === 2 && t.fixed === 1 && t.total === 3, 'tally counts by status and total') + assert(t.refuted === 0, 'a declared-but-unused status counts 0, not undefined') + + const c = claimsIn + const has = (s, kind, value) => c(s).some((x) => x.kind === kind && x.value === value) + + assert(has('Ledger: **306 fixed / 38 open / 3 declined / 1 duplicate = 348**.', 'open', 38), 'enumeration: reads each pair') + assert(has('Ledger: **306 fixed / 38 open / 3 declined / 1 duplicate = 348**.', 'total', 348), 'enumeration: reads the = total') + assert(has('**38 open** · 0 blocked · 306 fixed · 3 declined', 'fixed', 306), 'enumeration: FINDINGS.md header shape') + assert(has('| open | **38** |\n| **Total** | **348** |', 'open', 38), 'status table with a Total row') + assert(has('the ledger holds\n348 records', 'total', 348), '"N records" near a ledger mention') + + // The false positives that made a looser version unusable. + assert(c('The 45 open P1 rows are tracked in the register.').length === 0, 'a lone "45 open" is not a ledger claim') + assert(c('| All 8 findings F1-F8 closed |').length === 0, 'a different register is not a ledger claim') + assert(c('| `golangci-lint` | claimed broken (G-05) | G-05 **refuted** |').length === 0, '"G-05 refuted" is an id, not a count') + assert(c('`tools/mcp-introspect/package.json` (`">=20"`)').length === 0, '">=20" is not a count') + assert(c('go build -ldflags "-X main.version=1.2.0-alpha.3"').length === 0, 'a version string is not a count') + assert(c('11 medium, 27 low').length === 0, 'severities are not statuses') + assert(c('22 sit under Client/').length === 0, 'a bare number is not a claim') + assert(c('348 records in some unrelated table').length === 0, '"N records" without ledger context is ignored') + + console.log(failed ? `\nselftest: ${failed} assertion(s) failed` : '\nselftest: all assertions pass') + process.exit(failed ? 1 : 0) +} + +if (process.argv.includes('--selftest')) selftest() +else main() diff --git a/scripts/run.mjs b/scripts/run.mjs new file mode 100644 index 00000000..20964e6e --- /dev/null +++ b/scripts/run.mjs @@ -0,0 +1,173 @@ +#!/usr/bin/env node +// Root command facade (RL-04 / L-04). +// +// One entry point for the checks CI runs, so a contributor does not have to +// know which directory each stack lives in. `node scripts/run.mjs --list` +// prints every task and the exact commands it runs. +// +// Two rules this file exists to keep: +// +// 1. Cross-platform. No `make`, no shell syntax, no `cd &&`. Every step is +// spawned directly with an explicit `cwd`, so there is no shell to quote +// for and nothing that behaves differently on Windows. +// 2. The facade orchestrates, it never becomes the only path. Each step +// prints the command it runs, in the directory it runs it in, so a +// Go-only contributor can read the output and type those commands +// instead — and never needs Node to work on the server. +// +// Dependency-free by design: Node's standard library only, like +// .superpowers/render-ledger.mjs. Adding a dependency here would mean +// `npm run check` could not run until `npm install` had. + +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const WIN = process.platform === 'win32' + +// npm and npx are batch shims on Windows; everything else is a real binary. +const bin = (c) => (WIN && (c === 'npm' || c === 'npx') ? `${c}.cmd` : c) + +/** A step that always runs. */ +const step = (cmd, args, cwd = '.') => ({ cmd, args, cwd }) + +/** + * A step that is skipped, with a printed reason, when `probe` is not on PATH. + * Used for tools CI installs but a contributor may not have: golangci-lint has + * no wrapper in this repo at all, and sqlc is pinned by Server/sqlc.version. + */ +const optional = (probe, cmd, args, cwd, why) => ({ cmd, args, cwd, probe, why }) + +// `git diff --exit-code` after regenerating is what `make protocol-verify` and +// `make sqlc-verify` reduce to. Inlined so neither needs make. +const PROTOCOL_VERIFY = [ + step('go', ['run', './scripts/genprotocol'], 'Server'), + step('git', ['diff', '--exit-code', 'ws/message_types.go', '../Client/src/lib/protocolTypes.ts'], 'Server'), +] +const SQLC_VERIFY = [ + optional('sqlc', 'sqlc', ['generate'], 'Server', 'sqlc not on PATH — install the version in Server/sqlc.version'), + optional('sqlc', 'git', ['diff', '--exit-code', 'db/dbgen'], 'Server', 'sqlc not on PATH'), +] + +const CHECK_SERVER = [ + step('go', ['build', './...'], 'Server'), + step('go', ['build', '-tags', 'otel', './...'], 'Server'), + step('go', ['build', '-tags', 'wazero', './...'], 'Server'), + step('go', ['build', '-tags', 'otel,wazero', './...'], 'Server'), + step('go', ['vet', './...'], 'Server'), + step('go', ['test', '-race', './...'], 'Server'), + step('go', ['test', '-tags', 'deadlock', '-count=1', './ws/'], 'Server'), + optional('golangci-lint', 'golangci-lint', ['run', './...'], 'Server', 'golangci-lint not on PATH — CI pins v2.11.3'), + ...PROTOCOL_VERIFY, + ...SQLC_VERIFY, +] + +const CHECK_CLIENT = [ + step('npm', ['run', 'typecheck'], 'Client'), + step('npm', ['run', 'lint'], 'Client'), + step('npm', ['run', 'format:check'], 'Client'), + step('npm', ['test'], 'Client'), +] + +// Matches ci.yml's Rust Unit Tests job exactly: --lib for tests, --all-targets +// for clippy. They differ deliberately; do not "align" them. +const CHECK_RUST = [ + step('cargo', ['test', '--lib'], 'Client/src-tauri'), + step('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'], 'Client/src-tauri'), +] + +// Fast and dependency-free, so it goes first: a contradicted count should not +// wait behind ten minutes of -race. +const CHECK_DOCS = [step('node', ['scripts/check-doc-counts.mjs'], '.')] + +const TASKS = { + bootstrap: [ + step('npm', ['ci'], '.'), + step('npm', ['ci'], 'Client'), + step('npm', ['ci'], 'tools/mcp-introspect'), + ], + 'check:server': CHECK_SERVER, + 'check:client': CHECK_CLIENT, + 'check:rust': CHECK_RUST, + 'check:docs': CHECK_DOCS, + check: [...CHECK_DOCS, ...CHECK_SERVER, ...CHECK_CLIENT, ...CHECK_RUST], + generate: [ + step('go', ['run', './scripts/genprotocol'], 'Server'), + optional('sqlc', 'sqlc', ['generate'], 'Server', 'sqlc not on PATH — install the version in Server/sqlc.version'), + ], + format: [ + step('npm', ['run', 'format'], 'Client'), + optional('gofmt', 'gofmt', ['-w', '.'], 'Server', 'gofmt not on PATH'), + ], + 'release:preflight': [ + ...CHECK_DOCS, + ...CHECK_SERVER, + ...CHECK_CLIENT, + ...CHECK_RUST, + step('npm', ['run', 'build'], 'Client'), + ], +} + +function onPath(cmd) { + const probe = spawnSync(WIN ? 'where' : 'command', WIN ? [cmd] : ['-v', cmd], { + stdio: 'ignore', + shell: !WIN, // `command` is a shell builtin; `where` is a real binary + }) + return probe.status === 0 +} + +function runTask(name) { + const steps = TASKS[name] + if (!steps) { + console.error(`unknown task: ${name}\nknown: ${Object.keys(TASKS).join(', ')}`) + process.exit(2) + } + const skipped = [] + for (const s of steps) { + if (s.probe && !onPath(s.probe)) { + console.log(`\n--- SKIP ${s.cmd} ${s.args.join(' ')} (${s.why})`) + skipped.push(s.probe) + continue + } + const where = s.cwd === '.' ? '' : ` [in ${s.cwd}]` + console.log(`\n--- ${s.cmd} ${s.args.join(' ')}${where}`) + const r = spawnSync(bin(s.cmd), s.args, { + cwd: join(ROOT, s.cwd), + stdio: 'inherit', + shell: false, + }) + if (r.error && r.error.code === 'ENOENT') { + console.error(`\nFAILED: ${s.cmd} is not installed or not on PATH.`) + process.exit(1) + } + if (r.status !== 0) { + console.error(`\nFAILED: ${s.cmd} ${s.args.join(' ')}${where} exited ${r.status}`) + process.exit(r.status ?? 1) + } + } + if (skipped.length) { + console.log(`\n${name}: passed, with ${[...new Set(skipped)].join(', ')} skipped (not installed). CI runs them.`) + } else { + console.log(`\n${name}: passed`) + } +} + +const arg = process.argv[2] +if (!arg || arg === '--list') { + for (const [name, steps] of Object.entries(TASKS)) { + console.log(`\n${name}`) + for (const s of steps) { + const where = s.cwd === '.' ? '' : ` (in ${s.cwd})` + console.log(` ${s.probe ? '[optional] ' : ''}${s.cmd} ${s.args.join(' ')}${where}`) + } + } + console.log('') + process.exit(0) +} +if (!existsSync(join(ROOT, 'Server')) || !existsSync(join(ROOT, 'Client'))) { + console.error('run this from the repository root') + process.exit(2) +} +runTask(arg) diff --git a/tools/mcp-introspect/.npmrc b/tools/mcp-introspect/.npmrc new file mode 100644 index 00000000..2e536932 --- /dev/null +++ b/tools/mcp-introspect/.npmrc @@ -0,0 +1,5 @@ +# engine-strict makes the engines block in package.json a hard error rather +# than an npm warning. npm reads the project .npmrc from the package +# directory and does not walk parent directories, so this file has to exist +# in every package root or the gate silently downgrades to a warning there. +engine-strict=true diff --git a/tools/mcp-introspect/package.json b/tools/mcp-introspect/package.json index 6804ac5b..bd46a202 100644 --- a/tools/mcp-introspect/package.json +++ b/tools/mcp-introspect/package.json @@ -5,7 +5,8 @@ "type": "module", "description": "Local MCP server to introspect a running OwnCord instance (dev tool for Claude Code).", "engines": { - "node": ">=20" + "node": ">=24", + "npm": ">=10" }, "scripts": { "start": "node index.mjs"