Skip to content

Commit 7e1945a

Browse files
author
Agent
committed
feat(scripts): add README truthiness checker
1 parent 49b6bd9 commit 7e1945a

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"test": "turbo run test",
4242
"test:web:focused": "bun run --cwd apps/web test",
4343
"brand:check": "node scripts/check-brand-identity.ts",
44+
"readme:truthiness": "node scripts/check-readme-truthiness.ts",
4445
"migrations:check": "node scripts/check-migration-lineage.ts",
4546
"test:desktop-smoke": "turbo run smoke-test --filter=@synara/desktop",
4647
"test:device": "bun scripts/device-helper-smoke.ts",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
detectReadmeTruthiness,
5+
parseUpstreamRemoteSignals,
6+
type ReadmeTruthinessEvidence,
7+
} from "./check-readme-truthiness";
8+
9+
describe("readme truthiness checker", () => {
10+
it("flags a built-from-scratch claim when upstream metadata exists", () => {
11+
const readme = "Forkara was built from the ground up and is a masterpiece.";
12+
const evidence: ReadmeTruthinessEvidence = {
13+
upstreamRef: "origin/main",
14+
hasUpstreamRemote: true,
15+
remotes: [{ name: "origin", fetchUrl: "https://github.com/example/forkara.git" }],
16+
};
17+
18+
const findings = detectReadmeTruthiness(readme, evidence);
19+
expect(findings).toHaveLength(1);
20+
expect(findings[0]).toMatchObject({
21+
id: "technically-ambitious",
22+
title: "Technically Ambitious",
23+
readmeClaims: [{ line: 1, text: "Forkara was built from the ground up and is a masterpiece." }],
24+
});
25+
expect(findings[0]!.evidence[0]).toContain("upstream ref: origin/main");
26+
});
27+
28+
it("flags a no-fork claim when a non-origin tracked remote exists", () => {
29+
const readme = "Technically: not a fork and never meant to be related.";
30+
const evidence: ReadmeTruthinessEvidence = {
31+
upstreamRef: null,
32+
hasUpstreamRemote: false,
33+
remotes: [{ name: "origin", fetchUrl: "https://github.com/example/forkara.git" }],
34+
};
35+
const upstreamRemote = { name: "upstream", fetchUrl: "https://github.com/original/forkara.git" };
36+
37+
const findingsWithFakeUpstream = detectReadmeTruthiness(readme, {
38+
...evidence,
39+
hasUpstreamRemote: true,
40+
remotes: [evidence.remotes[0]!, upstreamRemote],
41+
});
42+
43+
expect(findingsWithFakeUpstream).toHaveLength(1);
44+
expect(findingsWithFakeUpstream[0]).toMatchObject({
45+
id: "factual-fork-relationship",
46+
title: "Factual Fork Relationship",
47+
readmeClaims: [{ line: 1 }],
48+
});
49+
expect(findingsWithFakeUpstream[0]!.evidence[1]).toContain("upstream: https://github.com/original/forkara.git");
50+
});
51+
52+
it("returns no findings when no upstream signal exists", () => {
53+
const readme = "Forkara was built from the ground up.";
54+
const evidence: ReadmeTruthinessEvidence = {
55+
upstreamRef: null,
56+
hasUpstreamRemote: false,
57+
remotes: [{ name: "origin", fetchUrl: "https://github.com/example/forkara.git" }],
58+
};
59+
expect(detectReadmeTruthiness(readme, evidence)).toEqual([]);
60+
});
61+
62+
it("parses git remote -v lines and keeps only one entry per remote name", () => {
63+
const lines = [
64+
"origin https://github.com/example/forkara.git (fetch)",
65+
"origin https://github.com/example/forkara.git (push)",
66+
"upstream https://github.com/original/forkara.git (fetch)",
67+
"upstream https://github.com/original/forkara.git (push)",
68+
];
69+
expect(parseUpstreamRemoteSignals(lines)).toEqual([
70+
{ name: "origin", fetchUrl: "https://github.com/example/forkara.git" },
71+
{ name: "upstream", fetchUrl: "https://github.com/original/forkara.git" },
72+
]);
73+
});
74+
});

scripts/check-readme-truthiness.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// FILE: check-readme-truthiness.ts
2+
// Purpose: Parody-safe checker for README claims that conflict with local git facts.
3+
4+
import { execFileSync, type ExecSyncOptions } from "node:child_process";
5+
import { readFileSync } from "node:fs";
6+
import { dirname, resolve } from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
interface RemoteEntry {
10+
readonly name: string;
11+
readonly fetchUrl: string;
12+
}
13+
14+
export interface ReadmeTruthinessEvidence {
15+
readonly upstreamRef: string | null;
16+
readonly remotes: readonly RemoteEntry[];
17+
readonly hasUpstreamRemote: boolean;
18+
}
19+
20+
interface ReadmeClaim {
21+
readonly line: number;
22+
readonly text: string;
23+
readonly pattern: string;
24+
}
25+
26+
export interface ReadmeTruthinessFinding {
27+
readonly id: string;
28+
readonly title: string;
29+
readonly message: string;
30+
readonly readmeClaims: readonly ReadmeClaim[];
31+
readonly evidence: readonly string[];
32+
}
33+
34+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
35+
const README_PATH = resolve(REPO_ROOT, "README.md");
36+
37+
const BUILT_FROM_SCRATCH_PATTERNS = [
38+
/\bbuilt\s+from\s+the\s+ground\s+up\b/i,
39+
/\bbuilt\s+from\s+scratch\b/i,
40+
/\bfrom\s+scratch\b/i,
41+
] as const;
42+
43+
const NO_FORK_RELATIONSHIP_PATTERNS = [
44+
/\bno\s+fork\s+relationship\b/i,
45+
/\bthis\s+is\s+not\s+a\s+fork\b/i,
46+
/\bnot\s+a\s+fork\b/i,
47+
/\bnot\s+technically\s+a\s+fork\b/i,
48+
] as const;
49+
50+
function runGit(args: readonly string[], cwd: string): string | null {
51+
const options: ExecSyncOptions = {
52+
cwd,
53+
encoding: "utf8",
54+
stdio: [\"ignore\", \"pipe\", \"pipe\"],
55+
};
56+
try {
57+
return execFileSync("git", [...args], options).trim();
58+
} catch {
59+
return null;
60+
}
61+
}
62+
63+
export function parseUpstreamRemoteSignals(lines: readonly string[]): readonly RemoteEntry[] {
64+
const remoteLine = /^([^\s]+)\s+([^\s]+)\s+\((fetch|push)\)$/u;
65+
const remotesByName = new Map<string, string>();
66+
67+
for (const raw of lines) {
68+
const match = remoteLine.exec(raw.trim());
69+
if (!match || match[1] === undefined || match[2] === undefined) continue;
70+
const [name, url] = [match[1], match[2]!] as const;
71+
if (!remotesByName.has(name)) {
72+
remotesByName.set(name, url);
73+
}
74+
}
75+
76+
return [...remotesByName.entries()].map(([name, fetchUrl]) => ({ name, fetchUrl }));
77+
}
78+
79+
export function collectGitEvidence(cwd = process.cwd()): ReadmeTruthinessEvidence {
80+
const upstreamRef = runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
81+
const remoteLines = runGit(["remote", "-v"], cwd);
82+
const remotes =
83+
remoteLines === null || remoteLines.length === 0 ? [] : parseUpstreamRemoteSignals(remoteLines.split("\n"));
84+
const hasUpstreamRemote = remotes.some((remote) => remote.name === "upstream");
85+
86+
return {
87+
upstreamRef,
88+
remotes,
89+
hasUpstreamRemote,
90+
};
91+
}
92+
93+
function collectReadmeClaims(readmeContents: string): {
94+
readonly builtFromScratch: readonly ReadmeClaim[];
95+
readonly noFork: readonly ReadmeClaim[];
96+
} {
97+
const builtFromScratch: ReadmeClaim[] = [];
98+
const noFork: ReadmeClaim[] = [];
99+
const lines = readmeContents.split(/\r?\n/);
100+
101+
for (const [index, line] of lines.entries()) {
102+
const lineNumber = index + 1;
103+
for (const pattern of BUILT_FROM_SCRATCH_PATTERNS) {
104+
if (pattern.test(line)) {
105+
builtFromScratch.push({ line: lineNumber, text: line.trim(), pattern: pattern.source });
106+
break;
107+
}
108+
}
109+
for (const pattern of NO_FORK_RELATIONSHIP_PATTERNS) {
110+
if (pattern.test(line)) {
111+
noFork.push({ line: lineNumber, text: line.trim(), pattern: pattern.source });
112+
break;
113+
}
114+
}
115+
}
116+
117+
return { builtFromScratch, noFork };
118+
}
119+
120+
function hasUpstreamFact(evidence: ReadmeTruthinessEvidence): boolean {
121+
const hasAnyRemotes = evidence.remotes.length > 0;
122+
const hasNonOriginRemote = evidence.remotes.some((remote) => remote.name !== "origin");
123+
return (
124+
Boolean(evidence.upstreamRef) ||
125+
evidence.hasUpstreamRemote ||
126+
(hasAnyRemotes && hasNonOriginRemote)
127+
);
128+
}
129+
130+
function formatEvidence(evidence: ReadmeTruthinessEvidence): readonly string[] {
131+
const remoteLines = evidence.remotes.map(
132+
(remote) => `${remote.name}: ${remote.fetchUrl}`,
133+
);
134+
return [
135+
evidence.upstreamRef !== null ? `upstream ref: ${evidence.upstreamRef}` : "upstream ref: none",
136+
remoteLines.length > 0 ? `remotes: ${remoteLines.join(", ")}` : "remotes: none",
137+
];
138+
}
139+
140+
export function detectReadmeTruthiness(
141+
readmeContents: string,
142+
gitEvidence: ReadmeTruthinessEvidence,
143+
): readonly ReadmeTruthinessFinding[] {
144+
const claims = collectReadmeClaims(readmeContents);
145+
const evidence = formatEvidence(gitEvidence);
146+
const findings: ReadmeTruthinessFinding[] = [];
147+
148+
if (!hasUpstreamFact(gitEvidence)) {
149+
return [];
150+
}
151+
152+
if (claims.builtFromScratch.length > 0) {
153+
findings.push({
154+
id: "technically-ambitious",
155+
title: "Technically Ambitious",
156+
message:
157+
"The README says this was built from the ground up, but local git still remembers where the lineage came from.",
158+
readmeClaims: claims.builtFromScratch,
159+
evidence,
160+
});
161+
}
162+
163+
if (claims.noFork.length > 0) {
164+
findings.push({
165+
id: "factual-fork-relationship",
166+
title: "Factual Fork Relationship",
167+
message:
168+
"The README calls this unrelated, yet git metadata points to a tracked upstream lineage.",
169+
readmeClaims: claims.noFork,
170+
evidence,
171+
});
172+
}
173+
174+
return findings;
175+
}
176+
177+
function renderConsoleFinding(finding: ReadmeTruthinessFinding): void {
178+
console.error(`- ${finding.title}: ${finding.message}`);
179+
for (const claim of finding.readmeClaims) {
180+
console.error(` - README ${String(claim.line)}: ${claim.text}`);
181+
}
182+
for (const source of finding.evidence) {
183+
console.error(` - evidence: ${source}`);
184+
}
185+
}
186+
187+
function main(): void {
188+
let readmeContents: string;
189+
try {
190+
readmeContents = readFileSync(README_PATH, "utf8");
191+
} catch {
192+
console.error("README truthiness checker could not read README.md from repository root.");
193+
process.exitCode = 1;
194+
return;
195+
}
196+
197+
const findings = detectReadmeTruthiness(readmeContents, collectGitEvidence(REPO_ROOT));
198+
if (findings.length === 0) {
199+
console.log("README truthiness check passed with nothing suspicious.");
200+
return;
201+
}
202+
203+
console.error("README Truthiness Checker: local facts disagree with README bravado.");
204+
for (const finding of findings) {
205+
renderConsoleFinding(finding);
206+
}
207+
process.exitCode = 1;
208+
}
209+
210+
if (import.meta.main) main();

0 commit comments

Comments
 (0)