Repository navigation
QA run · release-18.2.0-w2-manual-api (29/29) · 756220c4 · 2026-10-27 · 27 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #26476
Description
QA run record for the 18.2.0 release sweep, wave 2, card W2-C3a: the manual set, api / cli / build half (wave anchor #25807). The subject is the release head 756220c4, against the Version Packages PR #25640. Run with the checklist-test skill, following docs/qa/platform-checklist/RUNNER.md and the dogfood-verification skill.
This record supersedes the api/cli/build half of the wave-1 manual set #25817, which ran at 6679644a. The browser half belongs to sibling card W2-C3b. Every item in this card's list was re-judged here, so nothing needs to be carried forward from #25817 for this half (see "Supersede" below).
Environment fingerprint
- Framework:
756220c4a7adcbb1228864607cb4b6a1ca486dc0, detached. It was a cold tree:pnpm install --frozen-lockfile, thenpnpm build --concurrency=2, 119/119 tasks, 0 cached, 11m54s.node scripts/check-dev-prereqs.mjsis green. The tree is unreleased, so health reports18.1.0; the protocol handshake reports protocolVersion18.0.0. - Console: not separately built (
pnpm console:buildwas not run). Only one clause needs it:automation.packaged-flow-clone-contractA6, the Studio reachability half.- A tree boot's
/_console/answered 503 "Console is not built … run pnpm console:build", becausepnpm buildleavespackages/console/distabsent. - So that leg ran on a scratch app copy whose
@objectstack/consoleresolves toui/apps/console/dist, the in-repo console thatpnpm buildproduced from this same commit at 01:41Z. .objectui-shais retired, and the console is theui/tree of this commit.
- A tree boot's
- App and boot:
examples/app-showcase, booted withOS_PORT=PORT setsid pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p PORT -d file:SCRATCH/RUNNER/BOOT.db. Every boot had its own port in 42100–42699 and its own file DB. The seed was--seed-admin(admin@objectos.ai) plus the showcase seed. Every server was stopped by process group, and its port was confirmed free.- Scratch app copies (tar, without
node_modules,distor.objectstack;node_modulessymlinked to the tree's copy) were used for:- every
os build/os compile/os migrateleg, built from the blank scaffold; - the production
os servefresh-install leg (randomOS_AUTH_SECRET/OS_SECRET_KEY); - every install-local and manifest-install boot;
- the connector-degraded twins;
- the Studio leg.
- every
examples/app-showcase/.objectstack/installed-packages/never existed.- The tree boots did write
examples/app-showcase/.objectstack/data/(uploads/,showcase_external.db). It is git-ignored, sogit statusstayed clean, but a per-boot-dDB does not isolate these two (see the accuracy findings).
- Scratch app copies (tar, without
- Special switches:
OS_AUTH_TWO_FACTOR=trueon the two-factor armed boot, with an unarmed control boot;OS_CLOUD_URL=offon the install-local B boot;--log-level infoon the connector-degraded boot;NODE_OPTIONS=--import packages/cli/test/fixtures/live-conversion-probe.mjsonly on the build/validate conversion-parity legs;mfa_required/ grace were changed viaPUT /api/settings/authand restored.- No walled posture, and no scheduled-work switch.
- Execution: six runner subagents ran in parallel, one per port range: R1 access/approvals · R2 api-backend + retests · R3 automation/MCP + retests · R4 two-factor/search/storage · R5 cli/build · R6 platform-core/connector. The coordinator built the tree and ran one smoke boot (42000, healthy, stopped).
git status --porcelainwas empty at the end. - Timings (UTC, 2026-10-27):
- Env prep: 01:29 to 01:43, about 14 min (install 0.5 min, build 12 min, smoke boot 1 min).
- Testing wall clock: 01:43 to 02:02, about 19 min.
- Runner-minutes: in-run prep (boots, personas, scratch copies) about 25, clauses about 64, off-clause probes about 32.
Scope
The set is named by hand (card W2-C3a). It holds the unpinned since:v18.x items plus the unpinned P1 items whose source[] cites a file changed by a Clause-②: yes changeset in #25640. P0 items are excluded (card W2-C1). Every id was resolved with node scripts/checklist-select.mjs ID --json: all 29 are runnable, none is blocked or planned, and none carries an automated.ref, so every clause was driven live (REST / CLI).
| item | priority | surface | rev | verdict |
|---|---|---|---|---|
| access-security.record-share-grant-revoke | P1 | api | 7 | PASS |
| access-security.owd-save-gate | P1 | api | 7 | PASS |
| access-security.packaged-flow-write-door-parity | P1 | api | 4 | PASS |
| ai.mcp-run-action-exposure-gate | P1 | api | 4 | PASS |
| api-backend.batch-transactional-discovery | P1 | api | 7 | PASS |
| api-backend.enforce-or-remove-authoring-gates | P1 | build | 6 | PASS |
| api-backend.error-envelope-ledger | P1 | api | 6 | PASS |
| api-backend.filter-comparand-conformance | P1 | api | 3 | PASS |
| api-backend.aggregate-contract-matrix | P1 | api | 5 | PASS |
| api-backend.api-methods-verb-gate | P1 | api | 2 | PASS |
| approvals.decision-only-via-service | P1 | api | 3 | PASS |
| approvals.approver-resolution-matrix | P1 | api | 7 | PASS |
| attachments-storage.field-accept-maxsize-server-enforced | P1 | api | 5 | PASS |
| automation.trigger-status-contract | P1 | api | 6 | PARTIAL (A4's 503 STORE_UNAVAILABLE arm blocked(fixture)) |
| automation.packaged-flow-subflow-disable-refusal | P1 | api | 9 | PARTIAL (N1 blocked(fixture)) |
| automation.packaged-flow-clone-contract | P1 | api | 8 | PASS |
| cli.build-own-contract | P1 | build | 8 | PASS |
| cli.hook-body-extraction-gates | P1 | cli | 6 | PASS |
| cli.fresh-install-migrate-apply-first | P1 | cli | 2 | PASS |
| identity-auth.two-factor-enrollment-reveal | P1 | api | 3 | PASS |
| identity-auth.two-factor-verify-to-activate | P1 | api | 2 | PASS |
| identity-auth.two-factor-backup-codes | P1 | api | 5 | PASS |
| identity-auth.two-factor-disable-lifecycle | P1 | api | 1 | PASS |
| integration-system.connector-degraded-recovery | P1 | api | 5 | PASS |
| platform-core.manifest-install-contract | P1 | api | 6 | PASS |
| platform-core.marketplace-install-local-lifecycle | P1 | api | 8 | PASS |
| platform-core.runtime-config-boot-read | P1 | api | 4 | PASS |
| platform-core.seed-mode-matrix | P1 | api | 5 | PASS |
| search.rls-both-personas | P1 | api | 8 | PASS |
Derived: 27 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN. No clause failed, so no reproduction rule is owed. No authentication or authorization defect was found. One authz-adjacent probe observation, judged by design by its runner, is listed below with its detail withheld pending maintainer.
Retests of the wave-1 extractions (#25818–#25822, all closed completed)
All five fixes hold at 756220c4. Each was judged with fresh live requests.
- aggregate query answers wrong data under 200: dateGranularity on a non-date field, a duplicate alias, and an alias equal to a groupBy field #25818 holds (PR fix(objectql,metadata-protocol): refuse aggregate queries that answered a wrong table, and an empty aggregate list #25935). On
POST /api/v1/data/showcase_account/query:dateGranularityon textname→ 400INVALID_FIELD("only a date or datetime field can be date-bucketed. The query was NOT run").- A duplicate alias
n→ 400INVALID_QUERY("aggregations.1.alias names the column 'n', which aggregations.0.alias already names"). - alias
statusbeside groupBystatus→ 400INVALID_QUERY("…which groupBy.0 already names"). - Also refused: a currency field with granularity, an alias equal to an object-form groupBy field, and a duplicate groupBy.
- Still allowed: a case-different alias (
N/n) and month oncreated_at.
- aggregate query: an empty
aggregations: []answers 404 OBJECT_NOT_FOUND "Object … is not registered" for a registered object #25819 holds (PR fix(objectql,metadata-protocol): refuse aggregate queries that answered a wrong table, and an empty aggregate list #25935).{"aggregations":[]}→ 400INVALID_QUERY"'query.aggregations' is an empty list … The query was NOT run", never "not registered".- groupBy
[]with aggregations[]→ 400. - groupBy
["status"]with aggregations[]→ 200 (3 groups). - Control: one count → 200
[{n:17}].
- automation: the resume and run-read doors ignore the flow name in the path, so POST /automation/OTHER_FLOW/runs/RUNID/resume completes another flow's paused run #25820 holds (PR fix(runtime): the automation run doors answer another flow's run as an unknown one #25944). It was retested twice, by R3 and independently by R1. The run was paused via
POST /api/v1/automation/trigger/showcase_reassign_wizard.- Resume via
showcase_project_closure, and via the unknown nameqa_nope_flow→ the identical 404RUN_NOT_FOUND. GET /api/v1/automation/showcase_notify_owner/runs/RUNID→ 404RUN_NOT_FOUND.- The run stays paused and the assignee is unchanged.
- Via the owning flow: GET → 200 paused, and resume → 200 completed.
- Resume via
- automation trigger door silently drops a non-object
params(string or array) and dispatches the flow with an empty params bag #25821 holds (PR fix(runtime)!: the flow trigger door refuses a params that is not a JSON object #25974, extended by fix(runtime)!: the action and flow trigger doors refuse a body or params that is not a JSON object, through the resume door's shape check #26034).- Both trigger routes of
showcase_notify_ownerwithparamsset to"oops",[1,2],null,42ortrue→ 400VALIDATION_FAILED, withdetails.fields[0] = {field:"params", code:"invalid_type"}. - The flow's run count was 2 before the ten calls and 2 after, so no run row was written.
- Beside it,
/actionswith params"oops"→ 400 (automation: the action door drops a non-object params, and the trigger door accepts a non-object body (siblings of #25821) #25976).
- Both trigger routes of
- spec: a translation item accepts any
localestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822 holds (PR feat(spec)!: a locale must be a BCP-47 tag — zh_CN, padded values and language names are refused #25983).PUT /api/v1/meta/translation/qa_bad_localewith localenot a locale!!→ 422INVALID_METADATA, issues[0] pathlocale, codeinvalid_format, "must be a BCP-47 language tag". Reproduced twice; a GET afterwards → 404.- Controls
zh-CNandzh-Hans-CNsave 200 withvalid:true, and were cleaned up with DELETE. - The probe line below has a low follow-up note on the message wording.
Per-clause evidence (one line per clause; text oracles)
Labels: AK = acceptance[K], NK = negative[K], zero-based.
access-security.record-share-grant-revoke (R1)
Personas were members A, B, C and D, minted by invite then sign-up, each with sys_member role member read back.
- A0 pass: B's GET of note N1 → 404
RECORD_NOT_FOUND, and B's list has 0 rows. - A1 pass: A's
POST …/N1/shares {user,B,read}→ 201. The row reads recipient_type user, recipient B, access read, source manual, granted_by A. - A2 pass: B by id → 200 and B's list has n=1. C → 404 and C's list has 0 rows.
- A3 pass: A's GET of the shares → 200 with the grant. B's self-grant, B granting C and B listing the shares → 403
PERMISSION_DENIED"requires record ownership or Modify All Data (ADR-0111 D1)". - A4 pass: A's DELETE of the share → 204. B → 404, B's list is empty, and the system re-read → 404.
- A5 pass (run before the real revoke): DELETE of the share id under N2 → 404 "share … not found on showcase_private_note/N2". The share survived, and B still read N1 (200).
- A6 pass:
POST /sharing/rules/share_key_account_qualified_contacts_with_managers/evaluate→ 200{matchedRecords:1, expandedUsers:1, grantsCreated:0…}. After a manager position was assigned to D, expandedUsers is 2. The rule rows on Nora West are admin + D, with source rule and source_id = the ruleId. D reads 200 and C reads 403. - N0 pass, N1 pass.
access-security.owd-save-gate (R1)
- A0 pass:
PUT /meta/object/qa_owd_probecarryingexternalSharingModel→ 422INVALID_METADATAon both the active door and?mode=draft. There is one issue, atexternalSharingModel, with the "removed in @objectstack/spec 18 … Delete the key." prescription. Nothing was stored (404 /NO_DRAFT). - A1 pass: without the key → 200 (active, seq 1), and the read-back shows sharingModel private with no externalSharingModel.
- A2 pass:
- Widening
showcase_announcement→ 403NOT_OVERRIDABLE, and with?package=→ 403ITEM_LOCKED. The tightened control is refused the same way, so the package door answers. - The R1 pins were run at 756220c:
object-posture-gate.test.ts10/10 (plugin-unit layer) andmeta-object-owd-gate.test.ts11/11 (rest protocol layer, in-process).
- Widening
- A3 pass: GET before and after are byte-identical (sha256 b550eb72…).
- A4 pass:
showcase_accountandshowcase_announcementcarry sharingModel, with no externalSharingModel. - N0 pass (server half; surface api).
- N1 pass.
access-security.packaged-flow-write-door-parity (R1)
- A0 pass:
?layers=trueshows a code layer with_packageId com.example.showcaseand lock full.PUT /meta/flow/showcase_urgent_task_alert→ 403NOT_OVERRIDABLE, and with?package=→ 403ITEM_LOCKED. The reads before and after are identical. - A1 pass:
PUT /automation/showcase_urgent_task_alert→ 403NOT_OVERRIDABLEwith the same lock text, and the read-back is byte-identical. - A2 pass:
DELETE /automation/…→ 403 ("sealed against removal"). The GET afterwards → 200, identical. - A3 pass:
_statusreads enabled and bound. One urgent task create took runs from 0 to 1 (completed, trigger.recordId = the task). - N0 pass: Mei → 403
PERMISSION_DENIED"requires themanage_metadatacapability", a different message from the package lock. - N1 pass.
ai.mcp-run-action-exposure-gate (R3)
The MCP Streamable HTTP endpoint is /api/v1/mcp, called with an admin osk_ key.
- A0 pass:
run_action showcase_mark_done, with and without objectName → isError "…is not exposed to AI — the app author must opt it in withai: { exposed: true, description: … }". The task's status and updated_at are unchanged. - A1 pass:
list_actions {}→ onlyshowcase_portfolio_snapshot(totalCount 1). - A2 pass: the result is
accounts:14, projects:5, invoices:12, equal to the REST totals. - A3 pass:
[action-audit] MCP run_action 'showcase_portfolio_snapshot' … executes TRUSTED … for user 'IMG…'. The line names the user. - A4 pass:
revoke_sessionwithout objectName → "is on a system object and is not exposed via MCP"; withsys_session→ the system-object refusal. The victim session's get-session → 200. - N0 pass, N1 pass.
api-backend.batch-transactional-discovery (R2)
- A0 pass:
/discoverycapabilities.transactionalBatch = {enabled:true, description}. An atomic/batchwith a note create and a task create → 200, and both re-read 1 / 1. - A1 pass:
atomic:false→ 400BATCH_NOT_ATOMIC, and 0 / 0 rows persisted. - A2 pass:
- (a) A validation failure mid-list → 400, and the note and task re-read 0 / 0.
- (b) A failure inside the transaction (an update of an unknown id) → 404
RECORD_NOT_FOUND. The earlier note rolled back (0).
- A3 pass: 201 creates → 400
BATCH_TOO_LARGE(201 > max 200) and 0 rows; 200 creates → 200 and 200 rows. - A4 pass:
/data/showcase_private_note/batchwith[valid, no-title, valid]:continueOnError→ succeeded 2, failed 1.- The default → row 0 ok, row 1
VALIDATION_FAILED, row 2NOT_ATTEMPTED, matching the re-reads.
- A5 pass:
#24781fixed. Update and delete with id""→ 400VALIDATION_FAILED"requires an id", identical to the no-id control, and no Unhandled error in the log. - N0 pass, N1 pass.
api-backend.error-envelope-ledger (R2)
- A0 pass: 7 sampled codes all pass
ErrorCode.safeParse. - A1 pass:
POST showcase_invoice {}→ 400 withfields[0] {field:name, code:required, message, label}. - A2 pass: no
fieldErrorsin any body. - A3 pass: the statuses match
HttpStatusErrorCodeMapand the families. - A4 pass: an unknown object → 404
OBJECT_NOT_FOUND, and an unknown record → 404RECORD_NOT_FOUND. The two are distinct. - A5 pass: all 7 families sampled: validation 400, auth 401, authz 403 (Mei), object 404, record 404, batch 400, query 400
UNSUPPORTED_QUERY_PARAM. - N0 pass.
api-backend.filter-comparand-conformance (R2)
- A0 pass: 6 comparand members. The string / number / boolean / null / ISO-date cases through POST
whereand GET?filter=→ 200 with ids identical to the local baseline. Date and bigint were scored in process, through specparseFilterAST+ the InMemoryDriver dist. - A1 pass: object comparands → 400 (POST
VALIDATION_FAILEDatquery.where.title.$eq; GETINVALID_FILTER), naming the path, "NOT applied" and no rows. An array in a scalar slot → a located 400 prescribing$in/$between. - A2 pass (engine + driver layers):
engine-comparand-type-door.test.ts12/12. The driver suites: memory 23/23, sql 46 passed + 2 skipped, sqlite-wasm 22/22, turso 22/22, mongodb 22/22. An out-of-range bigint givesINVALID_FILTER"magnitude exceeds 2^53". - A3 pass:
- A relation head
project.name, a scalar headtitle.fooand a virtual headbudget_remaining.x→ 400INVALID_FIELDon both doors. - The nesting prescription works (200, 5 rows).
- A JSON head → 400
INVALID_FILTERfrom the driver; it is not dropped.
- A relation head
- A4 pass: the same sentence at POST, GET and the engine.
- N0–N3 pass.
api-backend.aggregate-contract-matrix (R2)
Planted rows: a null revenue, plus dates 2024-11-24, 2024-12-30 and 2027-01-03. The baseline is 17 rows.
- A0 pass: 0 mismatches across the 6 functions, grouped and ungrouped.
count(field)16 vs fieldless 17. On a boolean, min/max are the numbers 0 / 1. - A1 pass (clock 01:50Z UTC): day 15, week 15, month 12, quarter 9, year 5 buckets, all equal to the ISO computation. The null dates form one null bucket. Weeks: 2024-11-24 → 2024-W47, 2024-12-30 → 2025-W01, 2027-01-03 → 2026-W53. Quarter keys read
YYYY-Qn. - A2 pass: on an empty where, count 0, sum 0, avg/min/max null, count_distinct 0; grouped,
[]. A per-measure filter empties only its own measure. - A3 pass:
array_agg/string_agg→ 400 with the retirement text andos migrate meta --from 16.median→ 400 naming the six functions. An unknown field → 400INVALID_FIELD. A non-array → 400. - A4 pass: the contributor-bound member sees 0 seeded invoices and INV-1003 → 404. Their grouped count is
{draft:1}; the admin's is{draft:3, paid:5, sent:5}. - A5 pass: 11 variant rows, all 5 granularities discriminating.
- N0 pass, N1 pass.
- N2 pass:
/analytics/queryday buckets give the same label/count sets under UTC, America/Los_Angeles and Pacific/Auckland.
api-backend.api-methods-verb-gate (R2)
- A0 pass:
sys_oauth_consent→ 404OBJECT_API_DISABLED.PATCH sys_session/ID→ 405OBJECT_API_METHOD_NOT_ALLOWED, allowed[get, list, aggregate, export].sys_device_codelist → 405, allowed[get].
- A1 pass: the scratch
qa_verb_gate_probewith[get, list, create]→ PATCH / DELETE 405, allowed equal to speceffectiveOperationsArray(resolveEffectiveApiMethods(…))element for element. - A2 pass: undefined → 200;
[]and['upsert']→ deny-all 405 with allowed[]; non-array → pinapi-derivation.test.tspassed, and live gives 422. - A3 pass:
- Without
bulk: createMany and the per-object batch → 405 naming bulk. - With
bulk: createMany → 201. updateMany / deleteMany → 405. - Import: insert → 200; update / upsert → 405.
- Without
- A4 pass:
['upsert']is stored with the retired-value WARN and the deny-all WARN.['get','history']logs the trackHistory guidance. - A5 pass: Mei and the contributor get the same 405 as the admin, with no 403. Anonymous → 401.
- A6 pass:
/batch[task create, sys_session update]→ 405 in both orders, with nothing persisted. - N0, N1, N3 pass.
- N2 n/a: nothing internal was filed.
approvals.decision-only-via-service (R1)
- A0 pass: a generic resume of a paused
showcase_invoice_signoffrun, with{}and with{branchLabel:"approve"}→ 403PERMISSION_DENIED"paused at a 'approval' node, which only its owning service may resume". - A1 pass: the run is still paused, the request still pending, and
/actionsunchanged (1 row). - A2 pass:
POST /approvals/requests/ID/approve→ approved (slate [admin, admin], distinct slots). A second approve → 409INVALID_STATE. The run completed. - A3 pass: the reassign wizard paused through
/actions; the generic resume with{inputs:{new_assignee}}→ 200 completed, and the assignee changed. - N0 pass.
approvals.approver-resolution-matrix (R1)
Scratch flows were registered with POST /api/v1/automation, fired by field_zoo creates, and deregistered after each leg.
- A0 pass: team → [B, Mei]; field
f_users→ [Ada, Mei]; manager → [Ada]; user → [Mei]; org_membership_level owner → [admin]. - A1 pass: department
bu_field_ops→ [A, Ada, Mei], deduplicated. Withbu_west_coastdeactivated → [A, Ada]. An unknown or inactive root gives the "expanded to nobody" WARN and goes to admin_rescue. - A2 pass:
pending_approversholds 2 ids, and__resolvedFromholds the 2-element field array. - A3 pass: role gives the ADR-0090 D3 deprecation WARN, and its slate equals the owner slate.
- A4 pass:
- admin_rescue / absent → [admin], abandoned
['position:qa_vacant_r1'], an actorless comment and the WARN. - fail →
NO_APPROVERSwith no row. - auto_approve → no row; the marker step is written.
- a fallback to user B → [B], which discriminates.
- a fallback to nobody → admin_rescue.
- admin_rescue / absent → [admin], abandoned
- A5 pass: queue gives the not-implemented WARN, [admin], abandoned
['queue:q_r1'], and the request stays pending. - N0–N4 pass.
attachments-storage.field-accept-maxsize-server-enforced (R4)
The admin signed in through the form, so the session cookie was held. The recipe object was renamed media_qa_media (see the accuracy findings).
- A0 pass: poster ← a committed PDF → 400; 0 rows, and the sys_file ref_id stays null.
- A1 pass: a 2 MiB real PNG → 400
ERR_FILE_CONSTRAINT"(2097152 bytes > 1048576 bytes)"; 0 rows, and ref_id stays null. - A2 pass: both answers are 400
ERR_FILE_CONSTRAINT, naming the field and the accept list or the byte counts. 0 Unhandled error lines. - A3 pass: PATCH to the PDF or the oversize file → 400, and the original poster is unchanged.
- A4 pass: a compliant PNG → 201, and its sys_file is stamped with ref_object, ref_field and ref_id.
- A5 pass: the PDF is refused; after a system write NULLs its mime_type and size, the same POST → 201.
- A6 pass:
README(no extension) → 201,report.txt→ 400,good.pdf→ 201. - N0 pass, N1 pass.
- N2: a documented boundary, recorded. PDF bytes declared as image/png are accepted.
- N3 pass in substance, against stale text: an unknown id is now refused 400
VALIDATION_FAILED(files: a record write that names a file its writer may not read keeps a dangling id (the quiet answer of #25951); refuse the field instead, so no slot holds a file it does not own #26043), which is stronger than "passes untouched" (accuracy finding).
automation.trigger-status-contract (R3)
- A0 pass: the
qa-flow-status-doorsrecipe provisioned live. Both trigger routes answer:- unknown → 404
RESOURCE_NOT_FOUND - always-fails → 400
FLOW_FAILED - disabled → 409
FLOW_DISABLED - no start node → 422
FLOW_NO_START_NODE - valid → 200
No refusal comes back as a 200 wrappingsuccess:false.
- unknown → 404
- A1 pass: the wizard → 200
{status:'paused', runId, screen.nodeId:'collect'}, later resumed to completed. - A2 pass:
- With the wizard disabled,
/actions/showcase_task/showcase_bulk_reassign→ 409. - With
showcase_inquiry_purgedisabled,POST /api/v1/apps/showcase/inquiries/purge→ 409. - Type:'flow' actions authored under the QA package → 400
FLOW_FAILEDand 422FLOW_NO_START_NODE. - All three doors import
classifyFlowRefusal.
- With the wizard disabled,
- A3 pass:
details.errorMessageanddetails.summaryat the trigger door and at/actions. - A4 partial (6 arms pass; each retryable arm was re-read as still paused):
{inputs:{}}and bodyless → 400INVALID_SCREEN_INPUT- output
$x→ 400INVALID_SIGNAL - inputs + variables → 400
VALIDATION_FAILED - member → 403
- a concurrent resume during a 6 s http node → 409
RESUME_IN_PROGRESS - unknown or consumed run → 404
RUN_NOT_FOUND - The 503
STORE_UNAVAILABLEarm is blocked(fixture): no stock trigger reaches it.
- A5 pass: a flow deregistered under a pause → 404 "Flow 'qa_stale_dereg' not found for run". A node edited away → 404 "Suspended node 'collect' no longer exists".
- A6 pass: live
@objectstack/client:trigger()/execute()reject 404 / 400 / 409 / 422, andresume()rejects 400 / 400 / 403 / 404. The success, paused and valid-resume calls resolve. - N0–N5 pass.
automation.packaged-flow-subflow-disable-refusal (R3)
- A0 pass: toggling
showcase_notify_owneroff → 409DELETE_RESTRICTED, namingshowcase_task_done_notify_owner, ADR-0126 §7.3 and the remedy.showcase_closure_signoffgives a 409 namingshowcase_project_closure. - A1 pass: map child
showcase_one_task_signoff→ 409 namingshowcase_release_signoff. - A2 pass: no
sys_metadata_activationrows after the refusals. A caller run completed with itscall_notifysubflow step succeeding. - A3 pass: enabling the child → 200, with the caller off and with it armed.
- A4 pass: caller off → 200, then the child → 200, and the ledger reads both false. The teardown re-enabled both.
- N0 pass.
- N1 blocked(fixture): a second packaged caller needs a package-loader-installed package. A tenant caller authored via
POST /api/v1/automationdoes not guard, which confirms knownGaps[0].
automation.packaged-flow-clone-contract (R3)
- A0 pass:
{}/{label}→ 400VALIDATION_FAILED{field:name, code:required};{name}→ 400 for label. Nothing was registered (404). - A1 pass: the diff is exactly name, label and
status:'draft', plus the absence of_packageId,_packageVersionand_provenance. None of the 10 dropped keys is present (counted from spec/kernel), and there is no ancestry key. - A2 pass: one urgent create took base runs 0→1 and clone runs 0→1, for the same recordId. The notice equals
FLOW_CLONE_NOTICE. - A3 pass:
- same name → 409
RESOURCE_CONFLICT, suggesting…_copy - unknown → 404
Bad-Name!→ 400 with the pattern- an unknown key → 400
- same name → 409
- A4 pass: the subflow node keeps
config.flowName. - A5 pass: a PUT on the clone → 200, and the edit is read back.
- A6 pass:
- After a cold restart over the same DB, GET → 200 (edited, draft) and
_packageId null; a new create took clone runs 1→2. - Studio
/_console/studio/~org/automations(cookie + token) lists both clones under "Organization flows" and opens the designer. The console was served as noted in the fingerprint.
- After a cold restart over the same DB, GET → 200 (edited, draft) and
- N0–N2 pass.
- N3 pass: the member → 403 and anonymous → 401, with no clone created.
cli.build-own-contract (R5)
Fixtures are scratch copies of the blank scaffold.
- A0 pass:
os buildexits 0 with "✓ Build complete", the stats andArtifact: dist/objectstack.json (2.6 KB).--jsongives success, output, size, stats, warnings[]and conversions[]. - A1 pass: a field with no type → human exit 2, located "objects.1.fields.body.type: Invalid field type ''. Did you mean 'url'?";
--jsonexit 1STACK_SCHEMA_INVALID. No dist. - A2 pass: two approval-expression errors plus one warning → exit 1 on both paths. Both errors are reported in one run with rule, where, path and fix. The warning goes to
warnings[]. No artifact. - A3 pass: build
--jsonand validate--jsonwarnings and conversions are equal on 5 fixtures. With the live-conversion probe hook, both carry the same non-emptyconversions[0](byte-equal). - A4 pass: 3 repeats each of build and compile. clean 0/0, undecl 0/0, schema 2/1, rules 1/1, load-throw 2/1. Stable, and only 0/1/2. Build and compile JSON are byte-identical minus duration.
- A5 pass: an undeclared nested key under
strict:falseprints the "Undeclared authoring keys (1) — dropped at load" block, exit 0, and the artifact has the key stripped. JSON carries it in warnings. - N0 pass, N1 pass.
cli.hook-body-extraction-gates (R5)
- A0 pass:
FORBIDDEN_PATTERNSholds 13 entries at 756220c; the item says 14 (accuracy finding).- One hook per entry:
--strict-bodyexits 1 "13 callable(s) lack a metadata body", each bullet quoting its own reason. --json:issues[13]with origin, reason, kind and freeIdentifiers.- The controls lower:
ctx.provenance?.flowRunId,Object.create(null)andctx.api.transaction(fn).
- One hook per entry:
- A1 pass: find →
["api.read"], update →["api.write"], and the alias →["api.read"]. - A2 pass: randomUUID →
["crypto.uuid"]andctx.log.info→["log"]. hash-only →[]. - A3 pass: an authored body ships exactly
["api.write","log"]. A// @capabilitiescomment is ignored (["api.read"], a byte-identical body). - A4 pass: free identifiers → the default build exits 0 with a warning and a bundled handler.
--strict-bodyexits 1 naminglocalHelper, qaHelper. The implicit-return arrow lowers toreturn (…);. - A5 pass: once every callable is body-only, "Skipping legacy runtime bundle" prints and the old
.mjsis cleaned. - A6 pass: the 13-hook default build exits 0 with the warning block (one line per hook), and JSON carries 13
bodyExtractionWarningsseparate fromwarnings. A 23-hook probe shows 20 lines and "… and 3 more". - N0–N3 pass.
cli.fresh-install-migrate-apply-first (R5)
- A0 pass: on an empty DB (no db, -wal or -shm),
os migrate apply --yesexits 0 with "Created/extended 107 table(s)" and the birth marker (shape generation 1, platform 18.1.0). Before any boot there is exactly 1 marker row: blocking 0, advisory 0,details.birth.shapeGeneration 1. After the boot it is byte-identical. - A1 pass: production
os serve→ "✓ Server is ready", not DEGRADED, and health 200 about 8 s in. All three refusal greps count 0. - N0 pass.
identity-auth.two-factor-enrollment-reveal (R4)
- A0 pass: the armed boot's
features.twoFactor:trueandenable {}→ 400. The unarmed control'stwoFactor:falseand enable → 404. - A1 pass: enable → 200 with
method, totpURI, backupCodes. The URI has labelObjectStack:admin@objectos.ai,issuer=ObjectStack, digits 6 and period 30. The secret is 52 base32 chars, which is 32 bytes. - A2 pass: 10 codes
xxxxx-xxxxx. - A3 pass: the
sys_two_factor.enable_two_factorresultDialog paths resolve against the live response. - A4 pass (recorded):
get-totp-urireturns 400 without a body, 400INVALID_PASSWORDfor a wrong password and 401 for anonymous. The correct password returns the same secret, before and after verify, so it is password-gated and re-reveals. The item's owed ruling stands. - A5 pass:
sys_user.enable_two_factordeclares the qrcode and code-list fields and the acknowledge text, with no successMessage. - N0–N2 pass: a refused enable writes 0 rows.
identity-auth.two-factor-verify-to-activate (R4)
- A0 pass: the same guard as above.
- A1 pass: a sign-in between enable and verify → 200 with no twoFactorRedirect and only the session cookie.
two_factor_enabledstays 0. - A2 pass: the row is
verified=0right after enable (DB read). - A3 pass: session-lane
verify-totp→ 200;verified=1,two_factor_enabled=1. - A4 pass: a fresh sign-in →
{twoFactorRedirect:true, twoFactorMethods:['totp']}with onlybetter-auth.two_factor. get-session and data reads give 401 mid-challenge. A cookie-lane verify → 200, and then 200 on both. - N0–N2 pass: wrong codes → 401
INVALID_CODE, and the row stays unverified.
identity-auth.two-factor-backup-codes (R4)
- A0 pass: the enrollment is verified and the challenge fires.
- A1 pass: a backup code → 200, then get-session → 200.
- A2 pass: the same code at a fresh challenge → 401
INVALID_BACKUP_CODE(twice); another code → 200. - A3 pass: regenerate → 10 new codes with no overlap. Old codes → 401, and a new one → 200.
- A4 pass (recorded):
backup_codesat rest is a 306-char hex string with no plaintext and no JSON (DB file read; REST never returns it). - A5 pass:
sys_user.generate_backup_codesdeclares a code-list, the acknowledge text andrefreshAfter:false. - N0–N3 pass: a wrong-password regenerate → 400, anonymous → 401, and the refused regenerate is a no-op.
identity-auth.two-factor-disable-lifecycle (R4)
- A0 pass: 1 verified row, and the challenge fires.
- A1 pass: a wrong-password disable → 400
INVALID_PASSWORD(twice), no password → 400, anonymous → 401, and the state is intact. - A2 pass: disable → 200, leaving 0 rows and the flag at 0.
- A3 pass: the next sign-in → 200 with no twoFactorRedirect.
- A4 pass:
- A retained code with no cookie → 401
INVALID_TWO_FACTOR_COOKIE; on a live session → 400BACKUP_CODES_NOT_ENABLED. - Re-enable gives a new secret with no overlap.
- An old-secret TOTP → 401
INVALID_CODEon both lanes, and old codes → 401. The new secret → 200.
- A retained code with no cookie → 401
- A5 pass (recorded), with
mfa_requiredon:- Disable → 200, and
mfa_required_atis stamped lazily. - With grace 0, data → 403
MFA_REQUIREDanduser.authGateis set. Enable and verify are reachable, and data → 200 afterwards. - The stamp is not cleared on re-enrolment. Settings were restored.
- Disable → 200, and
- N0–N3 pass.
integration-system.connector-degraded-recovery (R6)
- A0 pass: in a scratch copy with the MCP fixture script moved away, health is 200 in 14 s. The log has WARN
Connector registered DEGRADED: showcase_mcp_tools … Connection closed, and the descriptor readsstate:'degraded', the reason andactions:[]. - A1 pass: the
carrier_pigeontwin compiles, then fails at boot ("transport.kind must be 'stdio' or 'http'", rollback complete). Nothing listens. - A2 pass: echo trigger → 400
FLOW_FAILEDin 18 ms, quoting the degraded reason. The unknown-connector comparator was read from source (accuracy finding). - A3 pass: retries at +5, +10, +20 and +40 s, with the next at 80000 ms. A view publish caused attempt 6 with the next at 160000 ms, so it did not reset the backoff.
- A4 pass: the file was restored with the same pid.
recovered connector instance 'showcase_mcp_tools' via provider 'mcp' (1 action(s)), the state reads ready, and echo → 200{upper:'OBJECTSTACK'}(uptime 281 s, so no restart). - N0 pass, N1 pass.
platform-core.manifest-install-contract (R6)
- A0 pass:
engines.protocol '^17'→ 422OS_PROTOCOL_INCOMPATIBLE; GET → 404, and there is no list entry and no sys_packages row. The^18baseline → 201. - A1 pass:
details {requiredRange '^17', rangeSource 'engines.protocol', protocolVersion '18.0.0', targetMajor 17, migrateCommand 'objectstack migrate meta --from 17'}. - A2 pass: protocol takes precedence over platform both ways. Legacy
engine.objectstack '^17.0.0'→ 422 (rangeSourceengine.objectstack), and^18.0.0→ 201. - A3 pass: no range → 201 with the "declares no engines.protocol range" log.
workspace:*→ 201 with the "unrecognized … range" log. - A4 pass:
- A namespace conflict → 422
NAMESPACE_CONFLICT, naming all three identities. - An
?overwrite=truereinstall → 201; a plain re-POST → 409. base/system/sys→ 201, and a second package onbase→ 201.
- A namespace conflict → 422
- A5 pass: the derived namespace
leaveand the declaredtimeoffagree across POST, GET and sys_packages. - N0–N4 pass.
platform-core.marketplace-install-local-lifecycle (R6)
Every boot ran from a scratch copy (A: default; B: OS_CLOUD_URL=off).
- A0 pass:
- A: install-local 200;
/marketplace/packages502MARKETPLACE_PROXY_FAILED(mounted, upstream unreachable); cloud-connection 200. - B: install-local 200; marketplace 404; cloud-connection 404. The catalog POST → 503
MARKETPLACE_UNAVAILABLE, with no dial-out.
- A: install-local 200;
- A1 pass:
featuresA{installLocal:true, marketplace:true}and B{installLocal:true, marketplace:false}match the route probes. - A2 pass:
os package install …/app-crm/dist/objectstack.json -r :42602→ "Package installed into the running kernel, com.example.crm 4.0.0". The manifest is cached in the scratch copy's.objectstack/installed-packages/. crm_app is listed, and crm_account has 3 rows. - A3 pass: admin sees installedBy and storageDir; a member gets 200 without them (also after a restart); anonymous → 401.
- A4 pass:
- purge → deleted 28, skipped 0, and the user row survives. A second purge → deleted 0, skipped 28.
- The ledger reads
sampleDataPurged:true, and the rows hold across a restart. - Reseed → inserted 28.
- A5 pass: on the 4 doors, the member (identity proven by get-session) → 403 naming
manage_metadata, anonymous → 401, and admin succeeds. - A6 pass: DELETE → 200, cleanups
[security.package-permissions: success]. Then crm_account → 404, the app is unlisted and the ledger dir is empty, also after a restart. - N0–N4 pass.
- The tree's
installed-packages/never existed.
platform-core.runtime-config-boot-read (R6)
- A0 pass: an anonymous GET of
/api/v1/runtime/config→ 200 JSON, no set-cookie. It carries cloudUrl, singleEnvironment, features, branding and telemetry. - A1 pass:
/api/v1/studio/runtime-configis byte-identical, on both boots. - A2 pass: anonymous 200 on the cloud arm and on
OS_CLOUD_URL=off. - A3 pass: no secret, token or DSN material. The admin, member and garbage-bearer reads are byte-identical to the anonymous read.
- N0–N2 pass.
platform-core.seed-mode-matrix (R6)
The fixture package com.objectstack.qa.seedmodes uses seeds applied by publish-drafts.
- A0 pass: upsert: publish 1 inserts 2; publish 2 inserts 1 and updates 1. The hand-edited A is untouched (same id and updated_at), B is updated in place, and C is inserted.
- A1 pass: update → updated 1, inserted 0, and there is no D.
- A2 pass: ignore → inserted 1, and A is untouched.
- A3 pass: insert → a second A row (the duplication shape).
- A4 pass (recorded): replace inserts and the hand row F survives. This matches source (
seed-loader.ts: "caller should have cleared the table";decideWriteActionroutes replace to insert). - A5 pass:
SeedModehas 5 members;defaultModeisSeedMode.default('upsert');packages/runtime/src/seed-loader.tsre-exports@objectstack/metadata-protocol. - N0 pass: replay 1 updates 1 (re-converges a duplicate), and replays 2 and 3 → 0/0 with updated_at untouched.
- N1 pass.
- knownGaps[2] was reproduced (replay binds the newer duplicate). It is not scored.
search.rls-both-personas (R4)
- A0 pass: the recipe was replayed as written (uninvited sign-up → 403
SELF_REGISTRATION_CLOSED; invite → sign-up → contributor position). The member's list has 0 rows and INV-1003 → 404. Member search → [] with total 0; admin → [INV-1003] with total 1. - A1 pass: paged
{search:'INV', limit:2}:- skip 0 → 2 rows, total 3
- skip 2 → 1 row, total 3
- skip 4 / 6 / 100 → [], total 3
The admin's total of 15 never leaks.
- A2 pass: the member's own 3 rows are found (total 3).
- A3 pass: the deny, allow and own-row evidence is complete.
- N0 pass.
Derived item verdicts and gaps
- PARTIAL:
automation.trigger-status-contract(A4's 503 arm, blocked(fixture): no stock trigger) ·automation.packaged-flow-subflow-disable-refusal(N1, blocked(fixture): a second packaged caller needs a package-loader-installed package). Both gaps are the same as wave 1, and both are parked on [sweep] v18.2.0 release test sweep #25807. - Every other item is PASS.
Off-clause probe line (rule 8)
About 32 runner-minutes (R1 6, R2 5, R3 6, R4 6, R5 4, R6 5).
Personas: admin, Mei, Ada, a demo auditor, a contributor-bound member, plain members (A–D, minted), an MFA-gated admin and anonymous.
Doors:
- data CRUD,
/batch, per-object batch, query / aggregate / filter - the verb gate, record shares, the sharing-rule evaluate
- meta object / flow / translation / action PUT and DELETE
/automationPOST / PUT / DELETE / toggle / trigger / resume / run-read / clone/actions, approvals approve / reject- MCP
list_actions/run_action - the two-factor family, settings
- storage presign / complete and the file-field write
- data search and
/api/v1/search os build/compile/migrateboundaries/api/v1/packages, install-local, runtime-config, seed PUT, connectors
Boundary classes: empty, whitespace, missing and unknown ids and names; non-object and non-array bodies; malformed JSON; colliding aliases and groupBy entries; a wrong flow name in the path; path/body name mismatch; case variants; TOTP ±1/±2 steps; exact-limit and limit+1 sizes; MIME parameters; upper-case extensions; path-traversal ids; opposite posture (cloud on / off, armed / unarmed); readers outside scope; deciding after the flow is gone.
Extracted (see the close-out comment):
- A deleted flow with a paused approval run: the approve answers 500
RESUME_FAILEDafter recording the decision, and the run is stranded. Besideapprovals.decision-only-via-service. - The runtime bundle's content hash changes on every identical rebuild, because the temp entry path (pid +
Date.now()) is embedded in the code and the inline sourcemap. This is the repro [finding] UNCONFIRMED: runtime bundle filename hash differs across byte-identical rebuilds — needs a deterministic-rebuild repro #7651 lacked. Besidecli.hook-body-extraction-gates. - A file field accepts a presigned upload that was never completed (
sys_filestatus pending), and claims it. Besideattachments-storage.field-accept-maxsize-server-enforced. OS_PROTOCOL_INCOMPATIBLEfor a package that declares a newer major (^19on an 18 runtime) prescribesobjectstack migrate meta --from 19, which does not apply. Besideplatform-core.manifest-install-contract.
Low, not extracted:
- The error-envelope dialect differs between doors:
/batchschema refusals carry zodissues[], the data doors carryfields[], and malformed JSON gives a nestedINVALID_REQUEST. - The spec: a translation item accepts any
localestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822 refusal message says a language name is refused, but localeenglishsaves 200, because it is syntactically a valid language subtag. The message overclaims. POST /api/v1/batch {"operations":[]}→ 200results:[].- A record share to a nonexistent user id → 201 with no recipient check. A re-POST without accessLevel downgrades edit to read (upsert default).
- An all-body-only build reports
handlersBundled 8withruntimeModule null. - On
OS_CLOUD_URL=off, an empty install-local body → 503 "OS_CLOUD_URL not configured" instead of a 400. - One
manage_metadatagate answersFORBIDDENon install-local andPERMISSION_DENIEDon/api/v1/packages. - A declared mimeType with parameters (
image/png; charset=binary) is refused by an accept list namingimage/png. This errs strict.
Authz-adjacent observation, detail withheld pending maintainer. Seen beside ai.mcp-run-action-exposure-gate. Its runner judged it consistent with the action's documented trusted-execution contract, so it is not filed as a defect.
Nothing else was auth-class.
Checklist-accuracy findings and fixture gaps (wave anchor #25807, not extracted)
attachments-storage.field-accept-maxsize-server-enforced
- The
qa-media-constraintsrecipe step 2 now answers 422NAMESPACE_PREFIX: the object must bemedia_qa_media. - steps1 "the bytes need not be that large" is stale:
upload/completeanswers 409 on a size mismatch (Storage upload/complete commits a file record for bytes the backend never stored; the download then fails 500 #24548). - negative[3] is stale: an unknown id is refused 400 (files: a record write that names a file its writer may not read keeps a dangling id (the quiet answer of #25951); refuse the field instead, so no slot holds a file it does not own #26043).
- steps[5] "PUT/PATCH": PUT on a record answers 405.
two-factor items
- The system-context read was done as a read-only DB-file read, which is equivalent; the items could name it.
- The A5 gate is at
user.authGate. - The settings door is
/api/settings/:namespace;/api/v1/settings/authanswers 404.
api-backend.batch-transactional-discovery
- knownGaps[0] and A5's "Red until rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781 lands" are stale: rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781 is fixed.
api-backend.error-envelope-ledger
- A3 calls
OBJECT_NOT_FOUND/RECORD_NOT_FOUNDextension codes, but both areStandardErrorCodemembers.
api-backend.filter-comparand-conformance
- A0's "one request per member" cannot be met for Date and bigint, which have no JSON form. Say they are scored in process.
- The JSON-head null class is refused by the driver (
INVALID_FILTER); the item does not mention it.
api-backend.aggregate-contract-matrix
- count_distinct over
annual_revenuedoes not discriminate (all values are distinct). Usestatus.
api-backend.api-methods-verb-gate
- The closure includes search only when
searchableis not false. - The createMany body is a bare array.
api-backend.enforce-or-remove-authoring-gates
- The
os migrate meta --from 16$regexadvisory prints for a stack with no$regex, so it is not source-sensitive. - The REST 400 message is capped at 500 characters, which cuts the
$containsalternative the clause names.
access-security.record-share-grant-revoke
- fixtures.requires[3] / steps[10]: the position assignment creates the rule grant reactively (26 ms). The next evaluate answers
grantsCreated 0, so the grant is evidenced by the row andexpandedUsers, never the counter. sys_record_share refuses a data-API DELETE (405).
approvals.approver-resolution-matrix
- The marker step needs a real field (
f_textarea; there is nof_text). - Administrators are identified by sys_member role.
- The lint advisory
approval-approver-type-unsupportedstill says queue "stalls" (packages/lint/src/validate-approval-approvers.ts), which is stale since approvals: an empty slate underonEmptyApprovers: 'admin_rescue'opens on the tenant's administrators as named approvers, not as a nameless rescue request (retires the #22725unstaffedarm) #22824.
approvals.decision-only-via-service
- steps[5] is driven via
POST /api/v1/actions/showcase_task/showcase_bulk_reassign/RECORD, with the resume body{inputs:{new_assignee}}.
automation.trigger-status-contract
classifyFlowRefusalhas a fifth row, 422FLOW_INPUT_SCHEMA_INVALID. A0, the variants and N2 do not list it. Not driven.
automation.packaged-flow-clone-contract
- The surface is api, but A6 / steps[7] need a console and a browser. On a
pnpm buildtree,/_console/answers 503 untilpnpm console:build.
automation.packaged-flow-subflow-disable-refusal
- knownGaps[0]'s "the tenant arm is drivable and does not guard" holds live.
ai.mcp-run-action-exposure-gate
- A1's "permitted" filter is not discriminated: no stock action pairs an exposed action with a permission gate a persona lacks.
cli.hook-body-extraction-gates
- The title, steps[0], A0 and A6 say 14
FORBIDDEN_PATTERNSincludingctx.provenance. At 756220c there are 13: hook sandbox: a metadata-only hook body cannot read ctx.provenance (flowRunId), so a guard that tells a flow write from a user's own write must ship bundled (hotcrm#2285) #25905 lets provenance lower. - steps[2]'s jq on
isExpression: the artifact has no such key, so namesourceas the oracle.
cli.build-own-contract
- Stock fixtures yield
conversions [], so A3's parity is vacuous withoutpackages/cli/test/fixtures/live-conversion-probe.mjs; name it. - A plain-literal object must carry the namespace prefix, or the namespace refusal masks steps[2] / [5].
platform-core.manifest-install-contract
- The A1 runtime field is
protocolVersion(18.0.0), not the platform version. - Derived namespaces of long ids are truncated to 20 chars.
platform-core.marketplace-install-local-lifecycle
- The A5 refusal code is
FORBIDDEN(vsPERMISSION_DENIEDon/packages). - The stock admin create-user answers
membershipCreated:false, yet the member holds rolemember.
platform-core.seed-mode-matrix
- fixtures.requires: a body
namethat mismatches the URL answers 400VALIDATION_ERRORbefore the 422unrecognized_keys.
integration-system.connector-degraded-recovery
- The unknown-connector comparator for A2 has no live recipe.
- Reading the retry attempts needs
--log-level info.
platform-core.runtime-config-boot-read
- The showcase passes
controlPlaneUrl '', socloudUrlis empty by design on both arms.
RUNNER.md environment facts
- A per-boot
-dDB does not isolate the showcase's local upload root or its external datasource: tree boots writeexamples/app-showcase/.objectstack/data/uploads/andshowcase_external.db(git-ignored, so invisible togit status). This is the same class as the install-local entry. - A repo PreToolUse hook (
guard-process-kill.sh) refuses a Bash command that contains bothgrepandkill. The process-group stop must stand in a command of its own.
Fixture gaps (already recorded on their items):
automation.trigger-status-contract: the 503 arm has no stock trigger.automation.packaged-flow-subflow-disable-refusal: N1 needs a package-loader-installed second caller.
Supersede
This card supersedes the api/cli/build half of #25817. Of its 29 items, 13 were judged on #25817 and were all re-judged here: cli.fresh-install-migrate-apply-first, access-security.packaged-flow-write-door-parity, ai.mcp-run-action-exposure-gate, api-backend.aggregate-contract-matrix, approvals.approver-resolution-matrix, automation.trigger-status-contract, automation.packaged-flow-subflow-disable-refusal, automation.packaged-flow-clone-contract, identity-auth.two-factor-enrollment-reveal, identity-auth.two-factor-backup-codes, identity-auth.two-factor-disable-lifecycle, integration-system.connector-degraded-recovery, search.rls-both-personas.
#25817 also carried platform-core.marketplace-install-local-lifecycle forward from #25479, and it is re-judged here too. This half therefore carries nothing forward. The remaining #25817 verdicts belong to sibling card W2-C3b, and #25817 is closed by whichever of the two records lands second.
Handover
None. Every item has a verdict.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Close-out for this record (card W2-C3a,
release-18.2.0-w2-manual-api, 29/29 at756220c4: 27 PASS / 2 PARTIAL / 0 FAIL).Extracted: #26477 · approvals.decision-only-via-service · observed beside the clauses
Extracted: #26478 · cli.hook-body-extraction-gates · observed beside the clauses
Extracted: #26479 · attachments-storage.field-accept-maxsize-server-enforced · observed beside the clauses
Extracted: #26480 · platform-core.manifest-install-contract · observed beside the clauses
Parked-on: #25807 · automation.trigger-status-contract · A4 503 STORE_UNAVAILABLE arm, no stock trigger (fixture)
Parked-on: #25807 · automation.packaged-flow-subflow-disable-refusal · N1 needs a package-loader-installed second packaged caller (fixture)
Parked-on: #25807 · checklist-accuracy findings for the 29 items (listed in this record's body)
Retests of the wave-1 extractions: #25818, #25819, #25820, #25821 and #25822 all hold at
756220c4.An authz-adjacent probe observation was made beside
ai.mcp-run-action-exposure-gate. Its detail is withheld pending maintainer. The runner judged it consistent with the documented contract, so it is not extracted.Supersede: this record replaces the api/cli/build half of #25817. Every item in this half was re-judged here, so nothing is carried forward. #25817 is closed with
Superseded-by:naming both wave-2 records by whichever of W2-C3a / W2-C3b lands second.Generated by Claude Code