Repository navigation
QA run · tier2b:browser-1 (21/21) · e4e5c6e3 · 2026-08-18 · 7 PASS / 12 PARTIAL / 2 FAIL #9453
Description
Tier-2B runner for the platform-checklist QA wave — 21 unpinned P0/P1 items on the browser / mixed surfaces, the half no automation touches. Every one of the 21 carries a verdict. Links #9296.
Environment fingerprint
| framework sha (subject, pinned) | e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd — never moved; git log e4e5c6e3c..HEAD = 0 commits |
.objectui-sha |
665661ab093263f39f2e660a295ea615dbcee35a |
| console dist stamp | 665661ab093263f39f2e660a295ea615dbcee35a — equals the pin; objectui:build/objectui:refresh never run |
| tree | /home/user/objectstack-qa-wave (detached at the pin) |
| spec (checklist) | re-synced from origin/main at start (83fe945d6..1b93550af); validator: 15 areas, 190 items (190 active), 30 kinds mapped, 0 waived |
| instances | A: localhost:3161 / file:/tmp/qa-wave/t2b-a.db · B: localhost:3162 / file:/tmp/qa-wave/t2b-b.db (both rm -f'd before boot) |
| personas | admin admin@objectos.ai · member qa.member.t2b@example.com provisioned in-run via POST /auth/sign-up/email (id i8CaANTg7X…) |
| browser | playwright-core 1.62.1 with executablePath=/opt/pw-browsers/chromium-1194/chrome-linux/chrome |
| liveness | curl /api/v1/health only — ss never used |
| concurrency | max 2 instances; no test suite ever run beside a build |
| final state | both servers stopped; probe metadata/packages removed; git status shows only docs/qa/ paths; 0 commits |
Scope + per-item revision + derived verdict
| # | item | pri | surface | rev | verdict |
|---|---|---|---|---|---|
| 1 | platform-core.boot-health |
P0 | mixed | 2 | pass |
| 2 | platform-core.console-login |
P0 | browser | 2 | pass |
| 3 | platform-core.builtin-apps-nav-render |
P0 | browser | 1 | partial |
| 4 | platform-core.settings-hub-roundtrip |
P1 | mixed | 2 | partial |
| 5 | platform-core.package-lifecycle-enable-disable |
P1 | mixed | 1 | partial |
| 6 | platform-core.notification-center |
P1 | mixed | 1 | partial |
| 7 | records-forms.crud-roundtrip |
P0 | browser | 3 | FAIL |
| 8 | records-forms.form-view-gallery |
P1 | browser | 1 | partial |
| 9 | records-forms.record-discussion-mentions |
P1 | browser | 1 | partial |
| 10 | studio-authoring.first-run-loop |
P0 | mixed | 1 | pass |
| 11 | studio-authoring.view-authoring-live |
P1 | mixed | 1 | FAIL |
| 12 | studio-authoring.authoring-validation-not-persisted |
P1 | mixed | 2 | partial |
| 13 | approvals.account-app-entry |
P0 | browser | 1 | pass |
| 14 | approvals.per-group-signoff |
P1 | browser | 4 | partial |
| 15 | approvals.inbox-metadata-actions |
P1 | browser | 3 | pass |
| 16 | approvals.decision-action-matrix |
P1 | mixed | 2 | pass |
| 17 | access-security.audit-log-browser |
P1 | mixed | 1 | pass |
| 18 | access-security.owd-save-gate |
P1 | mixed | 1 | partial |
| 19 | access-security.public-form-intake |
P1 | mixed | 1 | partial |
| 20 | access-security.permission-matrix-edit-loop |
P1 | mixed | 1 | partial |
| 21 | access-security.capability-declaration-lifecycle |
P1 | mixed | 2 | partial |
7 PASS / 12 PARTIAL / 2 FAIL / 0 not-run items. No access-control finding — the #9296 carve-out was not triggered.
Per-clause verdicts
Oracle evidence is text only. Screenshot oracles are recorded as a one-line description of what the screenshot showed; no images are attached or linked anywhere.
1 · platform-core.boot-health — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 health 200 in boot window | pass | curl /api/v1/health → 200, {"status":"ok","uptime":10.52}; first 200 at ~10s from launch |
| c1 ready 200 | pass | curl /api/v1/ready → 200 |
| c2 no ⚠ flows / no ERROR lines | pass | banner Flows: 29 flow(s) 22 bound to triggers · 7 draft — no ⚠; zero ERROR-level lines in the 46-line boot log; Seeds: com.example.showcase 130 rows, no SeedLoader rejection |
| c3 console shell + app metadata | pass | /_console/ → 200 text/html 6467 B; merged app metadata resolves via ?id=showcase_app (see checklist finding CF-1) |
| c4 served nav == authored nav | pass | 58 served nav ids vs 58 authored in src/ui/apps/index.ts; zero diff in either direction; grp_data serves all 12 objects |
Recorded observation: the boot log carries a ⚠ Boot diagnostics — 3 warnings banner — #8686 autonumber tenancy split on showcase_field_zoo.f_autonumber (already filed), plus two SHARING_NOT_ENABLED rule-backfill warns. All WARN, no ERROR; the clause as written is satisfied.
2 · platform-core.console-login — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 login lands in shell | pass | screenshot showed the ObjectOS home shell — "Working late, Dev Admin", "Your apps 2", nav + header; URL /_console/home |
| c1 session survives reload | pass | post-reload trace: /api/v1/runtime/config 200, /auth/get-session 200, /i18n/locales 200, no redirect to login |
| c2 expired session → 401 from the SERVER | pass | after POST /api/v1/auth/sign-out (200): GET /api/v1/data/showcase_account?$top=1 → 401 UNAUTHENTICATED; GET /api/v1/meta/app?id=showcase_app → 401 UNAUTHENTICATED |
| c3 console returns user to login | pass | navigation after true expiry redirected to /_console/login?redirect=%2Fhome; screenshot showed the "Sign in to your account" card — not a dead shell |
| c4 re-auth restores | pass | re-login → /_console/home, POST /auth/sign-in/email 200, zero 401s in the post-login trace, fresh GET /data/showcase_account 200, new bearer differs from the old |
| negative (step 8) | pass | wrong password → 401 {"code":"INVALID_EMAIL_OR_PASSWORD"} |
3 · platform-core.builtin-apps-nav-render — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 every destination renders (3 apps) | blocked(fixture) | Studio app absent (see FG-1). Setup 43/43 and Account 7/7 destinations walked screenshot-first then DOM-checked: 50/50 ok, zero pageerror, zero blank <main>, zero "failed to load"/placeholder text |
| c1 served nav == authored shell (3 apps) | blocked(fixture) | Setup serves all 9 authored group anchors (group_overview/apps/people_org/access_control/approvals/configuration/diagnostics/integrations/advanced) filled to 43 leaves; Account serves Profile + Inbox/Security/Developer (10 ids). Studio unavailable |
| c2 member refused Setup+Studio, Account opens | blocked(fixture) | Setup: absent from the member's /meta/app; /apps/setup root falls back to the member's own app; Account opens and renders their Profile. Studio leg unavailable |
| c3 app gate is server-side | pass | forged member GET /api/v1/meta/app?id=setup → {"type":"app","items":[]} — the server prunes, it is not a launcher courtesy |
| c4 gated ENTRIES absent from member nav | pass | admin-vs-member diff: nav_api_keys + the 10 nav_settings_* (manage_platform_settings) appear only in the admin set. Named limitation: on this fixture the proof degenerates to app-level pruning — the member holds no setup.access, so no member payload contains a manage_platform_settings entry to prune entry-by-entry |
| c5 service/object gates resolve to ABSENCE | pass | nav_organizations (requiresService: org-scoping) absent in single-org mode; nav_business_units (requiresObject: sys_business_unit) present, object registered |
| c6 off-capability destinations pruned | pass | no nav_sso_providers on stock boot (knownGap holds); no nav_jwks for any persona incl. admin (#7544); sys_verification/sys_device_code absent |
Server-truth control run alongside (member vs admin, direct API): sys_permission_set 403/200 · sys_audit_log 403/200 · sys_capability 403/200 · /api/settings/localization 403/200 · sys_user RLS-scoped 2 rows vs 4. The app-level gate is intact and no privileged dataset leaked.
4 · platform-core.settings-hub-roundtrip — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 value round-trips, source flips | pass | PUT /api/settings/localization {timezone:'Asia/Tokyo'} 200; re-read {"value":"Asia/Tokyo","source":"tenant",...,"cascadeChain":[{"scope":"tenant",...,"effective":true},{"scope":"default","value":"UTC"}]} — default→tenant |
| c1 EFFECT reaches a consumer | blocked(fixture) | no observable consumer surfaced: /api/v1/auth/me/localization reports {timezone:null,locale:null,currency:null} (user scope, not the tenant cascade); the analytics date-bucket route 404s at the path tried; the sanctioned fallback branding.workspace_name persists (source:'tenant') but does not render in the console shell (shell still reads "ObjectOS"). Per the item's own knownGap, effect is not ticked off a persisted form |
| c2 audit row per write | pass | sys_setting_audit row: {namespace:'localization', key:'timezone', action:'set', source:'api', actor_id:'VcatFzcx…', new_hash:true, scope:'tenant'} |
| c3 console source badge flips | pass | screenshot of /apps/setup/system/settings/localization showed the timezone row reading **"Overrides Default |
| c4 env-pinned key is server-authoritative | pass | with OS_LOCALIZATION_TIMEZONE=Europe/Paris: {"value":"Europe/Paris","source":"env","locked":true,"lockedReason":"Set via env: OS_LOCALIZATION_TIMEZONE"}; PUT {timezone:'UTC'} → 409 SETTINGS_LOCKED reason:'locked-by-env' |
| c5 EnvLockBadge + non-editable | pass | screenshot showed "Locked by env" beside the timezone field (and beside mail Password), with the control disabled |
| c6 test action does not fake success | pass | POST /api/settings/mail/test → 400 SETTINGS_ACTION_FAILED, {ok:false, message:"Provide a \"to\" address (or set from_email)."} |
| c7 secret at rest is a handle | pass | sys_secret row sec_998094ee611623a81f6ebef4d2586246 (ciphertext present); sys_setting row {namespace:'mail', key:'smtp_password', value_enc:'sec_998094…', value:null} |
| c8 POSITIVE masked read, both flavours | pass | mail.smtp_password served as exactly •••••••• (8×U+2022); every cascadeChain entry mask-or-null; UNSET keys (smtp_host,smtp_user,api_key,from_email) still null (presence-preserving); no plaintext anywhere in the body. Second flavour ai.openai_api_key: same — mask exact, chain masked, no leak |
| c9 redaction is METADATA-preserving | pass | with OS_MAIL_SMTP_PASSWORD set: {"value":"••••••••","source":"env","locked":true,"lockedReason":"Set via env: OS_MAIL_SMTP_PASSWORD","cascadeChain":[{"scope":"env","value":"••••••••","locked":true,…}]}; PUT → 409 SETTINGS_LOCKED |
| c10 echoed mask is a NO-OP | pass | both shapes ({smtp_password: mask} and {values:{smtp_password:{value:mask}}}) → 200 with sys_secret inventory unchanged (1 row, same id) and sys_setting.value_enc unchanged; a genuinely new value minted a different handle (sec_998094… → sec_72cb82…) and its PUT response is redacted (contains the mask, not the plaintext); non-encrypted control company.legal_name set to the mask stores it verbatim. Named gap: the in-process consumer read (getNamespace, deliberately unmasked) is not reachable over REST, so "still resolves the ORIGINAL plaintext" rests on the unchanged-handle proxy rather than a direct read |
| c11 anonymous denied | pass | anon GET /api/settings/localization → 403 SETTINGS_FORBIDDEN; anon GET /api/settings → 200 with zero namespaces enumerated |
| c12 every namespace resolves | pass | 11 namespaces each 200 with {manifest, values}: branding 7, company 16, localization 12, auth 34, mail 16, sms 15, storage 16, ai 52, knowledge 16, feature_flags 7, lifecycle 5 specifiers. No 500, no empty manifest |
| guardrails (step 14) | pass | {timezone:'Mars/Phobos'} → 400 SETTINGS_VALIDATION naming the IANA requirement; {bogus_key:1} → 400 UNKNOWN_KEY |
5 · platform-core.package-lifecycle-enable-disable — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 disable STOPS serving | pass | probe com.qa.t2bx + published object t2bx_qa_widget: GET /api/v1/data/t2bx_qa_widget 200 → after PATCH /disable (200) → 404 |
| c1 enable RESTORES | pass | after PATCH /enable (200) the same data read is 200 again |
| c2 uninstall DE-REGISTERS | pass | DELETE /api/v1/packages/com.qa.t2bx → 200 {registryRemoved:true, deleted:[{type:'object',name:'t2bx_qa_widget',state:'active'}]}; GET /meta/object/t2bx_qa_widget → 404; the package is gone from GET /packages |
| c3 commits GROW per publish | blocked(fixture) | GET /packages/:id/commits answers 200 {"commits":[]} before and after two successful publish-drafts (delta 0). The route the clause names, POST /packages/:id/publish, answers 200 but reports "No metadata items found for package" for a draft saved with ?package=<id> — so the documented publish path could not be driven. Not scored as a defect: two candidate causes (commit minting bound to a publish path I could not exercise; org-scoped listCommits filtering) were not separated |
| c4 revert restores prior shape | blocked(dependency) | no commits exist to revert (c3) |
| c5 console shows the disabled package gone | not-run | needs a probe package carrying a nav entry; not driven |
| c6 DENY side on read-only code packages | pass | PATCH /disable and DELETE on both com.example.showcase and com.objectstack.setup → 422 WRITABLE_PACKAGE_REQUIRED naming the read-only cause |
| c7 rows survive the toggle | pass | 1 row before disable, 1 row after re-enable |
Recorded observation: after disable, the data door 404s but GET /api/v1/meta/object/<probe_object> still answers 200 — a disabled package's schema stays readable through the metadata door. The clause's own verify (the data/route reads) is met, so this is filed as an observation, not a fail.
6 · platform-core.notification-center — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 badge = distinct unread TOPICS + approvals | pass | bell badge read 5; the popover states it in words: "5 total · 2 notifications + 3 pending approvals". The recurring digest contributes one topic while carrying 19 rows — the row reads "Scheduled project digest ×19 … 19 unread". sys_approval_request my_pending independently = 3. Exactly the #2765 property |
| c1 three tabs | pass | popover tabs: Notifications 2 / Approvals 3 / Activity feed |
| c2 (topic,title) repeats COALESCE with ×N pill | pass | after the screenshot confirmed the popover rendered, the group row carries the pill ×19 with a single collapsed entry. Named gap: the chevron-expand revealing the 19 members was not driven |
| c3 per-group mark-read is ONE request | not-run | only global mark-all was driven |
| c4 mark-all-read drops the notifications portion | pass | captured POST /api/v1/notifications/read/all (200); badge 5 → 3 — notifications portion to 0, approvals portion (3) unaffected |
| c5 Approvals tab count == my_pending | pass | tab reads Approvals 3; direct sys_approval_request my_pending query = 3 |
| c6 read-state PERSISTS across reload | pass | after mark-all + full reload the badge stays 3 (does not revert to 5) — the sys_notification_receipt upsert held |
7 · records-forms.crud-roundtrip — FAIL (c2)
| clause | verdict | oracle evidence |
|---|---|---|
| c0 create + verbatim re-read | pass | console "Create Account" modal → POST /api/v1/data/showcase_account 201 with body {"name":"os-qa-qa872072","industry":"technology","status":"active"}; filtered re-read returns those three verbatim and the untouched optional website is null, not defaulted |
| c1 update persists ONLY the edited field | pass | PATCH {annual_revenue:4242} 200; the record History entry reads exactly "Annual Revenue: — → 4242" and no other field row; full-row reads differ only in annual_revenue + updated_at |
| c2 delete removes the row | FAIL | see reproduction rule R-1 |
| c3 list reflects each mutation after reload | blocked(dependency) | create half verified — post-reload list screenshot showed the new row (Annual Revenue 4,242) and "17 records". The delete half is unprovable because the delete never succeeds (c2) |
| c4 History shows create + update with display values | pass | after the screenshot confirmed the tab rendered: "Dev Admin UPDATE 17 seconds ago — Annual Revenue: — → 4242" and "Dev Admin CREATE 39 seconds ago — ▸ 5 fields populated" — real actor, display values, no phantom value→null rows |
| c5 unicode round-trips | pass | os-qa-qa872072-华宁 created 201, re-read byte-identical; $search=华宁 returns it alongside the seeded 华宁科技 |
| c6 clone re-derives engine columns, owned by cloner | pass | POST /data/showcase_account/<id>/clone 201 {id:'Dj3B405h57_X1ldZ' ≠ sourceId:'sXI6lveJX_4Wv-km', sourceId, record}. Field diff: re-derived = id,created_at,updated_at (+ overridden name); copied = all 19 business columns. Named gap: cloner≠source-owner is untested — admin owns both on this fixture |
| c7 clone is RLS-gated | blocked(fixture) | showcase_account is public_read_write, so no bound persona cannot read the source row — exactly the condition the item says to record blocked(fixture) for |
8 · records-forms.form-view-gallery — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 per-variant layout mechanics | not-run | all five variants exist in metadata (GET /meta/view?object=showcase_task lists showcase_task.edit / .tabbed / .wizard / .split / .quick), but no console door was found that selects a form-view variant — ?formView=<name> is ignored (all five URLs rendered byte-identical detail content, len 378). The modal variant stays blocked(fixture) per the knownGap |
| c1 wizard issues ONE save at the end | not-run | wizard variant not reachable (c0) |
c2 view-level visibleWhen works live |
pass | in the Create Task form, screenshot first, then DOM label read: before priority=urgent labels = [Title*, Project*, Assignee, Status*, Priority, Due Date]; after setting priority=urgent via native setter + input/change = […, Due Date, **Notes**]. Notes mounts on the CEL condition |
| c3 every variant's save persists | not-run | depends on c0 |
| c4 tab/step navigation preserves drafts | not-run | depends on c0 |
9 · records-forms.record-discussion-mentions — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 comment PERSISTS to sys_comment | pass | captured POST /api/v1/data/sys_comment 201 with {thread_id:'showcase_task:4JOOoiUJcrR25Knt', author_id:'EMA74oMclN…', body:'QA t2b probe @QA Member T2B please review', mentions:'["i8CaANTg7XEXXBHKvxO2ZD3TqHjn6bG3"]'}; the API read returns the row |
| c1 optimistic row reconciles by id | pass | across the post→refetch transition the feed shows the body exactly once (Discussion (1), 1 occurrence) — no duplicate, no phantom |
| c2 sys_activity interleaves into the same feed | not-run | an attributable edit + re-read was not driven |
| c3 mentioned user's bell + deep link | not-run | the member session's bell was not opened |
| c4 recipient scoping both sides | not-run | not driven |
Positive observation for the wave: the mention autocomplete works — typing @QA opened the suggestion list with "QA Member T2B", and selecting it resolved the member's real id into the mentions array. The composer's action bar is [Bold, Italic, List, Inline code, Mention someone, Preview, Submit (Ctrl+Enter)].
10 · studio-authoring.first-run-loop — pass (contradicts the wave's standing "blocked")
| clause | verdict | oracle evidence |
|---|---|---|
| c0 package create round-trips, switcher lists it | pass | POST /api/v1/packages {id:'com.example.repairs', label:'维修中心'} → 201, namespace repairs. The App Builder's "MY PACKAGES (WRITABLE)" panel lists com.example.repairs in the same session, no reload |
| c1 published object is server-real with its 3 options | pass | GET /api/v1/meta/object/repairs_repair_ticket → status field type:'select' with exactly [{New,new},{In Repair,in_repair},{Done,done}] |
| c2 record persists to the data plane | pass | POST /api/v1/data/repairs_repair_ticket 201; read back [{"name":"Broken drill","status":"in_repair"}] |
| c3 app live in the launcher, label chip renders | pass | Home launcher screenshot showed "Your apps 3 · Showcase · Setup · Repair Center"; the end-user list at /apps/repairs_repair_center/repairs_repair_ticket rendered **"Broken drill |
| c4 ZERO server restarts | pass | grep -c "Plugins:" boot-b.log = 1 — a single boot spans the whole loop |
| c5 read-only gate is SERVER-side | pass | authoring write into com.example.showcase → 422 writable_package_required: "that package is read-only (provided by code or an installed app), so it is not a writable base" |
This overturns the standing wave fact. #9296 and the R1 record say this item is blocked(fixture) because Studio is not installed. The app registration is indeed absent (GET /meta/app = showcase_app, setup, account) — but the Studio App Builder UI is reachable at /_console/studio and at /_console/apps/setup/component/studio/builder, and Home carries the "Build an app" tile the item's step 2 starts from. The whole loop closes. See CF-4.
11 · studio-authoring.view-authoring-live — FAIL (c3)
| clause | verdict | oracle evidence |
|---|---|---|
| c0 draft receipt with state + version | pass | PUT /meta/view/qa_repair_asset_views?mode=draft → {"success":true,"version":"sha256:06fc841b…","seq":2,"state":"draft"} |
| c1 pre-publish the draft is staged only | pass | while drafted: GET /meta/view?object=qa_repair_asset → [] and direct GET /meta/view/qa_repair_asset_views → 404 — no draft leakage |
| c2 post-publish the list renders authored columns | blocked(dependency) | precondition provably fails (c3); no screenshot taken, so no screenshot verdict is reported |
| c3 published container round-trips through the consumer door | FAIL | see reproduction rule R-2 |
| c4 edit + re-publish updates the live list | blocked(dependency) | same precondition failure |
12 · studio-authoring.authoring-validation-not-persisted — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 invalid object rejected with a LOCATED error | pass | 422 INVALID_METADATA, message [invalid_metadata] object/qa_invalid_probe2 failed spec validation: fields.amount.type: …, issue {path:"fields.amount.type", code:"invalid_value"} |
| c1 wrong-layer view key rejected with guidance | pass | container-level type/columns → 422, text carries the ViewSchema prescription: "Wrap it: defineView({ list: { type, data, columns, … } }), or name it — defineView({ listViews: { my_view: { … } } })" |
| c2 rejected drafts NOT persisted | pass | GET after each rejected PUT → 404 for both names |
| c3 browser door surfaces the same located error | blocked(fixture) | the object designer refuses to edit either candidate: a runtime-authored object reads back as source "Artifact" with "This object is provided by an installed package, so it is read-only at runtime", and no editable Settings panel is offered. Driving an invalid save through the designer needs a draft-state object in a writable package that the designer will open for editing |
| c4 corrected shapes save 200 | pass | corrected object → 200 state:'draft'; corrected view (wrapped under list:) → 200 state:'draft' |
Known spurious _diagnostics banner (knownGap) was not observed this run.
13 · approvals.account-app-entry — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 registration survives BUNDLING | pass | dist/.objectui-sha == .objectui-sha == 665661ab0932…; grep -o 'approvals:inbox' packages/console/dist/assets/index-B1QKp1HQ.js = 1 hit, in context V({ref:'approvals:inbox', label:'Approvals Inbox', source:'@object-ui/console', component:…}) |
| c1 entry renders the FULL inbox | pass | as the member, screenshot of the nav entry's target showed tabs My Pending 1 / Submitted by me / All, a request row (Invoice Dual Sign-off · INV-1010 · Pending) and the keyboard hint bar. Not a component-not-found state. URL /_console/apps/account/component/approvals/inbox |
| c2 a NON-ADMIN reaches it | pass | that session's GET /api/v1/meta/app = ["showcase_app","account"] — no setup — and the same session renders the inbox |
| c3 decision actions render and one moves the request | pass | member drawer rendered the server-declared set [Approve, Reject, Reassign, Send back, Request info, Close]; the approve executed and the timeline gained approve by i8CaANTg7X… (member); the request left the member's pending slate |
c4 ?request= deep-links into the drawer |
pass | cold-load /apps/account/component/approvals/inbox?request=areq_2439498e… opened the drawer on that exact request — screenshot showed "INV-1010 … Waiting on QA Member T2B ×2" |
| c5 record link keeps the CURRENT app segment | pass | href = /_console/apps/**account**/showcase_invoice/record/GsufOSAcr7Nar-c- — the mounting app, never setup |
| c6 zh-CN wording consistent | pass | with locale zh-CN, screenshot showed nav entry 待我审批, tab 待我审批, status badge 待审批 (page chrome 审批中心 / 我发起的 / 全部) |
14 · approvals.per-group-signoff — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 both groups 0/1, two distinct pending users | pass | EXP-2001 areq_cba953dc…: status:'pending', pending_approvers:['EMA74oMclN…','usr_showcase_auditor_demo'], pending_approver_groups:{admin:['manager'], auditor:['finance']}, decision_progress:{behavior:'per_group', got:0, need:2, groups:[{finance,0/1,false},{manager,0/1,false}]} |
| c1 manager satisfied and dropped, still pending on finance | pass | after the admin's approve: pending_approvers:['usr_showcase_auditor_demo'], decision_progress:{got:1, need:2, groups:[{finance,0/1,false},{manager,1/1,**satisfied:true**}]}, status still pending |
| c2 drawer group chips are server-computed | pass | drawer screenshot read **"Sign-off progress — 1 of 2 groups |
| c3 finance approval finalizes; run resumes | blocked(fixture) | the finance holder Ada Auditor is a sys_user row with no better-auth credential account (the item's own knownGap). Path used and its outcome recorded: the documented reassign action moved the slot to the provisioned member (pending_approvers:['i8CaANTg7X…']) and that member's approve returned 200 — but the finance tally stayed 0/1 and the slate re-resolved to Ada, so the group was never satisfied. Request remained pending; run remained paused at dual_signoff. Admin override is not a path: can_override:true does not let the admin act for another group |
| c4 timeline: one approve per group, distinct actors, comments | pass | 4 rows: submit(usr_showcase_phone_demo), approve(admin, "QA t2b finance-group via admin override"), reassign(admin, …), approve(member, "QA t2b finance-group approval by reassigned persona") — distinct actors, comments round-tripped |
| c5 server derives the actor from the session | pass | stronger than the clause expects: posting {actorId:'usr_showcase_auditor_demo'} is refused 403 FORBIDDEN — "cannot act as 'usr_showcase_auditor_demo' — an approval action is recorded against the authenticated caller". The build rejects the forged hint outright rather than ignoring it |
Recorded observation for the wave: a reassign to a user outside the node's group yields a 200 approve that satisfies no group and silently re-resolves the slate back to the group's real holder. Coherent with per-group semantics (a group needs one approval from that group), but the 200 gives the actor no signal that their decision counted for nothing. No clause covers it; recorded, not scored.
15 · approvals.inbox-metadata-actions — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 approver-side actions render (other-submitter) | pass | EXP-DEMO (submitted by Mei Phone) drawer: [Approve, Reject, Reassign, Send back, Request info, Close] |
| c1 submitter-side actions ONLY on the own request | pass | own pending request (areq_06008af9…, submitter = admin): […, **Send reminder**, **Recall**, Close]; the other-submitter drawer shows neither. Side-by-side confirmed |
| c2 rendering mirrors the server viewer flags | pass | three data points: {can_act:true, is_submitter:true} → approver and submitter groups render; {can_act:true, is_submitter:false} → approver group only; a finalized request renders [Close] only — the declared record.status gate |
| c3 each action executes its REST route | pass | remind click path → POST /api/v1/approvals/requests/:id/remind 200, status stays pending |
| c4 every executed action appends one timeline row | pass | before/after GET /:id/actions: delta +1, ['submit','remind'] |
16 · approvals.decision-action-matrix — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 all nine actions execute + exactly one timeline row each | pass | approve ✓ · reject ✓ · revise ✓ · resubmit ✓ · recall ✓ · reassign ✓ · remind ✓ · request-info ✓ · comment ✓. The three non-finalizing ones measured +1 row each (cumulative delta 1, 2, 3) |
| c1 decisions finalize per APPROVAL_STATUSES | pass | reject → status:'rejected', run completed, steps start:success → manager_review:success; approve advances the node (manager approve opened exec_review) |
| c2 revise → returned, run parked at wait_revision | pass | status:'returned'; run paused, steps start → manager_review → **wait_revision:success** |
| c3 resubmit MINTS a new round request | pass | after the submitter's resubmit: a new areq_0596255b… status:'pending' on the same flow_run_id, freshly resolved approvers |
| c4 the resubmitted row is TERMINAL at returned | pass | GET /:id on the original still reads status:'returned' after the new round opened |
| c5 maxRevisions guard holds | pass | clean chain: round 1 revise→returned, resubmit→new; round 2 revise→returned, resubmit→new; round 3 revise → status:'rejected' (auto-rejected). Final chain = 2 returned siblings + 1 rejected — exactly maxRevisions: 2 |
| c6 recall is submitter-only | pass | non-submitter (member) → 403 FORBIDDEN "only the submitter may recall this request"; submitter → 200, status:'recalled' |
| c7 non-finalizing actions mutate only what they own | pass | reassign swapped the slate to the target (pending_approvers:['i8CaANTg7X…'], source removed); remind/request-info/comment each left status:'pending' and added one row. Bonus: a second immediate remind → 429 (throttle) |
| c8 lockRecord enforced on BOTH sides | pass | under exec_review (lock_record:true): PATCH /data/showcase_project/<id> → 409 RECORD_LOCKED "is locked while an approval is in progress"; under manager_review (lock_record:false): the same PATCH → 200 |
| exec no-revise gate (step 12) | pass | POST /:id/revise at exec_review → 400 VALIDATION_FAILED "approval node 'exec_review' has no 'revise' out-edge — the flow does not support send-back for revision" |
Named gap: resubmit-by-a-non-submitter was not attempted, so that half of c3's "refused for anyone but the submitter" is unproven.
17 · access-security.audit-log-browser — pass
| clause | verdict | oracle evidence |
|---|---|---|
| c0 three ops produce correctly attributed rows | pass | delete on showcase_task with record_id:'0M3CY0CC4DYXilAE' + actor; config_change on sys_setting + actor; login rows attributed to a user id with object_name:'sys_session' |
| c1 the browser renders those same rows | pass | screenshot confirmed the table painted, then DOM read: `["1 |
| c2 filter narrows server-side | pass | the "Writes" view re-issued GET /api/v1/data/sys_audit_log?…&filter=[["action","in",["create","update","delete"]]] — the narrowing rides the request, and the table then showed only write actions. Named deviation: the console exposes prebuilt filtered views rather than the item's "Action = delete" dropdown; server-side narrowing is proven, the exact one-value selection is not |
| c3 drawer shows Before/After JSON panels | pass | drawer screenshot showed "…ACTION update · ACTOR EMA74oMclN… · AUDIT LOG ID koWHGrKm7UAa1sb3 · More details · OLD VALUE [Object] · NEW VALUE [Object]" — two separate before/after panels, not a merged semantic diff |
| c4 API twin reconciles with the page | pass | the four page rows correspond 1:1 to GET /api/v1/data/sys_audit_log bodies; no page row lacks a backing API row |
| c5 trail is append-only | pass | POST /api/v1/data/sys_audit_log → 405; PATCH /api/v1/data/sys_audit_log/<id> → 405; no forged row appears in a follow-up list |
18 · access-security.owd-save-gate — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 Studio flags external-wider inline | blocked(fixture) | the object designer opens runtime-authored objects read-only ("provided by an installed package, so it is read-only at runtime", source "Artifact") and offers no editable Settings panel, so isExternalWider could not be driven in the browser |
| c1 server refuses external > internal | pass | with a corrected oracle — see CF-2. sharingModel:'private' + externalSharingModel:'public_read' is refused 422 INVALID_METADATA / rule security-external-wider-than-internal: "externalSharingModel 'public_read' is WIDER than the internal sharingModel 'private' … (ADR-0090 D11)", at path: objects[96].externalSharingModel. Reproduced on both the draft→active publish and a direct active PUT. The 403 owd_external_wider the clause names no longer exists — retired by the #8310 ruling, and draft-state saves defer by design |
| c2 server admits external ≤ internal | pass | qa_owd_probe3 with public_read / private → 200 state:'active', read back sharingModel:'public_read', externalSharingModel:'private' |
| c3 packaged OWD cannot be widened | pass | full-body PUT on showcase_announcement widening both → 403 owd_widening_forbidden: "an environment overlay may only TIGHTEN a packaged object's OWD … (sharingModel 'public_read_write' > declared 'public_read'; externalSharingModel 'public_read' > declared 'private')". Answering layer recorded: R1 (the plugin-security authoring gate), on a STOCK deploy — not WRITABLE_PACKAGE_REQUIRED, contradicting the item's knownGap (CF-3) |
| c4 the denied widening is inert | pass | GET /meta/object/showcase_announcement before vs after the refused PUT: byte-identical (JSON.stringify equality), sharingModel:'public_read', externalSharingModel:'private' unchanged |
| c5 the shipped app passes its own gate | pass | showcase_account external public_read ≤ internal public_read_write ✓; showcase_announcement external private ≤ internal public_read ✓ |
19 · access-security.public-form-intake — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 resolved schema is the whitelist only | pass | anon GET /api/v1/forms/contact-us → 200, objectSchema.fields keys = exactly ["name","email","company","message"]; owner_id/organization_id/status/source/created_by all absent |
| c1 forged anchors stripped on submit | pass | submitted owner_id:'usr_victim', organization_id:'org_victim', created_by:'usr_victim'; landed row reads owner_id:null, organization_id:'org_msxwsg97x4ia76nh' (the server's real org), created_by:null. Observation: owner_id lands null rather than a server-stamped owner — the forge is defeated either way |
| c2 non-whitelisted business fields dropped | pass | submitted status:'closed', source:'forged'; landed status:'new' (seeded default), source:'web' (server-stamped) |
| c3 inquiry lands under publicFormGrant | pass | anon POST …/submit → 201; staff read returns the row with name/email/company/message persisted |
| c4 non-whitelisted lookup refused | pass | anon GET /api/v1/forms/contact-us/lookup/owner_id → 403 LOOKUP_NOT_PUBLIC — "Field "owner_id" is not enabled for public lookup on this form" |
| c5 submitBehavior thank-you renders | blocked(environment) | the anonymous browser page could not be located on this build: GET /forms/contact-us (the publicLink in inquiry.view.ts) returns {"error":"Not found"} and renders no form, while the API door /api/v1/forms/contact-us serves correctly. The public-page route needs identifying before this clause is runnable |
| c6 unpublish kills the link both ways | not-run | not driven |
Also passed (step 8, no clause): a __proto__-nested owner_id/status smuggle landed nothing inherited — the row came back with owner_id:null, status:'new'.
20 · access-security.permission-matrix-edit-loop — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 baseline withheld verb denied | pass | member DELETE /api/v1/data/showcase_task/<id> → 403 PERMISSION_DENIED "You do not have permission to perform this action" — the object-verb denial |
| c1 matrix edit saves and publishes | pass (substantive half) | PUT /api/v1/meta/permission/qa_t2b_set → 200 state:'active', and the published set reads back objects.showcase_task.allowDelete: true. Route deviation: POST /api/v1/meta/permission/<name>/publish answers 404 — the publish sub-route the clause names is not mounted for permission (it is for view and object); the PUT lands active directly (CF-5) |
| c2 access flips ON after publish | blocked(fixture) | across four matrix states M's DELETE never reached 2xx. The gate did move: with allowDelete:false the refusal is "You do not have permission to perform this action" (verb), with allowDelete:true it becomes "You do not have access to this record" (record level) — including on a task created with owner_id = M. Could not establish an effective end-to-end grant of a runtime-authored set on this fixture; a control granting a shipped delete-bearing set was not run |
| c3 revoke flips it back | blocked(dependency) | c2 never reached the ON state |
| c4 assign/unassign moves access | blocked(fixture) | the grant row was accepted (POST sys_user_permission_set {user_id, permission_set_id:'ps_msxzisf0v1jy79ti'} → 201) and removed (200), but no observable access change bracketed it |
| c5 editor reflects the published state | not-run | the matrix editor DOM was not read |
Useful mechanics recorded: the permission-set matrix key is objects, not objectPermissions — the server's guidance error says so outright ("Unrecognized key(s) … Did you mean objectPermissions → objects? Until #4001 these were dropped silently"). The grant field is permission_set_id and it wants the sys_permission_set row id (ps_…), not the metadata name; a runtime-authored set is projected into that table (ps_msxzisf0v1jy79ti | qa_t2b_set).
21 · access-security.capability-declaration-lifecycle — partial
| clause | verdict | oracle evidence |
|---|---|---|
| c0 declared capability materializes at boot | pass | sys_capability (10 rows) carries {name:'showcase.export_data', label:'Export Showcase Data', description:'Bulk-export showcase records (accounts, invoices) to CSV/XLSX.', scope:'org'} matching examples/app-showcase/src/security/capabilities.ts; showcase.restricted_ops likewise |
| c1 the three-way resolves on the platform action path | blocked(fixture) | no reachable invoke surface exposes the specimens. REST: POST /api/v1/actions, /api/v1/actions/<name>, /api/v1/data/zoo/actions/<name> all 404 — correct per #7680. MCP: the bridge is up (initialize 200, 11 tools incl. run_action), but run_action refuses all four at an earlier gate — "Action 'showcase_zoo_perm_held' on 'showcase_field_zoo' is not exposed to AI — the app author must opt it in with ai: { exposed: true, … }" — identically for admin and member, so the AI-exposure gate shadows the capability gate. In-process ql.executeAction is not reachable from an HTTP client. See FG-2 |
| c2 platform-name shadowing refused LOUDLY | pass (oracle downgraded to test) |
the guard's boot seeder cannot be driven without authoring a scratch package into the app source, which this wave forbids. Cited instead: packages/plugins/plugin-security/src/bootstrap-declared-capabilities.test.ts at the pin → 23/23 passed, including "refuses to hijack a curated platform capability" and "CURATED platform name: suppresses, and the curated pass seeds the row regardless" |
| c3 write door validates before the upsert | blocked(fixture) | same constraint — needs a scratch package carrying a malformed capability plus a boot; no runtime metadata door reaches upsertPackageCapability |
Recorded: the four REQUIRE-side specimens exist and are correctly declared — GET /api/v1/meta/action (70 actions) lists showcase_zoo_perm_held (requiredPermissions:['showcase.export_data']), _missing (['showcase.restricted_ops']), _and (both), _empty ([]), all type:'script' on showcase_field_zoo. OpsPermissionSet grants showcase.export_data via systemPermissions as documented.
Reproduction rules
R-1 — records-forms.crud-roundtrip c2 · DELETE is refused for any object targeted by a multiple: true reference
Not a new defect. Already twice-derived and reported this wave — #9296 comment (R1) and #9351 (R3). Confirmed a third time here, and for the first time through the console door.
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3161 -d file:/tmp/qa-wave/t2b-a.db
POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"}
# create through the console: Accounts -> New -> name/industry/status -> Create (POST /api/v1/data/showcase_account -> 201)
DELETE /api/v1/data/showcase_account/<id>
- expected: 200, row gone.
- actual: 400 — "The bare equality spelling { "f_lookups": value } WAS NOT APPLIED: "f_lookups" is a multi-value (or otherwise JSON-valued) field, stored by this driver as a JSON TEXT column … Use "$contains" for membership" — and
GET /api/v1/data/showcase_account/<id>still answers 200.
Reproduced ×2 on distinct fresh ids in this run (sXI6lveJX_4Wv-km, eIvHMCjKKc0AvJ1R). Control ruling out "DELETE is broken": DELETE /api/v1/data/showcase_category/<id> → 200 on the same boot, same session.
R-2 — studio-authoring.view-authoring-live c3 · a runtime-authored view is accepted with a 200 state:'active' receipt but is served by no read door
New defect. P1. Not an auth/authz finding — a metadata read-path gap, so the recipe is published in full.
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p 3162 -d file:/tmp/qa-wave/t2b-b.db
POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"} # use the bearer token it returns
# 1. baseline — the read door works for shipped views
GET /api/v1/meta/view?object=showcase_task -> 200, 20 items
# 2. author a view container over the SAME object
PUT /api/v1/meta/view/qa_t2b_task_views
{"name":"qa_t2b_task_views","object":"showcase_task",
"list":{"type":"grid","columns":["name","status"]}}
-> 200
{"success":true,"version":"sha256:8f18ae7d…","seq":5,"state":"active",
"message":"Saved view 'qa_t2b_task_views' (org=org_msxx3cuqkidc7f5c, state=active) [seq=5]"}
# 3. read it back
GET /api/v1/meta/view?object=showcase_task -> 200, still 20 items; 'qa_t2b_task_views' ABSENT
GET /api/v1/meta/view/qa_t2b_task_views -> 404 RESOURCE_NOT_FOUND
GET /api/v1/meta/view -> 200, 35 items; 'qa_t2b_task_views' ABSENT
- expected: the published/active container is served by
GET /meta/view?object=…(thegetViewsByObjectconsumer door) and by its direct name. - actual: accepted, versioned, sequenced — and invisible to every read door. An authored view can never render.
The ?mode=draft → POST …/publish path behaves identically: draft receipt {state:'draft', seq:2}, publish 200 {"message":"Published draft — type=view, name=qa_repair_asset_views [seq=3]"}, then the same 404 / [] / absent-from-listing triple.
Scope measured, not assumed — 4 saves, 3 names, 2 objects, 2 independent boots:
| host / db | name | door | object | save | read-back |
|---|---|---|---|---|---|
| 3162 / t2b-b.db | qa_repair_asset_views |
draft → publish | runtime-authored qa_repair_asset |
200 / 200 | 404, ?object= [] |
| 3162 / t2b-b.db | qa_asset_views2 |
direct active PUT | runtime-authored qa_repair_asset |
200 | 404, ?object= [] |
| 3162 / t2b-b.db | qa_t2b_task_views |
direct active PUT | shipped showcase_task |
200 | 404, listing 20→20 |
| 3161 / t2b-a.db | qa_t2b_a_views |
direct active PUT | shipped showcase_task |
200 | 404, listing 20→20 |
Traps and confounds ruled out
stale-console-bundle— n/a: every oracle is a direct HTTP call to the runningobjectstack dev; no console, no DOM.hydration-race— n/a: no DOM oracle anywhere in this finding.dispatcher-vs-hono-route— the oracle is the live server, not a simulated dispatch.- automation self-check — plain
fetchwith a Bearer header; no coordinate clicks, no React controlled inputs. The server's own 200 receipt is the acceptance evidence. - read door broken? No — the same request in the same session returns the 20 shipped
showcase_taskviews. - org-scoping mismatch? No —
sys_organizationholds exactly 1 row,org_msxx3cuqkidc7f5c, andGET /api/v1/auth/organization/get-full-organizationresolves that same id as the session's active org: the write org and the read org are identical. - writability? RUNNER's standing environment fact says
viewis in the overlay-allowed set, so a 2xx write is expected and observed. The failure is on the read side, after an accepted write.
Because c3 fails, c2 and c4 (the post-publish rendering clauses) are recorded blocked(dependency) — no screenshot was taken and no screenshot verdict is claimed.
Checklist-accuracy findings (for the wave anchor, not extraction)
- CF-1 —
?id=on/api/v1/meta/appkeys on the app NAME.?id=com.example.showcasereturns{"items":[]}even authenticated;?id=showcase_appreturns the merged app. Re-confirmed;boot-healthstep 6 / c3 still carry the package-id spelling. (Already known from R1.) - CF-2 —
owd-save-gatec1 names a retired refusal. The item (rev 1) expects 403owd_external_wideron a?mode=draftPUT. That gate was retired by the [3 of #7891] Flip the registration:runtimeTypesgainsobject+permission/book— whole rule families cross the publish door #8310 maintainer ruling (2026-08-13) —object-posture-gate.tssays so in its header, and draft saves defer to the draft→active promotion by design. The live refusal is 422INVALID_METADATA, rulesecurity-external-wider-than-internal, raised at the active-state door. Item needs the code/status/door corrected andrevisionbumped. - CF-3 —
owd-save-gate's knownGap mispredicts which layer answers. It says R1owd_widening_forbiddenis reachable only viaOS_METADATA_WRITABLE=object, and that a stock deploy answersWRITABLE_PACKAGE_REQUIREDfirst. On this stock deploy R1 answered directly with 403owd_widening_forbidden. - CF-4 —
first-run-loopis NOT blocked on stock showcase. tracking: full platform-checklist regression against main @ e4e5c6e3 — five-round orchestration #9296 finding 4 and R1 record itblocked(fixture)because Studio is not installed. The app is indeed unregistered (meta/app=showcase_app, setup, account), but the App Builder UI is reachable at/_console/studioand/_console/apps/setup/component/studio/builder, Home carries the "Build an app" tile, and the whole loop closes (item 10 above: 6/6 clauses pass). The standing fact should be narrowed to "the Studio app registration is absent" and the item taken off the blocked list. - CF-5 —
permission-matrix-edit-loopc1 names a route that 404s forpermission.POST /api/v1/meta/permission/<name>/publish→ 404; the same sub-route works forview(200) and is mounted forobject. ThePUTlandsstate:'active'directly, so the publish step is unnecessary here — but the clause requires a 2xx from it. - CF-6 —
capability-declaration-lifecyclenames the wrong object. The fixture block says the four specimens are "type:'script' on thezooobject"; they are onshowcase_field_zoo. An invoke keyed onzooreturns "Action … not found on object 'zoo'", which reads like a missing specimen. - CF-7 —
settings-hub-roundtripsays 10 open-framework namespaces; the build serves 11.GET /api/settingsenumeratesbranding, company, localization, auth, mail, sms, storage, ai, knowledge, feature_flagspluslifecycle(5 specifiers). - CF-8 —
console-loginstep 5's expiry gesture is wrong for this console. "clear the auth cookies for the origin" does not expire the session: the console authenticates with a bearer token inlocalStorageunderauth-session-token, sent as anAuthorizationheader (requests carryauthHdr=true, cookie=NO; the only cookie,better-auth.session_token, is not used by the console's own client). Clearing cookies leaves a fully valid session, and the shell then legitimately renders authed data — which reads exactly like the "dead shell rendering stale data" failure c3 warns about. This run nearly filed that false P0; the automation self-check caught it. The step should say: sign out server-side (POST /api/v1/auth/sign-out) or clearlocalStorageas well. - CF-9 —
audit-log-browserc2's "Action filter" control does not exist as described. The console offers prebuilt filtered views (Recent / Writes / Auth / Config) plus a generic "Filter Records / Where / Add filter" builder, not an Action dropdown. Server-side narrowing is provable through the Writes view; the clause should name the actual control.
Fixture gaps
- FG-1 — the three platform apps are not all present on a stock boot.
builtin-apps-nav-render's fixture block asserts Setup/Studio/Account "register as one-app packages … present on any stock boot, no showcase-specific fixture". Studio is not served (GET /meta/app?id=studio→{"items":[]}for admin and member alike), so three of the item's seven clauses cannot be completed. Either the fixture must install@objectstack/studioor the item must scope itself to the apps a stock boot actually registers. - FG-2 — the capability REQUIRE-side specimens are unreachable on every surface this build boots. REST mounts no action-invoke route by design (capability-declaration-lifecycle: no shipped resource enforces a package capability server-side (no REST action-invoke route on this build) #7680, confirmed 404). The MCP
run_actionbridge refuses them at theai.exposedopt-in gate — before the capability check and identically for every persona — so the ADR-0066 D4actionPermissionErroris never reached. Making c1 runnable needs eitherai: { exposed: true }on the fourpredicate-matrix.action.tsspecimens, or a build that mounts the dispatcher/actionsdomain. - FG-3 — no credentialed persona holds the
financegroup.per-group-signoffc3 stays blocked: Ada Auditor (usr_showcase_auditor_demo) is a routablesys_userwith no better-auth credential account, and neithercan_overridenorreassignsubstitutes — reassigning the slot to an out-of-group user produces a 200 approve that satisfies nothing. The item's own knownGap names the fix (provision the account with issuerlocal:credential); it remains unprovisioned on stock. - FG-4 — no non-reading persona for
showcase_account.crud-roundtripc7 (clone RLS refusal) cannot be driven: the object ispublic_read_write, so no bound persona is unable to read the source row. - FG-5 — the object designer will not open a runtime-authored object for editing. Both
owd-save-gatec0 andauthoring-validation-not-persistedc3 need the Studio designer's Settings/save path, but a published runtime object reads back as source "Artifact" with "This object is provided by an installed package, so it is read-only at runtime". A draft-state object in a writable package that the designer opens editable is the missing fixture. - FG-6 — the public form's browser page route is unknown.
public-form-intakec5 needs the anonymous rendered form; thepublicLinkvalue/forms/contact-usanswers{"error":"Not found"}while/api/v1/forms/contact-usserves correctly. - FG-7 — package commit history could not be exercised.
GET /packages/:id/commitsanswers{"commits":[]}after two successfulpublish-drafts, andPOST /packages/:id/publishreports "No metadata items found for package" for drafts saved with?package=<id>.package-lifecyclec3/c4 need whichever publish path actually mints commits.
Other observations (no clause covers these)
- A disabled package's schema stays readable. After
PATCH /packages/:id/disable,GET /api/v1/data/<obj>→ 404 butGET /api/v1/meta/object/<obj>→ 200. reassignacross a per-group boundary is silently ineffective. Detailed under item 14.- Boot diagnostics carry 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 on both instances:
showcase_field_zoo.f_autonumberruns two autonumber counters (globalLastValue:2, organizationLastValue:0, organizationCount:0). WARN-level, already filed, fails no clause. - Playwright confirmed again:
executablePath=/opt/pw-browsers/chromium-1194/chrome-linux/chromeis required; nothing in this run contradicts the standing note. POST /auth/sign-up/emailyields a genuinely loginable second persona — used throughout for every both-sides gate proof here. Confirms the R3 note.
Activity
os-zhuang commentedon Aug 18, 2026
ACCEPT — reviewer of record: PM loop (QA wave #9296), session
session_01Ai9gUdihXjg7FumzsBNSSs.21/21 scored. 7 PASS / 12 PARTIAL / 2 FAIL / 0 not-run.
The part that matters most: this round corrected the PM
I had written into #9296's standing-facts block that
studio-authoring.first-run-loopwas unscoreable because "Studio is not installed", and the dispatch brief told this round to recordblockedand move on. The runner checked anyway, found the App Builder UI reachable at/_console/studio, and drove the full loop: 6/6 clauses pass, zero restarts. Scoredpass.That is the right call and the right instinct. The claim was an inference I propagated from an earlier round without measuring, and left standing it would have cost every future round a P0 item. #9296's body now carries the entry struck through rather than deleted, plus a new rule above the list: inherited facts are briefing material, not verdicts — if your evidence contradicts one, follow your evidence. A wrong fact left visible teaches that rule; a quietly deleted one teaches nothing.
Verified independently
The new defect's acceptance half is expected behaviour, which is what makes the read half anomalous:
viewcarriesallowOrgOverride: trueinpackages/spec/src/kernel/metadata-plugin.zod.ts, unlikeobject/field/hook/seedwhich arefalse. So the 200 receipt is correct and the missing read door is the defect. Extracted to #9455 with the controls intact — shipped-view control ruling out a broken read path, org-scoping confound ruled out (onesys_organizationrow, and it is the session's active org), 4 saves × 3 names × 2 objects × 2 boots.The false P0 that was caught rather than filed
After
clearCookies()the console rendered a fully authed shell — which reads exactly like the item's own c3 failure mode, "dead shell rendering stale data as if authed". The self-check found the console authenticates by bearer token inlocalStorage, so clearing cookies expired nothing; true expiry viaPOST /auth/sign-outbehaves correctly (401 on the wire, redirect to login). The item's step 5 is wrong for this build, which is a checklist defect, not a platform one.This is the single most valuable thing a browser round produces. A filed P0 here would have consumed a triage cycle, a dev dispatch, and a retraction — and it would have looked completely convincing on the way in.
Carried into close-out
records-forms.crud-roundtripc2 — thef_lookupsDELETE outage, third reproduction and the first through the console door, with ashowcase_categoryDELETE-200 control. Already filed as DELETE is broken for any object targeted by amultiple: truereference — 400 INVALID_FILTER and the row survives (regression since #8895) #9390; no new card.publishsub-route that 404s forpermission, capability specimens onshowcase_field_zoonotzoo, 11 settings namespaces not 10, the wrong step 5 above) join the consolidated ledger-edit PR — now 26 corrections across four rounds, one docs-only change withrevisionbumps.first-run-loopshould also lose any inherited assumption in the ledger itself, not just on this card.Tier-2C (final 18 unpinned P0/P1 browser/mixed) dispatched. When it lands, every unpinned P0/P1 item in the ledger has a verdict.
Generated by Claude Code
os-zhuang commentedon Aug 18, 2026
Correction to the ACCEPT above: the extracted defect card is #9454, not #9455. Wrong number, right card — "a runtime-authored
viewis accepted with a 200state:'active'receipt but is served by no read door".Generated by Claude Code
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 (2 FAILs)
1 ·
records-forms.crud-roundtripc2 → (a) product defect → #9362, linked not refiled. Thef_lookupsDELETE outage, confirmed here a third time and for the first time through the console door, with theshowcase_categoryDELETE 200 control. This round correctly filed no new card. #9362 carries an open fix PR (#9437).2 ·
studio-authoring.view-authoring-livec3 → (a) product defect → #9454. A runtime-authoredviewaccepted with a 200state:'active'receipt and served by no read door. Extracted at review time from this record; linked, not refiled. The card's scope was widened the same day when Tier-2C #9467 found the identical shape ondashboard— kept as one card deliberately, so one mechanism does not get two half-fixes. Worth noting the earliest sighting: #9353 recorded aPUT /meta/view200 followed by a 404 read and declined to score it, on thin evidence, as a fail; this round supplied the missing half (published/active state, 4 saves, 3 names, 2 objects, 2 independent boots, org and read-door confounds ruled out).Both fixes land in this repo — the API read door is already wrong before the console is involved, so neither is an objectui defect.
PARTIAL gaps — no cards, by rule
The 9 checklist-accuracy findings (CF-1…CF-9) and 7 fixture gaps (FG-1…FG-7) itemised above are parked on the wave anchor per the PM's 2026-08-18 comment; the security-priority pin upgrades from the wave's gap inventory are already filed as #9481 #9482 #9483 and this record's gaps are not to be re-filed against them. Two of the CFs have already earned their own follow-ups elsewhere: the
owd-save-gateR1 divergence (CF-3) is #9477, and the RUNNER.md carve-out this round's discipline depends on is #9471.One correction from this round is now wave doctrine.
studio-authoring.first-run-loopis not blocked on stock showcase — the Studio app registration is absent, but the App Builder UI is reachable and the loop closes 6/6 with zero restarts. The PM had instructed this round to recordblockedand move on; the runner checked anyway. The standing fact on #9296 is corrected and left struck through rather than deleted.Extracted: #9454
Parked-on: #9296
Generated by Claude Code
docs(qa): land the objectstack-ai#9296 wave's item corrections and th…
docs(qa): name the condition that selects the OWD gate's answering la…