mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
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:
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. */
|
||||
|
||||
Reference in new issue
Block a user