QDB Quote Numbering — Step 4: Implementation Summary

Feature plan: Step 1 · Step 2 · Step 3 · Step 4

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-model threads table, returns COALESCE(result, 0) + 1.

    • Added LocalWriteService::mintThreadPostId(): string — returns thread-<timestamp>-qdb-<N> when SiteProfileRegistry::active() ['name'] === 'qdb', otherwise falls back unchanged to the existing generateRecordId('thread').

    • createThread() and prepareThread() both call mintThreadPostId() in place of their previous direct generateRecordId('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, via Application::handle()), run under php -l clean syntax check first.

    • FORUM_SITE_ID=qdb: two sequential POST /api/create_thread calls minted thread-<ts>-qdb-1 then thread-<ts>-qdb-2.

    • Same qdb environment: a following POST /api/prepare_thread minted thread-<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 at 1 (COALESCE fallback), matching Step 3's planned edge case.

    • FORUM_SITE_ID=zenmemes (default/non-qdb): POST /api/create_thread minted the original unchanged thread-<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 SiteProfileRegistry site-identity check.

    • No change to quote_card.php was 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-1 through -qdb-10, crossing the single-to-double-digit boundary the numeric CAST guards against (vs. a naive string-sorted MAX).

    • testQdbSiteStartsQuoteNumberingAtOneWithNoExistingQuotes — a fresh qdb instance's first submission mints -qdb-1 (COALESCE fallback on an empty threads table).

    • testQdbPrepareThreadContinuesSameQuoteNumberSequenceAsCreateThread — a createThread() call (-qdb-1) followed by a prepareThread() 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 original thread-<timestamp>-<random-hex> form, with no -qdb- suffix.

  • Verification:

    • php -l tests/WriteApiSmokeTest.php — no syntax errors.

    • ./v3 test targeted 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, plus testIncrementalApprovalMatchesFreshRebuildForTransitiveApprovalAndScoreRefresh, 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 in qdb_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 -S via public/router.php, the same entry point ./v3 start uses) against a disposable scratch git repository seeded from tests/fixtures/parity_minimal_v1, with FORUM_SITE_ID=qdb.

    • Plain-form path: curl POST /api/create_thread minted thread-<ts>-qdb-1; curl GET /latest showed the real rendered board card #1 for 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 via POST /api/link_identity, called POST /api/prepare_thread under that identity (minted -qdb-2), signed the returned canonical record with gpg --detach-sign --armor, and completed POST /api/create_prepared_post — real signature verification passed and the commit succeeded.

    • Re-fetched GET /latest: board now shows both #1 and #2 cards, 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 own BrowserSigningNormalizationTest suite covers that, with its own pre-existing unrelated failures noted above); that layer only calls the same /api/prepare_thread and /api/create_prepared_post endpoints 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.