fix(ops): refuse an account whose rows are not the thousandfold class

The provenance criterion proves where a row came from, not which build
imported it. An account re-imported on the fixed build carries the same
`EXPORT_XML_SOURCE_MAX` stamp and the same `stats:` external id with the
right numbers and no audit row, so it is selected exactly like a broken
one — and its rest days, under 200 m because the phone stayed at home,
survive the plausibility check after another thousandfold multiply.

So one candidate row that would leave the range now refuses the whole
account: nothing written, no audit row, the offending rows and a printed
reason instead. The idempotency guard only ever held against the
script's own prior run; this holds against the case it could not see.

The runbook says the same in words — a re-imported account must not be
repaired, the re-import already did it — and says what the dry run is
for: it is where the decision is made, not a formality in front of one
already taken.

Integration coverage for the re-imported account: correct rows, an
`--apply` run, nothing changes and no audit row is left behind.

Review finding H1.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 00:27:43 +02:00
1 parent 65e9ecbde6
commit 361e577864
4 files changed
+189 -18

No files matched your search

@@ -35,6 +35,16 @@
* archive on the fixed build repairs those, and is the authoritative
* repair for any account that still has its `export.zip`.
*
* ── The refusal ────────────────────────────────────────────────────
*
* The criterion proves an archive ORIGIN. It cannot prove the archive was
* imported on the broken build — an account re-imported since carries the
* same stamp with the right numbers, and its rest days still pass the
* plausibility check after another x1000. So any candidate whose repaired
* value would leave the range refuses the whole account: nothing is
* written, the rows are reported, and the operator decides. Re-importing
* the archive is the authoritative repair and needs no script.
*
* ── Idempotency ────────────────────────────────────────────────────
*
* Applying the repair writes one `AuditLog` row per account inside the
@@ -81,8 +91,8 @@ export interface AccountRepairPlan {
repairable: RepairCandidateRow[];
/**
* Candidates whose repaired value would leave the plausibility range
* (`validateMeasurementRange`) — reported, never written. A row here is
* a sign the account's rows are NOT the 1000x class.
* (`validateMeasurementRange`). One row here refuses the whole account:
* it is evidence the rows are NOT the 1000x class.
*/
outOfRange: RepairCandidateRow[];
/** Non-null when this account already carries the repair's audit row. */
@@ -178,6 +188,35 @@ export interface RepairOutcome {
userId: string;
updated: number;
skipped: number;
/**
* Why the account was left alone, when it was. Null on a run that wrote.
* The script prints it verbatim.
*/
refusedReason: string | null;
/** The out-of-range candidates — what a refusal was decided on. */
skippedRows: RepairCandidateRow[];
}
/**
* The refusal an out-of-range candidate earns.
*
* The provenance criterion proves an archive origin; it cannot prove the
* archive was imported on the BROKEN build. An account that has since been
* re-imported on the fixed build carries the same stamp with the right
* numbers, and its rest days — under 200 m, phone left at home — pass the
* plausibility check after another x1000 and would be written. So a single
* row that would leave the range is treated as what it is: evidence the
* account is not the 1000x class. The whole account stops.
*/
function outOfRangeRefusal(plan: AccountRepairPlan, factor: number): string {
return (
`${plan.outOfRange.length} of ${plan.candidateCount} candidate row(s) ` +
`leave the plausible range after x${factor}, so these rows are not the ` +
"1000x class — the account was most likely re-imported on the fixed " +
"build, or its archive was never in the affected unit. Nothing written. " +
"Check the dry run's worked examples against the account's Health app " +
"before forcing anything."
);
}
/**
@@ -192,8 +231,24 @@ export async function applyAppleHealthDistanceRepair(
): Promise<RepairOutcome> {
const factor = repairFactorFor(options.archiveUnit);
const skipped = plan.outOfRange.length;
if (plan.alreadyRepairedAt || plan.repairable.length === 0) {
return { userId: plan.userId, updated: 0, skipped };
const base = {
userId: plan.userId,
updated: 0,
skipped,
skippedRows: plan.outOfRange,
};
if (plan.alreadyRepairedAt) {
return {
...base,
refusedReason: `already repaired on ${plan.alreadyRepairedAt.toISOString()}`,
};
}
// Refuse the ACCOUNT, not just the row: see `outOfRangeRefusal`.
if (plan.outOfRange.length > 0) {
return { ...base, refusedReason: outOfRangeRefusal(plan, factor) };
}
if (plan.repairable.length === 0) {
return { ...base, refusedReason: "no repairable rows" };
}
const ids = plan.repairable.map((row) => row.id);
@@ -239,7 +294,13 @@ export async function applyAppleHealthDistanceRepair(
"repair-apple-health-distance",
);
return { userId: plan.userId, updated: ids.length, skipped };
return {
userId: plan.userId,
updated: ids.length,
skipped,
refusedReason: null,
skippedRows: plan.outOfRange,
};
}
/** Narrowing helper for the script's `--unit` flag. */