fix(service-queue): compare the publish idempotency window as instants, not strings - #14200
Conversation
…s, not strings (#13993) The idempotency check deduped terminal rows with a lexicographic String(row.created_at) compare against ISO text. On Postgres/MySQL the builtin audit column materialises as a JS Date whose String() starts with a weekday letter, unconditionally above the ISO window-start's digit, so the predicate was always true: terminal rows blocked re-publish forever and publish() silently enqueued nothing. Normalise created_at to an instant (the canonicalVersionInstant shape) and compare epoch ms; the pending/running arm and SQLite verdicts are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
…register its pins The new double's update()/delete() now open with the engine's own dispatch predicates, find() bounds by presence and refuses combinators, and the engine-double-contract RETAINED ledger records the new (file, verb) pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 3 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 54f7c949d28fb7d78732371f3c5c2bf933d389b0 && git checkout 54f7c949d28fb7d78732371f3c5c2bf933d389b0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33 51b3804a0b304c7e15adbc39f31f719065885d0c && git checkout -B drift-repro 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33 && git merge --no-ff 51b3804a0b304c7e15adbc39f31f719065885d0c
node scripts/docs-audit/affected-docs.mjs --json 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33 |
PM 复核 — 实现 ACCEPT;⛔ Clause-② 由
|
⏸ 落地暂停 —— 卡在契约复审档位不可用,⛔ 不是卡在这个 PR
⛔ 本席不会降档代跑。 低于档位的复审无效 —— 绕过熔断等于给一次从未发生的审查盖章。宁可明说被挡住。 这个 PR 自身的状态(⛔ 与上面无关)
|
契约复审:PASS —— 条款②经审定不适用(conformance 类);head
|
Fixes #13993
What
DbQueueAdapter#publishdeduped terminal (completed/dlq) rows with a lexicographic compare —String(row.created_at) >= windowStart— of the raw driver value against canonical ISO text. On Postgres/MySQL the builtin audit columncreated_atcomes out of the record read door as a JSDate(pinned indriver-sql'ssql-driver-13567-audit-stamp-materialisation.test.ts), whoseString()begins with a weekday letter (0x41–0x5A), unconditionally above the ISO window-start's leading digit'2'(0x32). The predicate was therefore always true: any terminal row with the same idempotency key blocked re-publish forever, andpublish()returned the old id having enqueued nothing — silent message loss on the production default drivers. SQLite hands ISO-Z text to both sides, so the check was correct there and every existing test stayed green.Shape A per the triage ruling (comment 5486943842): normalise at the consumer and compare INSTANTS — the #13382
canonicalVersionInstantshape.createdAtInstantMsreads the measured materialisation domain (JSDateon PG/MySQL/Mongo; canonical ISO text on the SQLite family and memory; epoch-ms number on legacy columns) and the predicate compares epoch milliseconds. Acreated_atthat denotes no instant cannot be inside a window measured on thecreated_ataxis and does not block (decision documented on the helper). Thepending/runningarm — which blocks regardless of age — is untouched. No??tolerance fallback (#13973 standing prohibition); shape B (read-door normalisation of builtin audit columns) not taken — that is a maintainer floor.Out of scope, deliberately: the #13973 census and #13994 (the packages/rest sibling) are not addressed here; #13973 remains open.
Files
packages/services/service-queue/src/db-queue-adapter.ts— the instant compare +createdAtInstantMspackages/services/service-queue/src/db-queue-adapter-13993-idempotency-window-materialisation.test.ts— the pin: 8 directional tests (out-of-window Date terminal row no longer blocks; in-window Date terminal row still blocks; ISO controls both directions; epoch-ms both directions; pending/running block regardless of age on both materialisations; non-instant does not block)scripts/engine-double-contract.pinned.json— the double gate's RETAINED ledger learning the new fake's update/delete pins (the gate's own--writeremedy).changeset/queue-idempotency-window-instant-compare.md— patch for@objectstack/service-queueVerification
pnpm --filter @objectstack/service-queue test: 4 files, 64/64 green at51b3804a0(verify-lock VERDICT: command-exit 0).tsc --noEmitexit 0;--listFilesshows the new test file inside the tsc program (1 hit) — measured, not excluded.node scripts/pm/dispatch-gates.mjs, change set taken by the script itself, re-derived after the ledger file joined the diff, which added 7 families): 41 families exit 0 at51b3804a0;scripts/check-test-completeness.mjsexits 3 = PREREQUISITE NOT MET (CI-only shard summaries) = NOT MEASURED — neither pass nor red; CI owns that run.85f0e450toaceb87be; injected-anchor grep count 0 to 1, deleted-call anchor 1 to 0); restore proven BY STATE (git hash-objectequals the HEAD blob85f0e450,git diff HEADempty), with an absolute-path trap as crash-path convenience only. The test subject resolves through a same-package relative src import — no dist in the resolution path — so neither leg needed a rebuild.Clause-② declaration
Clause-②: no— path limb: nothing underpackages/spec/src/**. Content limb, argued in the claim comment (5492835648): the PG/MySQL behaviour shift is the restoration of the already-declared window semantics (QueuePublishOptions.idempotencyKeydeclares suppression "within the dedup window"; the adapter option doc declares "how long the same key blocks re-publish"; the ADR-0057 retention comments measure retention against that same bounded window) — not a movement of the contract. No published surface widens, and SQLite verdicts are byte-identical.Session: session_016ZC5rNQj3WEet5HAmmAkMs
Generated by Claude Code
Generated by Claude Code