Skip to content

Commit beca15e

Browse files
committed
chore(ci): add a drift guardrail against new gittensory branding
This session found and fixed real behavioral drift left over from the loopover rebrand -- a hardcoded MCP resource URI (#6786), a Qdrant collection default, and metric names that only got caught by manual investigation. scripts/check-branding-drift.mjs snapshots today's known "gittensory" mentions in executable src/ and packages/*/{bin,lib,src} code (test/**, docs, and CHANGELOGs are excluded -- those legitimately reference the rename permanently) and fails CI if a file's count rises without the baseline being regenerated, the same generated-artifact convention this repo already uses for openapi.json and cf-typegen. Wired into npm run test:ci via a new branding-drift:check script; branding-drift:update regenerates the baseline for an intentional change.
1 parent 67af510 commit beca15e

5 files changed

Lines changed: 281 additions & 1 deletion

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
"ui:version-audit": "node scripts/check-ui-mcp-version-copy.mjs",
6666
"ui:version-audit:sync": "node scripts/check-ui-mcp-version-copy.mjs --write",
6767
"docs:drift-check": "node scripts/check-docs-drift.mjs",
68+
"branding-drift:check": "node scripts/check-branding-drift.mjs",
69+
"branding-drift:update": "node scripts/check-branding-drift.mjs --update",
6870
"manifest:drift-check": "tsx scripts/check-manifest-drift.mjs",
6971
"engine-parity:drift-check": "tsx scripts/check-engine-parity.ts",
7072
"ui:deploy": "npm run ui:build && npm run ui:deploy:built",
@@ -94,7 +96,7 @@
9496
"test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs",
9597
"test:smoke:browser:install": "playwright install chromium",
9698
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
97-
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
99+
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
98100
"test:release": "npm run test:ci && npm run changelog:check",
99101
"test:release:mcp": "npm run test:ci",
100102
"test:watch": "vitest",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"packages/loopover-engine/src/signals/engine.ts": 2,
3+
"packages/loopover-mcp/bin/loopover-mcp.js": 5,
4+
"src/api/routes.ts": 3,
5+
"src/db/repositories.ts": 1,
6+
"src/github/app.ts": 11,
7+
"src/github/backfill.ts": 7,
8+
"src/github/commands.ts": 2,
9+
"src/github/comments.ts": 3,
10+
"src/integrations/project-tracker-adapter.ts": 1,
11+
"src/orb/federated-bundle.ts": 1,
12+
"src/queue/processors.ts": 6,
13+
"src/review/check-names.ts": 8,
14+
"src/review/contributor-calibration.ts": 2,
15+
"src/review/enrichment-analyzers-taxonomy.ts": 1,
16+
"src/review/enrichment-wire.ts": 1,
17+
"src/review/ops-wire.ts": 3,
18+
"src/review/outcomes-wire.ts": 2,
19+
"src/review/parity-wire.ts": 9,
20+
"src/review/planner.ts": 1,
21+
"src/review/rag.ts": 3,
22+
"src/review/repo-doc-render.ts": 2,
23+
"src/review/repo-skill-render.ts": 2,
24+
"src/review/selftune-wire.ts": 1,
25+
"src/selfhost/ai.ts": 6,
26+
"src/selfhost/health.ts": 3,
27+
"src/selfhost/monitored-work.ts": 1,
28+
"src/selfhost/orb-collector.ts": 1,
29+
"src/selfhost/sentry.ts": 2,
30+
"src/services/ai-review.ts": 4,
31+
"src/services/ai-slop.ts": 1,
32+
"src/services/ai-summaries.ts": 1,
33+
"src/services/contributor-issue-draft.ts": 2,
34+
"src/services/merge-failure.ts": 1,
35+
"src/services/notify-pagerduty.ts": 2,
36+
"src/upstream/ruleset.ts": 3,
37+
"src/utils/crypto.ts": 1
38+
}

scripts/check-branding-drift.d.mts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const BASELINE_RELATIVE_PATH: string;
2+
export const BRANDING_DRIFT_PATHSPECS: string[];
3+
4+
export function scanBrandingHits(options: {
5+
root: string;
6+
exec?: (root: string, args: string[]) => string;
7+
}): Record<string, number>;
8+
9+
export function diffBrandingBaseline(baseline: Record<string, number>, current: Record<string, number>): string[];

scripts/check-branding-drift.mjs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env node
2+
// Guards against the "gittensory" branding silently creeping back into runtime source after the LoopOver
3+
// rebrand -- distinct from docs/prose residue (already owned by the ongoing rebrand-sweep PR series), this
4+
// targets the class of bug that actually broke things: a hardcoded metric name, MCP resource URI, or Qdrant
5+
// collection default left on the pre-rebrand string. That exact pattern caused live drift more than once
6+
// (e.g. #6786 -- ENRICHMENT_ANALYZERS_URI silently left as "gittensory://enrichment-analyzers" while its
7+
// sibling FINDING_TAXONOMY_URI was correctly renamed in the same PR). Scoped to executable code in `src/**`
8+
// and each workspace package's `bin/`, `lib/`, `src/`, `scripts/` dirs -- NOT `test/**` or `*.md`, where a
9+
// literal "gittensory" is usually an intentional, permanent historical reference (a Sentry ticket ID like
10+
// GITTENSORY-K/8, a stable comment-marker the bot must keep matching in already-posted PR bodies, a DB
11+
// `source` column value joined against historical rows) rather than drift; those files churn constantly and
12+
// would make this check pure noise if included.
13+
//
14+
// Baseline-diff, not a hard "zero gittensory" ban: scripts/branding-drift-baseline.json snapshots today's
15+
// known-legitimate per-file hit count (grandfathered, same shape as KNOWN_MIGRATION_DUPLICATES in
16+
// src/db/migration-collisions.ts). A file's count rising means new drift; falling means a cleanup landed
17+
// without updating the baseline. Either way the fix is the same: run `npm run branding-drift:update` and
18+
// commit the regenerated baseline -- mirrors this repo's existing generated-artifact convention (openapi.json,
19+
// cf-typegen, migrations) rather than inventing a new one.
20+
import { execFileSync } from "node:child_process";
21+
import { readFileSync, writeFileSync } from "node:fs";
22+
import { join } from "node:path";
23+
import { fileURLToPath } from "node:url";
24+
25+
export const BASELINE_RELATIVE_PATH = "scripts/branding-drift-baseline.json";
26+
27+
// git pathspecs: executable code only. Each workspace package's bin/lib/src/scripts dirs mirror the
28+
// top-level src/** scope; docs/README/CHANGELOG/schema/terraform/css and every test dir are deliberately
29+
// excluded (see header comment).
30+
export const BRANDING_DRIFT_PATHSPECS = [
31+
"src/**/*.ts",
32+
"src/**/*.tsx",
33+
"packages/*/bin/**",
34+
"packages/*/lib/**/*.js",
35+
"packages/*/lib/**/*.ts",
36+
"packages/*/src/**/*.ts",
37+
"packages/*/scripts/**/*.mjs",
38+
":(exclude)**/*.test.ts",
39+
":(exclude)**/*.test.tsx",
40+
":(exclude)packages/*/test/**",
41+
];
42+
43+
function defaultExec(root, args) {
44+
try {
45+
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
46+
} catch (error) {
47+
// git grep exits 1 for "zero matches" -- not a real failure, just an empty result.
48+
if (error.status === 1) return "";
49+
throw error;
50+
}
51+
}
52+
53+
/** Every tracked, non-excluded file with >=1 case-insensitive "gittensory" MATCHING LINE, and that line
54+
* count (line-granularity, not raw occurrence count -- sufficient to detect "something new appeared" without
55+
* the fragility of an exact substring-occurrence count). Uses `git grep`, so it only ever sees tracked files
56+
* exactly as CI would check them out -- no node_modules/dist/build noise to exclude by hand. */
57+
export function scanBrandingHits({ root, exec = defaultExec }) {
58+
const output = exec(root, ["grep", "-ciI", "gittensory", "--", ...BRANDING_DRIFT_PATHSPECS]);
59+
const counts = {};
60+
for (const line of output.split("\n")) {
61+
if (!line) continue;
62+
const separatorIndex = line.lastIndexOf(":");
63+
const file = line.slice(0, separatorIndex);
64+
counts[file] = Number(line.slice(separatorIndex + 1));
65+
}
66+
return counts;
67+
}
68+
69+
/** Pure comparison: every failure is one of "increased" (new drift -- or a file that didn't exist in the
70+
* baseline at all, same failure shape) or "decreased" (a cleanup landed; still a failure so the baseline
71+
* never silently drifts stale, but a one-line fix -- regenerate). Sorted for stable, reviewable CI output. */
72+
export function diffBrandingBaseline(baseline, current) {
73+
const failures = [];
74+
const files = new Set([...Object.keys(baseline), ...Object.keys(current)]);
75+
for (const file of [...files].sort()) {
76+
const before = baseline[file] ?? 0;
77+
const after = current[file] ?? 0;
78+
if (after > before) {
79+
failures.push(
80+
`${file}: "gittensory" mentions increased from ${before} to ${after} -- looks like new branding drift, not an intentional historical reference. If it genuinely belongs (e.g. a permanent Sentry ticket ID or a stable comment-marker already posted to live PRs), run \`npm run branding-drift:update\` and commit the regenerated baseline.`,
81+
);
82+
} else if (after < before) {
83+
failures.push(
84+
`${file}: "gittensory" mentions decreased from ${before} to ${after} -- looks like a cleanup landed without regenerating the baseline. Run \`npm run branding-drift:update\` and commit the result.`,
85+
);
86+
}
87+
}
88+
return failures;
89+
}
90+
91+
function readBaseline(root) {
92+
return JSON.parse(readFileSync(join(root, BASELINE_RELATIVE_PATH), "utf8"));
93+
}
94+
95+
function writeBaseline(root, counts) {
96+
const sorted = Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
97+
writeFileSync(join(root, BASELINE_RELATIVE_PATH), `${JSON.stringify(sorted, null, 2)}\n`);
98+
}
99+
100+
function main() {
101+
const root = process.cwd();
102+
const update = process.argv.includes("--update");
103+
const current = scanBrandingHits({ root });
104+
105+
if (update) {
106+
writeBaseline(root, current);
107+
console.log(`Branding-drift baseline regenerated: ${Object.keys(current).length} file(s) with a "gittensory" reference.`);
108+
return;
109+
}
110+
111+
const baseline = readBaseline(root);
112+
const failures = diffBrandingBaseline(baseline, current);
113+
114+
if (failures.length > 0) {
115+
console.error(`Branding-drift check found ${failures.length} issue(s):`);
116+
for (const failure of failures) console.error(failure);
117+
process.exit(1);
118+
}
119+
120+
console.log(`Branding-drift check ok: ${Object.keys(current).length} file(s) match the recorded baseline.`);
121+
}
122+
123+
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { execFileSync } from "node:child_process";
2+
import { describe, expect, it } from "vitest";
3+
import { diffBrandingBaseline, scanBrandingHits } from "../../scripts/check-branding-drift.mjs";
4+
5+
describe("scanBrandingHits", () => {
6+
it("parses git grep -c output into a { file: count } map", () => {
7+
const exec = () => "src/a.ts:2\nsrc/b.ts:1\n";
8+
const result = scanBrandingHits({ root: "/fake", exec });
9+
10+
expect(result).toEqual({ "src/a.ts": 2, "src/b.ts": 1 });
11+
});
12+
13+
it("returns an empty map when there is no output (git grep found nothing)", () => {
14+
const exec = () => "";
15+
const result = scanBrandingHits({ root: "/fake", exec });
16+
17+
expect(result).toEqual({});
18+
});
19+
20+
it("uses the LAST colon as the file/count separator, so a path containing a colon still parses", () => {
21+
const exec = () => "src/weird:name.ts:3\n";
22+
const result = scanBrandingHits({ root: "/fake", exec });
23+
24+
expect(result).toEqual({ "src/weird:name.ts": 3 });
25+
});
26+
27+
it("passes the branding-drift pathspecs through to the injected exec", () => {
28+
let capturedArgs: string[] = [];
29+
const exec = (_root: string, args: string[]) => {
30+
capturedArgs = args;
31+
return "";
32+
};
33+
scanBrandingHits({ root: "/fake", exec });
34+
35+
expect(capturedArgs[0]).toBe("grep");
36+
expect(capturedArgs).toContain("src/**/*.ts");
37+
expect(capturedArgs).toContain(":(exclude)**/*.test.ts");
38+
});
39+
40+
// Real regression guard, mirroring check-manifest-drift-script.test.ts's own real-repo-state test: proves
41+
// the actual defaultExec (real `git grep` subprocess, real exit-1-means-empty handling) works against this
42+
// repo's real tracked files, not just the injected fake above.
43+
it("runs the real git grep against this repo without throwing", () => {
44+
const result = scanBrandingHits({ root: process.cwd() });
45+
46+
expect(typeof result).toBe("object");
47+
for (const count of Object.values(result)) {
48+
expect(count).toBeGreaterThan(0);
49+
}
50+
});
51+
});
52+
53+
describe("diffBrandingBaseline", () => {
54+
it("reports no failures when baseline and current match exactly", () => {
55+
const failures = diffBrandingBaseline({ "src/a.ts": 2 }, { "src/a.ts": 2 });
56+
57+
expect(failures).toEqual([]);
58+
});
59+
60+
it("flags a file whose count increased (new drift)", () => {
61+
const failures = diffBrandingBaseline({ "src/a.ts": 1 }, { "src/a.ts": 2 });
62+
63+
expect(failures).toHaveLength(1);
64+
expect(failures[0]).toContain("src/a.ts");
65+
expect(failures[0]).toContain("increased from 1 to 2");
66+
expect(failures[0]).toContain("branding-drift:update");
67+
});
68+
69+
it("flags a brand-new file not present in the baseline at all (increased from 0)", () => {
70+
const failures = diffBrandingBaseline({}, { "src/new.ts": 1 });
71+
72+
expect(failures).toHaveLength(1);
73+
expect(failures[0]).toContain("increased from 0 to 1");
74+
});
75+
76+
it("flags a file whose count decreased (stale baseline after a cleanup)", () => {
77+
const failures = diffBrandingBaseline({ "src/a.ts": 3 }, { "src/a.ts": 1 });
78+
79+
expect(failures).toHaveLength(1);
80+
expect(failures[0]).toContain("decreased from 3 to 1");
81+
});
82+
83+
it("flags a file removed entirely from current (decreased to 0)", () => {
84+
const failures = diffBrandingBaseline({ "src/gone.ts": 2 }, {});
85+
86+
expect(failures).toHaveLength(1);
87+
expect(failures[0]).toContain("decreased from 2 to 0");
88+
});
89+
90+
it("reports one failure per affected file, sorted, when several files differ", () => {
91+
const failures = diffBrandingBaseline({ "src/b.ts": 1, "src/a.ts": 1 }, { "src/b.ts": 2, "src/a.ts": 2 });
92+
93+
expect(failures).toHaveLength(2);
94+
expect(failures[0]).toContain("src/a.ts");
95+
expect(failures[1]).toContain("src/b.ts");
96+
});
97+
});
98+
99+
describe("check-branding-drift script (real repo state)", () => {
100+
// Most important test in this file: proves the checked-in baseline actually matches the real repo right
101+
// now. If this fails, real drift landed (or a cleanup did) without regenerating the baseline -- either way,
102+
// fix it with `npm run branding-drift:update`, don't weaken this test.
103+
it("the committed baseline matches the real current repo state (regression guard)", () => {
104+
const output = execFileSync("node", ["scripts/check-branding-drift.mjs"], { encoding: "utf8" });
105+
106+
expect(output).toMatch(/Branding-drift check ok: \d+ file\(s\) match the recorded baseline\./);
107+
});
108+
});

0 commit comments

Comments
 (0)