Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion scripts/check-privacy-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ function commitExists(commit) {
}
}

function isShallowClone() {
try {
return (
execFileSync("git", ["rev-parse", "--is-shallow-repository"], {
cwd: root,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim() === "true"
);
} catch {
return false;
}
}

function commitIsAncestor(commit) {
try {
execFileSync("git", ["merge-base", "--is-ancestor", commit, "HEAD"], { cwd: root, stdio: "ignore" });
Expand Down Expand Up @@ -208,10 +222,52 @@ export function validatePrivacyReadiness(
return errors;
}

/**
* Whether the reviewedCommit checks can be skipped for this run.
*
* Only in structural mode, and only when the commit is genuinely unreachable in
* a genuinely shallow clone. Release mode never skips: `check:privacy-readiness
* :release` and `governance:release` are release gates, and the reviewedCommit
* ancestry plus evidence-at-commit checks are what bind the register to this
* repository. Dropping them to spare a truncated checkout would let a release
* print PRIVACY_READINESS_PASS having proved nothing about the reviewed commit.
*/
export function shallowSkipDecision({ release, shallow, commitPresent }) {
if (!shallow || commitPresent) return { skip: false, blocked: false };
if (release) return { skip: false, blocked: true };
return { skip: true, blocked: false };
}

function main() {
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const release = process.argv.includes("--release");
const errors = validatePrivacyReadiness(manifest, { release });
// Same reasoning as check-clinical-hazard-controls.mjs: on a depth-one clone
// the reviewedCommit checks report a real commit as missing, which reads as a
// corrupt register rather than a truncated checkout. Say what was skipped.
// CI's static-pr job checks out with fetch-depth 0, where the checks do run.
const { skip, blocked } = shallowSkipDecision({
release,
shallow: isShallowClone(),
commitPresent: commitExists(manifest?.reviewedCommit ?? ""),
});
if (blocked) {
console.error("PRIVACY_READINESS_FAIL mode=release");
console.error(
`- reviewedCommit ${manifest?.reviewedCommit ?? "(unset)"} is unreachable in this shallow clone, and ` +
"release mode will not skip the ancestry and evidence-at-commit checks that bind this register to the " +
"repository. Re-run on a full-history checkout: git fetch --unshallow (or git fetch --deepen=2000).",
);
process.exit(1);
}
if (skip) {
console.warn(
"PRIVACY_READINESS_SHALLOW_CLONE: this is a shallow git clone and reviewedCommit is not present, " +
"so the reviewedCommit existence/ancestry and evidence-at-commit checks were skipped. Run on a " +
"full-history checkout (git fetch --unshallow) to prove them; every other check below still ran. " +
"Release mode does not skip them.",
);
}
const errors = validatePrivacyReadiness(manifest, { release, checkGit: !skip });
if (errors.length) {
console.error(`PRIVACY_READINESS_FAIL mode=${release ? "release" : "structural"}`);
for (const error of errors) console.error(`- ${error}`);
Expand Down
69 changes: 67 additions & 2 deletions tests/privacy-readiness-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { validatePrivacyReadiness } from "../scripts/check-privacy-readiness.mjs";
import { shallowSkipDecision, validatePrivacyReadiness } from "../scripts/check-privacy-readiness.mjs";

const manifest = JSON.parse(
readFileSync(new URL("../docs/governance/privacy-readiness.v1.json", import.meta.url), "utf8"),
Expand All @@ -11,9 +12,73 @@ const retentionParityMigration = readFileSync(
"utf8",
);

// Same shallow-clone guard as tests/clinical-hazard-controls.test.ts and
// tests/rag-plan-package-parity.test.ts. A web-container session clones at
// depth ~102, so reviewedCommit resolves to nothing and the register looks
// corrupt when only the history is truncated.
function isShallowClone(): boolean {
try {
return (
execFileSync("git", ["rev-parse", "--is-shallow-repository"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim() === "true"
);
} catch {
return false;
}
}

function isCommitAvailable(commit: string): boolean {
try {
execFileSync("git", ["cat-file", "-e", `${commit}^{commit}`], {
stdio: ["ignore", "ignore", "ignore"],
});
return true;
} catch {
return false;
}
}

describe("privacy readiness contract", () => {
it("accepts the honest structural register", () => {
expect(validatePrivacyReadiness(manifest)).toEqual([]);
// Probe only. The unit suite is offline, so this never fetches to deepen
// history the way the sibling governance specs do: `git fetch --deepen` is
// remote I/O and a repository mutation, and an unreachable or
// credential-prompting remote would stall the suite instead of failing.
// A truncated checkout simply skips the commit checks and says so.
let checkGit = true;
if (!isCommitAvailable(manifest.reviewedCommit) && isShallowClone()) {
console.warn(
`PRIVACY_READINESS_SHALLOW_CLONE: reviewedCommit ${manifest.reviewedCommit} is unavailable in this shallow clone; skipping commit ancestry check. Run on a full-history checkout to prove it.`,
);
checkGit = false;
}
expect(validatePrivacyReadiness(manifest, { checkGit })).toEqual([]);
});

it("never skips the reviewedCommit checks in release mode, however shallow the checkout", () => {
// The release gate's repository binding is exactly these checks, so a
// truncated checkout must block the release rather than quietly pass it.
expect(shallowSkipDecision({ release: false, shallow: true, commitPresent: false })).toEqual({
skip: true,
blocked: false,
});
expect(shallowSkipDecision({ release: true, shallow: true, commitPresent: false })).toEqual({
skip: false,
blocked: true,
});
// A reachable commit or a full clone is proved, not skipped, in either mode.
for (const release of [false, true]) {
expect(shallowSkipDecision({ release, shallow: true, commitPresent: true })).toEqual({
skip: false,
blocked: false,
});
expect(shallowSkipDecision({ release, shallow: false, commitPresent: false })).toEqual({
skip: false,
blocked: false,
});
}
});

it("keeps Railway processor evidence linked to the privacy impact assessment", () => {
Expand Down
Loading