Repository navigation
Description
Tier-2C — the final round of the platform-checklist QA wave (#9296). Scope: the last 18 unpinned P0/P1 items on the browser / mixed surfaces. All 18 consulted; every unpinned P0/P1 item in the ledger now has a verdict.
Environment fingerprint
| framework sha (subject) | e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd — unmoved throughout |
.objectui-sha |
665661ab093263f39f2e660a295ea615dbcee35a; check:console-sha ✓ stamp = pin |
| spec (checklist) | re-synced from origin/main at round start — check-platform-checklist: OK — 15 areas, 190 items (190 active); 30 kinds mapped, 0 waived |
| tree | objectstack-qa-wave, detached at the pin. objectui:build / objectui:refresh never run |
| ports / DBs | 3191 file:/tmp/qa-wave/t2c-A.db (main) · 3192 t2c-B.db (pinyin-OFF) · 3193 t2c-C.db (fresh-boot repro) · 3194/3195 fresh scaffold. Max 2 concurrent |
| seed | --seed-admin; personas minted with POST /auth/sign-up/email (bravo@qa.example, charlie@qa.example) |
| browser | playwright-core 1.62.1 via executablePath=/opt/pw-browsers/chromium (the RUNNER-prescribed symlink) |
| date | 2026-08-18 |
| exit state | all instances stopped; every probe datasource / dashboard / flow / overlay removed (sys_metadata runtime rows: none); git status = 11 docs/qa/** paths, 0 commits, HEAD still e4e5c6e3 |
Scope and per-item revision
| # | item | rev | pri | surface | verdict |
|---|---|---|---|---|---|
| 1 | identity-auth.session-list-revoke |
1 | P1 | mixed | PASS |
| 2 | identity-auth.api-key-ui-lifecycle |
1 | P1 | mixed | PASS |
| 3 | identity-auth.self-service-profile-password |
1 | P1 | mixed | PARTIAL |
| 4 | identity-auth.org-membership-team-management |
2 | P1 | mixed | PASS |
| 5 | integration-system.job-scheduled-run |
2 | P1 | mixed | PARTIAL |
| 6 | integration-system.notify-inbox-delivery |
2 | P1 | mixed | PASS |
| 7 | integration-system.datasource-credential-refusal-matrix |
1 | P0 | mixed | FAIL |
| 8 | dashboards.strict-widget-rejects-stray-keys |
2 | P1 | mixed | FAIL |
| 9 | dashboards.chart-type-matrix |
3 | P1 | browser | PARTIAL |
| 10 | dashboards.global-filters-rescope |
2 | P1 | browser | FAIL |
| 11 | i18n.notification-localized-and-clears |
3 | P1 | browser | FAIL |
| 12 | i18n.surface-matrix |
3 | P1 | browser | PARTIAL |
| 13 | i18n.strict-translation-key-rejection |
1 | P1 | mixed | PASS |
| 14 | search.pinyin-flag-both-sides |
2 | P1 | mixed | PASS |
| 15 | search.console-global-search |
3 | P1 | mixed | PASS |
| 16 | automation.flow-run-step-nesting |
2 | P1 | mixed | PASS |
| 17 | automation.flow-error-handling |
2 | P1 | mixed | PASS |
| 18 | cli.scaffold-console-first-paint |
1 | P1 | mixed | PASS |
10 PASS / 4 PARTIAL / 4 FAIL.
Per-clause verdicts
1 · identity-auth.session-list-revoke rev 1 — PASS
| clause | verdict | oracle evidence (text) |
|---|---|---|
| c0 owner-scoped list | pass | A(bravo) GET /auth/list-sessions + GET /data/sys_session → 3 rows, all user_id=fUn5OO…; B(charlie) → 2 rows, all user_id=pTunKp…. B's row ids GVz0GATJ…, N5G8bwKM… absent from A's read |
| c1 Account sessions surface | pass | screenshot: /apps/com.objectstack.account/sys_session/view/mine as bravo renders "1 record" with IP Address / Active Organization / Created At / Expires At columns — count matches bravo's post-revoke API list (exactly 1) |
| c2 revoke-session by token | pass | POST /auth/revoke-session {token:<device-2>} → {"status":true}; device-2 GET /data/showcase_task → 401; device-1 → 200 |
| c3 revoke-other-sessions | pass | called from device-1 → list-sessions returns exactly 1 row (device-1's own token); a previously-other token → 401; caller → 200 |
| c4 admin all-users surface | pass | screenshot: Setup → Sessions all_sessions shows a User column with cross-user rows (Dev Admin / QA bravo / QA Charlie) and a Revoked view; API: admin read = 7 rows across 3 users, 2 with revoked_at set and revoke_reason="user_revoked" |
| c5 read-only over data API | pass | PATCH and DELETE /data/sys_session/{id} → OBJECT_API_METHOD_NOT_ALLOWED, allowed:["get","list","aggregate","export"] — as bravo and as admin |
2 · identity-auth.api-key-ui-lifecycle rev 1 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 show-once | pass | POST /api/v1/keys → 201 {id, name, prefix:"osk_0LHT6VwR", key:"osk_0LHT6VwRXOKC…"}; follow-up GET /data/sys_api_key/{id} returns prefix only, grep for the raw secret = 0 |
| c1 authenticates as owner | pass | x-api-key:<secret> on GET /data/showcase_task → 200, 10 rows — identical to the owner's session read (10) and to admin's (10); no superuser widening |
| c2 revoke immediate | pass | PATCH /data/sys_api_key/{id} {revoked:true} → 200; next x-api-key call → 401 |
| c3 restore reversible | pass | {revoked:false} → 200; next x-api-key call → 200 |
| c4 mine-view scoping | pass | charlie's sys_api_key read = 0 rows, key id absent; admin all_keys read contains it |
| c5 dialog + list render | pass | screenshot of Setup → Connect an Agent after a ref-targeted "Create key" (native setter + input/change on the name field): panel reads "For CI, scripts, and agents without a browser. The key acts as you — treat it like a password. It is shown ONCE." and "Key "qa-console-mint" created — copy it now, it will not be shown again:" with the copyable secret and the x-api-key: header form. Re-opened all_keys list shows prefix-only rows |
3 · identity-auth.self-service-profile-password rev 1 — PARTIAL
| clause | verdict | oracle evidence |
|---|---|---|
| c0 name persists | pass | POST /auth/update-user {name} → {"status":true}; GET /auth/get-session → "name":"Dev Admin QA2C"; after a hard reload the profile card still renders "Dev Admin QA2C" |
| c1 avatar is a real storage write | pass | network trace: POST /storage/upload/presigned → 200 → PUT /storage/_local/raw/<token> → 200 → POST /storage/upload/complete → 200 {fileId:"efa8853a-…"} → POST /auth/update-user → 200; get-session.image = "/api/v1/storage/files/efa8853a-…"; rendered <img src> equals it. Not a data: URL |
| c2 avatar remove | pass | profile-avatar-remove-btn → POST /auth/update-user → 200; get-session.image = null; zero <img> on the card, initials "DA" fallback returns |
| c3 password rotation | pass | change-password (correct current) → 200; sign-in with new → 200; sign-in with old → 401 |
| c4 wrong current refused server-side | pass | change-password with a wrong current → 400 {"code":"INVALID_PASSWORD"}; the old password still signs in (200) → credential unchanged |
| c5 passwordless set-initial | blocked(fixture) | stock showcase ships no configured IdP, so no hasLocalPassword=false user exists — exactly the item's own knownGap |
Also observed (c6-adjacent, no clause): the profile card renders "Email cannot be changed." with the email input disabled.
4 · identity-auth.org-membership-team-management rev 2 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
c0 {current_org_id} resolves |
pass | the Setup nav emits /_console/apps/com.objectstack.setup/sys_organization/record/org_msy0avxsf5eiqdj6 — the record page, not the list; screenshot shows Members (3) / Invitations / Teams (1) tabs. Cross-checked: get-active-member and get-full-organization both return org_msy0avxsf5eiqdj6 |
| c1 role writes and bites | pass | update-member-role bravo→admin → membership read shows admin; bravo's organization/update → 200. Demote to member → same call → 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION |
| c2 closed vocabulary | pass | role guest → 400 {"code":"ROLE_NOT_FOUND","message":"ROLE_NOT_FOUND: guest"}; stack-declared position contributor → same; delegated_admin → accepted. Follow-up list-members shows no stray row |
| c3 rename persists | pass | organization/update {name:"QA Renamed Org"} → 200; get-full-organization.name = "QA Renamed Org"; the org record page header follows |
| c4 remove shrinks access | pass | charlie before: get-full-organization 200 / list-members 200 / get-active-member 200. After remove-member: 403 / 400 / 400 with {"code":"NO_ACTIVE_ORGANIZATION"} |
| c5 team join rows | pass | create-team → team V1ZCv5iv…; two add-team-member calls → GET /data/sys_team_member = exactly 2 rows, pairs (V1ZCv5iv…, fUn5OO…) and (V1ZCv5iv…, pTunKp…) |
| c6 gate both ways | pass | as a non-admin member, all five forged calls refused server-side — update-member-role 403, remove-member 403, update 403, create-team 403, add-team-member 403 — and org name + all three roles unchanged after. UI side: the same member loading the org record page gets "App not available" and zero management affordances |
5 · integration-system.job-scheduled-run rev 2 — PARTIAL
| clause | verdict | oracle evidence |
|---|---|---|
| c0 all three schedule variants fire | blocked(fixture) | interval ✓ flow-schedule:showcase_scheduled_digest run_count=75, last_status=success, with its own sys_job_run rows and real side effects (75 sys_inbox_message digest rows). once ✓ flow-wait:run_e97bd614…:hold run_count=1. cron ✗ showcase_health_sweep is registered with schedule_type=cron, schedule_expression="0 1 * * *", timezone=UTC but run_count=0 — its next fire is outside the session window. The item prescribes near-term scratch jobs in a writable package; the runtime job door is closed (c5), so that needs source + redeploy |
| c1 durable name-keyed truth | pass | sys_job row {name:"showcase_health_sweep", id:"job_58a9d9a1-…"} — keyed by name, adapter-minted row id, schedule persisted (schedule_type/schedule_expression/timezone). Counters move: run_count 75 / 15, last_run_at, last_status=success. sys_job_run rows carry {job_name, status, started_at, completed_at, duration_ms, attempt, trigger} |
| c2 retry ladder | blocked(fixture) | needs a deliberately-throwing scratch job; runtime authoring closed |
| c3 timeout status | blocked(fixture) | same |
| c4 two loud skip doors | blocked(fixture) | neither skip line appears in the boot log (grep -ic "job handler not found|skipping" = 0) because the stock showcase has no disabled job and no handler-missing job at this pin — the #4774/#4888 regression is fixed. Observing the lines needs authored specimens |
| c5 closed doors refuse loudly | pass | runtime create: PUT /meta/job/qa_runtime_job → [not_creatable] Metadata type 'job' is code-only: the metadata-type registry declares allowRuntimeCreate=false and allowOrgOverride=false … Declare it in source (**/*.job.ts) and redeploy. An operator may set OS_METADATA_WRITABLE=job …. Parse door (JobSchema from @objectstack/spec/system): id → "job.id was removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) … name IS the job's identity everywhere"; stray wibble → named; aliases → cron→schedule, interval→schedule, fn→handler, each with a did-you-mean. Valid control accepted |
6 · integration-system.notify-inbox-delivery rev 2 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 delivery | pass | after reassigning showcase_task/5BiNGEhsVjoH0UeB to bravo, bravo's GET /notifications → {id:"kq18I3z8jXVFhSnD", type:"task.assigned", title:"New task assigned: Warehouse schema", body:"You have been assigned \"Warehouse schema\".", actionUrl:"/showcase_task/5BiNGEhsVjoH0UeB"} |
| c1 recipient-scoped | pass | charlie's authenticated listing → {"notifications":[],"unreadCount":0} |
| c2 mark-read persisted per receipt | pass | POST /notifications/read {ids:[…]} → {readCount:1}; ?read=false relist → empty; sys_notification_receipt as bravo → {notification_id:"kq18…", channel:"inbox", state:"read"} (was delivered) |
| c3 both sides of the gate | pass | anonymous GET /notifications → 401 {"code":"UNAUTHENTICATED"}. (The 501 capability-unavailable half was not exercised — the clause makes that side optional/citable from a minimal boot.) |
| c4 unimplemented channel dead-letters | pass | scratch flow with channels:['push'] triggered → zero sys_inbox_message rows for the recipient (bravo still holds only the task.assigned row) and sys_notification_delivery gained exactly one row channel="push", state="dead" against 13 inbox/success rows. No fabricated inbox row |
| c5 backing rows reflect surface | pass | as bravo: sys_inbox_message 1 row {notification_id:"kq18…", topic:"task.assigned"}, sys_notification_receipt 1 row keyed (notification_id, user_id, channel:"inbox"). RLS-scoped — admin's read of the same tables shows only admin's 11 rows |
7 · integration-system.datasource-credential-refusal-matrix rev 1 — FAIL (P0)
| clause | verdict | oracle evidence |
|---|---|---|
| c0 every inline door refuses | pass | 6/6 doors, each refusing with the offending key/position named and external.credentialsRef prescribed: config-key (config.password → "password is a credential and is not accepted inline in driver config (#7990)"), URL userinfo ("this url embeds a password in its userinfo … (#8082)"), URL query param ("carries ?password= in its query string … (#8337)"), mongo driver-options (config.options.auth.password → "not accepted in the driver-options passthrough (#9040)"), placeholder (${DB_PASSWORD} → same config-key refusal), connector inline authentication (PUT /meta/connector → "must not inline secrets via authentication … drop authentication (or set { type: 'none' })") |
| c1 credentialsRef path works | pass | the external/credentialsRef-shaped datasource published {success:true} with status:"ok" on the connect probe |
| c2 both read doors redact | pass | full-body grep of GET /api/v1/datasources, GET /api/v1/datasources/:name and GET /api/v1/meta/datasource for all 10 planted cleartext values → 0 hits on both doors. Shapes agree (both drop the key rather than masking) |
| c3 presence-preserving | pass | credential SET → hasSecret:false, redactedConfigKeys:["encryptionKey"]; credential UNSET → redactedConfigKeys:[]. Distinguishable |
| c4 legacy alias spellings | pass | planted passwd / pwd / authtoken through the metadata door (structural validation, as the knownGap predicts) and passwd through the datasource-admin door on an unknown driver. Metadata door serves config:{host:…} only; admin door reports redactedConfigKeys:["passwd"]. All three planted values: 0 hits on both doors |
| c5 unknown driver redacted by name | pass | driver cockroachdb (no shipped contract) with config.password → admin door config:{host:"h.qa.example"}, redactedConfigKeys:["password"]; metadata door config:{host:…}. Value absent from both |
| c6 echoed-mask write guard | pass | read the datasource, PATCH the response back unmodified → 200; post-round-trip read still reports redactedConfigKeys:["encryptionKey"] and the stored sys_metadata row still contains the credential (grep count 1). Live connect probe unavailable — @objectstack/driver-turso is not installed — so the "still works" half used the stored-credential oracle |
| c7 redaction is not a privilege artifact | fail | access-control finding — detail withheld pending maintainer; held by the runner. Item integration-system.datasource-credential-refusal-matrix, clause c7. Already filed as #9391; independently re-confirmed this round. No endpoint, method, payload or reproduction published here. |
Note on encryptionKey (turso): write-accepted + read-redacted on both doors — the correct expectation per #8081 scope item 4, not a write-refusal failure. Separately observed and not a clause failure: the still-writable encryptionKey is stored in cleartext in sys_metadata, which the ordinary data API serves — but that path is admin-gated (PERMISSION_DENIED for a member, UNAUTHENTICATED for anonymous) and is exactly the "stays stored" behaviour datasource-credential-redaction.ts documents.
8 · dashboards.strict-widget-rejects-stray-keys rev 2 — FAIL
| clause | verdict | oracle evidence |
|---|---|---|
| c0 all 11 legacy keys rejected, key named | pass | object, categoryField, categoryGranularity, valueField, aggregate, aggregation, rowField, columnField, xAxisField, yAxisFields, measures — 11/11 rejected, each error containing the literal key name |
| c1 error prescribes the ADR-0021 shape | pass | "The pre-ADR-0021 inline analytics shape (object + categoryField + valueField + aggregate, pivot rowField/columnField) was removed — bind a dataset and select dimensions + values by name. Renderer-only settings belong under options." |
| c2 hallucinated key | pass | chartFlavour → "Unrecognized key(s) on this dashboard widget: chartFlavour." — key echoed, no did-you-mean offered (honestly absent, no near match). Quarantined component/data and drill near-keys drillDown/drilldown/drill also rejected with their own prescriptions |
c3 wrong-layer keys inside chartConfig |
pass | chartConfig.dataset → "dataset is not part of the chart config — it is the dashboard widget's own key (ADR-0021), a sibling of chartConfig, not a key inside it."; chartConfig.drillDown → "it is a REACT-TIER prop, written beside objectName/aggregate: <ObjectChart objectName=… drillDown={…} />"; aggregate and objectName likewise named |
| c4 rejected draft not persisted | pass | after every rejected PUT …?mode=draft, GET /meta/dashboard/qa_stray_key_probe returns a bare lock descriptor with no widgets — nothing carrying the stray key |
| c5 corrected widget renders | fail | the corrected widget (dataset: showcase_task_metrics, dimensions:['status'], values:['task_count']) published {success:true, version:"sha256:319d0221…", seq:3} and then renders "Dashboard Not Found". See R-1 below |
9 · dashboards.chart-type-matrix rev 3 — PARTIAL (matrix: 16 of 20 variants verified)
Ground truth first (/api/v1/data): 11 tasks · 5 distinct statuses · 4 distinct priorities · 2 month buckets · 5 projects across 3 accounts.
| clause | verdict | oracle evidence |
|---|---|---|
| c0 each of 20 variants renders | blocked(product-bug → R-1) | 16 verified per-variant in the Chart Gallery (screenshot first, DOM after). 4 unreachable: kpi/gauge/solid-gauge/bullet need a scratch dashboard, which R-1 makes unrenderable |
| c1 mark counts match buckets | pass | bar Tasks by Status 5 recharts-rectangle paths = 5 statuses · column Tasks by Priority 4 = 4 priorities · horizontal-bar Hours by Status 5 bars (screenshot) · line Tasks Created (monthly) recharts-curve over 2 month buckets · area idem (curve + fill) · pie Status Split 5 recharts-sector · donut Priority Split 4 sectors · funnel Status Funnel 5 recharts-trapezoid · radar Priority Radar recharts-polar-grid + recharts-polygon over 4 priorities |
| c2 combo = mixed marks, dual axes | pass | Task Count vs Avg Progress carries both recharts-rectangle and recharts-curve in one SVG, with two y-axis tick sets rendered (0 2 4 6 8 left, 0 25 50 75 100 right) |
| c3 distinctive geometry | pass | treemap Hours Treemap = 6 nested <rect> (no cartesian grid); sankey Status Flow (Sankey) = recharts-sankey-link with 11 link paths; scatter Estimate vs Progress = its own cartesian point plot. None falls back to a bar |
| c4 four single-value synonyms | blocked(product-bug → R-1) | qa_single_value_gallery (kpi/gauge/solid-gauge/bullet) published {success:true, seq:4} → "Dashboard Not Found" |
| c5 pivot cross-tab | pass | Tasks by Status × Priority renders a real table: headers Status | Low | Medium | High | Urgent | Total, 5 status rows + Total. 3-cell spot check vs the API: Done×Medium=3 (API done=3), Backlog×Low=1, grand Total=11; the Total row 1,5,3,2 equals the API priority buckets {low:1, medium:5, high:3, urgent:2} exactly |
| c6 coverage ratchet | pass | examples/app-showcase/test/coverage.test.ts → 1 file, 13 tests passed |
| c7 compareTo renders | blocked(product-bug → R-1) | qa_compareto_probe published {success:true, seq:5} → "Dashboard Not Found" |
| c8 converged compareTo pinned at the parse | pass | packages/spec/src/ui/dashboard-compareto.test.ts → 1 file, 17 tests passed. Confirmed live through the dashboard metadata root too: {kind:'previousPeriod'} accepted; the bare string → "dashboard.widgets[].compareTo: 'previousPeriod' (the bare string form) was removed in @objectstack/spec 17.0.0 (#5011)…"; {offset:'7d'} → "dashboard.widgets[].compareTo.offset was removed in @objectstack/spec 17.0.0 (#5011, ADR-0049 …)" |
Enum re-read as the item's step 0 requires: ChartTypeSchema has exactly 20 members — the variants list is current.
10 · dashboards.global-filters-rescope rev 2 — FAIL
| clause | verdict | oracle evidence (network trace) |
|---|---|---|
| c0 region re-scopes per widget's own field | pass | selecting EMEA re-issues 9 POST /analytics/dataset/query; invoice widgets carry {"region":"emea"}, account widgets {"sales_region":"emea"} — per-widget field mapping in one trace |
| c1 dateRange through each object's own date field | pass | invoice widgets {"issued_on":{"$gte":"2026-05-20","$lte":"2026-08-18"}}; account widgets {"signed_on":{…}} |
| c2 opted-out KPI is real | pass | "Accounts (all time)" issues {"measures":["account_count"]} with no runtimeFilter on load and is absent from all re-query sets; its value stays 14 across region and reset changes |
| c3 displayed aggregate = server truth | pass | under EMEA + last-90-days the Invoices tile shows 1; the identical direct query returns {"invoice_count":1} with SELECT COUNT(*) … WHERE ((issued_on >= $1 AND issued_on < $2) AND region = $3) |
| c4 prospects excluded by date scope | pass | 14 accounts, 3 with signed_on = null; a wide signed_on window returns 11 = 14 − 3; unfiltered returns 14 |
| c5 composition on Delivery Operations | pass | with global task_status=in_progress, the "Awaiting Review" tile's query is $and[{status:"in_review"}, $and[{created_at:…},{status:"in_progress"}]] and the tile goes 2 → 0; the direct aggregate with both filters returns {"task_count":0} and with the tile filter alone {"task_count":2} |
c6 Studio inspector authors filterBindings |
fail | see R-1 — the authored binding does not persist and does not drive the widget |
| c7 Reset is dirty-gated | pass | pristine bar exposes only the two filter buttons (no Reset); after selecting APAC the bar reads Last 90 days | APAC | Reset; clicking Reset restores Last 90 days | Region: All, fires 9 re-queries in the default (region-free) shape, and the Invoices tile returns to 7 while the opted-out tile stays 14 |
c8 {value,label} options + server GROUP BY |
blocked(fixture) | {value,label} half proven: the trigger renders the LABEL "EMEA" while the committed value on the wire is "emea". The server-GROUP-BY half has no fixture — both revenue-pulse.dashboard.ts and ops-dashboard.dashboard.ts declare static inline options arrays, and opening the select fired 0 analytics requests. No dataset-sourced option list exists on stock showcase |
11 · i18n.notification-localized-and-clears rev 3 — FAIL
| clause | verdict | oracle evidence |
|---|---|---|
| c0 title is the flow-authored literal | pass | flow showcase_task_assigned_notify cfg.title = 'New task assigned: {record.title}'; on a zh-CN recipient session the bell entry reads exactly New task assigned: Warehouse schema, character for character. Session locale confirmed zh by the surrounding chrome (消息中心 / 全部标记为已读 / 未读 / 38秒钟前 / 查看全部通知). No raw {record.title} |
| c1 deep-link target renders zh-CN | fail | see R-3 — the click-through never reaches the record |
| c2 mark-as-read clears | pass | ref-targeted click inside the bell popover fired POST /api/v1/notifications/read {"ids":["enFXX2Z6ZecLsZtH"]} → 200 {"readCount":1} |
| c3 cleared state is authoritative | pass | after a full page reload the re-opened panel reads 已读完所有通知 with an empty 未读 tab — the server persisted it |
| c4 title is recipient-INDEPENDENT | pass | the same event notified an EN-session recipient (charlie) with the identical string New task assigned: Warehouse schema — no per-recipient divergence, which is the correct (retired-capability) contract |
12 · i18n.surface-matrix rev 3 — PARTIAL
Expectations grounded first in GET /api/v1/i18n/translations/zh-CN (134,700 bytes; top-level groups objects, globalActions, dashboards, pages, messages, apps, metadataForms, settingsCommon, settings).
| clause | verdict | oracle evidence |
|---|---|---|
| c0 every variant renders its known-translated key | blocked(fixture) | 12 of 13 variants verified — nav (setup/account app nav fully zh: 总览/软件包/API 密钥/审批中心/会话/数据源/连接智能体), list headers (标题/项目/负责人/状态/优先级/截止日期/进度/操作), form labels + placeholders (选择…/请选择), select options (待规划/待办/进行中/评审中/已完成; 低/中/高/紧急), view emptyState (see CF-1), _sections (task form 任务信息; contact form 这是谁), dashboard (widget title 任务数与平均进度), page (姓名/邮箱/公司/留言/提交咨询), Settings namespace (本地化 · 区域/格式/财务 · 默认时区/默认语言/日期格式), Studio metadataForms (数据/自动化/界面/权限/发布/保存草稿/记录/表单/验证/钩子), sys_ object (用户 · 身份 · 名称/邮箱/手机号码), feed verb (c10). 1 blocked: action dialog (c5) |
| c1 expectations from server truth | pass | the translation read was captured first and every per-variant expectation cites its key path |
| c2 fallback is honest | pass | untranslated declared strings render their source label (e.g. showcase_task's EN description; the showcase app's own nav labels — apps.showcase_app is legitimately absent from the bundle). A regex scan for dotted key paths across every driven zh surface found zero raw keys |
| c3 option labels localize everywhere | blocked(environment) | proven in 2 of 3 places — list cells (评审中/进行中/高/紧急) and form selects (待规划…已完成; 低/中/高/紧急). The list filter panel did not open under the driver, so the third place is unverified |
| c4 fully-covered list never mixes locales | pass | the showcase_task list header row is 100% zh-CN: 标题 | 项目 | 负责人 | 状态 | 优先级 | 截止日期 | 进度 | 操作 |
| c5 action dialog copy | blocked(fixture) | the only two translated _actions (showcase_recalc_estimate → 重算工时, showcase_recalc_selection → 重算所选) are not reachable from the showcase_task list row menu (编辑/删除/Mark Done/Reassign…/Quick View) or the record page (Mark Done/Log Time/Archive); the actions that are reachable carry no _actions entry and so legitimately render EN |
c6 /i18n/locales envelope |
pass | {"success":true,"data":{"locales":[{"code":"en","label":"en","isDefault":true},{"code":"zh-CN","label":"zh-CN","isDefault":false}]}} — the declared envelope, exactly the configured set, no superset, not a bare array |
c7 /i18n/translations/zh-CN vocabulary |
pass | non-empty; all 9 top-level keys sit inside the declared translationDataShape vocabulary — no fourth dialect |
c8 /labels/:object/:locale agrees with the UI |
pass | the labels response {title:标题, project:项目, assignee:负责人, status:状态(+options), priority:优先级(+options), due_date:截止日期, progress:进度, …} matches the rendered zh list-header row field-for-field |
| c9 unknown locale degrades honestly | pass | /translations/zz-ZZ → 200 {"locale":"zz-ZZ","translations":{}}; /labels/showcase_task/zz-ZZ → 200 {"object":"showcase_task","locale":"zz-ZZ","labels":{}}. Empty maps, no 500, no dotted-key dump |
| c10 feed/audit verb localizes | pass | and it nearly went the other way — see CF-2. With the tenant localization setting locale=zh-CN, the next write produced the activity row 更新了 任务 "Warehouse schema" — the messages.activityUpdated template resolved, object label included |
13 · i18n.strict-translation-key-rejection rev 1 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 every variant rejected at the item door | pass | 9 variants via PUT /meta/translation/<name>, each alongside a valid objects.showcase_task.label. All rejected with the surface named ("Unrecognized key(s) on this translation") and the key echoed: wibble, o, app, nav, dashboard, errors, notifications, validationMessages, _meta |
| c1 same variants rejected at the bundle door | pass | scratch script against @objectstack/spec: defineTranslationBundle (root export) and defineTranslation (via the /system subpath — see CF-3) each throw for all 11 variants with equivalent error content; both valid controls accepted |
| c2 did-you-mean on near misses | pass | field helpText → "Unrecognized key(s) on this field translation: helpText. Did you mean helpText → help?"; dashboard widget label → "Unrecognized key(s) on this widget translation: label. Did you mean label → title?" |
| c3 prescriptions on legacy + retired keys | pass | o → "use 'objects.<object_name>'"; app → "use 'apps.<app_name>'"; nav → "use 'apps.<app_name>.navigation.<node_id>.label'"; dashboard → "use 'dashboards.<dashboard_name>' (plural)"; errors → "has no replacement … Author the message on the rule itself (object.validations[].message)"; notifications → "notifications have no translation group, omit them"; validationMessages → "removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) … author the message on the rule itself"; _meta → "use the top-level 'locale' field" |
| c4 failed save persists nothing | pass | after every rejection GET /meta/translation/qa_t2c_tr → RESOURCE_NOT_FOUND — the valid sibling key was not half-saved |
| c5 corrected document saves and resolves | pass | corrected save → {success:true, seq:1, state:'active'}, and it resolves: /i18n/translations/zh-CN then returned the authored objects.showcase_task.label = 任务(QA已改) and fields.title.help = 帮助; /i18n/labels/showcase_task/zh-CN served the new help too. Deleted afterwards, and the shipped values (任务 / 标题) came back |
14 · search.pinyin-flag-both-sides rev 2 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 flag ON, every latin variant hits | pass | per-variant on /data/<obj>/query: zhangwei→张伟 (1), zw→张伟 (1), ZhangWei→张伟 (1), 张→张伟 (1), huaning→华宁科技 (1), hnkj→华宁科技 (1), 华宁→华宁科技 (1) |
| c1 additive, display-field scoped | pass | clean contrast: keji (科技's pinyin) returns 0 on showcase_contact — whose company field holds 华宁科技 — and 1 on showcase_account, where 华宁科技 IS the display name. Control: wangfang → 王芳 (1) |
| c2 flag OFF splits correctly | pass | after OS_SEARCH_PINYIN_ENABLED=false restart: initials zw → 0, hnkj → 0, wf → 0 (companion gone); 张 → 1 and 华宁 → 1 (source column). Full-pinyin zhangwei/huaning still return 1 — a source-column hit, evidenced by the exact stored values email=zhangwei@huaning.example and website=https://huaning.example / billing_email=billing@huaning.example. See CF-4 |
| c3 OFF is inert end-to-end | pass | plugin roster diff: 48 loaded (ON) vs 47 (OFF); full-text grep of the 6.1 MB OFF debug boot log for "pinyin" → 0 occurrences |
| c4 seeded rows recallable on first boot | pass | every ON-side query above succeeded on a fresh boot with no manual rebuild |
15 · search.console-global-search rev 3 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 one idempotent open path | pass | the header Search button (action:command-palette:open) and Ctrl+K each yield the overlay:command-palette overlay with ?palette=1 on the URL; calling open while open is a no-op |
| c1 palette calls the platform endpoint | pass | wire trace GET /api/v1/search?q=INV-1003&limit=24&objects=… → 200 (searchAll, not a per-object fanout) |
| c2 CJK/pinyin recall reaches grouped hits | pass | zhangwei → GET /api/v1/search?q=zhangwei… → 200 with the 张伟 hit; the palette renders it under the "Contact" object heading (screenshot before DOM) |
| c3 Enter navigates | pass | ref-targeted select → /_console/apps/com.example.showcase/showcase_contact/record/XiTyx8y4Db0CtnoA, and the record page renders 张伟 / 华宁科技 / zhangwei@huaning.example |
| c4 empty input shows recents | pass | with a blank query the overlay renders the "Recently viewed" group and 0 /api/v1/search requests fire |
| c5 RLS parity, both personas | pass | restricted member (signed-up, bound to the contributor position): GET /api/v1/search?q=INV-1003&objects=showcase_invoice → 200 {"hits":[],"totalObjects":1,"totalHits":0} — searched, no hit, no leaked count; the identical admin query returns INV-1003. Positive control that the member's read is not a blanket refusal: the same member's /data/showcase_invoice returns 200 with 0 rows (all 12 seeded invoices are owned by ada/linus/grace) |
c6 /search page, same path + empty state |
pass | /apps/com.example.showcase/search?q=INV-1003 fires GET /api/v1/search?q=…&limit=60 → 200 and renders 1 result for "INV-1003" grouped under Invoice; a no-hit query renders the designed empty state 0 results for "zzzznohitzzz" / No results found / Try adjusting your search terms — no error envelope, no error toast |
A distinct defect surfaced while running this item and fails none of its clauses (the console always scopes its query): the unscoped documented route answers 400 — see R-4.
16 · automation.flow-run-step-nesting rev 2 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 3-item run completes, body ran 3× | pass | POST /automation/showcase_batch_reminders/trigger with 3 tasks → run run_b6640d78… status=completed; steps with nodeId=send_reminder = 3 |
| c1 region tags | pass | the three body steps each carry {parentNodeId:"loop_tasks", regionKind:"loop-body"} with iteration 0, 1, 2 — no two share an iteration; start and loop_tasks carry no parentNodeId |
| c2 designer Runs panel renders a nested tree | pass | the Studio flow-designer Runs panel (/studio/com.example.showcase/automations?surface=flow:showcase_batch_reminders, Runs toggle → expand the 17 ms run) — not the developer Flow Runs page. Measured indentation: start/loop_tasks at x=950, the three send_reminder (NOTIFY) rows at x=974 each prefixed by a ↳ marker at x=912. Screenshot taken before the DOM read |
| c3 tree agrees with the API | pass | 5 rendered steps = 5 API steps; the 3 children grouped under loop_tasks = the 3 API steps tagged parentNodeId=loop_tasks, iterations 0/1/2 |
| c4 empty-collection contrast | pass | trigger with tasks: [] → run run_45d2a21f… status=completed, steps [start, loop_tasks(success)], zero steps with parentNodeId=loop_tasks |
17 · automation.flow-error-handling rev 2 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
| c0 handled run completes, container reads success | pass | run run_ebd1af1b… status=completed, durationMs=7156; the try_catch step guarded_push carries status="success" (by design per #7546 — not a finding) |
| c1 failed try attempts recorded, ordered before catch | pass | steps 2–5 all {nodeId:"push", nodeType:"http", status:"failure", parentNodeId:"guarded_push", regionKind:"try", error:{code:"NODE_FAILURE", message:"http: fetch failed"}}; the catch step {nodeId:"record_failure", regionKind:"catch", status:"success"} sits at index 6, after all four |
| c2 retry ladder countable | pass | retryAttempt values 0, 1, 2, 3 — exactly maxRetries(3)+1, each status=failure. Duration cross-check: durationMs=7156 ≥ the 1 s first backoff, and 1+2+4 = 7 s matches backoffMs:1000, backoffMultiplier:2 |
c3 $error binds into data |
pass | the task after the run: sync_status="failed", sync_error="Node 'push' failed: http: fetch failed" |
| c4 unhandled run fails loudly | pass | the unwrapped probe flow → status="failed" with the run-level error: "Node 'push' failed: http: fetch failed" populated; steps [start(success), push(failure)] |
| c5 both surface in the designer Runs panel | pass | the Runs panel renders the region-labelled tree: SUCCESS start START 0ms / SUCCESS guarded_pu… TRY_CATCH 7.1s / ↳ TRY → 4× FAILURE p… HTTP …ms http: fetc… / ↳ CATCH → SUCCESS r UPDATE_RECORD 101ms. The unhandled probe's run row shows Failed |
| c6 unhandled failure is loud in the server log | pass | ERROR-level lines for trigger-fired unhandled failures, e.g. "ERROR Trigger-fired run of flow 'showcase_declarative_connector_ping' failed (trigger 'record_change') — no caller holds this result and nothing retries the run…" and the same for showcase_task_completed_rest_ping — structurally identical (an outbound call failing outside try_catch). See CF-5 on why an api-triggered probe legitimately produces no such line |
18 · cli.scaffold-console-first-paint rev 1 — PASS
| clause | verdict | oracle evidence |
|---|---|---|
c0 npx os validate on a pristine scaffold |
pass | npx -y create-objectstack@latest qa-console-first-paint → ✓ Environment created!; npm install → 438 packages; npx os validate → exit 0, ✓ Validation passed (79ms), Data: 1 Objects 2 Fields · Runtime: 3 plugins |
| c1 console PAINTS under the documented command | pass | npx os dev --ui → /_console/ 200 and the shell renders: login page reads "Development instance / Sign in with the seeded dev admin: admin@objectos.ai / admin123"; after sign-in the authed home renders ("Working late, Dev Admin", Build an app, Your apps 1, Setup). Not a 404, not a white screen, not a stack trace |
c2 the scaffold's own dev script agrees |
pass | package.json scripts.dev = "objectstack dev" (no --ui). Bare npm run dev → /_console/ 200, Plugins: 35 loaded, banner advertising ➜ Console: http://localhost:3194/_console/. npx os dev --ui → 200, Plugins: 35 loaded, identical painted shell. The two invocations agree |
| c3 the newcomer can complete the visual check | pass | an identity does exist out of the box — the seeded dev admin, printed both in the boot banner and on the login page (see CF-6). Driven end to end: the starter Note object list renders its designed empty state ("Nothing here yet / Create your first record to get started"), the create form renders (Title* / Body), Save lands the record, and the list then shows 1 record with working nav / filter / group / sort affordances |
Honest caveat on c3: the scaffold registers no app of its own (GET /meta/app = setup, account), so its starter object qa_console_first_pai_note has no dedicated nav entry — a newcomer reaches it through the Setup app's object route or Studio.
Reproduction rules
R-1 — Runtime-authored / org-overlaid dashboard metadata is accepted with a 200 receipt and served by no read door (P1)
Fails dashboards.strict-widget-rejects-stray-keys c5 and dashboards.global-filters-rescope c6; blocks dashboards.chart-type-matrix c0/c4/c7. Same shape as the Tier-2B view finding (R-2 in #9453), now evidenced on dashboard.
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3193 -d file:/tmp/qa-wave/t2c-C.db
POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"}
PUT /api/v1/meta/dashboard/qa_repro2
{"name":"qa_repro2","label":"QA Repro 2",
"widgets":[{"id":"w1","title":"Tasks","type":"metric",
"dataset":"showcase_task_metrics","values":["task_count"]}]}
- expected: the dashboard is served back and renders.
- actual: the save returns
{"success":true,"version":"sha256:15b715a1…","seq":1,"state":"active","message":"Saved dashboard 'qa_repro2' (org=org_msy16t4w8wiz56vr, state=active) [seq=1]"}and thenGET /api/v1/meta/dashboard/qa_repro2→{"type":"dashboard","name":"qa_repro2","lock":"none","editable":true,"deletable":true,"resettable":false}— the envelope without theitemkey;GET /api/v1/meta/dashboard→ 4 items (showcase_chart_gallery, showcase_ops_dashboard, showcase_revenue_pulse, system_overview), the authored name absent;- browser
/_console/apps/com.example.showcase/dashboard/qa_repro2→ "Dashboard Not Found — The dashboard "qa_repro2" could not be found. It may have been removed or renamed."
Overlay variant (same defect, drives c6): re-PUT the shipped showcase_revenue_pulse document with widgets[kpi_new_accounts].filterBindings.dateRange changed to "created_at" and widgets[kpi_invoices].filterBindings = {region:false}. The save answers {"success":true,"seq":6,"message":"Saved customization overlay (org=…, state=active) — type=dashboard, name=showcase_revenue_pulse"}, and then the re-read still returns the code values ({dateRange:"signed_on", region:"sales_region"}, and no filterBindings at all on kpi_invoices), and the reloaded dashboard still queries signed_on.
Controls and trap rulings (all measured):
- Positive control, same door:
GET /api/v1/meta/dashboard/showcase_chart_galleryreturns{type, name, item:{…18 widgets…}}— the read door works; it simply does not resolve org-scoped runtime-authored rows. - Storage control: the rows are stored —
sys_metadataholds 3type=dashboardrows,state=active,organization_id=org_msy0avxsf5eiqdj6. - Org confound ruled out: exactly 1
sys_organizationrow and it is the session's active org (get-full-organization.id= the id in the save receipt). - Reproduced: 4 saves across 3 names on instance A, plus a 5th on a fresh boot with a fresh DB and a different org id (instance C) — deterministic, schema-driven, not state-dependent.
automation-input: not applicable — purecurl+ direct navigation; no clicks in the write path.hydration-race: the browser leg settled 6 s and was screenshot-confirmed before any DOM read.stale-console-bundle: the console stamp equals the.objectui-shapin, and the same bundle renders the shipped dashboards correctly in the same session.
Likely owner: framework metadata read path (the same layer as #9453's view finding), not objectui — the API read door is already wrong before the console is involved.
R-2 — (withheld)
integration-system.datasource-credential-refusal-matrix clause c7 — access-control finding — detail withheld pending maintainer; held by the runner. Already filed as #9391; independently re-confirmed this round. No reproduction published.
R-3 — The notification deep link does not reach the record (P1)
Fails i18n.notification-localized-and-clears c1.
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3191 -d file:/tmp/qa-wave/t2c-A.db
POST /api/v1/auth/sign-up/email {"email":"bravo@qa.example","password":"QaWave!2026","name":"QA bravo"}
as admin: PATCH /api/v1/data/showcase_task/<id> {"assignee":"<bravo user id>"}
→ flow showcase_task_assigned_notify fires; the row's actionUrl is "/showcase_task/<id>"
sign in as bravo in the console → open the bell (button aria-label 打开消息中心 / "Open message center")
→ click the notification entry INSIDE the popover (ref-target the entry's own <button> ancestor)
- expected: navigation to the task's record page.
- actual: navigation to
/_console/apps/showcase_app/page/showcase_capability_map— the app's landing page. Direct navigation to the authored shape/_console/showcase_task/<id>lands on the same page. - Positive control:
/_console/apps/showcase_app/showcase_task/record/<id>renders the record page correctly, and in zh-CN (状态 评审中; sections 概览/排期/详细信息; labels 项目/负责人/优先级/截止日期/进度) — so the detail page and its localization are fine; only the deep link is.
Traps ruled out: automation-input — the first attempt was an artifact (a bare text match hit the home page's "Needs your attention" card, not the popover) and was discarded; the recorded run used a ref-targeted click on the popover entry's own <button> ancestor, and that same click provably ran the handler because POST /api/v1/notifications/read fired from it. hydration-race — 7 s settle, screenshot before DOM. shared-browser-tab — own context, absolute origin. wrong-panel — the DOM chain was scoped inside [data-radix-popper-content-wrapper]. Reproduced twice (popover click + direct navigation).
Two candidate homes, for triage not for me to decide: the showcase fixture authors actionUrl: '/showcase_task/{record.id}' (examples/app-showcase/src/automation/flows/index.ts), and the console resolves an app-relative actionUrl to the app root rather than to /apps/<app>/<object>/record/<id>.
R-4 — GET /api/v1/search (unscoped) is 400 whenever pinyin recall is on and a federated object is registered (P1)
Surfaced under search.console-global-search; fails none of its clauses — the console always sends an objects= allowlist that excludes the offending object, so the palette and /search page are unaffected. It bites any caller using the bare documented route.
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3191 -d file:/tmp/qa-wave/t2c-A.db
GET /api/v1/search?q=INV-1003 (admin bearer)
- expected: 200 with hits.
- actual: 400
{"code":"INVALID_FILTER","error":"Filter on '__search' names a column that object 'showcase_ext_customer' has no column for, so the predicate never ran. A filter on a field that does not exist can only match zero records, so the query was refused instead of answered with an empty list. …"}
Isolation and controls:
- Reproduced twice on distinct terms (
INV-1003,Warehouse). - Flag discriminator: the identical request on a boot with
OS_SEARCH_PINYIN_ENABLED=falsereturns 200 with hits. The pinyin search-companion ORs{__search:{$contains:term}}for every searchable object;showcase_ext_customer(external datasource) has no__searchcolumn, and the fail-closed filter guard refuses the whole query rather than skipping that object. - Culprit isolated:
POST /data/showcase_ext_customer/query {"search":"INV"}reproduces the same 400 alone, whilePOST /data/showcase_invoice/query {"search":"INV-1003"}returns 200 with rows. - This is the stock configuration — the showcase declares
supportedLocales: ['en','zh-CN'], so pinyin defaults ON. automation-inputnot applicable (purecurl).
Checklist-accuracy findings
- CF-1 ·
i18n.surface-matrixrev 3 step s6 and its_views.emptyStateknownGap are stale. They instruct the runner to record the emptyState variant asblocked(fixture)citing showcase: no view authors anemptyState, so theobjects._views.<view>.emptyStatetranslation surface has no fixture to test #7714 because "no view in examples/app-showcase authors an emptyState at all". At this pinexamples/app-showcase/src/ui/views/task.view.tsdoes author one on the filteredurgentview (its comment block explicitly cites showcase: no view authors anemptyState, so theobjects._views.<view>.emptyStatetranslation surface has no fixture to test #7714), and the zh-CN bundle carriesobjects.showcase_task._views.urgent.emptyState.{title,message}. Driven this round: with the two urgent rows demoted, the view renders 暂无紧急任务 / 当前没有需要立即处理的事项。任务优先级调整为「紧急」后会出现在这里。 The variant is runnable — remove the waiver and bumprevision. - CF-2 ·
i18n.surface-matrixc10's premise is wrong, and it nearly produced a false resolver FAIL. The clause asserts the feed/audit verb "localizes on the zh-CN session". It does not, and cannot:packages/plugins/plugin-audit/src/audit-writers.ts:1393renders the summary once at write time viaresolveWriteLocale(tenantId, userId)and stores the string, with an EN hardcoded fallback. Switching only the browser session locale changes nothing — a write performed from the zh session still producedUpdated Task "Warehouse schema". Flipping the tenant setting (PUT /api/settings/localization {"values":{"locale":"zh-CN"}}) and writing again produced更新了 任务 "Warehouse schema". Re-word the clause around the write locale. - CF-3 ·
i18n.strict-translation-key-rejectionstep s3 names both bundle-door functions as if they share an entry point.defineTranslationBundleis on the package root (@objectstack/spec);defineTranslationis not — it resolves only through the@objectstack/spec/systemsubpath. A scratch script importing both from the root getsdefineTranslation is not a functionand reads exactly like a missing guard. - CF-4 ·
search.pinyin-flag-both-sidesc2 over-specifies the OFF-side expectation. It says'zhangwei'must no longer return 张伟 with the flag off. On the stock fixture it still does — via the source columnemail = zhangwei@huaning.example, and likewisehuaningon the account viawebsite/billing_email. The initials forms (zw,hnkj,wf) do go to zero, which is the real discriminator. Re-target the clause at the initials, or add a pinyin-only term with no source-column spelling. - CF-5 ·
automation.flow-error-handlingstep s4 prescribes an api-triggered probe, whose failure c6 then cannot observe. The ERROR line exists precisely because "no caller holds this result" — for a manually/api-triggered run the caller does hold it (the trigger response carries the error), so no ERROR line is written and the log stays clean. Score c6 from a trigger-fired specimen (this round usedshowcase_declarative_connector_pingandshowcase_task_completed_rest_ping), or have the probe fire from a record-change trigger. - CF-6 ·
cli.scaffold-console-first-paintfixture note "no seeded admin unless one is requested" is stale. At this pin bareobjectstack devseeds one automatically on an empty DB and prints🔑 Dev admin: admin@objectos.ai / admin123in the banner, and the console login page repeats it. c3's open question ("is there any identity a newcomer can sign in with?") has a definite answer: yes. - CF-7 ·
identity-auth.session-list-revokec4 describesrevoke_reasonas a closed vocabulary. It names{idle_timeout, absolute_max, concurrent_cap, admin}, butsys-session.object.ts:154declares free text whose description lists those only as examples ("…, …"). The observed value afterrevoke-other-sessionsisuser_revoked— legitimate, not a violation. Soften the clause or declare the select. - CF-8 · Correction to the wave's standing bearer-token fact (tracking: full platform-checklist regression against main @ e4e5c6e3 — five-round orchestration #9296 briefing / QA run · tier2b:browser-1 (21/21) · e4e5c6e3 · 2026-08-18 · 7 PASS / 12 PARTIAL / 2 FAIL #9453 item 1). "The console authenticates by bearer token in
localStorage, not by cookie" is true for the data client but incomplete: a real form sign-in also sets abetter-auth.session_tokencookie, and at least one route family requires it. Driving with a localStorage token alone madePOST /api/v1/storage/upload/presignedanswer 401 AUTH_REQUIRED whileGET /auth/get-sessionon the same page returned 200 — a convincing fake "avatar upload is broken". With a real form sign-in the whole chain is 200. Runners driving any storage/upload surface must sign in through the form.
Fixture gaps
- FG-1 — No passwordless/SSO-only user on stock showcase →
self-service-profile-passwordc5 staysblocked. (Restates the item's own knownGap; unchanged at this pin.) - FG-2 — Job probes are unauthorable at runtime.
jobisallowRuntimeCreate=false, allowOrgOverride=false, so the interval/once/failing/timing-out/disabled/handler-missing scratch jobsjob-scheduled-runprescribes need source + redeploy. Four of its six clauses (c0 cron arm, c2, c3, c4) are blocked on this alone. Candidate: ship the specimens inexamples/app-showcase/src/automation/jobs/behind an env flag. - FG-3 — No dataset-sourced dashboard filter anywhere in the showcase. Both
revenue-pulseandops-dashboarddeclare static inlineoptionsarrays, soglobal-filters-rescopec8's server-GROUP-BY half has nothing to observe (opening the select fires 0 requests). One filter sourced from a dataset would close it. - FG-4 — The two translated
_actionsonshowcase_task(showcase_recalc_estimate,showcase_recalc_selection) are not surfaced on the list row menu, the list toolbar, or the record page, soi18n.surface-matrixc5 (action confirm/success copy) has no reachable specimen. Either surface one of them, or add an_actionstranslation for an action that is reachable (Mark Done / Reassign… / Log Time / Archive are all EN with no bundle entry). - FG-5 — No turso driver in the tree (
@objectstack/driver-tursonot installed), sodatasource-credential-refusal-matrixc6's live connect probe after the mask round-trip had to fall back to a stored-row oracle. - FG-6 — The showcase app declares no
apps.showcase_apptranslation entry, so thenav labelsvariant ofi18n.surface-matrixis only demonstrable on the platformsetup/accountapps. Correct as fallback behaviour, but it means the app-authored nav surface is untested. - FG-7 — No seeded persona owns any
showcase_invoicerow, so a member bound tocontributorsees 0 of 12 rather than a partial slice.console-global-searchc5 still passes (invisible row absent, no leaked count, object provably searched), but a sharper "sees 11, not INV-1003" contrast needs one seeded invoice owned by a signup-able persona.
Derived observations (no clause failed)
translationhas the mirror-image asymmetry of R-1, and it is benign: a runtime-authored translation is served by the i18n resolver (/i18n/translations/:localeand/i18n/labels/:object/:localeboth returned the authored values) but not byGET /meta/translation/<name>or the/meta/translationlisting, which returnRESOURCE_NOT_FOUND/ 0 items. Worth a look alongside R-1 since both are org-scoped runtime metadata read paths.- Boot diagnostics carry the known Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) #8686 autonumber tenancy split (
showcase_field_zoo.f_autonumber,organizationCount: 0) — WARN level, already filed, fails no clause here.
Close-out inputs for the PM
Per this round's brief the run record carries qa-run + tests only; defect extraction is the PM's close-out duty. Cards this round warrants:
- R-1 — runtime-authored/org-overlaid
dashboardmetadata never served. Very likely the same root cause as QA run · tier2b:browser-1 (21/21) · e4e5c6e3 · 2026-08-18 · 7 PASS / 12 PARTIAL / 2 FAIL #9453's R-2 (view); consider one card covering both metadata types. - R-3 — notification deep link does not reach the record.
- R-4 — unscoped
GET /api/v1/search400s whenever pinyin recall is on and a federated object is registered. - R-2 — already filed as The datasource-admin HTTP family is reachable unauthenticated, read and write — mounts outside the enforceAuth seam #9391; nothing to extract, nothing published here.
CF-1…CF-8 and FG-1…FG-7 close out through this wave's anchor card (#9296), not as separate cards.
Wave anchor: #9296.
Activity
docs(qa): land the #9296 wave's item corrections and the console-sess…
os-zhuang commentedon Aug 18, 2026
Close-out — extracted. Wave #9296, executed under #9480. Four-way FAIL disposition applied to this record's own evidence; nothing re-judged, nothing re-run.
FAIL disposition (4 FAILs + 1 clause-less defect)
1 ·
integration-system.datasource-credential-refusal-matrixc7 → (a) product defect, access-control → #9391, linked not refiled. Existence and clause only; no endpoint, payload or reproduction is restated here or anywhere else on GitHub, per the disclosure ruling at the top of #9296. The item's other seven clauses pass on this round's own evidence.2 ·
dashboards.strict-widget-rejects-stray-keysc5 and 3 ·dashboards.global-filters-rescopec6 → (a) one product defect → #9454, linked not refiled. Both are the runtime-authored / org-overlaiddashboardread-door failure (R-1) — accepted with a 200state:'active'receipt, served by no door, rendered as "Dashboard Not Found". The card was filed from Tier-2B'sviewfinding and widened todashboardwith this round's evidence: 5 saves, 4 names, 2 boots including a fresh DB with a different org id, with the storage, read-door and org confounds each ruled out by a control. It also blockschart-type-matrixc0/c4/c7, which are recordedblocked(product-bug)rather than as separate failures — correctly, and no card is owed for them.4 ·
i18n.notification-localized-and-clearsc1 → (a) product defect → #9468, linked not refiled. The bell entry marks read correctly (POST /notifications/read→ 200) and then navigates to the app landing page instead of the record. Both candidate homes named in this record — the showcase fixture'sactionUrl: '/showcase_task/{record.id}'and the console's app-relative resolution — are carried on the card; the routing decision is triage's, not the runner's. If triage rules it a console resolution defect, the fix re-homes toobjectstack-ai/objectuiunder the file-at-destination rule; the card says so.5 · R-4, no clause fails → (a) product defect → #9469, linked not refiled. Unscoped
GET /api/v1/searchanswers 400INVALID_FILTERon the stock configuration: the pinyin search companion ORs{__search:{$contains:…}}across every searchable object and the federatedshowcase_ext_customerhas no__searchcolumn, so the fail-closed guard refuses the whole query. Isolated by a flag flip and by the culprit object reproducing it alone. Recorded honestly as failing no clause — the console always sends anobjects=allowlist, sosearch.console-global-searchispassand the bare documented route is what breaks.PARTIAL gaps and corrections — no cards, by rule
The 8 checklist-accuracy findings (CF-1…CF-8) and 7 fixture gaps (FG-1…FG-7) are parked on the wave anchor per the PM's 2026-08-18 comment. Two standing wave facts this round proved wrong are already corrected on #9296 — the bearer-token fact (a real form sign-in also sets a
better-auth.session_tokencookie, and the storage/upload chain needs it; driving with a localStorage token alone manufactures a convincing fake "avatar upload is broken"), and thei18n.surface-matrixfeed-verb premise (the activity verb is rendered once at write time from the tenant locale, so the reader's session locale cannot change it). Both near-misses were caught by the rule-2 self-check, which is the protocol earning its cost twice in one round.Extracted: #9454 #9468 #9469
Parked-on: #9296
Generated by Claude Code
docs(qa): land the objectstack-ai#9296 wave's item corrections and th…