You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Full api-backend area run of the checklist-test skill — all 11 items driven against a live showcase (3 opus subagents, one isolated boot each; ports 4011–4013). Convention labels: qa-run + bug.
Result: 6 PASS · 2 PARTIAL · 3 FAIL. Text-only report per RUNNER.md; every verdict rests on a live-HTTP / gate-output / test oracle.
⚠️One additional SECURITY-SENSITIVE finding from this run is deliberately NOT described here, pending the maintainer's decision on disclosure (same handling as the earlier FOLLOW-UPS.md D1). It is a cross-persona data-disclosure defect with a located root cause and a 2/2 reproduction. Ask for the private write-up.
showcase · SqlDriver(better-sqlite3) · fresh isolated file DB + port per batch
personas
admin@objectos.ai + purpose-signed-up member/contributor (created in-run rather than declaring items blocked)
🔴 FAIL — 3 items, 7 defects
query-contract-matrix (P0) — 4 defects
The matrix itself is solid: 16 operators, $-params, pagination windows, sort (+INVALID_SORT both doors), $in:[]/$nin:[] arity, and the #5869 scalar-on-collection gate all verified across three doors (POST /query {where}, POST {filter} AST, GET ?$filter=) with exact id-set equality. Four contract breaks:
1. Dotted projection silently WIDENS.POST /data/showcase_invoice/query {"fields":["name","account.name"]} → 200 returning every business field and no resolved account.name. Same on GET ?$select=name,account.name and on showcase_task {fields:["title","project.name"]}; control {fields:["title"]} correctly narrows. A nonexistent dotted column is likewise swallowed (200 + every field) while a nonexistent plain column answers 400 INVALID_FIELD — whose own message names this exact failure mode. Root cause:assertProjectionFieldsExist validates only f.split('.')[0], so the dotted entry passes ingress then defeats the projection.
2. Unknown field inside where/$filter answers 200/0 instead of 400 INVALID_FIELD.{"where":{"not_a_field":"x"}} → 200 records:[] total:0, no code; identically for the $filter and AST doors. Same-run control ?not_a_field=x → 400 INVALID_FIELD. The two doors disagree about one mistake, and "indistinguishable from no data" is exactly what #4134 closed for the bare-key door.
3. like is folded to $contains — wildcards bound as literals.["name","like","%Industries"] → 0 rows, while ["name","like","Industries"] → the 2 matching rows, byte-identical to the $contains control. Contradicts canonicalAstOperator's own comment ("driver-sql passes them to SQL verbatim, so the caller binds the wildcards. Folding them onto contains would silently wrap the value in %…% and change what the query means"). The driver's like/ilike bind arm is unreachable from the wire since #5158.
4. expand is a silent no-op when fields omits id.{"expand":{"account":{"object":"showcase_account","fields":["name"]}}} → 200 with account left as the raw FK id; adding "id" expands correctly (2/2 on a second object pair too). Root cause:engine.ts expandRelatedRecords forwards nestedAST.fields to the sub-find then keys recordMap on rec.id, so a projection omitting id yields an empty map and the inject falls through to recordMap.get(val) ?? val. This spelling is the one prescribed verbatim in two spec retirement messages (FIELD_NODE_OBJECT_FORM_REMOVED, QUERY_JOINS_REMOVED) and in this item's own steps.
POST /data/showcase_private_note/batch with no options and a failing middle record → 200 {total:3, succeeded:1, failed:1, results:[idx0 ok, idx1 VALIDATION_FAILED]} — only 2 results for 3 records, no NOT_ATTEMPTED for idx2, succeeded+failed (2) != total (3), and the third valid row returns 0 rows on re-read (never attempted). Expected per ADR-0119 D4 (atomic defaults false on this door): valid rows land, neither blocks the other. Reproduced 2/2; two controls isolate it — continueOnError:true → 3 results with both valid rows persisted, atomic:true → the correct ROLLED_BACK/VALIDATION_FAILED/NOT_ATTEMPTED triple. Root cause: the non-atomic loop breaks out on failure while buildBatchDataResponse reports total = records.length, so the un-attempted tail is invisible. (The rest of the item passes: discovery bit matches live behaviour incl. $ref parenting and BATCH_UNRESOLVED_REF; atomic:false on the cross-object door is refused 400 BATCH_NOT_ATOMIC; cross-object rollback held at both write-time and validation-time failures; BATCH_TOO_LARGE bracketed exactly at 200/201 on all three doors.)
route-ledger-live-parity — 2 defects
5. A ledgered dispatcher route is never mounted (#3361 class).GET /api/v1/meta/objects/showcase_task/state/status?from=in_review → 404 {"error":"Not found"} — byte-identical to an unmounted-path control, not a handler answer. Expected per route-ledger.ts (ADR-0020 D3.3): 200 {object,field,from,next:["done","in_progress"]}. Root cause: zero route registrations deliver a 4-segment /meta path; the domain handler exists and ships in dist (so not stale-dist), but REST registers ~17 /meta routes maxing at 3 segments, so it dies at Hono's notFound. Same seam: GET /meta/types is swallowed by REST's /meta/:type.
6. A discovery capability bit contradicts live behaviour.capabilities.search = {enabled:false} (and services.search: unavailable) while GET /api/v1/search?q=audit serves 200 with 5 real hits. A client that trusts the bit skips a working surface. Root cause: the bit derives from a registered searchservice slot, while registerSearchEndpoints only 501s when protocol.searchAll is missing — and the protocol implements searchAll regardless.
7. Raw TypeError leaks in the error envelope (surfaced by the API-Console batch, belongs to error-envelope-ledger)
POST /api/v1/data/showcase_task body {"title": 12345} (a number into a declared text field) → 400 {"error":"TypeError: not a function","object":"showcase_task"} — no code, no fields[]. Expected the ledgered envelope the other invalid cases produce ({} → 400 VALIDATION_FAILED with fields[]; a bad enum → invalid_option with the allowed list). Reproduced 3×. Suspected: a coercion path calls a string method on a number before validation reports it.
🟡 PARTIAL — 2 items, both checklist drift, not product defects
declarative-endpoint-execution — live endpoints serve with their declared cache-control: private, max-age=30, flow-target endpoints run real flows, anonymous is default-closed (401), unknown path/wrong method give the bare 404. The clause's literal 501 NOT_IMPLEMENTED for script/proxy targets is unreachable: the publish gate refuses them at boot with a located message (17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 §7-3) — stricter than the clause asks. automated.ref dogfood suites 22/22 green. → revise the item text.
server-timing-admin-gated — admin gets full spans plus the admin-only SQL-shape detail header; a non-admin gets no header in per-request mode (the security half holds; both personas measured live in the same run). Two clauses are contradicted by the implemented two-mode design rather than by a leak: with OS_SERVER_TIMING=true (the flag the item's own steps set) the basic header goes to everyone including anonymous — documented deliberate "global" mode — while the sensitive detail header stays admin-only; and with the flag off, an admin with the debug header still gets spans (only serverTiming:false removes the middleware). Also: no hooks span exists anywhere — that name in the contract is aspirational. → revise the item text.
✅ PASS — 6 items
error-envelope-ledger (6/6 — every sampled code a ledger/standard member, fields[] shape + catalog, zero fieldErrors across 13 bodies, exact status↔code table, OBJECT_NOT_FOUND vs RECORD_NOT_FOUND distinguishable, zero 5xx; the flat 401 dialect confirmed sanctioned under the ADR-0112 amendment) · bulk-write-contract (6/6, and the #2982 negative proven: a member's updateMany/deleteMany against an invisible admin-owned row → FORBIDDEN/PERMISSION_DENIED with the row verifiably unchanged) · package-rest-lifecycle (6/6 incl. duplicate → 409 with no#2995 clobber, and a true partial PATCH) · formula-gates (4/4 against the built dist with a resolved-path guard; date arithmetic refused at build with daysBetween/addDays prescribed) · enforce-or-remove-authoring-gates (4/4 across four registries; every rejection names its replacement) · api-console-discovery-execute (5/5; tree matches /discovery 10/10, real 200/404/400 rendered with the server's own envelopes).
Smaller observations
Unledgered live mounts:/api/settings, /api/v1/datasources, /api/v1/datasources/drivers, GET /api/v1/health, GET /api/v1/meta/types all answer 200 but appear in no route ledger (PENDING-GAPS §E still open).
POST /api/v1/packages/publish answers a misleading 405 (Allowed: DELETE, GET, HEAD, PATCH) — the path is only reachable because /packages/:id absorbs it; the REST package registrar isn't mounted on showcase.
API Console never renders the Storage group on any host: its catalog key is storage while /discovery names the service file-storage, so the fail-closed branch fires and 3 endpoints are unreachable from that page.
Doc nit: skill.zod.ts's docblock still says a permissions key is "silently stripped", but SkillSchema now rejects it — behaviour is safer than the prose.
Screenshots
None — all oracles are live HTTP / build-gate output / cited test runs. The 7 public defects carry reproduction rules above.
Full
api-backendarea run of thechecklist-testskill — all 11 items driven against a live showcase (3 opus subagents, one isolated boot each; ports 4011–4013). Convention labels:qa-run+bug.Result: 6 PASS · 2 PARTIAL · 3 FAIL. Text-only report per RUNNER.md; every verdict rests on a live-HTTP / gate-output / test oracle.
Environment
a86db175— branchclaude/platform-test-checklist-ocwugl(PR #7304)🔴 FAIL — 3 items, 7 defects
query-contract-matrix(P0) — 4 defectsThe matrix itself is solid: 16 operators,
$-params, pagination windows, sort (+INVALID_SORTboth doors),$in:[]/$nin:[]arity, and the #5869 scalar-on-collection gate all verified across three doors (POST /query {where},POST {filter}AST,GET ?$filter=) with exact id-set equality. Four contract breaks:1. Dotted projection silently WIDENS.
POST /data/showcase_invoice/query {"fields":["name","account.name"]}→ 200 returning every business field and no resolvedaccount.name. Same onGET ?$select=name,account.nameand onshowcase_task {fields:["title","project.name"]}; control{fields:["title"]}correctly narrows. A nonexistent dotted column is likewise swallowed (200 + every field) while a nonexistent plain column answers400 INVALID_FIELD— whose own message names this exact failure mode. Root cause:assertProjectionFieldsExistvalidates onlyf.split('.')[0], so the dotted entry passes ingress then defeats the projection.2. Unknown field inside
where/$filteranswers 200/0 instead of400 INVALID_FIELD.{"where":{"not_a_field":"x"}}→200 records:[] total:0, no code; identically for the$filterand AST doors. Same-run control?not_a_field=x→400 INVALID_FIELD. The two doors disagree about one mistake, and "indistinguishable from no data" is exactly what #4134 closed for the bare-key door.3.
likeis folded to$contains— wildcards bound as literals.["name","like","%Industries"]→ 0 rows, while["name","like","Industries"]→ the 2 matching rows, byte-identical to the$containscontrol. ContradictscanonicalAstOperator's own comment ("driver-sql passes them to SQL verbatim, so the caller binds the wildcards. Folding them onto contains would silently wrap the value in %…% and change what the query means"). The driver'slike/ilikebind arm is unreachable from the wire since #5158.4.
expandis a silent no-op whenfieldsomitsid.{"expand":{"account":{"object":"showcase_account","fields":["name"]}}}→ 200 withaccountleft as the raw FK id; adding"id"expands correctly (2/2 on a second object pair too). Root cause:engine.ts expandRelatedRecordsforwardsnestedAST.fieldsto the sub-find then keysrecordMaponrec.id, so a projection omittingidyields an empty map and the inject falls through torecordMap.get(val) ?? val. This spelling is the one prescribed verbatim in two spec retirement messages (FIELD_NODE_OBJECT_FORM_REMOVED,QUERY_JOINS_REMOVED) and in this item's own steps.batch-transactional-discovery— per-object batch default blocks valid rowsPOST /data/showcase_private_note/batchwith nooptionsand a failing middle record →200 {total:3, succeeded:1, failed:1, results:[idx0 ok, idx1 VALIDATION_FAILED]}— only 2 results for 3 records, noNOT_ATTEMPTEDfor idx2,succeeded+failed (2) != total (3), and the third valid row returns 0 rows on re-read (never attempted). Expected per ADR-0119 D4 (atomicdefaults false on this door): valid rows land, neither blocks the other. Reproduced 2/2; two controls isolate it —continueOnError:true→ 3 results with both valid rows persisted,atomic:true→ the correctROLLED_BACK/VALIDATION_FAILED/NOT_ATTEMPTEDtriple. Root cause: the non-atomic loop breaks out on failure whilebuildBatchDataResponsereportstotal = records.length, so the un-attempted tail is invisible.(The rest of the item passes: discovery bit matches live behaviour incl.
$refparenting andBATCH_UNRESOLVED_REF;atomic:falseon the cross-object door is refused400 BATCH_NOT_ATOMIC; cross-object rollback held at both write-time and validation-time failures;BATCH_TOO_LARGEbracketed exactly at 200/201 on all three doors.)route-ledger-live-parity— 2 defects5. A ledgered dispatcher route is never mounted (#3361 class).
GET /api/v1/meta/objects/showcase_task/state/status?from=in_review→404 {"error":"Not found"}— byte-identical to an unmounted-path control, not a handler answer. Expected perroute-ledger.ts(ADR-0020 D3.3):200 {object,field,from,next:["done","in_progress"]}. Root cause: zero route registrations deliver a 4-segment/metapath; the domain handler exists and ships in dist (so not stale-dist), but REST registers ~17/metaroutes maxing at 3 segments, so it dies at Hono'snotFound. Same seam:GET /meta/typesis swallowed by REST's/meta/:type.6. A discovery capability bit contradicts live behaviour.
capabilities.search = {enabled:false}(andservices.search: unavailable) whileGET /api/v1/search?q=auditserves 200 with 5 real hits. A client that trusts the bit skips a working surface. Root cause: the bit derives from a registeredsearchservice slot, whileregisterSearchEndpointsonly 501s whenprotocol.searchAllis missing — and the protocol implementssearchAllregardless.7. Raw
TypeErrorleaks in the error envelope (surfaced by the API-Console batch, belongs toerror-envelope-ledger)POST /api/v1/data/showcase_taskbody{"title": 12345}(a number into a declaredtextfield) →400 {"error":"TypeError: not a function","object":"showcase_task"}— nocode, nofields[]. Expected the ledgered envelope the other invalid cases produce ({}→400 VALIDATION_FAILEDwithfields[]; a bad enum →invalid_optionwith the allowed list). Reproduced 3×. Suspected: a coercion path calls a string method on a number before validation reports it.🟡 PARTIAL — 2 items, both checklist drift, not product defects
declarative-endpoint-execution— live endpoints serve with their declaredcache-control: private, max-age=30, flow-target endpoints run real flows, anonymous is default-closed (401), unknown path/wrong method give the bare 404. The clause's literal501 NOT_IMPLEMENTEDforscript/proxytargets is unreachable: the publish gate refuses them at boot with a located message (17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 §7-3) — stricter than the clause asks.automated.refdogfood suites 22/22 green. → revise the item text.server-timing-admin-gated— admin gets full spans plus the admin-only SQL-shape detail header; a non-admin gets no header in per-request mode (the security half holds; both personas measured live in the same run). Two clauses are contradicted by the implemented two-mode design rather than by a leak: withOS_SERVER_TIMING=true(the flag the item's own steps set) the basic header goes to everyone including anonymous — documented deliberate "global" mode — while the sensitive detail header stays admin-only; and with the flag off, an admin with the debug header still gets spans (onlyserverTiming:falseremoves the middleware). Also: nohooksspan exists anywhere — that name in the contract is aspirational. → revise the item text.✅ PASS — 6 items
error-envelope-ledger(6/6 — every sampled code a ledger/standard member,fields[]shape + catalog, zerofieldErrorsacross 13 bodies, exact status↔code table,OBJECT_NOT_FOUNDvsRECORD_NOT_FOUNDdistinguishable, zero 5xx; the flat 401 dialect confirmed sanctioned under the ADR-0112 amendment) ·bulk-write-contract(6/6, and the #2982 negative proven: a member'supdateMany/deleteManyagainst an invisible admin-owned row →FORBIDDEN/PERMISSION_DENIEDwith the row verifiably unchanged) ·package-rest-lifecycle(6/6 incl. duplicate →409with no #2995 clobber, and a true partialPATCH) ·formula-gates(4/4 against the built dist with a resolved-path guard; date arithmetic refused at build withdaysBetween/addDaysprescribed) ·enforce-or-remove-authoring-gates(4/4 across four registries; every rejection names its replacement) ·api-console-discovery-execute(5/5; tree matches/discovery10/10, real 200/404/400 rendered with the server's own envelopes).Smaller observations
/api/settings,/api/v1/datasources,/api/v1/datasources/drivers,GET /api/v1/health,GET /api/v1/meta/typesall answer 200 but appear in no route ledger (PENDING-GAPS §E still open).POST /api/v1/packages/publishanswers a misleading 405 (Allowed: DELETE, GET, HEAD, PATCH) — the path is only reachable because/packages/:idabsorbs it; the REST package registrar isn't mounted on showcase.storagewhile/discoverynames the servicefile-storage, so the fail-closed branch fires and 3 endpoints are unreachable from that page.continueOnError(same family as defect [WIP] Fix error in step four of the action run #5 above).skill.zod.ts's docblock still says apermissionskey is "silently stripped", butSkillSchemanow rejects it — behaviour is safer than the prose.Screenshots
None — all oracles are live HTTP / build-gate output / cited test runs. The 7 public defects carry reproduction rules above.