refactor(measurements): move the batch ingest out of its route

postBatch was one 1255-line function. The route keeps authentication, the
rate limit, the body, the entry cap, the schema and the bridge-source
refusal; the ingest moves verbatim into lib/measurements/batch-ingest/, one
module per stage in the order they always ran: the first pass (prepare),
the measurement writes with their prefilters and set-based insert, the
hourly sums, the sync checkpoint, the audit and wide-event report, and the
cache and arrival tail. The route is 274 lines; no function in the folder
is over 200.

The parity walk recorded before the move (measurements-batch.parity) is
unchanged, as is the set-insert golden. The guards that read the route for
the per-entry external-id check, the plausibility validator, the write
tail, the dense-read bound and the unit audit now read the service files.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 11:30:46 +02:00
1 parent 3df80389ab
commit 25c3c050ee
14 files changed
+1831 -1323

No files matched your search

@@ -48,7 +48,7 @@ import {
convertHkValue,
isConvertibleHkUnit,
} from "@/lib/measurements/hk-units";
import { readSource } from "@/__tests__/helpers/source-files";
import { readSource, readSourceFolder } from "@/__tests__/helpers/source-files";
const ENTRIES = Object.values(APPLE_HEALTH_TYPE_MAP);
@@ -194,10 +194,17 @@ describe("Apple Health mapping — the record's own unit (issue #944)", () => {
"utf8",
);
expect(archive).toContain(optIn);
const batch = readSource("src/app/api/measurements/batch/route.ts", [
"export const POST",
"hkIdentifier",
]);
// The route parses; its ingest maps the entries in the service folder.
const batch = [
readSource("src/app/api/measurements/batch/route.ts", [
"export const POST",
"hkIdentifier",
]),
...readSourceFolder("src/lib/measurements/batch-ingest", {
floor: 5,
}).map(([, text]) => text),
].join("\n");
expect(batch).toContain("mapAppleHealthEntry(");
expect(
batch,
"the batch route opted into record-unit conversion",
@@ -64,6 +64,9 @@ const SCOPED_DIRS = [
"lib/export",
"lib/targets/",
"lib/doctor-report",
// Request-handler logic moved out of its route into a service keeps the
// route's scope.
"lib/measurements/batch-ingest/",
];
/** Types a device writes at sampling rate rather than a few times a day. */
@@ -103,7 +106,7 @@ const ALLOWED: Record<string, string> = {
"Health-score inputs: steps (drained to one row per day nightly), sleep, waist, weight, blood pressure, fasting glucose only.",
'app/api/analytics/route.ts::"BLOOD_GLUCOSE"':
"The 30-day per-context glucose summary, the same window and bound as the dashboard and coach glucose panels: at a sensor's fixed rate, below 45 000 rows.",
"app/api/measurements/batch/route.ts::p.row.type as MeasurementType":
"lib/measurements/batch-ingest/write-measurements.ts::p.row.type as MeasurementType":
"Cross-source merge probe: one ±tolerance window around each posted reading's instant, so the rows found are bounded by the batch size.",
"app/api/measurements/route.ts::(shorthand)":
"Cross-source merge probe for one posted sleep segment: SLEEP_DURATION in a ±tolerance window around its end.",
@@ -394,17 +394,28 @@ describe("the batch surfaces refuse per entry, never whole-batch", () => {
// Each of these drains a client sync queue. A schema-level refusal here
// would stop a whole batch of good rows because of one bad one, which is
// worse than the duplicate it prevents.
const BATCH_ROUTES = [
"app/api/measurements/batch/route.ts",
"app/api/mood-entries/bulk/route.ts",
"app/api/medications/intake/bulk/route.ts",
"app/api/workouts/batch/route.ts",
"app/api/cycle/day-logs/bulk/route.ts",
"app/api/import/route.ts",
];
//
// A surface whose ingest moved into a service names the service files
// beside the route: the route parses, the service refuses per entry.
const BATCH_SURFACES: Record<string, readonly string[]> = {
"app/api/measurements/batch/route.ts": [
"lib/measurements/batch-ingest/prepare.ts",
"lib/measurements/batch-ingest/ingest.ts",
],
"app/api/mood-entries/bulk/route.ts": [],
"app/api/medications/intake/bulk/route.ts": [],
"app/api/workouts/batch/route.ts": [],
"app/api/cycle/day-logs/bulk/route.ts": [],
"app/api/import/route.ts": [],
};
const BATCH_ROUTES = Object.keys(BATCH_SURFACES);
/** The route and the service files that carry its per-entry ingest. */
const surface = (rel: string): string =>
[rel, ...BATCH_SURFACES[rel]].map((file) => body(file)).join("\n");
it.each(BATCH_ROUTES)("%s classifies each entry itself", (rel) => {
const source = body(rel);
const source = surface(rel);
expect(source).toContain("classifyExternalId");
expect(source).toContain("unstableExternalIdMeta");
});
@@ -414,7 +425,7 @@ describe("the batch surfaces refuse per entry, never whole-batch", () => {
(rel) => {
// The JSON importer has no per-entry status envelope — it reports a
// skipped COUNT — so it is the one batch surface without the code.
expect(body(rel)).toContain("UNSTABLE_EXTERNAL_ID_REASON");
expect(surface(rel)).toContain("UNSTABLE_EXTERNAL_ID_REASON");
},
);
});
@@ -73,10 +73,13 @@ const WRITERS: Record<string, Disposition> = {
validatedIn: "lib/validations/measurement.ts",
},
"app/api/import/route.ts": { kind: "validator" },
// v1.44 — the set-based insert writes rows that already passed the
// route's `validateMeasurementRange` per entry; everything else goes
// through the reconciler, which gates itself.
"app/api/measurements/batch/route.ts": { kind: "validator" },
// The batch ingest's set-based insert writes rows that already passed
// `validateMeasurementRange` per entry in its first pass; everything else
// goes through the reconciler, which gates itself.
"lib/measurements/batch-ingest/write-measurements.ts": {
kind: "upstream",
validatedIn: "lib/measurements/batch-ingest/prepare.ts",
},
"lib/mcp/writes.ts": { kind: "validator" },
"lib/measurements/create-from-telegram.ts": { kind: "validator" },
"lib/measurements/import-apple-health-export.ts": { kind: "validator" },
@@ -32,6 +32,13 @@ const WRITE_RE =
/** Runs `afterMeasurementMutation` on the rows it wrote. */
const TAIL = "runs afterMeasurementMutation";
/**
* Writes here; the module named runs `afterMeasurementMutation` on the same
* rows (a service split into stages, whose write and tail are siblings).
*/
const TAIL_VIA_PREFIX = "runs afterMeasurementMutation via ";
const TAIL_VIA = (file: string) => `${TAIL_VIA_PREFIX}${file}`;
/**
* Every file with a measurement write, and how it keeps the two readers
* current. A file listed here must still contain a write (a stale entry
@@ -44,7 +51,9 @@ const WRITE_SITES: Record<string, string> = {
"app/api/measurements/bulk-delete/route.ts": TAIL,
"app/api/measurements/by-external-ids/route.ts": TAIL,
"app/api/measurements/restore/route.ts": TAIL,
"app/api/measurements/batch/route.ts": TAIL,
"lib/measurements/batch-ingest/write-measurements.ts": TAIL_VIA(
"lib/measurements/batch-ingest/tail.ts",
),
"app/api/measurements/route.ts": TAIL,
"app/api/mental-health/assessments/route.ts": TAIL,
"lib/insights/score-row.ts": TAIL,
@@ -157,8 +166,14 @@ describe("measurement write tail", () => {
it("the files that claim the shared tail call it", () => {
const missing = Object.entries(WRITE_SITES)
.filter(([, reason]) => reason === TAIL)
.map(([file]) => file)
.filter(
([, reason]) => reason === TAIL || reason.startsWith(TAIL_VIA_PREFIX),
)
.map(([file, reason]) =>
reason.startsWith(TAIL_VIA_PREFIX)
? reason.slice(TAIL_VIA_PREFIX.length)
: file,
)
.filter(
(file) =>
!/afterMeasurementMutation\s*\(/.test(