Skip to content

chore(#2680): fifth sync of main into the epic — #3276's build-up, composed with the type-safety stages - #3362

Merged
thatskiff33-agents merged 18 commits into
epic/2680-mepfrom
chore/sync-main-into-epic-2680-e
Sep 12, 2026
Merged

thatskiff33-agents merged 18 commits into
epic/2680-mepfrom
chore/sync-main-into-epic-2680-e

Conversation

@thatskiff33-agents

Copy link
Copy Markdown
Collaborator

Linked Issue

Summary

The fifth and final sync of main into epic/2680-mep, bringing the epic level so its own pull request into main resolves once rather than repeatedly. main's #3276 night-adjustment build-up is the substance of it.

Seven conflicts. Three are code and all three are the same shape — #3276 and the epic's type-safety migration edited the same lines — so each takes main's substance in the epic's shape, because the two are orthogonal.

policies/pricing.tsmain added a targets array and two destructured fields to a counter loop the epic had replaced with a slice prefix. Kept the slice and added main's fields, after checking the loop body reads only the destructured names and never the index.

booking-create-promo.ts — kept the epic's refusal when a guest has no per-night rates, and took main's plain index read for nightDatesByGuest, which is declared Date[][]: the optional chain the epic wrote was guarding a parameter that cannot be absent.

member-merge.ts — kept the epic's typed mergeDelegate accessor. main wrote its new dependents block against an inline Record<string, ...> cast of the transaction, which is the exact shape the type-safety stage removed from six other sites. Taking it verbatim would have left one untyped hole in a file that no longer has any, so that block routes through the same accessor.

The comment-stripper's importer population is 76 test files, two test helpers and one CI script, measured on the composed tree. Neither side had it: the epic said 75 and one helper, main said 74 and two. #3276 added the second helper; the test-file count is what the composition actually holds.

INV-MONEY-029 arrives from main and its index row was 17 words against a 12-word cap that has no register and no exceptions, so the row is compacted the way INV-MOD-028's was. The rule itself is untouched — only the one-line pointer.

booking-modifications.md takes the epic's empty side. main's hunk is the "what reads a re-based strand total" passage still sitting inside its unsplit INV-MOD-028, and the epic already holds that passage exactly once under the ## INV-MOD-038 the fourth sync gave it. Taking main's copy would have duplicated it in the same file.

Three new diagnostics arrived with main's code, and are fixed rather than recorded

promo.ts and work-party.ts landed three noUncheckedIndexedAccess diagnostics into a tree the earlier stages had already cleared. Both now filter the array instead of mapping its indexes, so there is no lookup left to prove. In promo.ts that also removes a non-null assertion, and makes the two parallel vectors parallel by construction — which is the property that code's own comment says must hold.

Decisions, and why

Two word-budget rows moved. INV-MONEY-029 joins the migration ratchet at 667 words. INV-MOD-005 is re-measured from 351 to 381 — and that is a growth, which the register's shrink-only rule forbids. It stands, and the register now says why: the entry is byte-identical to main's, so main grew it where no register was watching and the merge took that side without a conflict. Rewording an owner-decided rule inside a merge resolution is the thing this epic has consistently declined to do. The shrink-only rule still binds every row an epic child touches.

One real size-allowance collision, and one that only looked like one. Two epic children grew public-booking-requests-panel.tsx#3342's currency change and #3350's type-safety fix — so both fragments are live and one entry had to go. #3350's survives as the single home because it can state both causes, and #3342's is replaced by a pointer. That is genuinely different from the eleven stale numbers beside it, which were main's growth of files the epic had also grown, and from the mid-merge illusion the fourth sync taught: a collision naming a fragment already merged to main is an artifact of running the gate on a staged, uncommitted merge and disappears once the base moves.

Pros and cons

The gain is that the epic is level with main and its own pull request has one resolution to do rather than several, with main's newest money work composed against the epic's type-safety work under test. The cost is one more oversize invariant entry carried as declared debt and one recorded growth that the register had to be honest about.

Risk Level

  • Critical
  • High
  • Medium
  • Low
  • Informational/docs only

Medium rather than Low: three of the resolutions are in money-path code, and one of them changes how a promo's per-night rates and dates are filtered.

Changed Areas

  • Booking/capacity
  • Payment/refund/credits
  • Membership/family lifecycle
  • Xero/Stripe/SES/Sentry integration
  • Auth/security/privacy
  • Admin/finance/lodge UI
  • Public UI/UX/accessibility
  • Database schema/migrations
  • Deployment/operations
  • Docs/agent workflow only

Tests Added Or Updated

None added. Every line this branch authors is a conflict resolution or a recorded measurement; the behaviour under test is main's and the epic's, both already covered. The existing suites are the check on the composition — see below.

Validation Commands Run

  • npm run docs:indexcheck — passes, 626 invariant ids.
  • npm run docs:linkcheck — 657 Markdown files, all links and anchors resolve.
  • npm run typecheck — clean across all three projects.
  • npm run typecheck:nuia52, none new and none stale, after fixing the three that arrived.
  • npm run lint — 0 errors, 48 pre-existing warnings.
  • npm run knip — exit 0.
  • npm run quality:budget — OK, 342 production files changed since origin/main, all growth declared.
  • npx vitest related over all 53 changed code paths, in nine chunks.
  • All 302 disk-scanning census suites by name, through the fail-closed runner, in ten chunks.

Every failure in those two test passes was a timeout, and I checked rather than assumed. Seventeen Test timed out in 5000ms across the module-graph run and one in the census run, concentrated in suites that walk the tree from disk while sharing a worker pool with up to 3,500 other tests. Re-run in isolation, the seven nearest my changes — the promo, sold-price, group-discount, notify-choice, batch-modify, club-time and stay-range suites — pass together in 25 seconds, 173 tests; the census chunk passes fully on its own at 30 files and 735 tests. One case looked like a real assertion (expected "vi.fn()" to be called at least once) and is not: it is the test immediately after a 5000 ms timeout in the same file, and the file passes alone.

Commands Not Run And Why

  • Full npm test and the build: PR CI owns both, and this repository has measured that running the full suite locally on Windows red-lights suites that pass in isolation.
  • Real-Postgres harnesses: no resolution alters a query shape, lock key or transaction boundary.

Screenshots Or UI Evidence

N/A.

Security And Privacy Impact

None. No auth, session, token, permission or audit-visibility rule is touched by any resolution.

Data Integrity Impact

The three code resolutions all preserve main's behaviour. The one worth naming is promo.ts: filtering both vectors by the same position set keeps a guest's per-night rates and their dates parallel, which is what stops an adjustment being attributed to the wrong night on a stay the promo window only partly covers. That was already #3276's intent; this makes it structural rather than dependent on two index maps agreeing.

Concurrency And Lock Impact

  • N/A — no transaction, lifecycle, capacity, settlement, credit, webhook,
    cron, or concurrency-sensitive writer changed.
  • Writer class(es), canonical lock key(s), and acquisition order: unchanged by this branch. No lock participant, canonical key, tier or acquisition order is added, moved, re-keyed or renamed in any resolution. The sensitive-path files in the diff carry main's writers and the epic's type-safety edits, neither of which touches a lock site.
  • Immutable pre-lock key source and mutable under-lock re-read: unchanged. No resolution acquires a lock, so there is no pre-lock key source to move and no under-lock re-read to add.
  • Status-guarded claim and proof that a lost claim runs no side effect: unchanged. The one refusal kept in a resolution — a promo evaluated against a guest with no per-night rates — fires during promo validation, before any claim or transaction is opened, so it cannot leave a claimed row behind.
  • Relevant open/last-10 PR numbers, counterpart writers/tests, and compatibility evidence: Stage 2 of #3272: record what each adjustment took off a night #3276 is the writer arriving from main and owns the night-adjustment build-up; its own lock declaration governs that code and nothing here alters it. TYPE-SAFETY 3 (#2800): every noUncheckedIndexedAccess diagnostic in non-test src/lib, 818 to zero #3338, feat(#2799): the noUncheckedIndexedAccess ratchet, and the policy/capacity foundations under it #3310 and feat(#2801): clear noUncheckedIndexedAccess from src/app and src/components, 255 to 52 #3350 are the epic-side type-safety stages that touched the same files, all type-only at the conflict sites. Compatibility evidence is mechanical: the nuia ratchet back at 52 with none new, typecheck across three projects, vitest related over every changed path, and all 302 disk-scanning censuses by name — including main's own new INV-MONEY-029 build-up census, which passes against the resolved promo.ts.
  • Provider calls inside a transaction (write None, or justify the bounded exception from docs/CONCURRENCY_AND_LOCKING.md): None.

Single Source Of Truth

  • New or moved definitions, and the one module each now lives in: nothing new. Two homes were preserved against a merge that would have split them — the typed delegate accessor in member-merge.ts, and the single ## INV-MOD-038 home for the passage main still carries inside its unsplit entry.
  • If anything was copied rather than imported, or a guard was added instead of a structural fix: nothing was copied, and one duplicate was refused — taking main's side of booking-modifications.md would have put the same passage in the file twice. The promo.ts resolution replaces two index maps that had to agree with one filter over a shared position set, which is a structural fix rather than a guard.

Payment Or Accounting Impact

No amount is calculated differently. main's promo build-up arrives unchanged in substance.

Migration Or Deployment Impact

None authored here. main's #3276 migration arrives with it and is main's.

Docs Updated

  • docs/DOMAIN_INVARIANTS.mdINV-MONEY-029's index row compacted to the 12-word cap; the epic's compacted INV-MONEY-028 row kept.
  • docs/invariants/single-source-of-truth.md — importer population re-measured to 76.
  • docs/invariants/WORD_BUDGETS.mdINV-MONEY-029 recorded at 667, INV-MOD-005 re-measured to 381, and the register now states that a row can grow when the growth arrived from main, and why the shrink-only rule still binds this branch.

Changelog Entry

  • Fragment added (changelog.d/<pr-number>-<slug>.md): none authored here. This branch ships no behaviour of its own; the fragments in the diff arrive with main and already describe its changes.

Residual Risks

None.

Stated limits

Not exercised against a real Postgres: no resolution alters a query shape, a lock key or a transaction boundary, so those harnesses have nothing new to arbitrate.

Manual Checks Required

None.

Safety Confirmation

  • I did not use production credentials, production databases, production backups, or live provider endpoints.
  • Merge handling follows the AGENTS.md "Completion and Merge" risk gate: branch maintenance into the epic's own integration branch, eligible for autonomous merge once the required checks pass. The epic's merge into main remains the owner-gated one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CAXBDdR1kBnUf4WQkayDrV

thatskiff33-agents and others added 18 commits September 8, 2026 23:29
…er-night output

Hand-worked literals for every promo type and scoping rule (own-night
assignment, booker-picks-guests, group fixed-nightly, work-party date
window on a non-contiguous stay): booking total, per-guest and per-night
rates, discount, adjustment, allocations and final price. D3 on #3272
says stage 2 changes no figure a member sees; this file is that proof
and is committed first so nothing after it can move a number unnoticed.
…tate (expand only)

BookingGuestNightAdjustment: one row per adjustment per target (night for
percentage/free-night/fixed-nightly, guest for fixed-amount), signed
integer-cent amount with NULL meaning not known, CHECK that exactly one
target is set, provenance by redemption (CASCADE) and code (RESTRICT),
bookingId for one indexed rewrite. BookingGuestNight.adjustmentsState
defaults to UNKNOWN so an omitted-column insert by the draining colour
reads honestly. No stored amount changes. Ledger row, verification
fixture (default, CHECK and cascade pinned with mutants) and the
member-merge relation spec ship with it.
…s through nights

Settling a financial review re-runs the promotion over the strands' stored
nights (rebaseBookingPriceFromStrands), so it is a promotion writer the
plan's list missed; it now records what the engine took off each night,
after the repair's own night writes. The snapshot read for mechanical
rewrites goes through BookingGuestNight (night-scope rows nested) and the
guest-scope rows on their own, so the module never touches a night delegate
it does not write. The batch path narrows to the priced branch before it
reads the engine's guest list.
…order doubles

promo-night-attribution pins what each promo type takes off each night or
guest across the promo-guest-scope rules and a work-party window;
night-adjustment-write proves the INV-MONEY-029 guard both ways and the
snapshot/restore carry; the new census holds the single writer and every
promotion-writer pairing in the order the night rows allow; the stage-1
census and the INV-MOD-028 repair census register the state-only writer
with a control. Existing transaction doubles gain the recorder's
delegates and the promo mock factories gain requiredAdjustmentTargets.
money.md gains INV-MONEY-029 (the reconciliation rule, the deciding grain,
never default an adjustment to zero, unknown stays unknown, credit excluded
with its one home named) and DOMAIN_INVARIANTS.md its row; the changelog
fragment and the size allowances for the nine over-budget files the
recorder call had to land in.
The financial-review settle re-bases the booking through the promotion
engine and now records the build-up, so its transaction doubles gain the
recorder's delegates, and the two 'no night write' assertions narrow to
'no price write' — the adjustmentsState flip is the one in-place night
update that path may make.
…mber migration, refuse-before-mutate helper, merge dependents (in progress)
…ided, bundled discount shape, recorder outside the waitlist degrade path (in progress)
…canner, redemption-write scan, merge dependents test, INV-MONEY-029 rewrite (in progress)
…ws follow their allocation

Owner decision 10 Sep 2026: whether a booking's build-up can be trusted is
derived by summing its rows against the recorded promo totals, never stored;
BookingGuestNight.adjustmentsState and its enum are gone from the schema,
the migration (renumbered 20260913010000 to clear two branches on the old
prefix), the fixture, the writer, every caller, the censuses, INV-MONEY-029
and the changelog. deriveNightAdjustmentState is the one home for the
reader's answer (KNOWN / NOT_KNOWN / NO_PROMOTION).

M4: a member merge that drops a duplicate's colliding allocation deletes
that duplicate's adjustment rows on the same redemption in the same step.
S1: the row's beneficiary is stamped in calculatePromoDiscountForGuestRates,
the branch that decides the allocation. S3: both censuses read one AST
night-writer scanner and stage 2 requires every discovered writer to be
paired or exempt. S4: the discount carries its targets; PromoChangeResult
discriminates on promoEngineRan. S5: the rule is stated once in money.md.
C1: the recorder refuses before it mutates and the waitlist calls it outside
its degrade path. C4: direct redemption writes are confined to promo.ts.
K1/K2/K4/K6: tests for the parked-removal shape, a guest with no night rows,
required nightDatesByGuest, own-night FIXED_AMOUNT and a FREE_NIGHTS tie.
…tested and documented, census names the indirect writer

F1: a colliding merge leaves each surviving allocation matching its rows per
beneficiary while the redemption total still carries the dropped share, so
the booking derives as not known until the next engine run; money.md, the
drop note and the relation spec say so, and the derive tests pin the state.
F2: two waitlist tests — a recorder refusal fails the sweep (no offer, the
invariant logged by name, nothing written) while a pricing failure never
reaches the recorder and offers the stored snapshot; the guide and
booking-modifications.md name the recorder as the one step outside the
degrade block. F3: the censuses name member-merge.ts as the acknowledged
indirect writer (delegate-name literals censused; it writes no amount) and
the docblock, schema and money.md say 'one writer of amounts'. F4: two stale
comments in manual-refund-task.test.ts reworded.
… round (2828 -> 2831)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the measured stripper importers

The night adjustment writer decoded and shifted stay dates through the
retired date-only adapter, which raised the escape-hatch census's
production-importer count to 222 against its 221 ratchet. It now reads the
@db.Date value through calendarDateOfDateOnlyInstant, shifts whole days
with addCalendarDays and re-encodes with dateOnlyInstantOf (INV-CONFIG-002,
INV-DATE-019); the census passes at 221 unchanged.

The shared night-writer scanner is a second test helper importing the
canonical comment stripper, so the INV-SSOT-004 guard names it, its regex
accepts 'two test helpers' exactly, and both publishers now say the
measured 74 test files, two test helpers and one CI script.
…tment-buildup

feat(#3276): record what each promotion took off a night or guest, reconciled to the promo totals (stage 2 of #3272)
Seven conflicts. Three are code, and all three are the same shape: `main`'s #3276
night-adjustment build-up and the epic's type-safety migration edited the same
lines. Each takes main's SUBSTANCE in the epic's SHAPE, because the two are
orthogonal.

`policies/pricing.ts` — main added a `targets` array and two destructured fields
to a counter loop the epic had replaced with a slice prefix. Kept the slice and
added main's fields, after checking the body reads only the destructured names
and never the index.

`booking-create-promo.ts` — kept the epic's refusal when a guest has no per-night
rates, and took main's plain index read for `nightDatesByGuest`, which is
declared `Date[][]`, so the optional chain the epic wrote was guarding a
parameter that cannot be absent.

`member-merge.ts` — kept the epic's typed `mergeDelegate` accessor. main wrote
its new dependents block against an inline `Record<string, ...>` cast of the
transaction, which is the exact shape the type-safety stage removed from six
other sites; taking it verbatim would have left one untyped hole in a file that
no longer has any. That block now routes through the same accessor.

The comment-stripper's importer population is **76 test files, two test helpers
and one CI script**, measured on the composed tree. Neither side had it: the epic
said 75 and one helper, `main` said 74 and two. main's #3276 added the second
helper; the test-file count is what the composition actually holds.

`INV-MONEY-029` arrives from main and its index row was 17 words against a
12-word cap that has no register and no exceptions, so the row is compacted the
way `INV-MOD-028`'s was. The RULE is untouched — only the one-line pointer.

`booking-modifications.md` takes the epic's empty side: main's hunk is the
"what reads a re-based strand total" passage still sitting inside its unsplit
`INV-MOD-028`, and the epic already holds that passage exactly once, under the
`## INV-MOD-038` the fourth sync gave it. Taking main's copy would have
duplicated it in the same file.

Three NEW `noUncheckedIndexedAccess` diagnostics arrived with main's code, in
`promo.ts` and `work-party.ts` — a tree the earlier stages had already cleared.
Fixed rather than recorded: both filter the array instead of mapping its indexes,
so there is no lookup left to prove. In `promo.ts` that also removes a non-null
assertion and makes the two parallel vectors parallel BY CONSTRUCTION, which is
the property its own comment says must hold.

Two word-budget rows: `INV-MONEY-029` joins the ratchet at 667, and `INV-MOD-005`
is re-measured from 351 to 381. The second is a GROWTH, which the shrink-only
rule forbids — and the register now says why it stands: the entry is
byte-identical to main's, so main grew it where no register was watching and the
merge took that side without a conflict. The shrink-only rule still binds every
row an epic child touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAXBDdR1kBnUf4WQkayDrV
… real collision

Eleven recorded lengths no longer matched their files. None of that is this
branch's doing: `main`'s #3276 night-adjustment build-up grew the same booking,
promo, member-merge and pricing modules the type-safety stages had already
grown, so each is longer than either change left it. Re-measured off the merged
tree rather than adjusted.

One genuine one-file-one-allowance collision, and this one is real rather than
the mid-merge illusion the last sync taught me to expect. Two EPIC children grew
`public-booking-requests-panel.tsx`: #3342 rendered its Internet Banking amounts
in the configured currency, and #3350's type-safety stage fixed the money
accessor in it. Both fragments are new since `main`, so both are live.

#3350's entry survives as the single home, because it can state both causes
accurately, and it now does. #3342's entry for that file is removed and replaced
by a pointer saying where the number lives and why — its own reasoning stays
where it was written.

Recording the distinction, because the two look identical in the gate's output
and I resolved the wrong way once already: a collision between two EPIC children
is real and one entry has to go, while a collision that names a fragment already
merged to `main` is an artifact of running the gate on a staged, uncommitted
merge, and disappears the moment the merge is committed and the base moves.

Gate reports OK: 342 production files changed since origin/main, all growth
declared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAXBDdR1kBnUf4WQkayDrV
@thatskiff33-agents
thatskiff33-agents merged commit eda3b77 into epic/2680-mep Sep 12, 2026
16 checks passed
@thatskiff33-agents
thatskiff33-agents deleted the chore/sync-main-into-epic-2680-e branch September 12, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants