mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* 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 @eb873fe7, not estimated: 20 files under Client/src/ import @tauri-apps, using 26 distinct invoke command names against 30 #[tauri::command] handlers, with zero dangling calls and zero uses of the window.__TAURI__ global. Every native dependency is an import, so a static check can find all of them — which is what BPR-025 will eventually enforce. The count is 26 and not 22 because Client/src/lib/ws.ts binds core.invoke to a local tauriInvoke before calling it; a regex matching only invoke("…") misses ws_connect, ws_send, ws_disconnect and accept_cert_fingerprint. Any future lint rule enforcing the seam has to match the binding, not the call site. The 20 files collapse into 13 capability clusters, three of which have no browser equivalent and are flagged as product decisions rather than shims: certificate TOFU in ws.ts, the OS keychain behind credentials.ts/identity.ts, and out-of-focus push-to-talk in ptt.ts. Ownership is recorded by phase (B7/B8/B2). No human owners exist for these folders anywhere in the repository; the document says so rather than leaving the absence to read as an oversight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: perform the HP-1 structural review and measure the B1 exit gate HP-1 asks whether B1's migrations were mechanical. It had never been run, and it cannot be run against dev: dev is squash-merge only, so #1411 landed as one commit and the pure-move/path-rewrite separation the hold point exists to review survives only on refs/pull/1411/head. The scorecard records the pre-squash SHAs so the review is reproducible. Four proofs, all passing: - Pure move (4befe699): 473 renames, all R100, zero non-rename entries, zero line changes, and every renamed blob byte-identical. The blob-OID comparison is what actually covers the six binaries — --numstat prints "-" for them, so the obvious line-count filter reports false positives. - Path rewrite (38ddca73): 983 added / 983 removed, and after normalising the substitution, six unpaired pairs remain — all relative-path depth arithmetic from losing one directory level. Each was resolved against HEAD. The release signer is among them and runs only on a tag, so no CI run on any branch executes it; it is correct (working-directory: Client, artifacts at the root) and guarded by a downstream verify step that fails closed. - Go module rename (7a4e5dc3): 350 files, 728/728, zero unpaired lines. The largest change in B1 is provably a pure substitution. - Active path inventory: 11 files still name tauri-client, all historical — ledger lens labels, dated audits, and plans that describe the move. Zero in code, workflows, scripts, hooks or the Dockerfile. The seed move (93ee14d5) does change behaviour — init() deleted, os.MkdirAll moved into main(). That was authorised by the plan and is isolated in its own commit, which is what HP-1 asks for. Exit gate: seven of eight conditions evidenced. Condition 6 is recorded as PARTIALLY MET and is a real gap — dev has 11 required checks pinned but strict:false, so when dev advances after a PR goes green that PR can still merge without re-testing, and the squash commit that lands was never itself tested. Deliberately not changed here: flipping strict forces a rebase on every open PR whenever another lands, and enforce_admins is on. Owner's call. ENV-01 is closed. Every B0 number was measured on Node 26 while CI pins 24. The client suite now re-runs on Node 24 from a fresh clone in a node:24 container: 192 files, 5257 tests — identical to B0, and the clone doubles as the exit gate's Linux setup smoke. ENV-02 also reproduces at 50.1 MB booting on :8443. Corrects the plan's stale Docker command along the way: the script moved to Server/scripts/ and now takes the image as an argument, and the build context is Server/ rather than the repository root — building from the root streams the whole working tree and then fails on the missing go.mod. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: record the applied repository settings in the HP-1 scorecard Both checked-in settings scripts were run on 2026-08-27 — they had landed in #1418 and #1419 but were deliberately never executed, because repo-settings writes need a person. b0-dev-branch-protection.sh pinned the twelfth required check on dev, "Docs & Ledger Consistency". Until that run the FINDINGS.md drift gate reported but could not block a merge. Condition 6 now reads 12 pinned checks; it stays PARTIALLY MET because strict is still false, which the script itself encodes as a deliberate choice. b1-release-tag-protection.sh created the "Release tags" ruleset (active, target tag, refs/tags/v*, blocks update and deletion, zero bypass actors) and the release environment with one required reviewer. Checked for a pre-existing ruleset of that name first — the POST half is not idempotent and a second run would have created a duplicate. Three rulesets existed, all targeting branches, none named "Release tags". Condition 7 closes: B1-7 merged, and the Discussions slugs its issue-template config hardcodes — q-a and ideas — both exist, so the contact links resolve rather than silently dropping the user on the category picker. Two things the read-back surfaced, both recorded as open, neither blocking: - The release environment has can_admins_bypass: true, GitHub's default. The ruleset has zero bypass actors, but the reviewer gate does not. Moot while the sole admin is also the sole reviewer. - claude.yml passes secrets.CLAUDE_CODE_OAUTH_TOKEN and the repository has no such secret. Nothing is failing, because all five issue_comment runs are skipped at the B1-7 guard before the missing secret would matter — but the paid-automation surface RL-22 hardens is inert today. environment: release is still absent from release.yml, deliberately. The environment now exists, so that is a separate two-line change. Gate re-run after rebasing ontoc0c87366so condition 8 is measured over the final tree, B1-7 included: green, 5257 client tests, exit 0. B1-7's check-workflow-guards.mjs runs locally; its sibling verify-gate-evidence.mjs does not — CI runs the selftest, and the assert form needs a token and a real SHA. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
247 lines
9.6 KiB
JavaScript
247 lines
9.6 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 was
|
||
// RL-07, and B1-6 answered it by not tracking FINDINGS.md at all, so there is
|
||
// no committed rendering left to drift. `npm run check:docs` runs this script
|
||
// and then regenerates the rendering, which is where a generation failure
|
||
// surfaces.
|
||
//
|
||
// 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/hp-1-scorecard-2026-08-27.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",
|
||
"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();
|