test2123-hub-postgres-ladder — local Docker runs (2026-09-30)
image: oven/bun:1.3.14 + Debian postgresql 17.11 + pg@8.16.3, all inside one container
source_commit: f57e17b38b49ec937916c4e0f74a8ff18d62a0c0 (origin/main at the time)

1. unmodified source (what CI will run)
   PG_LADDER level=0 floor=0 first_fail=FAIL L1 hub not listening after 3s (hub exit=1): error: PG: syntax error at or near "desktop"
   RESULT: PASS (level 0, floor 0)                                  rc=0

2. witnessed red: PG_LADDER_FLOOR=1
   RESULT: FAIL — regressed below the floor (level 0 < floor 1).    rc=1

3. witnessed red: EXPECTED_SOURCE_COMMIT=deadbeef
   FAIL: source provenance mismatch                                 rc=1

4. positive control for the upper rungs: db-adapter.ts / server.ts replaced
   (bind-mounted, not committed) with the RFC-039 §1.3 probe patches
   (comment-aware split, BLOB→BYTEA, $$ sent whole, text timestamps,
   scheduler gate bypassed)
   FAIL L2 first user role is "user", expected "admin"
   PG_LADDER level=1                                                rc=0
   same + int8 parsed as number:
   PASS L2 first registered user is admin
   PASS L3 login, node token, report_status, POST /api/task
   FAIL L4 send_reply: {"ok":false,"error":"PG: could not determine data type of parameter $2"}
   PG_LADDER level=3                                                rc=0

   L0–L3 detectors observed passing and failing; L4 observed failing only
   (no build reaches it yet).

---
RFC-039 S2a (branch fix/pg-s2a-schema) — rungs renumbered: L1 is now "schema
built" (startHub() reached); old L1–L4 are L2–L5.

5. S2a source, unmodified (what CI will run)
   FAIL L2 hub not listening after 106s (hub exit=1): error: scheduled_tasks_require_transactional_sqlite_backend
   PG_LADDER level=1 floor=1                                        rc=0

6. positive control: S2a + scheduler gate bypassed (server.ts bind-mounted)
   PASS L3 first registered user is admin          ← int8 fix, in the real adapter now
   PASS L4 login, node token, report_status, POST /api/task
   FAIL L5 send_reply: {"ok":false,"error":"PG: could not determine data type of parameter $2"}
   PG_LADDER level=4                                                rc=0

7. SQLite server suite (bun run test, Docker oven/bun:1.3.14), per-file diff vs base
   base  (5cad7977): files=126 pass=1539 fail=0 skip=0
   S2a   (dcfb45f3): files=127 pass=1551 fail=0 skip=0
   only difference: + server/src/db-adapter-translate.test.ts pass=12 (new)

8. test637-database-url-guard on S2a: RESULT: PASS (guard + mutation red intact)

---
RFC-039 S2b (branch fix/pg-s2b-worker) — PgAdapter = one Bun.SQL connection in
a worker thread; image no longer installs nodejs or pg.

9. S2b source, unmodified (what CI will run)
   14/14 PASS contract (rollback, commit, caught error inside a transaction,
   nested savepoint, COUNT number, BLOB bytes, bool→1, text datetime,
   changes, 3 MB value, 0.91 ms/statement over 500 inserts)
   PASS L3 first registered user is admin
   PASS L4 login, node token, report_status, POST /api/task
   INFO task row after the failed reply: status=delivered result=null
        (on the subprocess bridge the same failure left it status=replied)
   FAIL L5 send_reply: PG: could not determine data type of parameter $2  (S3, F1)
   PG_LADDER level=4 floor=4                                        rc=0

10. witnessed red on the contract (adapter bind-mounted with one mutation)
    ROLLBACK → COMMIT in transaction():  FAIL contract: ROLLBACK discards …   rc=1
    per-statement savepoint guard off:   "current transaction is aborted"    rc=1

11. SQLite server suite, per-file diff vs S2a: none
    S2b (28a21808): files=127 pass=1551 fail=0 skip=0

12. neighbouring suites on S2b: test637 PASS (its mutation now also copies
    pg-worker.ts), test601 PASS (L11 mutation repointed to the
    atomicTransactions gate, still MUTATION_RED), test660 PASS, test1195 PASS

13. S2b rework: PG feature gates closed by default (COMMHUB_PG_EXPERIMENTAL)
    PASS gate: without the opt-in the PG Hub refuses and names COMMHUB_PG_EXPERIMENTAL=1
    PG_LADDER level=4 floor=4 COMMHUB_PG_EXPERIMENTAL=1 …            rc=0
    witnessed red — PgAdapter refusal hard-wired to null (gate open by default):
    FAIL gate: without the opt-in the PG Hub kept running (gate open by default)   rc=1

---
RFC-039 S3 (branch fix/pg-s3)

14. S3 source (what CI will run)
    PASS gate (no opt-in → refuses, names COMMHUB_PG_EXPERIMENTAL=1)
    PASS L3 admin / PASS L4 task / PASS L5 send_reply ok and task row replied
    PG_LADDER level=5 floor=5 COMMHUB_PG_EXPERIMENTAL=1 first_fail=none   rc=0
    full hub.log: 0 "PG:"/"failed" lines (retention PRAGMA and integer
    out-of-range errors gone); pg.log ERRORs are only the idempotent
    "column … already exists" ALTERs and contract.ts's deliberate
    no_such_table.

15. witnessed red: db.ts CAST reverted (bind-mounted)
    FAIL L5 send_reply: could not determine data type of parameter $2
    PG_LADDER level=4 floor=5 → RESULT: FAIL                         rc=1

---
RFC-039 S4 part 1 (branch fix/pg-s4-tests)

16. existing test files run unmodified-in-intent on PostgreSQL via
    COMMHUB_TEST_PG_URL (role anet_tester, scram password, 127.0.0.1,
    anet_*_test databases) with COMMHUB_PG_EXPERIMENTAL=1:
    src/scheduled-tasks-http.test.ts        15 pass 0 fail  (incl. two real
        processes claiming one occurrence, and dispatch rollback)
    src/side-thread-command-transport.test.ts  27 pass 0 fail
    src/task-consumption.test.ts (runtime evidence)  17 pass 0 fail
    first run found 3 PG-only defects in the fixtures/product:
      - side_chat_operations ORDER BY rowid → fixed (PG rowid column)
      - test used PRAGMA table_info / a SQLite trigger for fault injection →
        dialect-branched in the test (SQLite branch byte-identical)
    PG_LADDER level=5 floor=5 COMMHUB_PG_EXPERIMENTAL=1                  rc=0

17. test637 L3: host check and anet_*_test check each removed by sed →
    MUTATION_RED (the mutated copy ACCEPTs the foreign host / prod name)

---
RFC-039 F3/F5 (branch fix/pg-f3f5, after S4)

18. adds src/agent-acl-slice3-http.test.ts (human DMs, node lookups,
    renames) to the PostgreSQL features stage:
    before this PR (S4 db.ts / db-adapter.ts bind-mounted): 4 DM tests
      fail (user_inbox ORDER BY rowid; strftime) + rename commit fails with
      "PG: insufficient data left in message"
    this PR: 14 pass 0 fail; ladder level=5 floor=5; contract 15/15
    root cause of the rename failure: 10 call sites pass get(sql, [a, b])
    — bun:sqlite treats a lone array as the parameter list, PgAdapter sent
    it as one array-typed parameter. PgAdapter.get/all now do the same as
    bun:sqlite; contract.ts checks it; removing the fix → RESULT: FAIL.

---
RFC-039 F6 (branch fix/pg-f6-like, after F3/F5)

19. adds src/skillhub-http.test.ts to the PostgreSQL features stage, with a
    new case-insensitive list_skills query test.
    this PR: all 5 feature files rc=0; ladder level=5 floor=5
    tools.ts without the PG ILIKE (bind-mounted): the new test FAILS on PG
    restricted-files LIKE kept as-is (prefilter; JS does the exact match) —
    covered on PG by agent-acl-slice3's "cannot smuggle a file" DM test.

---
RFC-039 S5a/S5b (branches fix/pg-s5a-real, feat/pg-s5b-migrate)

20. S5a: contract "REAL keeps SQLite's 8-byte precision" — PASS; with the
    old translator: FAIL (15.678901234567 read back as 15.678901672363281).

21. S5b migrate stage (migrate-stage.sh), all PASS, MIGRATE_FAILED=0:
    seed Hub on SQLite reached L5; stopped seed Hub left a 440872-byte -wal
    a database open in a running Hub is refused (rc=2)
    source under ~/.commhub is refused (rc=2)
    non-empty target is refused (rc=2); refusals left the target untouched
    dry run with --i-know-this-is-a-copy verified every table; target empty after
    tampered copy fails verification (MUTATION_RED); target empty after
    migration committed: 42 tables, 19 rows
    the seeded, replied task is in PostgreSQL
    Hub on the migrated PostgreSQL: migrated admin logs in, reached L5
    first design ("non-empty -wal = Hub running") was wrong: a cleanly
    stopped Hub leaves one; replaced by a /proc open-file check.

22. S5b review follow-ups:
    - triggers re-enabled in a `finally`; after a failed (tampered) run the
      same target takes a clean migration and all user triggers are enabled
      (tgenabled='O'). Mutation: ENABLE removed → "total=2 disabled=2", FAIL.
      (The "terminal events keep being recorded" check stays green under that
      mutation, so it is not trigger-specific; the tgenabled check is.)
    - every outcome prints one SUMMARY line:
      SUMMARY result=COMMITTED tables=42 rows=19 verify=OK          rc=0
      SUMMARY result=DRY-RUN … verify=OK                             rc=0
      SUMMARY result=FAILED … verify=MISMATCH (tamper)               rc=1

== 2026-09-30 — L6 (task board slim reads), local Docker run of perf/hub-requirements-summary-etag ==
    PASS L3 first registered user is admin
    PASS L4 login, node token, report_status, POST /api/task
    PASS L5 send_reply ok and task row replied
    PASS L6 requirements view=summary, changes=1 with tombstones, ETag 304
    [features] src/scheduled-tasks-http.test.ts rc=0
    [features] src/side-thread-command-transport.test.ts rc=0
    [features] src/task-consumption.test.ts rc=0
    [features] src/agent-acl-slice3-http.test.ts rc=0
    [features] src/skillhub-http.test.ts rc=0
    [features] src/task-access-http.test.ts rc=0
    [features] src/requirements-stats-http.test.ts rc=0
    [features] src/requirement-tag-ops-http.test.ts rc=0
    [features] src/scheduled-reply-unread-http.test.ts rc=0
    [features] src/requirements-list-slim-http.test.ts rc=0
    PASS migrate: seed Hub on SQLite reached L6
    PASS L3 the migrated admin logs in with its original password and is still admin
    PASS L4 login, node token, report_status, POST /api/task
    PASS L5 send_reply ok and task row replied
    PASS L6 requirements view=summary, changes=1 with tombstones, ETag 304
    PASS migrate: Hub on the migrated PostgreSQL reached L6
    PG_LADDER level=6 floor=6 COMMHUB_PG_EXPERIMENTAL=1 first_fail=none
    RESULT: PASS (level 6, floor 6)
