From f894b3d2b5e1de7de8bc101d030670f4a2041302 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 15:01:46 +0000 Subject: [PATCH 1/2] docs(qa): lift blocked on four fixture-blocked checklist items with the recipes run 21845 proved Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv --- .../platform-checklist/areas/approvals.json | 63 ++++++++++++------- .../areas/identity-auth.json | 37 ++++++----- .../areas/records-forms.json | 34 +++++----- 3 files changed, 80 insertions(+), 54 deletions(-) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index ced668055bd..be86e8f1d59 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -97,28 +97,31 @@ "title": "M-of-N quorum approves at the threshold; one rejection vetoes", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "browser", "personas": ["three DISTINCT users each holding one of the approver positions"], "fixtures": { "app": "showcase", - "requires": ["a quorum (minApprovals: 2) request whose three position approvers resolve to three distinct users"], + "requires": [ + "a quorum (minApprovals: 2) request whose three position approvers resolve to three distinct users", + "RUNTIME FIXTURE (admin API only, stock boot, nothing seeded or committed; proved by run #21845) — apply it before the quorum steps, in this order: (1) bind a second committee position holder: as the admin, POST /api/v1/data/sys_user_position {user_id: , position: 'finance', organization_id: } — admin + Ada is a real 2-of-2 slate; (2) for the full 2-of-3, invite + sign up a third user and bind them to position 'legal' the same way; prove each persona with GET /api/v1/auth/get-session before it acts; (3) DISABLE the competing per-group sign-off flow for the run: POST /api/v1/automation/showcase_expense_signoff/toggle {\"enabled\": false} — it has no amount ceiling and opens its request first on any >= 5000 report, so without this the quorum flow's open fails DUPLICATE_REQUEST; re-enable it with {\"enabled\": true} afterwards; (4) open a FRESH request after the bindings, because a slate is fixed when its request opens: create a draft showcase_expense_report, add one showcase_expense_line of 6000 (total_amount >= 5000, the committee threshold), then PATCH the report's status to 'submitted'. Use a second fresh request the same way for the veto case" + ], "knownGaps": [ "the FLOW half of the fixture is real and seeded — showcase_committee_quorum (2-of-3 quorum over positions manager/finance/legal, examples/app-showcase/src/automation/flows/index.ts) is launched on EXP-DEMO by seed-approval-demo.ts. What still collapses the slate is POSITION ASSIGNMENT, not a missing flow or a pending design call: the admin holds all three committee positions (ADMIN_APPROVAL_POSITIONS = manager/finance/legal/exec, seed-approval-demo.ts) and Ada Auditor deliberately holds ONLY auditor, so all three slots resolve to one person and the runtime clamps 2-of-3 to 1-of-1", - "the remaining unblock is a SEED LINE: assign Ada 'finance' (or 'legal') in seed-approval-demo.ts. She has been a real credential login on a stock boot since #9308, and the per-group demo is untouched by the grant — its finance GROUP routes position 'auditor' (flows/index.ts), not position 'finance'. That one line yields a two-distinct-holder slate (admin + Ada), turning minApprovals 2 into a real 2-of-2: the threshold tally, actor distinctness and the one-rejection veto all become demonstrable. Only the 'third approver's pending task is closed by finalization' clause needs more — a THIRD distinct holder (e.g. a legal-holding persona), an equally mechanical seed addition" + "CLOSED for runs by the runtime fixture in requires (#21845) — the seed gap itself remains and is why the fixture exists: the remaining unblock is a SEED LINE: assign Ada 'finance' (or 'legal') in seed-approval-demo.ts. She has been a real credential login on a stock boot since #9308, and the per-group demo is untouched by the grant — its finance GROUP routes position 'auditor' (flows/index.ts), not position 'finance'. That one line yields a two-distinct-holder slate (admin + Ada), turning minApprovals 2 into a real 2-of-2: the threshold tally, actor distinctness and the one-rejection veto all become demonstrable. Only the 'third approver's pending task is closed by finalization' clause needs more — a THIRD distinct holder (e.g. a legal-holding persona), an equally mechanical seed addition. Landing those seed lines (and an amount ceiling on the per-group flow) retires the runtime fixture; until then a run states that its verdict rests on the runtime fixture, and the seeded EXP-DEMO request stays the clamp contrast because its slate was fixed when it opened at seed time" ] }, - "blocked": { "by": "fixture", "ref": "#3358 — slate collapse is now a one-line seed gap, not a design call: the quorum flow ships (showcase_committee_quorum) and Ada is loginable (#9308); assign her a committee position for 2-of-2, add a third holder for full 2-of-3 (see knownGaps)" }, "steps": [ "boot showcase isolated (dogfood §0); sign in as the dev admin", "runnable today (the clamp contrast): GET the seeded showcase_committee_quorum request on EXP-DEMO (behavior quorum, minApprovals 2 over manager/finance/legal — all resolving to the admin); record the collapsed pending slate", "approve it ONCE as the admin and re-read: it finalizes immediately — the documented runtime clamp (minApprovals can never exceed the resolvable approver count), recorded as the CONTRAST, never as M-of-N proof", - "once the three-distinct-users fixture exists: as approver 1, POST /api/v1/approvals/requests/:id/approve — re-read: status STILL pending (1 of 2), approver 1 dropped from the slate", + "apply the runtime fixture (fixtures.requires, steps 1-4) and open a FRESH quorum request; then as approver 1, POST /api/v1/approvals/requests/:id/approve — re-read: status STILL pending (1 of 2), approver 1 dropped from the slate", + "repeat-actor probe: as the NON-PRIVILEGED approver who already decided (e.g. Ada), approve the same request again — refused (403) and the tally unchanged; a user holding none of the routed positions is refused the same way. ⛔ Never use the platform/tenant admin for this probe — an admin decision is the documented override (#3424) and finalizes by design", "as approver 2, approve — re-read: status=approved, remaining pending task for approver 3 closed", "GET /:id/actions — exactly two approve rows from two distinct actors", "on a SECOND quorum request: as any single approver, POST /reject — re-read immediately", - "screenshot the drawer's M-of-N progress at each stage alongside the API reads" + "screenshot the drawer's tally line ('Approvals — x of y') at each stage alongside the API reads — the drawer subtitle '(2 of 3)' is the flow's static text, not the tally" ], "acceptance": [ { @@ -136,37 +139,43 @@ { "clause": "a single rejection vetoes even with quorum-1 approvals already recorded", "oracle": "api", - "verify": "on the second request: status flips to rejected on the first reject; no further tasks remain actionable; the flow run resumes down its reject edge", + "verify": "on the second request: status flips to rejected on the first reject; no further tasks remain actionable; the run goes paused → completed and the request is rejected. The reject EDGE itself is not observable — end nodes log no step — so the run's status transition is the run-side evidence", "evidence": "API read after the reject + run read" }, { "clause": "the third approver's pending task is closed by finalization, not left dangling", "oracle": "api", - "verify": "after quorum is met, the request no longer lists approver 3 in pending_approvers and their inbox 待我审批 count drops", + "verify": "after quorum is met, the request no longer lists approver 3 in pending_approvers and their inbox 'My Pending' tab count drops (the English console's label; 待我审批 only on a zh-CN session)", "evidence": "request read + inbox count" }, { "clause": "the drawer's M-of-N tally is server-computed and matches the API at each stage", "oracle": "screenshot", - "verify": "progress screenshot after each decision alongside the paired API read", + "verify": "after each decision, the drawer's 'Approvals — x of y' tally line alongside the paired API read; the drawer subtitle '(2 of 3)' is static flow text and is never read as the tally", "evidence": "screenshots + reads" } ], "negative": [ - "a second approval by the SAME user must not count twice toward the quorum — a request that finalizes on two approvals from one actor is a FAIL of the distinctness the tally claims", - "the clamp contrast must be filed as evidence of the fixture gap, never ticked as M-of-N passing — a run record marking this item pass on stock seeds is itself a FAIL of protocol" + "a second approval by the SAME NON-PRIVILEGED user must not count twice toward the quorum — the repeat decision is refused, and a request that finalizes on two approvals from one such actor is a FAIL of the distinctness the tally claims. The repeat actor must be a plain member holder: a platform/tenant admin's decision is the documented privileged override (#3424) and finalizes by design, recorded with via_override:true — that is never a distinctness FAIL", + "the clamp contrast must be filed as evidence of the fixture gap, never ticked as M-of-N passing — a run record marking this item pass on stock seeds WITHOUT the runtime fixture is itself a FAIL of protocol" ], "traps": ["seed-data-thin", "wrong-persona"], "source": [ "#3358 §1", "examples/app-showcase/src/automation/flows/index.ts#CommitteeQuorumFlow (CommitteeQuorumFlow, #3266)", "packages/spec/src/automation/approval.zod.ts#minApprovals (behavior 'quorum' + minApprovals clamp: 'Clamped at runtime so it can never exceed the resolvable approver count')", - "examples/app-showcase/src/security/seed-approval-demo.ts#admin (EXP-DEMO launch + admin position grants)" + "examples/app-showcase/src/security/seed-approval-demo.ts#admin (EXP-DEMO launch + admin position grants)", + "examples/app-showcase/src/automation/flows/index.ts#ExpenseSignoffFlow (the competing per-group flow the fixture disables — same trigger, no amount ceiling)", + "packages/runtime/src/domains/automation.ts (POST /automation/:name/toggle — the enablement door the fixture uses)", + "packages/plugins/plugin-security/src/objects/sys-user-position.object.ts (the position binding row the fixture writes: user_id / position / organization_id)", + "packages/plugins/plugin-approvals/src/approval-service.ts (privileged-override gate #3424, via_override on the action row)", + "#21845 (follow-up run: 2-of-2 and 2-of-3 PASS under the runtime fixture)" ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "initial import from #3358; carried the fixture blocker forward explicitly", "ref": "#3358" }, { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 3, "date": "2026-08-30", "change": "blocked ref re-worded to price the gap honestly: the old 'showcase design call pending' read as an open-ended product decision, but the quorum flow fixture ships (showcase_committee_quorum, 2-of-3 over manager/finance/legal) and Ada Auditor has been a real login since #9308 — the slate collapse survives only because seed-approval-demo.ts gives the admin all three committee positions and Ada only auditor. knownGaps now states the exact remaining cost (one seed line assigning Ada a committee position → 2-of-2 demonstrable; a third distinct holder for the full 2-of-3), verified against source including the per-group demo's non-collision (its finance group keys on position 'auditor')", "ref": "#sweep-2026-08-30" } + { "revision": 3, "date": "2026-08-30", "change": "blocked ref re-worded to price the gap honestly: the old 'showcase design call pending' read as an open-ended product decision, but the quorum flow fixture ships (showcase_committee_quorum, 2-of-3 over manager/finance/legal) and Ada Auditor has been a real login since #9308 — the slate collapse survives only because seed-approval-demo.ts gives the admin all three committee positions and Ada only auditor. knownGaps now states the exact remaining cost (one seed line assigning Ada a committee position → 2-of-2 demonstrable; a third distinct holder for the full 2-of-3), verified against source including the per-group demo's non-collision (its finance group keys on position 'auditor')", "ref": "#sweep-2026-08-30" }, + { "revision": 4, "date": "2026-10-05", "change": "blocked lifted: run #21845 passed 2-of-2 and 2-of-3 on fresh requests under a runtime fixture (admin-API position bindings, a third signed-up holder, and the per-group sign-off flow disabled because it opens first on any >= 5000 report and the quorum open then fails DUPLICATE_REQUEST); the recipe is written into fixtures.requires and the seed-gap knownGap is marked CLOSED-for-runs, not deleted. Three clauses re-pointed from the same run: negative[0] now requires a NON-PRIVILEGED repeat actor, because an admin decision is the documented override (#3424, via_override:true) and finalizes by design; acceptance[2] asserts the run's paused → completed transition with the request rejected, since the reject edge is not observable (end nodes log no step); acceptance[3] names the English inbox tab 'My Pending', and acceptance[4] plus the screenshot step read the drawer's 'Approvals — x of y' line as the tally, not the static '(2 of 3)' subtitle (card #21851 entries 1, 5, 6, 7)", "ref": "#21845" } ] }, { @@ -813,31 +822,32 @@ "title": "A node's SLA escalation fires once past its timeout — the declared action runs and an escalate timeline row lands", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": ["dev admin"], "fixtures": { "app": "showcase", "requires": [ - "a scratch autolaunched flow in a WRITABLE package with an approval node carrying config.escalation {enabled:true, timeoutHours, action, escalateTo, notifySubmitter} (ApprovalEscalationSchema, packages/spec/src/automation/approval.zod.ts)", - "clock control OR fractional-hour support: timeoutHours has a min of 1 and the escalation sweep (ApprovalService.runEscalations, ESCALATION_JOB_NAME, ADR-0042) runs on an interval against this.clock.now(), so driving a request PAST its deadline needs an injected/advanced clock or a direct runEscalations() call with a clock whose now() is beyond slaDueAt = created_at + timeoutHours" + "one scratch autolaunched flow PER escalation action, each with an approval node carrying config.escalation {enabled:true, timeoutHours:1, action, escalateTo (reassign only), notifySubmitter:true} (ApprovalEscalationSchema, packages/spec/src/automation/approval.zod.ts), registered at runtime by the admin through POST /api/v1/automation — NO writable package is needed", + "clock control: timeoutHours has a min of 1 and the escalation sweep (ApprovalService.runEscalations, ESCALATION_JOB_NAME, ADR-0042) reads this.clock.now(), so the deadline is reached by moving the process's wall clock, never by waiting an hour", + "RUNTIME FIXTURE — wall-clock offset shim (proved by run #21845; a test fixture that lives in the run's scratch dir, ⛔ never committed and never a product change): a small CommonJS module loaded into the server with NODE_OPTIONS=--require that wraps the global Date constructor and Date.now so every reading adds an offset in milliseconds read from a file the runner controls, and sets Date.prototype.constructor to the wrapper; timers (setTimeout / setInterval) are untouched, and no product logic is changed. Start the boot with the offset at 0; advance by writing a new offset (e.g. +2h) to the file" ], "knownGaps": [ - "hour-granular SLAs are not drivable on stock fixtures in a single session: the minimum timeoutHours is 1 and the sweep uses real time, so reaching the deadline requires a timing harness (clock injection / controllable runEscalations) that stock showcase does not provide — the timeout-dependent clauses (2/3/4) run only under that harness; clauses 1 and 5 are runnable today" + "CLOSED for runs by the wall-clock offset shim in requires (#21845): hour-granular SLAs are not drivable on stock fixtures without it — the minimum timeoutHours is 1 and the sweep uses real time — so the timeout-dependent clauses (2/3/4) rest on the shim, and a run states that it used it; clauses 1 and 5 need no shim", + "the sweep cadence a run waits on: the periodic sweep runs every 5 minutes (ESCALATION_SCAN_INTERVAL_MS = 5 * 60 * 1000, approval-service.ts; scheduled as ESCALATION_JOB_NAME in approvals-plugin.ts), and the plugin also runs one catch-up sweep at boot. After advancing the offset, either wait for the next 5-minute sweep or restart the server (shim still loaded, offset file kept) to get the boot sweep — there is no public route that fires a sweep on demand" ] }, - "blocked": { "by": "fixture", "ref": "hour-granular SLA needs a clock-control / runEscalations timing harness (timeoutHours min 1, sweep on real-time interval) — no stock-fixture way to advance past the deadline in-session" }, "variants": ["reassign", "auto_approve", "auto_reject", "notify"], "steps": [ "boot showcase isolated (dogfood §0); sign in as the dev admin", - "in a writable package author + register a flow whose approval node config.escalation = {enabled:true, timeoutHours:1, action:'reassign', escalateTo:'', notifySubmitter:true}", + "start the server with the wall-clock offset shim loaded (NODE_OPTIONS=--require , offset 0 — see fixtures.requires); as the admin, register one flow per variant through POST /api/v1/automation (no writable package needed) whose approval node config.escalation = {enabled:true, timeoutHours:1, action:, notifySubmitter:true}, plus escalateTo:'' on the reassign flow", "trigger the flow; GET /api/v1/approvals/requests/:id — status=pending and the request carries sla_due_at = created_at + timeoutHours (the SLA is materialized on open)", - "[needs clock control] advance the clock past sla_due_at (inject a clock / drive ApprovalService.runEscalations() with a clock whose now() is beyond the deadline) and run one escalation sweep", + "advance the shim's offset to +2h (past sla_due_at) and let ONE escalation sweep run: wait for the next 5-minute periodic sweep, or restart the server with the shim and offset kept so the boot catch-up sweep runs", "GET /:id/actions — assert exactly one action='escalate' row (the audit-first idempotency marker) whose comment names the action and which carries no actor: actor_id is absent on the read (stored null), because a machine action records no actor and the escalate row is the attribution (ADR-0118 D1)", "assert the declared action's effect: reassign → pending_approvers swapped to the escalatees + an approval.escalated notification to them; auto_approve/auto_reject → request finalized approved/rejected and the owning run resumes; notify → an approval.sla_breached notification to the pending approvers", "with notifySubmitter!==false, read the submitter's inbox — an approval.sla_breached notification addressed to them", - "idempotency: run the sweep a SECOND time — GET /:id/actions shows NO second escalate row (single-shot, marker-guarded)", + "idempotency: let a SECOND sweep run (the next 5-minute tick, and a restart for the boot sweep) — GET /:id/actions shows NO second escalate row (single-shot, marker-guarded)", "author-negative: build a scratch flow whose escalation carries an unknown key (or the `timeout`/`sla` alias) and validate it" ], "acceptance": [ @@ -850,7 +860,7 @@ { "clause": "past the deadline the sweep escalates exactly ONCE: one action='escalate' timeline row, and a re-run adds none", "oracle": "api", - "verify": "after advancing past sla_due_at and sweeping, GET /:id/actions has exactly one action='escalate' row with no actor (actor_id absent on the read, stored null — ADR-0118 D1: a machine action records no actor, and the escalate row is the attribution); a second sweep adds no further escalate row (the audit row is the idempotency marker, written before any mutation)", + "verify": "after advancing the shim past sla_due_at and a sweep (5-minute periodic or the boot catch-up), GET /:id/actions has exactly one action='escalate' row with no actor (actor_id absent on the read, stored null — ADR-0118 D1: a machine action records no actor, and the escalate row is the attribution); a second sweep adds no further escalate row (the audit row is the idempotency marker, written before any mutation)", "evidence": "actions reads after the first and second sweeps" }, { @@ -881,11 +891,16 @@ "source": [ "packages/spec/src/automation/approval.zod.ts#ApprovalEscalationSchema (ApprovalEscalationSchema — enabled/timeoutHours(min 1)/action(reassign|auto_approve|auto_reject|notify)/escalateTo/notifySubmitter, strict, #4001; carried on the approval node as config.escalation)", "packages/plugins/plugin-approvals/src/approval-service.ts#runEscalations (runEscalations sweep + escalateRequest — audit-first escalate row, per-action effects, notifySubmitter; slaDueAt; ESCALATION_JOB_NAME ADR-0042; this.clock)", - "packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (sla_due_at surfaced on the request)" + "packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (sla_due_at surfaced on the request)", + "packages/plugins/plugin-approvals/src/approval-service.ts#ESCALATION_SCAN_INTERVAL_MS (the 5-minute sweep interval)", + "packages/plugins/plugin-approvals/src/approvals-plugin.ts (schedules the interval sweep under ESCALATION_JOB_NAME and runs one boot catch-up sweep)", + "packages/runtime/src/domains/automation.ts (POST /automation → registerFlow: the runtime registration door the fixture uses)", + "#21845 (follow-up run: the clauses ran under the wall-clock offset shim)" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "initial — pins the ADR-0042 SLA escalation (declared action fires once past timeout + escalate timeline row); blocked on a clock-control timing harness (hour granularity), with the sla_due_at materialization and the strict-schema build clause runnable today", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-10-03", "change": "the escalate row's expected actor corrected: step 5 and the single-shot clause asserted actor SLA_ACTOR_ID, but under ADR-0118 D1 the sweep records no actor (actor_id null, absent on the GET /:id/actions read) and the escalate row is the attribution, so a run following the old text would report a false failure. Nothing else in the item changes", "ref": "#21517" } + { "revision": 2, "date": "2026-10-03", "change": "the escalate row's expected actor corrected: step 5 and the single-shot clause asserted actor SLA_ACTOR_ID, but under ADR-0118 D1 the sweep records no actor (actor_id null, absent on the GET /:id/actions read) and the escalate row is the attribution, so a run following the old text would report a false failure. Nothing else in the item changes", "ref": "#21517" }, + { "revision": 3, "date": "2026-10-05", "change": "blocked lifted: run #21845 drove every clause under a wall-clock offset shim loaded with NODE_OPTIONS=--require (Date and Date.now offset from a file, timers untouched, no product logic changed). The shim is written into fixtures.requires as a recipe, not committed as code; the clock knownGap is marked CLOSED-for-runs, not deleted. The item now registers its flows through POST /api/v1/automation (no writable package needed), one flow per escalation action, and names the 5-minute sweep interval plus the boot catch-up sweep that the advance-the-clock and idempotency steps wait on (card #21851 entry 2)", "ref": "#21845" } ] }, { diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index f0734d3cb74..1c2b9345da0 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -1103,24 +1103,27 @@ "title": "Linked accounts: link a social/OIDC identity through the redirect round-trip → a sys_account row appears in mine-view; unlink removes it; provider-less boot degrades honestly", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["a signed-in user linking a second identity", "the same user after unlinking"], "fixtures": { "app": "showcase", "requires": [ - "at least one social/OIDC provider configured (socialProviders or oidcProviders) so link_social has a provider to dance with — stock showcase ships none, so this item is blocked(fixture) until a provider is configured", - "the Account 'Identity Links' surface (nav_accounts → sys_account) reachable" + "at least one social/OIDC provider configured (socialProviders or oidcProviders) so the link door has a provider to dance with — stock showcase ships none, so the runtime fixture below supplies one", + "RUNTIME FIXTURE — a local OIDC provider (proved by run #21845; a test fixture in the run's scratch dir, ⛔ never committed and never a product change): a localhost authorization-code + PKCE OpenID provider serving discovery (/.well-known/openid-configuration), authorize, token, userinfo and jwks, registered with the auth service through its public config patch from a scratch app plugin — call the auth manager's applyConfigPatch({ oidcProviders: [ { the local provider's id, discovery URL, client id/secret } ] }). The 'auth:configure' hook is NOT usable for this: it fires before a scratch app plugin can contribute. The provider must be up, and the patch applied, before the first auth request (better-auth is built lazily on that request)", + "the Account app's 'Linked Accounts' surface (nav_account_linked → sys_account) reachable" + ], + "knownGaps": [ + "CLOSED for runs by the local OIDC provider in requires (#21845): stock showcase ships no social/OIDC IdP, so linking is not drivable without it — the same fixture class as identity-auth.sso-enforced-first-paint / oauth-app-consent-loop. A run states that its link clauses rest on the local provider; the provider-less clause (acceptance[3]) runs on a boot WITHOUT the fixture" ] }, - "blocked": { "by": "fixture", "ref": "no stock showcase social/OIDC IdP — link_social needs a configured provider, same fixture class as identity-auth.sso-enforced-first-paint / oauth-app-consent-loop" }, "steps": [ - "as a signed-in user, open Account → Identity Links (sys_account `mine` view, filter user_id={current_user_id}); screenshot the initial link set", - "invoke link_social for a configured provider: the action is type:'url' — GET /api/v1/auth/sign-in/social?provider=

&callbackURL=/_console/apps/account/sys_account (full-page navigation, NOT XHR, so the OAuth 302 dance and link cookie work); complete the provider round-trip and land back on the Identity Links view", + "as a signed-in user, open the Account app → Linked Accounts (sys_account `mine` view, filter user_id={current_user_id}); screenshot the initial link set", + "link through the AUTHENTICATED link door: with the user's session, POST /api/v1/auth/link-social {provider: '', callbackURL: '/_console/apps/account/sys_account'} — it answers the provider's authorization URL; navigate the browser to that returned URL (full-page navigation, NOT XHR, so the OAuth round-trip and its state cookie work), complete the provider round-trip and land back on Linked Accounts. ⛔ Do not link through the sign-in door (/api/v1/auth/sign-in/social): that signs in, it does not link to the current user", "confirm a sys_account row now exists for that provider in the caller's mine-view (provider_id = the provider, user_id = the caller) — (provider_id, account_id) is the whole account identity; there is no issuer to check, sys_account.issuer retired with better-auth 1.7.3 (packages/spec/src/migrations/entries/semantic/18.sys-account-issuer-retired.ts)", "unlink it: the unlink_account action → POST /api/v1/auth/unlink-account with accountId = the sys_account ROW id (better-auth 1.7 keys on the row id); confirm the row is gone from mine-view", - "both-sides / degradation: on a boot with NO provider configured, confirm link_social degrades honestly — the affordance is absent or names the missing provider, rather than offering a link that dead-ends (open-edition honest-degradation posture)", + "both-sides / degradation: on a boot with NO provider configured, confirm the Linked Accounts surface degrades honestly — any link affordance is absent or names the missing provider, rather than offering a link that dead-ends (open-edition honest-degradation posture)", "confirm sys_account is read-only over the data API — a forged direct insert/delete is refused (apiMethods ['get','list'], writes 405)" ], "acceptance": [ @@ -1133,8 +1136,8 @@ { "clause": "the link surface renders the caller's own links only (mine-view scoped) — screenshot-confirmed", "oracle": "screenshot", - "verify": "Identity Links shows the caller's provider rows; a different user's links are not present", - "evidence": "the Identity Links screenshot" + "verify": "Linked Accounts shows the caller's provider rows; a different user's links are not present", + "evidence": "the Linked Accounts screenshot" }, { "clause": "unlink removes the row: unlink_account keyed on the sys_account row id deletes exactly that link from mine-view", @@ -1143,9 +1146,9 @@ "evidence": "the unlink response + the follow-up read" }, { - "clause": "a provider-less boot degrades honestly: link_social is absent or names the missing provider — never a link affordance that dead-ends", + "clause": "a provider-less boot degrades honestly: the link affordance is absent or names the missing provider — never a link affordance that dead-ends", "oracle": "screenshot", - "verify": "with no provider configured, the Identity Links surface shows no dead link action (or an explicit unavailable state)", + "verify": "with no provider configured, the Linked Accounts surface shows no dead link action (or an explicit unavailable state)", "evidence": "the provider-less screenshot" }, { @@ -1159,17 +1162,21 @@ "a link that appears in the UI but does not create a sys_account row (client-only) is a FAIL — the row is the durable identity link", "unlink that hides the row from the list but leaves the sys_account (so the provider still signs the user in) is a FAIL", "one user's identity links appearing in another's mine-view is an RLS FAIL", - "a provider-less boot offering a link_social action that navigates to a dead endpoint is a dishonest-degradation FAIL" + "a provider-less boot offering a link affordance that navigates to a dead endpoint is a dishonest-degradation FAIL" ], "traps": ["wrong-persona", "dispatcher-vs-hono-route", "hydration-race"], "source": [ - "packages/platform-objects/src/identity/sys-account.object.ts#user_id (link_social type:'url' → /api/v1/auth/sign-in/social?provider=&callbackURL=; unlink_account → /api/v1/auth/unlink-account accountId=row id; mine view user_id={current_user_id} vs all_links; provider options; apiMethods ['get','list'])", + "packages/platform-objects/src/identity/sys-account.object.ts#user_id (unlink_account → /api/v1/auth/unlink-account accountId=row id; mine view user_id={current_user_id} vs all_links; apiMethods ['get','list'])", "packages/plugins/plugin-auth/src/auth-route-ledger.ts#AUTH_ROUTE_LEDGER (POST link-social=auth.accounts.linkSocial, GET list-accounts=auth.accounts.list, POST unlink-account=auth.accounts.unlink — re-pointed #18104: linkSocial is a CLIENT METHOD name, carried in this file only inside a dotted string value)", - "packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_accounts (nav_accounts → 'Identity Links', objectName sys_account)" + "packages/platform-objects/src/apps/account.app.ts#nav_account_linked (nav_account_linked → 'Linked Accounts' in the Account app, objectName sys_account — the user's surface this item drives)", + "packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_accounts (nav_accounts → 'Identity Links', objectName sys_account — the Setup app's admin entry over the same object, not the user's surface)", + "packages/plugins/plugin-auth/src/auth-manager.ts#applyConfigPatch (the public config patch the local-provider fixture registers through; oidcProviders)", + "#21845 (follow-up run: link / mine-view / unlink / read-only clauses passed against a local OIDC provider through the authenticated link door)" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "new item: social/OIDC account linking round-trip → sys_account mine-view row, unlink removal, provider-less honest degradation; blocked(fixture) pending a configured IdP (PENDING-GAPS §C)", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-09-23", "change": "the 'issuer stamped' requirement is withdrawn from both the step and the first acceptance clause's verify: better-auth 1.7.3 rolled the issuer-scoped account identity back and sys_account.issuer retired with it, so nothing stamps it and the old verify failed a healthy system. The reason is kept — the step now says (provider_id, account_id) is the whole identity and names the retirement's migration entry in packages/spec — and the verify states that an issuer-less row is the expected shape", "ref": "#19217" } + { "revision": 2, "date": "2026-09-23", "change": "the 'issuer stamped' requirement is withdrawn from both the step and the first acceptance clause's verify: better-auth 1.7.3 rolled the issuer-scoped account identity back and sys_account.issuer retired with it, so nothing stamps it and the old verify failed a healthy system. The reason is kept — the step now says (provider_id, account_id) is the whole identity and names the retirement's migration entry in packages/spec — and the verify states that an issuer-less row is the expected shape", "ref": "#19217" }, + { "revision": 3, "date": "2026-10-05", "change": "blocked lifted: run #21845 passed the link, mine-view, unlink and read-only clauses against a local authorization-code + PKCE OIDC provider registered through the auth manager's applyConfigPatch from a scratch app plugin ('auth:configure' fires too early); the provider recipe is written into fixtures.requires, not committed as code, and the missing-IdP gap is kept as a CLOSED-for-runs knownGap. Step 2 now links through the authenticated POST /api/v1/auth/link-social and navigates to the URL it returns — the old step drove the sign-in door, which signs in rather than links. The step, the provider-less clause and its negative no longer name a specific sys_account action: they describe the link affordance, so the clause indices stay stable while the action itself is handled on its own card. The user's nav label is 'Linked Accounts' in the Account app (nav_account_linked); 'Identity Links' is the Setup app's admin entry (card #21851 entry 3)", "ref": "#21845" } ] }, { diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index ae2fd9ba4ad..e3f3ea64163 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -2983,7 +2983,7 @@ "title": "Async import job: terminal state + per-row results, undo removes exactly the imported rows, cancel mid-job leaves a coherent partial", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -2992,30 +2992,27 @@ "fixtures": { "app": "showcase", "requires": [ - "an import-job-capable client wired into the console AND an object with an async-import route (framework packages/rest/src/rest-route-ledger.ts: POST /api/v1/data/:object/import/jobs, /import/jobs/:jobId/{cancel,undo,results})", - "the objectui import specs, which self-gate on IMPORT_CONSOLE_LIVE=1 (console) or a reachable import harness /live.html" + "an import-job-capable client wired into the console AND an object with an async-import route (framework packages/rest/src/rest-route-ledger.ts: POST /api/v1/data/:object/import/jobs, /import/jobs/:jobId/{cancel,undo,results}) — the STOCK console already satisfies the client half (run #21845, console pin 2e818d0b51ec); no extra wiring is needed", + "the objectui import specs, which self-gate on IMPORT_CONSOLE_LIVE=1 (console) or a reachable import harness /live.html", + "RUNTIME FIXTURE — the live-gated console spec (proved by run #21845; nothing committed in either repo): in an objectui checkout, run e2e/import-console/import-console-undo.spec.ts with IMPORT_CONSOLE_LIVE=1 against the booted showcase, from a SCRATCH COPY of the spec whose page goto path is the console-mounted /_console/apps/{app}/{object} (the spec's own path does not carry the /_console mount the framework serves the console under)" ], "knownGaps": [ - "the async import + undo/cancel path is gated: e2e/import-console/import-console-undo.spec.ts skips unless IMPORT_CONSOLE_LIVE=1 with an import-job-capable client wired; e2e/import-harness/import-undo.spec.ts skips unless the harness origin serves /live.html — on a stock showcase boot these do not run, so record blocked(fixture)", + "CLOSED for runs by the live-gated console spec in requires (#21845): e2e/import-console/import-console-undo.spec.ts skips unless IMPORT_CONSOLE_LIVE=1, and its goto path needs the /_console mount — with both supplied it runs against the stock console. A skip without the gate is the control, never a result. The harness twin e2e/import-harness/import-undo.spec.ts still skips unless the harness origin serves /live.html; it is optional evidence, and the API half below needs no gate at all", "AUTOMATION IS PINNED IN THE objectui REPO, NOT HERE — `automated.ref` names objectui specs exclusively, so from this checkout the item is neither runnable nor pin-evidenced (no console bundle is built here and none of those specs exist here). Drive it by hand in the browser and score it as a manual run, OR run the pin in an objectui checkout and cite the output naming the revision (17.1.0 sweep subject: 9a3daf8d37ad). ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact." ] }, - "blocked": { - "by": "fixture", - "ref": "IMPORT_CONSOLE_LIVE / import-harness gate — objectui e2e/import-console/import-console-undo.spec.ts self-skips unless IMPORT_CONSOLE_LIVE=1 and an import-job-capable client is wired; PENDING-GAPS §C import-job-undo-cancel" - }, "steps": [ - "run the pinned specs gated on: IMPORT_CONSOLE_LIVE=1 pnpm exec playwright test e2e/import-console/import-console-undo.spec.ts (and the harness twin e2e/import-harness/import-undo.spec.ts); capture the suite output as the primary evidence", - "when the gate is available: POST /api/v1/data/:object/import/jobs with a marker-tagged CSV; poll GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results", + "run the console spec with its gate on, from the scratch copy whose goto path is /_console/apps/{app}/{object} (fixtures.requires): IMPORT_CONSOLE_LIVE=1 pnpm exec playwright test (the harness twin e2e/import-harness/import-undo.spec.ts only where /live.html is served); capture the suite output, naming the objectui revision, as the primary evidence", + "API half (no gate needed): create with POST /api/v1/data/:object/import/jobs with a marker-tagged CSV — every other job route lives under /api/v1/data/import/jobs: poll progress at GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results; poll GET /api/v1/data/import/jobs/:jobId until terminal; GET /api/v1/data/import/jobs/:jobId/results for per-row results", "count marker rows via a filtered GET before and after the import; then POST /api/v1/data/import/jobs/:jobId/undo and re-count", - "GET /api/v1/data/import/jobs and confirm the job list distinguishes undoable/non-undoable and reverted state ({ jobId, undoable, revertedAt, createdAt }) so the fresh job is findable", + "GET /api/v1/data/import/jobs and confirm the job list distinguishes undoable/non-undoable and reverted state ({ jobId, undoable, revertedAt, createdAt } — revertedAt is ABSENT from an entry until that job is reverted, not null) so the fresh job is findable", "start a fresh import and POST /api/v1/data/import/jobs/:jobId/cancel mid-job; inspect the row set for coherence" ], "acceptance": [ { "clause": "an async import creates an UNDOABLE job that reaches a terminal state and exposes per-row results", "oracle": "test", - "verify": "the job list shows { jobId, undoable:true, revertedAt:null } and GET /import/jobs/:jobId/results returns per-row outcomes (pinned by import-console-undo.spec.ts / import-undo.spec.ts under their gate)", + "verify": "the job list shows { jobId, undoable:true } with revertedAt ABSENT (the key is omitted until the job is reverted — read absence, not null, as not-yet-reverted) and GET /import/jobs/:jobId/results returns per-row outcomes (pinned by import-console-undo.spec.ts / import-undo.spec.ts under their gate)", "evidence": "the gated spec output + the jobs/results reads" }, { @@ -3033,14 +3030,14 @@ { "clause": "the job list distinguishes undoable/non-undoable and reverted state so a run can find the fresh undoable job it created", "oracle": "api", - "verify": "GET /api/v1/data/import/jobs returns entries with { jobId, undoable, revertedAt, createdAt } (the specs filter on j.undoable && !j.revertedAt)", + "verify": "GET /api/v1/data/import/jobs returns entries with { jobId, undoable, createdAt }, plus revertedAt once a job is reverted — the key is absent, not null, before that (the specs filter on j.undoable && !j.revertedAt, which reads both the same)", "evidence": "the jobs list read" } ], "negative": [ "an undo that deletes MORE than the imported rows (or leaves some behind) is a FAIL — exactly the imported set, no more, no less (the async threshold vs undo-capture mismatch is the exact bug the specs exist for)", "a cancel that leaves half-written rows (partial row, dangling FK) is a FAIL — coherent partial only", - "reporting the item PASS off a skipped (gated-out) spec is a FAIL — a skip is blocked(fixture), never a green tick" + "reporting the item PASS off a skipped (gated-out) spec is a FAIL — a skip means IMPORT_CONSOLE_LIVE=1 was not set (or the goto path missed the /_console mount); it is never a green tick" ], "traps": [ "seed-data-thin", @@ -3053,7 +3050,8 @@ "source": [ "framework: packages/rest/src/rest-route-ledger.ts#REST_ROUTE_LEDGER (POST /data/:object/import/jobs; /import/jobs/:jobId/{cancel,undo,results}; GET /import/jobs[/:jobId] — re-pointed #18104: jobId is a route PATH PARAMETER, carried in this file only inside a route string)", "objectui: e2e/import-console/import-console-undo.spec.ts, e2e/import-harness/import-undo.spec.ts", - "PENDING-GAPS §C import-job-undo-cancel" + "PENDING-GAPS §C import-job-undo-cancel", + "#21845 (follow-up run: the console spec passed twice with its live gate on, and the API half passed)" ], "history": [ { @@ -3067,6 +3065,12 @@ "date": "2026-08-21", "change": "recorded on the item that its automation is pinned exclusively in the objectui repo and is therefore neither runnable nor pin-evidenced from this checkout — one of 23 items in that position, each of which every sweep had been re-deriving from scratch. The knownGap names the two honest ways to score it (hand-drive it as a manual run, or run the pin in an objectui checkout and cite the revision) and forbids the third: treating the `automated` field itself as coverage. The protocol and the exclusive-vs-mixed distinction live once in RUNNER.md; this line is the pointer (#10236 A5)", "ref": "#10236" + }, + { + "revision": 3, + "date": "2026-10-05", + "change": "blocked lifted: run #21845 found that the stock console already satisfies the 'import-job-capable client' gate (console pin 2e818d0b51ec) and passed the objectui console spec twice with IMPORT_CONSOLE_LIVE=1 from a scratch copy whose goto path is /_console/apps/{app}/{object}, plus the API half. That recipe is written into fixtures.requires and the gate knownGap is marked CLOSED-for-runs, not deleted. The API step now says that creation is POST /api/v1/data/:object/import/jobs and that progress, results, cancel, undo and the list all live under /api/v1/data/import/jobs. The job-list reads now treat revertedAt as absent, not null, until the job is reverted — the old { revertedAt:null } shape would fail a healthy list (card #21851 entry 4)", + "ref": "#21845" } ] }, From 4f0fb3a8e0e182aaca799c4146ca8e3d8b05fc3f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 15:03:56 +0000 Subject: [PATCH 2/2] docs(qa): re-point the scaffold and locale-switch checklist clauses run 21845 found mis-asserted Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv --- docs/qa/platform-checklist/areas/cli.json | 34 +++++++++++++--------- docs/qa/platform-checklist/areas/i18n.json | 22 ++++++++------ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 9054d5ef096..1a22b2dd94b 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -391,21 +391,23 @@ "title": "The published first-run closes: create-objectstack scaffold → install → validate → build → boot → health, with the skills boundary holding", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": ["new user (published-registry consumer)"], "fixtures": { "app": "showcase", - "requires": ["npm registry reachability (the whole point is the PUBLISHED path, not the repo checkout)"], + "requires": [ + "npm registry reachability (the whole point is the PUBLISHED path, not the repo checkout)", + "PRE-RELEASE VARIANT (proved by run #21845 on the 17.7 candidate) — the published path can only measure what is already released, so to test an unreleased candidate: in the candidate checkout run pnpm install --frozen-lockfile && pnpm build, pack every public workspace package and publish the tarballs ONLY to a localhost registry configured with NO upstream for @objectstack/* and create-objectstack (so nothing under test can resolve from npm), point the scaffold at it (npm_config_registry), and run the repo's own bin — node /packages/create-objectstack/bin/create-objectstack.js qa-first-run -t blank --skip-skills — in place of npx create-objectstack@latest. Prove the candidate is what got installed: the scaffold lockfile's integrity hash for each @objectstack/* package equals the local tarball's. Nothing is published anywhere else. The run record names which path it took (published or pre-release)" + ], "knownGaps": [ "during an RC window the repo version is unpublished and `latest` points at the previous major, so the protocol-major handshake refuses the artifact — that refusal is the ADR-0087 D1 gate WORKING on a skew the fallback introduced (#4894); record it in evidence, do not fail the item for it" ] }, "steps": [ - "in a scratch dir: `npx -y create-objectstack@latest qa-first-run -t blank --skip-skills`", - "cd qa-first-run && npm install --no-fund --no-audit; echo $?", - "npm run validate; echo $? — then npm run build; echo $?", + "in a scratch dir: `npx -y create-objectstack@latest qa-first-run -t blank --skip-skills; echo $?` (or the repo's bin against the local registry, for the pre-release variant in fixtures.requires) — the scaffolder runs the install ITSELF (pnpm when its probe finds pnpm, npm otherwise), so its exit code and its install output are the install step's evidence. ⛔ Do not follow it with a second `npm install`: on a pnpm-installed scaffold that fails (17.6.0 and 17.7 alike) and the failure is the extra step's, not the product's", + "cd qa-first-run && npm run validate; echo $? — then npm run build; echo $?", "boot from the artifact exactly as the workflow does: `npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 &`, poll /api/v1/health up to 60s, then `curl -fsS http://localhost:8080/api/v1/ready`", "repeat scaffold + validate + build across the template matrix (variants) — remote templates always ship build; run validate where the script exists", "skills boundary probe: in a fresh scaffold run `npx -y skills add /skills --all --copy` and compare the installed set to the curated skills/ catalog; then list repo-root discovery and assert no internal skill (dogfood-verification) surfaces", @@ -433,7 +435,7 @@ { "clause": "the skills boundary holds: the installed set equals the curated skills/ catalog and repo-root discovery surfaces NO internal skill (the 15.1 third-party-eval leak, pinned in the workflow)", "oracle": "log", - "verify": "set-equality against the curated catalog; grep for dogfood-verification in the discovery listing comes back empty", + "verify": "set-equality, by UNIQUE SKILL NAME, against the curated catalog — the installed set is the de-duplicated names of the directories holding a SKILL.md (as scaffold-e2e.yml computes it with sort -u), never a raw file or path count; grep for dogfood-verification in the discovery listing comes back empty", "evidence": "the installed-set diff + discovery listing" }, { @@ -452,11 +454,14 @@ "automated": { "kind": "ci", "ref": ".github/workflows/scaffold-e2e.yml" }, "source": [ ".github/workflows/scaffold-e2e.yml (#2908 — the scaffold→install→validate→build→boot→health lane, the registry-canary template matrix, the skills-boundary assertions, the #4894 RC-window fallback)", - "packages/create-objectstack (the scaffolder under test)" + "packages/create-objectstack (the scaffolder under test)", + "packages/create-objectstack/src/detect-package-manager.ts (the scaffolder installs with pnpm when its probe finds it, npm otherwise — why a trailing npm install is not part of the path)", + "#21845 (follow-up run: the 17.7 candidate scaffold passed through the pre-release local-registry variant)" ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "new item: the published first-run experience mirrored step-for-step from scaffold-e2e.yml, template matrix as variants, RC-window protocol refusal recorded as gate-working instead of failure", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-08-14", "change": "variants narrowed to [blank]: the five remote content templates were delisted from the marketplace and retired from the create-objectstack catalog, and the scaffold-e2e registry-canary matrix this item mirrors was trimmed with them", "ref": "claude/issue-8677-retire-remote-template-catalog" } + { "revision": 2, "date": "2026-08-14", "change": "variants narrowed to [blank]: the five remote content templates were delisted from the marketplace and retired from the create-objectstack catalog, and the scaffold-e2e registry-canary matrix this item mirrors was trimmed with them", "ref": "claude/issue-8677-retire-remote-template-catalog" }, + { "revision": 3, "date": "2026-10-05", "change": "three corrections from run #21845. (1) The separate `npm install` step is dropped: the scaffolder already installs (pnpm when found, npm otherwise), and a trailing npm install fails on the pnpm layout on 17.6.0 and 17.7 alike, so the step measured itself rather than the product. The scaffolder's own exit code is now the install evidence, which is also what the published canary job in scaffold-e2e.yml does. Of the card's three options this one was picked as the least ambiguous: it does not depend on which package manager the host has, as `pnpm install` would, and it does not change the published scaffold command, as `--skip-install` would. (2) fixtures.requires gains a pre-release variant: pack the workspace to a localhost registry with no upstream for @objectstack/* and create-objectstack, run the repo's bin against it, and prove the candidate via lockfile integrity. The published path measures only what is released. (3) The skills-boundary clause compares unique skill names, not a raw count (card #21851 entries 8 and 9)", "ref": "#21845" } ] }, { @@ -819,7 +824,7 @@ "title": "The documented newcomer verification loop closes on a FRESHLY SCAFFOLDED project: os validate passes and the Console actually paints at /_console/ — the seam between the scaffold item and the showcase boot item", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": ["a brand-new developer following content/docs/getting-started/quick-start.mdx, with no prior project"], @@ -834,9 +839,9 @@ ] }, "steps": [ - "scaffold a fresh project exactly as the published docs instruct: `npx -y create-objectstack@latest qa-console-first-paint` (no -t flag — the docs never pass one), then `npm install`", + "scaffold a fresh project exactly as the published docs instruct: `npx -y create-objectstack@latest qa-console-first-paint; echo $?` (no -t flag — the docs never pass one; for an unreleased candidate use cli.scaffold-first-run's pre-release variant). The scaffolder installs ITSELF (pnpm when found, npm otherwise) and the docs prescribe no separate install — ⛔ do not add a trailing `npm install`: it fails on a pnpm-installed scaffold (17.6.0 and 17.7 alike)", "run the documented gate: `npx os validate; echo $?`", - "run `npm run dev` — the scaffold's OWN script, which is what a newcomer reaches for after `npm install` — and probe http://localhost:3000/_console/", + "run `npm run dev` — the scaffold's OWN script, which is what a newcomer reaches for once the scaffold has installed — and probe http://localhost:3000/_console/", "load the console in a browser and record what a newcomer actually sees: does the shell render, and is there any identity they can sign in with?", "stop; run the documented spelling `npx os dev --ui` and probe /_console/ again — the two must agree (see clause 3; `--ui` is a forwarder for a serve-side flag that is already default-on, so this is a no-op equivalence check, not a divergence hunt)", "capture the boot log for both invocations" @@ -863,8 +868,8 @@ { "clause": "a newcomer can complete the documented visual check, or the run states precisely what stops them — the docs promise 'create a record, watch an action show/hide, confirm the nav and filters' on this project", "oracle": "dom", - "verify": "attempt the documented visual check; if no identity exists to sign in with on a blank scaffold, that is the finding — record it rather than substituting showcase credentials", - "evidence": "what the newcomer sees, including any sign-in wall" + "verify": "attempt the documented visual check: sign in with the seeded dev admin and create a record on the scaffold's object (a 201 that reads back). The action/nav half ('watch an action show/hide, confirm the nav and filters') has NOTHING to act on here — a blank scaffold ships no app and no action — so it is a stated LIMIT of this item, recorded as not-applicable and never as a FAIL or a PASS; the in-console continuation that authors an app and lands it in the launcher is studio-authoring.first-run-loop (it does not author an action either). If no identity exists to sign in with on a blank scaffold, that is the finding — record it rather than substituting showcase credentials", + "evidence": "what the newcomer sees, including any sign-in wall, plus the record create + read-back" } ], "negative": [ @@ -893,7 +898,8 @@ "change": "new — angle-5 (docs claims) probe of the published first-run. Covers a real seam between two existing items: cli.scaffold-first-run boots the scaffold headless via `os start --artifact`, cli.dev-boot-contract drives the console against the fully-populated showcase, and nobody drives the console against a fresh BLANK scaffold — where there is no seed data and no seeded admin unless one is requested. Checked and CLEAN on three questions the sweep raised: no doc anywhere in content/docs/ prescribes a retired template (`todo`/`compliance`/`content`/`contracts`/`procurement`), every documented invocation is a bare `npx create-objectstack my-app` resolving to the only catalog entry, and `npm run validate` is the same binary as `npx os validate`. REFUTED during authoring, recorded here so it is not re-derived: the sweep first read the blank template's bare `\"dev\": \"objectstack dev\"` against the quick-start's `os dev --ui` and inferred that `npm run dev` would strand a newcomer with no console. Source-checking the chain disproves it — dev.ts forwards `--ui` only when set and never forwards `--no-ui`, serve.ts declares `ui` `default: true`, and cli.mdx states outright that the console is 'already on by default in dev'. The bare script is CORRECT; `--ui` on `dev` is a no-op forwarder. Clause 3 accordingly asserts the two invocations AGREE, and a difference is the failure", "ref": "#9299" }, - { "revision": 2, "date": "2026-08-18", "change": "corrected the stale 'no seeded admin unless one is requested' assumption in both knownGaps. commands/dev.ts resolves seed-admin as flags['seed-admin'] ?? true and documents it 'Default: on', so a bare `objectstack dev` on an empty DB seeds admin@objectos.ai / admin123 and prints it in the banner. Clause 4's open question ('is there any identity a newcomer can sign in with?') has a definite answer, and leaving the gap as written told the runner not to assume the answer the tree already fixes (#9467 CF-6)", "ref": "#9386" } + { "revision": 2, "date": "2026-08-18", "change": "corrected the stale 'no seeded admin unless one is requested' assumption in both knownGaps. commands/dev.ts resolves seed-admin as flags['seed-admin'] ?? true and documents it 'Default: on', so a bare `objectstack dev` on an empty DB seeds admin@objectos.ai / admin123 and prints it in the banner. Clause 4's open question ('is there any identity a newcomer can sign in with?') has a definite answer, and leaving the gap as written told the runner not to assume the answer the tree already fixes (#9467 CF-6)", "ref": "#9386" }, + { "revision": 3, "date": "2026-10-05", "change": "two corrections from run #21845. (1) Step 1's trailing `npm install` is dropped: the scaffolder already installs (pnpm when found, npm otherwise), the quick-start prescribes no separate install, and a trailing npm install fails on the pnpm layout on 17.6.0 and 17.7 alike. This matches the choice made on cli.scaffold-first-run, and the npm run dev step no longer presumes it. (2) acceptance[3] states its limit: a blank scaffold ships no app or action, so the docs' 'watch an action show/hide, confirm the nav and filters' half is recorded as not-applicable here, never as a FAIL. The record-create half stays scored. The clause names studio-authoring.first-run-loop as the in-console continuation and says outright that it covers the app/nav half and no action (card #21851 entries 8 and 10)", "ref": "#21845" } ] }, { diff --git a/docs/qa/platform-checklist/areas/i18n.json b/docs/qa/platform-checklist/areas/i18n.json index 178842d2eb9..16cd9acc6be 100644 --- a/docs/qa/platform-checklist/areas/i18n.json +++ b/docs/qa/platform-checklist/areas/i18n.json @@ -95,7 +95,7 @@ "title": "Studio follows the in-app locale switch — no mixed-language session", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "browser", "personas": ["admin"], @@ -109,12 +109,12 @@ ] }, "steps": [ - "sign in as admin on the showcase, switch the in-app locale to zh-CN, and confirm the app shell followed (nav in zh-CN)", + "sign in as admin on the showcase, switch the in-app locale to zh-CN, and confirm the app shell followed (nav in zh-CN). To switch, use the avatar-menu language switcher (it writes the user's locale); seeding browser storage alone is not a language switch — a run that only seeds the UI language in browser storage leaves the user's stored locale empty, so dates follow the browser and a FAIL read off it is the harness's, not the product's", "navigate into Studio / metadata-admin: open the object editor for showcase_project and the dashboard designer for showcase_chart_gallery", "screenshot the object editor's form — its labels/sections come from the metadataForms translation group (packages/spec/src/system/translation.zod.ts: metadataForms..label / sections / fields)", "screenshot a metadata list surface showing relative dates ('x 天前'-style) in the switched locale", "reload the browser fully and re-screenshot one Studio surface — the locale choice must survive the reload", - "switch back to en and re-screenshot the same two Studio surfaces" + "switch back to en through the same avatar-menu language switcher and re-screenshot the same two Studio surfaces" ], "acceptance": [ { @@ -157,12 +157,14 @@ "packages/spec/src/system/translation.zod.ts#metadataForms (metadataForms group + resolveMetadataFormLabels convention)", "scripts/check-i18n-coverage.mjs#baseline (platform metadata-form baseline is platform-objects-owned)", "scripts/i18n-coverage-baseline.json (the frozen per-config untranslated counts this item's mixed-language clause is judged against)", - "#7640 (i18n area run — the residual EN strings that exposed the clause/ratchet collision), #7686" + "#7640 (i18n area run — the residual EN strings that exposed the clause/ratchet collision), #7686", + "#21782 (re-run comment: the held relative-date FAIL was a harness gap — browser storage seeded, switcher never used; PASS through the avatar-menu switcher)" ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "initial import from #3358", "ref": "#3358" }, { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 3, "date": "2026-08-11", "change": "the mixed-language clause is reconciled with the coverage ratchet (#7640): as written it read 'no untranslated declared string next to translated ones', which the showcase's FROZEN untranslated debt violates by design — the sibling item i18n.surface-matrix already declares that debt legitimate, so this item contradicted it and cost a clean PASS. The clause now judges against the bundle (translated key renders translated; untranslated key renders its EN SOURCE label; a raw dotted key is the unconditional FAIL), the same knownGap is declared on this item's fixtures, and the negative carries the matching qualifier", "ref": "#7686" } + { "revision": 3, "date": "2026-08-11", "change": "the mixed-language clause is reconciled with the coverage ratchet (#7640): as written it read 'no untranslated declared string next to translated ones', which the showcase's FROZEN untranslated debt violates by design — the sibling item i18n.surface-matrix already declares that debt legitimate, so this item contradicted it and cost a clean PASS. The clause now judges against the bundle (translated key renders translated; untranslated key renders its EN SOURCE label; a raw dotted key is the unconditional FAIL), the same knownGap is declared on this item's fixtures, and the negative carries the matching qualifier", "ref": "#7686" }, + { "revision": 4, "date": "2026-10-05", "change": "the switch steps now name HOW to switch: through the avatar-menu language switcher, which writes the user's locale. Seeding browser storage alone is not a language switch. The 17.7 console run held a relative-date FAIL on this item, and its re-run on #21782 settled it as a harness gap: the first lane had set the UI language only in browser storage, so the user's stored locale stayed empty and dates followed the browser. Through the switcher the clause passed, so the clauses themselves are unchanged (card #21851 entry 11, filed with the follow-up run #21845)", "ref": "#21782" } ] }, { @@ -170,7 +172,7 @@ "title": "Every translatable surface localizes on a zh-CN session — one pass over the full translation-group vocabulary, plus the runtime i18n routes it resolves through", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": ["admin on a zh-CN session"], @@ -188,7 +190,7 @@ }, "steps": [ "before the browser: read the served translation metadata (GET /api/v1/meta/translation, or the bundle source) and note, per variant surface, at least one key that IS translated to zh-CN — expectations derive from data, not vibes", - "sign in as admin, switch to zh-CN, and walk one surface per variant, screenshotting each:", + "sign in as admin, switch to zh-CN, and walk one surface per variant, screenshotting each. To switch, use the avatar-menu language switcher (it writes the user's locale); seeding browser storage alone is not a language switch — a run that only seeds the UI language in browser storage leaves the user's stored locale empty, so dates follow the browser and a relative-dates FAIL read off it is the harness's, not the product's:", "nav labels — the Showcase app sidebar groups/nodes (apps..navigation..label group)", "list headers — the showcase_task list columns (objects..fields..label; the bundle translates every surfaced column incl. status/priority OPTION labels)", "form labels + placeholders on a task edit form (fields group), and select options in the form and list filters (fields..options)", @@ -308,13 +310,15 @@ "scripts/check-i18n-coverage.mjs + scripts/i18n-coverage-baseline.json (frozen-debt ratchet)", "packages/spec/src/ui/component.zod.ts (record:details sections — `name` is the i18n anchor; a nameless section renders its authored label in every locale)", "examples/app-showcase/src/ui/views/task.view.ts + ui/views/contact.view.ts (the named-section form views this item's _sections variant is read off) vs ui/pages/task-detail.page.ts (label-only record:details)", - "#7640 (i18n area run), #7686 (the maintenance card this revision answers)" + "#7640 (i18n area run), #7686 (the maintenance card this revision answers)", + "#21782 (re-run comment: the held relative-dates FAIL was a harness gap — browser storage seeded, switcher never used; PASS through the avatar-menu switcher)" ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "new matrix item: per-surface localization pass over the spec's full translation-group vocabulary, grounded in the showcase bundle and the coverage ratchet", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-08", "change": "clause-extension: runtime i18n routes (/i18n/locales configured set, /translations/:locale ⊂ declared vocabulary, /labels/:object/:locale matches the UI, unknown-locale honest degradation), plus the E11 activity-feed/audit verb-localization variant; traps gain dispatcher-vs-hono-route (the plugin mount and dispatcher /i18n domain must answer one shape, #3636/#3833)", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-11", "change": "two variants reconciled with what the stock fixture actually affords (#7640). _sections: the step pointed at the task DETAIL page, whose record:details block authors label-only sections — unaddressable by design per component.zod.ts. The variant is NOT unreachable, as first read: task.view.ts's tabbed/wizard/split form views and contact.view.ts's default form declare stable section names the bundle already translates, so the step now names those surfaces and the nameless record page is recorded as a knownGap plus a qualifier on the resolver negative. emptyState: no showcase view authors one, so the variant is waived as blocked(fixture) with the debt written down, never faked from console chrome", "ref": "#7686" }, - { "revision": 4, "date": "2026-08-18", "change": "two corrections. (1) The #7714 emptyState waiver is stale: the specimen has landed — task.view.ts authors an emptyState on the urgent view and the zh-CN bundle carries objects.showcase_task._views.urgent.emptyState.{title,message} — so step 7 and the knownGap now describe a runnable variant instead of instructing blocked(fixture) (#9467 CF-1). (2) Clause 11 asserted the feed/audit verb localizes on the READER's session; audit-writers.ts renders the summary once at write time via resolveWriteLocale(tenantId, userId) and stores it, so only the tenant setting at write time moves it. The clause and its step now flip the tenant locale before writing — this premise nearly produced a false resolver FAIL (#9467 CF-2)", "ref": "#9386" } + { "revision": 4, "date": "2026-08-18", "change": "two corrections. (1) The #7714 emptyState waiver is stale: the specimen has landed — task.view.ts authors an emptyState on the urgent view and the zh-CN bundle carries objects.showcase_task._views.urgent.emptyState.{title,message} — so step 7 and the knownGap now describe a runnable variant instead of instructing blocked(fixture) (#9467 CF-1). (2) Clause 11 asserted the feed/audit verb localizes on the READER's session; audit-writers.ts renders the summary once at write time via resolveWriteLocale(tenantId, userId) and stores it, so only the tenant setting at write time moves it. The clause and its step now flip the tenant locale before writing — this premise nearly produced a false resolver FAIL (#9467 CF-2)", "ref": "#9386" }, + { "revision": 5, "date": "2026-10-05", "change": "the switch step now names HOW to switch: through the avatar-menu language switcher, which writes the user's locale. Seeding browser storage alone is not a language switch. The 17.7 console run held the relative-dates variant of acceptance[0] as a FAIL, and its re-run on #21782 settled it as a harness gap: the first lane had set the UI language only in browser storage, so the user's stored locale stayed empty and dates followed the browser. Through the switcher the variant passed, so the clauses themselves are unchanged (card #21851 entry 11, filed with the follow-up run #21845)", "ref": "#21782" } ] }, {