mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
dbdb287651030c9eaf6c081b03f7816f174eff97
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5a70f7ae0f |
B1-8: platform contract map, HP-1 structural review, and the B1 exit gate (RL-02 / L-02) (#1420)
* docs: record the desktop/browser platform contract map (B1-8, RL-02/L-02) Client/src/platform/ does not exist — no commits, no files, zero importers. RL-02 asked for the boundary to be *recorded* in B1 so that B7 executes a decided plan rather than rediscovering the surface. This is that record, and nothing more: no directory, no interface, no code. Measured against dev @ |
||
|
|
c0c8736674 |
B1-7: community intake and automation authorization (RL-21 / L-15, RL-22 / L-16, RL-16 / R-09) (#1419)
* ci(claude): constrain automation triggers and bound run cost (L-16)
The Claude Code workflow consumes a metered credential, and the repository
stated nothing about who may spend it or for how long. Whatever downstream
behaviour happens to hold, an invariant this repository depends on should be
asserted and tested here, not inherited from a pinned dependency that a routine
version bump can re-derive.
Three controls, in the one workflow that spends:
- **Authorization.** The job condition now requires the actor to be on an
explicit maintainer allowlist as well as the trigger text to mention the bot.
An allowlist rather than an association check: this repository has exactly one
collaborator, the term is unambiguous to read and to review, and it matches
the actor-term pattern `ci.yml` already uses to exclude Dependabot. Adding a
login is a one-line edit, which is the honest cost.
- **Duration.** `timeout-minutes: 30`, in the band every other long-running job
here uses. Without it the job inherits GitHub's 360-minute default — the wrong
ceiling for metered work, and the only job in the repository that lacked one.
- **Fan-out.** A `concurrency` group keyed on the issue or pull request number
with `cancel-in-progress: true`, so repeated triggers on one thread collapse
into a single run instead of running in parallel. Exactly one of
`github.event.issue.number` and `github.event.pull_request.number` is present
per triggering event, so the key is stable across all four.
The `permissions:` block and the checkout are deliberately untouched. The
permissions are already minimal and the checkout takes no `ref:`, so it reads
the base branch rather than proposed code — both correct, and rewriting either
would be churn.
`scripts/check-workflow-guards.mjs` keeps all three from silently regressing.
Modelled on `scripts/check-doc-counts.mjs`: same `--selftest`-then-assert shape,
same dependency-free approach. It is text-level rather than YAML-parsed on
purpose — the root has no YAML parser, and adding a dependency to assert that a
file contains a `timeout-minutes` key would be a poor trade. That limit is
stated in the file: these are presence-and-shape checks, not semantics.
It runs from `CHECK_HYGIENE` in `scripts/run.mjs`, so it is reachable as
`npm run check:hygiene` locally and executes inside `Repository Hygiene`, which
is already a pinned required check on `dev`. No new CI job and no new pin — the
guard is blocking from the moment it lands.
`actionlint` cannot do this job. It validates expression syntax, action inputs
and runner labels; a job condition is valid input to it whatever the condition
admits, and it has no notion of cost at all. The two tools are complementary
and both now run.
Also corrects the record: `docs/plans/b1-repository-foundation-2026-08-25.md`
claimed impact was bounded by read-only content permissions. The workflow's own
token block is least-privilege, but that is not the only identity a run can
hold, so the claim was narrower than the truth and is now stated accurately.
And `docs/security.md` gains the private-coordination section that two planning
documents already cite it for. The citation pointed at a policy that was not
written down; it now says what stays private, that the rule covers the
repository's own automation and settings rather than only product code, and
that a commit message on a public repository is a disclosure channel.
Verified: both directions, per guard. `node scripts/check-workflow-guards.mjs`
exits 0 on the current tree and reports four guards present. Deleting the
`timeout-minutes` line makes it exit 1 naming that guard and the invariant to
restore; replacing the actor term with `true` makes it exit 1 naming that one;
restoring each returns exit 0. `--selftest` passes eight assertions covering
every guard's absence, a commented-out guard (which must not count), and the two
shapes that must not trip it — any positive timeout value, and any concurrency
key. `npm run check:hygiene` passes with prettier, shellcheck, actionlint and
both new steps running for real; actionlint accepts the edited workflow.
Not included: the workflow's `permissions:` block and checkout step, per above.
No change to the action version or its inputs. `ci.yml`, `release.yml` and
`load-baseline.yml` are outside this item — none is reachable the same way, and
each already carries per-job least-privilege permissions, and where relevant a
timeout and a concurrency group. `METERED` in the new script lists one workflow
because one workflow spends; a second entry is a one-line change when that
changes.
Refs RL-22, L-16
* feat(intake): structured bug form, and route ideas to Discussions (RL-21)
Both issue templates were Markdown with front matter, so nothing they collected
was structured, required, or validated. A reporter could submit the form
untouched. The Environment block was three bullets with `Windows 11` prefilled
as the OS — the single most common answer, pre-filled, on a project that ships
Windows and Linux builds and an ARM64 client.
And `feature_request.md` existed at all, which is the direct violation: BPR-100
says Issues is the bug tracker and Discussions hosts support, ideas and
community feedback. A feature-request template routes ideas into Issues by
construction.
Done:
- `bug_report.md` → `bug_report.yml`, a real issue form. Six fields are
`validations: required` — what happened, steps to reproduce, component, OS,
architecture, deployment mode — because those six are what turns a report into
something reproducible. The rest are optional on purpose; a form that demands
everything gets abandoned.
- `feature_request.md` deleted. Nothing in the tree referenced either template
by filename, so this breaks no link, script, or workflow.
- `config.yml` gains three routed destinations and keeps `blank_issues_enabled:
false` — which is what makes the routing hold, since a blank issue bypasses
every form and every warning on one.
The new environment fields are drawn from what this project actually ships, not
from a generic template:
- **Architecture** x64 / ARM64, with the note that ARM64 is the Linux desktop
client today and there is no ARM64 server release.
- **Deployment mode** covering the six paths `docs/deployment.md` documents —
prebuilt binary on either OS, from source, Docker/Compose, systemd, Windows
service.
- **TLS mode** matching `tls.mode`'s four values exactly, `off` quoted so YAML
does not read it as boolean false.
- **Network topology** — direct, port forward, reverse proxy, Tailscale — because
voice bugs in particular bifurcate hard on this, and the reverse-proxy path
cannot carry the WebRTC UDP range at all.
- **Separate client and server versions.** They are obtained differently and can
legitimately differ. The server field says where to look — admin panel or the
startup banner — and explicitly tolerates "unknown", because the version is
deliberately absent from the unauthenticated `/health` endpoint as
anti-fingerprinting hardening, so a non-admin reporter genuinely cannot get it.
- **Client webview**, WebView2 or WebKitGTK. No "PWA" option: no PWA exists, B1
excludes browser and PWA work, and BPR-092 forbids presenting unavailable
behaviour as functional. The field is diagnostic today regardless — the desktop
client renders through the OS webview, and that already drives real bug classes.
Every public template now carries the disclosure warning BPR-101 asks for, and
the security contact link is first in the chooser, above the Discussions links.
Four files, 189 insertions, 58 deletions.
Verified: both files parse as YAML, and the form was checked against the issue
form schema rather than only for parseability — 13 body elements, 12 unique ids
with no collisions, every non-markdown element carrying an id and a label, every
dropdown carrying options, and the markdown block carrying neither an id nor
validations (both of which GitHub rejects). `config.yml` has
`blank_issues_enabled: false` and four contact links each with exactly
name/url/about. `npm run check:hygiene` passes.
The gap that verification leaves, stated plainly: nothing in this repository
validates issue-form schema. Prettier confirms the YAML parses and actionlint
does not read `.github/ISSUE_TEMPLATE/` at all, so a file that is valid YAML but
an invalid form disappears from the "New issue" chooser silently. The checks
above are a local stand-in, not the real gate. The live chooser needs a look
after merge — which BPR-100's closure evidence ("dry-run submissions reach the
intended destination") requires in any case.
Not included: the Discussions `?category=` slugs are written as `q-a` and
`ideas`, GitHub's defaults. If this repository's categories were renamed, a
wrong slug drops the user on the category picker rather than erroring — confirm
against the live Discussions tab before relying on them. No PR-template or
documentation changes here; those are the next commit. L-15 is not closed by
this commit alone: BPR-100 names six surfaces and three of them are docs.
Refs RL-21, L-15
* docs(intake): route contributors, and state the security path (RL-21)
The previous commit fixed the forms. This is the half BPR-100 and BPR-102
actually ask for and the B1 plan's bullet does not mention: their closure
evidence names repository navigation, support links and contribution docs
alongside the issue forms, so a `.github/`-only change cannot satisfy either.
Three gaps, each verified rather than assumed:
**Discussions was invisible.** The only link to it anywhere in the tree was
inside `.github/ISSUE_TEMPLATE/config.yml` — the new-issue chooser. So "route
ideas and feedback to Discussions" worked for exactly one audience: people who
had already decided to file an issue. `README.md` and `docs/README.md` now each
carry the routing, so it is reachable from the two pages a newcomer actually
lands on.
**`docs/contributing.md` never mentioned security reporting.** Five files
carry the "never a public issue" rule — the root `README.md`, `CONTRIBUTING.md`,
`SECURITY.md`, `docs/security.md`, `CLAUDE.md` — and every one of them delegates
the full process to `docs/contributing.md`, which is also the document BPR-102's
evidence row sends a fresh contributor to. It said nothing about it. It now has
a routing table and a security section that says the thing that actually matters
on a public repository: the PR description, the commits and the branch name are
disclosure channels, so a fix for a vulnerability describes the control it adds
and nothing else.
**The README contradicted the issue chooser.** The banner said "there's no
support" while the chooser offered a link named "Community Support". Both were
defensible in isolation and together they told a user two different things
before they had read anything else. The banner now says the honest version — no
support *commitment* — and a "Getting Help and Reporting Problems" table names
the right destination for each kind of message without promising a response.
Also in the PR template, which the audit's remedy names as "PR guidance":
- The Test Plan asked for `npm test` / `go test ./...` / `npx tsc --noEmit`.
Those predate B1-4's root facade; `npm run check` is the entry point CI gates
on and the one `CONTRIBUTING.md` and `README.md` now tell people to run.
- A generated-files checkbox naming all five, since CI fails on drift and a
hand-edited generated file is the failure that wastes a cycle.
- A `Not included:` prompt, because `docs/contributing.md` makes a written
deferral a required commit element and the template asked for it nowhere.
- The disclosure warning BPR-101 wants on public templates.
Two stale claims fixed while in these files: `docs/contributing.md` said "ten
status checks are required" three lines from a section that says twelve, and
`docs/plans/README.md` still read "B1-0 done, B1-1 next" six phases later — in
the index that declares itself the authority over plan headers.
Five files, 70 insertions, 12 deletions.
Verified: `git grep "ten status checks"` returns nothing.
`node scripts/check-doc-counts.mjs` still agrees on 21 claims across 8 watched
documents — `docs/plans/README.md` and `README.md` are both watched, so a
count claim broken by these edits would have failed here.
`npm run check:hygiene` passes with prettier, shellcheck, actionlint and the
workflow-guard check all running.
One nearby claim checked and deliberately left: `docs/contributing.md` also says
"four of the ten" a hundred lines later. That is four of ten *CI steps keying on
a cache-dependency-path*, not required checks — correct in context, and changing
it would have been a wrong fix to a right-looking grep hit.
Not included: L-15 is **not** closed. BPR-100's closure evidence requires
dry-run submissions that reach the intended destination, and BPR-102's requires
a fresh Windows and Linux contributor to follow these docs and land a passing
sample change. Neither is a file edit. BPR-101 additionally wants a tabletop
report proving private receipt, triage, advisory and coordinated disclosure —
no such artifact exists in the tree, and this commit does not create one.
`CODE_OF_CONDUCT.md` and `GOVERNANCE.md` do not exist in this repository; adding
them is community-health scope, not RL-21's, and neither is named by the audit
row or the register row.
Refs RL-21, L-15
* ci(release): require exact-SHA gate evidence before publishing (RL-16)
A tag push starts `release.yml` and nothing else — `ci.yml` has no `tags:`
trigger. And `release.yml` re-runs none of the required checks: it verifies the
version, builds, boot-smokes and signs, which is a different question from
"did the gate pass on this commit". So a tag could publish from a commit whose
CI was red, and nothing would notice.
It already has. `v1.2.0-alpha.3` published from `fb04a579`, whose CI run
concluded **failure** — `Server Build & Test (windows-latest)`, the race and
coverage step. The Release run on the same commit went green and shipped. That
is R-09 demonstrated rather than hypothesised, and it is the fixture this commit
is verified against.
The obvious fix — re-run the test suite inside `release.yml` — is the wrong one.
It would double the tag-time cost, still not cover the checks that run in other
workflows (CodeQL's three `Analyze` jobs exist in no workflow file at all), and
answer a weaker question: "does it pass now" rather than "did the gate pass on
this commit". The evidence already exists; nothing was reading it.
Done:
- `scripts/verify-gate-evidence.mjs` resolves the tagged SHA's check runs and
asserts every required context is present and `success`. `skipped` and
`neutral` are not success — a required check that skipped on the tagged commit
proves nothing about it — and a still-`in_progress` check is called out as
unfinished rather than treated as absent. Where a context reported more than
once, the latest attempt decides, in both directions.
- The required set is **parsed out of `b0-dev-branch-protection.sh`**, not
restated. Pinning a thirteenth check cannot leave this gate behind, and a
change to that file's shape fails the self-test rather than silently
weakening the gate.
- A `gate-evidence` job in `release.yml` that `verify-versions` needs. Every
build job already needs `verify-versions` and both publishers need those, so
one edge gates the whole graph — including the GHCR push, which today can
mutate `:latest` before `publish` has run at all.
- `permissions: checks: read` and nothing else.
It is a script rather than a `run:` block because of the rule in the `ci-check`
skill: a step that exists only in `release.yml` first executes at tag time, so
its own bugs surface on the release. `Server/scripts/docker-smoke.sh` is the
worked example — one script, two call sites. Here the second call site is
`--selftest`, run by `ci.yml`'s docs-consistency job on every pull request.
`docs/plans/b1-release-tag-protection.sh` covers the half a workflow file
cannot express: a ruleset on `refs/tags/v*` blocking update and deletion, and a
`release` environment with a required reviewer. **NOT APPLIED** — both are
repository-settings writes this session cannot make. Run
`bash docs/plans/b1-release-tag-protection.sh` when you want them.
Deliberately **no `environment: release` key** in `release.yml` yet. The key is
PR-landable, but naming an environment that does not exist stalls the next
release; the script says to add it after creating the environment, and says why.
Verified: both directions, on real data rather than only fixtures. Feeding the
actual check runs from `fb04a579` — the commit alpha.3 shipped from — through
`evaluate` returns **NOT RELEASABLE**, naming `Server Build & Test
(windows-latest): failure` first. Feeding PR #1418's real check runs on
`8875238` returns **RELEASABLE**, and correctly ignores the red
`github-advanced-security` result because it is not a pinned context — the gate
tracks the required set, not "everything is green". `--selftest` passes 12
assertions covering a missing check, a failure, an unfinished run, `skipped`,
`neutral`, both re-run orderings, an unrequired extra, and a commit with no
checks at all. `bash -n` and `shellcheck` are clean on the new script and both
its heredocs parse as JSON. `npm run check:hygiene` passes with actionlint over
both edited workflows.
The module gained a direct-invocation guard so it can be imported and tested
without reaching the network — compared against `argv[1]` rather than
`import.meta.main`, which needs Node 24.2 against an engines floor of `>=24`
and would silently no-op on 24.0.
Not included: the network path itself is exercised only at tag time. The
self-test covers the decision logic and the required-set parsing, which is where
the bugs live; a live API call needs a token this environment does not have.
R-09's "protected release approval" limb stays open until the settings script is
run — the register phases R-09 **B1/B10**, so that half is B10's. `release.yml`'s
version stamping, both signing keys, the fail-closed minisign verify,
`checksums.sha256`'s bare filenames, both cold-boot smokes and the `git archive`
source snapshot are untouched; the remedy says to retain them and this commit
only adds an edge in front of them.
Refs RL-16, R-09
* docs(plans): record B1 progress through B1-7
B1-6 (#1418) merged and B1-7 is this branch, so the header and the plan index
both move on. B1-8 — the platform contract map — is next, and it is documentation
only: it records the browser-neutral contract folders and their owners, and moves
no native behaviour. Adapter extraction stays B7.
Verified: `node scripts/check-doc-counts.mjs` still agrees on 21 claims across 8
watched documents, both edited files among them; prettier clean.
Refs R-08
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
eb873fe7b2 |
B1-6: generated artifacts (RL-06 / L-06, RL-07 / L-07, RL-08 / L-08) (#1418)
* ci: verify FINDINGS.md against the ledger it renders from (RL-07) `.superpowers/FINDINGS.md` is generated from `findings-ledger.json`, and `CLAUDE.md` forbids hand-editing it — but nothing checked. The one automated consumer, `render-ledger.mjs --check`, validates the ledger's JSON schema and `return`s at line 116, *before* the only `render()` call at line 118, and never opens `FINDINGS.md` at all. A stale 1.09 MB rendering passed it cleanly. The audit says "no workflow runs it". That was true when it was written and is not now: B1-2 (#1412) wired `--check` into the `Docs & Ledger Consistency` job. So the gate exists, reports, and is blind to the thing its name suggests it watches — which is worse than absent, because it reads as covered. The obvious fix — render to a temp file and diff, as the B1 plan suggests — is not what this repository does. It has three implementations of one idea (`Server/Makefile` sqlc-verify and protocol-verify, `.githooks/pre-commit`, `scripts/run.mjs`), and all three regenerate **in place** and let `git diff --exit-code` be the differ. That needs no temp path, no cleanup, and inherits `.gitattributes`' line-ending normalisation for free. A fourth shape would cost a reader something for nothing. Done: - The gate, in all three places the existing two gates live: the `docs-consistency` CI job, `scripts/run.mjs`'s `CHECK_DOCS`, and a new `.githooks/pre-commit` block gated on the ledger, the rendering, or the renderer being staged. `npm run check` never ran the renderer at all before this, which contradicted `run.mjs`'s own stated purpose. - `validate()` now requires `severity`. This is not a nice-to-have riding along: `render()` sorts the open section by `SEV_RANK`, and an unranked severity makes the comparator return `NaN`, which leaves the sort order implementation-defined. A gate whose expected output is implementation-defined can go red across a Node upgrade for a reason that is not drift. The validation is what makes the gate's premise — that the rendering is a pure function of the ledger — true rather than merely true today. - `--stat` on the diff. Deliberate deviation from the three precedents: a fully drifted rendering is a ~40,000-line CI log, and the exit code is what gates. Eight files, 119 insertions, 24 deletions. The gate is one render (67-170 ms) plus one `git diff`. Rendering subsumes `--check`, because `main()` validates and exits 1 before it writes — so the CI job keeps both steps only so the checks UI names which fix is needed. Verified: both directions, and the naive test would have lied. Appending to `FINDINGS.md` proves nothing — the renderer overwrites it, so the perturbation vanishes and the diff comes back clean. `git diff <path>` compares the worktree against the **index**, so the drift has to live in the index. Changing one finding's title in the ledger and staging it *without* re-rendering — exactly the mistake the gate exists to catch — makes `git diff --exit-code --stat` exit 1 with a one-line stat, and `.githooks/pre-commit` fail with `FINDINGS.md is stale`. Restoring the ledger and re-rendering returns both to exit 0, and `git status --porcelain` is clean afterwards. Severity validation both ways: setting one finding to `moderate` makes `--check` print `INVALID OC-0001: bad severity moderate` and exit 1; `git checkout` of the ledger makes it valid again. The hook's grep pattern was exercised against five paths — the three `.superpowers/` targets match, `.superpowers/sdd/notes.md` and `scripts/check-doc-counts.mjs` do not. `node scripts/run.mjs --list` resolves `check:docs` to three steps rather than one; `npm run check:docs` and `npm run check:hygiene` pass, the latter with prettier, shellcheck (the new hook block) and actionlint (the new CI step) all running for real. Not included: untracking `FINDINGS.md` — that is the next commit, and the order matters. L-07 requires the drift check to exist *before* the removal, because the check is what proves the tracked copy was current at the moment it was deleted. No `import.meta.main` guard on the renderer: no caller imports it, and `import.meta.main` landed in Node 24.2 against an `engines` floor of `>=24`, so it would silently no-op on 24.0/24.1 — `scripts/check-doc-counts.mjs` documents the workaround and stays accurate. No `existsSync` guard for a missing ledger: the unhandled rejection already exits non-zero, so CI already rejects it and only the message is ugly, which is not drift. The `docs-consistency` job is not converted to `npm run check:docs`; it is deliberately `npm ci`-free with direct `node` calls in every step, and half-converting it would be worse than being internally consistent. No `Server/Makefile` target — the ledger is root-scoped, and `make` is not on PATH on a stock Windows box (RL-20). Refs RL-07, L-07 * chore: stop tracking the rendered FINDINGS.md (RL-07) The previous commit built the drift check RL-07 asked for. This is the second half: with the check in place proving the committed rendering was current, the rendering itself comes out of the index. Untracking is strictly stronger than checking. A drift check watches for a rendering that has fallen behind its source; not tracking it removes the possibility. `findings-ledger.json` stays the only tracked copy and remains canonical — `CLAUDE.md` tells contributors to open a PR against it — and the 1.09 MB view of it is regenerated in 67-170 ms by a command that was already documented. Why the drift check still had to land first, in its own commit: it is what proved the tracked copy was current at the moment it was deleted. Deleting a generated file you have never verified against its source is how you discover, later, that the source was wrong. L-07 sequences it the same way — "remove the tracked duplicate human rendering *after* deterministic on-demand/CI rendering and a drift check exist" — and this commit is the "after". The gate transforms rather than disappears. `git diff --exit-code` cannot watch an untracked file, so what remains of L-07's "CI rejects generation failure or drift" is the generation half, plus its separate "a downloadable rendering is reproducible" clause. CI now renders **twice and compares** — which tests both: the render must succeed (it validates and exits 1 before writing) and it must be a pure function of the ledger. The severity rule added in the previous commit is what makes that second property true rather than merely true today. The rendering is then uploaded as the `findings-ledger-rendering` artifact with `if: always()`, so a reviewer reads it without a Node run — and can read it precisely when the job failed. Six coordinated edits, and the fourth is not optional: - `.gitignore` — drop the `!` negation; the `.superpowers/*` blanket takes over. - `.gitattributes` — drop `linguist-generated=true`, now dead. - `.prettierignore` — drop the entry; Prettier 3 reads the root `.gitignore`. - `scripts/check-doc-counts.mjs` — drop it from `WATCHED`. A missing watched file is pushed to `failures` and exits 1 by design, with a message telling you to fix the list. Forgetting this line reds `Docs & Ledger Consistency` and `npm run check` on every subsequent run. - `CLAUDE.md` — the command stays, the "tracked artifact" framing goes. - `.claude/skills/bughunt-run/SKILL.md` — the human gate between hunt and fix reads this file, so it now says to generate it first. That reader is already at a terminal that ran the renderer seconds earlier. 13 files, 103 insertions, 9,278 deletions. The check-doc-counts gate goes from 27 claims across 9 documents to 21 across 8; the six it loses were rendered *from* the ledger they were checked against, so they were self-consistent by construction and could only ever have failed on a stale rendering — which is the thing that can no longer exist. Verified: both directions. `git ls-files .superpowers/` returns exactly two files; `git check-ignore -v .superpowers/FINDINGS.md` names `.gitignore:87` while the ledger itself is not ignored (exit 1), so the blanket rule did not overreach. Deleting the rendering outright and running `node scripts/check-doc-counts.mjs` prints `21 claim(s) across 8 watched document(s)` and exits **0** — the proof that the `WATCHED` line was dropped, because leaving it would have failed here. `npm run check:docs` then regenerates the file (1,087,051 bytes) and passes. Rendering twice and `cmp`-ing the results reports byte-identical output. The pre-commit hook was exercised both ways with the ledger staged: a severity of `moderate` fails with `findings-ledger.json is invalid`, and a valid tree passes with exit 0. `npm run check:hygiene` passes with prettier, shellcheck and actionlint all running for real. Not included: `findings-ledger.json` is untouched by this commit — it is the canonical copy and it stays tracked, at 1,205,085 bytes, which is *larger* than the rendering just removed. Anyone reaching for the size argument should know that untracking the rendering removes 47% of the pair and leaves the bigger, less readable half; the reason to do it is that the rendering is 100% derived and would otherwise write a fresh ~1.06 MB blob into permanent history on every hunt, not that it is the heavy one. No history rewrite — the blobs already committed stay where they are, per the B1 non-goal. `Server/Makefile` gains no ledger target: root-scoped, and `make` is not on PATH on a stock Windows box. Refs RL-07, L-07 * chore: stop tracking the prebuilt hello.wasm plugin example (RL-08) `Server/plugin/examples/hello/hello.wasm` was 946,410 bytes of committed build output — 84% of that directory — for a plugin subsystem that is disabled twice over: it compiles only under `-tags wazero`, and `plugins.enabled` defaults to `false`. Nothing verified it matched the `main.go` beside it. The remedy the audit names is a compile-and-compare gate. It cannot be built, and not for cost reasons: TinyGo embeds absolute host paths from the building machine's Go SDK and module cache into its output and offers no `-trimpath` equivalent, so two machines compiling identical source produce different bytes. A byte-identity gate cannot pass in principle. What is left is a compile-only check, and that needs three pinned downloads — TinyGo, a *second* Go SDK at 1.25.x because TinyGo 0.40.1 rejects the Go 1.26 this module pins, and Binaryen 129 — on every PR, to prove something weaker than advertised about a subsystem that ships in zero release artifacts. So the artifact goes and its provenance is written down instead. BPR-080 asks that the example WASM be "reproducible **or** provenance-verified" — disjunctive — and the second branch is the one that is actually reachable here. The repository had already made this call for itself. `sandbox_wazero_test.go` uses a 41-byte inline WASM literal, with the comment "Using a literal here avoids dragging a binary asset into the repo." This extends that from the tests to the example. Done: - `git rm --cached` the artifact; a narrow `.gitignore` entry naming the exact path. Deliberately **not** a blanket `*.wasm`: `Client/public/rnnoise.wasm` is a vendored npm artifact this repository does not build and the client fetches at runtime, so ignoring it would break noise suppression. The rule that separates them — untrack build output whose source we own and whose absence breaks nothing; keep vendored third-party artifacts required at runtime — is written into the ignore comment. - `Server/.dockerignore` gains `plugin/examples/`. `Dockerfile` does `COPY . .` and the file already excluded `scripts/` and `cmd/` but not this, so a developer who still has the untracked artifact on disk was shipping it into the build context. Same omission B1-5 fixed for `cmd/`. - The README carried two false statements, both now removed: it claimed the plugin is "used by `Server/plugin/plugin_test.go`" and that that test "exercises the manifest parser and the loader against this directory". Neither is true — `plugin_test.go` builds every fixture in `t.TempDir()`. - A Provenance section: TinyGo 0.40.1 + Go 1.25.3 + Binaryen 129, why the output is not byte-reproducible, and why the compile gate is deferred rather than merely absent. - The ABI-stability sentence L-08 requires, which existed nowhere in the repository: the ABI is experimental with no compatibility promise, and both halves of "disabled" are named with the files that prove them. Verbatim identical in the example README and `docs/contributing.md`. - The TinyGo/Go/Binaryen table existed in two hand-maintained copies that had already drifted in wording. It now lives in the example README only; `docs/contributing.md` links to it, which is the pattern that page already used two lines above for the ABI itself. Five files, 87 insertions, 20 deletions, plus the 946,410-byte deletion. Verified: both directions. The inertness proof is the load-bearing one, and it is the inverse of B1-5's remove-and-watch-it-fail, because here passing is the point: with `hello.wasm` moved out of the tree entirely, `go build ./...`, `go build -tags wazero ./...`, `go vet ./...`, `go test ./plugin/...`, `go test -tags wazero -count=1 ./plugin/...` and `go test ./api/...` all pass. `go list ./plugin/...` returns a single package with and without the tag, so `//go:build tinygo` keeps the example out of the module's build graph. The narrowness proof is one pair: `git check-ignore -v` matches `Server/plugin/examples/hello/hello.wasm` at `.gitignore:59` and exits 0, and exits 1 on `Client/public/rnnoise.wasm`, which `git ls-files` confirms is still tracked. `git ls-files Server/plugin/examples/` now returns exactly the three source files. `npm run check:hygiene` and `npm run check:docs` pass. Not included: no CI compile-and-compare job, per the reasoning above — deferred to B2, which the issue register already names as L-08's second phase. **L-08 is not claimed closed**: its closure evidence reads "Deterministic source build passes", and that is precisely what TinyGo cannot deliver here; the register's B1/B2 span is what makes deferring it in-scope rather than a slip. No `tinygo.version` pin file — `Server/sqlc.version` earns its existence through four mechanical consumers, and nothing would read this one; the gap in `docs/contributing.md`'s toolchain-pinning policy is closed by recording TinyGo and Binaryen as a documented exception instead. `main.go`, `plugin.json` and the README stay tracked — L-08 says keep the source, and this commit keeps all of it. `.gitattributes` keeps `*.wasm binary`, which still covers the client's vendored module. No history rewrite: the artifact's existing blobs stay where they are, per the B1 non-goal. Refs RL-08, L-08 * docs(plans): retire the removed graphify tooling from the B1 plan (RL-06) RL-06 asked for a 20.41 MB tracked `graphify-out/` payload to stop being tracked, after a portable regeneration command and a CI artifact existed. None of that happened. Instead `a5f7d95` (#1413) deleted the tool outright, taking all 7 tracked files with it — 20,408,656 bytes, `graph.json` at 19,463,420 — before B1-6 opened. `git ls-files` matches nothing graphify-related today. So the outcome RL-06 wanted holds (no large tracked payload, history intact) and the method it prescribed was bypassed. There is nothing left to do in the repository. What was left is a documentation problem, and a live one: this plan is an active document, and it still told a reader to run a tool that does not exist. The obvious response — delete every graphify mention — is wrong twice over. The `.gitignore` rule has to stay: the local directory reached ~208 MB with cache and dated snapshots on the machine that ran the tool, and dropping the rule would flood that contributor's `git status` with untracked noise. And the "do not rewrite history to shrink graphify-out" non-goal has to stay too: the files are gone from the tree but four `graph.json` revisions remain in the pack (~71 MiB logical, ~3.2 MiB packed of 13.28 MiB), so the line is still operative. It is what keeps "closed" honest rather than overclaiming. Done — nine edits, each a dead instruction rather than a stale mention: - **B1-2 Step 7, the worst of them.** It told a human to `unset GRAPHIFY_SKIP_HOOK`, run `graphify update .`, and `git commit -am` a refresh. The tool is gone, and `git commit -am` with nothing to commit exits non-zero while reading like a no-op success. Replaced with a retirement note; Step 7 is the last step, so nothing renumbers. - **The "Traps carried forward" entry.** A live instruction, in a list of traps, aimed at exactly the multi-commit sequence this phase is. Deleted. - B1-1 Step 1's `export GRAPHIFY_SKIP_HOOK=1` and its four-line hook rationale, collapsed to one sentence of history. The "close any editor, cargo, vite" paragraph beside it is still true and stays. - The RL-06 verdict row, the B1-6 bullet, the flatten's "leave alone" list, the `post-commit` parenthetical, B1-3's exclusion list, and the non-goal line. - `.gitignore`'s stale "delete the dir when convenient" TODO becomes a recorded decision citing the commit that caused it. Verified: `git grep -i graphify` outside the dated audit and the issue register returns exactly five hits, and every one is intended — the `.gitignore` rule and four plan lines that are explicitly retirement or history notes ("once began", "Retired", "closed by deletion", and the non-goal). `git grep GRAPHIFY_SKIP_HOOK` returns one hit, the sentence recording that it used to be required. `node scripts/check-doc-counts.mjs` still passes — this file is one of the documents it watches — and `npx prettier --check` is clean after the verdict-row rewrite reflowed the table. Not included: `docs/audit-2026-08-23-repository-layout.md` keeps its RL-06 row — dated point-in-time snapshot, and `check-doc-counts.mjs` already classifies `docs/audit-*` as report-only. `docs/plans/repo-health-issue-register-2026-08-23.md` keeps L-06 and the R-03 row that routes to it, and the reason is *not* that it is dated: it is in the watched set, i.e. this repository treats it as active. It is that no B1 phase has updated its closure column, so L-01, L-04, L-05 and L-09 through L-13 are all closed in fact and open on paper. Changing that convention in the phase with the least to say about it would leave the register half-updated, which is worse than uniformly stale. That sweep belongs to `R-06`, or to one pass at B1's end. No history rewrite, per the non-goal this commit deliberately keeps. Refs RL-06, L-06 * docs(plans): record B1 progress through B1-6 The header still read "B1-3 are complete; B1-4 is the next step" three merged phases later — B1-3 (#1414), B1-4 (#1415) and B1-5 (#1417) have all landed, and B1-6 is this branch. B1-3 set this convention with its own `docs(plans): record B1 progress through B1-3` commit, and then B1-4 and B1-5 both skipped it. A plan that misstates where it is costs a reader the same confusion whether it is one phase stale or three; three is just harder to notice, because the header looks deliberate. Verified: `node scripts/check-doc-counts.mjs` still agrees on 21 claims across 8 watched documents — this file is one of them — and prettier reports it clean. Refs RL-06 (the phase this records), R-08 * chore(ci): pin Docs & Ledger Consistency as a required check on dev The previous commits gave `Docs & Ledger Consistency` a gate that can actually fail: it now rejects a ledger that will not render, on top of the schema check it already ran. But the job is not among `dev`'s required contexts, so it reports and cannot block. L-07's closure evidence reads "CI **rejects** generation failure or drift" — reporting is not rejecting, and the item is not closed until this lands. The script's own header already diagnosed the omission: it listed `Docs & Ledger Consistency` under "deliberately NOT pinned" with the note that it "looks like an oversight from the 2026-08-25 pass rather than a decision". That entry is now wrong in the other direction, so it moves out of the not-pinned list and into a dated note beside `Repository Hygiene`'s. The name was read off **PR #1418's live check runs** after the job reported `success` — not copied out of `ci.yml`. That order is B1-3's rule and it is not pedantry: the B0 script records that three of the pinned names exist in no workflow file at all, because CodeQL runs from GitHub default setup. Two count claims move with it. `docs/contributing.md` said "All ten required checks" and the HP-0 scorecard's table said **10**, both stale since B1-3 added `Repository Hygiene` and now doubly so. B1-5 spotted the first and deferred it to "the branch-protection item's to fix"; this is that item, and it is also the commit that changes the number, so leaving them stale here would make this commit the proximate cause of a documented inconsistency. The scorecard is in `check-doc-counts.mjs`'s watched set — the repository classifies it as active, not as a frozen snapshot — so the don't-edit-dated-docs rule does not shield it. Its pinned block gains both names and a line recording when each was added. NOT APPLIED YET. Running this script is `gh api -X PUT repos/J3vb/OwnCord/branches/dev/protection`, a repository-settings write this session cannot perform. Run `bash docs/plans/b0-dev-branch-protection.sh` after this PR merges. The pre-flight is clear, stated positively rather than assumed: a required check that never reports blocks every PR forever, which is the hazard B1-3's own NOT-APPLIED note was about. It does not apply here. `Docs & Ledger Consistency` has existed in `dev`'s `ci.yml` since #1412, so no in-flight branch predates the job, and it reported `success` on this PR in 11 seconds. Verified: `bash -n` and `shellcheck` are clean. Extracting the heredoc and parsing it with `node` reports **12** contexts including `Docs & Ledger Consistency`, spelled exactly as the live check reports it — the JSON is machine-checked rather than eyeballed, because a typo here is a branch that cannot merge. `node scripts/check-doc-counts.mjs` still agrees on 21 claims across 8 watched documents, the scorecard among them, and `npm run check:hygiene` passes with prettier, shellcheck and actionlint all running. Not included: the script is not run — that is the owner's step, above. No other context is added or removed; the four remaining "deliberately NOT pinned" entries keep their recorded reasons, including `Admin Panel E2E`, whose `continue-on-error: true` still makes requiring it theatre until `R-01` graduates it. Refs RL-07, L-07, RL-14, G-03 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
9eba6969d2 |
B1-5: ownership moves (RL-09 / L-09, RL-10 / L-10, RL-11 / L-11, RL-13 / L-12) (#1417)
* refactor: move the protocol schema to protocol/schema.json (RL-09)
The WebSocket message-type schema is the one artifact in this repository that
neither component owns: `Server/ws/message_types.go` and
`Client/src/lib/protocolTypes.ts` are both generated from it, and neither may
be hand-edited. It nonetheless lived at `docs/protocol-schema.json` — filed
under the directory for prose, whose own README calls it "Reference" material
— and its generator lived at `Server/scripts/genprotocol/`, i.e. inside one of
the two consumers. Ownership was legible from neither location.
The obvious fix — move the generator to the repository root alongside the
schema, so the whole tool is at the cross-component boundary — is wrong here.
The generator is a Go `package main`, and Go modules are directory-rooted:
`Server/go.mod` roots at `Server/`, so a root-level Go program needs a second
module or a `go.work`. That second module would sit outside every path filter
this repository already has — `golangci-lint` runs with `working-directory:
Server/` (ci.yml), `go vet ./...` runs from `Server/` (scripts/run.mjs,
.githooks/pre-commit), `.githooks/pre-commit` selects Go files with
`^Server/.*\.go$`, `.githooks/pre-push` sets `server_changed` on `^Server/`,
setup-go caches on `Server/go.sum`, and dependabot has one gomod block for
`/Server`. Six gates would silently stop covering the generator, each failing
open. The schema is data and moves freely; the generator is Go and stays where
the Go toolchain already runs.
Done instead:
- `docs/protocol-schema.json` -> `protocol/schema.json`. A new top-level
`protocol/` is the cross-component boundary, with a `README.md` naming the
two generated consumers, the one command, and the four gates.
- `Server/scripts/genprotocol/` -> `Server/cmd/genprotocol/`, the module's
conventional home for an executable. This also empties `Server/scripts/` of
Go entry points except `seed.go`, which RL-10 moves next.
- `Server/cmd/` added to `Server/.dockerignore` and `Server/.air.toml`, which
both already excluded `Server/scripts/`. Without this the move would have
silently widened the Docker build context and the air watch set.
27 files, 115 insertions, 76 deletions. Two runtime path resolvers re-pointed
(`cmd/genprotocol/main.go:41` `-schema` default, `ws/protocol_contract_test.go:67`
`filepath.Join`); two git-hook grep patterns (`pre-commit:53`, `pre-push:57`);
eight generator call sites across five files (Makefile x2, scripts/run.mjs x2,
pre-commit x2, ci-check skill, bughunt-fix.js); two broken relative markdown
links (docs/README.md:47, docs/protocol.md:1497); two generated files
regenerated, header lines only, zero constants changed; two ledger prose hits
plus a `render-ledger.mjs` re-render. No new verify was written: the
regenerate-and-diff check is already enforced three times (CI `make
protocol-verify`, `.githooks/pre-commit`, `npm run check:server`) and
`ws/protocol_contract_test.go` independently checks the schema against the
constants a fourth time.
Verified: both directions, for both resolvers. With `protocol/schema.json`
removed, `go test ./ws/ -run TestProtocol` fails with `reading protocol schema
at /home/user/OwnCord/protocol/schema.json: no such file or directory` (two
tests) and `go run ./cmd/genprotocol` exits 1 with `read schema: open
../protocol/schema.json: no such file or directory`; with the file restored
both pass. So the new path is genuinely resolved, not merely spelled in a
comment. The hook patterns were exercised directly: the pre-commit pattern
matches `protocol/schema.json` and `Server/cmd/genprotocol/main.go` and no
longer matches `docs/protocol-schema.json`; the pre-push pattern matches
`protocol/schema.json`. `go run ./cmd/genprotocol` twice in a row leaves
`git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts`
clean, so the committed outputs are exactly what the generator emits.
`go build ./...` and `go vet ./...` pass; `npx prettier --check .`,
`npm run typecheck` and `npm run lint` pass; `node .superpowers/render-ledger.mjs
--check` reports 348 findings valid.
Not included: the four dated `docs/audit-*.md` files, the older
`docs/plans/*`, and `CHANGELOG.md` keep the old path — they are point-in-time
records, and `.prettierignore` and `scripts/check-doc-counts.mjs` already
treat them as deliberately unmaintained. The B1 plan itself keeps its own
wording, since it states intent rather than current state. `Server/scripts/`
is not deleted: it still holds `seed.go` (RL-10), `k6/`, `toxiproxy/` and two
shell scripts. `Server/telemetry/metrics.go:19` declares a scope for a
`Server/voice` package that does not exist — spotted here, unrelated to this
move, left for RL-13's sweep to carry forward verbatim rather than fixed
inside a relocation. No `seed:` Make target was added.
Refs RL-09, L-09
* refactor: move the seed tool under Server/cmd/seed (RL-10)
`Server/scripts/seed.go` was a `package main` sitting directly in
`Server/scripts/`, which made `Server/scripts` itself one of the module's
three main packages — a developer tool in the module's build graph under a
directory name that says "loose scripts". It also did filesystem work in
`func init()`: `os.MkdirAll("data", 0o750)` ran before `flag.Parse()`, so the
directory appeared even when the tool immediately refused to run.
The audit row (RL-10) claims that `init()` fires "during test discovery". It
does not, and the obvious fix aimed at that claim would be aimed at nothing:
`Server/scripts/` contains zero `_test.go` files, so Go never builds a test
binary there and `go test ./...` never runs the `init()`. The residual defect
is narrower and real — an untagged `package main` in the build graph, plus a
side effect on a path (`go run ./cmd/seed -h`) that has nothing to do with
tests.
Done:
- `Server/scripts/seed.go` -> `Server/cmd/seed/main.go`, joining
`cmd/genprotocol/` from RL-09. `Server/scripts/` now holds shell and JS
tooling only (docker-smoke.sh, k6/, toxiproxy/, voice-test.sh) and no Go
entry point at all.
- The `os.MkdirAll` moved out of `init()` to immediately before `db.Open` in
`main()` — the one call that needs the directory, since `db.Open` ->
`OpenWithMaxReaders` -> `openFile` creates no intermediate directories.
- The package doc comment's usage lines were wrong in two ways, not one: they
named `go run scripts/seed.go`, which no longer exists, and they omitted
the mandatory `-confirm-dev`, so neither documented command could ever have
run. Both corrected, and `seed.go is a standalone tool` became the
conventional `Command seed populates ...`.
- `Server/CLAUDE.md`'s Layout list now names `cmd/` and states that no Go
entry point lives in `scripts/`.
Two files, 20 insertions, 17 deletions. `go list` main packages go from
`{server, server/cmd/genprotocol, server/scripts}` to `{server,
server/cmd/genprotocol, server/cmd/seed}` — the count is unchanged at three,
which is the honest framing: this relocates a main package to a conventional
path, it does not remove one from the build graph.
Verified: both directions, by building the pre-change file and the
post-change file and running each in a fresh empty directory. Before, `seed`
with no flags exits 1 *and leaves a `data/` directory behind*; `seed -h`
exits 0 and also leaves `data/` behind. After, both exit the same way and
create nothing — `data/ exists=NO` in each case. The happy path is unchanged:
`seed -confirm-dev` in an empty directory creates `data/` at mode 0750,
writes `data/chatserver.db`, and reports 4 users / 5 channels / 31 messages;
a second run reports 0 new rows, so idempotence survives. The old documented
invocation now fails loudly (`go run scripts/seed.go` -> `stat
scripts/seed.go: no such file or directory`) and the new one is what the
comment says. All four build-tag variants compile, `go vet ./...` passes,
`gofmt -l` is clean outside `db/dbgen`, and `npx prettier --check .` passes.
Behaviour delta, called out rather than left silent: the two cases above
(`-h`, and a missing `-confirm-dev`) no longer create `./data`. That is a
change, not a pure relocation. It is the change RL-10 asks for — the remedy
text is "remove import/test-time filesystem side effects" — and the
alternative that preserves the old behaviour exactly, making the `MkdirAll`
the first statement of `main()` before `flag.Parse()`, would keep precisely
the side effect the item exists to remove.
Not included: `Server/scripts/genprotocol` was moved to `Server/cmd/` by the
RL-09 commit rather than here, so the "executable tooling under conventional
command ownership" class is closed across the two commits, not this one
alone. `filepath.Dir(*dbPath)` was evaluated for the `MkdirAll` and rejected:
it would fix a real gap (`-db /elsewhere/x.db` still creates a useless
`./data` and does not create `/elsewhere`) but it means creating an arbitrary
directory from CLI input, and that is a behaviour change past "shift it out
of `init()`" — worth its own item. No `make seed` target was added, and the
dated `docs/audit-*.md` rows naming `Server/scripts/seed.go` keep the old
path. The findings ledger has zero references to this file, so no re-render
was needed.
Refs RL-10, L-10
* test: give the cross-stack contracts a named tier (RL-11)
`Client/tests/unit/admin-static-channel-perms.test.ts` reads and executes
`Server/admin/static/index.html`. Filed under `tests/unit`, nothing about its
location or name said it locks a server-owned artifact, so a Go developer
editing the admin SPA got a red check called "Client Unit Tests" with no clue
why.
The register describes this as one file. It is not, and the measured set does
not match the description in either direction:
- Client -> Server: exactly ONE test crosses by filesystem read, not two.
`main-page.test.ts` was named in the plan but only carries a prose comment
citing `Server/admin/update_handlers.go:181` at line 1046 — no read, no
import, nothing to move.
- Server -> Client: the four tests the plan named do not cross.
`waf_test.go`/`waf_crs_test.go` set a `User-Agent: OwnCordClient/1.0`
literal that appears nowhere under `Client/`; `ws_integration_test.go:289`
and `sanitize_content_fuzz_test.go:46` are comments. The real crossing is
one the register never named: `Server/updater/updater_test.go:630` does
`os.ReadFile` on `Client/src-tauri/tauri.conf.json`.
The obvious fixes are both wrong. Moving the invariant "to the owning server
test" cannot work: `Server/go.mod` carries no JavaScript engine (no goja,
otto, v8go, quickjs, rogchap, duktape), so a Go port could only assert at the
text level like `admin/perm_grid_test.go` does — and that is not a
substitute. Flipping the guard at `admin/static/index.html:1182` to
`targetIsTouchedRole=false` reintroduces OC-0154 in full while leaving every
greppable identifier intact, so a text-level test passes on a broken file.
Relocating it to the e2e admin journey is worse: that job is
`continue-on-error: true` and deliberately unpinned ("requiring it is
theatre" — `docs/plans/b0-dev-branch-protection.sh`), so it would convert a
blocking, pinned gate into one that is green regardless. And the journey does
not cover the invariant today: `grep -Eic "perm|access|role|override|matrix"`
over its 142 lines returns 0, so the "if e2e already covers it, delete"
branch never fires.
Done — one tier, applied to the whole set, defined by artifact coupling and
placed by runtime capability:
- New `Client/tests/contract/`, holding
`server-admin-static-channel-perms.test.ts`. Same directory depth, so
`../../../Server/...` still resolves; the body is byte-identical apart from
a header naming the owner and the runner.
- `Server/updater/tauri_key_contract_test.go` splits the one cross-component
Go test out of `updater_test.go` verbatim, same `package updater`. It stays
in Go — placement follows capability, and Go parses JSON fine — so only the
file name has to declare the crossing. Without this the item would have
been "moved one file and declared the class closed".
- `npm run test:contract`, and the tier, the membership rule and a
blocking/non-blocking table in `docs/contributing.md#testing`, which
previously described no tiers at all.
- `Client/CLAUDE.md`'s tier list was missing `tests/e2e/admin` and
`tests/e2e/native` before this; it now lists all seven and states the rule.
`Server/CLAUDE.md` records why the SPA's execution-level invariant is
locked from the client tree, so nobody "fixes" it into a regex.
- Ledger `OC-0154.fix.test` re-pointed and `FINDINGS.md` re-rendered;
`.claude/workflows/bughunt.js` — the workflow that produced OC-0154 — no
longer describes the TS test surface as `tests/unit/*.test.ts` only.
- Three stale cross-stack pointers of exactly the class this item is about:
`tests/e2e/helpers.ts:348,351` and `tests/unit/types.test.ts:13` named
`docs/brain/06-Specs/PROTOCOL.md`, which does not exist (`docs/brain/` is a
gitignored path); all now name `docs/protocol.md`.
15 files, 125 insertions, 33 deletions. No CI job, workflow, vitest,
tsconfig, eslint, knip or stryker change, and no new pinned check —
`ci.yml`'s `npx vitest run --coverage` has no path filter and
`vitest.config.ts` includes `tests/**/*.test.ts`, so enforcement after the
move is bit-identical to enforcement before it. That is deliberate: `dev`
pins 11 contexts and a 12th is a branch-protection API write, not something a
PR can do, so any new job would be advisory until someone separately changed
repository settings — strictly less protection than today.
Verified: both directions, and the assertion was not weakened. Flipping
`admin/static/index.html:1182` to `const targetIsTouchedRole=false;` makes
the moved test fail (`AssertionError: expected 'DELETE' not to be 'DELETE'`);
`git checkout` of that file makes it pass again — so the invariant survived
the move intact rather than becoming a test that passes anywhere. The split
Go test's cross-boundary read is live too: with
`Client/src-tauri/tauri.conf.json` moved away, `go test ./updater/` fails
with `ReadFile(../../Client/src-tauri/tauri.conf.json): no such file or
directory` from `tauri_key_contract_test.go:20`, and passes once restored.
The full client suite is 192 files / 5257 tests passing, identical to the
count before the move; `npm run typecheck` passes, which proves
`tests/contract/` is inside the tsconfig graph and that `tests/types/jsdom.d.ts`
still resolves the moved test's `import { JSDOM }`. `npm run lint`,
`npx prettier --check .`, `go vet ./...` and `go test ./updater/` all pass.
`git grep "tests/unit/admin-static-channel-perms"` finds no survivor outside
the B1 plan itself.
Not included: nothing was deleted, because no e2e sibling covers OC-0154.
`Client/tests/types/jsdom.d.ts` was neither moved nor deleted — it is still
the only type source for the moved test's `jsdom` import. `capabilities-scope.test.ts`
and `tauri-conf-webview2-args.test.ts` read `src-tauri/` and stay in
`tests/unit`: `src-tauri` is inside the `Client` component, so they are not
contract tests, and the rule earns that rather than hand-waving it — moving
them would have forced repoints of ledger entry OC-0089 and
`docs/security.md:64` for no gain. Each gained a one-line header saying why.
`Server/admin/perm_grid_test.go` and `emoji_section_test.go` read their own
package's embedded asset and are unchanged; they are the text-level
complement to the execution-level test, not duplicates. No JS engine was
added to `go.mod`, no npm root was created under `Server/`, and no root-level
`tests/` tier was created — there is no runner for one and no way to make it
blocking from a PR. Separately noticed and NOT fixed here:
`docs/contributing.md:221` still says "All ten required checks" while
`docs/plans/b0-dev-branch-protection.sh` pins eleven since B1-3 added
`Repository Hygiene`, and `docs/plans/hp-0-scorecard-2026-08-25.md:109` is
stale the same way — that is the branch-protection item's to fix, not this
one's, and one register item per commit.
Refs RL-11, L-11
* refactor: rename the Go module to github.com/J3vb/OwnCord/Server (RL-13)
`Server/go.mod` declared `github.com/owncord/server` while the public
repository is `github.com/J3vb/OwnCord`. Nothing resolves that path — there is
no `owncord` GitHub org and no vanity-import host serving go-import metadata
for it — so every import line in the tree named a location that does not
exist. It compiles because a main module's own path is never fetched, which is
exactly why it went unnoticed.
The obvious fix — an AST-aware import rewriter (`gomvpkg`, `go mod edit`) —
is wrong here, and provably so. Six of the 722 occurrences are not imports at
all: `api/main_test.go:20` (a goleak `IgnoreTopFunction` pattern),
`telemetry/metrics.go:17-19` (three OTel instrumentation-scope names),
`invariants/syncutil_locks.go:73` (a diagnostic message), and
`invariants/syncutil_locks_test.go:56` (an import line inside a raw-string Go
fixture). An import rewriter touches none of them, and the compiler cannot
see any of them either.
Done as one scripted substitution over `git ls-files`, anchored on the full
`github.com/owncord/server` string. The anchor matters: `owncord-server` is a
different identifier — the OTel `service.name` (`config/config.go`,
`telemetry/telemetry_otel.go`) and the GHCR image name
(`.github/workflows/release.yml`, `docker-compose.yml`) — and a looser pattern
would have moved it. It is untouched: 10 occurrences across 9 files, before
and after.
350 files, 728 insertions, 728 deletions. 722 occurrences in 344 Go files,
plus `go.mod:1`, the `sed` at `Makefile:67`, `Server/CLAUDE.md:3`,
`docs/architecture/server.md:5`, and the ledger pair
(`findings-ledger.json:3758` plus a `render-ledger.mjs` re-render of
`FINDINGS.md`). Zero in any workflow, zero in the Dockerfile, zero in
`Server/.golangci.yml` (no `local-prefixes`, `gci`, `importas` or `depguard`
rule keys on the module path, so import grouping is not configured anywhere).
The plan's blast-radius estimate missed one thing, and it is the one that
would have gone red: **gofmt**. `J` (0x4A) sorts before every lowercase
letter, so in the 36 files where a module-local import shares a contiguous
group with a third-party one, the module's imports must move above
`github.com/go-chi/...`. `gofmt -l` was clean before the substitution and
listed exactly 36 files after it; `gofmt -w` on those 36 restores it to
clean. `gofmt` is an enforced gate — the `formatters` block in
`Server/.golangci.yml`, which is S-05 — so a substitution-only commit fails
Lint.
Verified: both directions, and the line accounting is exact. Every added line
in this diff contains the new module path (728) and every removed line
contains the old one (728); the count of changed lines containing neither is
**zero**, so the gofmt re-sort moved module-path lines only and touched no
third-party import. The residual check
(`git ls-files -z | xargs -0 grep -n 'github\.com/owncord/server'`) returns
exactly two hits, both deliberately out of scope: the RL-13 row in
`docs/audit-2026-08-23-repository-layout.md` and the measurement row in this
phase's own plan. The compiler-invisible half was proven by reverting *only*
`api/main_test.go:20` to the old path on the otherwise-renamed tree:
`go build ./...` and `go vet ./api/` both still pass — they see nothing wrong
— while `go test ./api/` FAILS, because the runtime function name now carries
the new path and goleak stops ignoring `ws.(*Hub).Run.func1`. Restoring the
line makes it pass. `go.sum` is byte-identical (no `go mod tidy` was run and
none was needed). All four build-tag variants compile; `go vet ./...`,
`go vet -tags otel,wazero ./...` and `go vet -tags deadlock ./...` pass;
`go test -race ./...` is 16/16 packages green; `go test -tags deadlock ./...`
passes; the tag-gated `./plugin/...` (wazero) and `./telemetry/...` (otel)
runs pass. `golangci-lint` v2.11.3 — the pinned CI version, rebuilt locally
against Go 1.26 because the packaged binary cannot load a 1.26 config —
reports **0 issues**. `go run ./cmd/genprotocol` leaves
`git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts`
clean, so the rename does not reach the generated protocol constants.
`npx prettier --check .` and `node .superpowers/render-ledger.mjs --check`
pass.
Not included: `docs/audit-2026-08-23-repository-layout.md` and
`docs/plans/b1-repository-foundation-2026-08-25.md` keep the old path — they
are the audit row and the measurement that motivated this change, and
rewriting them would erase the record of what was measured. They are why the
residual check needs a two-path allowance rather than being empty; that
allowance is stated above rather than hidden in a pathspec.
`telemetry/metrics.go:19` declares `scopeVoice` for a `Server/voice` package
that does not exist; the substitution carried the dead path forward verbatim
as `github.com/J3vb/OwnCord/Server/voice` rather than fixing it, because
correcting a real observability bug inside a mechanical rename would hide it
in a 350-file diff. It needs its own item. No `go.work`, no second module,
and no vanity-import host was set up — the new path resolves against the real
repository, but nothing imports this module as a library, so `go get`
reachability was not exercised either way.
Refs RL-13, L-12
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
2a37f386f9 |
B1-3: repository hygiene gates (RL-19 / L-13, S-05) (#1414)
* chore(format): one Prettier config at the repository root Every formatting rule in this repository lived under Client/ and covered exactly two globs: Client/src/**/*.ts and Client/tests/**/*.ts. Root Markdown, all of docs/, every YAML and JSON, all CSS, the root scripts and tools/mcp-introspect were formatted by nothing. There was no .editorconfig. The obvious fix -- a second Prettier config at the root for "everything else" -- gives two configs and two ignore files that can silently disagree about the same file. So the root takes ownership instead: config, ignore file and gate move up, and Client/ folds in. Client's inline "prettier" block, its .prettierignore, its format/format:check scripts and its now-unused prettier devDependency are all deleted; knip would have failed client-check on that last one. The .prettierrc.json values are lifted byte-for-byte from Client/package.json, which is what keeps the reformat commit free of client TypeScript churn: 87 tracked files need reformatting and not one of them is under Client/src or Client/tests. .prettierignore carries only what .gitignore does not. Prettier 3 reads the root .gitignore by default, so node_modules/, dist/, coverage/, Client/src/generated/ and docs/security-findings/ need no entry. It does NOT read nested .gitignore files, which is why .remember/ is listed explicitly -- 38 untracked per-machine scratch files were otherwise able to turn a shared gate red. graphify-out/ is listed because its seven files are tracked and .graphify_labels.json is signed byte-for-byte by its .sig, so formatting it would silently invalidate the signature. check:hygiene is registered in scripts/run.mjs and folded into check and release:preflight. It deliberately contains no `gofmt -l` step: gofmt -l prints offenders and still exits 0, so it cannot fail a build. Go formatting is enforced separately. shellcheck and actionlint take their file lists from `git ls-files`, never a filesystem glob -- .claude/worktrees/ holds a gitignored pre-flatten copy of the tree with three .sh files a glob would happily lint. This commit leaves the tree non-conformant on purpose. The reformat is the next commit, so the 87-file diff is reviewable separately from the rule that caused it. Not included: editorconfig-checker. .editorconfig is the editor baseline the audit asked for; Prettier, gofmt and rustfmt already fail CI on the same indentation and newline rules, so a fourth tool checking them again is a gate with no failure mode of its own. Verified: `npx prettier --check .` names 87 tracked files and zero untracked ones; the same command listed 38 .remember/ scratch files before the ignore entry and none after. `node scripts/run.mjs --list` resolves check:hygiene to 8 shell targets and 4 workflow targets. Both package.json files parse. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): reformat the tree to the repository Prettier rules Mechanical. This commit is `npx prettier --write .` and nothing else -- the rule that caused it landed in the previous commit so this diff can be reviewed as a transformation rather than as 84 files of hunks. 84 tracked files: 54 Markdown, 7 .mjs, 7 JSON, 6 YAML, 4 .js, 3 CSS, 2 TypeScript (the two Playwright configs at Client's root, which the old Client/src + Client/tests globs never covered). No file under Client/src or Client/tests moves, because .prettierrc.json carries Client's former inline values byte-for-byte. Prettier rewrote 87 files, not 84. The three in .github/ISSUE_TEMPLATE/ had CRLF on disk and differ only in line endings, which .gitattributes (`* text=auto eol=lf`) already normalises, so their committed blobs are unchanged. Worth knowing before someone reconciles the two numbers. The largest single diff is .superpowers/findings-ledger.json at 7976 lines rewritten. That is safe to format: nothing writes the ledger programmatically -- render-ledger.mjs reads it and writes only FINDINGS.md -- so no tool will fight Prettier over its style on the next hunt. FINDINGS.md itself is ignored as generated. Verified: `npx prettier --check .` reports "All matched files use Prettier code style", so the pass is both complete and idempotent. All 7 reformatted JSON files were parsed before and after and compared as values: semantically identical, zero content changes. `node .superpowers/render-ledger.mjs --check` still reports 348 valid findings and leaves FINDINGS.md untouched. `node scripts/check-doc-counts.mjs` still passes its selftest and still agrees on 27 claims across 9 watched documents -- table realignment did not break the patterns it matches on. `node scripts/run.mjs --list` still parses. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): enforce Go formatting in the Server linter S-05: repository-wide Go formatting was not a required gate. The only gofmt enforcement anywhere was .githooks/pre-commit, which is opt-in per clone (`npm run hooks:install`), only sees staged files, and warns-and-skips when gofmt is off PATH. The obvious fix -- a `gofmt -l` step in CI -- does not work: `gofmt -l` prints its offenders and still exits 0, so the step passes no matter what it finds. scripts/run.mjs has the same problem, which is why check:hygiene has no Go step either. So gofmt goes where it can actually fail something: Server/.golangci.yml. The file was already `version: "2"` but had no `formatters:` block at all, so the 19 enabled linters ran with zero formatters. In v2 gofmt/gofumpt/goimports moved out of `linters.enable` into their own section with its own exclusions. Adding it there means the gate reports through the Lint step of "Server Build & Test", which is already pinned as required on dev -- no new job and no new pin. Every tracked .go file is under Server/ (551 of them, one go.mod), so Server-scoped is repository-wide here. One file was genuinely misformatted: a one-space struct field alignment in Server/admin/handlers_users_broadcast_test.go, fixed in the same commit because a single line does not need its own reformat commit. Trap worth recording: `gofmt -l .` on a Windows working tree lists every file that has CRLF on disk, because gofmt normalises line endings. That reported 18 offenders here, 17 of them ghosts. The blobs are all LF -- .gitattributes forces `eol=lf` -- so CI never saw them, and the honest test is to run gofmt over `git show HEAD:<file>` rather than the working copy. Doing that across all 551 tracked Go files found exactly the one real offender above. Verified both directions with golangci-lint v2 locally: `golangci-lint run ./...` reports 0 issues on the formatted tree; appending a misformatted function to Server/auth/constants.go produces 2 gofmt findings; appending the same misformatted function to Server/db/dbgen/admin.sql.go produces 0, so the exclusion holds. Both files restored and verified clean afterwards. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): escape the NUL separator instead of embedding one The `tracked()` helper added earlier in this branch splits `git ls-files -z` output on NUL. The separator was written as a literal NUL byte rather than the two-character JavaScript escape, so scripts/run.mjs became a binary file: `git diff` refused to show it, `grep` reported "Binary file matches" instead of the line, and `* text=auto` in .gitattributes stops normalising line endings for a blob it detects as binary. The code worked -- splitting on a raw NUL and splitting on "\0" are the same operation -- which is exactly why this is worth fixing before it is inherited. A source file that tooling classifies as binary is a file nobody can review. Verified: zero NUL bytes remain, `grep -n "split("` now prints line 50 instead of "Binary file matches", `node scripts/run.mjs --list` still resolves the same 8 shell and 4 workflow targets, and prettier still reports the file clean. * chore(lint): enforce Rust formatting Rust had no formatting gate of any kind: no rustfmt.toml, no `cargo fmt` anywhere in CI, in scripts/run.mjs, in the Makefile or in the git hooks. Clippy was the only Rust gate, and clippy does not check layout. `cargo fmt --all -- --check` now runs in the rust-tests job, ahead of clippy: a formatting failure is cheap to produce and cheap to fix, and there is no reason to spend a clippy pass to surface one. The stable toolchain in that job requested `components: clippy` only, so rustfmt is added there. Only that job. ci.yml has a second, byte-identical `Install Rust` block in tauri-build; it stays clippy-only, because a full desktop build is the wrong place to discover a misplaced brace. No rustfmt.toml. The default profile is the point of a baseline -- a config file here would be a second opinion about style with nothing to say. Client/src-tauri is a single `[package]`, not a workspace, so `--all` is a safeguard against a future member rather than a fan-out today. Verified: `node scripts/run.mjs --list` resolves check:rust to three steps with `cargo fmt --all -- --check` first, `npm run format` now also runs `cargo fmt --all`, and prettier reports ci.yml, run.mjs and the ci-check skill clean. `cargo fmt --all -- --check` currently fails on 13 files -- that is the reformat, and it is the next commit. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): reformat the Rust crate to rustfmt defaults Mechanical. This commit is `cargo fmt --all` and nothing else; the gate that demands it landed in the previous commit so this diff is reviewable on its own. 13 of the 17 tracked .rs files, +343/-164. The crate had never been formatted, so the changes are the usual first-run set: aligned trailing comments collapsed to single spaces, single-element slice literals folded onto one line, long method chains broken across lines, closure bodies expanded into blocks. Verified: `cargo fmt --all -- --check` is clean, so the pass is complete and idempotent. `cargo clippy --all-targets -- -D warnings` finishes with no warnings, and `cargo test --lib` reports 115 passed / 0 failed -- identical to before the reformat, which is what "mechanical" has to mean for a commit that touches this much of the crate. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): make the root facade actually run on Windows Adding the first gate that a contributor would run from the repository root exposed two bugs in the facade, both of which made it silently wrong on the platform this project is developed on. 1. Every npm and npx step failed. bin() appends `.cmd` on Windows, but Node refuses to spawn a .cmd or .bat with shell:false -- the CVE-2024-27980 mitigation -- and fails with EINVAL and a *null* exit status. run.mjs only special-cased ENOENT, so the result was `FAILED: npx prettier --check . exited null` with nothing to explain it. check:client has three npm steps and has never been able to run here. Fixed by spawning only the npm shims through a shell. They are concatenated into a single command string rather than passed as an args array, because shell:true plus a separate array is deprecated (DEP0190) and prints a warning on every invocation; no argument in this file contains a space. 2. Every optional() step was skipped, always. onPath() shelled out to `where` on Windows, but where.exe lives in C:\WINDOWS\System32, which a Git Bash PATH does not necessarily contain -- on this machine PATH carries System32\Wbem, System32\WindowsPowerShell\v1.0 and System32\OpenSSH but not System32 itself. The probe could not start, `probe.status === 0` was false, and golangci-lint and sqlc reported as "not installed" while installed. Fixed by resolving against PATH and PATHEXT directly. No subprocess, and no dependency on which directories happen to be on PATH. A spawn error other than ENOENT now reports its code instead of surfacing as a null exit status. Verified: before, `node scripts/run.mjs check:hygiene` died with "exited null" and both optional steps printed SKIP with the tools present on PATH. After, the same command runs prettier, shellcheck and actionlint and prints "check:hygiene: passed", with no deprecation warning. `golangci-lint` is detected by the new onPath where the old one missed it. Refs RL-20 / L-14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(format): ignore build output that nested gitignores hide Prettier honours the root .gitignore and no other. Every build and scratch directory in this repository is ignored by a *nested* one -- Client/.gitignore, .serena/.gitignore, .superpowers/sdd/.gitignore -- so none of them were excluded from the new repository-wide gate. The effect is not subtle. Running `cargo test` once drops roughly 850 formattable files into Client/src-tauri/target/, and the hygiene gate goes from clean to "Code style issues found in 939 files". CI never sees it, because a fresh checkout has no build output; every contributor sees it on their second command. Mirrors the three nested files rather than inventing a list: dist, coverage, playwright-report, test-results, .vite, src-tauri/target and src-tauri/gen from Client/.gitignore, plus .serena/ and .superpowers/sdd/. node_modules needs no entry -- Prettier ignores it by default. Verified: `npx prettier --check .` reports "All matched files use Prettier code style" with a fully populated Client/src-tauri/target/ present on disk, and still names README.md when a misformatted table is appended to it. Refs RL-19 / L-13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(ci): shellcheck, actionlint, and a repository hygiene job The last two gates RL-19 asks for. Neither existed: the shell scripts were never linted, the workflows were never syntax-checked, and .githooks/pre-commit carried hand-written `# shellcheck disable=` directives that nothing had ever read. New `hygiene` job, ubuntu-only and root-scoped, modelled on docs-consistency for the same reason: every gate in it is platform-independent text analysis, and .gitattributes pins eol=lf so a second OS would only re-prove line endings. It runs `npm run check:hygiene` -- the same entry point a contributor runs, not a parallel copy of the commands. shellcheck ships in the runner image. actionlint does not, so it is pinned by version and verified by sha256: an installer script piped from a branch would be the one unverified download in a workflow file that pins every action by commit SHA. Prettier's step moves here from client-check, where it no longer belongs. Both linters found real defects. shellcheck, 3 findings in 8 scripts. Two are SC1125 errors in .githooks/pre-commit: `# shellcheck disable=SC2086 - repo paths contain no spaces` is not a valid directive. Trailing prose makes shellcheck discard the rest of the line, so neither suppression was ever in effect -- and one of the two was written earlier in this same branch, which is a fair demonstration of why the gate is worth having. The prose moves to its own line above. The third is SC2015 in start-server.sh, rewritten as an explicit if. actionlint, 5 findings, all inside `run:` blocks it shellchecks once shellcheck is on PATH. Three SC2015 in load-baseline.yml, rewritten as explicit ifs. Two SC2035 in release.yml, where `sha256sum *` should not become `sha256sum ./*`: the comment four lines above records that ParseChecksumFile exact-matches the last field, so a "./" prefix would strand every deployed server exactly as a "windows/" prefix would. `sha256sum -- *` satisfies the linter and leaves the output bytes identical. Verified all three gates in both directions with shellcheck 0.10.0 and actionlint 1.7.7 on PATH. Passing: `node scripts/run.mjs check:hygiene` prints "check:hygiene: passed" with all three steps run, not skipped. Failing: appending `bait_fn() { cat $1; }` to Server/scripts/voice-test.sh fails on SC2086; changing a runs-on to `ubunt-latest` fails on runner-label; appending a misformatted table to README.md fails prettier. All three files restored and confirmed clean afterwards. actionlint validates the new job in ci.yml itself. Refs RL-19 / L-13, S-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(plans): record B1 progress through B1-3 The header still read "B1-0 is complete; B1-1 is the next step" three merged phases later. A plan that misstates where it is costs a reader the same confusion whether it is stale by one phase or three. B1-0 (#1410), B1-1 (#1411), B1-2 (#1412) and B1-3 (this branch) are done; B1-4, dependency automation, is next. Verified: `node scripts/check-doc-counts.mjs` still agrees on 27 claims across 9 watched documents -- this file is one of them -- and prettier reports it clean. * chore(ci): pin Repository Hygiene as a required check on dev The second half of S-05. Its acceptance criterion is "tree is formatted AND a fast required gate fails future drift" -- a check that runs but is not pinned lets a formatting regression merge, so the gate is not a gate until this lands. The name was read off PR #1414 with `gh pr checks` after the job reported `pass` in 26s, not copied out of ci.yml. That order matters: the B0 script records that three pinned names exist in no workflow file at all, and that a required check which never reports blocks every PR forever. Extends the existing script rather than adding a second one, per the B1 plan. Also records, in the "deliberately NOT pinned" list, that Docs & Ledger Consistency reports and passes on a dev PR yet is unpinned. That reads as an oversight from the 2026-08-25 pass rather than a decision, but it belongs to G-04, so it is documented here and not changed. NOT APPLIED YET. Running this script now would pin a check that PR #1413 cannot report -- its branch predates the hygiene job, so the job does not exist in its workflow file and the check would never arrive. Run it after #1414 merges; #1413 needs a rebase onto dev regardless. Verified: shellcheck clean, the embedded JSON parses, and `check:hygiene` passes with prettier, shellcheck and actionlint all running. Refs S-05, RL-14 / G-03. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
70473e8e10 |
B1-2: truth, entry points, and contributor path (#1412)
* 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
"<number> <status>" 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 "<n> <status>" pairs, a status table row in a table that totals
itself, and "<n> 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 <noreply@anthropic.com>
|