Skip to content

feat(data): standardize business record chronology - #820

Merged
mforce merged 3 commits into
mainfrom
chore/agents-819
Sep 14, 2026
Merged

mforce merged 3 commits into
mainfrom
chore/agents-819

Conversation

@mforce

@mforce mforce commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

What changed

Fixes #819.

This replaces random-UUID tie-breaking with one business-record chronology
policy:

  • Business dates remain DateOnly.
  • Every business record implements ICreatedRecord; mutable records also
    implement IMutableRecord.
  • PostgreSQL triggers stamp CreatedAtUtc and UpdatedAtUtc for every write
    path. Mutable inserts receive the same value for both fields.
  • 11 paged chronological tables have a unique shadow identity Sequence.
  • Dated lists order by business date, creation time, and sequence. Existing
    direction is preserved, including the payments settlement history's
    intentional oldest-first order.
  • The movement ledger without a business date orders by creation time and
    sequence. Matching exports use the same complete tuple.
  • Model discovery fails when a new mapped business type lacks an explicit
    timestamp policy.

The change uses composable interfaces rather than base classes because the
entities already have domain and Identity inheritance.

Migration behavior

AddBusinessRecordChronology:

  • preserves existing creation timestamps;
  • backfills exact creation/update times from account-scoped, action-specific
    audit events where available, including User.RoleChanged;
  • uses the explicit 1970-01-01T00:00:00Z unknown-time sentinel otherwise;
  • assigns deterministic legacy sequences, then enables generated identities;
  • installs timestamp triggers for every discovered business record; and
  • uses a five-second lock-acquisition timeout.

The migration runs atomically through the pre-deploy job. The lock timeout
limits acquisition only; production-volume duration must still be measured.
Legacy sequences are deterministic but cannot recover historical chronology.

Generic timestamps and Sequence remain persistence-only. Existing API and
CSV contracts, FIFO allocation, and FOR UPDATE lock ordering are unchanged.

Independent review and remediation

Claude Opus, Fable, and Sonnet independently reviewed the change. Their two
blocking findings are fixed:

  • restored the payments list's documented oldest-first direction; and
  • added a mutation-proven regression test that fails if Sequence is removed
    from the ordering tuple.

The same pass replaced recalled type lists with interface-driven model
discovery, centralized chronology ordering, added AccountId to audit
backfills, included User.RoleChanged, split migration SQL statements, and
removed duplicated Identity timestamp snapshot bookkeeping.

Two limits are deliberate and documented. A database clock rollback can
defeat the CreatedAtUtc middle key. Offset pagination is not a snapshot when
concurrent inserts occur. Sequence provides the unique final key.

Load and upgrade rehearsal

The first local measurements were discarded because another test contended
for the host. The clean comparison used k6 v2.1.0, the same generated 90-day
fixture, and three fresh-database reps per commit. Each rep used 2 warm-up VUs
for 20 seconds, then all 10 cast users for 2 minutes.

Median throughput changed from 10.930 to 10.947 requests/s (+0.15%). Median
p50 changed from 4.03 to 4.20 ms (+4.2%), and p95 changed from 11.13 to 11.36
ms (+2.0%). Median p99 changed from 65.08 to 73.96 ms (+13.6%). App memory
changed from 230 to 249 MiB. All six reps had 100% checks, zero unexpected
statuses, and complete persona/user coverage. This uncapped local run shows no
throughput or p50/p95 regression outside host noise; it is not a production
sizing result. Detailed k6 evidence.

The upgrade rehearsal seeded main's full 90 days of activity, then applied
the PR migration in place. The migration changed no table row counts and took
about 0.45 seconds, or 3.23 seconds with container startup. The old binary
remained ready after the migration. The upgraded database had zero invalid
constraints or indexes, and every timestamp backfill, sequence ranking,
trigger, and generator check passed. The PR binary then returned readiness
200 and passed all 243 five-persona smoke checks.

Review order

  1. docs/decisions/819-business-record-chronology.md
  2. BusinessRecordModel.cs, ICreatedRecord.cs, and IMutableRecord.cs
  3. 20260913212515_AddBusinessRecordChronology.cs
  4. BusinessRecordOrdering.cs and repository/export consumers
  5. BusinessRecord*Tests.cs and ListChronologyTests.cs

Generated migration metadata and docs/schema/ account for most of the diff.

Verification

  • Build: passed with zero warnings.
  • Tests: 491 domain, 290 application, 1,809 integration, and 10 AppHost tests.
  • Final focused PostgreSQL chronology/export/Identity tests: 45 passed.
  • Sequence-removal mutation: failed at the expected ordering assertion.
  • Schema docs check: passed against PostgreSQL 18.
  • EF pending-model check: no changes.
  • All required CI checks pass, including Playwright, CodeQL, dependency review,
    GitGuardian, and the image/Trivy scan.
  • git diff --check: passed.

Review status

The PR has no GitHub approval yet. CodeRabbit skipped the review because the
diff has 105 files, above its 100-file limit. The repository-required
before/after screenshot of the same-day ordering change is not attached yet.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d87a3cb7-8963-4a96-a492-4376d88bb8d1


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce mforce changed the title fix(lists): order same-day rows by insertion sequence feat(data): standardize business record chronology Sep 13, 2026
@mforce

mforce commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 105 files exceed the limit of 100.

@mforce

mforce commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

Final load and migration rehearsal

I discarded the first measurements because another local test was contending
for the host. These are clean reruns with no other test/build job active.

k6: main vs PR

Same generated 90-day fixture and credentials; k6 v2.1.0; 3 fresh-database
reps per commit; each rep used 2 warm-up VUs for 20s, then all 10 cast users
for 2 minutes.

Capacity median main f769681 PR eabae19 Delta
requests/s 10.930 10.947 +0.15%
p50 4.03 ms 4.20 ms +4.2%
p95 11.13 ms 11.36 ms +2.0%
p99 65.08 ms 73.96 ms +13.6%
iterations 643 637 -0.9%
app CPU median 10.3% 10.2% -0.1 pp
app memory median 230 MiB 249 MiB +19 MiB

All 6 reps passed: 100% checks, zero unexpected statuses, all five personas
and all ten distinct users covered. The uncapped co-located harness is a
relative shakeout, not a production sizing result. The throughput and p50/p95
changes are within the observed local spread; there is no demonstrated
capacity regression. PR database size starts about 0.5 MiB higher from the
new columns/indexes; per-run growth remained 0.3-0.4 MiB on both commits.

In-place upgrade rehearsal

I then created main's 16-migration schema, seeded its full 90 days of
activity, and applied the PR migration to that same database.

  • Exactly one migration applied; DDL took about 0.45s (3.23s including the
    one-shot container startup).
  • Exact row counts for every public table were unchanged.
  • The old serving binary remained ready (/health/ready 200) after migration.
  • 17 migrations present; zero invalid constraints; zero invalid/unready
    indexes.
  • Exactly 26 CreatedAtUtc columns/triggers, 21 UpdatedAtUtc columns, and
    12 GENERATED ALWAYS sequence identities.
  • Every audit-derived creation/update backfill matched its account-scoped
    expected audit event across the seeded data.
  • Every one of the 11 legacy sequence backfills matched its specified ranking;
    no nulls, duplicates, or lagging generators.
  • A real trigger update advanced UpdatedAtUtc (then rolled back).
  • The PR binary started on the upgraded database and passed the five-persona
    smoke: readiness 200, 243/243 checks, zero unexpected statuses.

@mforce

mforce commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Before and after for the user-visible same-day ordering change. Both captures use a freshly built image at 1280x800 and the exact same upgraded 90-day simulation database. Main orders the five 09/13/2026 sales by random GUID; the PR orders them by creation chronology with Sequence as the final tie-breaker.

Before on main: five same-day sales orders appear in arbitrary GUID order

After on PR 820: the same five sales orders appear newest-created first

@mforce
mforce merged commit 6231b31 into main Sep 14, 2026
17 of 19 checks passed
@mforce
mforce deleted the chore/agents-819 branch September 14, 2026 03:01
mforce added a commit that referenced this pull request Sep 14, 2026
## What this is

Measurement for #839, plus one small scheduling change. **Read the
numbers before the title's verb** — this does not make the suite faster.
It stops the suite sometimes being much slower than it needs to be.

`docs/plans/839-integration-wall-clock/measurement.md` is the
deliverable. The code is two files.

## The measurement

Local, Ryzen 5 6600H / 12 logical CPUs / 18.8 GiB, cached images, Docker
29.8.0, .NET SDK 10.0.112.

| Run | Config | Wall | Tests |
| --- | --- | ---: | --- |
| `main` (`18b45dc`) | Debug | 320s | 1,801 passed |
| this branch | Debug | 326s | 1,815 passed |
| this branch | Release | 341s | 1,815 passed |

Full-suite counts differ because the branch is rebased past #820 and
#844.

**What the ~5.5 minutes is made of** (instrumented run, 331.5s wall):

- **Container readiness: 156.6 distinct wall-clock seconds**,
overlapping test execution. 119 Postgres + 7 Redis + 1 Ryuk.
- **The shared `integration` collection: 1,024 of 1,815 tests**, one
fixture, running serially end to end.
- **Three one-shot-process classes dominate**: `SeedCommandTests`
131.14s, `ProcessRoleGuardTests` 127.26s,
`OneShotVerbMinimalConfigTests` 119.05s. Repeated app process startup,
not SQL.

## Two premises in the issue did not survive

1. **Container reuse is already done.** Lever 1 assumed per-class
startup was the dominant cost. The shared collection already holds one
`ICollectionFixture<CluckworkWebApplicationFactory>` covering 1,024
tests. The remaining 119 Postgres containers are the specialized
factories that genuinely need their own database, migration state, or
advisory-lock behaviour.
2. **The remaining cost is subprocess, not SQL.** The issue lists
container reuse and parallelism. The measured cost is repeated one-shot
process startup in three classes, which the issue never names.

## The change

`IntegrationCollectionOrderer` puts the shared collection first and
delegates every other collection to xUnit's default orderer. Fixture
ownership, concurrency limits, serialization, and the race assertions
are untouched — `StealLossConnectionReleaseTests` keeps its dedicated
factory, one-slot pool, and timing assertions.

Why it is **variance, not speedup**: xUnit 2.9.3 documents its default
collection order as unstable between runs. When the 95-class serialized
collection draws a late slot, its ~300s of serial work becomes a tail
nothing else can overlap. Pinning it first removes that schedule. It
makes no test cheaper.

The first draft of this PR claimed 25.8% (434.51s → 322.46s). That
compared against a single slow run. A second run of the **unmodified**
scheduler came in at 327.06s, and the paired runs above put both
versions inside each other's spread. The correction is in the
measurement doc, not quietly dropped.

## Also in here

- `tools/test-timing/` — `measure.py` captures wall clock, TRX, and
Docker events; `summarize.py` reports per-class cost, container
readiness, and fixture phases. Opt-in timing in the base factory behind
`CLUCKWORK_TEST_TIMING=1`.
- Two guard collisions found while building the tooling, both fixed in
the tooling, **neither guard weakened** — worth knowing because they
will bite the next person who adds a file under `tools/`:
- the `#508` tracked-file image-pin guard read a Python dict key as a
live image reference (fixed by naming the variable `reference`);
- the same guard's bare-`postgres:` literal detector fired on a timing
phase *label* (fixed by naming the phase `container`).

## Verification

- Solution build: 0 warnings, 0 errors.
- Integration suite on this branch: **1,815/1,815 passed** in Debug and
Release.
- Domain 491/491, Application 290/290.
- `tools/test-timing/measure.py` driven end to end on the branch as it
now stands: exit 0, 1,815 passed, summary regenerated.
- CI is **not** measured. Local results do not establish a CI
improvement or an optimal worker count on hosted runners.

## Not done

No test deletion, no SQLite, no higher parallelism, no product change.
Remaining levers, in the order the data now suggests: the three
subprocess classes; splitting the shared collection after auditing its
shared-state assumptions; CI-side measurement.

Closes #839
mforce pushed a commit that referenced this pull request Sep 16, 2026
🤖 I have created a release *beep* *boop*
---


## [0.1.2](v0.1.1...v0.1.2)
(2026-09-16)


### Features

* **data:** standardize business record chronology
([#820](#820))
([6231b31](6231b31))
* **infra:** optional leader-lease endpoint for pooled deploys
([#869](#869))
([e9bc6a7](e9bc6a7))
* **sim:** seed a second farm for the README dashboard capture
([#867](#867))
([de407c6](de407c6))
* **web:** adopt MUI, themed from the farm palette tokens
([#674](#674))
([#860](#860))
([6c83c5c](6c83c5c))
* **web:** convert Daily entry to MUI, field-first on the phone
([#888](#888))
([b66f8b8](b66f8b8))
* **web:** convert the Dashboard and app shell to MUI
([#829](#829))
([#883](#883))
([2e94277](2e94277))
* **web:** retire the Slack-blue link colour for ink + a rule underline
([#884](#884))
([c08f9d8](c08f9d8))
* **web:** serve a per-request CSP nonce so Emotion's styles apply under
style-src 'self'
([#874](#874))
([ba4e6f3](ba4e6f3))
* **web:** visual language theme overrides for the MUI revamp
([#864](#864))
([#882](#882))
([0bb6b73](0bb6b73))
* **web:** whole-app MUI baseline, theme policy guard and the
[#740](#740) phone action rule
([#823](#823))
([#871](#871))
([af565e4](af565e4))


### Bug fixes

* **auth:** fail closed on unresolved flock-scope actors
([#787](#787))
([#868](#868))
([16d0350](16d0350))
* **auth:** make farm configuration owner-only
([#870](#870))
([42f9036](42f9036))
* **e2e:** repoint the canary at the markup two PRs replaced
([#844](#844))
([18b45dc](18b45dc))
* **i18n:** tl glossary uses the standard passive of ilagay
([#813](#813))
([20dec10](20dec10)),
closes [#738](#738)
* **sim:** stop the k6-baseline EXIT trap masking a clean run as failed
([#838](#838))
([f5ec96f](f5ec96f))
* **web:** declare the rule tokens the Dashboard reads, and guard
undeclared custom properties
([#885](#885))
([5bead1f](5bead1f))


### Performance

* **ci:** start the serialized integration collection first
([#861](#861))
([1dcc7f6](1dcc7f6)),
closes [#839](#839)


### Documentation

* **auth:** record the OAuth 2.1 decision for MCP authentication
([#801](#801))
([0510854](0510854))
* **designs:** MUI revamp design doc, component map, layout system, IA
([#862](#862))
([da49481](da49481))
* **readme:** recapture the daily entry, reports and sales screenshots
([#865](#865))
([f18e336](f18e336))
* **specs:** correct the sales_order_items column list in §10.5
([#812](#812))
([afe4a02](afe4a02)),
closes [#737](#737)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
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.

Standardize business-record timestamps and chronological list ordering

1 participant