Skip to content

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

Open

Description

@objectstack-fleet

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, then pnpm build --concurrency=2, 119/119 tasks, 0 cached, 11m54s. node scripts/check-dev-prereqs.mjs is green. The tree is unreleased, so health reports 18.1.0; the protocol handshake reports protocolVersion 18.0.0.
  • Console: not separately built (pnpm console:build was not run). Only one clause needs it: automation.packaged-flow-clone-contract A6, the Studio reachability half.
    • A tree boot's /_console/ answered 503 "Console is not built … run pnpm console:build", because pnpm build leaves packages/console/dist absent.
    • So that leg ran on a scratch app copy whose @objectstack/console resolves to ui/apps/console/dist, the in-repo console that pnpm build produced from this same commit at 01:41Z.
    • .objectui-sha is retired, and the console is the ui/ tree of this commit.
  • App and boot: examples/app-showcase, booted with OS_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, dist or .objectstack; node_modules symlinked to the tree's copy) were used for:
      • every os build / os compile / os migrate leg, built from the blank scaffold;
      • the production os serve fresh-install leg (random OS_AUTH_SECRET / OS_SECRET_KEY);
      • every install-local and manifest-install boot;
      • the connector-degraded twins;
      • the Studio leg.
    • 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, so git status stayed clean, but a per-boot -d DB does not isolate these two (see the accuracy findings).
  • Special switches:
    • OS_AUTH_TWO_FACTOR=true on the two-factor armed boot, with an unarmed control boot;
    • OS_CLOUD_URL=off on the install-local B boot;
    • --log-level info on the connector-degraded boot;
    • NODE_OPTIONS=--import packages/cli/test/fixtures/live-conversion-probe.mjs only on the build/validate conversion-parity legs;
    • mfa_required / grace were changed via PUT /api/settings/auth and 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 --porcelain was 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.

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_probe carrying externalSharingModel → 422 INVALID_METADATA on both the active door and ?mode=draft. There is one issue, at externalSharingModel, 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 → 403 NOT_OVERRIDABLE, and with ?package= → 403 ITEM_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.ts 10/10 (plugin-unit layer) and meta-object-owd-gate.test.ts 11/11 (rest protocol layer, in-process).
  • A3 pass: GET before and after are byte-identical (sha256 b550eb72…).
  • A4 pass: showcase_account and showcase_announcement carry sharingModel, with no externalSharingModel.
  • N0 pass (server half; surface api).
  • N1 pass.

access-security.packaged-flow-write-door-parity (R1)

  • A0 pass: ?layers=true shows a code layer with _packageId com.example.showcase and lock full. PUT /meta/flow/showcase_urgent_task_alert → 403 NOT_OVERRIDABLE, and with ?package= → 403 ITEM_LOCKED. The reads before and after are identical.
  • A1 pass: PUT /automation/showcase_urgent_task_alert → 403 NOT_OVERRIDABLE with 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: _status reads 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 the manage_metadata capability", 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 with ai: { exposed: true, description: … }". The task's status and updated_at are unchanged.
  • A1 pass: list_actions {} → only showcase_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_session without objectName → "is on a system object and is not exposed via MCP"; with sys_session → the system-object refusal. The victim session's get-session → 200.
  • N0 pass, N1 pass.

api-backend.batch-transactional-discovery (R2)

  • A0 pass: /discovery capabilities.transactionalBatch = {enabled:true, description}. An atomic /batch with a note create and a task create → 200, and both re-read 1 / 1.
  • A1 pass: atomic:false → 400 BATCH_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/batch with [valid, no-title, valid]:
    • continueOnError → succeeded 2, failed 1.
    • The default → row 0 ok, row 1 VALIDATION_FAILED, row 2 NOT_ATTEMPTED, matching the re-reads.
  • A5 pass: #24781 fixed. Update and delete with id "" → 400 VALIDATION_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 with fields[0] {field:name, code:required, message, label}.
  • A2 pass: no fieldErrors in any body.
  • A3 pass: the statuses match HttpStatusErrorCodeMap and the families.
  • A4 pass: an unknown object → 404 OBJECT_NOT_FOUND, and an unknown record → 404 RECORD_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 where and GET ?filter= → 200 with ids identical to the local baseline. Date and bigint were scored in process, through spec parseFilterAST + the InMemoryDriver dist.
  • A1 pass: object comparands → 400 (POST VALIDATION_FAILED at query.where.title.$eq; GET INVALID_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.ts 12/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 gives INVALID_FILTER "magnitude exceeds 2^53".
  • A3 pass:
    • A relation head project.name, a scalar head title.foo and a virtual head budget_remaining.x → 400 INVALID_FIELD on both doors.
    • The nesting prescription works (200, 5 rows).
    • A JSON head → 400 INVALID_FILTER from the driver; it is not dropped.
  • 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 and os migrate meta --from 16. median → 400 naming the six functions. An unknown field → 400 INVALID_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/query day 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 → 404 OBJECT_API_DISABLED.
    • PATCH sys_session/ID → 405 OBJECT_API_METHOD_NOT_ALLOWED, allowed [get, list, aggregate, export].
    • sys_device_code list → 405, allowed [get].
  • A1 pass: the scratch qa_verb_gate_probe with [get, list, create] → PATCH / DELETE 405, allowed equal to spec effectiveOperationsArray(resolveEffectiveApiMethods(…)) element for element.
  • A2 pass: undefined → 200; [] and ['upsert'] → deny-all 405 with allowed []; non-array → pin api-derivation.test.ts passed, 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.
  • 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_signoff run, with {} and with {branchLabel:"approve"} → 403 PERMISSION_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 /actions unchanged (1 row).
  • A2 pass: POST /approvals/requests/ID/approve → approved (slate [admin, admin], distinct slots). A second approve → 409 INVALID_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. With bu_west_coast deactivated → [A, Ada]. An unknown or inactive root gives the "expanded to nobody" WARN and goes to admin_rescue.
  • A2 pass: pending_approvers holds 2 ids, and __resolvedFrom holds 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_APPROVERS with 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.
  • 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-doors recipe 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 wrapping success:false.
  • 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_purge disabled, POST /api/v1/apps/showcase/inquiries/purge → 409.
    • Type:'flow' actions authored under the QA package → 400 FLOW_FAILED and 422 FLOW_NO_START_NODE.
    • All three doors import classifyFlowRefusal.
  • A3 pass: details.errorMessage and details.summary at the trigger door and at /actions.
  • A4 partial (6 arms pass; each retryable arm was re-read as still paused):
    • {inputs:{}} and bodyless → 400 INVALID_SCREEN_INPUT
    • output $x → 400 INVALID_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_UNAVAILABLE arm 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, and resume() 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_owner off → 409 DELETE_RESTRICTED, naming showcase_task_done_notify_owner, ADR-0126 §7.3 and the remedy. showcase_closure_signoff gives a 409 naming showcase_project_closure.
  • A1 pass: map child showcase_one_task_signoff → 409 naming showcase_release_signoff.
  • A2 pass: no sys_metadata_activation rows after the refusals. A caller run completed with its call_notify subflow 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/automation does not guard, which confirms knownGaps[0].

automation.packaged-flow-clone-contract (R3)

  • A0 pass: {} / {label} → 400 VALIDATION_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, _packageVersion and _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
  • 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.
  • 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 build exits 0 with "✓ Build complete", the stats and Artifact: dist/objectstack.json (2.6 KB). --json gives 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'?"; --json exit 1 STACK_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 --json and validate --json warnings and conversions are equal on 5 fixtures. With the live-conversion probe hook, both carry the same non-empty conversions[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:false prints 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_PATTERNS holds 13 entries at 756220c; the item says 14 (accuracy finding).
    • One hook per entry: --strict-body exits 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) and ctx.api.transaction(fn).
  • A1 pass: find → ["api.read"], update → ["api.write"], and the alias → ["api.read"].
  • A2 pass: randomUUID → ["crypto.uuid"] and ctx.log.info → ["log"]. hash-only → [].
  • A3 pass: an authored body ships exactly ["api.write","log"]. A // @capabilities comment 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-body exits 1 naming localHelper, qaHelper. The implicit-return arrow lowers to return (…);.
  • A5 pass: once every callable is body-only, "Skipping legacy runtime bundle" prints and the old .mjs is cleaned.
  • A6 pass: the 13-hook default build exits 0 with the warning block (one line per hook), and JSON carries 13 bodyExtractionWarnings separate from warnings. 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 --yes exits 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:true and enable {} → 400. The unarmed control's twoFactor:false and enable → 404.
  • A1 pass: enable → 200 with method, totpURI, backupCodes. The URI has label ObjectStack: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_factor resultDialog paths resolve against the live response.
  • A4 pass (recorded): get-totp-uri returns 400 without a body, 400 INVALID_PASSWORD for 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_factor declares 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_enabled stays 0.
  • A2 pass: the row is verified=0 right 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 only better-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_codes at 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_codes declares a code-list, the acknowledge text and refreshAfter: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 → 400 BACKUP_CODES_NOT_ENABLED.
    • Re-enable gives a new secret with no overlap.
    • An old-secret TOTP → 401 INVALID_CODE on both lanes, and old codes → 401. The new secret → 200.
  • A5 pass (recorded), with mfa_required on:
    • Disable → 200, and mfa_required_at is stamped lazily.
    • With grace 0, data → 403 MFA_REQUIRED and user.authGate is set. Enable and verify are reachable, and data → 200 afterwards.
    • The stamp is not cleared on re-enrolment. Settings were restored.
  • 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 reads state:'degraded', the reason and actions:[].
  • A1 pass: the carrier_pigeon twin compiles, then fails at boot ("transport.kind must be 'stdio' or 'http'", rollback complete). Nothing listens.
  • A2 pass: echo trigger → 400 FLOW_FAILED in 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' → 422 OS_PROTOCOL_INCOMPATIBLE; GET → 404, and there is no list entry and no sys_packages row. The ^18 baseline → 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 (rangeSource engine.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=true reinstall → 201; a plain re-POST → 409.
    • base / system / sys → 201, and a second package on base → 201.
  • A5 pass: the derived namespace leave and the declared timeoff agree 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/packages 502 MARKETPLACE_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.
  • A1 pass: features A {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-config is 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"; decideWriteAction routes replace to insert).
  • A5 pass: SeedMode has 5 members; defaultMode is SeedMode.default('upsert'); packages/runtime/src/seed-loader.ts re-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
  • /automation POST / 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 / migrate boundaries
  • /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):

  1. A deleted flow with a paused approval run: the approve answers 500 RESUME_FAILED after recording the decision, and the run is stranded. Beside approvals.decision-only-via-service.
  2. 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. Beside cli.hook-body-extraction-gates.
  3. A file field accepts a presigned upload that was never completed (sys_file status pending), and claims it. Beside attachments-storage.field-accept-maxsize-server-enforced.
  4. OS_PROTOCOL_INCOMPATIBLE for a package that declares a newer major (^19 on an 18 runtime) prescribes objectstack migrate meta --from 19, which does not apply. Beside platform-core.manifest-install-contract.

Low, not extracted:

  • The error-envelope dialect differs between doors: /batch schema refusals carry zod issues[], the data doors carry fields[], and malformed JSON gives a nested INVALID_REQUEST.
  • The spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822 refusal message says a language name is refused, but locale english saves 200, because it is syntactically a valid language subtag. The message overclaims.
  • POST /api/v1/batch {"operations":[]} → 200 results:[].
  • 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 8 with runtimeModule null.
  • On OS_CLOUD_URL=off, an empty install-local body → 503 "OS_CLOUD_URL not configured" instead of a 400.
  • One manage_metadata gate answers FORBIDDEN on install-local and PERMISSION_DENIED on /api/v1/packages.
  • A declared mimeType with parameters (image/png; charset=binary) is refused by an accept list naming image/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

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/auth answers 404.

api-backend.batch-transactional-discovery

api-backend.error-envelope-ledger

  • A3 calls OBJECT_NOT_FOUND / RECORD_NOT_FOUND extension codes, but both are StandardErrorCode members.

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_revenue does not discriminate (all values are distinct). Use status.

api-backend.api-methods-verb-gate

  • The closure includes search only when searchable is not false.
  • The createMany body is a bare array.

api-backend.enforce-or-remove-authoring-gates

  • The os migrate meta --from 16 $regex advisory 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 $contains alternative 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 and expandedUsers, never the counter. sys_record_share refuses a data-API DELETE (405).

approvals.approver-resolution-matrix

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

  • classifyFlowRefusal has a fifth row, 422 FLOW_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 build tree, /_console/ answers 503 until pnpm 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

cli.build-own-contract

  • Stock fixtures yield conversions [], so A3's parity is vacuous without packages/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 (vs PERMISSION_DENIED on /packages).
  • The stock admin create-user answers membershipCreated:false, yet the member holds role member.

platform-core.seed-mode-matrix

  • fixtures.requires: a body name that mismatches the URL answers 400 VALIDATION_ERROR before the 422 unrecognized_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 '', so cloudUrl is empty by design on both arms.

RUNNER.md environment facts

  • A per-boot -d DB does not isolate the showcase's local upload root or its external datasource: tree boots write examples/app-showcase/.objectstack/data/uploads/ and showcase_external.db (git-ignored, so invisible to git 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 both grep and kill. 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

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Close-out for this record (card W2-C3a, release-18.2.0-w2-manual-api, 29/29 at 756220c4: 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions