13.x → 14.0.0, entry by entry
Eight breaking entries. Four are compile errors the moment you upgrade (3, 4, 5, 8). Two are security fixes that change behaviour with nothing failing to compile (1, 2). One changes the text of a validation issue path (6), and one is a driver interface (7).
No app.config.ts key moves. No codemod: each entry names its own manual edit.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | a frame verb acts on the frame, not the parent document | never — a fix. frame(…).fill() used to clear the parent's same-id field and append to the frame's |
| 2 | a session key hashes each segment | never an edit — but every stored session logs in once more, see below |
| 3 | $migration(), toSql, invariantsToSql, constraintName removed | you called one — nothing in this repo did, and $migration() rendered ALTER TABLE "<entity name>", a relation that does not exist |
| 4 | t.number.int() demands a safe integer | you relied on 2^53 passing a boundary that the row write then refused |
| 5 | and() / or() refuse an empty clause list | you build a policy from a list that can filter to empty — and() used to answer allowed |
| 6 | a t.record issue path names the entry by position | you parse issue paths and expect the caller's key. The key is caller data and reached the log line |
| 7 | ScrapeTarget.setOfflineMode required; CdpTargetInit.ringCapacity deleted | you implement ScrapeTarget yourself |
| 8 | Repo.insert/insertAll/upsertAll take RowWrite<T> | you implement Repo — otherwise this accepts more than before |
2. Every stored session key changes spelling
Segments used to be sanitised by collapsing every run of non-[a-zA-Z0-9._-] to a single -, so
alice@corp.com and alice-corp.com produced one key. The browser then loaded account A's
cookies, auth.validate() answered true — the session is valid, for the wrong account — and
A's rows were stored under B's tenant.
Nothing to run. A stored record is not found under the new spelling, a miss reads as "no session", so the run logs in again and writes the new key: one extra login per stored session, no error, and the old objects are orphaned until your bucket's lifecycle rule collects them.
5. and() with no clauses used to admit anonymous callers
// before — answered ALLOWED, and admitsAnonymous() agreed, so http did not 401 first
policy: and(...requiredCaps.map(can)) // requiredCaps filtered to empty
// after — refused where it is written
policy: requiredCaps.length === 0 ? allow('public') : and(...requiredCaps.map(can))allow('public') is the explicit spelling for "no clauses required"; deny('<reason>') is the one
that carries a reason. Both are refused because a combinator with no clauses states nothing, and
and() stated the opposite of what its author meant.
8. Repo's whole-row writes take RowWrite<T>
They took the row type where money's write type belongs, so Repo.insert demanded a
MoneyValue while narrowMoney exists precisely to narrow a bigint handed to a driver — a
compile error on public API for a value the framework documents, implements and stores correctly.
RowWrite<T> accepts both spellings. If you implement Repo, widen those three signatures; if you
only call them, this accepts strictly more than before and costs you nothing.