Skip to content

Commit e2ddfd5

Browse files
claude[bot]claudeos-zhuang
authored
docs(qa): land the #9296 wave's item corrections and the console-session-auth environment fact (#9475)
Closes out the second half of the QA wave #9296 ledger debt. Part 1 (five of the six environment facts plus the absence-inference trap row) landed in PR #9427; this carries the sixth fact and every areas/*.json item correction. RUNNER.md — the one environment fact still missing: console session auth is a bearer token in localStorage (auth-session-token), so a clearCookies() gesture expires nothing and leaves a fully authed shell rendering; true expiry is POST /api/v1/auth/sign-out. The cookie half matters too — the storage family resolves its caller through better-auth's own getSession, so a bearer-only session 401s on /storage/upload/presigned. areas/*.json — 32 items corrected across 12 area files, each with its own revision bump and history entry naming what changed and why. Sources: the five run records #9334, #9401, #9417, #9453 and #9467. Every correction was re-grounded against origin/main before being written; four reported corrections were rejected as already-true or contradicted by the tree, and four items carrying the same defect class were found by that grounding rather than from any report. Claude-Session: https://claude.ai/code/session_01Ai9gUdihXjg7FumzsBNSSs Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <jack@objectstack.ai>
1 parent eccb8b2 commit e2ddfd5

13 files changed

Lines changed: 190 additions & 130 deletions

docs/qa/platform-checklist/RUNNER.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,29 @@ contradicts it, and correct it here when it does.
149149
the real port). This is "zero hits needs a positive control" applied to one tool: a
150150
negative from a command that never ran is not evidence.
151151

152+
- **Console session auth is a bearer token in `localStorage`, and a form sign-in ALSO
153+
sets a cookie — you need both halves.** The console's auth client stores the session
154+
under `auth-session-token` (objectui `packages/auth/src/createAuthClient.ts`:
155+
`TOKEN_STORAGE_KEY = 'auth-session-token'`) and sends it as an `Authorization` header;
156+
its own metadata-client note says outright that *"there is no session cookie"* for that
157+
path. Two consequences, and each has already misled a round on its own:
158+
1. **A `clearCookies()` gesture expires nothing.** The bearer token survives it, so the
159+
shell keeps rendering fully authed data — which reads exactly like the "dead shell
160+
serving stale data as if authed" failure the console-login item warns about, and
161+
nearly produced a false P0. **Check:** after the gesture, an in-page
162+
`fetch('/api/v1/meta/app?id=showcase_app')` still answers **200**. True expiry is
163+
server-side: `POST /api/v1/auth/sign-out` (clearing `localStorage` is the
164+
client-side equivalent); after either, the same fetch answers **401**.
165+
2. **The cookie is not decorative — some routes need it.** `better-auth.session_token`
166+
is set by a real form sign-in, and the storage family resolves its caller through
167+
better-auth's own `getSession` (`resolveSessionData` in
168+
`packages/runtime/src/security/resolve-session-principal.ts`) rather than the REST
169+
bearer seam. So a session driven with the localStorage token alone gets **401
170+
`AUTH_REQUIRED`** from `POST /api/v1/storage/upload/presigned` while
171+
`GET /auth/get-session` on the same page returns 200 — a convincing fake "avatar
172+
upload is broken". ⛔ Do not drive any storage/upload surface from an injected
173+
token: **sign in through the form** so both halves exist.
174+
152175
- **A cold tree cannot boot the app from the console-build recipe alone.**
153176
`pnpm objectui:build` runs `scripts/build-console.sh`, which builds the **console**, not
154177
the framework CLI. On a fresh tree `packages/cli` has no `dist`, and the bare binary

docs/qa/platform-checklist/areas/access-security.json

Lines changed: 44 additions & 31 deletions
Large diffs are not rendered by default.

docs/qa/platform-checklist/areas/ai.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"title": "agent / tool / skill metadata kinds author, persist and list over the meta surface — retired keys reject with prescriptions, the closed agent kind has no runtime write door",
99
"since": "v15",
1010
"status": "active",
11-
"revision": 2,
11+
"revision": 3,
1212
"priority": "P1",
1313
"surface": "api",
1414
"personas": ["admin"],
@@ -67,7 +67,7 @@
6767
{
6868
"clause": "the runtime write door matches the declared registry posture — BOTH sides: PUT ?mode=draft succeeds for skill (and tool), while the agent kind has NO governed runtime write path (allowRuntimeCreate:false; 'for agents, the code is the record', metadata-plugin.zod.ts — migrateStoredMetadata reports agent rows 'skipped' by design)",
6969
"oracle": "api",
70-
"verify": "skill draft PUT returns 200 and the draft is readable; the agent runtime save is refused/unsupported (capture the actual status), and that refusal is recorded as CORRECT, not filed as a bug",
70+
"verify": "skill draft PUT returns 200 and the draft is readable via GET /api/v1/meta/skill/qa_probe_skill?state=draft — ⚠️ the READ parameter is ?state=draft, not ?mode=draft: rest-server.ts keys the draft projection on req.query.state, while ?mode=draft is the WRITE-side parameter, so re-reading with ?mode=draft serves the active item (or 404s when none is active) and reads like the draft never landed. The agent runtime save is refused/unsupported (capture the actual status), and that refusal is recorded as CORRECT, not filed as a bug",
7171
"evidence": "the two PUT responses"
7272
},
7373
{
@@ -99,7 +99,8 @@
9999
],
100100
"history": [
101101
{ "revision": 1, "date": "2026-08-07", "change": "initial — closes the AI hole in capability coverage (no area covered agent/tool/skill kinds)", "ref": "claude/platform-test-checklist-ocwugl" },
102-
{ "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }
102+
{ "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" },
103+
{ "revision": 3, "date": "2026-08-18", "change": "named the draft READ parameter in clause 4. The clause said the draft is readable after PUT ?mode=draft without saying how; ?mode=draft is the write-side parameter and the read projection keys on req.query.state === 'draft' (rest-server.ts), so a runner re-reading with ?mode=draft gets the active item or a 404 and concludes the draft write silently failed (#9417)", "ref": "#9386" }
103104
]
104105
},
105106
{

docs/qa/platform-checklist/areas/api-backend.json

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"title": "transactionalBatch capability bit matches the mounted /batch behavior — atomicity, rollback codes, size cap",
99
"since": "v16",
1010
"status": "active",
11-
"revision": 2,
11+
"revision": 3,
1212
"priority": "P1",
1313
"surface": "api",
1414
"personas": [
@@ -22,30 +22,30 @@
2222
]
2323
},
2424
"steps": [
25-
"boot showcase isolated; GET /api/v1/discovery and read capabilities.transactionalBatch (also compare client.capabilities.transactionalBatch if driving via @objectstack/client)",
26-
"POST /api/v1/batch with a well-formed cross-object atomic batch: operations = [create showcase_private_note {title}, create showcase_task {title}], options {\"atomic\": true}; verify both rows exist afterwards",
27-
"POST /api/v1/batch with the same operations and options {\"atomic\": false}; capture status + error code",
25+
"boot showcase isolated; GET /api/v1/discovery and read capabilities.transactionalBatch.enabled — on the wire each capability is a CapabilityDescriptor object { enabled, features?, description? } (discovery.zod.ts), never a bare boolean. The @objectstack/client getter is the exception: it FLATTENS each descriptor to a real boolean, so client.capabilities.transactionalBatch is a boolean and the two reads are spelled differently on purpose",
26+
"POST /api/v1/batch with a well-formed cross-object atomic batch: operations = [create showcase_private_note {title}, create showcase_task {title}] and a TOP-LEVEL atomic: true; verify both rows exist afterwards. ⚠️ CrossObjectBatchRequestSchema declares operations + a top-level atomic and no options key at all — an options wrapper is silently STRIPPED by Zod, so the request runs with atomic defaulting to true",
27+
"POST /api/v1/batch with the same operations and a TOP-LEVEL atomic: false; capture status + error code. ⛔ Spelled as options {atomic: false} the key is stripped, atomic defaults to true, and the batch simply SUCCEEDS — the clause below then reads as a product failure when nothing failed",
2828
"POST /api/v1/batch where a MIDDLE operation must fail (e.g. create showcase_invoice missing its required name), sandwiched between two valid creates; capture the per-operation results and re-read all three would-be rows",
2929
"POST /api/v1/batch with operations.length > maxBatchSize (201 no-op creates on the default config); capture the rejection",
3030
"POST /api/v1/data/showcase_private_note/batch (the PER-OBJECT batch door) with a mixed valid/invalid set and NO atomic flag; capture the per-row outcomes"
3131
],
3232
"acceptance": [
3333
{
34-
"clause": "the discovery bit is true exactly when /batch is mounted and transaction-capable — capability read and live behavior agree",
34+
"clause": "the discovery capability is enabled exactly when /batch is mounted and transaction-capable — capability read and live behavior agree",
3535
"oracle": "api",
36-
"verify": "GET /api/v1/discovery capabilities.transactionalBatch == true AND the atomic batch in step 2 succeeds end-to-end (or, on a runtime without tx support, the bit is false and /batch refuses atomically-dependent use)",
36+
"verify": "GET /api/v1/discovery capabilities.transactionalBatch.enabled === true AND the atomic batch in step 2 succeeds end-to-end (or, on a runtime without tx support, enabled is false and /batch refuses atomically-dependent use). ⛔ A strict capabilities.transactionalBatch === true against the wire shape compares an object to a boolean and is always false",
3737
"evidence": "discovery body + batch trace"
3838
},
3939
{
40-
"clause": "atomic:false on the CROSS-OBJECT /batch answers 400 BATCH_NOT_ATOMIC — the endpoint is all-or-nothing by construction (batch.zod.ts: atomic accepted for symmetry, MUST be true)",
40+
"clause": "a TOP-LEVEL atomic:false on the CROSS-OBJECT /batch answers 400 BATCH_NOT_ATOMIC — the endpoint is all-or-nothing by construction (batch.zod.ts: atomic accepted for symmetry, MUST be true)",
4141
"oracle": "api",
4242
"verify": "step-3 response: status 400, error code BATCH_NOT_ATOMIC, message pointing at POST /data/:object/batch for non-atomic per-object batches",
4343
"evidence": "response"
4444
},
4545
{
46-
"clause": "a failing member rolls the whole atomic batch back with the #4793 per-row codes: rows before the failure report ROLLED_BACK (written then undone), rows after report NOT_ATTEMPTED (never ran) — and NO row from the batch persists",
46+
"clause": "a failing member rolls the whole atomic batch back and NO row from the batch persists. ⚠️ The #4793 per-row ROLLED_BACK / NOT_ATTEMPTED codes are NOT observable on this door: CrossObjectBatchResponseSchema is { results, droppedFields? } with no per-row error envelope — those codes live on the PER-OBJECT door (BatchResponseSchema.errors, step 6), which is where they were proved. Score the rollback here from the follow-up reads",
4747
"oracle": "api",
48-
"verify": "step-4 per-operation results carry the two codes in the right positions; follow-up GETs find none of the three records",
48+
"verify": "follow-up GETs find none of the three records (the authoritative oracle here); the per-row #4793 codes are read on the per-object door instead, since this response carries no errors array",
4949
"evidence": "batch response + the absent-row reads"
5050
},
5151
{
@@ -86,7 +86,8 @@
8686
"date": "2026-08-07",
8787
"change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants",
8888
"ref": "claude/platform-test-checklist-ocwugl"
89-
}
89+
},
90+
{ "revision": 3, "date": "2026-08-18", "change": "corrected three separate wrong shapes, each of which made correct behaviour read as a defect. (a) capabilities.transactionalBatch is a CapabilityDescriptor object on the wire, so the clause's literal == true is always false — only the @objectstack/client getter flattens it to a boolean. (b) Steps 2-3 sent atomic inside an options wrapper; CrossObjectBatchRequestSchema declares a TOP-LEVEL atomic and no options, so Zod strips the wrapper and atomic defaults to true — the atomic:false probe therefore succeeds instead of answering BATCH_NOT_ATOMIC. (c) Clause 3 attached the #4793 per-row codes to the cross-object door, whose response schema is { results, droppedFields? } with no per-row error envelope; those codes live on the per-object door (#9417)", "ref": "#9386" }
9091
]
9192
},
9293
{
@@ -298,7 +299,7 @@
298299
"title": "Data-API query contract: every filter operator gives known answers; $-params, select, sort, expand honored; malformed input 400s with the exact code",
299300
"since": "v15",
300301
"status": "active",
301-
"revision": 1,
302+
"revision": 2,
302303
"priority": "P0",
303304
"surface": "api",
304305
"personas": [
@@ -410,7 +411,7 @@
410411
],
411412
"automated": {
412413
"kind": "unit",
413-
"ref": "packages/objectql/src/engine.test.ts ([#2850] expand sub-read through the secured find path) + packages/objectql/src/filter-comparand-shape.ts (#5869 gate)"
414+
"ref": "packages/objectql/src/engine.test.ts ([#2850] expand sub-read through the secured find path = C4 only) + packages/objectql/src/engine-filter-array-lowering.test.ts (the #5869 list-comparand shape gate). ⚠️ The REST-contract clauses (operator answer sets, $top/$skip windowing, $select narrowing, sort + INVALID_SORT, INVALID_FILTER, UNSUPPORTED_QUERY_PARAM, the $in/$nin empty-list boundary) are NOT reached by these engine unit suites and remain manual"
414415
},
415416
"traps": [
416417
"seed-data-thin",
@@ -431,15 +432,16 @@
431432
"date": "2026-08-07",
432433
"change": "new — query-contract matrix over the spec operator vocabulary with known-answer checks, per the deep-test contract",
433434
"ref": "claude/platform-test-checklist-ocwugl"
434-
}
435+
},
436+
{ "revision": 2, "date": "2026-08-18", "change": "re-pointed automated.ref off a source module. packages/objectql/src/filter-comparand-shape.ts is not a test — it exports invalidFilterError / assertListComparandShapes / assertFilterIsMaterializable and declares no describe block; the #5869 gate is actually pinned by engine-filter-array-lowering.test.ts. Also recorded the pin's real reach, since it spans one clause of nine and a pass read off it would be a false green (#9401)", "ref": "#9386" }
435437
]
436438
},
437439
{
438440
"id": "api-backend.error-envelope-ledger",
439441
"title": "Sampled endpoints return the standard error envelope with ledgered codes — no invented codes, no retired keys, statuses match the map",
440442
"since": "v16",
441443
"status": "active",
442-
"revision": 1,
444+
"revision": 2,
443445
"priority": "P1",
444446
"surface": "api",
445447
"personas": [
@@ -462,13 +464,13 @@
462464
"not-found: GET /api/v1/data/not_a_real_object and GET /api/v1/data/showcase_task/nonexistent-id-0000 — the two 404 flavors",
463465
"batch: POST /api/v1/batch with options {\"atomic\": false} — the registered-extension-code sample (BATCH_NOT_ATOMIC)",
464466
"query-rejection: GET /api/v1/data/showcase_account?$pageSize=5 — the UNSUPPORTED_QUERY_PARAM sample",
465-
"validate every captured body against the spec: code membership in StandardErrorCode ∪ ERROR_CODE_LEDGER, envelope shape per ErrorResponseSchema/EnhancedApiErrorSchema (noting the REST flat dialect where it is the declared shape), fields[] entries against FieldErrorCode"
467+
"validate every captured body against the spec: code membership in ErrorCode (error-code-ledger.zod.ts — the canonical ADR-0112 D4 union, StandardErrorCode.optionsREGISTERED_ERROR_CODES), envelope shape per ErrorResponseSchema/EnhancedApiErrorSchema (noting the REST flat dialect where it is the declared shape), fields[] entries against FieldErrorCode"
466468
],
467469
"acceptance": [
468470
{
469-
"clause": "every sampled error code is LEDGERED: code ∈ StandardErrorCode (errors.zod.ts) ∪ ERROR_CODE_LEDGER (error-code-ledger.zod.ts) — an unregistered code is a FAIL per ADR-0112 D3/D4 (no silent fourth state)",
471+
"clause": "every sampled error code is LEDGERED: code ∈ ErrorCode (error-code-ledger.zod.ts) — an unregistered code is a FAIL per ADR-0112 D3/D4 (no silent fourth state). ⚠️ Check against ErrorCode itself, or against StandardErrorCode.options ∪ REGISTERED_ERROR_CODES: ERROR_CODE_LEDGER is a package-name to code-array MAP, not a flat code list, and several sampled codes (VALIDATION_FAILED, UNSUPPORTED_QUERY_PARAM, BATCH_NOT_ATOMIC) are absent from StandardErrorCode and reachable only through the flattened union",
470472
"oracle": "api",
471-
"verify": "for each captured body, membership check of error code against the two spec files",
473+
"verify": "for each captured body, membership check of error code against ErrorCode (the single export that already IS the union — no hand-unioning of two spec files, which is where the flat-vs-map shape trips runners up)",
472474
"evidence": "the captured bodies + the membership table"
473475
},
474476
{
@@ -530,7 +532,8 @@
530532
"date": "2026-08-07",
531533
"change": "new — error-envelope conformance sampling grounded in the two-tier code ledger, per the deep-test contract",
532534
"ref": "claude/platform-test-checklist-ocwugl"
533-
}
535+
},
536+
{ "revision": 2, "date": "2026-08-18", "change": "re-pointed clause 1 and steps 8/9 at ErrorCode, the canonical ADR-0112 D4 union export. The item asked the runner to hand-union StandardErrorCode with ERROR_CODE_LEDGER, but the latter is a Record of package name to code array rather than a flat code list, and three of the item's own sampled codes (VALIDATION_FAILED, UNSUPPORTED_QUERY_PARAM, BATCH_NOT_ATOMIC) are not in StandardErrorCode at all — they resolve only through REGISTERED_ERROR_CODES. error-code-ledger.zod.ts already exports ErrorCode as exactly that union, so the clause now names one export instead of prescribing a union the runner has to rebuild (#9417)", "ref": "#9386" }
534537
]
535538
},
536539
{

0 commit comments

Comments
 (0)