From b73ed483f00f05be381f756db12bd1564c7a8dd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 05:24:10 +0000 Subject: [PATCH] docs(qa): re-point the checklist clauses the 17.6.0 release-verification run proved stale The 17.6.0 run (#21330, subject 617f25f8, Console 31971ff1e28f) failed five clauses whose expectation the platform deliberately contradicts, and found three expected-fail notes that 17.6.0 has since made pass. Each revision cites the commit that changed the behaviour and the run's measurement: - access-security.audit-log-browser rev 3: delete rows are written but not served to non-system readers since 30c530e5 (#21194). - api-backend.filter-comparand-conformance rev 3: POST /query refuses in the request schema (VALIDATION_FAILED), GET and the engine INVALID_FILTER (#20116). - api-backend.date-range-preset-matrix rev 2: the preset message is an ordering-position refusal; an equality is refused by the temporal door. - records-forms.import-transform-matrix rev 2: refusals carry no tracker number since f115b1f (#21188). - studio-authoring.view-authoring-live rev 2: the object door serves a container's expanded . items, by design (#7163). - automation.packaged-flow-subflow-disable-refusal rev 2: the remedy sequence holds (36d043b, 0d9349f); a caller's enable is guarded by design (679f95e). - automation.packaged-flow-clone-contract rev 2: durability fixed (cb4c31d); reachability still fails, now filed as #21332. - access-security.packaged-flow-write-door-parity rev 2: the /automation doors keep the packaged-base lock since 4b45afae (#20817). Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6 Co-authored-by: Claude --- .../areas/access-security.json | 34 ++++++++++--------- .../platform-checklist/areas/api-backend.json | 26 ++++++++++---- .../platform-checklist/areas/automation.json | 22 ++++++------ .../areas/records-forms.json | 9 ++--- .../areas/studio-authoring.json | 7 ++-- 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 6eba711c5dd..6cf57b4f07b 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1199,7 +1199,7 @@ "title": "The audit-log browser surfaces attributable events over sys_audit_log with correct actor/object, filters, and a before/after payload drawer", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -1220,21 +1220,21 @@ "open /system/audit-log; wait for the table to settle; screenshot", "locate all three events in the page and read their action / object / actor cells", "narrow to writes and confirm the narrowing is server-side; capture the re-issued /api/v1/data/sys_audit_log request with its $filter. ⚠️ There is no Action dropdown on this console: use the prebuilt filtered views (Recent / Writes / Auth / Config) or the generic Filter Records → Where → Add filter builder to constrain action", - "click the delete row → the side drawer opens; screenshot the Before (old_value) / After (new_value) JSON panels", + "click an UPDATE row (e.g. PATCH a showcase_contact first) → the side drawer opens; screenshot the Before (old_value) / After (new_value) JSON panels. Since 17.6.0 no delete row is served to the page (#21194), so the drawer is checked on an update row", "cross-check the API twin directly: GET /api/v1/data/sys_audit_log?$filter=... for each of the three actions and compare actor/object/action to the page", "attempt to forge the trail: POST and PATCH /api/v1/data/sys_audit_log — both must be refused (get+list only)" ], "acceptance": [ { - "clause": "all three ops produce audit rows with the correct action, actor and target: login→action 'login' attributed to the member; delete→action 'delete' with object_name showcase_task + record_id; settings write→action 'config_change'", + "clause": "all three ops produce audit rows with the correct action, actor and target: login→action 'login' attributed to the member; settings write→action 'config_change' — both SERVED on the data API; delete→action 'delete' with object_name showcase_task + record_id is WRITTEN but, since 17.6.0, NOT served to a non-system reader (administrators included): the ledger serves only rows about records the reader can read, and a deleted record is readable by nobody (30c530e5, #21194)", "oracle": "api", - "verify": "GET /api/v1/data/sys_audit_log returns the three rows; action/actor(user_id)/object_name/record_id match what each op did (fields per packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts)", - "evidence": "the three audit rows" + "verify": "GET /api/v1/data/sys_audit_log returns the login and config_change rows with action/actor(user_id)/object_name/record_id matching what each op did (fields per packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts), and returns NO row for $filter action='delete' even to the admin who deleted; the delete row's existence and attribution are proven from the store itself (the audit table in the telemetry database, read under system context or with the server stopped) — a delete row SERVED on the data API is the FAIL, and so is a delete row missing from the store", + "evidence": "the two served rows + the delete row's stored copy + the empty delete-filtered read" }, { "clause": "the browser renders those same rows: after a screenshot confirms the table painted, the DOM rows carry the same action/actor/object the API returned — the page shows server truth, not a recomputation", "oracle": "dom", - "verify": "post-screenshot, the three rows' Action/Object/Actor cells equal the API values (read the DOM only after render is confirmed — hydration-race)", + "verify": "post-screenshot, the served rows' Action/Object/Actor cells equal the API values (read the DOM only after render is confirmed — hydration-race)", "evidence": "screenshot + the row DOM read" }, { @@ -1250,9 +1250,9 @@ "evidence": "the drawer screenshot annotated 'before/after panels, not a diff'" }, { - "clause": "the API twin reconciles with the page: GET /api/v1/data/sys_audit_log returns the same three events with matching actor/object — a page row without a backing API row (or vice versa) is a FAIL", + "clause": "the API twin reconciles with the page: GET /api/v1/data/sys_audit_log returns the same served events (login, config_change) with matching actor/object, and both omit the delete — a page row without a backing API row (or vice versa) is a FAIL", "oracle": "api", - "verify": "field-by-field compare of the page's three rows against the /api/v1/data/sys_audit_log bodies", + "verify": "field-by-field compare of the page's rows against the /api/v1/data/sys_audit_log bodies", "evidence": "API list vs page rows" }, { @@ -1283,7 +1283,8 @@ "change": "new — audit-log browser over sys_audit_log: attributable events, server-side filter, before/after payload drawer, API cross-check, append-only guard", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-08-18", "change": "re-pointed clause 2 and step 5 at controls that exist. The item named an 'Action filter' dropdown; this console offers prebuilt filtered views (Recent / Writes / Auth / Config) plus a generic Filter Records / Where / Add filter builder. Server-side narrowing — the property the clause is actually for — is provable through the Writes view; naming a control that does not exist invites the absence-inference trap (#9453 CF-9)", "ref": "#9386" } + { "revision": 2, "date": "2026-08-18", "change": "re-pointed clause 2 and step 5 at controls that exist. The item named an 'Action filter' dropdown; this console offers prebuilt filtered views (Recent / Writes / Auth / Config) plus a generic Filter Records / Where / Add filter builder. Server-side narrowing — the property the clause is actually for — is provable through the Writes view; naming a control that does not exist invites the absence-inference trap (#9453 CF-9)", "ref": "#9386" }, + { "revision": 3, "date": "2026-10-02", "change": "re-pointed clause 1 (and the clauses that counted its three rows) at 17.6.0's ledger read rule. 30c530e5 (#21194) serves a non-system reader, administrators included, only audit rows about records it can read, so the delete row is written but never served on the data API; the 17.6.0 release-verification run measured the admin's delete-filtered read at 0 rows while the stored row carried correct attribution. The clause now asserts the login and config_change rows served, the delete row stored and NOT served; step 6's drawer check moves to an update row, since no delete row reaches the page", "ref": "#21330" } ] }, { @@ -2273,10 +2274,10 @@ }, { "id": "access-security.packaged-flow-write-door-parity", - "title": "Write-door parity on a packaged flow: PUT/DELETE /automation/:name must refuse the same packaged artifact that PUT /meta/flow/:name refuses (ADR-0126 §2 locked base) — EXPECTED FAIL today, the /automation door sails through on manage_metadata alone", + "title": "Write-door parity on a packaged flow: PUT/DELETE /automation/:name must refuse the same packaged artifact that PUT /meta/flow/:name refuses (ADR-0126 §2 locked base)", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -2289,7 +2290,7 @@ "capture the flow's full current definition BEFORE any probe (GET /api/v1/automation/showcase_urgent_task_alert) — it is the restore payload" ], "knownGaps": [ - "the acceptance is the ADR-0126 §2 PARITY PROMISE ('the packaged base is locked — in-place edit refused loudly at the write door'), not today's behavior: as of #12438 the /automation door has NO lock — PUT/DELETE /automation/:name reach registerFlow/unregisterFlow with only the manage_metadata authoring gate in front (packages/runtime/src/domains/automation.ts), and the engine has zero lock/provenance check on that path (packages/services/service-automation/src/engine.ts). Clauses 2-3 are EXPECTED FAILS: a 200 there, where /meta refuses the same artifact, is the product finding, tracked centrally in FOLLOW-UPS (#12438). Keep the parity promise as the acceptance so the item flips green when the door is locked, without a rewrite" + "the acceptance is the ADR-0126 §2 PARITY PROMISE ('the packaged base is locked — in-place edit refused loudly at the write door'). It was an expected fail when authored (#12438: the /automation door had no lock); 4b45afae (#20817) gave the /automation write doors the packaged-base lock the /meta door keeps, and on 17.6.0 all three doors answer 403 NOT_OVERRIDABLE with the same lock message. The permanent pin is packages/qa/dogfood/test/packaged-flow-write-door-parity.dogfood.test.ts" ] }, "steps": [ @@ -2297,7 +2298,7 @@ "control leg, the /meta door: PUT /api/v1/meta/flow/showcase_urgent_task_alert with a trivially modified copy of the definition (e.g. label suffix) — capture status, code and WHICH layer answered; repeat with ?package=com.example.showcase and capture that code too", "probe leg 1: PUT /api/v1/automation/showcase_urgent_task_alert with the same trivially modified definition — capture status and, if 2xx, GET the flow back to prove the live registration mutated", "probe leg 2: DELETE /api/v1/automation/showcase_urgent_task_alert — capture status and, if 2xx, confirm GET /api/v1/automation/showcase_urgent_task_alert now 404s (the shipped flow is gone from the live engine)", - "RESTORE, unconditionally: PUT /api/v1/automation/showcase_urgent_task_alert with the stored original definition (re-registering is the cheap path; a cold restart's boot flow pull is the fallback), then GET it back and diff against the stored capture — byte-identical", + "RESTORE, only if a probe mutated anything: diff GET /api/v1/automation/showcase_urgent_task_alert against the stored capture; if it differs (or 404s), restore with a cold restart over the same database (the boot flow pull re-registers the packaged body — a restore PUT is itself refused 403 once the door is locked) and diff again — byte-identical", "verify the flow still fires: trigger its record-change mutation once and confirm a run appears (the restore must revive the trigger binding, not just the definition read)" ], "acceptance": [ @@ -2308,13 +2309,13 @@ "evidence": "both PUT traces (with and without ?package=) + the before/after /meta reads" }, { - "clause": "parity, update door: PUT /api/v1/automation/showcase_urgent_task_alert against the SAME packaged artifact is refused — ⚠️ EXPECTED FAIL today: the door runs only the manage_metadata authoring gate (automation.ts) and registerFlow re-registers with no lock or provenance check (engine.ts), so a 200 here while /meta refused the identical artifact IS the finding. Record the fail with both traces side by side; the defect is tracked centrally in FOLLOW-UPS (#12438) — do not re-file it per run", + "clause": "parity, update door: PUT /api/v1/automation/showcase_urgent_task_alert against the SAME packaged artifact is refused with the same lock the /meta door applies (4b45afae, #20817). A 200 here while /meta refused the identical artifact is the fail — record both traces side by side", "oracle": "api", "verify": "same admin session, same artifact, same-shape body at both doors; the verdicts must MATCH. A 2xx on /automation with a mutated GET read-back, paired with the /meta refusal from clause 1, is a fail of this clause and the expected present-day outcome", "evidence": "the /automation PUT trace + the mutated (or unchanged) GET read-back, paired with clause 1's refusal" }, { - "clause": "parity, delete door: DELETE /api/v1/automation/showcase_urgent_task_alert is refused for the same reason — ⚠️ EXPECTED FAIL today (unregisterFlow, engine.ts, removes the shipped flow from the live engine with no check; 'delete first, refuse second' is the exact shape the #10145 measurement recorded at this door before the capability gate existed, and the lock half is still missing)", + "clause": "parity, delete door: DELETE /api/v1/automation/showcase_urgent_task_alert is refused for the same reason (4b45afae, #20817) — a 200 that removes the shipped flow from the live engine is the fail ('delete first, refuse second' is the shape the #10145 measurement recorded at this door before the lock existed)", "oracle": "api", "verify": "DELETE answers >=400 and the flow still serves; a 200 followed by a 404 on GET /api/v1/automation/showcase_urgent_task_alert is the fail (and the deletion this item's restore step exists to undo)", "evidence": "the DELETE trace + the follow-up GET" @@ -2349,7 +2350,8 @@ "date": "2026-08-26", "change": "new — #12438 measured that PUT/DELETE /automation/:name re-register/unregister a PACKAGED flow on manage_metadata alone while PUT /meta/flow/:name refuses the same artifact: the ADR-0126 §2 lock is unimplemented at the /automation door. Authored as a door-parity item with the parity promise as the acceptance and the present-day 200 recorded as an expected fail (tracked centrally in FOLLOW-UPS), plus a mandatory restore step because the probe mutates a live engine registration", "ref": "#12438" - } + }, + { "revision": 2, "date": "2026-10-02", "change": "retired the expected-fail framing: 4b45afae (#20817) locked the /automation write doors against the packaged base, and the 17.6.0 release-verification run measured PUT and DELETE /automation/showcase_urgent_task_alert answering 403 NOT_OVERRIDABLE with the /meta door's lock message, the flow unchanged and still firing. Title, knownGaps and clauses 2-3 now state the parity as holding; the restore step is conditional (a restore PUT is itself refused), and the knownGap names the permanent pin packaged-flow-write-door-parity.dogfood.test.ts", "ref": "#21330" } ] }, { diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 999cf927d00..85d7fe81460 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1117,7 +1117,7 @@ "title": "Filter comparand conformance: only the accepted comparand TYPES pass the where-door, a bigint past the exact-integer limit is refused, and a dotted head is classified rather than guessed", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1167,9 +1167,9 @@ "evidence": "per-class request + observed handling" }, { - "clause": "the refusal is a CONTRACT-level refusal, not a transport quirk: the same bad comparand is refused through a second door with the same code", + "clause": "the refusal is a CONTRACT-level refusal, not a transport quirk: the same bad comparand is refused through a second door with the same contract sentence — and with the code that door's family answers: POST /api/v1/data/:object/query refuses it in the request schema, 400 VALIDATION_FAILED with a fields[] entry at query.where..; GET ?$filter= and the engine answer 400 INVALID_FILTER", "oracle": "api", - "verify": "compare the two doors' error codes; a comparand refused at REST but accepted through the engine is a FAIL of the single-contract property", + "verify": "drive the same bad comparand through POST /query, GET ?$filter= and the engine door; each must refuse (status 400, never rows, never 500) and each must quote the same accepted-type sentence. The code split is the documented door posture since #20116 moved the POST refusal into the request schema (cfc3bcf1 #20247, dd1b8031 #20325) — the same split api-backend.query-contract-matrix rev 3 records — so VALIDATION_FAILED on POST beside INVALID_FILTER on GET is NOT a fail. A comparand refused at one door but accepted (rows returned) at another IS the fail of the single-contract property", "evidence": "both refusals side by side" } ], @@ -1217,6 +1217,12 @@ "date": "2026-08-21", "change": "made clause 2 (the bigint exact-integer boundary) name the door it is actually assertable at, and marked it door-specific. As written it prescribed 'both requests' against the api oracle, but the refusal arm is guarded by `typeof value === 'bigint'` and JSON.parse never yields a bigint, so the clause was unreachable over REST at any magnitude — a runner following it collected a 200 that proves nothing in either direction. Re-derived rather than assumed: 9007199254740993 parses to 9007199254740992, which is exactly the limit and so also inside the inclusive bound, giving two independent reasons the REST arm cannot fail. Clause 2 now points at engine-comparand-type-door.test.ts and the five driver conformance suites with oracle: test, step 3 says to pass a real bigint in-process, and a knownGap records that the REST verdict is not-applicable rather than blocked (#10236 A3)", "ref": "#10236" + }, + { + "revision": 3, + "date": "2026-10-02", + "change": "re-pointed clause 5 at the door posture #20116 established. It demanded the SAME CODE at a second door; on 17.6.0 the POST /query door refuses the comparand in the request schema (400 VALIDATION_FAILED, fields[] at query.where.f_number.$eq) while GET $filter and the engine answer 400 INVALID_FILTER — every door refusing, none returning rows, the same contract sentence throughout. The release-verification run scored it a stale-clause fail; the clause now asserts refusal + shared sentence at every door and names each door's code, mirroring query-contract-matrix rev 3", + "ref": "#21330" } ] }, @@ -1225,7 +1231,7 @@ "title": "Every date-range preset resolves to its declared macro window, and a bare preset name used as a raw comparand is refused with the prescription", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -1246,7 +1252,7 @@ "record the run's wall clock and timezone BEFORE issuing any request — every expectation below is derived from it", "for EACH preset, issue a data-API read filtered by that preset against the date field and capture the answer set", "for each preset, independently compute the expected window from DATE_RANGE_PRESET_MACRO_WINDOWS and the recorded clock, then reconcile the answer set against a direct comparison-operator query over that literal window", - "issue a read whose comparand is the BARE preset name as a raw string (e.g. where: { created: 'today' }) and capture the refusal", + "issue reads whose comparand is the BARE preset name as a raw string against a date field — once in an ORDERING position (e.g. where: { signed_on: { $gte: 'this_week' } }) and once as an equality (e.g. where: { signed_on: 'today' }) — and capture both refusals", "issue a read with an unknown preset name and capture the refusal" ], "acceptance": [ @@ -1263,9 +1269,9 @@ "evidence": "the 13-row table" }, { - "clause": "a BARE preset name used as a raw comparand is REFUSED with the prescription telling the author the correct spelling — not treated as a literal string comparison that silently matches nothing", + "clause": "a BARE preset name used as a raw comparand on a date field is REFUSED — never treated as a literal string comparison that silently matches nothing: in an ORDERING position ($gt/$gte/$lt/$lte) the refusal carries the preset prescription telling the author the correct spelling; as an equality or membership it is refused by the temporal door ('not a date value this platform can interpret')", "oracle": "api", - "verify": "status 4xx and the message matches bareDateRangePresetComparandMessage; a 200 with zero rows is the FAIL mode (it is indistinguishable from a correct empty answer)", + "verify": "ordering arm: 400 and the message matches bareDateRangePresetComparandMessage (the schema door judges ordering positions only, by design — packages/spec/src/migrations/entries/semantic/18.filter-preset-ordering-comparand-refused.ts: 'it judges no equality or membership'); equality arm: 400 INVALID_FILTER with the temporal-door message. A 200 with zero rows on either arm is the FAIL mode (it is indistinguishable from a correct empty answer). A bare preset name against a TEXT field answering 200 is not this clause's subject — a text column may legitimately hold the word", "evidence": "status + message" }, { @@ -1316,6 +1322,12 @@ "date": "2026-08-17", "change": "new — DATE_RANGE_PRESETS landed at 17.0.0 (2026-08-15) with 13 members and no matrix item; the enum is exported as a const array so it is enumSource-pinnable, closing it against a 14th preset landing unnoticed. The bare-preset-comparand refusal is authored as its own clause because the spec ships a dedicated message for it, which means the failure mode was observed rather than imagined", "ref": "#9299" + }, + { + "revision": 2, + "date": "2026-10-02", + "change": "split clause 3 into its two designed arms. The clause expected the preset prescription for step 5's own example, an EQUALITY (where: { created: 'today' }); the platform deliberately raises that message only in ordering positions (18.filter-preset-ordering-comparand-refused.ts) and refuses an equality on a date field at the temporal door (400 INVALID_FILTER). Measured on 17.6.0 by the release-verification run: equality → INVALID_FILTER with the temporal message, $gte → VALIDATION_FAILED with bareDateRangePresetComparandMessage, neither ever 200. Step 5 now drives both arms", + "ref": "#21330" } ] }, diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 19cc7e43b9e..ab4fcda114a 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1408,10 +1408,10 @@ }, { "id": "automation.packaged-flow-subflow-disable-refusal", - "title": "Disabling a packaged flow that packaged callers invoke as a subflow is refused 409 DELETE_RESTRICTED naming every caller; the refused attempt writes no ledger row; enable is never guarded", + "title": "Disabling a packaged flow that packaged callers invoke as a subflow is refused 409 DELETE_RESTRICTED naming every caller; the refused attempt writes no ledger row; enabling the subflow itself is never guarded", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1433,7 +1433,7 @@ "map pair: POST /api/v1/automation/showcase_one_task_signoff/toggle {\"enabled\": false} — record the answer", "after each refusal: GET /api/v1/data/sys_metadata_activation and assert NO row exists for the refused flow; then prove it is still ARMED — trigger a run through its caller (PATCH a showcase_task to status='done' for the notify pair) and confirm the child's subflow step executed", "remedy sequence (the refusal's own instruction): POST /api/v1/automation/showcase_task_done_notify_owner/toggle {\"enabled\": false} (the CALLER — it has no packaged callers of its own, so this lands), then RETRY POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": false} and record what it answers", - "enable arm: toggle showcase_task_done_notify_owner back ON, then — with the caller armed — POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": true} (a no-op enable of an already-armed flow) and confirm it is never refused", + "enable arm, child FIRST: POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": true} while its caller is still off (confirm it is never refused), then toggle showcase_task_done_notify_owner back ON, then repeat the child's enable with the caller armed (a no-op enable). ⚠️ Enabling the CALLER while its packaged subflow is still disabled is refused by design since 679f95e (#20711) — 409 RESOURCE_CONFLICT 'cannot be enabled while 1 packaged subflow it calls is disabled … Enable it first' — so the caller-first order is not a fail of this item", "teardown: re-enable anything left disabled (or discard the isolated DB)" ], "acceptance": [ @@ -1456,15 +1456,15 @@ "evidence": "the ledger read (absence) + the caller run's step-log excerpt" }, { - "clause": "ENABLE is never guarded — arming a flow cannot break a caller, so the enable arm answers 2xx regardless of callers", + "clause": "enabling a CHILD (a called subflow) is never guarded — arming it cannot break a caller, so the child's enable answers 2xx regardless of its callers' state", "oracle": "api", - "verify": "step 6's enable answers 2xx with no DELETE_RESTRICTED (guard is on the !enabled branch only, engine.ts; pinned engine-level at flow-activation-ledger.test.ts)", + "verify": "both of step 6's child enables answer 2xx with no DELETE_RESTRICTED (the disable guard runs on the !enabled branch, engine.ts; pinned engine-level at flow-activation-ledger.test.ts). The separate enable-side guard on a CALLER (679f95e, #20711) is not this clause's subject", "evidence": "the enable response" }, { "clause": "the refusal's own remedy is followable: after disabling the calling flow first, the child's disable lands (2xx + ledger row active=false)", "oracle": "api", - "verify": "step 5's retry. ⚠️ EXPECTED FAIL at af56546, kept as the assertion on purpose: packagedSubflowCallers scans the REGISTERED flow map with no activation check (engine.ts — it skips self and non-packaged callers, nothing else), so a disabled caller still guards and the retry still answers 409 — while both the engine's own refusal message ('Disable the calling flow(s) first…') and ADR-0126 §7.3's rationale ('The refusal is honest, actionable (disable the callers first, or don't)') name exactly this sequence as the way out. A red here is a product finding about the remedy sentence — the door tells the administrator to do something it then refuses — already tracked by the #12438 sweep; record it, do NOT soften this clause to match the scan", + "verify": "step 5's retry answers 2xx and the ledger row reads active=false — the sequence both the engine's own refusal message ('Disable the calling flow(s) first…') and ADR-0126 §7.3's rationale ('The refusal is honest, actionable (disable the callers first, or don't)') name as the way out. (Authored as an expected fail at af56546, when the caller scan had no activation check; fixed by 36d043b, #20724, and 0d9349f, #20759.) A 409 on the retry is the fail — the door telling the administrator to do something it then refuses", "evidence": "the caller's successful disable + the retry's full answer, side by side with the first refusal's remedy sentence" } ], @@ -1492,7 +1492,8 @@ "date": "2026-08-26", "change": "new — the ADR-0126 §7.3 disable guard had only engine-level unit pins and no HTTP-level assertion anywhere: the wire 409 DELETE_RESTRICTED with callers named, the map-caller arm, the no-row-on-refusal invariant, and the never-guarded enable. The remedy-sequence clause (disable the caller first) is authored as an expected fail: the shipped scan consults registration only (engine.ts), so the refusal's own named remedy does not currently unblock the child — the clause keeps the promise the door itself makes and flags the red as a tracked product finding", "ref": "#12438" - } + }, + { "revision": 2, "date": "2026-10-02", "change": "retired clause 5's expected-fail note and re-ordered step 6. The remedy sequence now holds (36d043b #20724, 0d9349f #20759): on 17.6.0 the caller's disable then the child's retry both answer 200 and the ledger reads active=false. Step 6's caller-first enable is refused by design since 679f95e (#20711, 409 RESOURCE_CONFLICT naming the disabled subflow), so the step enables the child first and clause 4 is scoped to a child's enable. Measured by the 17.6.0 release-verification run", "ref": "#21330" } ] }, { @@ -1500,7 +1501,7 @@ "title": "POST /automation/:name/clone: whole-definition copy mutating exactly name/label/status, protection envelope dropped, no ancestry, draft-but-armed (double-fire notice verbatim), 409/404/400 refusal arms — and the honest clause: the clone must survive a restart and reach Studio", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -1564,7 +1565,7 @@ { "clause": "the honest clause — the clone OUTLIVES the process and REACHES Studio: after a cold restart over the same database file the clone still exists and dispatches, and it is listed on a Studio Automations rail (it is 'an ordinary org/install-owned flow', ADR-0126 §7.1, under the shipped 'customize in Studio' promise §1.3 — the Setup page's own copy says 'Editing happens in Studio')", "oracle": "api", - "verify": "step 8: GET /api/v1/automation/qa_urgent_alert_clone on the new process, and the flow present in metadata a Studio rail can list. ⚠️ EXPECTED FAIL at af56546, kept as the assertion on purpose: the clone route registers through automationService.registerFlow ONLY — no sys_metadata write anywhere in the arm (domains/automation.ts) — so the boot flow pull has nothing to re-register and the clone vanishes on restart; and the Studio Automations rail lists PACKAGE-scoped metadata (objectui StudioDesignSurface.tsx loadPackageSurfaces(client,'flow',packageId)) while the clone's _packageId was deliberately stripped (flow-clone.ts), so it is reachable in no rail. A red here is a product finding already tracked by the #12438 sweep (clone registered engine-only); record which half failed (durability, reachability, or both) — do NOT soften the clause to a same-process read", + "verify": "step 8: GET /api/v1/automation/qa_urgent_alert_clone on the new process, and the flow present in metadata a Studio rail can list. DURABILITY holds since cb4c31d (#20907, with the clone save path #20761): the clone survives a cold restart and still dispatches. ⚠️ REACHABILITY is an EXPECTED FAIL, kept as the assertion on purpose: the Studio Automations rail lists PACKAGE-scoped metadata (objectui StudioDesignSurface.tsx loadPackageSurfaces(client,'flow',packageId)) while the clone is stored with package_id null (_packageId deliberately stripped, flow-clone.ts), so it is reachable in no rail — tracked at #21332. Record which half failed (durability, reachability, or both) — do NOT soften the clause to a same-process read", "evidence": "the post-restart GET (present or 404) + the meta/flow listing showing where, if anywhere, the clone is held" } ], @@ -1595,7 +1596,8 @@ "date": "2026-08-26", "change": "new — the ADR-0126 §7.1 clone door landed with route-level mocked pins only; nothing asserted the live contract (double-fire with the notice verbatim, references not re-pointed, the refusal arms on the real dispatcher) and nothing anywhere asked the two questions the honest clause pins: the clone registers engine-only (no sys_metadata write) and its _packageId is stripped, so restart survival and Studio reachability — both halves of the ADR's own 'ordinary flow' promise — are authored as an expected-fail probe rather than left unasked", "ref": "#12438" - } + }, + { "revision": 2, "date": "2026-10-02", "change": "clause 7's expected-fail note now names only the reachability half. The durability half was fixed by cb4c31d (#20907, with #20761): on 17.6.0 the clone survives a cold restart, reads back with its edited label and still fires (release-verification run). The reachability half still fails — no Studio surface lists a package-less flow — and is now filed as #21332 instead of resting on the #12438 sweep", "ref": "#21330" } ] }, { diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index d6cd6dbe6ef..e4dbfc832ca 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -4025,7 +4025,7 @@ "title": "Import mapping TransformType matrix: each of the 7 declared transforms does exactly its documented conversion, javascript is refused loudly at the door (no sandbox), format mismatch is a named 400", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -4054,7 +4054,7 @@ "steps": [ "POST /api/v1/data/showcase_inquiry/import with mappingName 'showcase_inquiry_feed' and a CSV whose Channel column carries one mapped code ('Webform') and one unmapped string; read back the created rows", "author scratch mappings (one per remaining transform) over PUT /api/v1/meta/mapping/; import a vector file through each and read back the rows", - "author a scratch mapping declaring transform 'javascript' on one column; POST an import through it and capture the refusal", + "author a scratch mapping declaring transform 'javascript' on one column (bare — the javascript transform takes no params, and an `expression` key is refused 422 at authoring); POST an import through it and capture the refusal", "POST a JSON payload through a sourceFormat:'csv' mapping and capture the refusal; POST an xlsx payload through the same and confirm it is ACCEPTED (tabular-compatible)" ], "acceptance": [ @@ -4065,7 +4065,7 @@ "evidence": "the vector files + per-variant row reads" }, { - "clause": "javascript is declared-but-refused, LOUDLY and BEFORE any row lands: a mapping carrying transform 'javascript' answers 400 code UNSUPPORTED_TRANSFORM whose message names the missing server-side sandbox and framework#2611 — and zero rows are created", + "clause": "javascript is declared-but-refused, LOUDLY and BEFORE any row lands: a mapping carrying transform 'javascript' answers 400 code UNSUPPORTED_TRANSFORM whose message names the missing server-side sandbox in words (since f115b1f, #21188, refusals carry no tracker number) — and zero rows are created", "oracle": "api", "verify": "the refusal (import-mapping.ts — checked per-entry before applyMappingToRows runs) + an unchanged target count; the spec deliberately still admits the value (TransformType), so the DOOR is the contract", "evidence": "the 400 body + before/after counts" @@ -4110,7 +4110,8 @@ "date": "2026-08-30", "change": "initial — sweep gap (spec-enums angle): TransformType's 7 members had no per-variant coverage; named-import-mapping drives only the default copy + upsert path. Authored as a sibling item rather than widening that one (different axis, different fixtures), with the enumSource pin so an 8th transform trips VARIANTS STALE", "ref": "#sweep-2026-08-30" - } + }, + { "revision": 2, "date": "2026-10-02", "change": "clause 2 no longer requires the refusal to name framework#2611: f115b1f (#21188) made REST refusals state each decision in words instead of a tracker number, and on 17.6.0 the UNSUPPORTED_TRANSFORM message names the missing sandbox without it (measured by the release-verification run, 0 rows landed). Step 3 notes the javascript transform is authored bare", "ref": "#21330" } ] }, { diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 2d9bd132edc..dc5d6fbf802 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -169,7 +169,7 @@ "title": "List + form view authoring goes live in the running app on publish — no server restart", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "mixed", "personas": ["admin (authors)", "end user (consumes the published views)"], @@ -210,7 +210,7 @@ { "clause": "the published container round-trips through the consumer read door", "oracle": "api", - "verify": "GET /api/v1/meta/view?object=repair_asset includes qa_repair_asset_views with the authored list/form bodies", + "verify": "GET /api/v1/meta/view?object=repair_asset includes the container's EXPANDED items — repair_asset.default (list) and repair_asset.form (form) — carrying the authored list/form config. The object door never returns the container under its own name (expandViewContainer, packages/spec/src/ui/view.zod.ts; #7163, #7736, #13407), so the container name's absence is not a fail; an expanded item missing or carrying the old config is", "evidence": "the object-scoped view read" }, { @@ -231,7 +231,8 @@ "docs/audits/2026-07-studio-package-create-ux-dogfood.md (publish→live launcher/list inside one server session)" ], "history": [ - { "revision": 1, "date": "2026-08-07", "change": "new item: list/form view authoring through the ?mode=draft door with live-in-app verification, grounded in ViewSchema's container contract and the audit's zero-restart loop", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-07", "change": "new item: list/form view authoring through the ?mode=draft door with live-in-app verification, grounded in ViewSchema's container contract and the audit's zero-restart loop", "ref": "claude/platform-test-checklist-ocwugl" }, + { "revision": 2, "date": "2026-10-02", "change": "re-pointed clause 4 at the object door's designed shape. It expected the container name qa_repair_asset_views in GET /meta/view?object=repair_asset; the door serves the container's expanded . items instead, by design since #7163 (runtime door #7736 / #13407). The 17.6.0 release-verification run measured the authored bodies round-tripping as repair_asset.default / repair_asset.form with the container name appearing 0 times", "ref": "#21330" } ] }, {