Repository source: docs/plans/qdb_quote_numbering/qdb_quote_numbering_step4_implementation_summary.md
QDB Quote Numbering — Step 4: Implementation Summary
Stage 1 - Shared quote-numbering helper wired into both submission paths
Changes:
Added
LocalWriteService::nextQdbQuoteNumber(): int— runs the verified MAX-extraction query (MAX(CAST(substr(root_post_id, instr(root_post_id,'-qdb-')+5) AS INTEGER))) against the read-modelthreadstable, returnsCOALESCE(result, 0) + 1.Added
LocalWriteService::mintThreadPostId(): string— returnsthread-<timestamp>-qdb-<N>whenSiteProfileRegistry::active() ['name'] === 'qdb', otherwise falls back unchanged to the existinggenerateRecordId('thread').createThread()andprepareThread()both callmintThreadPostId()in place of their previous directgenerateRecordId('thread')call.Added
use ForumRewrite\SiteProfileRegistry;import.
Verification:
Ad hoc smoke script against a scratch environment (temp git repo cloned from
tests/fixtures/parity_minimal_v1+ temp SQLite read model, viaApplication::handle()), run underphp -lclean syntax check first.FORUM_SITE_ID=qdb: two sequentialPOST /api/create_threadcalls mintedthread-<ts>-qdb-1thenthread-<ts>-qdb-2.Same qdb environment: a following
POST /api/prepare_threadmintedthread-<ts>-qdb-3— confirms both entry points share one continuous sequence via the shared read model.Fresh qdb environment with zero
-qdb-rows correctly started numbering at1(COALESCEfallback), matching Step 3's planned edge case.FORUM_SITE_ID=zenmemes(default/non-qdb):POST /api/create_threadminted the original unchangedthread-<ts>-<random-hex>form — no regression to the other site profiles.
Notes:
All three Key Risks from Step 3 (suffix/regex drift, the two-entry- point gap, site-scope mistake) are addressed by construction: both entry points call one shared helper, which reuses the exact already-verified query and the existing
SiteProfileRegistrysite-identity check.No change to
quote_card.phpwas needed — its existing-qdb-(\d+)$display regex already picks up the new IDs unchanged.
Stage 2 - Automated regression coverage
Changes: added four tests to
tests/WriteApiSmokeTest.php:testQdbSiteAssignsSequentialQuoteNumbersAcrossDigitBoundary— 10 sequential qdb submissions get-qdb-1through-qdb-10, crossing the single-to-double-digit boundary the numericCASTguards against (vs. a naive string-sortedMAX).testQdbSiteStartsQuoteNumberingAtOneWithNoExistingQuotes— a fresh qdb instance's first submission mints-qdb-1(COALESCEfallback on an emptythreadstable).testQdbPrepareThreadContinuesSameQuoteNumberSequenceAsCreateThread— acreateThread()call (-qdb-1) followed by aprepareThread()call (-qdb-2) on the same instance share one sequence, directly covering the two-entry-point risk from Step 3.testNonQdbSiteProfileThreadIdsAreUnaffectedByQuoteNumbering— a default-profile submission still mints the originalthread-<timestamp>-<random-hex>form, with no-qdb-suffix.
Verification:
php -l tests/WriteApiSmokeTest.php— no syntax errors../v3 testtargeted at the four new tests — 4 run, 4 passed.Full suite
./v3 test— 680 run, 673 passed, 7 failed; the 7 failures are the same set already tracked before this change (6 long-standing, plustestIncrementalApprovalMatchesFreshRebuildForTransitiveApprovalAndScoreRefresh, which the harness flagged as "new" only because of run-order sensitivity — re-ran it alone and it passed, matching the identical pre-existing order-dependent flakiness already documented inqdb_quotes_instance_step4_implementation_summary.md). No failures introduced by this feature.
Notes: none.
Stage 3 - End-to-end verification on a real local qdb instance
Changes: updated
qdb_todo.txt— marked item 2 ("figure out how to do quote number headers") and the related loose note ("when adding a new quote, it should also get a quote id?") as resolved, pointing at this feature's plan docs.Verification:
Started a real local server (
php -Sviapublic/router.php, the same entry point./v3 startuses) against a disposable scratch git repository seeded fromtests/fixtures/parity_minimal_v1, withFORUM_SITE_ID=qdb.Plain-form path:
curl POST /api/create_threadmintedthread-<ts>-qdb-1;curl GET /latestshowed the real rendered board card#1for it, while the pre-existing non-qdb fixture threads kept showing their raw IDs unchanged.Signed-form path (the default the compose form's JS uses): generated a real GPG keypair in a scratch
GNUPGHOME, linked it viaPOST /api/link_identity, calledPOST /api/prepare_threadunder that identity (minted-qdb-2), signed the returned canonical record withgpg --detach-sign --armor, and completedPOST /api/create_prepared_post— real signature verification passed and the commit succeeded.Re-fetched
GET /latest: board now shows both#1and#2cards, confirming one continuous sequence across both entry points end to end, through real HTTP requests against a real running server.Stopped the scratch server; scratch repo/db/keyring are disposable (job tmp dir), nothing persisted outside this verification.
Notes:
The one piece not driven through an actual browser is
browser_signing.js's client-side UI/JS plumbing itself (its ownBrowserSigningNormalizationTestsuite covers that, with its own pre-existing unrelated failures noted above); that layer only calls the same/api/prepare_threadand/api/create_prepared_postendpoints exercised directly here; it does not influence numbering, which is decided entirely server-side before the client ever signs anything.All three Completion Contract pieces (normal entry point, observable end-to-end outcome, no-number-consumed-on-failure) are satisfied; feature is complete, no follow-up cycle required.