Files
Frank 8af40c402b chore(agent): commit the machine tier and the doctrine text the checkout was already running
Landing blocker for #61 and #63: the deployment checkout is 12 commits behind and
`merge --ff-only` refuses while tracked paths are dirty. Discard is not available here
(workspace policy: no `checkout`/`stash` on shared state), so the dirt is committed.

- `agent/APPEND_SYSTEM.md`: the point-of-use / one-work-log / PR-link doctrine, already
  byte-identical to `origin/main` (`cmp` against the trunk blob: no difference). No new
  prose — the commit exists so the trunk merge can proceed.
- `agent/config.yml` and `agent/managed-skills/**`: the machine-live tier the heartbeat
  owns (AGENTS.md § Live-sync tiers).

`ALLOW_SECRETS=1` was used, deliberately and after auditing the staged diff by hand: the
only credential-shaped strings in it are two copies of `AKIA0000000000000EXAMPLE` — the
inert all-zero AWS example id — and they appear inside the two skill pages that teach
that exact placeholder shape. This checkout is still running the pre-#61 hook; the merged
gate (value-scoped placeholder filter, `71601f7`) exempts this value, so the refusal
dies with this landing. No `--no-verify` was used: the gate ran and its verdict is
recorded here.

Refs #61
2026-10-21 01:44:51 -07:00

38 lines
2.5 KiB
Markdown

---
name: pdf-lib-determinism-moddate-leak
description: "Diagnose pdf-lib output that isn't byte-identical across fetches: PDFDocument.load restamps /ModDate; fake-clock determinism test; inflate object streams to diff."
---
# pdf-lib byte-identical output: the /ModDate clock leak
## Symptom
A "reconstruct-on-read, byte-identical forever" PDF endpoint passes its back-to-back refetch test but returns **different bytes when fetches cross a wall-clock second**. Same-second fetches match; a fetch 1s later differs (often a different stream `/Length`, e.g. object stream 411→420 bytes).
## Root cause
`PDFDocument.load()` has its own `updateMetadata` option (**defaults true**) and stamps `/Info /ModDate` from the wall clock **at load time**, not just at save. `doc.save({updateMetadata:false})` on its own is not enough (and in pdf-lib 1.17 `updateMetadata` isn't even in the `SaveOptions` type — only in `PDFOptions` on `load`); passing it to `save` also fails typecheck.
## Fix
```ts
const doc = await PDFDocument.load(templateBytes, { updateMetadata: false });
// ... fill, stamp print layer ...
return doc.save(); // save never stamps when load didn't
```
Output becomes a pure function of (template bytes, values).
## Determinism test law
A determinism test MUST straddle a time boundary. Back-to-back fetches/fills are structurally blind to per-second stamping.
- Unit: fake the clock (`@sinonjs/fake-timers` or `vi.useFakeTimers`) → fill → jump +2 months → re-fill → `expect(Buffer.from(second)).toEqual(first)`. No sleeps, no flake window. Faking `Date` outside a test framework requires patching `globalThis.Date` BEFORE the module import that uses it.
- Prove it red first against the old code (expect the byte/length drift).
## Byte-diff forensics (when lengths differ)
1. Dump the differing region raw: `Buffer.subarray(idx-60, idx+60)` rendered latin1 with non-printables stripped — locate the exact field (`D:YYYYMMDDHHmmss` = clock leak).
2. If the diff is inside a compressed object stream, inflate and compare streams:
```ts
import { inflateSync } from "node:zlib";
// find /Type /ObjStm ... stream chunks, inflateSync each, string-compare
```
3. pdf-lib escapes text-show operands — inflate + unescape `\\(\\[()\\\\]\\)` before substring-asserting printed values.
## Live proof recipe
Seed a submission, then fetch both retrieval doors at t, t+1.5s, t+3s; `sha256sum` all three → one hash. Same-second comparisons prove nothing. And kill any long-running dev service holding the app port first — a live :3000-style stack can hang or false-fail an e2e gate.