diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index dffe1d08d3c..6a3246f4674 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -2919,7 +2919,7 @@ "title": "OS_PLATFORM_OWNER_EMAIL platform-admin anchor: only a VERIFIED stored-email match confers PLATFORM_ADMIN — unset, unverified match, non-match and malformed lists each confer nothing, fail closed", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -2933,7 +2933,7 @@ "a manage_platform_settings-floor probe route: GET /api/v1/datasources answers 401/403 BEFORE any service resolution (packages/services/service-datasource/src/admin-routes.ts; DATASOURCE_ADMIN_CAPABILITY), and admin_full_access carries that capability (packages/spec/src/identity/eval-user.zod.ts) — so the probe discriminates platform-admin standing cleanly" ], "knownGaps": [ - "the ENTITLED live leg needs a VERIFIED second account, and no stock HTTP path verifies one: email_verified is statically readonly at the user-context API (stripped — the write-path-guards class), a fresh sign-up reads unverified, and the seeded dev admin — whose address IS stamped verified at seed time (#11343, auth-plugin.ts) — also holds the unscoped admin_full_access grant under single (the posture this item boots), so its 200s can never attribute to the config anchor. Verify M's address via a verification-enabled boot with mail capture (identity-auth.email-verification-loop's fixture, itself a knownGap there) or a system-context stamp through the same isSystem doorway the seed itself uses; without either, clause 5's live leg scores blocked(fixture) and the entitled side rests on the unit pin. Measured on main c91af8ed6: POST /api/v1/auth/admin/update-user {userId, data: {emailVerified: true}} answers the seeded dev admin 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS, so the HTTP lane has no verifying door; the run stamped M's email_verified directly on its own isolated scratch database with the server stopped (a system-level write, never on a shared database) and records that it did" + "the ENTITLED live leg needs a VERIFIED second account, and no stock HTTP path verifies one: email_verified is statically readonly at the user-context API (stripped — the write-path-guards class), a fresh sign-up reads unverified, and the seeded dev admin — whose address IS stamped verified at seed time (#11343, auth-plugin.ts) — also holds the unscoped admin_full_access grant under single (the posture this item boots), so its 200s can never attribute to the config anchor. Verify M's address via a verification-enabled boot with mail capture (identity-auth.email-verification-loop's fixture, itself a knownGap there) or a system-context stamp through the same isSystem doorway the seed itself uses; without either, clause 5's live leg scores blocked(fixture) and the entitled side rests on the unit pin. Measured on main c91af8ed6: POST /api/v1/auth/admin/update-user {userId, data: {emailVerified: true}} answers the seeded dev admin 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS, so the HTTP lane has no verifying door; the run stamped M's email_verified directly on its own scratch database with the server stopped (a system-level write, never on a shared database) and records that it did" ] }, "steps": [ @@ -3052,6 +3052,12 @@ "date": "2026-10-20", "change": "source[5] and source[7] re-pointed — the two comma-range line citations round 9 of wave anchor #24579 left, because PR #24667 held the item then (round 9's report 6283532816, out_of_scope_findings[0]). At the authoring commit 45397edf53, line 200 of env.ts was the resolvePlatformOwnerEmail declaration and line 261 of admin-routes.ts was the DATASOURCE_ADMIN_CAPABILITY declaration. At base 795ff708d8 source[5] anchors both PLATFORM_OWNER_EMAIL_ENV and resolvePlatformOwnerEmail in env.ts, and source[7] anchors the DATASOURCE_ADMIN_CAPABILITY declaration in admin-routes.ts instead of the string literal manage_platform_settings. All three resolve as declarations in the shared resolver. The ',NNN' residue is what #13786's strip left of a two-range list. No clause, step or other field changed", "ref": "#24788" + }, + { + "revision": 7, + "date": "2026-10-21", + "change": "Respelled — fixtures.knownGaps[0]: 'its own isolated scratch database' → 'its own scratch database'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. steps[0]'s 'a walled posture (group / isolated)' names the posture and is left as written. No clause changed", + "ref": "#25018" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 2d681e61af3..43683d707ad 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -796,82 +796,83 @@ }, { "id": "approvals.record-page-decisions", - "title": "Approve/Reject render in the record header; Reject fires after ONE dialog; a locking approval hides inline edit", + "title": "Approve/Reject render on the record page from sys_approval_request's declared actions; Reject fires after ONE dialog; a locking approval hides inline edit", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": ["dev admin (holds the manager position, so showcase_budget_approval's manager_review routes to them — a pending approver standing on the record page)"], "fixtures": { "app": "showcase", "requires": [ - "showcase_budget_approval raised on a showcase_project by PATCHing budget above 100000 (budget != previous.budget) — parks at manager_review (approvers position 'manager' = the admin), lockRecord:false; driving budget above 500000 reaches exec_review, lockRecord:true (the locking contrast, objectui#2902)", - "the record page surface: objectui packages/app-shell RecordDetailView + useRecordApprovals + buildApprovalDecisionActions" - ], - "knownGaps": [ - "the steps, clauses, fixtures.requires[1] and source[0]–[1] describe the record header's two hand-written decision buttons (buildApprovalDecisionActions: approve_request / reject_request in record_header, ordered -100 / -99, behind a client-side canDecide). objectui 99782f961 (objectui#3055) retired them before both console pins. At the console pin cf153eb2e535 the record page renders sys_approval_request's own declared actions through DeclaredActionsBar above the page body, not in the header: approval_approve, approval_reject and the other levers at location record_section, gated by the server's viewer block (can_act, can_override, is_submitter), in objectui RecordDetailView.tsx and recordApprovalActions.ts, over the declarations in packages/plugins/plugin-approvals/src/sys-approval-request.object.ts. Read each step and clause against that surface and record where it differs: this item's text has not been re-derived from it" + "showcase_budget_approval raised on a showcase_project by PATCHing budget above 100000 (budget != previous.budget) — parks at manager_review (approvers position 'manager' = the admin), lockRecord:false; on a budget above 500000, approving manager_review routes through needs_exec to exec_review (approvers position 'exec', which the admin also holds: examples/app-showcase/src/security/seed-approval-demo.ts#ADMIN_APPROVAL_POSITIONS), lockRecord:true (the locking contrast, objectui#2902). The admin's PATCH triggers the run, so the admin is each request's submitter as well (the approval node passes its trigger user)", + "the record page surface: objectui packages/app-shell RecordDetailView (DeclaredActionsBar over the record's pending sys_approval_request row, location record_section) + useRecordApprovals (the record's approvals read) + recordApprovalActions.ts; the actions themselves are the server's declarations in packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, each gated by its visible predicate over the row's server-computed viewer block" ] }, "steps": [ "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", - "PATCH a showcase_project budget to e.g. 200000 over /api/v1/data — showcase_budget_approval opens and parks at manager_review (routed to manager = admin), lockRecord:false", - "open that project's record page — /_console/apps/showcase_app/showcase_project/record/:id, the served record route (the shorthand /apps/showcase_app/showcase_project/:id still lands there: the console's ShorthandRecordRedirect rewrites it); screenshot the header — Approve and Reject must render in the primary slot (buildApprovalDecisionActions: Approve variant 'primary' order -100, Reject variant 'destructive' order -99, both locations ['record_header'])", - "because manager_review is lockRecord:false, confirm inline-edit affordances remain live on this record (the editable contrast)", - "click Reject: assert exactly ONE dialog opens — the param dialog carrying the comment textarea inline, titled by the Reject label with the confirm question as its description; there is NO separate confirm-then-comment second dialog (objectui#3126)", - "fill the comment and Confirm; capture POST /api/v1/approvals/requests/:id/reject and re-read the request (status=rejected)", - "approve path: raise another budget-approval request; on its record page click Approve → one dialog with an optional comment → Confirm → POST /:id/approve → re-read (status=approved)", - "lock contrast: drive a >500000 budget to exec_review (lockRecord:true) on a project record page; screenshot — the inline-edit affordances are HIDDEN (recordLockedByApproval → canEdit suppressed)", - "both-sides gate: open the same project's record page as a member who can READ the project but is NOT a pending approver and holds no override — e.g. a contributor-bound member — the header offers NO Approve/Reject (approvals.canDecide false). ⛔ Not the dev admin: on a record whose approval routes away from them the admin still sees 'Override Approve' / 'Override Reject' (the organization-administrator override path), so the admin cannot prove the gate's closed side" + "PATCH a showcase_project budget to e.g. 200000 over /api/v1/data — showcase_budget_approval opens and parks at manager_review (routed to manager = admin), lockRecord:false; the admin is the request's submitter as well as its approver", + "open that project's record page — /_console/apps/showcase_app/showcase_project/record/:id, the served record route (the shorthand /apps/showcase_app/showcase_project/:id still lands there: the console's ShorthandRecordRedirect rewrites it); once it settles, screenshot the decision bar ABOVE the page body, not the record header: DeclaredActionsBar renders sys_approval_request's declared record_section actions against the pending request row. For the admin (pending approver and submitter) that is Approve (approval_approve, declared variant 'primary' → filled), Reject (approval_reject, 'danger' → destructive), Reassign, Send back, Request info, Recall and Reply, in declaration order, each a button whose data-testid is declared-action- plus the action name; Send reminder is left to the approvals panel (approval_remind is excluded) and Resubmit stays hidden (the request is not 'returned'). Capture the read the bar ran on: GET /api/v1/approvals/requests?object=showcase_project&recordId=:id answers the pending row with viewer.can_act true and viewer.is_submitter true", + "because manager_review is lockRecord:false (the request row reads lock_record: false), confirm inline-edit affordances remain live on this record (the editable contrast)", + "click Reject (declared-action-approval_reject): assert exactly ONE dialog opens — the param dialog titled by the declared label, Reject, with the declared confirm question as its description ('Reject this request? A rejection is final for every approver.'), carrying the optional Comment textarea and Attachments file input inline; there is NO confirm dialog in front of it and no second comment dialog after it (approval_reject declares its question as description and no confirmText, so the runtime chains nothing, #7278; objectui#3126)", + "fill the comment and Confirm; capture POST /api/v1/approvals/requests/:id/reject — :id is the REQUEST's id, never the project's — with the comment in its body; re-read GET /api/v1/approvals/requests/:id (status=rejected) and GET /api/v1/approvals/requests/:id/actions (the reject row carries the comment). The bar re-reads the approvals after the decision (refreshAfter) and draws nothing once no request is pending", + "approve path: PATCH another project's budget to e.g. 200000; on its record page click Approve → one dialog (titled Approve, described 'Approve this request? Your approval is recorded, and the request moves on once this step has the approvals it requires.', optional Comment and Attachments) → Confirm → POST /api/v1/approvals/requests/:id/approve → re-read (status=approved; at or under 500000, needs_exec ends the run at approved)", + "lock contrast: PATCH a third project's budget above 500000 (e.g. 600000) and approve its manager_review from the record page as in step 7 — needs_exec routes the run to exec_review (position 'exec', the admin again; lockRecord:true), a NEW pending request whose row reads lock_record: true; once the page re-reads (the decision's refresh, or a reload), screenshot — the inline-edit affordances are HIDDEN (approvalLocked, from resolveApprovalLock over recordLockedByApproval of the pending request → canEdit suppressed) and the approval band shows its locked state", + "both-sides gate, closed side: open the step-8 project's record page (its exec_review is pending) as a member who can READ the project but is not on the slate, did not submit and holds no override — e.g. a contributor-bound member (the search:qa-contributor-bound-member recipe; RUNNER.md's self-registration entry says how to mint one). Capture the member's own GET /api/v1/approvals/requests?object=showcase_project&recordId=:id: it serves no row for this record (the approvals door serves a request to its participants and the override actor, and the record-reader tier is off: no showcase object declares enable.approvalsVisibleToReaders), so the page draws no declared decision action at all. Then forge the decision: as the member, POST /api/v1/approvals/requests/:id/approve with the id from the admin's read → 403, code FORBIDDEN ('is not a pending approver'), and the request re-reads still pending. ⛔ Not the dev admin: the admin holds the override (step 10), so the admin cannot prove the gate's closed side", + "override contrast (the admin off the slate): on a pending manager_review (PATCH a fourth project to e.g. 200000), Reassign the step to another user through the bar's New approver picker — e.g. the seeded Ada Auditor (demo) persona (examples/app-showcase/src/security/demo-personas.ts) — so the request waits on that user and the admin holds no slot. Re-read the row (viewer.can_act false, viewer.can_override true) and screenshot the bar: Approve, Reject and Reassign render as 'Override Approve' / 'Override Reject' / 'Override Reassign' (amber outline, a shield icon, data-override-decision='true'), Send back and Request info are gone (their predicate reads can_act alone), and Recall and Reply stay plain (the admin holds them as the submitter). That is the override arm of the same declared predicates, not the gate failing" ], "acceptance": [ { - "clause": "Approve and Reject render in the record header primary slot for the pending approver", + "clause": "the pending approver is offered the request's declared decisions on the record page: Approve and Reject, with the other declared record_section levers the viewer block admits, render in the decision bar above the page body, not in the record header", "oracle": "screenshot", - "verify": "after the header renders, screenshot shows Approve (primary) ahead of app record_header actions and Reject (destructive) — the strongly-negative order floats the decision buttons into the primary slot (buildApprovalDecisionActions #2670/objectui#2339)", - "evidence": "record-header screenshot" + "verify": "after the page settles, the screenshot shows the bar DeclaredActionsBar draws over the pending sys_approval_request row: Approve filled (declared variant 'primary') and Reject destructive ('danger'), then Reassign, Send back, Request info, Recall and Reply for the admin who is approver and submitter, and no decision button in the header (objectui#3055 retired the header pair, buildApprovalDecisionActions). The set is the server's declarations at location record_section, approval_remind excluded (recordApprovalActions.ts), each kept or dropped by its own visible predicate over the row's viewer block — read that block in the approvals read the bar ran on", + "evidence": "record-page screenshot (the bar and the header) + the GET /api/v1/approvals/requests read and its viewer block" }, { - "clause": "Reject fires after exactly ONE dialog: the comment param is collected INLINE in that single dialog and Confirm POSTs the reject — no second, unexpected comment dialog", + "clause": "Reject fires after exactly ONE dialog: the declared Comment (and Attachments) params are collected INLINE in that single dialog, which carries the declared confirm question as its description, and Confirm POSTs the reject — no confirm dialog before it and no second comment dialog after it", "oracle": "network", - "verify": "one param dialog (no chained confirm dialog) precedes exactly one POST /api/v1/approvals/requests/:id/reject; the objectui#3126 double-dialog bug (confirm then a second comment dialog, decision silently not sent) must not reproduce", + "verify": "one param dialog — titled Reject (the declared label) and described by approval_reject's declared question; the action declares no confirmText, so the runtime chains no confirm (#7278) — precedes exactly one POST /api/v1/approvals/requests/:id/reject, :id being the request's id; the objectui#3126 double-dialog bug (confirm then a second comment dialog, decision silently not sent) must not reproduce", "evidence": "screenshot of the single dialog + the reject network trace" }, { - "clause": "the decision POSTs the approvals route and the request re-reads with the new status", + "clause": "the decision POSTs the approvals route for the REQUEST and the request re-reads with the new status; the comment the dialog collected round-trips onto the decision's action row", "oracle": "api", - "verify": "reject → GET /:id status=rejected; approve → status=approved; the record header collected the comment (rides actionParams, not the dead collectParams #2955) and it round-trips onto the action row", - "evidence": "before/after request reads + the decision action row" + "verify": "reject → GET /api/v1/approvals/requests/:id answers status=rejected; approve → status=approved; the POST went to the request's id (never the project's) with the declared comment param in its body, and GET /api/v1/approvals/requests/:id/actions lists the decision row carrying that comment", + "evidence": "before/after request reads + the POST body + the decision action row" }, { "clause": "a LOCKING pending approval hides the record's inline-edit affordances; a non-locking one keeps them", "oracle": "screenshot", - "verify": "under exec_review (lockRecord:true) the inline-edit affordances are absent (recordLockedByApproval(pendingRequest) → canEdit false, objectui#2902); under manager_review (lockRecord:false) they remain — both sides, on the same flow", - "evidence": "two record-page screenshots (locked vs editable)" + "verify": "under exec_review (lockRecord:true) the request row reads lock_record: true and the inline-edit affordances are absent (approvalLocked from resolveApprovalLock over recordLockedByApproval of the pending request → canEdit false, objectui#2902; the approval band shows its locked state); under manager_review (lockRecord:false) the row reads lock_record: false and they remain — both sides, on the same flow", + "evidence": "two record-page screenshots (locked vs editable) + the two request rows' lock_record" }, { - "clause": "the decision buttons are gated by the viewer's approver relationship — a viewer who is neither a pending approver nor an override holder is offered neither Approve nor Reject; an organization administrator off the slate is offered the OVERRIDE pair instead, which is a different control, not this gate failing", - "oracle": "dom", - "verify": "after confirming render via screenshot, the header DOM of the non-approver, non-override member (e.g. a contributor-bound member) has no approve_request/reject_request actions when approvals.canDecide is false (the current user is not in pending_approvers); record, separately, that the admin routed away sees 'Override Approve' / 'Override Reject'", - "evidence": "screenshot + header action DOM list" + "clause": "the decision actions are gated by the SERVER's viewer block, not by a client check: a viewer who is not a pending approver, not the submitter and holds no override is offered no decision action, and the server refuses the decision a forged request makes; an organization administrator off the slate is offered the OVERRIDE framing of the levers the override arm admits, which is a different control, not this gate failing", + "oracle": "api", + "verify": "as the member (step 9): the member's GET /api/v1/approvals/requests?object=showcase_project&recordId=:id serves no row for the record; after a screenshot confirms the page rendered, its DOM holds no declared-action-* button; a forged POST /api/v1/approvals/requests/:id/approve answers 403 with code FORBIDDEN, and the request re-reads pending. As the admin off the slate (step 10): the row's viewer block reads can_act false and can_override true, and the bar shows the Override framing on Approve, Reject and Reassign (data-override-decision='true') with Send back and Request info absent — record it separately", + "evidence": "the member's approvals read + the record-page screenshot and its declared-action DOM list + the forged POST's 403 body; the admin's off-slate row read + bar screenshot" } ], "negative": [ "a Reject that opens a SECOND dialog (a confirm dialog followed by a separate comment dialog) and silently no-ops after the first Confirm is a FAIL — the objectui#3126 regression: the param dialog IS the confirmation, and nothing posts until its own Confirm", "an inline-edit affordance offered while a lockRecord:true approval is pending is a FAIL — the server would reject the save RECORD_LOCKED, and offering the edit is exactly the objectui#2902 mislabel", - "Approve/Reject rendered on the header for a viewer who is not a pending approver (canDecide false) is a FAIL even if the eventual POST would 403" + "a declared decision action rendered for a viewer whose viewer block admits none — not a pending approver (can_act false), not the submitter, no override (can_override false) — is a FAIL even though its POST would answer 403 FORBIDDEN; the bar holds no client-side approver test (objectui#3055), so the cause is in the row's viewer block or the declared predicate" ], "traps": ["automation-input", "hydration-race"], "source": [ - "objectui packages/app-shell/src/views/RecordDetailView.tsx (buildApprovalDecisionActions — record_header Approve/Reject, single-dialog Reject #3126, actionParams comment #2955; approvalLocked/canEdit gating #2902)", - "objectui packages/app-shell/src/hooks/useRecordApprovals.ts (recordLockedByApproval, canDecide, /approvals/requests decide routes)", + "objectui packages/app-shell/src/views/RecordDetailView.tsx (blob 64a7c675e50d at the console pin cf153eb2e535): the pending sys_approval_request row's declared actions render through DeclaredActionsBar above the page body, not in the header (objectui#3055) — the request row is the dispatch record, at location record_section with approval_remind excluded (recordApprovalActions.ts), and a decision re-reads the approvals and the record; approvalLocked (resolveApprovalLock over recordLockedByApproval of the pending request) gates canEdit (objectui#2902)", + "objectui packages/app-shell/src/views/DeclaredActionsBar.tsx (blob f27b4ab0efff): each declared action's visible predicate evaluated fail-closed against the row's viewer block, the declared variant mapped (primary → filled, danger → destructive), the override framing (isOverrideDecision in utils/approvalOverride.ts: 'Override' plus the label, amber outline, data-override-decision) and the data-testid declared-action- plus the action name; + hooks/useRecordApprovals.ts (blob b65a91f4a9a4): the record's read, GET /api/v1/approvals/requests?object=…&recordId=… then GET …/requests/:id, with no client-side canDecide; + hooks/useConsoleActionRuntime.tsx: the param dialog takes its title from the action's label and its description from the declared description", "objectui packages/app-shell/src/views/RecordDetailView.approvalDeclaredActions.test.tsx (objectui#3055, blob 9519ae70f743 at the console pin cf153eb2e535: the record page runs sys_approval_request's declared decision actions, and 'folds a declared decision output into the nested `outputs` body' asserts that a decision's comment and declared outputs reach the POST to the request) + packages/plugins/plugin-approvals/src/sys-approval-request.object.test.ts ('the confirm question rides `description`, not `confirmText`, so one decision opens one dialog (#7278)'). Between them they hold what the deleted RecordDetailView.approvalDecisionActions.test.tsx pinned, the decision params and one dialog per decision; objectui 99782f961 deleted that file before both console pins", - "examples/app-showcase/src/automation/flows/index.ts#BudgetApprovalFlow (BudgetApprovalFlow — manager_review lockRecord:false / exec_review lockRecord:true on showcase_project)" + "examples/app-showcase/src/automation/flows/index.ts#BudgetApprovalFlow (BudgetApprovalFlow — manager_review lockRecord:false / exec_review lockRecord:true on showcase_project)", + "packages/plugins/plugin-approvals/src/sys-approval-request.object.ts#approval_reject (the declared actions: approval_approve 'primary' and approval_reject 'danger', each with optional comment and attachments params and its confirm question as description, never confirmText, #7278; approval_reassign, approval_send_back, approval_request_info, approval_remind, approval_recall, approval_resubmit and approval_comment; all at record_section, every visible predicate over record.viewer, the attachedOnRead block) + packages/plugins/plugin-approvals/src/approval-service.ts#attachViewers (can_act: pending and the caller takes a slot; is_submitter; can_override: pending and isOverrideActor)", + "packages/plugins/plugin-approvals/src/request-read-gate.ts (header: the approvals door serves a request to its participants and the override actor, and to a record reader only where the object declares enable.approvalsVisibleToReaders, which no showcase object does) + packages/plugins/plugin-approvals/src/approval-service.ts#decide (a caller who takes no slot and holds no override: FORBIDDEN, answered 403 by packages/rest/src/approvals-routes.ts)" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "initial — the record-page approval surface (distinct from the inbox-drawer items): header Approve/Reject, one-dialog Reject (#3126), decision round-trip, and the lockRecord-driven inline-edit hide (#2902)", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-17", "change": "the both-sides clause and step 9 named no persona, and the natural choice — the dev admin on a record routed away from them — sees 'Override Approve' / 'Override Reject' (#23878 checklist-accuracy finding), so it can never show the closed side; the step and clause 5 now name a non-override member. The record route in step 3 is the served /_console/apps/showcase_app/showcase_project/record/:id (the shorthand form redirects to it). Re-run on main c91af8ed6 with the console at objectui f8267df93aba: on a project whose budget approval is pending for the admin, the admin's header shows Approve and Reject; a contributor-bound member's header shows neither; with the admin's slot reassigned away the admin's header shows Override Approve and Override Reject", "ref": "#24036" }, - { "revision": 3, "date": "2026-10-20", "change": "source[2] re-pointed, and fixtures.knownGaps[0] added. PR #24982's pin finding: source[2] cited objectui's RecordDetailView.approvalDecisionActions.test.tsx, absent at both console pins. Read in a full objectui clone: objectui 99782f961 (objectui#3055) deleted it, and that commit is an ancestor of 616df6598b42 and of cf153eb2e535 (exit 0 both; control, cf153eb2e535 as an ancestor of 99782f961: exit 1). The same commit retired buildApprovalDecisionActions and moved the record page's decisions onto sys_approval_request's declared actions. At cf153eb2e535, RecordDetailView.approvalDeclaredActions.test.tsx holds the params half (the comment and declared outputs reach the POST to the request), and this repository's sys-approval-request.object.test.ts holds the one-dialog half. The knownGap tells a runner that the rest of the item still describes the retired header buttons. steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } + { "revision": 3, "date": "2026-10-20", "change": "source[2] re-pointed, and fixtures.knownGaps[0] added. PR #24982's pin finding: source[2] cited objectui's RecordDetailView.approvalDecisionActions.test.tsx, absent at both console pins. Read in a full objectui clone: objectui 99782f961 (objectui#3055) deleted it, and that commit is an ancestor of 616df6598b42 and of cf153eb2e535 (exit 0 both; control, cf153eb2e535 as an ancestor of 99782f961: exit 1). The same commit retired buildApprovalDecisionActions and moved the record page's decisions onto sys_approval_request's declared actions. At cf153eb2e535, RecordDetailView.approvalDeclaredActions.test.tsx holds the params half (the comment and declared outputs reach the POST to the request), and this repository's sys-approval-request.object.test.ts holds the one-dialog half. The knownGap tells a runner that the rest of the item still describes the retired header buttons. steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, + { "revision": 4, "date": "2026-10-21", "change": "Re-derived against the shipped record page. PR #25003's carried finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): the steps, clauses, fixtures.requires[1] and source[0]–[1] still described the record header's hand-written Approve/Reject — buildApprovalDecisionActions, approve_request / reject_request in record_header, ordered -100 / -99, behind a client-side canDecide — which objectui 99782f961 (objectui#3055) retired before both console pins. Read at objectui cf153eb2e535 (RecordDetailView.tsx, DeclaredActionsBar.tsx, recordApprovalActions.ts, useRecordApprovals.ts, useConsoleActionRuntime.tsx, approvalOverride.ts) and at base 93e6c1a5f8 (sys-approval-request.object.ts; approval-service.ts attachViewers and decide; request-read-gate.ts; approvals-routes.ts; the showcase budget flow and approval seed): the record page runs sys_approval_request's declared record_section actions through DeclaredActionsBar above the page body, each gated by its visible predicate over the server's viewer block. title, fixtures.requires[0]–[1], steps[1]–[8], the verify and evidence of every clause, clauses [0], [1], [2] and [4] (whose oracle moves dom → api: the gate is the server's), negative[2] and source[0]–[1] rewritten; steps[9] (the off-slate admin's override framing) and source[4]–[5] (the server's declarations, viewer block, read gate and refusal) added; fixtures.knownGaps[0] removed, the retired text it warned about being gone. No automated ref is added: neither pin holds a clause whole at its layer (the objectui test mounts the page in jsdom over a fetch double, with a hand-written definition and the param dialog mocked; sys-approval-request.object.test.ts reads the declarations). The live verdicts at the pin are NOT MEASURED: the sources were read, not run", "ref": "#25018" } ] }, { diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 9c18b125f2f..5e17c55ad4e 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1176,7 +1176,7 @@ "title": "connector_action dispatches through every registered connector kind, and the registry feeds the designer pickers", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -1187,11 +1187,11 @@ "requires": [ "plugin connectors rest + slack (objectstack.config.ts) and declarative instances showcase_status_api (provider rest) + showcase_mcp_tools (provider mcp, in-repo stdio fixture server) from src/system/connectors/", "the three dispatching flows: showcase_task_completed_rest_ping (rest), showcase_declarative_connector_ping (declarative rest, ADR-0097), showcase_mcp_connector_echo (declarative MCP, #3056)", - "⚠️ a boot that EXPORTS OS_PORT= (or SHOWCASE_SELF_URL) as well as passing -p — the declarative instances resolve their baseUrl from the environment, not from the flag, so an isolated boot without it dials 3000 and every declarative dispatch fails `fetch failed` (see knownGaps)" + "⚠️ a boot that EXPORTS OS_PORT= (or SHOWCASE_SELF_URL) as well as passing -p — the declarative instances resolve their baseUrl from the environment, not from the flag, so a boot on its own port without it dials 3000 and every declarative dispatch fails `fetch failed` (see knownGaps)" ], "knownGaps": [ "slack DELIVERY is not assertable on stock fixtures — TaskCompletedSlackFlow points at a placeholder channel with no real bot token, so the slack variant is registry-enumeration only", - "`showcase_declarative_connector_ping` failing every record-change firing with `connector_action(showcase_status_api.request) failed: fetch failed` is a BOOT MISCONFIGURATION, not a blocked environment and not a missing stub. The showcase's declarative connector instances dial the running server itself via resolveShowcaseSelfUrl() (examples/app-showcase/src/system/self-url.ts), which reads SHOWCASE_SELF_URL -> OS_PORT -> PORT -> the literal http://127.0.0.1:3000 from the ENVIRONMENT. The CLI's `-p` flag only takes its DEFAULT from OS_PORT and never writes it back (packages/cli/src/commands/serve.ts), so a dogfood-§0 isolated boot on a non-3000 port leaves the connectors dialing 3000 while the server listens elsewhere. Export OS_PORT= (or SHOWCASE_SELF_URL) alongside -p and the variant runs. ⚠️ The string is indistinguishable from a sandbox egress block — that is what makes it expensive: #7516 burned a run proving it was an address problem with a TCP forwarder on 3000, and #7538 fixed the resolution but cannot fix a boot that never exports the variable. Verify the boot's exported OS_PORT and one completed declarative run BEFORE scoring the variant (step 3): GET /api/v1/automation/connectors carries no baseUrl, and /api/v1/meta/connector/ shows the build-time literal, not the resolved address." + "`showcase_declarative_connector_ping` failing every record-change firing with `connector_action(showcase_status_api.request) failed: fetch failed` is a BOOT MISCONFIGURATION, not a blocked environment and not a missing stub. The showcase's declarative connector instances dial the running server itself via resolveShowcaseSelfUrl() (examples/app-showcase/src/system/self-url.ts), which reads SHOWCASE_SELF_URL -> OS_PORT -> PORT -> the literal http://127.0.0.1:3000 from the ENVIRONMENT. The CLI's `-p` flag only takes its DEFAULT from OS_PORT and never writes it back (packages/cli/src/commands/serve.ts), so a dogfood-§0 boot on its own non-3000 port leaves the connectors dialing 3000 while the server listens elsewhere. Export OS_PORT= (or SHOWCASE_SELF_URL) alongside -p and the variant runs. ⚠️ The string is indistinguishable from a sandbox egress block — that is what makes it expensive: #7516 burned a run proving it was an address problem with a TCP forwarder on 3000, and #7538 fixed the resolution but cannot fix a boot that never exports the variable. Verify the boot's exported OS_PORT and one completed declarative run BEFORE scoring the variant (step 3): GET /api/v1/automation/connectors carries no baseUrl, and /api/v1/meta/connector/ shows the build-time literal, not the resolved address." ] }, "variants": [ @@ -1273,6 +1273,12 @@ "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 5, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[2]: 'so an isolated boot without it dials 3000' → 'so a boot on its own port without it dials 3000'; fixtures.knownGaps[1]: 'so a dogfood-§0 isolated boot on a non-3000 port' → 'so a dogfood-§0 boot on its own non-3000 port'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1742,7 +1748,7 @@ "title": "Disabling a packaged flow that packaged callers invoke as a subflow is refused 409 DELETE_RESTRICTED naming every caller; the refused attempt writes no ledger row; enabling the subflow itself is never guarded", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": [ @@ -1765,7 +1771,7 @@ "after each refusal: GET /api/v1/data/sys_metadata_activation and assert NO row exists for the refused flow; then prove it is still ARMED — trigger a run through its caller (for the notify pair: create a showcase_task with its required project and status 'in_review', then PATCH it to status='done' — task_status_flow allows done only from in_review) and confirm the child's subflow step executed", "remedy sequence (the refusal's own instruction): POST /api/v1/automation/showcase_task_done_notify_owner/toggle {\"enabled\": false} (the CALLER — it has no packaged callers of its own, so this lands), then RETRY POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": false} and record what it answers", "enable arm, child FIRST: POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": true} while its caller is still off (confirm it is never refused), then toggle showcase_task_done_notify_owner back ON, then repeat the child's enable with the caller armed (a no-op enable). ⚠️ Enabling the CALLER while its packaged subflow is still disabled is refused by design since 679f95e (#20711) — 409 RESOURCE_CONFLICT 'cannot be enabled while 1 packaged subflow it calls is disabled … Enable it first' — so the caller-first order is not a fail of this item", - "teardown: re-enable anything left disabled (or discard the isolated DB)", + "teardown: re-enable anything left disabled (or discard the run's own DB)", "scope, not a probe: the UI half (the caller names rendered verbatim in a role=alert on the Setup packaged-automation page) is deliberately NOT scored here; it is automation.setup-packaged-automation-board's refusal clause" ], "acceptance": [ @@ -1829,7 +1835,8 @@ { "revision": 4, "date": "2026-10-19", "change": "acceptance[0].clause quotes the remedy the engine writes — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-subflow-disable-refusal acceptance[0]; wave anchor #24579 §3): the remedy names the calling flow, so a substring match on the generic quote fails although the meaning is identical. Re-derived at base 9a7838edf5 (packages/services/service-automation/src/engine.ts, the subflow-caller refusal: 'disable the calling flow 'NAME' first' … ' — or leave this one armed.')", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 707, 795, 816, 1322 and 1339 of the showcase flows index.ts in the comma-range spelling (authored in e0abc386a4 as the list 678 to 1339). At that commit those lines were the names and child flowName values of the three stock caller flows; at this base all of them have moved. source[4] now anchors the three callers, index.ts#TaskDoneNotifyOwnerFlow, #ProjectClosureFlow and #ReleaseSignoffFlow, and names each child in prose. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#24963" }, - { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, + { "revision": 8, "date": "2026-10-21", "change": "Respelled — steps[6]: '(or discard the isolated DB)' → '(or discard the run's own DB)'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", "ref": "#25018" } ] }, { @@ -1837,7 +1844,7 @@ "title": "POST /automation/:name/clone: whole-definition copy mutating exactly name/label/status, protection envelope dropped, no ancestry, draft-but-armed (double-fire notice verbatim), 409/404/400 refusal arms — and the honest clause: the clone must survive a restart and reach Studio", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1859,7 +1866,7 @@ "references: POST /api/v1/automation/showcase_task_done_notify_owner/clone {\"name\":\"qa_task_done_clone\",\"label\":\"QA task-done clone\"}; read the clone's subflow node config", "ordinariness: PUT /api/v1/automation/qa_urgent_alert_clone with the clone's own definition (label tweaked) — it is a tenant flow, the edit lands", "restart probe: stop the server; cold-boot over the SAME database file; GET /api/v1/automation/qa_urgent_alert_clone and GET /api/v1/meta/flow (does any package/tenant metadata hold the clone?); then, in the browser as the admin, open /_console/studio/~org/automations (the 'Organization flows' scope) and confirm the clone is listed on the Automations rail and opens in the designer; create one more urgent task (with its required project) and confirm the clone's runs list grows on the new process", - "teardown: DELETE /api/v1/automation/qa_urgent_alert_clone and …/qa_task_done_clone (or discard the isolated DB)" + "teardown: DELETE /api/v1/automation/qa_urgent_alert_clone and …/qa_task_done_clone (or discard the run's own DB)" ], "acceptance": [ { @@ -1937,7 +1944,8 @@ { "revision": 3, "date": "2026-10-17", "change": "clause 6's reachability EXPECTED FAIL is stale (checklist-accuracy finding, automation slice report on #23878): objectui#11553 gave Studio an org scope whose Automations rail lists package-less flows, and #21332 closed as a duplicate. The verify now asserts the clone is listed and opens at /_console/studio/~org/automations, steps[7] drives it after the restart, and the objectui source line names the org scope and its pin. Re-run on main c91af8ed6 with the console at objectui f8267df93aba, file DB: POST …/showcase_urgent_task_alert/clone {name qa_urgent_alert_clone, label} → 200 status draft; the Organization flows rail listed 'QA urgent alert clone' and opened it (Trigger: Record created or updated · showcase_task, Enabled, no lock banner); after a cold restart over the same DB the GET answered 200, the rail listed and opened it again, and one urgent task create added one run to the clone (0 → 1) and one to the base (1 → 2); teardown DELETE → deleted true", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "acceptance[1] (clause, verify) counts the dropped keys the clone really drops — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-clone-contract acceptance[1]; wave anchor #24579 §3): FLOW_CLONE_DROPPED_KEYS is 10, not 9, since packages/runtime/src/flow-clone.ts appends 'protection' (#23120, ADR-0010 §3.5) to the 2 read decorations and 7 protection fields. Re-derived at base 9a7838edf5", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). acceptance[3].verify cited line 439 of automation-flow-clone.test.ts in the comma-range spelling (authored in e0abc386a4 as the list 368-401 and 439). Those lines are unchanged at this base: the same-name 409 cases and the unknown-source 404 assertion. The verify now anchors the two codes those cases assert, automation-flow-clone.test.ts#RESOURCE_CONFLICT and #RESOURCE_NOT_FOUND. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 6, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } + { "revision": 6, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, + { "revision": 7, "date": "2026-10-21", "change": "Respelled — steps[8]: '(or discard the isolated DB)' → '(or discard the run's own DB)'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", "ref": "#25018" } ] }, { @@ -1945,7 +1953,7 @@ "title": "The Setup packaged-automation page: reachable from Setup nav, lists packaged flows + packaged actions, toggle persists, clone on flow rows only, no lineage surface, server refusals verbatim in role=alert", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "browser", "personas": [ @@ -1969,7 +1977,7 @@ "boot the showcase on its own port and DB (dogfood §0, OS_PORT exported); build/serve the pinned console; sign in as the dev admin", "nav probe: GET /api/v1/meta/app?id=setup and search the served navigation for a type:'component' item with componentRef 'automation:packaged'; then look for the entry in the rendered Setup sidebar and click it", "regardless of the nav verdict, reach the page and screenshot it settled (only if the sidebar entry is MISSING — a regression of #12457 — fall back to the typed URL /_console/apps/com.objectstack.setup/component/automation/packaged — where the sidebar entry lands, the app segment being the package id (QA run #24545) — recording that the typed URL was needed, so the rest of the item is still scored)", - "inventory: compare the flows table rows against GET /api/v1/meta/flow (which items carry a real _packageId and _provenance != 'org') and the actions section against its own packaged-actions read; confirm every switch reads On except a flow authored status: 'obsolete' (showcase_approver_bindings, examples/app-showcase/src/automation/flows/approver-bindings.flow.ts), whose switch correctly reads Off — 31 On / 1 Off at QA run #24545's subject", + "inventory: compare the flows table rows against GET /api/v1/meta/flow (which items carry a real _packageId and _provenance != 'org') and GET /api/v1/automation/_status (the runtime list the page joins as its spine), and the actions section against its own packaged-actions read. Read each row's Activation cell as two facts: the switch shows the activation flag, and the badge beside it (data-testid packaged-flow-activation, its data-reading attribute) says whether the flow RUNS (objectui packagedFlows.ts activationReading). The badge reads 'On' for an enabled flow whose _status row carries no unboundCause (bound, or with no trigger to bind); 'Not running here', with the switch still checked, for an enabled flow whose row carries one; and 'Off', switch unchecked, for a disabled flow — showcase_approver_bindings, authored status: 'obsolete' (examples/app-showcase/src/automation/flows/approver-bindings.flow.ts). On a default boot (OS_AUTOMATION_SCHEDULED_WORK_ENABLED unset) the two type: 'schedule' flows, showcase_scheduled_digest (interval) and showcase_task_due_reminder (time_relative), carry unboundCause 'deployment_policy' (RUNNER.md, the scheduled-work entry), so they read 'Not running here', each with a Details chip holding the platform's reason. Derived from the 32 flows allFlows declares at 93e6c1a5f8 (examples/app-showcase/src/automation/flows/index.ts): 1 obsolete → Off; those 2 → Not running here; 29 → On (18 record-triggered flows and 1 api-triggered flow, which bind, and 10 with no trigger); a boot that exports the switch reads 31 On / 1 Off. Reconcile the rows against _status, not against these digits: a row whose unboundCause is anything but deployment_policy (disabled_subflow, missing_trigger, binding_failure) is a finding to read, not a count to match", "toggle: flip showcase_urgent_task_alert OFF; reload the page fully; record the switch state and the sys_metadata_activation row; flip it back ON", "clone dialog: open Clone on a flow row — try submitting with name empty, label empty, then name 'Bad-Name!' + a label; record what renders; then a legal clone and the post-clone notice; DELETE the clone via the API afterwards", "refusal rendering (admin): flip showcase_notify_owner OFF — screenshot the row after the 409 lands", @@ -2089,6 +2097,12 @@ "date": "2026-10-20", "change": "automated.ref confirmed, unchanged. PR #24982's pin finding: objectui packages/app-shell/src/views/setup/PackagedAutomationPage.test.tsx changed blob between the console pins (b321640a6100 at 616df6598b42, aeb3e5f94e38 at cf153eb2e535; objectui#13185 and objectui#13279). Re-read at cf153eb2e535 it still stubs fetch. Its new cases (the Activation cell reading 'Not running here' for a flow the engine left unarmed, and the clone result's next steps) widen the mocked component half only, and PackagedActionsSection.test.tsx is unchanged, so the ref's scope holds and no clause moves. steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 7, + "date": "2026-10-21", + "change": "steps[3] rewritten to the Activation cell's real states. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): the step read \"confirm every switch reads On … 31 On / 1 Off\", and at the console pin cf153eb2e535 the cell carries a badge beside the switch that reads 'Not running here', switch still checked, for an enabled flow whose _status row carries an unboundCause (objectui#13159; packagedFlows.ts activationReading, PackagedAutomationPage.tsx, blobs a85c96d28e5a and f4aa90e11d32). On a default boot showcase_scheduled_digest and showcase_task_due_reminder carry unboundCause 'deployment_policy' (RUNNER.md, the scheduled-work entry; describeUnboundReason in service-automation engine.ts), so a count of 'On' badges is not 31. The step now names the three readings, and states the count with its derivation from the 32 flows allFlows declares at base 93e6c1a5f8: 29 On / 2 Not running here / 1 Off on a default boot, 31 On / 1 Off with the switch exported, and it tells the run to reconcile against _status. No clause changed; the live reading at the pin is NOT MEASURED", + "ref": "#25018" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 54283fb80b4..48773423735 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -9,11 +9,11 @@ "title": "Showcase boots clean: health + ready 200, no degraded startup banners, console + app metadata served", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P0", "surface": "mixed", "preconditions": [ - "isolated run per dogfood-verification §0: own free port (not 3000/3001/3210, checked with lsof), own file DB (--seed-admin -d file:/tmp//data.db)" + "the run's own boot per dogfood-verification §0: own free port (not 3000/3001/3210, checked with lsof), own file DB (--seed-admin -d file:/tmp//data.db)" ], "steps": [ "boot examples/app-showcase via `objectstack dev --ui --seed-admin -p -d file:/tmp//data.db`", @@ -108,6 +108,12 @@ "date": "2026-10-20", "change": "acceptance[3] narrowed to its server half, with the console half appended as acceptance[5]. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4 at 307ce4c4): acceptance[3] bundled GET /_console/ with the app-metadata read, so a run with no console build (QA runs #24838 and #24786: /_console/ answered 503) could never pass it, although the metadata half held. The two halves now score apart, and acceptance[5] says what an unbuilt tree answers and that it is blocked(environment) (console.ts createConsoleNotBuiltPlugin, read at base feb09db14a). No other clause changed", "ref": "#24963" + }, + { + "revision": 9, + "date": "2026-10-21", + "change": "Respelled — preconditions[0]: 'isolated run per dogfood-verification §0:' → 'the run's own boot per dogfood-verification §0:'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -116,14 +122,14 @@ "title": "Seed integrity: row counts match the authored seed, values land verbatim, replay is idempotent", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P0", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], "fixtures": { "app": "showcase", "requires": [ - "a FRESH isolated boot (new file DB) so first-boot seed behavior is what is measured, then a restart against the SAME DB for the idempotence clause" + "a FRESH boot on its own port and a new file DB so first-boot seed behavior is what is measured, then a restart against the SAME DB for the idempotence clause" ] }, "steps": [ @@ -186,6 +192,12 @@ "date": "2026-10-01", "change": "checklist-accuracy finding of QA run #21056: steps[4] and acceptance[2] grep the boot log for SeedLoader lines, but at the default warn level a healthy boot prints none — they now read the level-immune `Seeds:` banner row (or the --log-level info `[Seeder] Seed loading complete` line)", "ref": "#21060" + }, + { + "revision": 4, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'a FRESH isolated boot (new file DB)' → 'a FRESH boot on its own port and a new file DB'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1319,7 +1331,7 @@ "title": "Boot-time migration-journal scan: an interrupted run (started ∧ ¬done) is reported at kernel:ready with the exact `os migrate resume` prescription, boot PROCEEDS without resuming, and the two quiet-degradation arms (no journal object → silent; scan THROWS → 'could not check' warning) stay distinguishable", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["operator (local shell)"], @@ -1327,7 +1339,7 @@ "app": "showcase", "requires": [ "the app as shipped: every `os serve` boot (and so `os start` / `os dev`, which spawn it) composes MigrationRecoveryPlugin beside PlatformObjectsPlugin (the journal object) — serve.ts §5c and §5c-bis — so a stock config runs the scan; a config whose plugins array composes its own `new MigrationRecoveryPlugin()` keeps that instance instead", - "an isolated persistent sqlite DB (own port, -d file:/tmp//journal.db) whose base schema was synced by one prior boot, so sys_migration_journal exists as a table to seed — the same scratch-DB class of fixture cli.migrate-plan-apply-json uses", + "the run's own persistent sqlite DB (own port, -d file:/tmp//journal.db) whose base schema was synced by one prior boot, so sys_migration_journal exists as a table to seed — the same scratch-DB class of fixture cli.migrate-plan-apply-json uses", "a way to hand-insert rows into that sqlite file between boots (sqlite3 CLI or a small node script); the journal's API surface is read-only (apiMethods get/list), so REST cannot seed it" ] }, @@ -1399,6 +1411,12 @@ "date": "2026-10-03", "change": "the composition gap the fixtures encoded is closed: every `os serve` boot now composes MigrationRecoveryPlugin beside PlatformObjectsPlugin, and the `os migrate` data boot composes its registry. The scratch-config fixture, the knownGaps entry and the stock-boot negative are rewritten to match; steps, oracles and clauses are otherwise unchanged", "ref": "#21498" + }, + { + "revision": 3, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[1]: 'an isolated persistent sqlite DB (own port,' → 'the run's own persistent sqlite DB (own port,'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1407,7 +1425,7 @@ "title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1437,7 +1455,7 @@ "UNPARSED: POST 'com.example.qa-manifest-probe-wsrange' with engines: { protocol: 'workspace:*' } → expect install, plus the `declares an unrecognized engines.protocol range` warning. This is the direction that would break every workspace-linked package if it inverted", "NAMESPACE OWNERSHIP, three legs: (a) POST 'com.example.qa-manifest-probe-b' declaring namespace 'qa_mfp' — the one probe A already owns → expect refusal naming the namespace, the current owner and the incoming id; (b) re-POST probe A's OWN manifest unchanged (same id, same namespace) as a reinstall — POST /api/v1/packages?overwrite=true, since a plain re-POST of an installed id meets the install door's own already-installed refusal (api-backend.package-rest-lifecycle) before the namespace gate — → expect NO namespace conflict (reinstall/HMR is a normal path); (c) POST 'com.example.qa-manifest-probe-shared' declaring a shareable platform namespace (base / system / sys) → expect NO conflict (shareable namespaces are exempt)", "NAMESPACE DERIVATION: POST id 'com.example.leave' with NO namespace declared → GET it back and read manifest.namespace; then POST 'com.example.holiday' WITH an explicit namespace 'timeoff' → GET it back and confirm the declared value survived untouched", - "teardown: DELETE every scratch package created above, or simply discard the isolated file DB — the cheaper path an isolated boot makes free. ⚠️ Several legs here are NOT re-runnable in place (step 8b depends on probe A still being installed, and 8a stops being a conflict once probe A is gone), so a re-run starts from a clean DB rather than from the tail of the previous one" + "teardown: DELETE every scratch package created above, or simply discard the run's own file DB — the cheaper path a boot on its own port and DB makes free. ⚠️ Several legs here are NOT re-runnable in place (step 8b depends on probe A still being installed, and 8a stops being a conflict once probe A is gone), so a re-run starts from a clean DB rather than from the tail of the previous one" ], "acceptance": [ { @@ -1527,6 +1545,12 @@ "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 6, + "date": "2026-10-21", + "change": "Respelled — steps[9]: 'or simply discard the isolated file DB' → 'or simply discard the run's own file DB'; steps[9]: 'the cheaper path an isolated boot makes free' → 'the cheaper path a boot on its own port and DB makes free'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1535,14 +1559,14 @@ "title": "The activation ledger's registration home: sys_metadata_activation is registered by PlatformObjectsPlugin under its OWN manifest, so packaged disable works with or without the automation service — one owner, datasource binding carried across the move", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], "fixtures": { "app": "showcase", "requires": [ - "the with-automation composition is any stock boot: `objectstack dev`/`os serve` composes AutomationServicePlugin whenever the app requires the 'automation' capability token (packages/cli/src/commands/serve.ts CAPABILITY_PROVIDERS), and the showcase does — so the live-boot legs below need nothing beyond an isolated stock boot", + "the with-automation composition is any stock boot: `objectstack dev`/`os serve` composes AutomationServicePlugin whenever the app requires the 'automation' capability token (packages/cli/src/commands/serve.ts CAPABILITY_PROVIDERS), and the showcase does — so the live-boot legs below need nothing beyond a stock boot on its own port and DB", "the NO-automation composition — the one #12359 measured the 503 on — is a showcase-derived configuration that does not declare automation, booted with `bootStack` from @objectstack/verify: the showcase with the 'automation' token taken out of its `requires`, and with the plugins of its own `plugins` array that depend on com.objectstack.service-automation left out, which is exactly how the pinned dogfood suite constructs it (packages/qa/dogfood/test/packaged-activation-ledger-reach.dogfood.test.ts#showcaseWithoutAutomation). `bootStack(showcaseStack)` with the `automation` option omitted is NOT that composition: since #22301 bootStack composes what `os serve` composes, so the showcase's own `requires` mounts the automation service (packages/verify/src/harness.ts; the option only adds the service, never removes it)" ], "knownGaps": [ @@ -1551,7 +1575,7 @@ }, "steps": [ "run the pinned suite: pnpm --filter @objectstack/dogfood exec vitest run test/packaged-activation-ledger-reach.dogfood.test.ts — BOTH describes: '#12359 — actions and NO automation service' (the 503-turnaround leg, booted on the showcase-derived configuration that does not declare automation) and '#12159 Part 1 — a composition WITH automation' (the move's second end); capture the full output", - "live with-automation boot (stock `objectstack dev`, isolated port/DB), as admin: POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {\"enabled\":false} → 200; GET /api/v1/data/sys_metadata_activation and capture the row (metadata_type 'action', name showcase_mark_done, active false/0) together with its KEY SET — organization_id is ABSENT from it, the ledger having no tenant column at all (#15024 / ADR-0131 D7). ⛔ Never capture the tenant half as a value: `row.organization_id ?? null` answers `null` for a column that does not exist, so a value read passes while measuring nothing (the sibling item platform-core.activation-ledger-row-contract owns the schema-side probe)", + "live with-automation boot (stock `objectstack dev`, on its own port and DB), as admin: POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {\"enabled\":false} → 200; GET /api/v1/data/sys_metadata_activation and capture the row (metadata_type 'action', name showcase_mark_done, active false/0) together with its KEY SET — organization_id is ABSENT from it, the ledger having no tenant column at all (#15024 / ADR-0131 D7). ⛔ Never capture the tenant half as a value: `row.organization_id ?? null` answers `null` for a column that does not exist, so a value read passes while measuring nothing (the sibling item platform-core.activation-ledger-row-contract owns the schema-side probe)", "same boot, the flow half: POST /api/v1/automation/showcase_task_completed/toggle {\"enabled\":false} → 200; re-read the ledger — a metadata_type 'flow' row for showcase_task_completed appears BESIDE the action row, and the flow-name and action-name lists never cross (the discriminator is load-bearing, not decorative)", "restore both switches ({\"enabled\":true} / toggle on) — both rows persist with active true (updated, not deleted); leave the boot as found", "ownership is proven by the boot itself plus the registry read: the stock boot composes BOTH PlatformObjectsPlugin (serve auto-injects it) and the automation service, and it comes up green — a second registrant of sys_metadata_activation would be a boot FAILURE (`registerObject` throws 'already owned by package …', ADR-0029 D3), never a benign duplicate; the dogfood suite additionally reads registry.getObjectOwner('sys_metadata_activation').packageId === 'com.objectstack.platform-objects.activation-ledger'", @@ -1639,6 +1663,12 @@ "date": "2026-10-16", "change": "acceptance[3].verify corrected (checklist-accuracy finding of QA run #23939, wave #23878). It called sys_automation_run 'the control proving the resolver is not answering undefined for everything', but the dogfood test asserts sys_automation_run is undefined as well — a same-answer parity check that cannot discriminate. The verify text now says so and rests the binding claim on the manifest-triple pin it already cited; the clause's assertion is unchanged", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'need nothing beyond an isolated stock boot' → 'need nothing beyond a stock boot on its own port and DB'; steps[1]: '(stock `objectstack dev`, isolated port/DB)' → '(stock `objectstack dev`, on its own port and DB)'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1647,14 +1677,14 @@ "title": "sys_metadata_activation row contract: reads open / generic-data-API writes 405, one row per (metadata_type, name) with NO tenant column on the table at all, re-enable updates never deletes, and rows exist ONLY for the metadata types a shipped door writes — flow, action, position, permission, sharing_rule (the ADR-0126 scope wall, as ADR-0131 D3 and D6 widen it)", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P2", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], "fixtures": { "app": "showcase", "requires": [ - "an isolated boot on a file DB (-d file:/tmp//data.db) WITH direct sqlite access to that file between boots: the object declares apiMethods ['get','list'], so REST can never stage the org-row and duplicate-row probes — same scratch-DB class of fixture platform-core.interrupted-migration-boot-report uses. On a stock boot no 'cloud' datasource is registered, so the table rides the global default driver and lives in the run's own file DB", + "a boot on its own port and file DB (-d file:/tmp//data.db) WITH direct sqlite access to that file between boots: the object declares apiMethods ['get','list'], so REST can never stage the org-row and duplicate-row probes — same scratch-DB class of fixture platform-core.interrupted-migration-boot-report uses. On a stock boot no 'cloud' datasource is registered, so the table rides the global default driver and lives in the run's own file DB", "the shipped switches as row producers: the flow toggle (POST /api/v1/automation/showcase_task_completed/toggle), the action activation door (POST /api/v1/actions/_activation/showcase_task/showcase_mark_done), and the security catalog activation door POST /api/v1/security/_activation/:type/:name for :type position, permission and sharing_rule (packages/runtime/src/domains/catalog-activation.ts — e.g. position legal, permission set showcase_auditor and the declared sharing rule share_new_inquiries_with_field_ops; ⛔ never admin_full_access, which the last-admin guard refuses to switch off). Their own door contracts belong to automation.packaged-flow-disable-durable, api-backend.packaged-action-disabled-dispatch and access-security.security-catalog-activation-and-clone-doors — this item owns the ROWS they leave behind" ], "knownGaps": [ @@ -1663,7 +1693,7 @@ ] }, "steps": [ - "boot isolated on the file DB; as admin flip EVERY shipped switch off — the flow toggle (showcase_task_completed), the action flip (showcase_mark_done), and the catalog door for one position, one permission set and one declared sharing rule (POST /api/v1/security/_activation/position/legal, /permission/showcase_auditor and /sharing_rule/share_new_inquiries_with_field_ops, body {\"enabled\": false}); capture the five 200s", + "boot on its own port with that file DB; as admin flip EVERY shipped switch off — the flow toggle (showcase_task_completed), the action flip (showcase_mark_done), and the catalog door for one position, one permission set and one declared sharing rule (POST /api/v1/security/_activation/position/legal, /permission/showcase_auditor and /sharing_rule/share_new_inquiries_with_field_ops, body {\"enabled\": false}); capture the five 200s", "read the ledger through the open read path: GET /api/v1/data/sys_metadata_activation — capture every row; each carries the four declared ledger columns (metadata_type, name, package_id, active) beside the driver's id and the injected audit family, and NO tenant column: organization_id is ABSENT from the row's key set, never present-and-NULL on it. ⛔ Assert the KEY SET, never a value read — a value assertion of the shape `row.organization_id ?? null` is `null` for a column that does not exist, so it passes while measuring nothing (the dogfood pin was inverted into a key-set assertion for exactly this reason)", "scope wall sweep: the distinct metadata_type set over ALL rows is exactly {'flow','action','position','permission','sharing_rule'} — the five values the shipped doors write. Any other value (hook/capability/tool/skill/…) is a FAIL of the model, not a coverage discovery: hooks are code-only by ADR-0126 §3 amendment ruling 1 (no ledger row, no disable switch), and a capability is code-only on the regime with no switch (catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS). Then probe the door's own wall: POST /api/v1/security/_activation/capability/showcase.export_data {\"enabled\": false} answers 400 naming the three types it switches, and POST /api/v1/security/_activation/position/everyone {\"enabled\": false} answers 400 (an audience anchor) — re-read the ledger: neither left a row. Position and permission rows are no longer foreign here: ADR-0131 D3 moved the on/off switch of a position and of a permission set off the catalog row into this ledger (regime C as ADR-0131 D6 amends ADR-0126 §3; readDisabledCatalogNames in packages/core/src/security/resolve-authz-context.ts reads these two types and never the row's active column), which supersedes the 2026-08-26 #12159 ruling that a packaged permission set rides its kind's own active field. sharing_rule rows are not foreign either: ADR-0131 D6 charts a package-declared sharing rule under regime C, and the same door writes its row (packages/runtime/src/domains/catalog-activation.ts, CATALOG_ACTIVATION_SUBJECTS) — an organization-authored sys_sharing_rule row keeps its own switch and writes none", "write-refusal probes, all three verbs: POST /api/v1/data/sys_metadata_activation {metadata_type:'action',name:'qa_probe',package_id:'qa',active:false}, PATCH /api/v1/data/sys_metadata_activation/ {active:true}, DELETE /api/v1/data/sys_metadata_activation/ — each answers 405 naming the refused operation and the allowed set; re-read confirms every row unchanged", @@ -1774,6 +1804,12 @@ "date": "2026-10-19", "change": "the scope wall widened from four metadata_type values to five — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.activation-ledger-row-contract; wave anchor #24579 §3, which lists it under access-security). The security catalog activation door now also accepts :type sharing_rule (packages/runtime/src/domains/catalog-activation.ts CATALOG_ACTIVATION_SUBJECTS, ADR-0131 D6 regime C) and writes its row to this ledger through the same ObjectStoreMetadataActivationStore — a fifth writer the item's hand-enumerated variants did not carry, the case negative[4] says is a checklist defect. Title, steps[0] and [2], fixtures.requires[1], knownGaps[0], acceptance[4] (clause, verify) and negative[5] now name sharing_rule, variants gains it, and steps[0] flips the showcase's declared rule share_new_inquiries_with_field_ops. QA run #24601 drove that switch and read the row {metadata_type: sharing_rule, active: false}. No other clause changed", "ref": "#24632" + }, + { + "revision": 6, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'an isolated boot on a file DB (-d' → 'a boot on its own port and file DB (-d'; steps[0]: 'boot isolated on the file DB;' → 'boot on its own port with that file DB;'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -1861,7 +1897,7 @@ "title": "Marketplace install-local: the wiring matrix mounts the right arms per OS_CLOUD_URL, the air-gapped inline install works with NO control plane, reseed/purge touch exactly the sample rows, every mutating door is manage_metadata-gated both sides, and the listing projects installedBy/storageDir per principal", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": [ @@ -1873,7 +1909,7 @@ "fixtures": { "app": "showcase", "requires": [ - "TWO isolated boots of the same app: boot A = plain `objectstack dev` with NO OS_CLOUD_URL (resolveCloudUrl() falls back to DEFAULT_CLOUD_URL — truthy — so the CLOUD arm mounts: proxy + install-local + cloud-connection + runtime-config); boot B = the same line with OS_CLOUD_URL=off exported (the offline arm: install-local pinned to the 'off' control plane + runtime-config ONLY — nothing that dials out mounts). The matrix is decided by Serve.planMarketplaceWiring, pure and testable (packages/cli/src/commands/serve.ts). ⚠️ On showcase the boot-A cloud arm is the HOST's, not the CLI's: examples/app-showcase/objectstack.config.ts mounts MarketplaceProxyPlugin, MarketplaceInstallLocalPlugin and CloudConnectionPlugin itself whenever resolveCloudUrl() is truthy, and RuntimeConfigPlugin always, and planMarketplaceWiring checks presence — so on boot A the CLI wires none of the four, and on boot B its offline arm adds install-local alone (pinned to the 'off' control plane) beside the host's runtime-config. The observable route matrix is the same either way", + "TWO boots of the same app, each on its own port and DB: boot A = plain `objectstack dev` with NO OS_CLOUD_URL (resolveCloudUrl() falls back to DEFAULT_CLOUD_URL — truthy — so the CLOUD arm mounts: proxy + install-local + cloud-connection + runtime-config); boot B = the same line with OS_CLOUD_URL=off exported (the offline arm: install-local pinned to the 'off' control plane + runtime-config ONLY — nothing that dials out mounts). The matrix is decided by Serve.planMarketplaceWiring, pure and testable (packages/cli/src/commands/serve.ts). ⚠️ On showcase the boot-A cloud arm is the HOST's, not the CLI's: examples/app-showcase/objectstack.config.ts mounts MarketplaceProxyPlugin, MarketplaceInstallLocalPlugin and CloudConnectionPlugin itself whenever resolveCloudUrl() is truthy, and RuntimeConfigPlugin always, and planMarketplaceWiring checks presence — so on boot A the CLI wires none of the four, and on boot B its offline arm adds install-local alone (pinned to the 'off' control plane) beside the host's runtime-config. The observable route matrix is the same either way", "a compiled package artifact for the inline install: examples/app-crm/dist/objectstack.json. The tree's own `pnpm build` writes it (the root turbo build runs @objectstack/example-crm's `objectstack build` script), and the file is gitignored (examples/*/dist/), so only an unbuilt tree lacks it; then `pnpm -C examples/app-crm build` writes it. A run under a no-rebuild brief installs the existing file, as QA run #24779 did (the air-gapped path the #8343 fix exists for — `handleInstall`'s inline branch never reads the cloud URL). Both boots that install run from a SCRATCH COPY of the app, never the shared tree: install-local caches the manifest under the working directory's .objectstack/installed-packages (packages/cloud-connection/src/local-manifest-source.ts#DEFAULT_INSTALLED_PACKAGES_DIR, resolved against process.cwd()), which a per-boot OS_HOME / OS_STORAGE_LOCAL_ROOT does not redirect (RUNNER.md environment facts)", "admin credentials passed to the CLI door (--email/--password or OS_RUNTIME_EMAIL/OS_RUNTIME_PASSWORD) — the runtime refuses an unauthenticated install with a 401 that names exactly that remedy (packages/cli/src/commands/package/install.ts)" ], @@ -1995,6 +2031,12 @@ "date": "2026-10-20", "change": "negative[5] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a browse-leg upstream failure on an egress-blocked runner is not a product defect; score blocked(environment)) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (on an egress-blocked or air-gapped runner the leg scores blocked(environment), never a product FAIL; what this item scores about browse is the MOUNT), so nothing was added. No acceptance clause changed", "ref": "#24963" + }, + { + "revision": 8, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'TWO isolated boots of the same app:' → 'TWO boots of the same app, each on its own port and DB:'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -2113,18 +2155,18 @@ "title": "Runtime config boot read: GET /api/v1/runtime/config answers 200 ANONYMOUSLY with cloud URL, capability flags, branding and telemetry posture; the legacy /api/v1/studio/runtime-config alias is byte-identical (same handler); nothing credential-shaped is in the body", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": ["anonymous", "seeded admin (control only)"], "fixtures": { "app": "showcase", "requires": [ - "any stock isolated boot — a plain `objectstack dev` mounts RuntimeConfigPlugin through the marketplace wiring's cloud arm, and an OS_CLOUD_URL=off boot mounts it through the offline arm (#8389), so the route is served on BOTH stock shapes" + "any stock boot on its own port and DB — a plain `objectstack dev` mounts RuntimeConfigPlugin through the marketplace wiring's cloud arm, and an OS_CLOUD_URL=off boot mounts it through the offline arm (#8389), so the route is served on BOTH stock shapes" ] }, "steps": [ - "on a stock isolated boot, with NO session: GET /api/v1/runtime/config — capture status, headers and full body", + "on a stock boot on its own port and DB, with NO session: GET /api/v1/runtime/config — capture status, headers and full body", "confirm the payload shape: features (installLocal, marketplace, …), branding (productName, productShortName, pwaDescription and pwaThemeColor always; logoUrl, faviconUrl and brandColor ONLY when configured — by the plugin config or their env vars — since an unset one serialises as absent, which is not a missing key; `stage` likewise only when set — packages/cloud-connection/src/runtime-config-plugin.ts), telemetry (errorReporting when configured), and the cloud/control-plane URL field", "GET /api/v1/studio/runtime-config with the same (absent) auth — diff the two bodies byte for byte (the alias is mounted with the SAME handler instance; ledgered as its own row because it is its own wire path)", "scan the serialised body for credential-shaped content: no session token, no secret handle plaintext, no Authorization material, no sys_secret ciphertext; telemetry.errorReporting.dsn, when present, is a PUBLIC client key by design — record its presence, it is not a finding", @@ -2189,6 +2231,12 @@ "date": "2026-10-20", "change": "negative[3] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a filing rule (a flag/route contradiction belongs to the marketplace item's clause 1; do not double-file) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#24963" + }, + { + "revision": 4, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'any stock isolated boot —' → 'any stock boot on its own port and DB —'; steps[0]: 'on a stock isolated boot, with NO session' → 'on a stock boot on its own port and DB, with NO session'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -2597,14 +2645,14 @@ "title": "ADR-0057 lifecycle sweep: backdated telemetry rows past retention are reaped BOUNDED under a system context on the hourly sweep, onlyWhen scoping spares live rows, an archive-declaring object is NEVER hot-deleted unarchived (retained + reported archive-pending when no archive datasource exists), the transient class stays on the primary, and OS_LIFECYCLE_DISABLED=1 disarms the sweep", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": ["operator (local shell + direct sqlite access)", "seeded admin (API reads)"], "fixtures": { "app": "showcase", "requires": [ - "an isolated boot on a file DB (-d file:/tmp//data.db) WITH direct sqlite access between boots — retention judges created_at, which no REST write lets a caller backdate, so the crash-shaped fixtures are hand-seeded into the sqlite files (the same fixture class platform-core.interrupted-migration-boot-report proved). ⚠ TWO files: on `os dev` with a file-backed primary the CLI provisions a sibling telemetry datasource, data.telemetry.db beside data.db (packages/cli/src/utils/telemetry-datasource.ts#resolveTelemetryDbPath), and the engine routes every telemetry/audit/event-classed object there — so sys_automation_run and sys_audit_log rows are seeded into data.telemetry.db, while the transient sys_inbox_message stays in data.db", + "a boot on its own port and file DB (-d file:/tmp//data.db) WITH direct sqlite access between boots — retention judges created_at, which no REST write lets a caller backdate, so the crash-shaped fixtures are hand-seeded into the sqlite files (the same fixture class platform-core.interrupted-migration-boot-report proved). ⚠ TWO files: on `os dev` with a file-backed primary the CLI provisions a sibling telemetry datasource, data.telemetry.db beside data.db (packages/cli/src/utils/telemetry-datasource.ts#resolveTelemetryDbPath), and the engine routes every telemetry/audit/event-classed object there — so sys_automation_run and sys_audit_log rows are seeded into data.telemetry.db, while the transient sys_inbox_message stays in data.db", "stock lifecycle declarations as the subjects: sys_automation_run (class telemetry, retention 30d, onlyWhen status ∈ completed|failed — the MIXED-table scoping), sys_audit_log (class audit, archive after 90d to the 'archive' datasource — the never-hot-delete subject), sys_inbox_message (class transient)", "patience for the timer: the first sweep fires DEFAULT_LIFECYCLE_INITIAL_DELAY_MS = 60s after engine start, then hourly — every probe boot needs a ≥60s settle before reading results (eventual-consistency: do not read the absence of a reap before the sweep ran)" ], @@ -2724,6 +2772,12 @@ "date": "2026-10-20", "change": "automated.ref says which clause each part scores, and acceptance[5].verify names its check — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: 'acceptance[5] is checked by the item's enumSource gate, not a pin'), round 15 of wave anchor #24579. The pin drives LifecycleService.sweep over a fake engine: acceptance[2]'s pinned half, [3], and negative[0]–[1]. acceptance[5]'s count is held by the enumSource variants-freshness ratchet of check:platform-checklist (LifecycleClassSchema, expect 5), and its engine-set half by reading packages/objectql/src/engine.ts. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 6, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'an isolated boot on a file DB (-d' → 'a boot on its own port and file DB (-d'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -2732,7 +2786,7 @@ "title": "Seed mode matrix (insert/update/upsert/replace/ignore): each mode does exactly its verb through the ONE SeedLoaderService, no-op replays skip instead of churning, ignore reports skips — and `replace` is DECLARED delete-all-then-insert but IMPLEMENTED as bare insert (no caller clears the table): the posture is recorded, never ticked as scoped deletion", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -2756,7 +2810,7 @@ "INSERT: seed draft (mode insert) re-declaring A → a SECOND A-named row appears (no matching in insert mode); record the duplication; if the object declares a unique key on the natural key instead, record the refusal — either shape is the mode's real contract", "REPLACE (expected-posture probe): add one row F by hand through /api/v1/data (a row the seed does NOT declare); publish a seed draft (mode replace) declaring only G → capture what happens to F. At head: F SURVIVES and G is inserted — the loader's replace case is bare insert with the comment 'caller should have cleared the table', and NO caller in the tree clears anything. Record the declared-vs-implemented gap; do NOT tick 'replace scoped to its own object' (there is no deletion to scope) and do NOT file F's survival as data loss (it is the opposite)", "idempotence tie-in: re-publish the upsert draft unchanged TWICE. The first replay may legitimately write, because steps 2–4 rewrote A's rows (update mode set A to a2, insert mode added a second A-named row), so it re-converges them to the seed; the SECOND replay is the idempotence probe — zero inserts and zero updates (all no-op skips), every row's updated_at untouched (the churn-avoidance isNoOpReplay exists for exactly this)", - "teardown: the writable package's own delete, or discard the isolated DB" + "teardown: the writable package's own delete, or discard the run's own DB" ], "acceptance": [ { @@ -2846,6 +2900,12 @@ "date": "2026-10-20", "change": "steps[0] names the namespace prefix; negative[2] and negative[3] removed. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The scratch object needs the package namespace prefix: QA run #24841's unprefixed qa_seedprobe draft saved 200, and its publish answered 422 NAMESPACE_PREFIX. Read at base feb09db14a: the publish doors judge objectNamespacePrefixViolation (protocol.ts, ADR-0129 D2), and a runtime-created package with no namespace takes deriveNamespaceFromPackageId. (2) negative[2] and negative[3] stated a recording rule and a fixture rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[2]'s text already stands, unchanged in substance, in steps[5] and acceptance[4] (do not tick 'replace scoped to its own object'; there is no deletion to scope). negative[3]'s stands in fixtures.requires[1], which now carries its reason as well (it contaminates platform-core.seed-integrity's baselines). No acceptance clause changed", "ref": "#24963" + }, + { + "revision": 5, + "date": "2026-10-21", + "change": "Respelled — steps[7]: 'or discard the isolated DB' → 'or discard the run's own DB'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 211d546c5ae..2bb97aee01d 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -4147,7 +4147,7 @@ "title": "fieldGroups[].visibleWhen section predicate: both spellings accepted, the parse normalizes to the envelope, retired visibleOn refused with the located prescription, derivation carries it verbatim — the console gates the section on the form AND the detail page (objectui#11630 fixed)", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -4156,7 +4156,7 @@ "fixtures": { "app": "showcase", "requires": [ - "an admin session holding `manage_metadata` (PUT /api/v1/meta/:type/:name is capability-gated, packages/rest/src/rest-route-ledger.ts) and an isolated boot — the item authors a scratch object", + "an admin session holding `manage_metadata` (PUT /api/v1/meta/:type/:name is capability-gated, packages/rest/src/rest-route-ledger.ts) and a boot on its own port and DB — the item authors a scratch object", "the stock fieldGroups declarations as authoring models: examples/app-showcase/src/data/objects/contact.object.ts and semantic-zoo.object.ts — NEITHER carries visibleWhen" ], "knownGaps": [ @@ -4253,6 +4253,12 @@ "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 6, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'and an isolated boot — the item' → 'and a boot on its own port and DB — the item'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, @@ -4261,7 +4267,7 @@ "title": "Field-level `unique` on the write path: colliding writes refused 409 UNIQUE_VIOLATION on both driver families, NULL-distinct, organization-scoped by default, whole-batch refusal, autonumber resync converges", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": [ @@ -4270,7 +4276,7 @@ "fixtures": { "app": "showcase", "requires": [ - "an isolated dev boot on the stock SQLite store (no boot door accepts the in-memory engine any more — every one refuses it and names SQLite, 9a4182a75) + a manage_metadata session — the item authors its own scratch object", + "a dev boot on its own port and DB, on the stock SQLite store (no boot door accepts the in-memory engine any more — every one refuses it and names SQLite, 9a4182a75) + a manage_metadata session — the item authors its own scratch object", "scratch qa_unique object authored inline: sku text unique:true, gsku text unique:'global', nick text (no unique) — NO showcase object declares field-level unique (grepped; invoice.object.ts only mentions the word in a comment)" ], "knownGaps": [ @@ -4389,6 +4395,12 @@ "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated on ' → 'boot the showcase on its own port and DB, on '. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 8, + "date": "2026-10-21", + "change": "Respelled — fixtures.requires[0]: 'an isolated dev boot on the stock SQLite store' → 'a dev boot on its own port and DB, on the stock SQLite store'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", + "ref": "#25018" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 65672744940..5fdc0334136 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -1360,7 +1360,7 @@ "title": "The metadata governance sweep: GET /meta/diagnostics reports every STORED item failing spec validation with located errors, refuses outages and bad spellings loudly instead of answering '0 problems', and the Studio diagnostics page + directory tile badges paint the same numbers", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P2", "surface": "mixed", "personas": ["admin (the governance surface is Studio's)"], @@ -1371,7 +1371,7 @@ "the stock metadata corpus (any booted showcase — the sweep runs over whatever the store holds)" ], "knownGaps": [ - "THE POSITIVE ENTRY LEG NEEDS A PLANTED ROW, AND ONE CAN BE PLANTED ON AN ISOLATED SCRATCH FILE DB (QA run #24604): every authoring door validates at save (the 422 gate studio-authoring.authoring-validation-not-persisted proves), so a stock store holds NO invalid item and the sweep legitimately answers clean — entries:[], total:0 — and sys_metadata is a storage table, not a data-plane object, so /api/v1/data cannot plant one. The recipe: save a VALID view through the door WITH ?package= (DirectoryPage is always package-scoped, so a package-less row never reaches a tile badge), then rewrite that row's sys_metadata.metadata in the scratch SQLite file (e.g. config.columns[0].field = 123); the next sweep reports it with no restart, and deleting the row restores a clean sweep. ⛔ Only on the run's own file DB, never a shared one. The located-error half is also unit-pinned at the producer: packages/metadata-protocol/src/metadata-diagnostics.union-issues.test.ts (computeMetadataDiagnostics names the offending key per union branch, and decorateMetadataItem carries it). That pin scores neither the sweep entry nor the page's deep link", + "THE POSITIVE ENTRY LEG NEEDS A PLANTED ROW, AND ONE CAN BE PLANTED ON THE RUN'S OWN SCRATCH FILE DB (QA run #24604): every authoring door validates at save (the 422 gate studio-authoring.authoring-validation-not-persisted proves), so a stock store holds NO invalid item and the sweep legitimately answers clean — entries:[], total:0 — and sys_metadata is a storage table, not a data-plane object, so /api/v1/data cannot plant one. The recipe: save a VALID view through the door WITH ?package= (DirectoryPage is always package-scoped, so a package-less row never reaches a tile badge), then rewrite that row's sys_metadata.metadata in the scratch SQLite file (e.g. config.columns[0].field = 123); the next sweep reports it with no restart, and deleting the row restores a clean sweep. ⛔ Only on the run's own file DB, never a shared one. The located-error half is also unit-pinned at the producer: packages/metadata-protocol/src/metadata-diagnostics.union-issues.test.ts (computeMetadataDiagnostics names the offending key per union branch, and decorateMetadataItem carries it). That pin scores neither the sweep entry nor the page's deep link", "severity=warning ADDS NO ROWS ON TODAY'S PRODUCERS: the sweep's contract includes warning-only entries (protocol.ts getMetaDiagnostics: valid items with diagnostics.warnings), but computeMetadataDiagnostics emits errors only and the docstring reserves warnings 'for a future lint layer' — no read-decoration producer emits a warning at head. The severity clause asserts the MECHANISM (param accepted, error rows unchanged), not warning rows; a warning row appearing is a finding that the lint layer landed (revise this item), never noise", "REPO OWNERSHIP (ADR-0054): the API half — route, sweep, stats, refusals — is objectstack's; the page markup, grouping, deep links, badges are objectui's (metadata-admin/DiagnosticsPage.tsx, DirectoryPage.tsx, useMetadata.ts). A locator or copy fix goes to objectui" ] @@ -1465,7 +1465,8 @@ { "revision": 4, "date": "2026-10-16", "change": "acceptance[1].verify names the read that carries the envelope lock (checklist-accuracy finding 2 of QA run #23942, wave #23878). It told the runner to compare stats.locked against the lock GET /api/v1/meta/TYPE/NAME publishes, but that default read is the cached arm and carries no `lock` by design; the envelope lock is on GET /api/v1/meta/TYPE/NAME/layers, which is where #23942 read it (object 107, view 0, flow 32 locked, matching stats). The comparison itself is unchanged", "ref": "#24036" }, { "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[6], steps[8] and acceptance[6] (clause, verify) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), the round-2 §3 note that knownGaps[0] is wrong. The invalid stored row is plantable in-run on an isolated scratch file DB: save a valid view through the door with ?package=, rewrite its sys_metadata.metadata, and the next sweep lists it — the run scored acceptance[6] pass that way, deep link and badge included. The directory always repairs ?package= to a project package, so the red button is judged per package scope; rootless union-parent entries may ride beside the located leaf. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" }, { "revision": 6, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[5] cited lines 242-254 of objectui DirectoryPage.tsx and line 982 of app-shell's console AppContent.tsx in the comma-range spelling (authored in 45397edf53 as the lists 133-158 and 242-254, and 882 and 982). At the objectui pin that commit shipped, 9602dc820450, 242-254 was the diagnostics link that renders only when diagSummary.total is above 0, 133-158 the package-scoped useGlobalDiagnostics sweep, and 982 the nested metadata route block that holds _diagnostics; at the shipped pin 12dc7b47c340 all have moved. source[5] now names the MetadataDirectoryPage component's sweep and link, and the AppContent component's metadata/_diagnostics route. objectui is read at the shipped pin 12dc7b47c340 and cited in prose, because no objectui anchor resolves in this gate. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" }, + { "revision": 8, "date": "2026-10-21", "change": "Respelled — fixtures.knownGaps[0]: 'ON AN ISOLATED SCRATCH FILE DB' → 'ON THE RUN'S OWN SCRATCH FILE DB'. PR #25003's out-of-scope finding (round 17 of wave anchor #24579; ACCEPT 6304389370, landing record 6304641541): 22 non-history texts in 17 items named the own-port boot or its DB with 'isolated' in other words, the phrasing round 12 found a runner reading as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names the boot or the DB by what it is, the run's own port and DB (RUNNER.md's trap-table counter: own port/DB, skill §0), and names no posture. Read at base 93e6c1a5f8. No clause changed", "ref": "#25018" } ] }, {