Hygiene: name the FHIR immunization gap, cache the digest offline, freeze the no-FK pair, document the pin bump

Four small debts from the audit tail. The FHIR bundle's missing
Immunization resource gets the same written-decision paragraph the visits
exclusion carries — deliberate follow-up, not oversight. The service
worker's offline allowlist gains /api/daily/digest (both copies, policy
module and sw.js, plus the test) so the Today rail renders on an offline
reopen. The two Telegram plumbing tables that link to an account by a
bare userId string are frozen as a closed NO_FK_BY_DESIGN set in the wipe
guard — a third such table now fails the test instead of passing rule 1
silently. And deploy.md documents the manual digest-pin bump end to end:
imagetools inspect, the env update, and the three-probe /api/version
verification with the too-fast-deploy warning.
This commit is contained in:
Marc-André Bombeck committed 2026-08-14 09:56:45 +02:00
1 parent 9d64eb6aa2
commit bcabd43ca7
6 files changed
+79

No files matched your search

@@ -280,4 +280,36 @@ describe("data-wipe completeness", () => {
OR: [{ recordUserId: "u1" }, { recipientUserId: "u1" }],
});
});
it("freezes the set of user-linked tables that carry no foreign key", () => {
// v1.37.20 (A6-19) — NO_FK_BY_DESIGN, as an assertion instead of a pair
// of FK migrations. The two Telegram plumbing tables link to an account
// by a bare `userId` string on purpose: both hold a bounded, short-lived
// window of chat state (scheduled message deletions, prompt contexts)
// that a cron already reaps, and a cascade would buy nothing the wipe
// plan does not already do — rule 1 catches them because the column NAME
// matches the convention. That is the exact fragility this test freezes:
// a THIRD such table would also pass rule 1 silently, without anyone
// deciding it should. So the set is closed here; growing it is a
// decision made in this file, with a reason, or by adding the FK.
const NO_FK_BY_DESIGN = new Set([
"TelegramScheduledDeletion",
"TelegramPromptContext",
]);
const bareUserIdModels = [...models.entries()]
.filter(
([name, shape]) =>
name !== "User" &&
shape.scalars.includes("userId") &&
!shape.relations.some((rel) => rel.target === "User"),
)
.map(([name]) => name)
.sort();
expect(bareUserIdModels).toEqual([...NO_FK_BY_DESIGN].sort());
// And both stay in the wipe plan — the assertion above is about schema
// shape, this one about the plan actually reaching them.
for (const model of NO_FK_BY_DESIGN) {
expect(WIPE_MODELS).toContain(model);
}
});
});