Repository navigation
fix(spec,rest): the record page's timeline count counts one create or delete written to both sources once - #30405
Conversation
…(red: 3 over 2 rows) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q
…ources once Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q
…s the paired count; changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q
…ine count Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q
…implement Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q
📓 Docs Drift Check4 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8fcb7b626a86bcae52eb608f58613de3e0a3e48d && git checkout 8fcb7b626a86bcae52eb608f58613de3e0a3e48d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 821fd53816601d9cbc4b679f3e7d3b137a7efd94 && git checkout -B drift-repro 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 && git merge --no-ff 821fd53816601d9cbc4b679f3e7d3b137a7efd94
node scripts/docs-audit/affected-docs.mjs --json 0e4dfbe27a2a312864cd8d14e4954b6af14e2a22 |
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb Contract reviewServed-tier: Rendered 2026-11-05T09:55Z. Inputs: card #30343 body + 3 comments (6652104225, 6652912193, 6654843728), PR #30405 body, 9-file list, net diff ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgment
Should-fix (follow-up card, No must-fix. Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb 席位核对 · ACCEPT · 2026-11-05T09:56Z · head
|
Requested by Bill · project thread
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
维护者速读
packages/rest/src/record-page-routes.ts的answerTimeline(ofAction与paired);packages/spec/src/api/record-page.zod.ts的RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES与计数表;packages/rest/src/record-page-routes.test.ts的[#30343]块。pnpm --filter @objectstack/rest exec vitest run src/record-page-routes.test.ts -t 30343,预期 5 条全绿;其中「一建一改 → 2」在 main 上得 3。Fixes #30343
Clause-②: yes (narrowing) —
RecordPageTimelineCountSchema.totalfalls for the same rows: with a field change counted,created(deleted) activity rows now step aside as often as the ledger holdscreate(delete) writes, beside theupdatedrows that already did; widening in the same line:@objectstack/spec/apiexports one more constant,RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES. No exported member is removed or renamed (RECORD_TIMELINE_UPDATE_ACTIVITY_TYPEkeeps its name and role).Metadata-protocol: none — a read answer's count, not anything an author writes.
Skills-docs: none —
git grep -nE 'RECORD_TIMELINE|fieldChanges|timeline.*(total|count)|step aside' -- skills/has 0 hits.What changes
The ruling. Option A as the seat's PM discretion, sharpened after a measurement. PR #30342's client fold (
ui/packages/plugin-detail/src/recordTimelineStream.ts,pairActionsandmergeTimelineStream) is a per-row pairing, not a type rule: an auditcreate/update/deleteentry pairs with acreated/updated/deletedactivity row of the same actor stamped withinTIMELINE_PAIR_WINDOW_MS(5 s), nearest first, each once. On top, with any field change, an unpairedupdatedrow still steps aside; an unpairedcreated/deletedrow stays. A count-only server cannot pair rows. So the seat ruled (seat thread, 2026-11-05T08:04Z) the closest countable rule: with a field change counted, step aside everyupdatedrow, plusmin(created rows, create writes)andmin(deleted rows, delete writes). This is exact whenever plugin-audit writes both rows in one call (the common case). Option A taken literally (everycreated/deletedrow steps aside) would under-count a record created before its history was kept: the client drawscreated+ the edit (2), A would count 1.REST.
answerTimelinereads four more counts when bothactivityandfieldChangesare named:sys_activitybytypecreatedanddeleted,sys_audit_logbyactioncreateanddelete, each as the caller throughreadStream, in the samePromise.all. A refused ledger count pairs nothing (as a refused ledger total already counts no field change); a refused activity count refuses the part, as before.Spec. The table row, before:
after:
The new constant
RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES = { create: 'created', update: RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE, delete: 'deleted' }sits beside the update one, whose docblock now says every update steps aside and a create or delete only where paired. The "upper bound" sentences (module docblock, table text,RecordPageTimelineCountSchemadocblock andtotal'sdescribe()) keep theshowCompleted/types/ no-diff cases and gain the one case that falls the other way: a create (delete) whose two rows the panel does not pair.fieldChanges'sdescribe()names the created/deleted step-aside.content/docs/references/api/record-page.mdx,api-surface/api.jsonandexport-origins/api.jsonare the generators' output.Tests
packages/rest/src/record-page-routes.test.ts[#30343]block, row-backed (each count filtered from rows by its ownwhere): one create + one edit, audit reader → 2 (red on main: 3); same rows, ledger refused (member without audit read) → 2, unchanged; created before history (nocreatewrite) + one edit → 2; create + edit + delete → 3; a comment still adds one → 3. The[#28737]read-set pin now lists the 8 reads and its sum (262) carries the paired create.packages/spec/src/api/record-page.test.ts: the new map pairs the three write actions with drawn built-in types,updatebeingRECORD_TIMELINE_UPDATE_ACTIVITY_TYPE.packages/qa/dogfood/test/record-page-timeline.dogfood.test.ts: the expected sum from separate list reads adds the paired step-aside; on the real producers (one create, two renames)paired= 1.paired += Math.min(…)replaced bypaired += 0throughscripts/ablation-replace.mjs→ 4 failed / 33 passed inrecord-page-routes.test.ts(the 262 pin, create+edit → 2, create+edit+delete → 3, comment → 3); restore verified blob == HEAD.Acceptance notes
packages/qa/dogfood/test/record-page-timeline.dogfood.test.tsis the route's end-to-end pin of this total (its expected formula is the spec table's), so it moves with the formula (os-dev: a stale fixture made red by this change is fixed in the same PR). Noui/**file changed: the one ui test that names the constant (RecordTimelineTotal-28737.test.tsx) pins the client fold, not the server total.check:liveness(and socheck:generated's liveness entry andcheck-liveness.test.ts) was red onorigin/mainfdad178 (query/searchFields → packages/objectql/src/engine.ts, after refactor(objectql): move 36 hand-series private docblocks out of engine.ts with the codemod's third pass (slice 1 of 2) #30341). fix(spec): repoint the query.searchFields liveness row at engine-find.ts#expandSearchOnAst #30368 repointed it; after mergingorigin/main(8fad7b1) all three are green here. Not this PR's.Verification
Head
821fd5381(merge oforigin/main8fad7b1; regenerating spec build +gen:api-surface+gen:export-origins+gen:docs+gen:liveness-countson it wrote no diff).Derived gates: 104 derived, 104 run, 0 NOT-MEASURED
node scripts/pm/dispatch-gates.mjs --ranondc2c02ab8:✓ 104 derived famil(ies) accounted for — 104 run, 0 NOT-MEASURED. The loop ran on1c674657; the two commits after it (the changeset'sadr-0087:marker, the test double refusing combinators) were answered by re-running the gates they touch. 102 exit 0 on the pre-merge tree; the 2 red (check:liveness,check:generated) were the main red above. Re-run on the merged head821fd5381, all exit 0:check:liveness,check:generated,check:api-surface,check:export-origins,check:docs,check-adr-0087-registration --base origin/main,check:where-matcher,check-changeset-no-major.check:skill-examples,check:dual-build-cjs-loadsandcheck-plugin-teardown-shape --self-testexit 0 after a whole-workspace build and the pinned-commit fetch. Lists: project filesserver-seat5/gates-30343-*.txt.Suites, lint, dogfood
pnpm --filter @objectstack/rest test: 386 files, 7335 passed, 332 skipped.record-page-routes.test.tson the merged head: 37/37.pnpm --filter @objectstack/rest typecheck,pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/spec test: 25553 passed, 24 failed, all 24 inscripts/liveness/check-liveness.test.ts(the main red above). On the merged head,record-page.test.ts+check-liveness.test.ts: 85/85.pnpm lint: exit 0.pnpm --filter @objectstack/dogfood testunder the lock afterturbo run build --filter='!@objectstack/docs', ondc2c02ab8:VERDICT command-exit 0, Test Files 419 passed / 3 skipped, Tests 3492 passed / 36 skipped, 0 failed.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CYeVL11pNodMuPhy8q