mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* 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>
201 lines
9.1 KiB
JavaScript
201 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
||
// Fail when an active document states a finding count the ledger contradicts
|
||
// (the automated half of G-04).
|
||
//
|
||
// node scripts/check-doc-counts.mjs
|
||
// node scripts/check-doc-counts.mjs --selftest
|
||
//
|
||
// Scope, deliberately small: this counts ledger statuses and compares them to
|
||
// the numbers active documents assert. It is not a document-status framework,
|
||
// and it does not check that FINDINGS.md is in sync with the ledger — that is a
|
||
// different check (RL-07) with a different owner.
|
||
//
|
||
// It reads findings-ledger.json directly and does NOT import render-ledger.mjs.
|
||
// That module has no `import.meta.main` guard, so importing it to reuse
|
||
// `validate`/`render` runs `main()` and rewrites FINDINGS.md as a side effect.
|
||
//
|
||
// ── Why the patterns are narrow ──────────────────────────────────────────────
|
||
// "open" is overloaded in this repository. The issue register has 45 open P1
|
||
// *rows*; a security scan closed 8 *findings* F1–F8; `G-05 **refuted**` puts a
|
||
// digit next to a status word. None of those are ledger counts, and a loose
|
||
// pattern flags all of them — a check that cries wolf gets ignored, which is
|
||
// the failure mode G-04 already describes.
|
||
//
|
||
// So a number is only read as a ledger claim in three unambiguous shapes:
|
||
//
|
||
// 1. An enumeration — two or more "<n> <status>" pairs on one line, e.g.
|
||
// "306 fixed / 38 open / 3 declined / 1 duplicate = 348". A lone
|
||
// "45 open" is never enough.
|
||
// 2. A status table row "| open | **38** |", but only in a table that also
|
||
// carries a "| Total | 348 |" row nearby.
|
||
// 3. "<n> records" / "<n> findings", but only where the ledger is named
|
||
// within the preceding few lines.
|
||
//
|
||
// Dated docs/audit-*.md are reported, never failed: they are point-in-time
|
||
// snapshots that are deliberately not maintained, and editing them is out of
|
||
// scope for the repository-layout work. audit-2026-08-19.md does claim zero
|
||
// open findings — true when written, false now, and left alone on purpose.
|
||
|
||
import { readFileSync, existsSync } from 'node:fs'
|
||
import { dirname, join, resolve } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||
|
||
// Active documents that assert a count. Adding a count to a document means
|
||
// adding it here — an unlisted document is not checked.
|
||
const WATCHED = [
|
||
'docs/README.md',
|
||
'docs/plans/README.md',
|
||
'docs/plans/hp-0-scorecard-2026-08-25.md',
|
||
'docs/plans/repo-health-issue-register-2026-08-23.md',
|
||
'docs/plans/b0-baseline-2026-08-25.md',
|
||
'docs/plans/b1-repository-foundation-2026-08-25.md',
|
||
'.superpowers/FINDINGS.md',
|
||
'CLAUDE.md',
|
||
'README.md',
|
||
]
|
||
|
||
// Reported but never failed — dated snapshots, see the header.
|
||
const REPORT_ONLY = ['docs/audit-']
|
||
|
||
const STATUSES = ['open', 'fixed', 'declined', 'duplicate', 'refuted', 'blocked']
|
||
const S = STATUSES.join('|')
|
||
|
||
// Never let a digit that belongs to an identifier or comparison start a claim:
|
||
// G-05, >=20, CGO_ENABLED=0, version=1.2.0-alpha.3.
|
||
const LEAD = '(?<![\\w.\\-=<>/])'
|
||
const PAIR = new RegExp(`${LEAD}(\\d+)\\*{0,2}\\s+\\*{0,2}(${S})\\b`, 'gi')
|
||
const LEDGER_CONTEXT = /ledger|OC-\d|findings-ledger|FINDINGS\.md/i
|
||
|
||
export function tally(ledger) {
|
||
const counts = Object.fromEntries(STATUSES.map((s) => [s, 0]))
|
||
for (const f of ledger.findings) if (f.status in counts) counts[f.status]++
|
||
counts.total = ledger.findings.length
|
||
return counts
|
||
}
|
||
|
||
export function claimsIn(text) {
|
||
const out = []
|
||
const lines = text.split('\n')
|
||
|
||
// Which lines sit in a status table that has a Total row within 10 lines?
|
||
const totalRowAt = new Set()
|
||
lines.forEach((l, i) => {
|
||
if (/^\|\s*\*{0,2}total\*{0,2}\s*\|\s*\*{0,2}\d+\*{0,2}\s*\|/i.test(l)) totalRowAt.add(i)
|
||
})
|
||
const nearTotalRow = (i) => [...totalRowAt].some((t) => Math.abs(t - i) <= 10)
|
||
|
||
lines.forEach((line, i) => {
|
||
const at = i + 1
|
||
|
||
// 1. Enumeration: two or more "<n> <status>" pairs on one line.
|
||
const pairs = [...line.matchAll(PAIR)]
|
||
if (pairs.length >= 2) {
|
||
for (const m of pairs) {
|
||
out.push({ line: at, kind: m[2].toLowerCase(), value: Number(m[1]), text: m[0].trim() })
|
||
}
|
||
// "... = 348" closing an enumeration is the total.
|
||
const eq = line.match(/=\s*\*{0,2}(\d+)\*{0,2}/)
|
||
if (eq) out.push({ line: at, kind: 'total', value: Number(eq[1]), text: eq[0].trim() })
|
||
}
|
||
|
||
// 2. Status table row, only inside a table that totals itself.
|
||
const row = line.match(new RegExp(`^\\|\\s*\\*{0,2}(${S})\\*{0,2}\\s*\\|\\s*\\*{0,2}(\\d+)\\*{0,2}\\s*\\|`, 'i'))
|
||
if (row && nearTotalRow(i)) {
|
||
out.push({ line: at, kind: row[1].toLowerCase(), value: Number(row[2]), text: row[0].trim() })
|
||
}
|
||
const totalRow = line.match(/^\|\s*\*{0,2}total\*{0,2}\s*\|\s*\*{0,2}(\d+)\*{0,2}\s*\|/i)
|
||
if (totalRow) out.push({ line: at, kind: 'total', value: Number(totalRow[1]), text: totalRow[0].trim() })
|
||
|
||
// 3. "<n> records"/"<n> findings", only near an explicit mention of the ledger.
|
||
const ctx = lines.slice(Math.max(0, i - 3), i + 1).join('\n')
|
||
if (LEDGER_CONTEXT.test(ctx)) {
|
||
for (const m of line.matchAll(new RegExp(`${LEAD}(\\d+)\\*{0,2}\\s+(?:records?|findings?)\\b`, 'gi'))) {
|
||
out.push({ line: at, kind: 'total', value: Number(m[1]), text: m[0].trim() })
|
||
}
|
||
}
|
||
})
|
||
return out
|
||
}
|
||
|
||
function main() {
|
||
const ledgerPath = join(ROOT, '.superpowers/findings-ledger.json')
|
||
if (!existsSync(ledgerPath)) {
|
||
console.error(`missing ${ledgerPath}`)
|
||
process.exit(1)
|
||
}
|
||
const counts = tally(JSON.parse(readFileSync(ledgerPath, 'utf8')))
|
||
console.log(`ledger: ${STATUSES.map((s) => `${counts[s]} ${s}`).join(' / ')} = ${counts.total}`)
|
||
|
||
const failures = []
|
||
const notes = []
|
||
let claimCount = 0
|
||
|
||
for (const rel of WATCHED) {
|
||
const p = join(ROOT, rel)
|
||
if (!existsSync(p)) {
|
||
failures.push(`${rel}: watched file does not exist — fix the list in scripts/check-doc-counts.mjs`)
|
||
continue
|
||
}
|
||
for (const c of claimsIn(readFileSync(p, 'utf8'))) {
|
||
const actual = counts[c.kind]
|
||
if (actual === undefined) continue
|
||
claimCount++
|
||
if (c.value === actual) continue
|
||
const entry = `${rel}:${c.line} claims "${c.text}" — ledger says ${c.kind} = ${actual}`
|
||
if (REPORT_ONLY.some((prefix) => rel.startsWith(prefix))) notes.push(entry)
|
||
else failures.push(entry)
|
||
}
|
||
}
|
||
|
||
for (const n of notes) console.log(`NOTE ${n}`)
|
||
|
||
if (failures.length) {
|
||
console.error(`\n${failures.length} document claim(s) contradict the ledger:\n`)
|
||
for (const f of failures) console.error(` ${f}`)
|
||
console.error(
|
||
'\nThe ledger is the source of truth. Update the document, or if the ledger is\n' +
|
||
'wrong, fix .superpowers/findings-ledger.json and re-render FINDINGS.md.',
|
||
)
|
||
process.exit(1)
|
||
}
|
||
console.log(`\n${claimCount} claim(s) across ${WATCHED.length} watched document(s) agree with the ledger.`)
|
||
}
|
||
|
||
function selftest() {
|
||
let failed = 0
|
||
const assert = (cond, msg) => {
|
||
console.log(`${cond ? 'PASS' : 'FAIL'} ${msg}`)
|
||
if (!cond) failed++
|
||
}
|
||
const t = tally({ findings: [{ status: 'open' }, { status: 'open' }, { status: 'fixed' }] })
|
||
assert(t.open === 2 && t.fixed === 1 && t.total === 3, 'tally counts by status and total')
|
||
assert(t.refuted === 0, 'a declared-but-unused status counts 0, not undefined')
|
||
|
||
const c = claimsIn
|
||
const has = (s, kind, value) => c(s).some((x) => x.kind === kind && x.value === value)
|
||
|
||
assert(has('Ledger: **306 fixed / 38 open / 3 declined / 1 duplicate = 348**.', 'open', 38), 'enumeration: reads each pair')
|
||
assert(has('Ledger: **306 fixed / 38 open / 3 declined / 1 duplicate = 348**.', 'total', 348), 'enumeration: reads the = total')
|
||
assert(has('**38 open** · 0 blocked · 306 fixed · 3 declined', 'fixed', 306), 'enumeration: FINDINGS.md header shape')
|
||
assert(has('| open | **38** |\n| **Total** | **348** |', 'open', 38), 'status table with a Total row')
|
||
assert(has('the ledger holds\n348 records', 'total', 348), '"N records" near a ledger mention')
|
||
|
||
// The false positives that made a looser version unusable.
|
||
assert(c('The 45 open P1 rows are tracked in the register.').length === 0, 'a lone "45 open" is not a ledger claim')
|
||
assert(c('| All 8 findings F1-F8 closed |').length === 0, 'a different register is not a ledger claim')
|
||
assert(c('| `golangci-lint` | claimed broken (G-05) | G-05 **refuted** |').length === 0, '"G-05 refuted" is an id, not a count')
|
||
assert(c('`tools/mcp-introspect/package.json` (`">=20"`)').length === 0, '">=20" is not a count')
|
||
assert(c('go build -ldflags "-X main.version=1.2.0-alpha.3"').length === 0, 'a version string is not a count')
|
||
assert(c('11 medium, 27 low').length === 0, 'severities are not statuses')
|
||
assert(c('22 sit under Client/').length === 0, 'a bare number is not a claim')
|
||
assert(c('348 records in some unrelated table').length === 0, '"N records" without ledger context is ignored')
|
||
|
||
console.log(failed ? `\nselftest: ${failed} assertion(s) failed` : '\nselftest: all assertions pass')
|
||
process.exit(failed ? 1 : 0)
|
||
}
|
||
|
||
if (process.argv.includes('--selftest')) selftest()
|
||
else main()
|