Changes touching this path
- the shipper gets the entry point a timer will run, and the reviews caught three defects in it that no gate could: an unreadable log read as a CLEAN RUN, because open swallowed every error and the caller skipped the file - so an EACCES on www-data adm 0640, which is the precise hazard the whole runtime-home decision exists to handle, produced scanned zero, failed false, exit zero, indistinguishable from a fresh box. only absence is swallowed now and a permission error is rethrown, with a control asserting a genuinely missing file is still skipped, since every box before its first rotation has no .1.
the second was a rotation-sized race in the middle of the file source: stat by name then open by name lets logrotate rename between the two calls, so the read lands on the NEW inode bytes and they are committed under the OLD inode position - a double count and a misattribution at once, and scheduled rather than theoretical since rotation is unconditional and daily. a handle is bound to an inode rather than a name, so the source opens once and measures and reads through that same handle, which makes the race unrepresentable instead of unlikely.
the third would have broken every land and every deploy: build:shipper invokes esbuild, which was in overrides and in NEITHER dependency list, resolving only as vite transitive binary - and putting that step inside build made the gate depend on an undeclared one. declared now, matching the override range rather than pinned exact, which npm refuses as a conflict.
found by RUNNING the bundle rather than building it: bundled to esm it compiles clean and dies at startup on Dynamic require of pg, which is CommonJS - and external is no escape because the image has no node_modules at all since nitro bundles its own into server libs. it is cjs, pinned four ways including that the step runs AFTER vite build, since vite writes .output and an earlier step would have its artifact deleted silently.
two doc claims are narrowed rather than left standing, both mine and both the catalogued class: the entry said what the timer runs in the present tense with no timer anywhere, and credentials.ts said the site has no one entry point this module owns, which was true of the serving process and became false of the deployment the moment a one-shot binary existed. and two of my own assertions are DELETED rather than kept, because they were strict consequences of the set-equality above them and could not fail while it passed - decoration that read like coverage (#1756)
0c0385a8 · dbf3dbe6… - the failure signal is judged per FILE rather than per run, because one healthy log was answering for a broken one, and both halves of the old condition are shown wrong by reverting them rather than argued: the code that landed an hour ago in #1778 set failed once at the end of the run, bytesConsumed greater than zero and inserted equal to zero, which means the forge log parsing normally while the site log refuses every line reports SUCCESS - inserted is positive because of the forge, the busier surface goes dark, and the one signal built to tell an empty dashboard apart from a quiet site is the thing that hides it. the two logs are written by the same nginx stanza but rewritten per vhost, so diverging one vhost at a time is how this actually arrives rather than a hypothetical.
the second half is the opposite error. bytes and no rows also describes a file holding only blank lines, and shipFile skips those with a bare continue without counting them as refusals - so nginx touching a log was a format break. the predicate now requires that something was actually REFUSED, which is the evidence that we read something and failed to read it: understoodNothing is inserted equal to zero AND at least one refusal, evaluated on each ShipReport before its counts are summed away into the run total. blank lines refuse nothing and are not a failure; a format change refuses everything and is.
both were verified by mutation rather than by passing. restoring the exact run-wide expression that shipped turns BOTH new tests red at once, which is the demonstration that neither defect was theoretical, and dropping only the refusal term turns only the blank-line control red, which is the demonstration that the two terms are independent rather than one of them decorative.
main gets its exit code pinned for the first time, and the mapping is the only teeth any of this has - failed is inert unless something acts on it, and the single thing that does is what systemctl sees. it was untested because main could only run against a live Postgres, so the writer becomes a parameter with the production default beside it, the same shape logDir already had; mutating the return to a constant zero turns exactly one test red. four cases: exit 1 on a log understood not at all, exit 0 on a healthy run, exit 0 on an empty box, and the writer closed even when the run throws mid-batch - that last one written against a writer whose commitBatch rejects, because a first version pointed at a missing directory and never threw at all, which would have asserted the finally clause runs on the path where nothing went wrong.
two cleanups from the same review. a duplicated pair of assertions in the bundle-format block, where the earlier copy carried a comment reading build:shipper sits inside , so - the backticked word eaten by an inline edit, and vitest permits two it blocks of the identical name in one describe, so nothing reported either the duplication or the damage. and the FAILED line and three doc comments still described the old run-wide condition in words, which is the scope-drift class this repo keeps catching: a true-sounding sentence that outlived the code under it (#1756)
c1629576 · dbf3dbe6…diff - the retention number becomes an OUTCOME: a daily rollup writes the marginal totals that survive forever and only then drops raw partitions that are both rolled up and past the window
loot#1759, decided by loot#1622 and amended by loot#1625. Until this, thirty
days was a sentence: nothing dropped a row and the raw table grew without bound
on the Postgres volume the relay, the forge and the site share.
The drop is gated on the watermark, never on time alone, and that is the
ticket whole thesis. event_drop_expired() in migration 0003 takes NO argument:
it reads rolled_up_through from the table, uses its own UTC clock, and applies
a retention CONSTANT - so the rollup role cannot widen the drop by lying to it,
and with no watermark it drops nothing. A partition is due only when its day is
at-or-below the watermark AND its whole day is outside the window, so a stalled
rollup ACCUMULATES partitions, which is the observable failure mode we want,
rather than deleting a day nobody aggregated.
The ticket acceptance test is pinned as written: freeze the watermark and
nothing is dropped however old the partitions are. The mutation that makes the
rule time-based reddens exactly that pin and its BOTH-conditions sibling and
leaves every time-only test green, which is the discrimination the ticket asked
for. A day that fails to roll up STOPS the run with the watermark at the last
success; skipping it would advance past a day with no aggregates, the silent
loss the watermark exists to prevent.
Every surviving table is MARGINAL and a test PARSES the CREATE TABLE statements
to prove it: at most one dimension beyond (day, class), never two
visitor-descriptive columns together, never one with path, and no table carries
visitor_key, account_ref or telemetry_id - which is what makes an erasure
complete once the raw rows go. class rides every aggregate and agent gets its
own per-family table (loot#1625); rule_versions reaches every row so rule
boundaries outlive the raw window. No country table, because nothing produces
country.
A THIRD role, metrics_rollup: SELECT on event, which the writer may never have;
write on the rollup tables and the watermark, which the reader may never have;
NO grant on daily_salt; and DDL only through two SECURITY DEFINER doors. The
partition door moves a day rows out of event_default in one transaction with an
explicit UTC bound - p_day::timestamptz would have taken the session time zone
and filed one row under two days depending on who created the partition.
The TypeScript twin of the drop rule PREDICTS what the database will drop and
the summary reports a disagreement loudly with a non-zero exit, because two
implementations of a retention rule parting silently is exactly how a retention
promise stops being true while every timer reads healthy. The bundle was RUN,
not just built: with no credential it starts, reaches main and fails naming the
cause, so the cjs/pg hazard the shipper found is absent here.
The notice moved with it, and a test that asserted the page must NOT claim a
database bound is now inverted: it must claim it, from the shared constant,
and must say the deletion is gated on the totals - a page saying only deleted
after 30 days would describe the time-based expiry loot#1622 rejected. The
account-reference sentences from loot#1758 shortened from as long as the
account to at most 30 days, which is stricter and simpler. Rendered and read.
One header claiming nothing installs a shipper timer yet, a week after one was
installed, is corrected while the rollup was given the same shape.
587 site tests, budget green with the notice raise on the record.
50fe5557 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.