From 6b4253f59dd0d08810ace278aee76936e3945153 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:06:45 +0000 Subject: [PATCH 1/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20integr?= =?UTF-8?q?ation-system:=20declarative-boot=20ref=20narrowed=20to=20negati?= =?UTF-8?q?ve[0],=20degraded-recovery=20view=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy findings 1 and 8 (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/integration-system.json | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 16adba5f06b..fe9b8578b0d 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -9,7 +9,7 @@ "title": "Provider-bound declarative connectors materialize at boot for every installed provider kind (rest/openapi/mcp), list with origin:'declarative' state:'ready', and dispatch end-to-end from connector_action", "since": "v15.1", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "fixtures": { @@ -82,7 +82,7 @@ ], "automated": { "kind": "e2e", - "ref": "packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts — ⚠️ covers PART of this item, clause by clause: negative[0] whole (showcase_erp_catalog is absent from the runtime listing); acceptance[0]'s mcp variant whole (origin 'declarative', state 'ready'), and of the rest and openapi variants their state 'ready' only — their origin is not asserted; acceptance[1]'s mcp half as containment (echo_upper is among the actions), not the openapi half (getHealth); acceptance[2]'s mcp dispatch (the echo flow's output carries OBJECTSTACK), not the rest ping. acceptance[3]–[5] and negative[1] (the authoring refusals, the unknown-provider boot error, the disabled declaration) rest on no pin — drive them by hand" + "ref": "packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts — negative[0] ONLY: its case 'materializes all three generic-executor instances at boot — mcp included, state ready' asserts that showcase_erp_catalog is absent from the runtime listing. No pin asserts a full acceptance clause, so acceptance[0]–[5] and negative[1] are driven by hand. That case and the dispatch case 'dispatches the MCP tool end-to-end through the flow connector_action' assert halves only: acceptance[0]'s mcp variant (origin 'declarative', state 'ready') and the rest and openapi variants' state 'ready', not their origin, where the clause asks one verdict per variant; acceptance[1]'s mcp half as containment (echo_upper is among the actions), not the openapi half (getHealth); acceptance[2]'s mcp dispatch (the echo flow's output carries OBJECTSTACK), not the rest ping. acceptance[3]–[5] and negative[1] (the authoring refusals, the unknown-provider boot error, the disabled declaration) rest on no pin at all" }, "source": [ "docs/plans/release-15.1-test-plan.md §B1 (#2994/#3062)", @@ -110,6 +110,12 @@ "date": "2026-10-20", "change": "automated.ref narrowed clause by clause, in the scope-depth-asymmetry shape — round 2 part 1 of wave anchor #24579 (comment 6272032736: Tier 1 run #24598 named this ref among those that over-claim clause coverage). QA run #24575 scored every clause pass on the pin's 2/2; read against the spec, its two cases cover negative[0], acceptance[0]'s mcp variant and the other two variants' state, and the mcp halves of acceptance[1] and [2] only. No clause changed", "ref": "#24717" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "automated.ref narrowed to the one clause its pin asserts whole — checklist-accuracy finding of QA run #24779 (18.0 sweep round 3, 580fd4ed; wave anchor #24579 round 12, round 6's theme-B rule). Re-read at base 307ce4c4b4: the dogfood pin's two cases assert negative[0] whole and halves of acceptance[0]–[2] (the mcp variant; the other two variants' state, not their origin), so no acceptance clause is pin-scored and the ref now says so. `automated` stays, with negative[0]. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -118,7 +124,7 @@ "title": "An unreachable connector upstream degrades that ONE instance (CONNECTOR_UPSTREAM_UNAVAILABLE) instead of failing boot, retries on 5s→300s backoff, and recovers atomically — while config faults stay boot-fatal", "since": "v15.1", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "fixtures": { @@ -135,7 +141,7 @@ "watch the log through at least three retry cycles; record the intervals", "restore the upstream (fix the script path is a config CHANGE — instead restore by making the original path valid again, e.g. re-adding the file) WITHOUT restarting; wait one backoff cycle; re-read the descriptor. At the default `warn` log level the visible sign of the swap is the engine's warn `Connector 'showcase_mcp_tools' replaced`; the '[Automation] recovered connector instance …' line is info-level and prints only on a --log-level info boot (QA run #24575)", "separately, boot the config-fault twin and capture that boot's outcome", - "trigger a metadata:reloaded reconcile WITHOUT a restart and confirm it retries immediately while the attempt count keeps climbing. A per-item publish announces one: QA run #24575 used PUT /api/v1/meta/view/NAME?mode=draft, then POST /api/v1/meta/view/NAME/publish, and the retry followed 1.2 s later. ⛔ Not by touching or saving the entry's source: under os dev that rebuilds and RESTARTS the server (dev's --restart default), which discards the in-memory retry state instead of reconciling it. Only a config edit to the entry, a changed materialization signature, resets the backoff; this item does not drive that arm live" + "trigger a metadata:reloaded reconcile WITHOUT a restart and confirm it retries immediately while the attempt count keeps climbing. A per-item publish announces one: QA run #24575 used PUT /api/v1/meta/view/NAME?mode=draft, then POST /api/v1/meta/view/NAME/publish, and the retry followed 1.2 s later. The PUT body is a current view item (ViewItemSchema, packages/spec/src/ui/view.zod.ts): { name: 'OBJECT.NAME', object, viewKind: 'list', config: { type: 'grid', data: { provider: 'object', object }, columns: [...] } }, its name qualified by the object (QA run #24779 used showcase_task.qa_b_reload_view). A flat { type, columns } body answers 422 unrecognized_keys, and nothing is published. ⛔ Not by touching or saving the entry's source: under os dev that rebuilds and RESTARTS the server (dev's --restart default), which discards the in-memory retry state instead of reconciling it. Only a config edit to the entry, a changed materialization signature, resets the backoff; this item does not drive that arm live" ], "acceptance": [ { @@ -201,6 +207,12 @@ "date": "2026-10-20", "change": "checklist-accuracy findings of QA run #24575 (18.0 sweep, 5b14b7f3; wave anchor #24579 §3, which lists it under automation). acceptance[3].verify and steps[6] said a reload reconcile resets the backoff; it retries at once and keeps counting, and only a config edit (a changed materialization signature) resets it, as degradeConnectorInstance has it. steps[6] said to touch or save the entry, which restarts os dev; it now names a restart-free reload, a per-item publish. fixtures.requires[0] puts every boot in a scratch copy (the stdio path is cwd-relative) and names #24575's file-move break, the one steps[4] can undo with no config change; steps[4] names the warn line that shows the swap at the default log level. Not loosened: every clause keeps its oracle", "ref": "#24717" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[6] names the view body its per-item publish needs — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): a flat { type, columns } body answers 422 unrecognized_keys; the run published a view item carrying viewKind and config. Read at base 307ce4c4b4 against ViewItemSchema (view.zod.ts), which discriminates on viewKind and requires an OBJECT.view name. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From a9ab74f9a2bd4784a9b1475f117395b6728572a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:07:31 +0000 Subject: [PATCH 2/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20approv?= =?UTF-8?q?als:=20email-action-token-door=20ref=20narrowed=20per=20clause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy finding 2 (wave anchor #24579 round 12, claim 6286008102): acceptance[4] and [5] have no full-clause pin. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/approvals.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 9ffb93a422f..518fe7811cf 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1145,7 +1145,7 @@ "title": "The anonymous /approvals/act door: a minted one-tap token renders a confirm page and one POST decides as exactly the bound approver — every dead-token shape is a designed refusal page with no state change", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": ["dev admin (submitter of the seeded invoice request — remind is submitter-only). Since PR #24620 the remind fan-out mints no token, so the admin drives only the live legs: the reminder at-rest read and the server reads beside the anonymous probes", "the ANONYMOUS bearer of an action link: a clean HTTP client holding NO session cookie and NO bearer token"], @@ -1215,7 +1215,7 @@ "traps": ["dispatcher-vs-hono-route", "auth-state-leak"], "automated": { "kind": "unit", - "ref": "packages/plugins/plugin-approvals/src/approval-service.test.ts (acceptance[0] mint + FORBIDDEN and the remind-carries-no-link pin; acceptance[1] peek; acceptance[2]–[3] redeem + replay; acceptance[4] the dead-token reasons) + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (acceptance[1] the confirm page; acceptance[2] the redemption; acceptance[4] the invalid / expired / consumed pages; acceptance[5]'s converse) + packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts (acceptance[0] the closed door; acceptance[3] the replay) + packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts (acceptance[0] nothing redeemable at rest; acceptance[2]–[3] the positive control) + packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts (acceptance[0] the internal digest). The at-rest read and the token=garbage page still need the live run; acceptance[5]'s probe has no pin" + "ref": "packages/plugins/plugin-approvals/src/approval-service.test.ts (acceptance[0] mint + FORBIDDEN and the remind-carries-no-link pin; acceptance[1] peek; acceptance[2]–[3] redeem + replay) + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (acceptance[1] the confirm page; acceptance[2] the redemption) + packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts (acceptance[0] the closed door; acceptance[3] the replay) + packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts (acceptance[0] nothing redeemable at rest; acceptance[2]–[3] the positive control) + packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts (acceptance[0] the internal digest). Full clauses: acceptance[0]–[3] only. No pin covers acceptance[4] or acceptance[5]. acceptance[4] is pinned in halves: the invalid / expired / consumed pages (action-page-member's 'a dead link — unknown, expired, consumed — is told on the page with 200, through GET and POST alike') and the not_pending / not_approver reasons (approval-service's 'redeem: dead tokens — invalid, expired, decided request, reassigned slot'). The summary-rendering split, the not_pending / not_approver PAGES rendered WITH their summary rows, has no page-level pin, and the live invalid capture stays the run's. acceptance[5]'s bearer probe has no pin: action-page-member's 'the token is the only credential: a cookie or Authorization header is never read, and changes nothing' pins its converse only. The at-rest read and the token=garbage page still need the live run" }, "source": [ "packages/plugins/plugin-approvals/src/approvals-plugin.ts#http (the raw Hono mount at /api/v1/approvals/act — GET renders via peekActionToken, POST redeems; mounted on kernel:ready off the canonical http.server handle, #4251 B5)", @@ -1233,7 +1233,8 @@ { "revision": 1, "date": "2026-08-30", "change": "initial — the anonymous actionable-link door (ADR-0043) had no checklist coverage despite being an unauthenticated decision path. Authored from source with one register correction: the hunter proposed an emailed-link capture harness as the fixture, but messaging persists the reminder payload (raw action URLs included) on the sys_notification event row, so every token/door clause is stock-runnable; only the email-rendering leg still wants the LogTransport capture (recorded in knownGaps)", "ref": "#sweep-2026-08-30" }, { "revision": 2, "date": "2026-10-10", "change": "re-pointed after PR #22633 (#22616) closed sys_approval_token to the automatic API (apiEnabled: false, apiMethods: [] — 404 OBJECT_API_DISABLED for every caller): the token-row oracles move to the act door's outcome pages, the request's action history (GET /:id/actions) and the two pinned integration tests (internal-hash, generic-door); the mint clause is scored by test; the expiry leg, whose only recipe was backdating expires_at over the data API, has no surviving door (ttlMs is not REST-reachable and the plugin threads no clock) and is recorded in knownGaps as blocked(fixture) with the pinned 'expired' reason as its oracle. Two steps had contradicted the door before that PR already: the digest has been withheld since #21197, and update was never in the whitelist", "ref": "#22631" }, { "revision": 3, "date": "2026-10-16", "change": "step 7's dead-slot leg said 'reassign the bound approver away first' without saying who can (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on a two-slot request [admin, Ada] minted by remind: the admin's reassign without `from` moved the admin's own slot ([OTHER, Ada]) and Ada's approve token still peeked as the 'Approve · 通过' confirm page; the admin's reassign with from: Ada moved Ada's slot and the same token then rendered 'No longer your approval · 已不在你名下'. The step now passes `from` (or has the bound approver reassign their own slot); the clause is unchanged", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" } + { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" }, + { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "claude/issue-24579-v18-sweep-round12" } ] }, { From 55a3850d56438815997a167bab3a7105192c7eb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:07:31 +0000 Subject: [PATCH 3/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20attach?= =?UTF-8?q?ments-storage:=20the=20update-path=20pin,=20and=20the=20scan-ca?= =?UTF-8?q?p=20clause=20per=20ADR-0139?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy findings 3 and 4 (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/attachments-storage.json | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index 71972d56b12..be3c78b1d84 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -343,7 +343,7 @@ "title": "sys_attachment reads inherit parent visibility: a restricted member sees neither rows nor counts for invisible parents", "since": "v15.1", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": ["member who can read the parent record", "member who cannot read the parent record"], @@ -391,10 +391,10 @@ "evidence": "the by-id response" }, { - "clause": "the filter fails CLOSED: a filter-compute failure or a pre-scan past the 2000-candidate cap excludes rows rather than leaking them, and the cap logs a warning naming the fail-closed truncation", + "clause": "the filter fails CLOSED and never truncates an ordinary parent: since ADR-0139 D4 (step E7) the gate writes one ReadableIds leaf per parent object and pre-scans nothing, so a broad read past 2000 candidate rows serves every visible row with a true `total` and logs no cap warning. The bounded 2000-row probe remains only for ADR-0139 D5's three classes (a delegated caller, a parent an object-scoped read middleware narrows, a federated parent) and the two whole-read states knownGaps[3] names; there a pre-scan past the cap or a probe failure excludes rows rather than leaking them, and logs the '[storage] attachment read visibility' warning naming the fail-closed truncation or the denial", "oracle": "log", - "verify": "on a very broad unscoped list, either all rows resolve visibly or the '[storage] attachment read visibility' warning appears — silence plus leaked rows is the failure", - "evidence": "log excerpt for the broad-read case" + "verify": "on a very broad unscoped list over the recipe's ordinary parents, every visible row is served, `total` (the read passes $top) equals the rows served, the invisible-parent row stays excluded, and no '-row cap' warning is logged. Leaked rows are the failure; a cap warning on an ordinary parent contradicts ADR-0139 D4 and is recorded as a finding. The probe half is not reachable from a run here (knownGaps[2]): cite attachment-read-leaf.integration.test.ts and attachment-read-leaf.test.ts", + "evidence": "the broad read's row count, its total and the log around it" } ], "negative": [ @@ -428,6 +428,12 @@ "date": "2026-10-16", "change": "knownGaps names the gate's two whole-read probe states (no canReadObject or no security service; an engine that cannot classify) and says that the driver-mongodb 500 names its driver in the server log, not in the response body — the two precision notes carried from the attachment landing's contract review. No clause, step or pin changes.", "ref": "#23857" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "acceptance[3] (clause, verify, evidence) says what the gate does now — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): the clause still named a 2000-row cap warning that the scan-cap pin says an ordinary parent no longer emits. Governing text ADR-0139 D4 and D5 (step E7, the attachment gate): one ReadableIds leaf per parent object and no pre-scan, the bounded fail-closed probe kept for D5's three classes (attachment-access-hooks.ts, READ_SCAN_LIMIT), as knownGaps[2]–[3] already record. Read at base 307ce4c4b4", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -892,7 +898,7 @@ "title": "A field's declared accept/maxSize is re-enforced SERVER-SIDE on record write — the control the client widget check explicitly is not — with the declared missing-metadata and dotless-extension holes scored as documented boundaries", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai) — the check is a declaration constraint, not an authz gate: it binds every caller identically, so no restricted persona is needed"], @@ -976,7 +982,7 @@ "⛔ NOT a violation probe: a random/unknown id token in the field passes untouched BY DESIGN — an id matching no sys_file row is treated as an external/legacy value and skipped before any constraint is read (file-reference-lifecycle.ts); probe with a REAL committed sys_file or the run measures nothing" ], "traps": ["absence-inference", "wrong-panel"], - "automated": { "kind": "unit", "ref": "packages/services/service-storage/src/file-reference-lifecycle.test.ts ('accept / maxSize enforcement' describe) — HANDLER-DIRECT: drives the hook through a fake engine, covering the semantics of clauses 0/1/4/5/6 (rejection, per-entry vocabulary, missing-metadata pass, no-constraints pass) at the unit level. It is NOT evidence about the wired REST lane or the wire envelope (clause 2), and per the attach-requires-parent-edit clause-3 lesson a handler-direct green can coexist with different wired behaviour — the dogfood lane has NO pin for this seam, which is precisely why this item exists." }, + "automated": { "kind": "unit", "ref": "packages/services/service-storage/src/file-reference-lifecycle.test.ts ('accept / maxSize enforcement' describe) — HANDLER-DIRECT: drives the hook through a fake engine, covering the semantics of clauses 0/1/3/4/5/6 at the unit level: rejection; the UPDATE refusal (clause 3: its case 'refuses an UPDATE onto an oversize file with the same envelope, leaving the record and both files as they were' asserts the refused swap and the unchanged stored value); per-entry vocabulary; missing-metadata pass; no-constraints pass. It is NOT evidence about the wired REST lane or the wire envelope (clause 2), and per the attach-requires-parent-edit clause-3 lesson a handler-direct green can coexist with different wired behaviour — the dogfood lane has NO pin for this seam, which is precisely why this item exists." }, "source": [ "packages/services/service-storage/src/file-reference-lifecycle.ts#assertFileConstraints (the rationale: the widget check is a convenience, the server re-check is the control; missing metadata is not evidence of a violation), (assertFileConstraints — maxSize guard, testable filter + early return, accept mismatch throw), (matchesAcceptEntry — exact MIME / type\\/* wildcard /.ext-against-NAME vocabulary), (the check rides copy-on-claim in the before hooks — beforeInsert, beforeUpdate — and unknown ids are skipped)", "packages/spec/src/data/field.zod.ts#maxSize (accept/maxSize declared on FieldSchema, 'Offered to the file picker AND enforced on write') + packages/spec/liveness/field.json#live (both keys `live` with this enforcement as evidence)", @@ -988,7 +994,8 @@ "history": [ { "revision": 1, "date": "2026-08-20", "change": "new item from the scoped scan-functionality sweep (扫描功能): the server-side re-enforcement of field accept/maxSize had NO coverage on either lane — the only pinned guard is the client dialog's (records-forms.upload-guard-blocks-confirm), which the enforcement source itself calls 'a convenience rather than a control'. Authored with the qa-media-constraints recipe (no stock showcase field declares either key), the two source-documented holes (missing metadata, dotless-name vs extension-only accept) as documented-boundary passes, the declaration-based content boundary recorded so runs do not score sniffing absence as a leak, and the per-source wire-envelope analysis (ERR_FILE_CONSTRAINT ledgered but status-less → sanitized 500 expected) carried as a knownGap for the run to measure rather than rediscover", "ref": "claude/new-session-0pv25p" }, { "revision": 2, "date": "2026-10-16", "change": "clause 3 and knownGaps WIRE-ENVELOPE expected, per an unmeasured source reading, the sanitized 500 INTERNAL_ERROR with the prose only in the server log, and told the next run to revise the clause if a 4xx appeared (#23878 checklist-accuracy finding). It did: re-measured on main c91af8ed6 with the qa-media-constraints recipe, the accept probe and the size probe both answer 400 ERR_FILE_CONSTRAINT with the field-naming message in the body (FileConstraintError now declares status 400), the compliant control answers 201, and the info-level log carries no line for the refusal (#23932 closed). The clause's oracle moves from log to api and scores status, code and the named field; the knownGap records the measured envelope", "ref": "#24036" }, - { "revision": 3, "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]). fixtures.requires[1] cited line 255 of storage-routes.ts, and fixtures.knownGaps[0] and source[5] line 337 of the showcase actions index.ts, in the comma-range spelling (authored in 8d74a57487 as the lists 241-243 and 255, and 334 and 337). At that commit line 255 was the mime_type of the pending sys_file row the presigned-upload route writes, and line 337 the accept/maxSize of the action gallery's p_attachments param; at this base both have moved. requires[1] now anchors storage-routes.ts#registerStorageFamily, whose POST /storage/upload/presigned handler writes that row, and knownGaps[0] and source[5] anchor index.ts#ActionParamGalleryAction. 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": 3, "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]). fixtures.requires[1] cited line 255 of storage-routes.ts, and fixtures.knownGaps[0] and source[5] line 337 of the showcase actions index.ts, in the comma-range spelling (authored in 8d74a57487 as the lists 241-243 and 255, and 334 and 337). At that commit line 255 was the mime_type of the pending sys_file row the presigned-upload route writes, and line 337 the accept/maxSize of the action gallery's p_attachments param; at this base both have moved. requires[1] now anchors storage-routes.ts#registerStorageFamily, whose POST /storage/upload/presigned handler writes that row, and knownGaps[0] and source[5] anchor index.ts#ActionParamGalleryAction. 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": 4, "date": "2026-10-20", "change": "automated.ref adds clause 3, the update path — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12). Read at base 307ce4c4b4: file-reference-lifecycle.test.ts's 'accept / maxSize enforcement' case 'refuses an UPDATE onto an oversize file with the same envelope, leaving the record and both files as they were' asserts the refused swap and the unchanged stored value at the same handler-direct level as the clauses the ref already names. No clause changed", "ref": "claude/issue-24579-v18-sweep-round12" } ] }, { From 74060513034060baac2897bd9dba8fa23852b779 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:08:12 +0000 Subject: [PATCH 4/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20access?= =?UTF-8?q?-security:=20sharing-rules-widen's=20boot,=20and=20the=20os=20d?= =?UTF-8?q?ev=20walled=20sentence=20in=20two=20more=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy finding 5 and round 11's carried finding 3 (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index af6e592d327..5b3d0404b0c 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1060,7 +1060,7 @@ "title": "Criteria sharing rules widen the OWD baseline: matching rows become visible to the rule's audience, non-matching stay hidden", "since": "v15.1", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -1085,7 +1085,7 @@ "manual one-off share via the record-shares route" ], "steps": [ - "boot showcase isolated; add the audience member to bu_west_coast through sys_business_unit_member (fixtures.requires); give a second member the manager position to record the position confound", + "boot the showcase on its own port and file DB (dogfood §0's isolation, RUNNER's canonical boot line) in the stock `single` tenancy posture: no clause needs OS_TENANCY_POSTURE=isolated or any walled boot; add the audience member to bu_west_coast through sys_business_unit_member (fixtures.requires); give a second member the manager position to record the position confound", "the probes are seeded: share_new_inquiries_with_field_ops matches status='new', and the seed ships one new, one contacted and one closed showcase_inquiry (OWD private). Use the new one as the MATCHING probe and a non-new one as the NON-MATCHING probe", "as the audience member: GET the object list and GET each probe by id", "as a member OUTSIDE the audience: same reads", @@ -1156,6 +1156,12 @@ "date": "2026-10-19", "change": "steps[6] and acceptance[4] (clause, verify, evidence) re-pointed — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.sharing-rules-widen acceptance[4]; wave anchor #24579 §3) with its oracle from round 2 (QA run #24601). §A13 asked that an admin edit to the SEEDED sys_sharing_rule row survive a restart; the seeder that copied declared rules into that table is retired (ADR-0131 D2, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts) and GET /api/v1/data/sys_sharing_rule answers total 0, so there is no seeded row to edit. The administrator's durable act on a declared rule is now the activation switch (POST /api/v1/security/_activation/sharing_rule/NAME), and the clause asserts that switch across two restarts. QA run #24601 drove exactly this recipe at ba4afcc2 and passed. It measured that GET /api/v1/meta/sharing_rule/NAME keeps reading active: true while the ledger holds the rule off, so the oracle is the ledger row and the member's reads, never the metadata read. acceptance[0]–[3] are unchanged", "ref": "#24632" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] spells out its boot — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): the runner read 'boot showcase isolated' as the isolated tenancy posture, which no clause needs (all six passed on stock single). The ledger's 'isolated' boot is dogfood §0's own port and DB; the step now says that, and names the stock single posture. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -3769,7 +3775,7 @@ "title": "Grant-table write contract (sys_user_permission_set / sys_user_position): the retired permission_set_id column is refused, a duplicate grant is 409, an inverted validity window is 400, granted_by is stamped with the caller, and (walled) a grant to a non-member is refused", "since": "v18", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -3783,7 +3789,7 @@ "the member's user id and a showcase permission-set NAME" ], "knownGaps": [ - "acceptance[4] and steps[4] are written for a walled boot holding a second organization A, which a stock showcase boot is not. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. The second organization is created on that boot through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records acceptance[4] blocked(fixture) naming those entries, never fail. Its refusal is pinned through acceptance[0] (grant-holder-membership-refusal.test.ts in automated.ref). ⚠️ The refusal itself does not read the posture — that pin's case 'the predicate is the same on every posture' refuses an organization-scoped row naming a non-member under single too (packages/plugins/plugin-security/src/grant-holder-membership-refusal.ts#registerGrantHolderMembershipRefusal) — so the leg is walled by its wording, not by its mechanism. A single-posture rig with a grantee who holds no membership in the writer's organization is not measured here" + "acceptance[4] and steps[4] are written for a walled boot holding a second organization A, which a stock showcase boot is not. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). The second organization is created on that boot through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records acceptance[4] blocked(fixture) naming those entries, never fail. Its refusal is pinned through acceptance[0] (grant-holder-membership-refusal.test.ts in automated.ref). ⚠️ The refusal itself does not read the posture — that pin's case 'the predicate is the same on every posture' refuses an organization-scoped row naming a non-member under single too (packages/plugins/plugin-security/src/grant-holder-membership-refusal.ts#registerGrantHolderMembershipRefusal) — so the leg is walled by its wording, not by its mechanism. A single-posture rig with a grantee who holds no membership in the writer's organization is not measured here" ] }, "steps": [ @@ -3864,6 +3870,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "knownGaps[0]'s prerequisite sentence says os dev — round 11's carried finding 3 (wave anchor #24579 round 12; report 6285435593 on PR #24801), from QA run #24783's F2: a bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md, the serve.ts AuthPlugin-skipped branch at base 307ce4c4b4). The same sentence PR #24801 wrote into four other walled items, byte for byte. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -3872,7 +3884,7 @@ "title": "Write guards tightened on v18: a walled update cannot null out organization_id, a detail write naming an unreadable header is reference_not_found, a validation rule reading an unreadable header answers like a missing one, and a controlled_by_parent child's attachments / comments / explain follow the master's edit grant", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -3887,7 +3899,7 @@ "for the organization leg: the walled boot" ], "knownGaps": [ - "acceptance[1] and steps[0] (the organization leg, on fixtures.requires[2]'s walled boot) need a walled posture: the Layer 0 write wall refuses an update that empties organization_id only under one, and its pin's control says the single posture is unchanged (tenant-wall-organization-removal.test.ts in automated.ref). On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. A run that does not provision it records acceptance[1] blocked(fixture) naming that entry, never fail; acceptance[0]'s pin carries the leg. acceptance[2] and acceptance[3] are not walled legs and run as written" + "acceptance[1] and steps[0] (the organization leg, on fixtures.requires[2]'s walled boot) need a walled posture: the Layer 0 write wall refuses an update that empties organization_id only under one, and its pin's control says the single posture is unchanged (tenant-wall-organization-removal.test.ts in automated.ref). On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). A run that does not provision it records acceptance[1] blocked(fixture) naming that entry, never fail; acceptance[0]'s pin carries the leg. acceptance[2] and acceptance[3] are not walled legs and run as written" ] }, "steps": [ @@ -3962,6 +3974,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "knownGaps[0]'s prerequisite sentence says os dev — round 11's carried finding 3 (wave anchor #24579 round 12; report 6285435593 on PR #24801), from QA run #24783's F2: a bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md, the serve.ts AuthPlugin-skipped branch at base 307ce4c4b4). The same sentence PR #24801 wrote into four other walled items, byte for byte. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From 79b325d7df85b266144701d9b80203a4ca25468c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:08:12 +0000 Subject: [PATCH 5/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20identi?= =?UTF-8?q?ty-auth:=20the=20reset=20mail=20is=20read=20from=20sys=5Fidenti?= =?UTF-8?q?ty=5Femail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy finding 6 (wave anchor #24579 round 12, claim 6286008102); ADR-0131 D7. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/identity-auth.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 3525723491b..fa826565547 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -982,23 +982,23 @@ "title": "Forgot-password: request → token captured at the dev mail transport → reset; old password refused, expired/reused token refused loudly", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": ["anonymous visitor (holds no session)", "the account owner (before and after reset)"], "fixtures": { "app": "showcase", "requires": [ - "email+password sign-in enabled and an email transport configured — a stock boot sends through the dev `log` transport, so nothing leaves the box; the mail is read from its sys_email row (admin GET /api/v1/data/sys_email — body_text / body_html hold the rendered body and its link): the dev `log` transport is not an env switch (EMAIL_TRANSPORT_PROVIDERS is a const list of what the package can build, packages/plugins/plugin-email/src/transports/index.ts) and LogTransport logs only to, subject and hasText/hasHtml — never the body or the link (packages/plugins/plugin-email/src/email-service.ts)", + "email+password sign-in enabled and an email transport configured — a stock boot sends through the dev `log` transport, so nothing leaves the box; the mail is read from its sys_identity_email row (admin GET /api/v1/data/sys_identity_email — body_text / body_html hold the rendered body and its link). The reset mail is identity mail: plugin-auth's auth.password_reset send passes identityMail: true, and the mail service records identity mail in the deployment-level sys_identity_email, never in the tenant log sys_email (ADR-0131 D7, packages/plugins/plugin-email/src/email-log-objects.ts), so GET /api/v1/data/sys_email holds no reset row: the dev `log` transport is not an env switch (EMAIL_TRANSPORT_PROVIDERS is a const list of what the package can build, packages/plugins/plugin-email/src/transports/index.ts) and LogTransport logs only to, subject and hasText/hasHtml — never the body or the link (packages/plugins/plugin-email/src/email-service.ts)", "a target user with a known current password" ], "knownGaps": [ - "capturing the reset TOKEN means reading the reset mail's sys_email row (admin GET /api/v1/data/sys_email, newest row to the requester) — the LogTransport output carries no link. A deployment whose provider persists no sys_email row leaves the token-capture step blocked(fixture); the request/refusal clauses that don't need the token still run" + "capturing the reset TOKEN means reading the reset mail's sys_identity_email row (admin GET /api/v1/data/sys_identity_email, newest row to the requester) — the LogTransport output carries no link, and sys_email holds no identity mail. The object is readable by the platform administrator only (its rows carry reset and sign-in links, sys-identity-email.object.ts), which the --seed-admin admin is. A deployment whose provider persists no sys_identity_email row leaves the token-capture step blocked(fixture); the request/refusal clauses that don't need the token still run" ] }, "steps": [ "as anonymous, request a reset: POST /api/v1/auth/request-password-reset with the target email (NOTE the live route is request-password-reset / reset-password — the spec's AuthEndpointPaths says /forget-password, a divergence; trust the live server per RUNNER, and file the doc divergence)", - "capture the reset email: as admin, read the newest sys_email row addressed to the requester (GET /api/v1/data/sys_email; to_addresses reads 'NAME ', so match on the address it contains, not on equality) and extract the token from body_text / body_html (the link is GET /api/v1/auth/reset-password/:token). The LogTransport line in the server log shows only to and subject — it never carries the link", + "capture the reset email: as admin, read the newest sys_identity_email row addressed to the requester (GET /api/v1/data/sys_identity_email — identity mail, never sys_email; to_addresses reads 'NAME ', so match on the address it contains, not on equality) and extract the token from body_text / body_html (the link is GET /api/v1/auth/reset-password/:token). The LogTransport line in the server log shows only to and subject — it never carries the link", "complete the reset: POST /api/v1/auth/reset-password with the token + a new password; capture the response", "sign in with the NEW password (2xx) and confirm the OLD password is now refused", "reuse the SAME token a second time (POST /api/v1/auth/reset-password again) and capture the loud refusal — a consumed token must not reset again", @@ -1010,8 +1010,8 @@ { "clause": "the request issues a real reset artifact: POST /api/v1/auth/request-password-reset produces an email at the dev transport carrying a reset token/link (GET /api/v1/auth/reset-password/:token)", "oracle": "api", - "verify": "the sys_email row the request wrote (to = the requester) carries the reset link with a token in its body; the request response is a non-leaking 2xx/accepted", - "evidence": "the captured sys_email row (to, subject, the link with the token redacted after use)" + "verify": "the sys_identity_email row the request wrote (to = the requester) carries the reset link with a token in its body; the request response is a non-leaking 2xx/accepted", + "evidence": "the captured sys_identity_email row (to, subject, the link with the token redacted after use)" }, { "clause": "the token completes the reset and rotates the credential: POST /api/v1/auth/reset-password with the token + new password succeeds, the new password signs in and the old one is refused", @@ -1058,6 +1058,12 @@ "date": "2026-10-19", "change": "acceptance[0].oracle 'log' → 'api' — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.self-service-password-reset acceptance[0]; wave anchor #24579 §3): the clause's verify and evidence already read the sys_email row the request wrote, an API read, not a log line; the oracle label now names what the run consults. Clause text, verify and evidence unchanged", "ref": "#24632" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "fixtures.requires[0], knownGaps[0], steps[1] and acceptance[0] (verify, evidence) read the reset mail from sys_identity_email — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): sys_email held 0 rows and the mail was in sys_identity_email. Governing text ADR-0131 D7 (identity mail is deployment-level); read at base 307ce4c4b4 in auth-manager.ts (the auth.password_reset send passes identityMail: true) and email-service.ts (emailLogObjectOf). Not loosened: the clause still requires a real mail carrying a working tokened link", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From b47fccc36dad680f1a9406324c6d08659c3ff817 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:09:09 +0000 Subject: [PATCH 6/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20automa?= =?UTF-8?q?tion:=20trigger-status-contract's=20INVALID=5FSIGNAL=20body=20a?= =?UTF-8?q?nd=20404=20code,=20flow-authoring=20ref=20narrowed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy finding 7 and QA run #24786 C8 (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/automation.json | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 91a9bacc3e0..0d7b3287862 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1401,7 +1401,7 @@ "title": "The flow-dispatch HTTP status contract: every never-dispatched, ran-and-failed and non-terminal outcome answers its own status + ADR-0112 code, at every door that dispatches a flow", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)", "org member for the resume 403 arm — created by the admin with POST /api/v1/auth/admin/create-user and past its password change (POST /api/v1/auth/change-password): a held session answers 403 PASSWORD_RESET_REQUIRED, the right status for the wrong reason"], @@ -1422,19 +1422,19 @@ "steps": [ "read the table first, from its single definition: packages/runtime/src/flow-dispatch-status.ts (FLOW_NOT_FOUND_STATUS, classifyFlowRefusal, isPausedRun) — it is a module rather than a route mapper precisely because three doors must agree on it, and a second copy of it is a defect by construction. The resume arms are a separate list, read them from packages/runtime/src/domains/automation.ts (the `POST /:name/runs/:runId/resume` branch and its code→status arms)", "boot showcase isolated (dogfood §0) with OS_PORT exported as well as -p (RUNNER.md's connector self-URL fact), sign in as the dev admin, and run the qa-flow-status-doors recipe", - "TRIGGER, per row, against BOTH doors — POST /api/v1/automation/:name/trigger and the legacy POST /api/v1/automation/trigger/:name (the shape client.automation.trigger() calls): a name the deployment does not hold; qa_always_fails; the disabled showcase_urgent_task_alert; qa_no_start_node; a flow that succeeds — showcase_notify_owner with {\"params\": {\"ownerId\": , \"message\": \"…\"}} answers 200 (not showcase_resilient_sync, whose CEL condition reads the record's status and answers a bare trigger 400 FLOW_FAILED); showcase_reassign_wizard (pauses)", + "TRIGGER, per row, against BOTH doors — POST /api/v1/automation/:name/trigger and the legacy POST /api/v1/automation/trigger/:name (the shape client.automation.trigger() calls): a name the deployment does not hold (404 RESOURCE_NOT_FOUND: the status table's 404 row names no code of its own, so the envelope carries the spec's status default, HttpStatusErrorCodeMap in packages/spec/src/api/errors.zod.ts); qa_always_fails; the disabled showcase_urgent_task_alert; qa_no_start_node; a flow that succeeds — showcase_notify_owner with {\"params\": {\"ownerId\": , \"message\": \"…\"}} answers 200 (not showcase_resilient_sync, whose CEL condition reads the record's status and answers a bare trigger 400 FLOW_FAILED); showcase_reassign_wizard (pauses)", "record for every response the HTTP status AND error.code together — neither alone is the contract, and a run that captured only the status cannot tell 409 FLOW_DISABLED from any other 409", "on the 400 FLOW_FAILED responses, additionally record error.details.errorMessage and error.details.summary (the flow author's own text and the per-node accounting) — they are the documented payload of that row and the console reads them from exactly there", - "RESUME: trigger showcase_reassign_wizard with {\"params\": {\"recordId\": , \"objectName\": \"showcase_task\"}} to obtain a live paused runId (without recordId the valid resume fails 400 FLOW_FAILED: its update_record filters on recordId), then drive each refusal arm against POST /api/v1/automation/:name/runs/:runId/resume — a bodyless/valid resume (200, next screen or completion); a resume missing a required screen input (400 INVALID_SCREEN_INPUT); a resume whose signal writes the engine's `$` namespace (400 INVALID_SIGNAL) — spelled {\"output\": {\"$x\": 1}}, which lands as collect.$x: a `$` key inside `inputs` meets the screen check first and answers INVALID_SCREEN_INPUT, and a body carrying both `inputs` and `variables` is refused 400 VALIDATION_FAILED since #24594; a resume of an unknown runId (404 RUN_NOT_FOUND); a resume as the member persona, neither the trigger identity nor a resumeAuthority holder (403 PERMISSION_DENIED); and 409 RESUME_IN_PROGRESS, reproducible with a scratch screen flow start → screen(collect, required x) → http GET a local delay server (e.g. http://127.0.0.1:FREE-PORT/slow?s=6) → screen(collect2, required y) → end: send a second resume while the first is inside the http node (the engine's `resuming` guard holds for the whole continuation), and the second screen makes the intact suspension and a later resume observable (QA run #24596)", + "RESUME: trigger showcase_reassign_wizard with {\"params\": {\"recordId\": , \"objectName\": \"showcase_task\"}} to obtain a live paused runId (without recordId the valid resume fails 400 FLOW_FAILED: its update_record filters on recordId), then drive each refusal arm against POST /api/v1/automation/:name/runs/:runId/resume — a bodyless/valid resume (200, next screen or completion); a resume missing a required screen input (400 INVALID_SCREEN_INPUT); a resume whose signal writes the engine's `$` namespace (400 INVALID_SIGNAL) — spelled {\"inputs\": {\"new_assignee\": \"…\"}, \"output\": {\"$x\": 1}}, which lands as collect.$x. The screen check runs first (refuseInvalidScreenInput before applyResumeSignal, engine.ts), so the body must also satisfy the screen's required new_assignee: {\"output\": {\"$x\": 1}} alone answers 400 INVALID_SCREEN_INPUT (QA run #24779), as does a `$` key inside `inputs`; and a body carrying both `inputs` and `variables` is refused 400 VALIDATION_FAILED since #24594; a resume of an unknown runId (404 RUN_NOT_FOUND); a resume as the member persona, neither the trigger identity nor a resumeAuthority holder (403 PERMISSION_DENIED); and 409 RESUME_IN_PROGRESS, reproducible with a scratch screen flow start → screen(collect, required x) → http GET a local delay server (e.g. http://127.0.0.1:FREE-PORT/slow?s=6) → screen(collect2, required y) → end: send a second resume while the first is inside the http node (the engine's `resuming` guard holds for the whole continuation), and the second screen makes the intact suspension and a later resume observable (QA run #24596)", "after each RETRYABLE refusal, re-read the run and confirm the suspension is STILL PAUSED and a later valid resume still works — 'the refusal left the suspension intact' is half of what those arms mean, and a status alone does not show it", "cross-door: dispatch the SAME disabled flow through POST /api/v1/actions/... with a type:'flow' action and through a declared type:'flow' endpoint, and record what each answers", "capture the per-row × per-door verdict table" ], "acceptance": [ { - "clause": "every TRIGGER row answers its own status and code, on BOTH trigger routes: 404 (no such flow), 409 FLOW_DISABLED, 422 FLOW_NO_START_NODE, 400 FLOW_FAILED — and a successful run answers 200", + "clause": "every TRIGGER row answers its own status and code, on BOTH trigger routes: 404 RESOURCE_NOT_FOUND (no such flow), 409 FLOW_DISABLED, 422 FLOW_NO_START_NODE, 400 FLOW_FAILED — and a successful run answers 200", "oracle": "api", - "verify": "one request per row per route; the observed (status, error.code) pair equals the row in flow-dispatch-status.ts. ⚠️ The dangerous direction is a 200 whose body carries an inner {success:false}: that is the pre-17.1.0 double envelope these rows replaced, and a caller branching on HTTP status alone reads a failed or refused run as a successful one — so a 200 on any of the four refusal rows is a FAIL, not a formatting difference", + "verify": "one request per row per route; the observed (status, error.code) pair equals the row in flow-dispatch-status.ts, whose 404 row names no code of its own: that row's code is the spec's status default RESOURCE_NOT_FOUND (HttpStatusErrorCodeMap, packages/spec/src/api/errors.zod.ts). ⚠️ The dangerous direction is a 200 whose body carries an inner {success:false}: that is the pre-17.1.0 double envelope these rows replaced, and a caller branching on HTTP status alone reads a failed or refused run as a successful one — so a 200 on any of the four refusal rows is a FAIL, not a formatting difference", "evidence": "the per-row per-route status + code table, with one full response body per row" }, { @@ -1527,6 +1527,12 @@ "date": "2026-10-19", "change": "personas, steps[2] and steps[5] name the specimens and spellings each arm needs — checklist-accuracy findings of the 18.0 QA sweep (QA run #24545 steps[2] and steps[5], QA run #24596 acceptance[4]'s 409 arm; wave anchor #24579 §3, comment 6272314386). The success row now names showcase_notify_owner; the wizard is triggered with recordId + objectName; INVALID_SIGNAL is spelled through `output`, since a `$` key in `inputs` meets the screen check first (refuseInvalidScreenInput runs before applyResumeSignal, engine.ts) and the `inputs` + `variables` body #24545 measured as silently dropped is refused since PR #24594 (c3954057fe, the fix of #24546); the 403 member must be past its password change; and 409 RESUME_IN_PROGRESS reproduces with a slow http node before a second screen. Re-derived at base 9a7838edf5. The 503 STORE_UNAVAILABLE arm stays blocked(fixture) (§4)", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[5]'s INVALID_SIGNAL body and the 404 row's code — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). {\"output\":{\"$x\":1}} alone answers 400 INVALID_SCREEN_INPUT on the wizard, whose screen requires new_assignee, because refuseInvalidScreenInput runs before applyResumeSignal (engine.ts at base 307ce4c4b4); the body now satisfies the input too. The 404 row answers RESOURCE_NOT_FOUND: flow-dispatch-status.ts names no code for it, and the envelope takes the spec's HttpStatusErrorCodeMap default. steps[2] and acceptance[0] (clause, verify) state it. No other clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -2055,7 +2061,7 @@ "title": "Flow authoring contract on v18, refused at `os validate` — and at PUT /meta/flow for every flow-body arm but the CEL root scope, which only `os validate` judges: single-brace {…} value tokens (CEL only; text slots use {{ }}), $-prefixed bound variable names, builtin node config keys/values the executor does not declare, dangling or repeated edges, `record`/`user.id` roots a flow cannot bind, retryPolicy.maxRetry, and an anonymous endpoint targeting a runAs:\"system\" flow", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "cli", "personas": [ @@ -2105,7 +2111,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/spec/src/automation/flow-value-slot-template.test.ts + packages/spec/src/automation/flow-text-slot-template.test.ts + packages/spec/src/automation/flow-builtin-node-config-keys.test.ts + packages/spec/src/automation/flow-builtin-node-config-values.test.ts + packages/spec/src/automation/flow-bound-variable-name.test.ts + packages/spec/src/automation/flow.test.ts + packages/lint/src/validate-expressions.flow-cel-root.test.ts + packages/spec/src/shared/retry-policy.test.ts + packages/spec/src/api/anonymous-elevated-flow-gate.test.ts" + "ref": "packages/spec/src/automation/flow-value-slot-template.test.ts + packages/spec/src/automation/flow-text-slot-template.test.ts + packages/spec/src/automation/flow-builtin-node-config-keys.test.ts + packages/spec/src/automation/flow-builtin-node-config-values.test.ts + packages/spec/src/automation/flow-bound-variable-name.test.ts + packages/spec/src/automation/flow.test.ts + packages/lint/src/validate-expressions.flow-cel-root.test.ts + packages/spec/src/shared/retry-policy.test.ts + packages/spec/src/api/anonymous-elevated-flow-gate.test.ts — acceptance[0] ONLY, the clause that scores these nine spec and lint unit files themselves. None of them runs `os validate` or the PUT /api/v1/meta/flow door, so no pin covers acceptance[1] (each arm's `os validate` exit and located refusal), acceptance[2] (the door's 422s with nothing stored, and the two root-scope arms' 200) or acceptance[3] (the three controls validate and save): drive them by hand with the CLI and a boot" }, "source": [ "packages/spec/src/automation/flow-value-slot-template.ts#flowNodeValueTemplateRefusals", @@ -2132,6 +2138,12 @@ "date": "2026-10-19", "change": "steps[0] and acceptance[1] (clause, verify) name arms and verdict shapes os validate really has — checklist-accuracy findings of the 18.0 QA sweep (QA run #24545 acceptance[1], QA run #24558 steps; wave anchor #24579 §3). \"A REST-triggered flow reading record.x\" was ambiguous: an api-type flow binds record (packages/lint/src/flow-cel-root-scope.ts hands record to the api kind); the refused shape is an autolaunched flow. The anonymous-endpoint arm needs an armed rateLimit or ADR-0121 D6 fires beside it (packages/spec/src/api/endpoint-publish-gate.ts). Schema-layer arms surface as STACK_SCHEMA_INVALID with the contract sentence, not a rule id. Re-derived at base 9a7838edf5; round 1's acceptance[2] re-point is unchanged", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 12, round 6's theme-B rule): the ref listed nine spec and lint unit pins with no clause qualifier, though acceptance[1]–[3] need the CLI or the PUT door. The pins are acceptance[0]'s own subject, so `automated` stays, narrowed to it. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From 5c4196502b9403cfe0ac28ff1ab20012523977dd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:09:10 +0000 Subject: [PATCH 7/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20platfo?= =?UTF-8?q?rm-core:=20install-local's=20flag=20and=20artifact,=20two=20kno?= =?UTF-8?q?wnGaps=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA run #24779 checklist-accuracy findings 9, 10 and 11 (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/platform-core.json | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 3e6c6ff2ada..62bd415a948 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1196,7 +1196,7 @@ "title": "Documentation audience gating (book + doc): the tree endpoint and the doc read layer gate on ONE semantics — public/org/permissionSet, fail-closed — and an unclaimed doc never leaks through a public book", "since": "v14.1", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1213,7 +1213,8 @@ ], "knownGaps": [ "Stock showcase authors ONE book with `audience: 'public'` — `showcase_manual` (examples/app-showcase/src/system/books/index.ts) — and none with `audience: { permissionSet: '' }`. Verify against the installed corpus FIRST (GET /api/v1/meta/book and read each item's `audience`): the public clauses run on `showcase_manual`; if no permissionSet-gated book exists, the permissionSet clauses are blocked(fixture) and MUST be recorded as such (or staged per knownGaps[1]) — do NOT score them from the `org` default alone, which is the one audience every authenticated caller passes and therefore proves nothing about the gate.", - "Authoring a book/doc at runtime to create the fixture is acceptable only if the run records that it did so and tears it down; the audience is read off the stored row, so a runtime-authored book exercises the same resolver." + "Authoring a book/doc at runtime to create the fixture is acceptable only if the run records that it did so and tears it down; the audience is read off the stored row, so a runtime-authored book exercises the same resolver.", + "acceptance[2]'s last sentence, an unknown future audience shape also denies, has no pin: the audienceAllows cases in packages/spec/src/system/book.test.ts drive public, org / unset and { permissionSet } (the fail-closed case included), never a shape outside that vocabulary (QA run #24779). The branch is audienceAllows' final `return false` (book.zod.ts, 'unknown future shape → fail closed'): a run reads it from source and records that half not pin-scored, never as a pass carried by the permissionSet case" ] }, "steps": [ @@ -1299,6 +1300,12 @@ "date": "2026-10-19", "change": "fixtures.knownGaps[0] and acceptance[3] (clause, verify) corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.docs-audience-gate; wave anchor #24579 §3). knownGaps[0] said stock showcase authors neither a public nor a permissionSet book; it ships the public `showcase_manual` (examples/app-showcase/src/system/books/index.ts), so only the permissionSet half is a fixture gap. acceptance[3] said a lesser persona's tree is a subset of admin's; audienceAllows (packages/spec/src/system/book.zod.ts) admits a permissionSet doc for holders only and has no admin bypass, so for those books the run's admin non-holder saw less than a holder — the subset relation is holder vs non-holder. acceptance[2]'s unknown-shape half (unreachable live, no pin) is a fixture/pin gap left on #24579 §4", "ref": "#24632" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "knownGaps[2] is new — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): no pin covers acceptance[2]'s 'unknown future audience shape → deny' branch. Read at base 307ce4c4b4: book.test.ts's audienceAllows cases drive the three declared shapes only. The item carries no automated.ref, so there is no ref to narrow. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -1843,7 +1850,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": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1856,7 +1863,7 @@ "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", - "a compiled package artifact for the inline install: pnpm -C examples/app-crm build → dist/objectstack.json (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)", + "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)" ], "knownGaps": [ @@ -1867,12 +1874,12 @@ "steps": [ "boot A (no OS_CLOUD_URL), as admin: GET /api/v1/runtime/config and record features.marketplace + features.installLocal; probe the mounts — GET /api/v1/marketplace/install-local (authed) → 200, and the browse namespace answers as a mounted passthrough (a non-404 that reflects the upstream's reachability, not this kernel's routing). Capture the boot banner's Marketplace plugin line", "boot B (OS_CLOUD_URL=off), as admin: GET /api/v1/runtime/config → still served, now reporting installLocal:true AND marketplace:false (#8389 discovery + #8356 route-table derivation); GET /api/v1/marketplace/install-local → 200 []; a browse-namespace probe (e.g. GET /api/v1/marketplace/packages) → 404 (the proxy did NOT mount); GET /api/v1/cloud-connection/status → 404 (cloud-connection did NOT mount)", - "build the artifact: pnpm -C examples/app-crm build; confirm dist/objectstack.json exists", + "locate the artifact examples/app-crm/dist/objectstack.json, which the tree's `pnpm build` already wrote (fixtures.requires[1]); run `pnpm -C examples/app-crm build` only when it is absent, and record the manifest's id and version", "air-gapped install against boot B: `os package install ./examples/app-crm/dist/objectstack.json` with admin credentials — the CLI signs in, then POSTs the manifest inline to /api/v1/marketplace/install-local (install.ts); capture the success output and the installed entry", "confirm the install registered: GET /api/v1/meta/app lists the CRM app; a CRM object answers on /api/v1/data; GET /api/v1/marketplace/install-local as admin shows the entry WITH installedBy, and storageDir on the listing envelope (data.storageDir — one value for the whole ledger, not a per-entry field)", "per-principal projection (#9011): repeat the GET as the plain member — 200, the entry listed, but installedBy ABSENT from the entry and storageDir ABSENT from the envelope; repeat anonymous — 401", - "reseed/purge on the installed entry: create ONE user-authored row in an installed CRM object and record its id; POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data as admin — capture {deleted, skipped, errors}; re-read: every manifest-declared seed id is gone, the user-authored row SURVIVES (the purge deletes only ids declared in the cached manifest's seed datasets — marketplace-install-local-plugin.ts); restart boot B against the same DB and confirm the purged rows do NOT come back (sampleDataPurged persisted on the ledger entry)", - "POST …/:manifestId/reseed-sample-data as admin — the sample rows return and sampleDataPurged flips back off", + "reseed/purge on the installed entry: create ONE user-authored row in an installed CRM object and record its id; POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data as admin — capture {deleted, skipped, errors}; re-read: every manifest-declared seed id is gone, the user-authored row SURVIVES (the purge deletes only ids declared in the cached manifest's seed datasets — marketplace-install-local-plugin.ts); restart boot B against the same DB and confirm the purged rows do NOT come back. Read the flag where each side keeps it: the purge answer and the listing entry carry withSampleData:false (the listing derives it per request from the caller's own rows and serves no sampleDataPurged key, #21775), while sampleDataPurged:true is persisted in the ledger file STORAGEDIR/MANIFESTID.json (the admin listing's data.storageDir; local-manifest-source.ts), which the rehydrate-time healer reads", + "POST …/:manifestId/reseed-sample-data as admin — the sample rows return, the listing entry reads withSampleData:true again, and the ledger file's sampleDataPurged reads false", "capability gate both sides (#8976): as the plain member, POST install-local (any body), DELETE …/:manifestId, POST reseed, POST purge — each 403 naming the manage_metadata requirement; anonymous on each → 401; the admin's same calls succeeded above", "DELETE /api/v1/marketplace/install-local/:manifestId as admin → 200 {success:true, data:{manifestId, cleanups, note}}; WITHOUT restarting, re-read: a CRM object's data route answers 404 and GET /meta/app no longer lists the CRM app. Then restart once and re-read — the package stays gone (the ledger entry was removed first, so rehydrate does not bring it back)" ], @@ -1904,7 +1911,7 @@ { "clause": "purge deletes EXACTLY the manifest-declared sample rows: every seed-dataset id gone, the user-authored row untouched, already-deleted ids counted as skipped, and the emptiness is DURABLE — sampleDataPurged persists so the rehydrate-time healer does not re-seed on the next restart", "oracle": "api", - "verify": "the purge response counts + the post-purge reads (user row present, seed ids absent) + the post-restart re-read (still absent); reseed then restores them and flips the flag back (marketplace-install-local-plugin.ts rehydrate guard, flag write)", + "verify": "the purge response counts + the post-purge reads (user row present, seed ids absent) + the post-restart re-read (still absent); reseed then restores them and flips the flag back — sampleDataPurged is read in the ledger file, and the listing entry's withSampleData, derived from the caller's rows, follows the rows (marketplace-install-local-plugin.ts rehydrate guard, flag write, handleList)", "evidence": "purge response + before/after/post-restart row reads" }, { @@ -1966,6 +1973,12 @@ "date": "2026-10-19", "change": "personas[1], steps[3] and fixtures.requires[0] corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.marketplace-install-local-lifecycle; wave anchor #24579 §3). The plain member cannot be a fresh sign-up: the stock audience posture is invite_only (plugin-auth audience-posture.ts), so the run created the member through the admin create-user door. steps[3] read `install./examples` (a missing space). On showcase the cloud arm of boot A, and runtime-config on both boots, are mounted by the host config (examples/app-showcase/objectstack.config.ts), and Serve.planMarketplaceWiring wires only what the host did not — the matrix the clauses score is unchanged. No acceptance clause changed", "ref": "#24632" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "fixtures.requires[1], steps[2], steps[6]–[7] and acceptance[4].verify — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). The listing serves withSampleData, derived per request from the caller's rows (#21775), never sampleDataPurged, which lives in the ledger file (local-manifest-source.ts, marketplace-install-local-plugin.ts handleList at base 307ce4c4b4). The artifact step now says what a runner needs: the existing examples/app-crm/dist/objectstack.json the tree's build writes, rebuilt only when absent. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -2691,7 +2704,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": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -2703,7 +2716,8 @@ ], "knownGaps": [ "the publish response's seedApplied summary surfaces inserted/updated/errors but not per-row skip detail — the `ignore says so` clause therefore reads the skip from effect (counts + untouched values) plus the loader's summary where surfaced, not from a UI report", - "`insert` mode's schema comment says 'fail on duplicate', but the loader does NO matching in insert mode (no existing-records preload) — the failure only materializes where the object itself carries a unique constraint; on an unconstrained scratch object an insert replay simply duplicates rows. The matrix records both shapes honestly" + "`insert` mode's schema comment says 'fail on duplicate', but the loader does NO matching in insert mode (no existing-records preload) — the failure only materializes where the object itself carries a unique constraint; on an unconstrained scratch object an insert replay simply duplicates rows. The matrix records both shapes honestly", + "after insert mode has duplicated a natural key (acceptance[3]'s unconstrained shape), upsert and update bind each key to ONE of the duplicates and leave the other unconverged, silently: loadExistingRecords (packages/metadata-protocol/src/seed-loader.ts) keys an unordered find by externalId, so the last row read wins — the NEWER duplicate on QA run #24779's SQLite boot, which upsert rewrote while the original kept its value. No clause scores it: record it beside acceptance[3] as an observation, never as a FAIL of acceptance[0] or negative[0]" ] }, "steps": [ @@ -2794,6 +2808,12 @@ "date": "2026-10-16", "change": "fixtures.requires[0], steps[0] and [6] and acceptance[4].verify corrected (checklist-accuracy findings of QA run #23939, wave #23878; steps[6] from this card's re-run). A seed body refuses a `name` key (SeedSchema is strict; the URL names the seed) and a scratch object needs a sharingModel (its publish is refused, rule security-owd-unset), so the fixture as written could not be authored; steps[6]'s replay ran after steps 2–4 had rewritten A's rows, so its first replay legitimately writes — the step now scores the second replay, which the re-run measured at 0 inserted / 0 updated; and the verify text pinned decideWriteAction to a source line that has since moved — it now cites the symbols only. Not loosened: every mode clause and the replace posture are scored as before", "ref": "#24036" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "knownGaps[2] is new — QA run #24779's observation on acceptance[3] (wave anchor #24579 round 12): after insert mode duplicated a natural key, upsert bound the newer duplicate and left the original unconverged. Read at base 307ce4c4b4 in seed-loader.ts loadExistingRecords, which keys an unordered find by externalId (last row wins). No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From 984ffc56b32b7e1a9e1b9ba7a5611d06c126dc04 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:09:10 +0000 Subject: [PATCH 8/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20api-ba?= =?UTF-8?q?ckend:=20QA=20run=20#24787=20B1=E2=80=93B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit retired-def-refusal's prefixes, the migrate advisory's headline, the showcase's one today() use, and the routes tombstone ref per clause (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/api-backend.json | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index ed165fe6372..60a85c7ee81 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -113,7 +113,7 @@ "title": "Formula runtime fixes hold and date-arithmetic fails at build time", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "build", "steps": [ @@ -143,7 +143,7 @@ "evidence": "error text" }, { - "clause": "the gate does not over-fire: os build on the stock showcase (which uses legitimate date comparisons like due_date == today()) exits 0", + "clause": "the gate does not over-fire: os build on the stock showcase exits 0. Its one today() use is a legitimate date comparison, the field-zoo predicate `record.f_date < today()` (examples/app-showcase/src/ui/actions/predicate-matrix.action.ts); the showcase holds no `== today()`", "oracle": "build", "verify": "untouched showcase os build exit code 0", "evidence": "build output" @@ -170,6 +170,12 @@ "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-10-20", + "change": "acceptance[3]'s example corrected — checklist-accuracy finding B3 of QA run #24787 (wave anchor #24579 round 12): the showcase holds no `due_date == today()`; at base 307ce4c4b4 its one today() use is predicate-matrix.action.ts's `record.f_date < today()`. The over-fire guard is unchanged: the untouched showcase still builds with exit 0", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -178,12 +184,12 @@ "title": "Removed/retired authoring keys fail at parse/build with located guidance", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "build", "steps": [ "sample retired keys across the ADR-0087 registries, at minimum: a query-surface tombstone (QueryAST cursor / joins / distinct / windowFunctions — retiredKey per #4286), the error-envelope tombstone (EnhancedApiError.fieldErrors, renamed to fields by ADR-0114 D4), and one entry from the retired-filter-operator registry in packages/spec/src/data/filter.zod.ts (RetiredFilterOperatorGuidance)", - "author a config/payload using each sampled key: parse the query via the spec schema (QuerySchema.safeParse with cursor: {...}), parse an error envelope carrying fieldErrors; run ObjectSchema.create / os build where the key is config-side. The retired filter operator is DIFFERENT and is driven at runtime: author a stored filter carrying it on a FilterCondition-typed slot — a dashboard widget's `filter`, e.g. { name: { $regex: 'acme' } } (QA run #24541 used an ops-dashboard widget). ⛔ Not a view: a view's `filter` is the ViewFilterRule array [{ field, operator, value }] with a closed operator vocabulary, so the $-object predicate is no view shape at all and confirm parse/build stay GREEN (FilterConditionSchema is an open record keyed by field name — no tombstone can live on it); then send the same predicate as the where of a query request on the running stack (e.g. POST /api/v1/data//query) and capture the 400 INVALID_FILTER refusal; then run `os migrate meta --from 16` and locate the filter-regex-options-retired advisory", + "author a config/payload using each sampled key: parse the query via the spec schema (QuerySchema.safeParse with cursor: {...}), parse an error envelope carrying fieldErrors; run ObjectSchema.create / os build where the key is config-side. The retired filter operator is DIFFERENT and is driven at runtime: author a stored filter carrying it on a FilterCondition-typed slot — a dashboard widget's `filter`, e.g. { name: { $regex: 'acme' } } (QA run #24541 used an ops-dashboard widget). ⛔ Not a view: a view's `filter` is the ViewFilterRule array [{ field, operator, value }] with a closed operator vocabulary, so the $-object predicate is no view shape at all and confirm parse/build stay GREEN (FilterConditionSchema is an open record keyed by field name — no tombstone can live on it); then send the same predicate as the where of a query request on the running stack (e.g. POST /api/v1/data//query) and capture the 400 INVALID_FILTER refusal; then run `os migrate meta --from 16` and locate the advisory by its headline: the human report prints `⚠ [protocol 17] data.filter $regex / $options — …` (printNotice, packages/cli/src/commands/migrate/meta.ts) and never the id; the id filter-regex-options-retired is in the `--json` output only, as todos[].id", "capture each rejection's full text", "the documented-strip control: parse an UNKNOWN key on the RestServerConfig `routes` sub-object — RestServerConfigSchema.safeParse({ routes: { someUnknownKey: 1 } }) (or RouteGenerationConfigSchema directly) — and confirm it parses clean with the key gone (routes reads back {}). The behavior is declared in packages/spec/src/api/rest-server.zod.ts: RouteGenerationConfigSchema's docblock states the schemas are not `.strict()`, which is exactly why its retired keys carry tombstones. No spec docblock declares a 'pure display annotation' that is stripped by design — that example is not the control; this documented non-strict sub-object is", "record which registry each sampled key came from" @@ -256,6 +262,12 @@ "date": "2026-10-19", "change": "steps[1] names a slot the $-operator predicate can be stored on — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.enforce-or-remove-authoring-gates step 2; wave anchor #24579 §3): a view filter is the array shape [{field, operator, value}], not a FilterCondition. Re-derived at base 9a7838edf5: ViewFilterRuleSchema (packages/spec/src/ui/view.zod.ts) is a strict rule object, while the widget filter is FilterConditionSchema (ui/dashboard.zod.ts). No clause changed", "ref": "#24649" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[1] locates the filter advisory by what the report prints — checklist-accuracy finding B2 of QA run #24787 (wave anchor #24579 round 12): the human output of `os migrate meta --from 16` prints the headline `⚠ [protocol 17] data.filter $regex / $options …`, never the id, which is only in `--json` todos[].id (printNotice, migrate/meta.ts at base 307ce4c4b4). No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -1439,7 +1451,7 @@ "title": "A retired metadata DEFINITION (whole schema type withdrawn) refuses at every authoring door — distinct from a retired key, and the door that must not fall through to 'unknown type, ignored'", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "build", "personas": [ @@ -1456,7 +1468,7 @@ ] }, "steps": [ - "enumerate packages/spec/src/migrations/entries/retired-defs/ and record the count and the version prefixes present (the registry currently carries entries at both the current major and the next)", + "enumerate packages/spec/src/migrations/entries/retired-defs/ and record the count and the version prefixes present (the registry carries entries at the previous major and the current one: prefixes 17 and 18, against PROTOCOL_VERSION '18.0.0' in packages/spec/src/kernel/protocol-version.ts. Read each prefix against PROTOCOL_VERSION; no prefix is staged for a later major)", "sample at least four entries spanning BOTH version prefixes and at least three different domains (e.g. data/, system/, identity/, ui/)", "for each sampled def, read its entry file and record which ADR-0087 route retired it, whether a prescription was authored, and its CARRIER: the authored key that embedded it, retired as a retiredKey() tombstone (e.g. manifest.loading for kernel/PluginLoadingConfig and its closure; agent.lifecycle for automation/StateMachine and its family), or none — most entries are schema-export definitions with zero consumers: no metadata-type binding, no stack collection, no embedding (e.g. data/ExternalDataSource, automation/ScheduleState). Sample at least one def of each kind", "build door, per sampled def, each in its own scratch stack run through os build: for a def WITH a carrier, author the carrier key (e.g. manifest.loading: { strategy: 'lazy' }; an agent carrying lifecycle) — the refusal is the carrier's tombstone. For a def WITHOUT one there is no metadata of that type to author ('a stack that declares metadata of the retired type' could only be an invented key, which is the control below), so drive the door it actually had: import its Zod export from its @objectstack/spec subpath in the config (e.g. ExternalDataSourceSchema from '@objectstack/spec/data') and confirm json-schema/DOMAIN/NAME.json is absent from packages/spec/json-schema", @@ -1483,7 +1495,7 @@ "evidence": "the per-door verdict table" }, { - "clause": "the sample spans both version prefixes present in the registry, so the mechanism is proven for the retirements already staged for the NEXT major and not only for those of the shipped one", + "clause": "the sample spans both version prefixes present in the registry, so the mechanism is proven for the current major's retirements (prefix 18, the protocol this runtime implements) and not only for the previous major's (prefix 17)", "oracle": "build", "verify": "the run record lists each sampled def with its version prefix; a single-prefix sample does not satisfy this clause", "evidence": "the sample table" @@ -1521,6 +1533,12 @@ "date": "2026-10-19", "change": "steps[4] says what manifest.loading's runtime door is — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.retired-def-refusal step 5; wave anchor #24579 §3): the carrier's type has no runtime write door. Re-derived at base 9a7838edf5: DEFAULT_METADATA_TYPE_REGISTRY (packages/spec/src/kernel/metadata-plugin.zod.ts) has no manifest, package or plugin entry, and the meta door answers an unregistered type 'is not a metadata type' (metadata-protocol protocol.ts). No clause changed", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] and acceptance[3]'s clause name the registry's prefixes for what they are — checklist-accuracy finding B1 of QA run #24787 (wave anchor #24579 round 12): the prefixes are 17 and 18 and the runtime implements protocol 18, so they are the previous and the current major, and 'staged for the NEXT major' no longer describes prefix 18. Re-measured at base 307ce4c4b4: 224 retired-defs entries (53 at 17, 171 at 18), PROTOCOL_VERSION '18.0.0'. The clause still requires both prefixes", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, @@ -2567,7 +2585,7 @@ "title": "RestServerConfig.routes is an all-tombstone sub-object: every one of the four retired selectors refuses at construction with the prescription that names where the contract moved, while the empty block still constructs", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "build", "personas": [ @@ -2640,7 +2658,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/rest/src/rest-sub-config-parse-not-cast.test.ts (§E pins all four routes.* refusals, the empty-block control and the plugin-path leg)" + "ref": "packages/rest/src/rest-sub-config-parse-not-cast.test.ts — per clause. acceptance[1] in full, by §B's case 'rejects `createRestApiPlugin({ api: { routes: { nameTransform: \"none\" } } }).start()` — a #14691 tombstone, through the plugin path' (§E's own plugin-path case drives batch.defaultAtomic, not a routes key). acceptance[0] by §E's 'refuses every `routes.*` key — the whole sub-object is tombstones', for one authored value per key: nameTransform is driven with 'none' only, so its former 'plural' value is driven by hand. acceptance[4]'s empty-block half by §E's 'an empty `routes` sub-object still constructs — the tombstones refuse keys, not the block'; its other half, an unknown key inside `routes` stripped, is pinned for batch and metadata.endpoints only (§C's 'STRIPS an unknown key inside a sub-object rather than refusing it — the schemas are non-strict'), never for routes. No pin covers acceptance[2] (each prescription names a replacement that exists today) or acceptance[3] (the tsc error): drive them by hand" }, "source": [ "packages/spec/liveness/route_generation.json (the ADR-0049 ledger this item answers: all four properties dead, removed 2026-09-03 — the sub-object ships nothing but its refusals)", @@ -2656,6 +2674,12 @@ "date": "2026-09-04", "change": "new — `route_generation` was UNCLASSIFIED in coverage.json and is the one of the four kinds with no live property left: all four selectors were retired under ADR-0049 on 2026-09-03. Authored rather than waived because the retirement is itself the shipped behaviour — the sub-object stays declared so an authored key is refused with its prescription instead of silently stripped, and that refusal is exactly what a checklist can drive. The prescription-accuracy clause exists because a correct refusal pointing at a replacement that does not exist would still leave the operator stuck", "ref": "#14961" + }, + { + "revision": 2, + "date": "2026-10-20", + "change": "automated.ref narrowed per clause — checklist-accuracy finding B4 of QA run #24787 (wave anchor #24579 round 12, round 6's theme-B rule): the ref named one file with no clause qualifier and does not cover acceptance[2] or [3]. Read at base 307ce4c4b4, narrower than the record's '§E covers a0, a1 and a4': acceptance[1] rests on §B's routes case, acceptance[0] on §E for one value per key (not nameTransform 'plural'), and acceptance[4] on §E's empty-block case only, the routes strip being unpinned. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round12" } ] }, From e44bbdfc84c37fceffd35e9c688b076bc56bc274 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 08:25:45 +0000 Subject: [PATCH 9/9] =?UTF-8?q?docs(qa):=20round=2012=20=E2=80=94=20the=20?= =?UTF-8?q?new=20history=20rows'=20ref=20is=20#24824?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills the 18 history rows written with the branch name before the PR existed (wave anchor #24579 round 12, claim 6286008102). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/access-security.json | 6 +++--- docs/qa/platform-checklist/areas/api-backend.json | 8 ++++---- docs/qa/platform-checklist/areas/approvals.json | 2 +- docs/qa/platform-checklist/areas/attachments-storage.json | 4 ++-- docs/qa/platform-checklist/areas/automation.json | 4 ++-- docs/qa/platform-checklist/areas/identity-auth.json | 2 +- docs/qa/platform-checklist/areas/integration-system.json | 4 ++-- docs/qa/platform-checklist/areas/platform-core.json | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 5b3d0404b0c..19efafa2dd0 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1161,7 +1161,7 @@ "revision": 4, "date": "2026-10-20", "change": "steps[0] spells out its boot — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): the runner read 'boot showcase isolated' as the isolated tenancy posture, which no clause needs (all six passed on stock single). The ledger's 'isolated' boot is dogfood §0's own port and DB; the step now says that, and names the stock single posture. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -3875,7 +3875,7 @@ "revision": 6, "date": "2026-10-20", "change": "knownGaps[0]'s prerequisite sentence says os dev — round 11's carried finding 3 (wave anchor #24579 round 12; report 6285435593 on PR #24801), from QA run #24783's F2: a bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md, the serve.ts AuthPlugin-skipped branch at base 307ce4c4b4). The same sentence PR #24801 wrote into four other walled items, byte for byte. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -3979,7 +3979,7 @@ "revision": 5, "date": "2026-10-20", "change": "knownGaps[0]'s prerequisite sentence says os dev — round 11's carried finding 3 (wave anchor #24579 round 12; report 6285435593 on PR #24801), from QA run #24783's F2: a bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md, the serve.ts AuthPlugin-skipped branch at base 307ce4c4b4). The same sentence PR #24801 wrote into four other walled items, byte for byte. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 60a85c7ee81..8bab717034a 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -175,7 +175,7 @@ "revision": 3, "date": "2026-10-20", "change": "acceptance[3]'s example corrected — checklist-accuracy finding B3 of QA run #24787 (wave anchor #24579 round 12): the showcase holds no `due_date == today()`; at base 307ce4c4b4 its one today() use is predicate-matrix.action.ts's `record.f_date < today()`. The over-fire guard is unchanged: the untouched showcase still builds with exit 0", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -267,7 +267,7 @@ "revision": 6, "date": "2026-10-20", "change": "steps[1] locates the filter advisory by what the report prints — checklist-accuracy finding B2 of QA run #24787 (wave anchor #24579 round 12): the human output of `os migrate meta --from 16` prints the headline `⚠ [protocol 17] data.filter $regex / $options …`, never the id, which is only in `--json` todos[].id (printNotice, migrate/meta.ts at base 307ce4c4b4). No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -1538,7 +1538,7 @@ "revision": 4, "date": "2026-10-20", "change": "steps[0] and acceptance[3]'s clause name the registry's prefixes for what they are — checklist-accuracy finding B1 of QA run #24787 (wave anchor #24579 round 12): the prefixes are 17 and 18 and the runtime implements protocol 18, so they are the previous and the current major, and 'staged for the NEXT major' no longer describes prefix 18. Re-measured at base 307ce4c4b4: 224 retired-defs entries (53 at 17, 171 at 18), PROTOCOL_VERSION '18.0.0'. The clause still requires both prefixes", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -2679,7 +2679,7 @@ "revision": 2, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding B4 of QA run #24787 (wave anchor #24579 round 12, round 6's theme-B rule): the ref named one file with no clause qualifier and does not cover acceptance[2] or [3]. Read at base 307ce4c4b4, narrower than the record's '§E covers a0, a1 and a4': acceptance[1] rests on §B's routes case, acceptance[0] on §E for one value per key (not nameTransform 'plural'), and acceptance[4] on §E's empty-block case only, the routes strip being unpinned. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 518fe7811cf..2f29a7867e2 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1234,7 +1234,7 @@ { "revision": 2, "date": "2026-10-10", "change": "re-pointed after PR #22633 (#22616) closed sys_approval_token to the automatic API (apiEnabled: false, apiMethods: [] — 404 OBJECT_API_DISABLED for every caller): the token-row oracles move to the act door's outcome pages, the request's action history (GET /:id/actions) and the two pinned integration tests (internal-hash, generic-door); the mint clause is scored by test; the expiry leg, whose only recipe was backdating expires_at over the data API, has no surviving door (ttlMs is not REST-reachable and the plugin threads no clock) and is recorded in knownGaps as blocked(fixture) with the pinned 'expired' reason as its oracle. Two steps had contradicted the door before that PR already: the digest has been withheld since #21197, and update was never in the whitelist", "ref": "#22631" }, { "revision": 3, "date": "2026-10-16", "change": "step 7's dead-slot leg said 'reassign the bound approver away first' without saying who can (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on a two-slot request [admin, Ada] minted by remind: the admin's reassign without `from` moved the admin's own slot ([OTHER, Ada]) and Ada's approve token still peeked as the 'Approve · 通过' confirm page; the admin's reassign with from: Ada moved Ada's slot and the same token then rendered 'No longer your approval · 已不在你名下'. The step now passes `from` (or has the bound approver reassign their own slot); the clause is unchanged", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "claude/issue-24579-v18-sweep-round12" } + { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "#24824" } ] }, { diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index be3c78b1d84..5a0d3a6b2e0 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -433,7 +433,7 @@ "revision": 6, "date": "2026-10-20", "change": "acceptance[3] (clause, verify, evidence) says what the gate does now — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): the clause still named a 2000-row cap warning that the scan-cap pin says an ordinary parent no longer emits. Governing text ADR-0139 D4 and D5 (step E7, the attachment gate): one ReadableIds leaf per parent object and no pre-scan, the bounded fail-closed probe kept for D5's three classes (attachment-access-hooks.ts, READ_SCAN_LIMIT), as knownGaps[2]–[3] already record. Read at base 307ce4c4b4", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -995,7 +995,7 @@ { "revision": 1, "date": "2026-08-20", "change": "new item from the scoped scan-functionality sweep (扫描功能): the server-side re-enforcement of field accept/maxSize had NO coverage on either lane — the only pinned guard is the client dialog's (records-forms.upload-guard-blocks-confirm), which the enforcement source itself calls 'a convenience rather than a control'. Authored with the qa-media-constraints recipe (no stock showcase field declares either key), the two source-documented holes (missing metadata, dotless-name vs extension-only accept) as documented-boundary passes, the declaration-based content boundary recorded so runs do not score sniffing absence as a leak, and the per-source wire-envelope analysis (ERR_FILE_CONSTRAINT ledgered but status-less → sanitized 500 expected) carried as a knownGap for the run to measure rather than rediscover", "ref": "claude/new-session-0pv25p" }, { "revision": 2, "date": "2026-10-16", "change": "clause 3 and knownGaps WIRE-ENVELOPE expected, per an unmeasured source reading, the sanitized 500 INTERNAL_ERROR with the prose only in the server log, and told the next run to revise the clause if a 4xx appeared (#23878 checklist-accuracy finding). It did: re-measured on main c91af8ed6 with the qa-media-constraints recipe, the accept probe and the size probe both answer 400 ERR_FILE_CONSTRAINT with the field-naming message in the body (FileConstraintError now declares status 400), the compliant control answers 201, and the info-level log carries no line for the refusal (#23932 closed). The clause's oracle moves from log to api and scores status, code and the named field; the knownGap records the measured envelope", "ref": "#24036" }, { "revision": 3, "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]). fixtures.requires[1] cited line 255 of storage-routes.ts, and fixtures.knownGaps[0] and source[5] line 337 of the showcase actions index.ts, in the comma-range spelling (authored in 8d74a57487 as the lists 241-243 and 255, and 334 and 337). At that commit line 255 was the mime_type of the pending sys_file row the presigned-upload route writes, and line 337 the accept/maxSize of the action gallery's p_attachments param; at this base both have moved. requires[1] now anchors storage-routes.ts#registerStorageFamily, whose POST /storage/upload/presigned handler writes that row, and knownGaps[0] and source[5] anchor index.ts#ActionParamGalleryAction. 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": 4, "date": "2026-10-20", "change": "automated.ref adds clause 3, the update path — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12). Read at base 307ce4c4b4: file-reference-lifecycle.test.ts's 'accept / maxSize enforcement' case 'refuses an UPDATE onto an oversize file with the same envelope, leaving the record and both files as they were' asserts the refused swap and the unchanged stored value at the same handler-direct level as the clauses the ref already names. No clause changed", "ref": "claude/issue-24579-v18-sweep-round12" } + { "revision": 4, "date": "2026-10-20", "change": "automated.ref adds clause 3, the update path — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12). Read at base 307ce4c4b4: file-reference-lifecycle.test.ts's 'accept / maxSize enforcement' case 'refuses an UPDATE onto an oversize file with the same envelope, leaving the record and both files as they were' asserts the refused swap and the unchanged stored value at the same handler-direct level as the clauses the ref already names. No clause changed", "ref": "#24824" } ] }, { diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 0d7b3287862..edd18fe7444 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1532,7 +1532,7 @@ "revision": 4, "date": "2026-10-20", "change": "steps[5]'s INVALID_SIGNAL body and the 404 row's code — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). {\"output\":{\"$x\":1}} alone answers 400 INVALID_SCREEN_INPUT on the wizard, whose screen requires new_assignee, because refuseInvalidScreenInput runs before applyResumeSignal (engine.ts at base 307ce4c4b4); the body now satisfies the input too. The 404 row answers RESOURCE_NOT_FOUND: flow-dispatch-status.ts names no code for it, and the envelope takes the spec's HttpStatusErrorCodeMap default. steps[2] and acceptance[0] (clause, verify) state it. No other clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -2143,7 +2143,7 @@ "revision": 4, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 12, round 6's theme-B rule): the ref listed nine spec and lint unit pins with no clause qualifier, though acceptance[1]–[3] need the CLI or the PUT door. The pins are acceptance[0]'s own subject, so `automated` stays, narrowed to it. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index fa826565547..f582c738867 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -1063,7 +1063,7 @@ "revision": 4, "date": "2026-10-20", "change": "fixtures.requires[0], knownGaps[0], steps[1] and acceptance[0] (verify, evidence) read the reset mail from sys_identity_email — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): sys_email held 0 rows and the mail was in sys_identity_email. Governing text ADR-0131 D7 (identity mail is deployment-level); read at base 307ce4c4b4 in auth-manager.ts (the auth.password_reset send passes identityMail: true) and email-service.ts (emailLogObjectOf). Not loosened: the clause still requires a real mail carrying a working tokened link", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index fe9b8578b0d..6b44c60d9d7 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -115,7 +115,7 @@ "revision": 4, "date": "2026-10-20", "change": "automated.ref narrowed to the one clause its pin asserts whole — checklist-accuracy finding of QA run #24779 (18.0 sweep round 3, 580fd4ed; wave anchor #24579 round 12, round 6's theme-B rule). Re-read at base 307ce4c4b4: the dogfood pin's two cases assert negative[0] whole and halves of acceptance[0]–[2] (the mcp variant; the other two variants' state, not their origin), so no acceptance clause is pin-scored and the ref now says so. `automated` stays, with negative[0]. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -212,7 +212,7 @@ "revision": 4, "date": "2026-10-20", "change": "steps[6] names the view body its per-item publish needs — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): a flat { type, columns } body answers 422 unrecognized_keys; the run published a view item carrying viewKind and config. Read at base 307ce4c4b4 against ViewItemSchema (view.zod.ts), which discriminates on viewKind and requires an OBJECT.view name. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 62bd415a948..1f07836da22 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1305,7 +1305,7 @@ "revision": 3, "date": "2026-10-20", "change": "knownGaps[2] is new — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): no pin covers acceptance[2]'s 'unknown future audience shape → deny' branch. Read at base 307ce4c4b4: book.test.ts's audienceAllows cases drive the three declared shapes only. The item carries no automated.ref, so there is no ref to narrow. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -1978,7 +1978,7 @@ "revision": 6, "date": "2026-10-20", "change": "fixtures.requires[1], steps[2], steps[6]–[7] and acceptance[4].verify — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). The listing serves withSampleData, derived per request from the caller's rows (#21775), never sampleDataPurged, which lives in the ledger file (local-manifest-source.ts, marketplace-install-local-plugin.ts handleList at base 307ce4c4b4). The artifact step now says what a runner needs: the existing examples/app-crm/dist/objectstack.json the tree's build writes, rebuilt only when absent. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] }, @@ -2813,7 +2813,7 @@ "revision": 3, "date": "2026-10-20", "change": "knownGaps[2] is new — QA run #24779's observation on acceptance[3] (wave anchor #24579 round 12): after insert mode duplicated a natural key, upsert bound the newer duplicate and left the original unconverged. Read at base 307ce4c4b4 in seed-loader.ts loadExistingRecords, which keys an unordered find by externalId (last row wins). No clause changed", - "ref": "claude/issue-24579-v18-sweep-round12" + "ref": "#24824" } ] },