Skip to content

QA run · release-18.3.0-w4-c3a (44/44) · 7fa9873f · 2026-11-02 · 41 PASS / 2 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #28942

Open

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
Part of #27377 (wave 4, W4-C3a)

QA run record for the 18.3.0 release-candidate sweep, wave 4, card W4-C3a: the unpinned P1 set (plus unpinned v18.x P2) on the api · cli · build surface. Every clause was driven live over REST, CLI, MCP or a build, on a fresh showcase boot (or a scratch app) per item.

Result: 44 of 44 items judged. 41 PASS, 2 PARTIAL, 1 FAIL, 0 BLOCKED, 0 NOT-RUN. The one FAIL is ai.mcp-run-action-exposure-gate, with two failing clauses: one stale spec and one product defect. All five owed retests hold. Off-clause probes found six further product defects, extracted at close-out through the pm-dispatch filing gate; a seventh observation did not pass the gate and is kept in the notes below. One off-clause security observation was reported privately to the dispatching PM session and is not described here.

The per-clause verdict table (370 rows) is too long for one issue body. It is posted in the three comments directly below this body, grouped by runner, in the same column shape as wave 3.

Environment fingerprint

  • Framework: objectstack-ai/objectstack @ 7fa9873f38f7a0158e3ce17338dcc0f446b113e3 (detached), which includes ui/. Its push CI (incl. Dogfood Regression Gate) and Lint were green.

  • Build: npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (23 s), then pnpm exec turbo run build --concurrency=2 under scripts/pm/os-verify-lock.sh (16m12s, exit 0). node scripts/check-dev-prereqs.mjs green (106 package artifacts). The console was not built: no item has a browser surface.

  • Boot, per item: OS_PORT=PORT OS_STORAGE_LOCAL_ROOT=RUN/uploads setsid pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin --no-compile --no-watch -p PORT -d file:RUN/data.db, stopped by process group. examples/app-showcase/dist/objectstack.json was compiled once by the build and never rewritten (mtime and md5 unchanged at the end).

    • Items that install packages, compile or build an app, write the external datasource, or need an edited app booted from a scratch copy of the app (tar, node_modules symlinked back) or a scratch create-objectstack -t blank scaffold.
    • examples/app-showcase/.objectstack/installed-packages/ was never created in the tree. git status was clean at the end. Every tree boot creates the git-ignored .objectstack/data/showcase_external.db (the RUNNER environment fact); it was left in place.
  • Ports 41300–41439, one file DB per boot. Execution was parallel: seven runner subagents (5–7 items each, grouped by area), one boot at a time per runner. Their verdicts were accepted after the coordinator cross-checked every runner's clause rows against each item's acceptance and negative counts, and re-read the FAIL against the item history and the rulings it cites.

  • Seed: stock showcase plus --seed-admin (admin@objectos.ai). Personas: the demo personas (password showcase123); members minted by invite then sign-up, or by admin create-user; the search:qa-contributor-bound-member recipe. Each persona's identity was proven with GET /api/v1/auth/get-session before its cells.

  • Special boots:

    • OS_AUTH_TWO_FACTOR=true for the four two-factor items (one boot, run as one sequence as backup-codes fixtures.requires[0] prescribes);
    • OS_METADATA_WRITABLE=object for the second leg of owd-save-gate, OS_METADATA_WRITABLE=agent for a retired-def-refusal leg;
    • OS_CLOUD_URL=off for marketplace-install-local-lifecycle boot B and the runtime-config offline arm;
    • a scratch copy with the MCP upstream moved away for connector-degraded-recovery, with the dispatch proven to land on that boot (ping uptime 21.16 s against /health 24.31 s three seconds later), and a config-fault twin booted with compile from its own scratch copy;
    • os serve with OS_AUTH_SECRET and OS_SECRET_KEY for fresh-install-migrate-apply-first.

    OS_AUTOMATION_SCHEDULED_WORK_ENABLED was unset; no clause in this set is clock-driven.

  • Timestamps (UTC, 2026-11-02): preparation 03:33, build 03:34–03:50, runners 03:51–04:17, coordinator verification and close-out drafting from 04:17.

Scope

The selector is release-18.3.0-w4-c3a: the W4-C3a id list of anchor #27377's wave 4, computed from the ledger at 7fa9873. Revisions are the ones node scripts/checklist-select.mjs ID --json reported at 7fa9873. All 44 items are runnable and none is planned.

access-security.scope-depth-asymmetry r5 · access-security.sharing-rules-widen r4 · access-security.record-share-grant-revoke r8 · access-security.owd-save-gate r7 · access-security.packaged-flow-write-door-parity r4 · ai.agent-tool-skill-metadata-roundtrip r8 · ai.mcp-run-action-exposure-gate r6 · api-backend.batch-transactional-discovery r8 · api-backend.formula-gates r3 · api-backend.enforce-or-remove-authoring-gates r7 · api-backend.error-envelope-ledger r8 · api-backend.filter-comparand-conformance r4 · api-backend.retired-def-refusal r4 · api-backend.aggregate-contract-matrix r7 · api-backend.api-methods-verb-gate r4 · approvals.decision-only-via-service r5 · approvals.approver-resolution-matrix r9 · attachments-storage.field-accept-maxsize-server-enforced r7 · automation.trigger-status-contract r11 · automation.packaged-flow-subflow-disable-refusal r11 · cli.build-own-contract r9 · cli.migrate-plan-apply-json r8 · cli.verify-verdict-exit-mapping r3 · cli.qa-suite-execution r11 · cli.migrate-meta-codemod r7 · cli.doctor-health-report r5 · cli.migrate-duplicates-inventory r10 · cli.hook-body-extraction-gates r8 · cli.fresh-install-migrate-apply-first r3 · identity-auth.self-service-password-reset r5 · identity-auth.two-factor-enrollment-reveal r5 · identity-auth.two-factor-verify-to-activate r5 · identity-auth.two-factor-backup-codes r6 · identity-auth.two-factor-disable-lifecycle r3 · integration-system.connector-degraded-recovery r6 · integration-system.external-datasource-federated-read r5 · platform-core.docs-audience-gate r4 · platform-core.manifest-install-contract r8 · platform-core.marketplace-install-local-lifecycle r9 · platform-core.runtime-config-boot-read r5 · platform-core.seed-mode-matrix r6 · records-forms.validation-rule-type-matrix r7 · search.rls-both-personas r8 · studio-authoring.removed-key-save-door-refusal r4

No automated.ref pin scored a clause in this set. Supporting pins that were run are cited with their layer in the clause evidence.

Comparison with wave 3 (#27392, at c4c515a)

25 of the 44 items were in #27392; 19 are new to this card.

  • Deltas on the 25 shared items:
    • ai.mcp-run-action-exposure-gate r5 → r6: PARTIAL → FAIL. Clause 1's permitted half, blocked in wave 3, now passes on the qa_ai_gated recipe added by r6 (its first live proof). Clause 4 fails as a stale spec and negative 1 as a product defect (see FAIL).
    • platform-core.docs-audience-gate r3 → r4: PARTIAL → PARTIAL, the same gap (clause 2's unknown-future-shape half has no pin and is unreachable live).
    • The other 23: PASS → PASS. Revisions moved on 14 of them (record-share-grant-revoke r7→r8, agent-tool-skill-metadata-roundtrip r7→r8, aggregate-contract-matrix r6→r7, api-methods-verb-gate r3→r4, error-envelope-ledger r7→r8, packaged-flow-subflow-disable-refusal r10→r11, trigger-status-contract r8→r11, fresh-install-migrate-apply-first r2→r3, hook-body-extraction-gates r7→r8, qa-suite-execution r10→r11, two-factor-disable-lifecycle r2→r3, two-factor-enrollment-reveal r3→r5, manifest-install-contract r7→r8, removed-key-save-door-refusal r2→r4).
  • New to this card (19): 18 PASS and 1 PARTIAL (cli.verify-verdict-exit-mapping). The new items are access-security.scope-depth-asymmetry, access-security.sharing-rules-widen, api-backend.formula-gates, api-backend.enforce-or-remove-authoring-gates, api-backend.filter-comparand-conformance, api-backend.retired-def-refusal, cli.build-own-contract, cli.migrate-plan-apply-json, cli.verify-verdict-exit-mapping, cli.migrate-meta-codemod, cli.doctor-health-report, cli.migrate-duplicates-inventory, identity-auth.self-service-password-reset, identity-auth.two-factor-verify-to-activate, identity-auth.two-factor-backup-codes, integration-system.external-datasource-federated-read, platform-core.marketplace-install-local-lifecycle, platform-core.runtime-config-boot-read, records-forms.validation-rule-type-matrix. Two of them carried a FAIL or PARTIAL in their last record (identity-auth.two-factor-backup-codes FAIL at QA run · surface:api (89/89) · 251a7dd4 · 2026-10-04 · 68 PASS / 7 PARTIAL / 14 FAIL / 0 BLOCKED / 0 NOT-RUN #21720, cli.migrate-plan-apply-json PARTIAL at QA run · area:cli (17/17) · 94114c1f · 2026-10-16 · 16 PASS / 0 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #23921); both PASS here.

Derived item verdicts

PASS (41): every item in Scope except the three below.

PARTIAL (2):

  • cli.verify-verdict-exit-mapping: clause 3's "a hole fails the run" leg is blocked(fixture). A hole is a real RLS bypass and cannot be staged on a healthy build. The RLS stack, the rls section and the staged fail side (fidelity-gap, create-failed, needs-fixture → exit 1, hardFailures 2) all passed.
  • platform-core.docs-audience-gate: clause 2's unknown-future-audience-shape half is blocked(fixture): no pin, and the save door refuses that shape with 422 so it cannot be planted live (knownGaps[2]). The other half passes on the spec pin book.test.ts (45/45, pure schema layer).

FAIL (1):

  • ai.mcp-run-action-exposure-gate: acceptance[4] (stale spec) and negative[1] (product defect). Clauses 0–3 and negative[0] pass.

FAIL — reproduction rules and dispositions

ai.mcp-run-action-exposure-gate · acceptance[4] — disposition: stale spec

The clause pins the pre-#27681 wholesale refusal of every sys_* action. Maintainer ruling 6513869839 on #27681 (letter B) and the ADR-0011 addendum "the platform identity actions an agent may reach" (ruling 6523087116 on #27900, amended by 6540309323) replaced it with a per-action ai.exposed opt-in, implemented by systemObjectActionExposureError (packages/runtime/src/action-execution.ts) and isListableAction (packages/mcp/src/mcp-http-tools.ts). Item r6 (2026-10-31) changed only the clause-1 fixture and kept this clause.

Reproduction (three fresh stock boots, 41325–41327): admin osk_ key from POST /api/v1/keys {"name":"k"}; MCP Streamable HTTP on /api/v1/mcp (initialize, then tools/call).

  1. list_actions {} → totalCount 37, of which 36 have a sys_* objectName (sys_user, sys_session, sys_api_key, sys_organization, sys_member, sys_invitation, sys_team, sys_team_member, sys_account, sys_oauth_application, sys_sso_provider). Expected by the clause: none.
  2. run_action {"actionName":"create_api_key","objectName":"sys_api_key","params":{"name":"x"}} → refused, nothing created, but with 'Action "create_api_key" on system object "sys_api_key" is not exposed via MCP: a system object's action is reachable only when its author exposed it to AI (ai.exposed) and list_actions lists it for you'. Expected: 'Object "sys_api_key" is a system object and its actions are not exposed via MCP'.
  3. The same call without objectName → "Action 'create_api_key' is on a system object ('sys_api_key') and is not exposed via MCP — a system object's action is reachable here only when its author declares ai: { exposed: true, description: … } on it". Expected: "Action 'create_api_key' is on a system object and is not exposed via MCP".
  4. run_action {"actionName":"revoke_api_key","objectName":"sys_api_key","recordId":ID} passes the guard and meets the next gate, 428 ACTION_CONFIRMATION_REQUIRED. Expected by the clause: the guard refuses it.

Product side is correct (non-exposed system actions are refused with zero side effects). The clause, step 6 and acceptance[1].verify ("showcase_portfolio_snapshot is the one exposed action") need re-pointing. Carried by the wave anchor #27377 §1 ("Re-point — red rows this anchor owns"), the way wave 3's stale-spec row was; under the pm-dispatch filing gate a ledger re-point is not a card of its own (close-out Parked-on: line).

ai.mcp-run-action-exposure-gate · negative[1] — disposition: product defect

The negative forbids list_actions advertising an action that run_action then refuses. Reproduced on three fresh boots (41325, 41326, 41327):

  1. Fresh stock boot. Admin (session): POST /api/v1/keys {"name":"r2-repro"} → key K; POST /api/v1/keys {"name":"throwaway"} → id T.
  2. MCP on /api/v1/mcp with Authorization: Bearer K (initialize, notifications/initialized).
  3. list_actions {}: revoke_api_key (objectName sys_api_key, requiresConfirmation true) carries NO unavailable key, while every identity-route action in the same list carries unavailable: {code: UNAUTHENTICATED, reason: "…accepts a signed-in session only, and this request was authenticated with an API key…"}.
  4. run_action {"actionName":"revoke_api_key","objectName":"sys_api_key","recordId":T,"confirm":true} → isError {"code":"PERMISSION_DENIED","status":403,"message":"PERMISSION_DENIED: Changing an API key requires a signed-in session. … a request authenticated with an API key cannot revoke, restore or change API keys."}. Admin GET /api/v1/data/sys_api_key shows T revoked: false.
  • Expected: either the listing marks revoke_api_key unavailable for an API-key caller, or the run succeeds.
  • Actual: listed as runnable, then refused 403; nothing changed.
  • Control: the same run_action through MCP with the admin's session bearer → ok: true, T reads revoked: true.

Self-check: the osk_ key is valid (the same MCP session ran showcase_portfolio_snapshot); Mei's key shows the same listing (not wrong-persona); the refusal text names the credential kind (not a driver artifact). Mechanism: apiForwardCredentialRefusal classes revoke_api_key's target PATCH /api/v1/data/sys_api_key/{id} as the data door, which its table says accepts api_key, while packages/core/src/security/api-key-management-credential.ts refuses API-key-authenticated changes to sys_api_key. The refusal holds and nothing changes, so this is a listing-honesty defect, not an authorization hole.

Retests (anchor #27377 §4 and SKILL §5 trigger 2)

node scripts/qa/retest-owed.mjs at 7fa9873 listed five owed rows on this card's items. All five hold.

Wave 3's two off-clause retests on this card's items were re-confirmed: #26478 (cli.hook-body-extraction-gates: five builds — first, rebuild, other cwd, other checkout path, fresh copy with no dist — emit the same bundle, sha256 11daba63…, with no absolute path in the bundle or its decoded sourcemap) and #26480 (platform-core.manifest-install-contract: ^19, >18, engines.platform ^19, legacy ^19.0.0 and ^20 each answer 422 OS_PROTOCOL_INCOMPATIBLE with no migrateCommand and leave no row; the older direction still offers migrate meta --from 17).

Off-clause probes (RUNNER rule 8)

Probes took about 60 runner-minutes across the 44 items. Doors probed: every door the clauses drove, plus /api/v1/analytics/dataset/query, the createMany/batch doors, the sys_two_factor and sys_record_share data doors, the encoded-path install-local doors, and os test / os migrate / os build argument edges. Personas: admin, the demo personas, minted members, the contributor-bound member, anonymous, a garbage or rotated bearer, and osk_ keys. Boundary classes: empty string, null, missing and unknown id or name, case variants, encoded ..%2F segments, oversize and malformed codes, opposite severity and posture (info/warning/error, password_history_count 3 vs 0, isolated vs single), readers outside scope, a trailing slash, an inverted range.

Seven product defects were found, each reproduced at least twice on independent boots. Six pass the pm-dispatch filing gate (class ①) and are extracted at close-out; number 3 does not and stays here as a note:

  1. Beside api-backend.formula-gates: the date-arithmetic gate passes bare date subtraction (record.end_date - record.start_date, today() - record.start_date); build, validate and lint exit 0, and the field reads null or {} at runtime.
  2. Beside api-backend.enforce-or-remove-authoring-gates: /api/v1/analytics/dataset/query refuses retired $regex / $options in runtimeFilter with a generic "Unsupported filter operator" instead of the RETIRED prescription naming $icontains.
  3. Beside cli.build-own-contract: os build reports size and the summary KB as UTF-16 code units, not bytes (jsonContent.length in compile.ts; blank scaffold 2540 reported vs 2542 on disk, showcase 743.0 KB vs 750.4 KB). Not filed: the filing gate finds no user-visible cost beyond a 0–3% misreported size that no gate or reader acts on.
  4. Beside cli.qa-suite-execution: os test --url with a trailing slash requests //api/v1/... and fails every HTTP scenario 404.
  5. Beside cli.migrate-meta-codemod: os migrate meta --json answers an empty (--from 18) or inverted (--from 19) range exactly like a clean replay; the "replayed nothing" warning exists only on the human path.
  6. Beside records-forms.validation-rule-type-matrix: the showcase flow showcase_project_escalation fails terminally (CEL no such overload: dyn‹null› > int) whenever a project with no budget turns red.
  7. Beside platform-core.seed-mode-matrix: an upsert seed whose externalId names a field the object does not have is accepted and inserts a duplicate on every publish, silently.

Security: identity-auth.self-service-password-reset · observed beside the clauses · security finding — detail withheld, reported to PM. It moves no clause verdict.

Observations recorded and not extracted (judged by design, documented, or wording only):

  • /automation answers a removed flow key with 400 VALIDATION_FAILED where /meta answers 422 INVALID_METADATA (deliberate in the code); view refusals add a bare root "Invalid input" issue; the unknown-key hint can suggest an unrelated key (aria → areas, defaultSort → defaults, a field with no type → "Did you mean 'url'?").
  • PUT /meta/TYPE/NAME?mode= with any value other than draft saves live, as documented, so a typo such as ?mode=drafts publishes env-wide; the same module refuses an unknown ?surface=.
  • os test prints a stack trace and a doubled "Error: Error:" prefix on an ordinary assertion miss (seen in wave 3 too); "Config file not found" exits 1 on the human path where other load failures exit 2 (within the clause's allowed set).
  • os migrate plan --json --database-url '' falls back to the default DB (the payload says kind: default); os migrate duplicates --object does not narrow liveConditions; os migrate organization-ownership --json writes its plan file into the CWD by default.
  • A field approver with an empty or unknown field logs only the node-level warning; a member's write to an external object returns datasource details before the permission refusal; any signed-in member can list connectors with their degraded reasons.
  • Derived namespaces of two long ids sharing their first 20 characters collide, and the 422 NAMESPACE_CONFLICT does not say the namespace was derived; a catalog install on OS_CLOUD_URL=off answers "OS_CLOUD_URL not configured" although it is set to off.
  • Two-factor: disable on an unenrolled user is a no-op 200 that still rotates the session; the challenge sign-in response carries a set-auth-token header whose token is dead.

Checklist-accuracy findings (for anchor #27377)

  • ai.mcp-run-action-exposure-gate r6: acceptance[4], step 6 and acceptance[1].verify are stale (the FAIL above, carried by the anchor §1 re-point row). fixtures.requires[3] (qa_ai_gated) is now proven live: admin list_actions 38 includes it, Mei's 37 does not, teardown DELETE 200 then GET 404; the history's "Not yet run live" can close.
  • api-backend.filter-comparand-conformance r4: acceptance[1].verify asks every refusal to quote the accepted-types sentence, but the array-for-scalar refusal comes from the operator-arity door (it names the path and prescribes $in/$between); acceptance[3] lists "a multiple field" in the null class, but a multiple lookup classifies as relation and is refused 400 INVALID_FIELD; no stock task row has a null number or boolean (use start_date, or seed one).
  • api-backend.aggregate-contract-matrix r7: knownGaps[0] still holds; one more seeded duplicate revenue row also makes count_distinct(annual_revenue) discriminating (12 vs 13).
  • api-backend.retired-def-refusal r4: steps[2]–[4] still name manifest.loading as the prescription-bearing carrier; since spec: retire tombstones, conversions and migration entries older than one protocol major; a strict unknown-key refusal with an upgrade-guide link takes over (maintainer ruling D2 = A) #25767 (ADR-0087 amendment D2=A) it aged out at 18 and gets the generic refusal plus upgrade-guide pointer. Name a live carrier (the themes stack key, manifest.extensions). Registry digits: 247 entries (53 at 17, 194 at 18).
  • api-backend.enforce-or-remove-authoring-gates r7: stored widget filters are served by /analytics/dataset/query; a clause leg there would have caught probe defect 2.
  • cli.verify-verdict-exit-mapping r3: acceptance[3].verify cites rls.summary.holes, but verify.ts sums rls.totals.holes plus rls.probe.degraded plus rls.positionCoverage.notRun.length; acceptance[1]'s hardFailures formula omits those and the five opt-in checks; steps[4] could say a bare walled posture fails loudly with WALLED_MEMBERSHIP_POLICY_UNDECLARED.
  • cli.build-own-contract r9, acceptance[3]: a plain thrown Error at load carries no code on either command; only coded throws do. Wording only.
  • cli.hook-body-extraction-gates r8: CAPABILITY_PATTERNS has a fifth shape, .title(…) → api.read, that steps[2] does not probe.
  • cli.migrate-duplicates-inventory r10: acceptance[3] "three entries from two migrations" is now four entries from three (sys_setting and notification-preference identity indexes added); steps[1]'s NULL organization_id row cannot be inserted on a freshly synced DB (column NOT NULL) without rebuilding the table to the nullable shape.
  • cli.migrate-plan-apply-json r8, steps[8]: meta needs --from though its USAGE shows it optional; record-currency-codes has no stock success path (no record-mode currency field) unless the scratch config adds one.
  • cli.migrate-meta-codemod r7: negative[6] should read "while schema refusals remain after the chain", since the 448 step-18 advisories always print beside "schema-valid"; planting the data key on Category is refused [shared] (also referenced by a seed) while PrivateNote writes cleanly.
  • records-forms.validation-rule-type-matrix r7, acceptance[1]: script and conditional rules without a declared field target _record, which the clause prose ("not a bare object-level failure") would read as a fail.
  • identity-auth.two-factor-backup-codes r6, fixtures.requires[2]: names the DB file only for a --fresh boot, not the -d path of the canonical boot.
  • identity-auth.two-factor-enrollment-reveal r5, acceptance[4].verify: "the pre-verify bearer answers 401" holds only after verify-totp rotated the session.
  • identity-auth.self-service-password-reset r5, steps[0]: the forgetPassword: '/forget-password' divergence in packages/spec/src/api/auth-endpoints.zod.ts is still unfiled.
  • automation.trigger-status-contract r11: acceptance[5]'s "node edited away under a live pause" exit no longer answers 404, because a paused run pins its flow body (resolveResumeTarget, run-resume.ts) and resumes on it (200); only the deregistered-flow exit is drivable, and the clause was scored on it. 409 RESUME_CHILD_PENDING (clause 4) has no step; it is drivable with a map parent over a screen child.
  • attachments-storage.field-accept-maxsize-server-enforced r7, fixtures.requires[2] / steps[7] / clause 5: since service-storage: sys_file lifecycle columns (status, size, mime_type) are writable through the data door, so a never-uploaded file can be marked committed and then claimed by a file field #27399 the admin data-door PATCH no longer clears mime_type/size; the working channel is an out-of-process write to the boot's file DB (or a system-context engine write).
  • approvals.decision-only-via-service r5, steps[4]: the dev admin fills both slots of the unanimous slate, so one approve satisfies it and a second answers 409 INVALID_STATE.
  • integration-system.external-datasource-federated-read r5, steps[1]: GET /api/v1/meta answers only types and entries; the federated objects are confirmed through GET /api/v1/meta/object. GET /api/v1/datasources/showcase_external carries no status (only the list does).
  • access-security.record-share-grant-revoke r8: the last step assigns the position through the Setup UI; the API door POST /api/v1/data/sys_user_position makes it replayable without a browser.
  • access-security.sharing-rules-widen r4, acceptance[4]: the re-enabled rule's share gets a new id; the clause (same id after the second restart) is right.
  • platform-core.seed-mode-matrix r6, steps[0]: an unprefixed object's publish answers HTTP 200 with outcome:"refused" and code NAMESPACE_PREFIX, not 422; discarding a draft takes ?state=draft (?mode=draft → 400).
  • platform-core.manifest-install-contract r8: steps[0] reads the protocol version from the boot banner or GET /api/v1/meta, which carry none (discovery version is the runtime's 18.2.0); steps7 needs a SECOND package claiming the same shareable namespace to discriminate the exemption (this run drove base and sys twice each, both 201).
  • RUNNER environment-fact candidate: an os dev --no-compile --no-watch boot still hot-reloads dist/objectstack.json when another process rewrites it and syncs the new schema into its DB within about 3 s, so on a shared tree any rebuild of examples/app-showcase/dist changes every other runner's live boot.

Fixture gaps

  • cli.verify-verdict-exit-mapping clause 3: no stageable rls-hole (the PARTIAL above); clause 2 has no lever for a 5xx create (a throwing hook answers 400 HOOK_ABORTED), staged instead with a mutating hook, a throwing hook and an always-false validation on a scratch scaffold.
  • platform-core.docs-audience-gate clause 2: unknown-shape half has no pin (knownGaps[2], the PARTIAL above). The stock showcase still has no permissionSet-gated book or orphan doc; the run authored them in a scratch package and discarded them.
  • api-backend.aggregate-contract-matrix: 5 showcase_account rows seeded at runtime (null revenue, Monday/Sunday, ISO-year boundary, duplicate revenue), plus the contributor-bound member and 2 own invoices.
  • api-backend.filter-comparand-conformance: 1 showcase_task with null fields seeded.
  • api-backend.api-methods-verb-gate: scratch objects authored through PUT /meta/object, as the item's knownGap prescribes.
  • ai.agent-tool-skill-metadata-roundtrip: the AI trio and 11 probes authored only in a scratch app copy (no stock AI seeds).
  • cli.migrate-plan-apply-json / cli.migrate-duplicates-inventory: no record-mode currency field and no second org-scoped identifier in the stock showcase; both added in scratch configs. The duplicates mongodb branch remains unverified (knownGaps).
  • access-security.sharing-rules-widen: the stock seed has no org-stamped sys_business_unit_member rows (boot WARN "expands to NO recipients"); the runtime BU member that fixtures.requires prescribes resolves it.

Timing

Wall clock, cold tree to last runner verdict: 44 min.

  • Environment: about 17 min wall (install 23 s, full build 16m12s), plus about 50 runner-minutes of boots, scratch copies and persona and fixture minting inside the runner window.
  • Testing: 26 min wall (03:51–04:17) for seven parallel runners; about 60 runner-minutes of that were off-clause probes.
  • The wall ratio of environment to testing is roughly 1 : 1.5.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

    Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

    Per-clause verdicts, part 1 of 3 (access-security, search, studio-authoring, api-backend, ai, build-surface cli). Clause numbers are acceptance indices; neg / negN are the item's negative list. Evidence is text oracle output only.

    item clause verdict evidence (text oracle)
    access-security.scope-depth-asymmetry 0 pass Manager (position manager, get-session qa-r1-mgr) GET list → total 5 incl. 'M probe' (same 5 as admin). By-id GET of M's inquiry Iz7eT6N1IL9GTp-r → 200
    access-security.scope-depth-asymmetry 1 pass Manager PATCH M's inquiry → 403 PERMISSION_DENIED "You do not have access to this record…". Admin re-read shows message 'scope probe', updated_at 03:53:38.353Z, updated_by M (unchanged). Manager PATCH of own inquiry → 200, re-read 'mgr edited own'. A seeded foreign inquiry also → 403
    access-security.scope-depth-asymmetry 2 pass Executive: inquiry list total 5 incl. M probe, by-id 200. showcase_private_note list total 1 = M's 'M private note', by-id 200. PATCH M's inquiry 403, manager's inquiry 403, M's note 403 (all PERMISSION_DENIED). Admin re-reads of all three unchanged (message/title/updated_at)
    access-security.scope-depth-asymmetry 3 pass Ops PATCH M's inquiry → 200. Admin re-read shows message 'ops edited M', updated_by = ops id U0B0U5…
    access-security.scope-depth-asymmetry 4 pass M by-id GET of manager's inquiry → 404 RECORD_NOT_FOUND. M list total 1 = ['M probe']. M's own by-id 200
    access-security.scope-depth-asymmetry 5 pass Outcome table: manager R=org/W=own, executive R=org (inquiry + private_note)/W=none, ops R=org/W=org, baseline M R=own/W=none. Matches access-matrix.json: showcase_manager inquiry readScope org/writeScope own/edit true; showcase_executive inquiry+private_note readScope org/edit false; showcase_ops inquiry readScope org/writeScope org; showcase_member_default inquiry read+create, no edit, no scope
    access-security.scope-depth-asymmetry neg pass Manager forged PATCH on M's inquiry → 403 and row unchanged. The same PATCH as ops → 200 and persisted (both sides of one guard)
    access-security.sharing-rules-widen 0 pass Audience member (bu_west_coast via sys_business_unit_member, get-session qa-r1-aud) list total 1 = [KmsAjtZm2oGETb-Y] (Priya Raman, status new). By-id 200
    access-security.sharing-rules-widen 1 pass Audience by-id: contacted wBT2O9… 404, closed qh6VW_… 404. Outsider by-id on the matching record 404, outsider list total 0
    access-security.sharing-rules-widen 2 pass sys_record_share shr_89666f92… on KmsAjtZm2oGETb-Y: recipient aud, access read, source 'rule', source_id share_new_inquiries_with_field_ops. POST /security/explain {userId: aud, recordId} → allowed true, record.decidedBy 'sharing'; layers: owd_baseline excluded, depth neutral, sharing 'widens' via sharing_rule share_new_inquiries_with_field_ops
    access-security.sharing-rules-widen 3 pass Before the share, tgt and third both get 404 on the contacted inquiry. Admin POST …/showcase_inquiry/wBT2O9…/shares → 201 source manual. Then tgt by-id 200 with list [wBT2O9…], while third gets 404 with list 0. Audience and outsider also still get 404 on it
    access-security.sharing-rules-widen 4 pass Five-stage table, aud list / aud by-id / out by-id / rule shares / ledger: baseline 1 [KmsAj…] / 200 / 404 / [shr_89666f92…] / no row. off (POST /security/_activation/… {enabled:false} → 200) 0 / 404 / 404 / [] / active false, updated_at 03:56:06.944Z. restart 0 / 404 / 404 / [] / active false, same 03:56:06.944Z. on ({enabled:true} → 200) 1 / 200 / 404 / exactly one [shr_f51231fc…] / active true, 03:56:35.322Z. restart 1 / 200 / 404 / [shr_f51231fc…] (same id) / active true, same 03:56:35.322Z. PATCH sys_record_share/‹id> → 405 OBJECT_API_METHOD_NOT_ALLOWED
    access-security.sharing-rules-widen neg pass No non-matching record reached the audience, no matching record leaked to the outsider, and nothing was dropped or re-armed across either restart. Position confound recorded per knownGaps: the manager-position member reads 32/32 contacts (the same count as admin), so depth decides and the rule does not
    access-security.record-share-grant-revoke 0 pass Before the grant, B by-id on A's note M1fNQLrMastekr9L → 404 RECORD_NOT_FOUND; B list total 0
    access-security.record-share-grant-revoke 1 pass A POST …/shares → 201 {recipient_type user, recipient_id B (BcQEZq…), access_level read, source manual, granted_by A (5nQRVp…)}. Admin GET sys_record_share/shr_f209d13e… → 200 with the same fields
    access-security.record-share-grant-revoke 2 pass B by-id 200, B list total 1 [M1fNQ…]. C by-id 404, C list total 0
    access-security.record-share-grant-revoke 3 pass A GET …/shares → 200 with the grant present. B POST …/shares (self, and to C) → 403 PERMISSION_DENIED "managing shares … requires record ownership or Modify All Data (ADR-0111 D1)". B GET …/shares → 403. C (invisible) POST → 404 NOT_FOUND
    access-security.record-share-grant-revoke 4 pass A DELETE …/M1fNQ…/shares/shr_f209d13e… → 204. Then B by-id 404, B list 0, admin GET of the share row 404
    access-security.record-share-grant-revoke 5 pass Run BEFORE the real revoke. DELETE via the second note's path (…/1m950rPSni8se-v8/shares/shr_f209…) → 404 "share … not found on showcase_private_note/1m950…". Via a showcase_inquiry path → 404. The share survived (admin GET 200) and B still read the note (200)
    access-security.record-share-grant-revoke 6 pass C bound to manager (sys_user_position 201). Evaluate → 200 {ruleId share_key_account_qualified_contacts_with_managers, matchedRecords 1, expandedUsers 2, grantsCreated 0, grantsUpdated 0, grantsRevoked 0, grantsRefused 0}. sys_record_share shr_04b0a579… on Nora West wpxfXnXekQEBmkpn: recipient C (SpB2rx…), source 'rule', source_id equals the body's ruleId
    access-security.record-share-grant-revoke neg0 pass After the revoke B gets 404. No leak to C at any stage
    access-security.record-share-grant-revoke neg1 pass B's share POST → 403; no 2xx
    access-security.owd-save-gate 0 pass PUT /meta/object/qa_owd_probe carrying externalSharingModel → 422 INVALID_METADATA, 1 issue at path externalSharingModel: "object.externalSharingModel was removed in @objectstack/spec 18 … Delete the key." The ?mode=draft twin gives the identical 422. The plain GET → 404 RESOURCE_NOT_FOUND and the ?state=draft GET → 404 NO_DRAFT. A GET with ?mode= → 400 (unsupported param). The same 422 appeared on the OS_METADATA_WRITABLE=object boot
    access-security.owd-save-gate 1 pass Same body without the key → 200 "Saved object 'qa_owd_probe' (env-wide, state=active) [seq=1]". Read-back sharingModel private, no externalSharingModel
    access-security.owd-save-gate 2 pass showcase_announcement (?layers=true: lock full, provenance package, editable false). Widen (public_read→public_read_write) → 403 NOT_OVERRIDABLE "…provided by a managed package and is sealed…", no declaredCode. With ?package=com.example.showcase → 403 ITEM_LOCKED. Tightened control (→private) gives the same two codes. The same four legs on the OS_METADATA_WRITABLE=object boot give the same codes. Answering layer: the package door, not R1. R1 pins: meta-object-owd-gate.test.ts 11/11 (engine + in-process route), object-posture-gate.test.ts 10/10 (plugin unit)
    access-security.owd-save-gate 3 pass GET /meta/object/showcase_announcement before and after the four refused PUTs is byte-identical (cmp clean, sha256 3a525e021c170c78…) on both boots
    access-security.owd-save-gate 4 pass showcase_account sharingModel public_read_write; showcase_announcement public_read; neither carries externalSharingModel
    access-security.owd-save-gate neg0 pass The server gate holds on the direct PUT (403, unchanged object), so the "direct PUT widens" premise of the fail never occurs. The Studio panel was not driven
    access-security.owd-save-gate neg1 pass No packaged-object PUT answered 2xx; the object is byte-identical
    access-security.packaged-flow-write-door-parity 0 pass ?layers=true shows lock full, provenance package, packageId com.example.showcase. PUT /meta/flow/showcase_urgent_task_alert (label + " (qa edit)") → 403 NOT_OVERRIDABLE "…sealed against in-place edits…". With ?package= → 403 ITEM_LOCKED "…packaged base is locked…". /meta GET byte-identical before and after
    access-security.packaged-flow-write-door-parity 1 pass Same admin session and same body: PUT /automation/showcase_urgent_task_alert → 403 NOT_OVERRIDABLE with the same lock message. /automation GET read-back byte-identical to the capture (sha256 79569037c073412a…)
    access-security.packaged-flow-write-door-parity 2 pass DELETE /automation/showcase_urgent_task_alert → 403 NOT_OVERRIDABLE "…sealed against removal…". The follow-up GET → 200 and is byte-identical
    access-security.packaged-flow-write-door-parity 3 pass Nothing mutated, so no restore was needed. The final GET is byte-identical to the capture. /automation/_status shows enabled true, bound true. An urgent showcase_task create (201) produced run run_4941ca5f…, status completed, trigger record-after-write, in 23 ms
    access-security.packaged-flow-write-door-parity neg0 pass Admin (holds manage_metadata) is refused with the package-lock message (NOT_OVERRIDABLE). A minted member is refused on the same PUT/DELETE with 403 PERMISSION_DENIED "Authoring automation flows requires the manage_metadata capability". The two tiers are distinct
    access-security.packaged-flow-write-door-parity neg1 pass No 2xx from PUT or DELETE on /automation
    search.rls-both-personas 0 pass Contributor-bound member (recipe, get-session qa-r1-contributor): premise by-id INV-1003 cpeX00… → 404, list 0 of the 12 seeded. Member query {search:'INV-1003'} → 200 records [] total 0. Admin same query → total 1 ['INV-1003']
    search.rls-both-personas 1 pass Member visible set = 3 own invoices (list total 3). Paged {search:'INV', limit 2}: offset 0 → total 3, hasMore true, 2 rows; offset 2 → total 3, hasMore false, 1 row; offset 4 and 6 → total 3, hasMore false, 0 rows. Admin's total for the same search is 15 (12 seed + 3), never seen by the member
    search.rls-both-personas 2 pass Member {search:'INV-QA-RLS'} → total 3 [INV-QA-RLS, -2, -3], the member's own rows
    search.rls-both-personas 3 pass Evidence set complete: the deny side (member 0 / admin 1), the allow side, and own rows (3). The count reconciles with the member's list at every offset. Mechanism per item: expandSearchOnAst ANDs into ast.where ahead of the security middlewares
    search.rls-both-personas neg pass No hit and no total inflation for the member from an invisible row
    studio-authoring.removed-key-save-door-refusal 0 pass 10 clean baseline drafts saved 200 in com.qa.rone first: object, hook, action, list view, form view, app, page, dashboard, flow, permission. Then 33/33 variants → 422 INVALID_METADATA, each matching the item's marked shape. 11 tombstones (issue at the key path; "was removed in @objectstack/spec N…", or for dashboard.refreshInterval "was renamed to refreshIntervalSeconds in @objectstack/spec 17"). 22 aged-out keys (unrecognized_keys at the PARENT path, e.g. fields.title, config, or root; message names the key and carries the upgrade-guide sentence). Run twice, 33/33 both times
    studio-authoring.removed-key-save-door-refusal 1 pass For all 33 rows in both runs, the ?state=draft&package= read after the refusal equals the baseline draft exactly
    studio-authoring.removed-key-save-door-refusal 2 pass Top-level zzz_junk_key → 422 INVALID_METADATA unrecognized_keys on view ("…on this view item record: zzz_junk_key…"), object, page and dashboard. No stored draft contains the key
    studio-authoring.removed-key-save-door-refusal neg pass No 200 for any removed key; nothing was stored or stripped
    api-backend.batch-transactional-discovery 0 pass GET /discovery capabilities.transactionalBatch = {enabled:true, description:…} (object, not bool); atomic cross-object [note, task(project=seeded id)] → 200 results[2]; filtered re-reads find note 1 and task 1 (filter control on a nonexistent title → 0 of 10 unfiltered)
    api-backend.batch-transactional-discovery 1 pass top-level atomic:false → 400 BATCH_NOT_ATOMIC "Cross-object batch is always atomic; use POST /data/:object/batch for non-atomic per-object batches"; re-reads 0 and 0
    api-backend.batch-transactional-discovery 2 pass [note, invoice{account,status,tax_rate} with no name, task] atomic → 400 VALIDATION_FAILED fields[0] {name, required}; re-reads note 0, task 0, invoice tax_rate=0.1 0; invoices unfiltered 12 → 12
    api-backend.batch-transactional-discovery 3 pass 201 creates → 400 BATCH_TOO_LARGE count 201 max 200; probe-titled rows 0; control: 200 ops → 200, 200 rows
    api-backend.batch-transactional-discovery 4 pass per-object [valid, no-title, valid] with continueOnError:true → 200 total 3 succeeded 2 failed 1, row1 VALIDATION_FAILED, re-read a=1 c=1; with no options → succeeded 1 failed 2, row1 VALIDATION_FAILED, row2 errors[0].code NOT_ATTEMPTED, re-read a=1 c=0. Supporting: explicit options.atomic:true → rows ROLLED_BACK / VALIDATION_FAILED / NOT_ATTEMPTED, succeeded 0
    api-backend.batch-transactional-discovery 5 pass /batch update id "" and no id → both 400 VALIDATION_FAILED "Operation 'update' on 'showcase_task' requires an id"; delete id "" and no id → both 400 VALIDATION_FAILED "…'delete'… requires an id"; per-object batch and updateMany id "" → row VALIDATION_FAILED "Record id is required"; deleteMany [""] → 400 VALIDATION_FAILED; boot log has 0 'Unhandled' lines
    api-backend.batch-transactional-discovery neg0 pass atomic:false refused (not accepted and ignored); the per-object door kept valid rows (a=1, c=1) when a sibling failed without atomic
    api-backend.batch-transactional-discovery neg1 pass "", missing, unknown, null and numeric ids → 400 or 404 at the door, never 5xx; 0 Unhandled in the log
    api-backend.error-envelope-ledger 0 pass ErrorCode.safeParse (spec dist) is true for all 7 codes: VALIDATION_FAILED, UNAUTHENTICATED, PERMISSION_DENIED, OBJECT_NOT_FOUND, RECORD_NOT_FOUND, BATCH_NOT_ATOMIC, UNSUPPORTED_QUERY_PARAM; the invented control R2_NOT_A_CODE is false. Std/reg split: 4 StandardErrorCode, 3 REGISTERED only
    api-backend.error-envelope-ledger 1 pass POST /data/showcase_invoice {} → 400 VALIDATION_FAILED fields[0] {field:name, code:required, message "Invoice Number is required", label "Invoice Number"} (fields[1] account/required); FieldErrorCode.safeParse('required') true
    api-backend.error-envelope-ledger 2 pass 'fieldErrors' substring absent from all 7 bodies and from the 18 probe bodies
    api-backend.error-envelope-ledger 3 pass floor codes: UNAUTHENTICATED 401, PERMISSION_DENIED 403 (HttpStatusErrorCodeMap reverse); family codes: VALIDATION_FAILED, BATCH_NOT_ATOMIC and UNSUPPORTED_QUERY_PARAM 400 (validation); OBJECT_NOT_FOUND and RECORD_NOT_FOUND 404 (not_found); no 5xx
    api-backend.error-envelope-ledger 4 pass 404 OBJECT_NOT_FOUND "Object 'not_a_real_object' is not registered" vs 404 RECORD_NOT_FOUND "Record nonexistent-id-0000 not found in showcase_task"
    api-backend.error-envelope-ledger 5 pass 7 families sampled: validation 400, auth 401 (anonymous), authz 403 (Mei, session proven), not-found object 404, not-found record 404, batch 400 BATCH_NOT_ATOMIC, query-rejection 400 UNSUPPORTED_QUERY_PARAM ($pageSize). Envelope: the REST flat dialect {error, code, …} on /data and /batch (the declared data-door dialect; ErrorResponseSchema's nested shape does not apply)
    api-backend.error-envelope-ledger neg0 pass no 5xx and no 2xx on any induced error; the 401 body carries message beside error
    api-backend.filter-comparand-conformance 0 pass ACCEPTED_FILTER_COMPARAND_TYPES = [string, number, bigint, boolean, null, Date] (6 = enumSource expect). REST POST /query and GET ?filter= over showcase_task (11 rows unfiltered): string $eq done 2/2, $in 5/5, number $gt 20 4/4, boolean true 2/2, null $eq 1/1, $ne null 10/10, Date wire (ISO) $lt 2026-11-10 4/4, bigint wire (number 2^53) 0/0; every answer set equals the locally computed ids. In process (engine + memory driver, schema synced): bigint 2n**53n → [d3], 10n → [d1], Date $gte 2026-03-01 → [d2], all equal to expectation
    api-backend.filter-comparand-conformance 1 pass plain object {a:1} in $eq, object in $in, nested no-operator object, array in $eq/$gt → 400 on POST, GET filter and GET $filter, never rows (unfiltered 11). Type-door messages name the path (where.estimate_hours.$eq) and quote "a string, number, bigint, boolean, null or Date". The array case names path and remedy ($in/$between) but not the sentence (accuracy note)
    api-backend.filter-comparand-conformance 2 pass (test oracle; REST not-applicable) engine door test "refuses an oversized bigint with the envelope, and NO driver read runs" and "narrows an exact-range bigint" ✓; 'a bigint beyond ±2^53 is refused — precision loss must not answer silently' ✓ in memory, sql, sqlite-wasm, turso and mongodb suites; own engine script: 2n53n+1n → 400 INVALID_FILTER quoting the sentence, 2n53n accepted. No REST probe is scored (JSON cannot carry a bigint)
    api-backend.filter-comparand-conformance 3 pass relation heads (f_lookup.name, f_master_detail.name, f_user.name, task project.name) → 400 INVALID_FIELD "follows the relationship…"; virtual f_formula.x → 400 INVALID_FIELD "virtual 'formula' field"; scalar name.x and f_number.x → 400 INVALID_FIELD "stores a single scalar value"; null class f_json.a and f_address.city → 400 INVALID_FILTER (SQL driver), never rows; both doors agree
    api-backend.filter-comparand-conformance 4 pass the same {a:1} comparand: POST /query 400 VALIDATION_FAILED fields[0].field query.where.estimate_hours.$eq; GET filter= and $filter= 400 INVALID_FILTER; engine find() 400 INVALID_FILTER. All quote the same sentence and none returns rows
    api-backend.filter-comparand-conformance neg0 pass no refused predicate dropped: every refusal is a 400; an empty where {} answers 11 (= unfiltered), as authored
    api-backend.filter-comparand-conformance neg1 pass string "16" on a number field → 200, 2 rows. This is the ruled numeric-string grammar (filter-number-comparand-declared-type.ts, #20336), not coercion: "abc" and " 16 " → 400 INVALID_FILTER; boolean "yes" → 400
    api-backend.filter-comparand-conformance neg2 pass the past-limit bigint is refused (engine and 5 drivers), never rounded
    api-backend.filter-comparand-conformance neg3 pass name.x and f_number.x → 400 INVALID_FIELD, not traversed
    api-backend.aggregate-contract-matrix 0 pass showcase_account 19 rows (14 stock + 5 seeded). Grouped by status and ungrouped equal the local computation for count, sum, avg, min, max and count_distinct (annual_revenue: active 12 distinct vs 13 count, thanks to a seeded duplicate; status: 1/1/1, ungrouped 3); count fieldless = count '*' = 19 (14/2/3) vs count(annual_revenue) = 18 (13/2/3), the null-revenue row discriminating. Boolean (field_zoo f_boolean, f_toggle) min 0 / max 1 numbers
    api-backend.aggregate-contract-matrix 1 pass clock 2026-11-02T04:00Z, zone UTC (TZ unset, /etc/timezone Etc/UTC). Keys and counts reconcile exactly: day 15, week 14, month 14, quarter 11, year 5 buckets, each with one JSON-null bucket of 4. Week: 2025-12-29 (Mon) and 2026-01-04 (Sun) share 2026-W01 (3 rows); 2027-01-01 → 2026-W53. /analytics/query showcase_delivery count by due_date/day: UTC (data.sql present) = America/Los_Angeles = Pacific/Auckland (in-memory, no sql) labels
    api-backend.aggregate-contract-matrix 2 pass ungrouped empty where → {count:0, count_distinct:0, sum:0, avg:null, min:null, max:null}; grouped empty where → 0 groups (as ruled); per-aggregation filter matching nothing → the same identities in each of 3 groups while sibling count/sum keep 14/3/2 and 609107000/25100000/47500000 (= baseline)
    api-backend.aggregate-contract-matrix 3 pass array_agg and string_agg → 400 VALIDATION_FAILED at query.aggregations.0.function, each message saying there is no replacement and ending byte-exactly with AGG_RETIRED_TAIL (os migrate meta … https://objectstack.ai/docs/protocol-upgrade.); median → 400 "expected one of count|sum|avg|min|max|count_distinct" with no tail; no 500
    api-backend.aggregate-contract-matrix 4 pass contributor-bound member (position contributor; the list answered 0 rows, not 403, before own rows): own baseline 2 invoices {draft:1, sent:1}, aggregate {draft:1, sent:1}; admin baseline {draft:3, sent:6, paid:5}, aggregate equal; the two differ
    api-backend.aggregate-contract-matrix 5 pass 11 variant rows recorded: 6 functions as discriminating; day, week (ISO, Monday start), month, quarter and year all answer-discriminating (multi-bucket; the week leg is ISO-vs-Sunday and ISO-year discriminating thanks to the seeded rows); DateGranularity hand count 5
    api-backend.aggregate-contract-matrix neg0 pass aggregations [] → 400 "is an empty list… NOT run"; groupBy and aggregations both [] → 400; no raw rows returned on any refusal
    api-backend.aggregate-contract-matrix neg1 pass sum(no_such_field) → 400 INVALID_FIELD "Unknown field… could only aggregate blanks"; field "" → 400
    api-backend.aggregate-contract-matrix neg2 pass identical day labels across UTC, LA and Auckland on a date dimension (driver and in-memory paths)
    api-backend.api-methods-verb-gate 0 pass GET /data/sys_oauth_consent (list and get) → 404 OBJECT_API_DISABLED "is not exposed via the API"; PATCH sys_session/:id → 405 OBJECT_API_METHOD_NOT_ALLOWED "API operation 'update' is not allowed on object 'sys_session'"; list sys_device_code → 405 allowed [get] (system objects)
    api-backend.api-methods-verb-gate 1 pass scratch qa_verb_gate_probe [get,list,create]: update/delete 405 allowed [get,list,create,aggregate,search,import,export] = effectiveOperationsArray(resolveEffectiveApiMethods(...)) from spec dist, element for element; +bulk → [get,list,create,bulk,aggregate,search,import,export] equal; sys_session [get,list,aggregate,export] equal to the closure with searchable:false
    api-backend.api-methods-verb-gate 2 pass unrestricted stock showcase_private_note: create 201, patch 200, createMany 201, delete 200; authored [] (qa_verb_empty) and stripped ['upsert'] → every op 405 allowed []; non-array "get" → PUT 422 INVALID_METADATA enable.apiMethods invalid_type (nothing stored, GET 404) + pin api-derivation.test.ts -t non-array 1 passed (pure spec function)
    api-backend.api-methods-verb-gate 3 pass without bulk: createMany and per-object batch → 405 naming 'bulk', count unchanged; with bulk: createMany 201 (+2 rows); updateMany, deleteMany and per-object batch update → 405 naming update/delete, row unchanged; import insert 200 created 1, import update and upsert (matchFields [name]) → 405 'update'; a malformed body → 400 INVALID_REQUEST from the parser (distinguished)
    api-backend.api-methods-verb-gate 4 pass ['upsert'] PUT 200 + log "[spec] … retired legacy value(s) [upsert] … upsert: declare ['create','update'] … ⚠ After stripping, this whitelist is EMPTY — [] means DENY-ALL" + [Registry] per-object line; ['get','history'] PUT 200 + "history: declare ['get'] with enable.trackHistory: true", runtime 405 allowed [get] and get → 404 RECORD_NOT_FOUND (passes the gate)
    api-backend.api-methods-verb-gate 5 pass Mei (session proven) PATCH sys_session (admin's and her own) → the same 405 body as admin; anonymous → 401 UNAUTHENTICATED; admin list 200 (3) and get 200, Mei list 200 (1, own) and get 200
    api-backend.api-methods-verb-gate 6 pass /batch [create task, update sys_session] → 405 "API operation 'bulk' is not allowed on object 'sys_session'"; reversed order the same; the task title re-read is 0 both times; control batch with only the create → lands 1
    api-backend.api-methods-verb-gate neg0 pass refused update/delete/createMany/batch left the row and count unchanged
    api-backend.api-methods-verb-gate neg1 pass allowed[] never echoes the raw whitelist; restore/purge are absent everywhere
    api-backend.api-methods-verb-gate neg2 pass not filed: no internal write was probed as a bypass
    api-backend.api-methods-verb-gate neg3 pass legacy ['upsert'] and ['get','history'] were accepted (200) with warnings, not parse-rejected
    ai.agent-tool-skill-metadata-roundtrip 0 pass valid trio: objectstack compile exit 0 with the expected agent-authoring-withdrawn warning on qa_probe_agent. 11 probes each exit 2 with no artifact: tool.permissions/active/category/builtIn/requiresConfirmation → unrecognized_keys + per-key guidance (permissions: "promised a capability gate on tool invocation that nothing ever enforced"); agent.tools, agent.knowledge, skill.triggerPhrases → "Unrecognized key(s)… ‹key>… see the protocol upgrade guide: https://objectstack.ai/docs/protocol-upgrade"; agent.lifecycle → "removed in @objectstack/spec 17.7.0… Run os migrate meta --from 17"; skill.permissions and skill.trigger → their guidance sentences + the guide pointer
    ai.agent-tool-skill-metadata-roundtrip 1 pass GET /meta and /meta/types list agent, tool and skill (37 types, no plurals); /meta entries carry a resolved JSON schema per kind (agent 23 props, skill 16, tool 14); agent allowRuntimeCreate false, skill and tool true, all three allowEnvironmentOverlay false
    ai.agent-tool-skill-metadata-roundtrip 2 pass list finds each qa_probe_* (_provenance package); get field-diff: 0 authored mismatches; added keys are only surface 'ask' + active true (agent, skill) plus _package* and _diagnostics bookkeeping
    ai.agent-tool-skill-metadata-roundtrip 3 pass PUT skill/qa_runtime_skill?mode=draft → 200 state draft, GET ?state=draft 200 (GET ?mode=draft → 400 VALIDATION_ERROR; active read 404); tool/qa_runtime_tool draft 200 and readable; package-shipped qa_probe_skill and qa_probe_tool → 403 NOT_OVERRIDABLE; agent new name → 403 NOT_CREATABLE, shipped qa_probe_agent → 403 NOT_OVERRIDABLE (recorded as correct)
    ai.agent-tool-skill-metadata-roundtrip 4 pass anonymous GET /meta/agent, /meta/tool, /meta/skill, /meta/agent/qa_probe_agent, /meta → 401 UNAUTHENTICATED
    ai.agent-tool-skill-metadata-roundtrip 5 pass POST /ai/tools/qa_probe_tool/execute → 501 NOT_IMPLEMENTED "Provided by @objectstack/service-ai in ObjectOS Cloud/Enterprise"; not ticked as callable
    ai.agent-tool-skill-metadata-roundtrip neg0 pass no tombstone probe built clean (11 of 11 exit 2)
    ai.agent-tool-skill-metadata-roundtrip neg1 pass every anonymous meta read → 401
    ai.mcp-run-action-exposure-gate 0 pass run_action showcase_mark_done (with and without objectName) → isError "Action 'showcase_mark_done' on 'showcase_task' is not exposed to AI — the app author must opt it in with ai: { exposed: true, description: … }"; REST re-read status, done, progress and updated_at are unchanged
    ai.mcp-run-action-exposure-gate 1 pass list_actions {} (strict; objectName → -32602 "Unrecognized key(s)… objectName"): showcase_mark_done absent, showcase_portfolio_snapshot present. Staged qa_ai_gated (requires manage_metadata) is present for admin (totalCount 38) and absent for Mei (37), while the snapshot is in both. The permitted half is discriminated, and this is the first live proof of the rev-6 recipe
    ai.mcp-run-action-exposure-gate 2 pass run_action showcase_portfolio_snapshot → {ok, scope global, accounts 14, projects 5, invoices 12}; REST totals 14 / 5 / 12
    ai.mcp-run-action-exposure-gate 3 pass log "[action-audit] MCP run_action 'showcase_portfolio_snapshot' on 'global' — body executes TRUSTED (system-elevated context, RLS/FLS-bypassing) for user 'Lf78VIZT…'" = the admin's get-session id
    ai.mcp-run-action-exposure-gate 4 fail (stale spec) sys_* is no longer refused wholesale (ruling 6513869839 on #27681; ADR-0011 addendum, ruling 6523087116 on #27900). list_actions now lists 36 sys_* actions that declare ai.exposed. Only a non-exposed sys action is refused, in the new spellings: create_api_key with objectName → 'Action "create_api_key" on system object "sys_api_key" is not exposed via MCP: …ai.exposed… list_actions lists it for you'; without objectName → "Action 'create_api_key' is on a system object ('sys_api_key') and is not exposed via MCP — …". Neither of the clause's spellings appears. Nothing executed (sys_api_key rows 2 → 2). Reproduced on 3 fresh boots (41325, 41326, 41327)
    ai.mcp-run-action-exposure-gate neg0 pass the mark_done refusal left the task unchanged; the create_api_key, set_user_password and approval_approve refusals created or changed nothing; keys unrevoked
    ai.mcp-run-action-exposure-gate neg1 fail (product defect) list_actions under an osk_ API key lists sys_api_key.revoke_api_key with NO unavailable marker (the 35 identity-route actions carry unavailable UNAUTHENTICATED). run_action revoke_api_key + confirm:true then refuses: 403 PERMISSION_DENIED "Changing an API key requires a signed-in session… a request authenticated with an API key cannot revoke, restore or change API keys", and the key stays unrevoked. Reproduced on 3 fresh boots
    api-backend.formula-gates 0 pass harness resolved @objectstack/formula → /home/claude/objectstack/packages/formula/dist/index.js (dist; no src newer than dist). celEngine.evaluate: record.due_date == today() → true for '2026-11-02', false for '2026-11-01' (now=2026-11-02); record.flag ? record.x : null → 42 / null; floor(3.7)→3, ceil(3.2)→4, floor(-3.2)→-4. ALL PASS
    api-backend.formula-gates 1 pass scratch scaffold + object qa_doctor_time_off, formula qa_days_span = … ? (record.end_date - record.start_date) + 1 : null. os build exit 1: "✗ Author-time rules failed (1 issue)"
    api-backend.formula-gates 2 pass error text: object 'qa_doctor_time_off' · field 'qa_days_span' expression: date arithmetic \google.protobuf.Duration + int` — `record.start_date` is a date … Use `daysBetween(a, b)` …, rule expression-invalid`
    api-backend.formula-gates 3 pass untouched showcase (scratch copy) os build exit 0, "✓ Build complete", 65 author-time warnings, artifact 743.0 KB. The only today() use is predicate-matrix.action.ts:523 record.f_date < today()
    api-backend.formula-gates neg pass the resolved module path is in dist (above), never src
    api-backend.enforce-or-remove-authoring-gates 0 pass Built spec (dist/{index,data,api}/index.js) safeParse rejects query.cursor, query.joins, query.distinct, query.windowFunctions and EnhancedApiError.fieldErrors (each invalid_type at its path); routes.includeObjects is rejected too. The control fields parses clean. Filter registry: a widget filter: { name: { $regex: 'acme' } } in ops-dashboard (scratch) builds exit 0 and is stored verbatim; FilterConditionSchema parses $regex/$options clean. Live POST /api/v1/data/showcase_project/query {"where":{"name":{"$regex":"acme"}}} → 400 code: INVALID_FILTER "Filter operator "$regex" on field "name" is RETIRED … Write "$icontains" instead …" ($regex+$options and $options alone are refused the same way; the $icontains control → 200). os migrate meta --from 16 prints ⚠ [protocol 17] data.filter $regex / $options — …; --json todos[].id includes filter-regex-options-retired
    api-backend.enforce-or-remove-authoring-gates 1 pass Each message names its replacement: cursor → "where predicate on your sort key — where: { created_at: { $gt: … } }"; joins → "read through expand"; fieldErrors → "renamed to fields"; includeObjects → "enable.apiEnabled / enable.apiMethods"; $regex → "Write "$icontains" instead … or $contains" (registry to: '$icontains')
    api-backend.enforce-or-remove-authoring-gates 2 pass RestServerConfigSchema.safeParse({routes:{someUnknownKey:1}}) → success, routes reads back {}. RouteGenerationConfigSchema docblock (rest-server.zod.ts:648–665): "an authored key is refused with its prescription instead of being silently stripped (the schemas are not .strict())". The same sub-object's includeObjects fails with "routes.includeObjects was removed in @objectstack/spec 17 …"
    api-backend.enforce-or-remove-authoring-gates 3 pass Sample by registry: query surface (QuerySchema: cursor/joins/distinct/windowFunctions), error envelope (EnhancedApiErrorSchema: fieldErrors), filter operators (RETIRED_FILTER_OPERATORS: $regex, $options), plus REST config (RouteGenerationConfig: includeObjects)
    api-backend.enforce-or-remove-authoring-gates neg pass the documented-strip control was run (clause 2). No tombstoned key parsed clean anywhere it was sampled
    api-backend.retired-def-refusal 0 pass Baseline scaffold build exit 0. Each sampled def, in its own scratch stack, exits 2: data/ExternalDataSource (import), automation/ScheduleState (import), identity/ApiKey (import), kernel/PluginLoadingConfig (manifest.loading), automation/StateMachine (agents[].lifecycle), ui/Theme (themes stack key). No green build
    api-backend.retired-def-refusal 1 pass Control qaInventedFamily → "(root): Unrecognized key(s) on this stack definition … see the protocol upgrade guide". Carriers that authored a prescription differ from the control and name the successor: agent.lifecycle → "removed in 17.7.0 … skill … Flow … state_machine validation rule"; themes → "removed in 17.1 … set app.branding.primaryColor". Route-3 / zero-consumer defs (ExternalDataSource, ScheduleState, ApiKey) get the generic module refusal "does not provide an export named …", which is correct per knownGaps[0]. manifest.loading gets the generic strict "Unrecognized key(s) on this package manifest: loading … see the protocol upgrade guide". Its 16.x kit aged out at 18 under the #25767 ruling (ADR-0087 amendment D2=A), and the prescription now lives in content/docs/protocol-upgrade/17.md, so the generic answer is the correct one (see Checklist-accuracy). json-schema/{data/ExternalDataSource, automation/ScheduleState, automation/StateMachine, kernel/PluginLoadingConfig, ui/Theme, identity/ApiKey}.json are absent; data/ has 174 other files
    api-backend.retired-def-refusal 2 pass Live door (admin, 41341). PUT /meta/agent/qa_agent with lifecycle → 403 NOT_CREATABLE "code-only". PUT /meta/manifest/…, /meta/external_data_source/…, /meta/schedule_state/…, /meta/api_key/…, /meta/theme/… and invented /meta/qa_invented_family/… → 400 INVALID_REQUEST "'X' is not a metadata type", the same as the control. POST /meta/external_data_source → 405. /meta/types lists none of them. Under OS_METADATA_WRITABLE=agent (41342), an agent without lifecycle → 200, and with lifecycle → 422 INVALID_METADATA carrying the build's exact prescription; the GET answers 404 (not stored). No door disagrees with the build
    api-backend.retired-def-refusal 3 pass Sample: prefix 17 = data/ExternalDataSource, kernel/PluginLoadingConfig; prefix 18 = automation/ScheduleState, automation/StateMachine, ui/Theme, identity/ApiKey. Domains: data, kernel, automation, ui, identity. Registry re-measured: 247 entries (53 at 17, 194 at 18), PROTOCOL_VERSION '18.0.0'
    api-backend.retired-def-refusal neg0 pass no sampled def parsed clean or built green
    api-backend.retired-def-refusal neg1 pass every live authored prescription (lifecycle, themes) is surfaced. loading's aged out by ruling and points to the upgrade guide, which carries it
    api-backend.retired-def-refusal neg2 pass build and runtime doors agree (clause 2)
    api-backend.retired-def-refusal neg3 pass named successors match the registry and semantic entries (skill/Flow/state_machine; app.branding)
    cli.build-own-contract 0 pass clean scaffold os build exit 0; summary "Data: 1 Objects 2 Fields … Artifact: dist/objectstack.json (2.5 KB)"; the artifact parses. --json exit 0 carries success:true, output, size:2540, stats{…}, warnings:[], conversions:[], duration (plus bodyExtractionWarnings, handlersBundled, runtimeModule, protocolVersionGap)
    cli.build-own-contract 1 pass Plain-literal object qa_doctor_broken with field body and no type. Human run: exit 2, "✗ defineStack validation failed (1 issue): ✗ objects.1.fields.body.type: Invalid field type ''…". --json run: exit 1, {success:false, error:…objects.1.fields.body.type…, code:"STACK_SCHEMA_INVALID"}
    cli.build-own-contract 2 pass A flow with two broken expression approvers (record.owner ==, record.title &&) exits 1 on both paths and reports "✗ Author-time rules failed (2 issues)" in one run. Each finding has where / message / hint / rule approval-expression-invalid / path flows[0].nodes[1|2].config.approvers[0].value
    cli.build-own-contract 3 pass Build and validate --json payloads were paired per fixture and diffed on [warnings, conversions]; they are IDENTICAL for clean, schema (both carry code STACK_SCHEMA_INVALID), rule (build issues, validate errors) and load-throw. Live conversion: with NODE_OPTIONS=--import …/live-conversion-probe.mjs and a probe_old_callout node, both payloads carry the same record {code:OS_METADATA_CONVERTED, conversionId:cli-live-conversion-probe, from:probe_old_callout, to:http, path:flows[0].nodes[1].type, …}
    cli.build-own-contract 4 pass Exit codes per fixture, run twice each, all identical: clean human 0/0, json 0/0; schema human 2/2, json 1/1; rule human 1/1, json 1/1; advisory 0/0 and 0/0; load-throw (throw new Error in config) human 2/2, json 1/1. No other code was seen. os compile gives the same codes and payloads (JSON identical without duration; human output identical without timing lines)
    cli.build-own-contract 5 pass Raw field extra.visibleWhen = {dialect:'cel', source:…, qaUndeclared:1} under defineStack(…, {strict:false}): the block "⚠ Undeclared authoring keys (1) — dropped at load; reported here, never refused" prints, exit 0, and the artifact visibleWhen is {"dialect":"cel","source":"record.title != null"} (key gone). The --json warnings array carries the same line. Under the default defineStack only the one-line defineStack: warning prints, with exit 0
    cli.build-own-contract neg0 pass the clean run wrote and parsed the artifact. No nonzero exit on the clean config
    cli.build-own-contract neg1 pass no exit tracked a computed value; repeats were stable (clause 4)
    cli.hook-body-extraction-gates 0 pass FORBIDDEN_PATTERNS recounted at this base: 13 (7 escapes + .sudo/.create/dispatch.scope/submitted/ql/transaction). Human --strict-body exits 1 with "✗ --strict-body: 13 callable(s) lack a metadata body" and one line per hook quoting its own reason (fetch and import: "declare a Connector recipe instead"; each host member names itself and its alternative, e.g. .create( → "Spell the same payload .insert({ … })", ctx.ql → "ctx.api.object(name)", ctx.transaction → "ctx.api.transaction(fn)"). --json exits 1 with {success:false, error:"strict-body: missing body", issues:[13 × {origin,kind,reason,freeIdentifiers}]}
    cli.hook-body-extraction-gates 1 pass jq body.capabilities: qa_i_find ["api.read"], qa_i_update ["api.write"], qa_i_alias (const api = ctx.api) ["api.read"]
    cli.hook-body-extraction-gates 2 pass qa_i_uuid ["crypto.uuid"], qa_i_log ["log"], qa_i_hash (ctx.crypto.hash only) []
    cli.hook-body-extraction-gates 3 pass data body qa_d_caps ships ["api.read","log"] verbatim. qa_i_comment (TS handler with // @capabilities api.write, log) ships ["api.read"] only. qa_d_comment (data body whose source string keeps the comment, no capabilities) ships [] with the source intact. The build exits 0 and prints nothing about either
    cli.hook-body-extraction-gates 4 pass Default build: exit 0, "1 handler could not be lowered … qa_free_helper: handler references identifier(s) not in scope at runtime: helperFn", jq {body:null, handler:"qa_free_helper"}, bundle objectstack-runtime.2283156a1d78c903.mjs emitted. --strict-body: exit 1, same message; --json freeIdentifiers ["helperFn"]
    cli.hook-body-extraction-gates 5 pass A stale objectstack-runtime.11daba633880d173.mjs was placed in dist, then the all-body-only config built: "→ Skipping legacy runtime bundle (all 8 callables are body-only)". dist then holds only objectstack.json, and runtimeModule is null
    cli.hook-body-extraction-gates 6 pass Default build over the 13 forbidden hooks: exit 0, "⚠ 13 handlers could not be lowered to a metadata body — bundled via the legacy runtime module instead (build still succeeds)" plus 13 per-hook reason lines; "Bundling 13 handlers"; runtime .mjs emitted. --json exit 0 with success true, bodyExtractionWarnings 13 records and warnings []. jq shows all 13 hooks body:null with a handler ref
    cli.hook-body-extraction-gates neg0 pass no forbidden hook carries body or body.source in the artifact
    cli.hook-body-extraction-gates neg1 pass hash-only hook caps []
    cli.hook-body-extraction-gates neg2 pass strict exits 1 with worded reasons, never a bare parse error
    cli.hook-body-extraction-gates neg3 pass the alias is caught (["api.read"])
    cli.doctor-health-report 0 pass Scaffold plain and -v runs both exit 0 and carry rows Node.js, pnpm, TypeScript, Dependencies, Git, Environment files, NODE_ENV (unset → row; absent when NODE_ENV=development), and Package-authored scheduled work OFF. No @objectstack/spec row in the scaffold (0 hits). The monorepo-root run has "✓ @objectstack/spec Built". Tenancy posture appears only on isolatd (1 hit; 0 on clean and valid-isolated runs). Installed packages appears only with broken.json present (0 otherwise). The config block opens "✓ Platform protocol Declared engines.protocol covers the installed platform". -v adds only detail lines (diff = the indented → lines)
    cli.doctor-health-report 1 pass .env = OS_TENANCY_POSTURE=isolatd, nothing in the shell: the ✗ Tenancy posture finding fires (exit 1); "✓ Environment files .env (node_env=production) … — OS_TENANCY_POSTURE from .env"; -v "• OS_TENANCY_POSTURE from .env". Supporting pin doctor-env-provenance.test.ts passed (unit layer, part of the 69 passed)
    cli.doctor-health-report 2 pass OS_TENANCY_POSTURE=isolatd: "✗ Tenancy posture OS_TENANCY_POSTURE="isolatd" is not a recognized tenancy posture" with the allowed set, then the Test coverage/Deprecations rows and the whole config block still print; "❌ Some critical issues found"; exit 1
    cli.doctor-health-report 3 pass broken.json (non-JSON) with no posture set: "⚠ Installed packages 1 installed-package ledger entry could not be read (those packages NOT checked) — broken.json: Unexpected token …", exit 0
    cli.doctor-health-report 4 pass isolated + broken.json: "→ Checking unique scopes against the 'isolated' …" with no "Unique scope" line. With the entry removed: "✓ Unique scope No unconfirmed installation-wide uniques for this 'isolated' environment"
    cli.doctor-health-report 5 pass Staged: "⚠ Circular dependency: qa_doctor_a → qa_doctor_b → qa_doctor_a", "⚠ Object "qa_doctor_unused" is defined but not referenced…", "⚠ View "qa_missing" references non-existent object "qa_missing"". Clean (one view over qa_doctor_note): "✓ Dependencies No circular references detected", "✓ Object usage All objects are referenced", "✓ View integrity All views reference valid objects"
    cli.doctor-health-report 6 pass Warnings-only runs (baseline, staged, ledger) → exit 0 with "⚠️ Environment is functional but has some warnings". Posture runs (shell and .env) → exit 1 with "❌ Some critical issues found" plus the fix lines. The clean run → exit 0 with "✅ Environment is healthy"
    cli.doctor-health-report 7 pass Scaffold: "ℹ Test coverage Skipped — no packages/spec/src in this directory (monorepo-only check)" and the same for Deprecations. -v adds "→ Looked for /tmp/w4c3a/R3/doctor/qa-doctor/packages/spec/src and found nothing to walk. …" under each. ls packages/spec/src → No such file. There are no ✓ lines for these scans and no "→ Checking for missing test files" step, and the baseline summary is driven only by the Object usage ⚠. Monorepo root: "→ Checking for missing test files..." with ⚠ Missing test rows, and "→ Scanning for @deprecated usage..." with ⚠ Deprecated rows
    cli.doctor-health-report neg0–neg5 pass neg0: the posture is an ERROR row with exit 1, not swallowed. neg1: no Unique scope line under the broken ledger. neg2: warnings exit 0 and errors exit 1. neg3: no "@objectstack/spec Not built" row in the user app. neg4: skip rows are ℹ, not ⚠. neg5: skip rows are present by name
    cli.verify-verdict-exit-mapping 0 pass stock showcase (scratch copy) verify --json exit 0: 24 results, statuses {verified 15, needs-fixture 1, skipped 8}, nothing out of vocabulary. The staged fail app adds fidelity-gaps and create-failed. The --rls and isolated reports have the same vocabulary
    cli.verify-verdict-exit-mapping 1 pass Recomputed from results[] (summary reconciles every time). Stock: hardFailures=0 → exit 0 (bare, --rls, isolated-provisioned). Staged fail app (4 objects: hook mutating title → fidelity-gaps; hook throwing → create-failed 400 HOOK_ABORTED; always-false validation → needs-fixture): recomputed 2 = report.hardFailures 2 → exit 1. Repeated on a second run: exit 1. Human run "✗ verify FAILED — 2 runtime failure(s)", exit 1
    cli.verify-verdict-exit-mapping 2 pass showcase_account → needs-fixture, code 400, "code":"VALIDATION_FAILED", on an exit-0 run. qa_doctor_strict (400 VALIDATION_FAILED) → needs-fixture and is excluded from the 2 hard failures. A non-validation create (400 HOOK_ABORTED) → create-failed and counted. The isValidation branch (verify.ts:110) sends everything that is not a 400 VALIDATION_FAILED, a 5xx included, to create-failed. A live 5xx was not staged (see Fixture gaps)
    cli.verify-verdict-exit-mapping 3 blocked{by: fixture, ref: no stageable rls-hole — a hole is a by-id write past RLS, i.e. a live platform defect; knownGaps stage only the CRUD fail side} Observed halves: --rls --json exit 0 carries an rls section (totals 264 objects, 38 PROVEN, 0 holes, 10/10 positions). Separate stack: the --rls stderr shows 2 server starts and 2 graceful shutdowns, against 1 and 1 for a bare run. The recomputed sum with the rls terms matches report.hardFailures. Not observed: a hole → exit 1
    cli.verify-verdict-exit-mapping 4 pass OS_TENANCY_POSTURE=isolated verify --json → exit 1, {error:"✖ FATAL: tenancy posture 'isolated' is walled, but this deployment never declared a membership policy…", code:"WALLED_MEMBERSHIP_POLICY_UNDECLARED"} (group gives the same). With OS_AUTH_MEMBERSHIP_POLICY=invite-only OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai → exit 0, multiTenant:true, the same 15/1/8 summary. There was never a quiet single-org run
    cli.verify-verdict-exit-mapping neg0 pass no exit-0 run carried a hard failure. The staged gaps/create-failed run exits 1
    cli.verify-verdict-exit-mapping neg1 pass a walled posture is a loud error or multiTenant:true

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

    Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

    Per-clause verdicts, part 2 of 3 (cli migrate and qa suite, identity-auth, records-forms). Clause numbers are acceptance indices; neg / negN are the item's negative list. Evidence is text oracle output only.

    item clause verdict evidence (text oracle)
    cli.migrate-plan-apply-json 0 pass A field qa_r4_note was added to showcase_category, then os build, then bare os migrate twice. Both exited 0. The report sections are byte-identical except the elapsed-ms line, and both plan + showcase_category [add_columns: qa_r4_note] plus the 7 lifecycle create_table entries (7 table(s) to create, 1 column(s) to add). DB digests taken before, between and after are identical: sqlite_master 422 a242576e…, rows 71f0365e…, sequences e60816a2….
    cli.migrate-plan-apply-json 1 pass os migrate plan --json exits 0. stdout is one JSON document with keys changes, composition, database, databaseSource, duration, managedTables(76), nullableTenantColumns, pending(8: the add_columns plus 7 create_table), total(0), unmanagedTables.
    cli.migrate-plan-apply-json 2 pass os migrate --help lists 22 subcommands: account-issuer, activity-changed-fields, apply, audit-key-material, audit-mail-bodies, audit-metadata-bodies, comment-parent-pair, comment-reactions, duplicates, files-to-references, meta, multi-value-columns, organization-ownership, packageless-overlays, plan, record-currency-codes, recorded-by, resume, security-catalog-overlays, summary-nulls, unmapped-columns and value-shapes. Every successful --json run printed one JSON document and exited 0: plan; apply --json --yes (×4); recorded-by; resume; account-issuer; activity-changed-fields; audit-key-material; audit-mail-bodies; audit-metadata-bodies; comment-parent-pair; comment-reactions; files-to-references; multi-value-columns; organization-ownership; packageless-overlays; security-catalog-overlays; summary-nulls; unmapped-columns --object showcase_category; value-shapes; meta --from 17; record-currency-codes --object showcase_category --field qa_amount --code USD (with a record-mode currency field added to the scratch, see the fixture gaps); and duplicates run bare. Two invocations exited 1, and both were refusals rather than successes: bare meta --json (missing_from_major) and record-currency-codes on the stock showcase_project.budget (INVALID_FIELD: not a record-mode field). DB digests were identical before and after the sweep.
    cli.migrate-plan-apply-json 3 pass The field was removed and the app rebuilt. apply --yes printed ✗ showcase_category.qa_r4_note [drop_column] and ⚠ 1 destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them)., and the column was still present. apply --allow-destructive --yes printed ✓ Applied 1 change(s)., and the column was gone. The JSON twin on qa_r4_b: skipped:[drop_column qa_r4_b] with message nothing_safe_to_apply and exit 0, then applied:[drop_column qa_r4_b] with exit 0.
    cli.migrate-plan-apply-json 4 pass The post-apply plan printed ⚠ No drift over the 76 object(s) this plan examined — but 2 of 78 declared object(s) were NOT examined … This is a PARTIAL plan. apply --json --yes gave {applied:[],skipped:[],created:[],message:"in_sync_partial",composition.coverage:{registeredObjects:78,examinedObjects:76,unexaminedObjects:2,reasons:{federated:2,…}}} and exit 0.
    cli.migrate-plan-apply-json 5 pass With os dev holding the DB (pid 22919), apply --yes exited 1 with ✗ …/migrate.db is in use — it is open in pid 22919 (node) (migrate.db-wal, migrate.db-shm present). and the --force hint. The JSON form exited 1 with {error:"database_busy",signal:"file_open"}. plan exited 0 with ⚠ … is in use … The plan below is still accurate — it writes no row or table and listed the pending + showcase_category [add_columns: qa_r4_d]. The column was still absent after the refused apply.
    cli.migrate-plan-apply-json 6 pass A pending add_columns qa_r4_b, then apply --json without --yes, gave {…,pending:[add_columns qa_r4_b],message:"confirmation_required",hint:"pass --yes"} with exit 1 (a refusal, not a success). DB digest unchanged, and a follow-up plan --json still listed the pending add.
    cli.migrate-plan-apply-json neg0 pass The drop was not performed without --allow-destructive: the column was still in sqlite_master after the first apply.
    cli.migrate-plan-apply-json neg1 pass The busy apply exited 1, not 0. No successful --json run in the sweep exited nonzero.
    cli.migrate-duplicates-inventory 0 pass The seeded run exited 0 and report.json parses as one document. Top level: report:"duplicate-identifiers", reportVersion:2, generatedAt, database:"/tmp/w4c3a/R4/dup/dup.db", globalPartition:"__global__", filter:null, counters:{table:"_objectstack_sequences",status:"read"}, plus scanned[2], skipped[], duplicates[1], liveConditions[1] and runtimeIndexPreflight[4]. summary{objectsScanned:1,fieldsScanned:2,duplicateValues:1,duplicateRows:2,liveConditions:1,runtimeIndexesBlocked:1,runtimeIndexBlockingRows:2}. The duplicate carries object, field, value, holderCount:2 and partitions, and each holder carries id, organization (null for the NULL-org row), partition and createdAt. The duplicates.contract.test.ts pin is green (3 files, 18 tests, in-process command over a real sqlite seam).
    cli.migrate-duplicates-inventory 1 pass Cross-partition: f_autonumber 0001 is held by qa_null_org_row (organization NULL, partition __global__) and CsjtAD5_… (org_mvyqf398…), and it is reported with both partitions. Within-partition: qa_auto2 0002 is held twice in org_mvyqf398… after uniq_showcase_field_zoo_organization_id_qa_auto2 was dropped, and it appears nowhere in .duplicates.
    cli.migrate-duplicates-inventory 2 pass Dump digests before and after a full run: sqlite_master 140afd47…/140afd47…, rows 1937e602…/1937e602…, _objectstack_sequences 6a00d596…/6a00d596…. Whole-file md5 of dup.db, -wal and -shm was also identical. The duplicates.pre-repair.test.ts pin is green.
    cli.migrate-duplicates-inventory 3 pass The index was already in the declared NULL-distinct form UNIQUE (type,name,package_id,bundle_member), so no swap was needed. Two active view rows qa_dup_view were seeded with null package_id and bundle_member and body {name,label,object,list:{columns:['name']}}. .runtimeIndexPreflight shows idx_sys_metadata_overlay_active status:"blocked", groups:[{key:{type:"view",name:"qa_dup_view",package_id_key:"",bundle_member_key:""},rowCount:2}]. No entry names sys_view_definition. The same DB's plan --json (exit 0) has 0 hits for idx_sys_metadata_overlay_active or qa_dup_view on stdout. stderr carries Overwriting view: qa_dup_view ×1 and Registered view: qa_dup_view ×2. Matched control: plan does report the within-partition repeat as index_mismatch uniq_showcase_field_zoo_organization_id_qa_auto2, actual "(absent)", BLOCKED with × 2 rows. ⚠ The preflight now has 4 entries from 3 kernel:ready migrations (overlay_active and overlay_draft, uniq_sys_setting_…, uniq_sys_notification_preference_…), not the clause's "three, two migrations" (see the checklist-accuracy findings).
    cli.migrate-duplicates-inventory 4 pass Seeded run: liveConditions:[{object:"showcase_field_zoo",field:"f_autonumber",globalLastValue:5,organizationCounters:[{organization:"org_mvyqf398…",lastValue:2}]}]. After deleting the org counter row: liveConditions:[] and .duplicates unchanged (jq -S diff empty). With the sequences table renamed away: counters.status:"absent", and the duplicates inventory was still 1 (complete).
    cli.migrate-duplicates-inventory 5 pass .filter read null on the full run, {object:"showcase_field_zoo"} on the filtered run (1 duplicate), and {object:"showcase_category"} on the no-findings object (scanned 0, duplicates 0).
    cli.migrate-duplicates-inventory 6 pass --database-url file:…/dup.db with OS_DATABASE_URL unset gave database:"/tmp/w4c3a/R4/dup/dup.db", and its duplicates are identical to the env-var run's. With column qa_auto2 renamed away, .skipped:[{object:"showcase_field_zoo",field:"qa_auto2",reason:"duplicate probe failed: The database refused to run a raw statement…"}].
    cli.migrate-duplicates-inventory 7 pass Pin duplicates.null-seam.test.ts passed (in-process command over a seam double, which is the item's prescribed evidence). Live: os migrate duplicates --database-url memory://qa exited 1 with {error:"boot_failed",detail:"[StandaloneStack] A database URL with the \"memory://\" scheme selects the in-memory (mingo) engine, which is no longer a boot store…"}.
    cli.migrate-duplicates-inventory neg0 pass No write: the three dump pairs and the file md5 are identical, and plan left the digest unchanged.
    cli.migrate-duplicates-inventory neg1 pass The within-partition qa_auto2 0002 repeat is absent from .duplicates.
    cli.migrate-duplicates-inventory neg2 pass On the skip run, .skipped names showcase_field_zoo.qa_auto2 with a reason beside a non-empty duplicates list. Nothing is summarised as "no duplicates".
    cli.qa-suite-execution 0 pass Bare os test --url http://localhost:41362 --token … from examples/app-showcase printed Found 1 test suites., ✅ … [health-liveness] (22ms), ✅ … [category-create-read-roundtrip] (120ms) and SUCCESS: All 2 scenarios passed., and exited 0 (wall 2.7 s). The second run was green the same way. The showcase_category ids are the same 4 before and after both runs, so nothing is left over.
    cli.qa-suite-execution 1 pass Audit read: the create and delete of minOuYZyCzp565Iq (run 1) and C5FmYTw-PLf4Z0c2 (run 2) are on the same id. A staged no-teardown copy of the roundtrip passed, and the record QA R4 capture probe id 3UIK4OtttvrYHHc1 was found by an independent list. GET by that id returned 200, and the audit create shows the same id. The teardown probe's {{cid}}-built GET returned got YrIJJjaQiv3wPqtu, which equals the audit create id, so the URL was interpolated and not literal.
    cli.qa-suite-execution 2 pass ❌ … [health-degraded] with Assertion failed: data.status expected degraded, got ok under Step Failed: GET health expecting degraded. The three-suite run exited 1.
    cli.qa-suite-execution 3 pass ❌ … [contains-nope]: data.nope cannot be evaluated by 'contains' — expected an array or a string at that path, got undefined. … Use 'is_null' if asserting absence is what you meant.
    cli.qa-suite-execution 4 pass qa/a-malformed.test.json is not a valid Quality Protocol suite (TestSuiteSchema):, ✗ scenarios.0.steps: Invalid input: expected array, received undefined, the expected shape, and Reference: content/docs/references/qa/testing.mdx. No Scenario: line follows it. Both sibling suites printed 📄 Running suite: and their Scenario: lines. Summary FAILED: 3 scenarios failed. 0 passed. with exit 1, and no TypeError.
    cli.qa-suite-execution 5 pass Staged create → capture → false assertion → teardown DELETE {{cid}}. The scenario stayed ❌. The category list was identical before and after (4 stock rows), and the audit shows create and then delete of YrIJJjaQiv3wPqtu.
    cli.qa-suite-execution 6 pass One scenario per member against pre-created ids: create_record ✅, update_record ✅ (API read-back name QA R4 sweep upd CHANGED), delete_record ✅ (GET afterwards gave 404), read_record ✅, query_records ✅ (direct POST …/query with the same where returned n=1 with the same id, so the predicate was honoured), api_call ✅, wait ✅ (200 ms), and run_script ❌ Unsupported action type in HttpAdapter: run_script. Run total: FAILED: 1 scenarios failed. 7 passed. No member answered 404.
    cli.qa-suite-execution 7 pass Bare os test from the app dir found the shipped suite. Quoted qa/**/*.test.json gave Found 1 test suites., SUCCESS and exit 0 with 2.9 s wall. Order probe (created zz, mm, sub/bb, aa) ran order-aa, order-mm, order-bb (sub/), order-zz, which is sorted.
    cli.qa-suite-execution neg0 pass os test 'qa/nothing-matches-*.test.json' printed Found 0 test suites. and No test files found matching: qa/nothing-matches-*.test.json and exited 0 ("Exiting 0 — an empty match is not a failure. Pass --fail-on-empty…"). With --fail-on-empty it exited 1. The green-path verdict quotes Found 1 test suites. with N > 0.
    cli.qa-suite-execution neg1 pass The malformed suite was refused at load, did not reach the runner, named the file and raised no TypeError (clause 4).
    cli.qa-suite-execution neg2 pass contains over data.nope came back ❌, not ✅ (clause 3).
    cli.qa-suite-execution neg3 pass All five record-shaped types round-tripped against the stock server with no 404 (clause 6). Persona check with no --token: health-liveness ✅, while the roundtrip ❌ HTTP Error 401: {"error":"UNAUTHENTICATED",…} with exit 1.
    cli.migrate-meta-codemod 0 pass Registries: retired-defs 247 (17:53, 18:194), retired-keys 290 (17:29, 18:261), semantic 523 (17:77, 18:446). Shipped messages name --from 16 and --from 17. Planted as raw text: ui Dashboard.refreshInterval: 300 (ops dashboard), ui ListView.sort: 'estimate_hours desc' (task view) and data Object.externalSharingModel: 'private' (category). --from 17 exited 0 with Applied 6 mechanical change(s):, including dashboards[1].refreshIntervalSeconds: refreshInterval → refreshIntervalSeconds (dashboard-refresh-interval-to-refresh-interval-seconds), views[0].listViews.tabular.sort: estimate_hours desc → [{"field":"estimate_hours","order":"desc"}] (list-view-sort-string-clause-to-array), objects[4].externalSharingModel: externalSharingModel → (removed) (object-external-sharing-model-removed), and 3× the stock flow-decision-mode-inclusive-explicit.
    cli.migrate-meta-codemod 1 pass Per-file md5 over 172 files of the planted tree was taken before the run, after the plain run, and after the --out run. All byte-identical. The only new path was /tmp/w4c3a/R4/meta/snapshot.json (686 KB, outside the tree).
    cli.migrate-meta-codemod 2 pass Snapshot read-back: dashboards[1].refreshIntervalSeconds:300 with no refreshInterval, views[0]…sort:[{field:"estimate_hours",order:"desc"}], objects[4] has no externalSharingModel, and flows 6, 7 and 26 decision mode:"inclusive". This reconciles with the 6 applied. ✓ Migrated stack is schema-valid agrees with --json schemaValid:true. Warning branch (extra copy with an unknown key qaBogusKey): ⚠ Migrated stack does not yet pass schema validation — 1 refusal left after the chain. Resolve them, then run \os validate`:followed by the refusal, and--json schemaValid:false`.
    cli.migrate-meta-codemod 3 pass Canonical replay --from 17 over the written tree (write2) exited 0 with no Applied … mechanical change(s): block and 448 advisories. --from 18 printed ⚠ No migration step exists for protocol 18 → 18, so this run replayed nothing — that is not a finding that the metadata is canonical. and no Nothing to migrate. Two identical planted-tree --from 17 runs differ only in the (‹n>ms) stamp (normalized diff empty).
    cli.migrate-meta-codemod 4 pass --from 15 and --from 3 exited 1 with ✗ Cannot migrate from protocol 15: the chain's support floor is 16 (ADR-0087 D3)…. JSON: {error:"unsupported_from_major",…} with exit 1. MIGRATION_SUPPORT_FLOOR = 16.
    cli.migrate-meta-codemod 5 pass Mechanical edits are listed under Applied 6 mechanical change(s):. Semantics are listed separately under 448 manual change(s) require your judgment:, each ⚠ [protocol 18] … with why: and verify:. The externalSharingModel advisory is listed beside its mechanical removal, and none is reported as handled.
    cli.migrate-meta-codemod 6 pass os migrate meta --from 17 --json on the planted tree exited 0 (schemaValid:true, applied:6, todos:448). --write --json exited 0.
    cli.migrate-meta-codemod 7 pass --help: --write Rewrite the authored source files in place for each mechanical change traced to one literal in one project file…. First copy (write1, data key on Category): Wrote 5 of 6, and the externalSharingModel site was refused not written [shared]: \Category` … is referenced 1 more time(s) than this site(bydefineSeed(Category, …)). That is the contract working, and the entry was re-planted on PrivateNote as planted2. write2: Wrote 6 of 6 mechanical change(s) into 4 file(s):at flows/index.ts:1726, :283and:1440, private-note.object.ts :28, ops-dashboard :61and task.view:221. It closed ✓ Re-ran the chain over the written sources: no mechanical change remains.with exit 0.diff -ragainst write2.orig: exactly those 4 files and 6 hunks, each at the listed line. No semantic advisory was written, and there was no range rewrite (the showcase declares ^18). Re-run without--write: no Applied block, 448 advisories. Second --write: ℹ --write: the chain made no mechanical change here, so no file was written., and the tree md5 was unchanged. --write --jsonon freshwrite3exited 0 withwrite.status:"written", range:null, and written[]` of 6 with file and line.
    cli.migrate-meta-codemod neg0 pass Every shipped prescription sampled (--from 17 on all three planted keys) is listed.
    cli.migrate-meta-codemod neg1 pass The successors parse: the migrated stack is schemaValid:true.
    cli.migrate-meta-codemod neg2 pass Runs without --write changed no authored byte (checksums).
    cli.migrate-meta-codemod neg3 pass The --write diff touched only the listed sites. No advisory was written, no planted spelling was dropped (write1's refused one sits in the by-hand list with kind and reason), and the re-run converts nothing.
    cli.migrate-meta-codemod neg4 pass A below-floor --from exited 1 loudly.
    cli.migrate-meta-codemod neg5 pass Replay from the target major reported no changes, and two identical runs agree once the elapsed time is normalized.
    cli.migrate-meta-codemod neg6 pass schema-valid is printed only when schemaValid:true. The warning branch fires when a refusal remains. (The semantic advisories always print and are not schema refusals; see the checklist-accuracy findings.)
    cli.fresh-install-migrate-apply-first 0 pass test ! -e held for fresh.db, -wal and -shm, and still held after a pre-apply os migrate plan, which exited 0 and created nothing. os migrate apply --yes exited 0 with ✓ Created/extended 114 table(s): 114 table(s) to create, 0 column(s) to add. and the supporting line [organization-ownership] a datastore created from empty: birth marker written (shape generation 1, platform 18.2.0). Marker read before any boot: exactly 1 row adr-0131-organization-ownership, verified_at 2026-11-02T04:11:10.126Z, applied_at null, blocking 0, advisory 0, details.birth.shapeGeneration 1, which equals ORGANIZATION_OWNERSHIP_SHAPE_GENERATION = 1. After the boot the same row is unchanged (the boot added 2 other sys_migration rows, adr-0093-membership-backfill and pinyin-search-companion, but no second marker).
    cli.fresh-install-migrate-apply-first 1 pass The first os serve -p 41363 (same environment, setsid) printed ✓ Server is ready (not DEGRADED; 0 hits for DEGRADED), Mode: production, Driver: SqlDriver(better-sqlite3) → file:/tmp/w4c3a/R4/fresh/fresh.db and Tenancy: single. /api/v1/health returned 200 after 4 polls. Refusal greps: carries no organization-ownership marker 0, refuses to start 0, os migrate organization-ownership 0.
    cli.fresh-install-migrate-apply-first neg0 pass This release carries the fix: the first serve after apply was not refused.
    identity-auth.self-service-password-reset 0 pass Anonymous POST /auth/request-password-reset {email: auditor.demo} → 200 {"status":true,"message":"If this email exists in our system, check your email for the reset link"}. Admin GET /data/sys_identity_email → 1 row to "Ada Auditor (demo)" ‹auditor.demo@example.com>, subject "Reset your ObjectStack password", body carries http://localhost:41381/api/v1/auth/reset-password/<24-char token>?callbackURL=%2Freset. GET on that link → 302 to /reset?token=…. sys_email rows 0 before and after.
    identity-auth.self-service-password-reset 1 pass POST /auth/reset-password {token, newPassword} → 200 {"status":true}. Sign-in with the new password → 200 and get-session 200. Sign-in with the old password → 401 INVALID_EMAIL_OR_PASSWORD.
    identity-auth.self-service-password-reset 2 pass Second reset with the same token → 400 {"message":"Invalid token","code":"INVALID_TOKEN"}. Afterwards the first new password still signs in (200) and the attempted password does not (401).
    identity-auth.self-service-password-reset 3 pass Expired leg: a fresh request wrote a second mail with a different token. sys_verification row reset-password:TOKEN was found (1 row, expires_at 04:57:30Z); UPDATE … expires_at=02:57:31Z changed 1 row. The reset then answered 400 INVALID_TOKEN; the current password signs in 200 and the attempted one 401. Garbage leg: 3 garbage tokens (literal, 24×'x', last-char-mutated real token) each → 400 INVALID_TOKEN; the current password signs in 200 and the garbage password 401.
    identity-auth.self-service-password-reset 4 pass Known and unknown email requests both answered 200 with byte-identical bodies (rq.text === rqU.text → true). No sys_identity_email row was written for the unknown address. (A timing observation is in the probes; see SECURITY.)
    identity-auth.self-service-password-reset neg0 pass The 2xx reset did rotate the credential (new 200 / old 401).
    identity-auth.self-service-password-reset neg1 pass Reused and expired tokens were both refused 400 INVALID_TOKEN, and neither changed the password.
    identity-auth.self-service-password-reset neg2 pass The known and unknown request responses had identical status and body.
    identity-auth.self-service-password-reset neg3 pass Every garbage token was refused with a named 400 INVALID_TOKEN; there was no silent 2xx.
    identity-auth.two-factor-enrollment-reveal 0 pass GET /auth/config → 200 {success:true, data.features.twoFactor:true}. POST /auth/two-factor/enable {} → 400 VALIDATION_ERROR both anonymous and authed (not 404).
    identity-auth.two-factor-enrollment-reveal 1 pass enable {password} → 200 with keys method,totpURI,backupCodes. totpURI is otpauth://totp/ObjectStack:admin%40objectos.ai?secret=<52 chars>&issuer=ObjectStack&digits=6&period=30 (no algorithm param, so the SHA-1 default). The secret base32-decodes cleanly to 32 bytes, and a locally computed TOTP from it later verified 200.
    identity-auth.two-factor-enrollment-reveal 2 pass backupCodes is an array of 10 strings, all shaped xxxxx-xxxxx (mixed-case alphanumeric). Values are redacted.
    identity-auth.two-factor-enrollment-reveal 3 pass GET /meta/object/sys_two_factor enable_two_factor.resultDialog.fields = [{path:totpURI, format:qrcode}, {path:backupCodes, format:code-list}]. response.totpURI is a string (len 144) and response.backupCodes is a string[] of 10, so both resolve.
    identity-auth.two-factor-enrollment-reveal 4 pass (observed) POST /auth/two-factor/get-totp-uri. Pre-verify on the live bearer: right password → 200 with the SAME secret, wrong → 400 INVALID_PASSWORD, missing → 400 VALIDATION_ERROR, anonymous → 401. Post-verify on the rotated token echoed by verify-totp: right → 200 SAME secret, wrong → 400 INVALID_PASSWORD, missing → 400, anonymous → 401. The pre-verify bearer after verify → 401 (session rotated). Password-gated in every state; no ungated re-reveal.
    identity-auth.two-factor-enrollment-reveal 5 pass GET /meta/object/sys_user enable_two_factor: target /api/v1/auth/two-factor/enable, resultDialog fields [totpURI qrcode, backupCodes code-list], acknowledge "I have saved my backup codes", no successMessage key. Both paths resolve against the live response as in clause 3.
    identity-auth.two-factor-enrollment-reveal neg0 pass enable with a wrong password → 400 INVALID_PASSWORD, sys_two_factor rows 0. Anonymous enable → 401 UNAUTHORIZED, rows 0.
    identity-auth.two-factor-enrollment-reveal neg1 pass The guard was scored first on an armed boot.
    identity-auth.two-factor-enrollment-reveal neg2 pass get-totp-uri never re-revealed without the password (wrong 400, missing 400, anonymous 401, before and after verify).
    identity-auth.two-factor-verify-to-activate 0 pass Same guard read as reveal clause 0: data.features.twoFactor true, enable probe 400.
    identity-auth.two-factor-verify-to-activate 1 pass Password sign-in between enable and verify → 200 body without twoFactorRedirect, token present, cookies better-auth.session_token only (no two_factor cookie). get-session 200 with user.twoFactorEnabled false. DB sys_user.two_factor_enabled 0. TOTP_NOT_ENABLED is cited from the lockout pin, not driven.
    identity-auth.two-factor-verify-to-activate 2 pass (observed) DB row read at 03:53:22.016Z, 3 ms after the enable answered (03:53:22.013Z): sys_two_factor.verified = 0 (integer), failed_verification_count 0, two_factor_enabled 0. This is the healthy reading.
    identity-auth.two-factor-verify-to-activate 3 pass Session-lane POST /two-factor/verify-totp {correct code} with the live bearer → 200 {token,user}. Row verified 1, failed_verification_count 0, sys_user.two_factor_enabled 1. get-session twoFactorEnabled true. REST GET /data/sys_user/:id two_factor_enabled true.
    identity-auth.two-factor-verify-to-activate 4 pass Fresh sign-in → 200 {"twoFactorRedirect":true,"twoFactorMethods":["totp"]}, Set-Cookie better-auth.two_factor (Max-Age 600), session_token/session_data cleared (Max-Age 0). That challenge cookie gets get-session 401 and data read 401, so no usable session. Cookie-lane verify-totp → 200 with token; data read with it → 200.
    identity-auth.two-factor-verify-to-activate neg0 pass The never-verified enrollment took no part in sign-in: no challenge appeared, and a wrong code did not activate it (row verified 0).
    identity-auth.two-factor-verify-to-activate neg1 pass A wrong code on the session lane → 401 INVALID_CODE. failed_verification_count stayed 0 and verified stayed 0.
    identity-auth.two-factor-verify-to-activate neg2 pass Armed boot, guard checked first.
    identity-auth.two-factor-backup-codes 0 pass data.features.twoFactor true. The activation walk above completed on this boot (verified 1), and the challenge shows twoFactorRedirect true.
    identity-auth.two-factor-backup-codes 1 pass At the challenge (sign-in twoFactorRedirect), POST /two-factor/verify-backup-code with retained code #0 through the cookie → 200 with token; an authed data read with it → 200.
    identity-auth.two-factor-backup-codes 2 pass The same code #0 at a fresh challenge → 401 INVALID_BACKUP_CODE. Code #1 → 200 (positive control). Re-spending #1 → 401 INVALID_BACKUP_CODE, which is the second reproduction of single-use. Stored blob length went 362 → 334 after one spend.
    identity-auth.two-factor-backup-codes 3 pass POST /two-factor/generate-backup-codes {password} → 200 {status:true, backupCodes: 10 strings}, 0 overlap with the old set. Old unspent #3 and #4 at fresh challenges → 401 INVALID_BACKUP_CODE each. New #0 → 200, with an authed read 200.
    identity-auth.two-factor-backup-codes 4 pass (observed) DB read of sys_two_factor.backup_codes: a single string of length 334, base64 charset, does not parse as JSON, contains none of the 20 issued plaintext codes. sys_two_factor.secret is length 144 and does not contain the plaintext secret. Both are encrypted blobs, the healthy reading.
    identity-auth.two-factor-backup-codes 5 pass GET /meta/object/sys_user generate_backup_codes: resultDialog.fields [{path:backupCodes, format:code-list}], acknowledge "I have saved the new codes", refreshAfter false, no successMessage key. response.backupCodes of the regenerate is a non-empty string[] (10).
    identity-auth.two-factor-backup-codes neg0 pass Re-spending a code was refused twice (codes #0 and #1), 401 INVALID_BACKUP_CODE.
    identity-auth.two-factor-backup-codes neg1 pass Two old-set unspent codes after regenerate → 401 each.
    identity-auth.two-factor-backup-codes neg2 pass generate-backup-codes with a wrong password → 400 INVALID_PASSWORD, anonymous → 401 UNAUTHORIZED, {} → 400 VALIDATION_ERROR.
    identity-auth.two-factor-backup-codes neg3 pass After the refused wrong-password regenerate, old-set code #2 still spent → 200, so the old set survived.
    identity-auth.two-factor-disable-lifecycle 0 pass /auth/config {success:true, data.features.twoFactor:true}. BEFORE: sign-in twoFactorRedirect true, sys_two_factor rows 1, two_factor_enabled 1.
    identity-auth.two-factor-disable-lifecycle 1 pass POST /two-factor/disable wrong password → 400 INVALID_PASSWORD; {} → 400 VALIDATION_ERROR; anonymous → 401. Afterwards: rows 1, tfe 1, a fresh sign-in still twoFactorRedirect true, and the caller's bearer still live (200).
    identity-auth.two-factor-disable-lifecycle 2 pass Correct-password disable → 200 {"status":true}, with Set-Cookie session_token and set-auth-token (rotated; the old bearer then answers 401). DB sys_two_factor rows for the user 0; sys_user.two_factor_enabled 0. REST sys_user two_factor_enabled false.
    identity-auth.two-factor-disable-lifecycle 3 pass Next sign-in → 200 with no twoFactorRedirect, cookie better-auth.session_token only, token present. get-session 200 twoFactorEnabled false. Data read 200.
    identity-auth.two-factor-disable-lifecycle 4 pass Post-disable, a retained unspent backup code → 401 INVALID_TWO_FACTOR_COOKIE (no cookie) or 400 BACKUP_CODES_NOT_ENABLED (with session). Old-secret TOTP → 400 TOTP_NOT_ENABLED. Re-enable gave a new secret (≠ old) and 0 overlapping codes. Old-secret TOTP on the session lane → 401 INVALID_CODE (not activated); the new TOTP → 200. At the new challenge: old-secret TOTP 401 INVALID_CODE, old original-set code 401 INVALID_BACKUP_CODE, old regenerated-set code 401 INVALID_BACKUP_CODE; the new TOTP completes, with data read 200.
    identity-auth.two-factor-disable-lifecycle 5 pass (observed) PUT /api/settings/auth {values:{mfa_required:true}} → 200 (read back true, grace 7); /api/v1/settings/auth → 404, as the item notes. Starting stamp mfa_required_at null. Disable under mfa_required → 200 (not refused), and the old bearer → 401 (rotated). Re-sign-in had no redirect; get-session authGate null; authed read 200, which is the in-grace branch. Stamp written lazily: 2026-11-02T03:55:29.712Z. grace set to 0 → authed read 403 {"error":{"code":"MFA_REQUIRED",…}}, get-session user.authGate {code:MFA_REQUIRED}. While gated: POST /two-factor/enable → 200 (totpURI+backupCodes), then verify-totp → 200 and authed read 200 again. Stamp after re-enrollment is unchanged (03:55:29.712Z, never cleared, as the source says). Restored mfa_required false and grace 7, both read back.
    identity-auth.two-factor-disable-lifecycle neg0 pass Wrong-password disable was refused, and the challenge and row stayed consistent: the row was gone and the challenge gone after the correct disable, and both intact after the refused one.
    identity-auth.two-factor-disable-lifecycle neg1 pass Stale codes and old-secret TOTPs were all refused after disable and after re-enroll (clause 4).
    identity-auth.two-factor-disable-lifecycle neg2 pass While gated (403 MFA_REQUIRED), enable answered 200 and verify-totp 200, so the user was not stranded.
    identity-auth.two-factor-disable-lifecycle neg3 pass Recorded as mechanism: a lazy stamp that is never cleared. Not filed.
    records-forms.validation-rule-type-matrix 0 pass Six codes, each from its own driven 400 VALIDATION_FAILED: invalid_initial_state (project POST status=active, project_status_flow); invalid_transition (project active→planned, and showcase_account active→prospect from account_lifecycle); invalid_format (tax_id 'abc', billing_email 'not-an-email', PATCH tax_id '123456789'); json_schema_violation (support_config {tier:'gold'}); invalid_json (support_config '{not json'); rule_violation (cross_field end‹start, script spent 200 > 1.2×budget 100, conditional churned without churn_reason).
    records-forms.validation-rule-type-matrix 1 pass fields[].field per variant: state_machine → status; format → tax_id / billing_email; json_schema → support_config; cross_field → start_date (fields[0] of [start_date,end_date]). script and conditional rules declare no field → _record; see the accuracy note.
    records-forms.validation-rule-type-matrix 2 pass Each create rejection left the count unchanged (projects 5→5, 6→6, 7→7; accounts 15→15, 16→16, 17→17). Each update rejection left the row byte-identical on re-read (JSON string compare true for 4 PATCH rejections).
    records-forms.validation-rule-type-matrix 3 pass Twins: POST project status=planned 201; planned→active 200; active→on_hold 200; account create status=active 201; format twin (12-3456789, billing@example.com) 201; json_schema {tier:premium, seats:2} 201; cross_field 06-01..06-10 201; script spent 110 / budget 100 201; conditional churned with churn_reason 200.
    records-forms.validation-rule-type-matrix 4 pass PATCH health green→red → 200 with warnings:[{"rule":"project_health_progression","severity":"warning","field":"health","code":"invalid_transition","message":"Health changed by more than one step — confirm this is intentional."}]. Re-read health=red. Log: 2026-11-02T03:59:52.718Z WARN Validation rule 'project_health_progression' (warning): Health changed by more than one step — confirm this is intentional.
    records-forms.validation-rule-type-matrix 5 pass PUT /meta/object/qa_vr_probe and qa_vr_probe_err → 200 each. POST qa_vr_probe {title:"abc"} → 201, re-read title "abc", count 0→1. Exactly one log line names qa_unevaluable_warn: WARN Validation rule 'qa_unevaluable_warn' (warning): the rule could not be evaluated (runtime: int() type error: cannot convert to int) — write proceeded: the rule is advisory, so it was not applied to this write, which contains neither "write rejected" nor "fails closed". Control: qa_vr_probe_err → 400 VALIDATION_FAILED, fields[0] rule_violation constraint {rule:qa_unevaluable_err, reason:unevaluable}, count 0→0, and its log lines say "write rejected: a rule that cannot be evaluated fails closed".
    records-forms.validation-rule-type-matrix neg0 pass The unevaluable error-severity rule failed closed: 400 rule_violation reason unevaluable, no row. An evaluable match (title "5") → 400; a non-match (title "0") → 201.
    records-forms.validation-rule-type-matrix neg1 pass Unevaluable warning → 201. Evaluable warning → 200. An evaluable info scratch rule (probe) → 201 with a warnings entry of severity info. No advisory blocked.
    records-forms.validation-rule-type-matrix neg2 pass The matching warning rule logged its line (clause 4).
    records-forms.validation-rule-type-matrix neg3 pass The landed unevaluable advisory write logs "write proceeded" with no rejection wording (clause 5).

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

    Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

    Per-clause verdicts, part 3 of 3 (approvals, attachments-storage, automation, integration-system, platform-core). Clause numbers are acceptance indices; neg / negN are the item's negative list. Evidence is text oracle output only.

    item clause verdict evidence (text oracle)
    approvals.decision-only-via-service 0 pass seeded run run_fce86c53… paused at dual_signoff (steps start, dual_signoff); POST /automation/showcase_invoice_signoff/runs/ID/resume {} → 403 PERMISSION_DENIED "…is paused at a 'approval' node, which only its owning service may resume…"; {branchLabel:"approve"} → same 403
    approvals.decision-only-via-service 1 pass after each forged call: run status paused, steps [start, dual_signoff]; request areq_18dbf8a9… status pending; GET /:id/actions = [submit] only (before and after)
    approvals.decision-only-via-service 2 pass POST /approvals/requests/areq_18dbf8a9…/approve → request status approved, pending_approvers [] (the dev admin holds both slots, one decision satisfies the unanimous slate; a second approve → 409 INVALID_STATE "request is approved"); run → completed, steps start/dual_signoff/notify_cleared all success
    approvals.decision-only-via-service 3 pass POST /actions/showcase_task/showcase_bulk_reassign/3vf8EH9jGo9G_Iwu → 200 paused runId run_d13e6d5f… screen collect; resume through the same generic route {inputs:{new_assignee:"r6-reassigned"}} → 200, run completed (start, collect, apply), task assignee "r6-reassigned"
    approvals.decision-only-via-service neg pass no 2xx on either forged approval resume; the screen-flow resume 2xx proves the route is live
    approvals.approver-resolution-matrix 0 pass scratch record-after-create flows on showcase_field_zoo via POST /api/v1/automation, one per leg, deregistered after each: user auditor → [auditor]; team (create-team + 2 add-team-member) → [auditor, phone]; manager (set-user-manager admin→auditor) → [auditor]; field f_users [phone, auditor] → [phone, auditor]; position auditor → [auditor] (sys_user_position holder); org_membership_level owner → [admin], member → [auditor, phone] (sys_member roles); queue per clause 5; pending_approver_names populated for every slot
    approvals.approver-resolution-matrix 1 pass memberships: auditor∈bu_west_coast, phone∈bu_east_coast and bu_field_ops. department bu_field_ops → [auditor, phone] (phone once, auditor is descendant-only); root bu_acme → [auditor, phone]; PATCH bu_west_coast active=false → re-trigger bu_field_ops → [phone]; inactive root bu_west_coast and unknown bu_nope → WARN "approver 'department:…' expanded to nobody", then admin_rescue on [admin]
    approvals.approver-resolution-matrix 2 pass pending_approvers ["usr_showcase_phone_demo","usr_showcase_auditor_demo"] in field order; node_config_json.__resolvedFrom {"field:f_users":[phone, auditor]}; no CSV slot
    approvals.approver-resolution-matrix 3 pass role owner/member each register 200 and log "WARN [approvals] approver type 'role' is deprecated (ADR-0090 D3) — author 'org_membership_level' instead {deprecated:role, canonical:org_membership_level}" (plus the [Protocol] authoring advisory); role owner = oml owner = [admin]; role member = oml member = [auditor, phone]
    approvals.approver-resolution-matrix 4 pass unstaffed position client_liaison: absent and admin_rescue → pending on [admin] (the org's only owner/administrator), __resolvedFrom admin_rescue:abandoned ["position:client_liaison"], reason "position 'client_liaison' has no holder", actions carry system comment (actor null) "Routed to this organization's administrators…", WARN "…opening on this organization's administrators per onEmptyApprovers: 'admin_rescue'"; fail → 0 requests, run failed "NO_APPROVERS…"; auto_approve → 0 requests, run completed, marker f_textarea "r6-approved-edge", WARN "auto-approving per onEmptyApprovers: 'auto_approve'"; fallback user phone → pending [phone], WARN "opening on the declared fallbackApprovers"; fallback to empty queue → WARN "…fallbackApprovers resolved to nobody either — falling back to admin_rescue", pending on [admin]
    approvals.approver-resolution-matrix 5 pass sys_queue qa_r6_queue + 2 sys_queue_member rows → pending [auditor, phone], no queue: slot; empty queue and unknown queue → WARN "approver 'queue:qa_r6_empty' expanded to nobody — … It adds no slot to this request…", pending on [admin], abandoned [], reason "no approver on the node resolved to a person", routed-to-administrators comment, request stays pending
    approvals.approver-resolution-matrix neg0 pass every graph expansion to nobody (department inactive/unknown, team unknown, queue empty/unknown, position unstaffed, manager unset) carried its own "expanded to nobody"/"resolved to nobody" warn
    approvals.approver-resolution-matrix neg1 pass no empty-slate request finalized; admin_rescue/default requests stayed pending; only auto_approve passed, with its warn
    approvals.approver-resolution-matrix neg2 pass no admin_rescue request opened on a literal slot alone; all named the admin
    approvals.approver-resolution-matrix neg3 pass role parses (200) and resolves identically to org_membership_level (two tiers)
    approvals.approver-resolution-matrix neg4 pass bu_field_ops included descendant-only member auditor
    attachments-storage.field-accept-maxsize-server-enforced 0 pass qa-media-constraints recipe replayed (POST /packages 2xx, PUT object 2xx, poster accept/maxSize and doc accept read back). POST qamedia_media {accept-probe, poster: A(pdf)} → 400; filtered re-list 0 rows (positive control: same filter on "control" → 1); sys_file A ref_id null
    attachments-storage.field-accept-maxsize-server-enforced 1 pass B (image/png, 2097152 bytes PUT, complete 200) → 400; re-list 0 rows; ref_id null
    attachments-storage.field-accept-maxsize-server-enforced 2 pass 400 ERR_FILE_CONSTRAINT "File type 'application/pdf' is not permitted by the accept list declared for 'poster' (image/png, image/jpeg)" and 400 ERR_FILE_CONSTRAINT "File exceeds the maximum size declared for 'poster' (2097152 bytes > 1048576 bytes)"; server log carries 0 lines for either (as knownGaps says)
    attachments-storage.field-accept-maxsize-server-enforced 3 pass PATCH control poster→A → 400 ERR_FILE_CONSTRAINT (accept); →B → 400 ERR_FILE_CONSTRAINT (size); re-read poster still c.png (89b00764…) both times
    attachments-storage.field-accept-maxsize-server-enforced 4 pass control C (png 10240) → 201 id -3zfT0g6OSk16XwA; sys_file C ref_object qamedia_media, ref_field poster, ref_id -3zfT0g6OSk16XwA
    attachments-storage.field-accept-maxsize-server-enforced 5 pass system write OUT OF PROCESS (node:sqlite UPDATE sys_file SET mime_type=NULL,size=NULL on the boot's primary data.db, 1 change; the data door now drops these columns, see Retests/accuracy) → sys_file A reads committed, mime_type null, size null → POST {nometa, poster: A} → 201, A claimed (ref_id afPqZ9bfyfcUt9e0)
    attachments-storage.field-accept-maxsize-server-enforced 6 pass doc ← D "README" (dotless) → 201, 1 row; doc ← E "report.txt" text/plain → 400 ERR_FILE_CONSTRAINT "File type 'text/plain' is not permitted by the accept list declared for 'doc' (.pdf)", re-list 0 rows, E ref_id null
    attachments-storage.field-accept-maxsize-server-enforced neg0 pass no qamedia_media row holds A or B (re-lists 0)
    attachments-storage.field-accept-maxsize-server-enforced neg1 pass A, B, E ref_id null after their refusals
    attachments-storage.field-accept-maxsize-server-enforced neg2 pass the two documented boundaries (clauses 5, 6) passed as designed, not scored as leaks
    attachments-storage.field-accept-maxsize-server-enforced neg3 pass unknown id 00000000-… → 400 VALIDATION_FAILED invalid_value "Poster cannot reference this file: it is not a file you can open…" (constraints never judged); all violation probes used real committed files
    automation.trigger-status-contract 0 pass qa-flow-status-doors recipe steps 1–4, 6 replayed (2xx). Both /automation/:name/trigger and /automation/trigger/:name: qa_r6_nope 404 RESOURCE_NOT_FOUND; qa_always_fails 400 FLOW_FAILED; showcase_urgent_task_alert (off) 409 FLOW_DISABLED; qa_no_start_node 422 FLOW_NO_START_NODE; qa_input_schema_invalid 422 FLOW_INPUT_SCHEMA_INVALID "Node 'assign' parameter 'assignments' expected type 'string' but got 'object'"; showcase_notify_owner {ownerId, message} 200 success; no refusal row answered 200
    automation.trigger-status-contract 1 pass showcase_reassign_wizard {recordId, objectName} → 200 data.status paused, runId run_37eec303… (new door) / run_05695e42… (legacy), screen collect; a paused runId (run_39beccd7…) later resumed 200 → completed
    automation.trigger-status-contract 2 pass disabled flow at each door: trigger 409 FLOW_DISABLED; /actions/showcase_task/showcase_bulk_reassign (wizard off) 409 FLOW_DISABLED; POST /apps/showcase/inquiries/purge (purge off) 409 FLOW_DISABLED, closed inquiries 1 before/1 after. Authored type:'flow' actions (PUT /meta/action/NAME?package=com.objectstack.qa.automation, 200) → /actions 400 FLOW_FAILED, 422 FLOW_NO_START_NODE, 422 FLOW_INPUT_SCHEMA_INVALID, 409 FLOW_DISABLED. Source: domains/automation.ts, action-execution.ts and endpoint-executor.ts each import classifyFlowRefusal/FLOW_NOT_FOUND_STATUS from flow-dispatch-status.ts
    automation.trigger-status-contract 3 pass 400 FLOW_FAILED details keys [errorMessage, summary] on both trigger routes (errorMessage "QA always-fails probe: the unroutable http node failed", summary failed 1 with per-node rows) and on /actions (same errorMessage)
    automation.trigger-status-contract 4 pass on paused run run_39beccd7…: member auditor 403 PERMISSION_DENIED; bodyless and {} 400 INVALID_SCREEN_INPUT; {inputs,output:{$x:1}} 400 INVALID_SIGNAL; output-only 400 INVALID_SCREEN_INPUT; inputs+variables 400 VALIDATION_FAILED; sys_automation_run renamed away in data.telemetry.db → 503 STORE_UNAVAILABLE "Durable suspended-run store unreachable…", renamed back; run read paused after every one; then valid resume 200 completed. 409 RESUME_IN_PROGRESS: scratch screen→http(6 s local delay)→screen flow, second resume 1.5 s into the first → 409, first → 200 paused at collect2, later resume 200 completed. 409 RESUME_CHILD_PENDING: scratch map parent over a screen child (POST /automation) → resume of the parent 409 "…on item run 'run_9a3476ad…', which is still suspended… Resume run 'run_9a3476ad…' instead", parent stays paused, child resume 200 → parent completed. Terminal: unknown run 404 RUN_NOT_FOUND; wizard run without recordId → valid resume 400 FLOW_FAILED, run failed, next resume 404
    automation.trigger-status-contract 5 pass flow deregistered under a live pause (DELETE /automation/qa_r6_stale2) → resume 404 RUN_NOT_FOUND "Flow 'qa_r6_stale2' not found for run 'run_8263a152…'". The node-edited-away arm did NOT 404: after PUT /automation/qa_r6_stale1 renaming the paused node collect→collect_v2, the resume answered 200 and completed on the run's pinned body (steps start, collect) — by design (run-resume.ts resolveResumeTarget recovers the pinned flowChecksum version); see Checklist-accuracy
    automation.trigger-status-contract 6 pass packages/client dist (ObjectStackClient) against 41403: trigger() and execute() REJECT with (httpStatus, code) 404 RESOURCE_NOT_FOUND, 400 FLOW_FAILED, 409 FLOW_DISABLED, 422 FLOW_NO_START_NODE, 422 FLOW_INPUT_SCHEMA_INVALID and RESOLVE for success and paused (runId); resume() rejects 403 PERMISSION_DENIED, 400 INVALID_SCREEN_INPUT, 400 INVALID_SIGNAL, 404 RUN_NOT_FOUND, 503 STORE_UNAVAILABLE, 409 RESUME_IN_PROGRESS and resolves the valid resume
    automation.trigger-status-contract neg0 pass no refusal row answered 200 at any door
    automation.trigger-status-contract neg1 pass never-dispatched exits answered 404/409/422, never 400 FLOW_FAILED
    automation.trigger-status-contract neg2 pass 409 FLOW_DISABLED and 422 FLOW_NO_START_NODE distinct at trigger, /actions, SDK
    automation.trigger-status-contract neg3 pass paused trigger answered 200 status paused with runId
    automation.trigger-status-contract neg4 pass each retryable refusal left the run paused and a later valid resume succeeded
    automation.trigger-status-contract neg5 pass the three doors import the one table (source grep above); answers matched
    automation.packaged-flow-subflow-disable-refusal 0 pass POST /automation/showcase_notify_owner/toggle {enabled:false} → 409 DELETE_RESTRICTED "Flow 'showcase_notify_owner' cannot be disabled while 1 packaged flow still calls it as a subflow: 'showcase_task_done_notify_owner'. … mid-run at its subflow node … (ADR-0126 §7.3). Disable the calling flow 'showcase_task_done_notify_owner' first — or leave this one armed."; showcase_closure_signoff likewise names showcase_project_closure
    automation.packaged-flow-subflow-disable-refusal 1 pass showcase_one_task_signoff disable → 409 DELETE_RESTRICTED naming 'showcase_release_signoff' (map caller)
    automation.packaged-flow-subflow-disable-refusal 2 pass GET /data/sys_metadata_activation after the three refusals → [] (same read later lists rows, positive control); task created in_review then PATCH done → showcase_task_done_notify_owner run completed with call_notify:subflow:success and a showcase_notify_owner run completed; showcase_release_signoff trigger → child showcase_one_task_signoff paused at review
    automation.packaged-flow-subflow-disable-refusal 3 pass child enable with caller off → 200 enabled true; with caller armed → 200 enabled true; no DELETE_RESTRICTED
    automation.packaged-flow-subflow-disable-refusal 4 pass caller disable → 200; retry child disable → 200; ledger rows showcase_task_done_notify_owner active false, showcase_notify_owner active false
    automation.packaged-flow-subflow-disable-refusal neg0 pass steps 2 and 3 answered 409, never 2xx
    automation.packaged-flow-subflow-disable-refusal neg1 pass qa-second-packaged-caller via POST /marketplace/install-local → 200 hotLoaded true; GET /automation/qasub_second_notify_caller _provenance package, _packageId com.objectstack.qa.qasub; disable showcase_notify_owner → 409 DELETE_RESTRICTED "…while 2 packaged flows still call it…: 'showcase_task_done_notify_owner', 'qasub_second_notify_caller'…"; ledger row for showcase_notify_owner unchanged (active true, updated_at 04:07:28.262Z); after teardown DELETE the same disable names showcase_task_done_notify_owner alone
    integration-system.connector-degraded-recovery 0 pass boot completed (health 200 after 14 s) with scripts/mcp-fixture.mjs absent; WARN "Connector registered DEGRADED: showcase_mcp_tools (origin: declarative)… degradedReason …MCP error -32000: Connection closed"; GET /automation/connectors → state degraded, degradedReason quoted, actions []
    integration-system.connector-degraded-recovery 1 pass twin (transport.kind carrier_pigeon, appC): os dev compile succeeded (dist carries carrier_pigeon), then "✗ Plugin com.objectstack.service-automation failed to start - rollback complete: … providerConfig.transport.kind must be 'stdio' or 'http'", process exited, health 000 / nothing on 41407; broken-upstream boot degraded instead
    integration-system.connector-degraded-recovery 2 pass trigger showcase_mcp_connector_echo → 400 FLOW_FAILED in 0.043 s "connector_action 'echo': connector 'showcase_mcp_tools' is degraded — connector-mcp provider: … Connection closed. Dispatch is unavailable…"; comparator scratch flow naming qa_r6_no_such_connector → 400 "no handler for 'qa_r6_no_such_connector.echo_upper' — is the connector plugin registered?"
    integration-system.connector-degraded-recovery 3 pass attempts at 04:09:35.851 (next 5000 ms), 39.910 (attempt 2, 4.06 s), 49.956 (+10.05 s, next 20000), 04:10:10.013 (+20.06 s, next 40000); view draft PUT + publish at 04:10:35.82 → attempt 5 at 04:10:36.609 (0.79 s), next retry 80000 ms (count kept, no reset)
    integration-system.connector-degraded-recovery 4 pass fixture copied back at 04:10:42 with no restart; 04:11:56.776 INFO "recovered connector instance 'showcase_mcp_tools' via provider 'mcp' (1 action(s))" (+80.17 s); descriptor state ready, degradedReason null, actions [echo_upper]; echo trigger → 200 output {upper:"OBJECTSTACK"}; /health uptime 151.4 s (same process)
    integration-system.connector-degraded-recovery neg0 pass dead upstream did not take the boot down
    integration-system.connector-degraded-recovery neg1 pass degraded husk stayed listed with its reason
    integration-system.external-datasource-federated-read 0 pass /meta/object lists showcase_ext_customer and showcase_ext_order with datasource showcase_external; /data/showcase_ext_customer → c1,c2,c3; /data/showcase_ext_order → o1–o4; filter {status:paid} → o1,o2,o4; top=2 → o1,o2; filter {region:EU} → c2; $filter → 400 INVALID_FILTER (must be JSON)
    integration-system.external-datasource-federated-read 1 pass POST customer → 403 EXTERNAL_WRITE_FORBIDDEN "Write 'insert' blocked on object 'showcase_ext_customer': datasource 'showcase_external' is external (schemaMode=external). Requires datasource.external.allowWrites=true (got false) AND object.external.writable=true (got false)."; PATCH c1 → 403 same ('update'); DELETE o1 → 403 same ('delete'); re-read 3 customers names unchanged, 4 orders; showcase_external.db read directly: same 3 customers, 4 orders
    integration-system.external-datasource-federated-read 2 pass GET /api/v1/datasources → [default (managed, ok), showcase_external (sqlite, schemaMode external, origin code, active true, status ok)]; GET /api/v1/datasources/showcase_external → descriptor (filename .objectstack/data/showcase_external.db, active true, origin code, hasSecret false)
    integration-system.external-datasource-federated-read neg pass no write appeared to succeed; file unchanged
    platform-core.docs-audience-gate 0 pass anon → org book qadocs_org, gated qadocs_gated, implicit com.example.qadocs: 401 {"error":"This documentation requires sign-in","code":"UNAUTHENTICATED"}; non-holders Mei and admin → qadocs_gated 403 {"error":"This documentation is limited to holders of a permission set you do not have","code":"PERMISSION_DENIED"}; no 404, no empty-200 tree
    platform-core.docs-audience-gate 1 pass matrix (status): public showcase_manual/qadocs_pub anon 200, Mei 200, Ada 200, admin 200; org qadocs_org anon 401, Mei/Ada/admin 200; permissionSet qadocs_gated anon 401, Mei 403, admin 403, Ada (holder) 200. Fixture: permissionSet/org books + docs authored at runtime in scratch package com.example.qadocs (stock has none, knownGaps[0])
    platform-core.docs-audience-gate 2 partial: half 1 pass; half 2 blocked(fixture: no pin, knownGaps[2]) packages/spec book.test.ts 45/45 passed (layer: pure spec function); the case "permission-set gate FAILS CLOSED when holdings are unknown (ADR-0049)" asserts audienceAllows({permissionSet:'crm_admin'}, {authenticated:true}) → false. Unknown-shape half: source return false; // unknown future shape → fail closed (book.zod.ts audienceAllows); unreachable live — PUT book with audience:{"role":"admin"} → 422 INVALID_METADATA invalid_union; no pin
    platform-core.docs-audience-gate 3 pass implicit package tree com.example.qadocs: Ada (holder) [orgdoc, orphan, pubdoc, secret], Mei and admin (non-holders) [orgdoc, orphan, pubdoc] → non-holder ⊂ holder, admin = non-holder (no bypass); qadocs_pub anon [pubdoc] vs authenticated [pubdoc + uncategorized orphan]; 65 per-entry GET /meta/doc/‹name> fetches across all 20 trees as each tree's own persona: 65×200, 0 refusals
    platform-core.docs-audience-gate 4 pass orphan qadocs_orphan absent from anon qadocs_pub tree (entries [qadocs_pubdoc] only); listed under uncategorized only for authenticated callers; anon GET /meta/doc/qadocs_orphan → 401 UNAUTHENTICATED; anon /meta/doc list omits it
    platform-core.docs-audience-gate 5 pass GET /meta/book/com.example.showcase/tree Mei 200 (implicit book, group all, 8 docs), anon 401; com.example.qadocs authenticated 200, anon 401; unknown no_such_book_xyz authed 200 {groups:[]}, anon 401
    platform-core.docs-audience-gate 6 pass showcase_manual tree showcase_index label: no header 'Showcase'; Accept-Language: zh-CN and zh 'Showcase 示例'; PUT book with inline translations → 422 INVALID_METADATA unrecognized_keys "…removed in @objectstack/spec 17.0.0 … A doc's own title and body still localize through doc.translations"; read-back 404 (nothing stored)
    platform-core.docs-audience-gate neg0 pass no non-public book served a tree to anon (all 401)
    platform-core.docs-audience-gate neg1 pass anon public-book tree carries no unclaimed doc
    platform-core.docs-audience-gate neg2 pass refusal bodies are the two fixed sentences + code; no book/group/doc names
    platform-core.docs-audience-gate neg3 pass no 404 where 401/403 due (empty name /meta/book//tree → 404 ENDPOINT_NOT_FOUND is a routing miss, not a gated book)
    platform-core.docs-audience-gate neg4 pass ?package=com.example.showcase&package=com.example.qadocs → 400 VALIDATION_ERROR "The "package" query parameter was supplied 2 times…"; single → 200
    platform-core.manifest-install-contract 0 pass RT=18 (discovery version 18.2.0; refusal protocolVersion 18.0.0). engines.protocol ^17 → 422 OS_PROTOCOL_INCOMPATIBLE; then GET /packages/com.example.qa-manifest-probe-old 404 and absent from GET /packages?limit=500 (30 rows; positive control probe-a present). Same for AHEAD ^19 (GET 404, absent from list)
    platform-core.manifest-install-contract 1 pass older: details {requiredRange:"^17", rangeSource:"engines.protocol", protocolVersion:"18.0.0", targetMajor:17, migrateCommand:"objectstack migrate meta --from 17"}; AHEAD ^19: details {requiredRange:"^19", rangeSource:"engines.protocol", protocolVersion:"18.0.0", targetMajor:19}, NO migrateCommand member, message "…built for a newer protocol than this runtime implements. Upgrade the runtime to protocol 19, or install a build of '…' for protocol 18"
    platform-core.manifest-install-contract 2 pass prec-ok (protocol ^18, platform ^17) → 201, GET 200; prec-no (protocol ^17, platform ^18) → 422 rangeSource engines.protocol, GET 404; legacy engine.objectstack ^17.0.0 → 422 rangeSource engine.objectstack, GET 404; ^18.0.0 → 201, GET 200; (extra rung: engines.platform ^17 alone → 422 rangeSource engines.platform)
    platform-core.manifest-install-contract 3 pass no range → 201 + log [protocol] package 'com.example.qa-manifest-probe-norange' declares no engines.protocol range; loading under protocol 18.0.0 without a compatibility check (ADR-0087).; workspace:* → 201 + [protocol] package 'com.example.qa-manifest-probe-wsrange' declares an unrecognized engines.protocol range 'workspace:*'; skipping the protocol handshake (ADR-0087).; both GET 200
    platform-core.manifest-install-contract 4 pass (a) probe-b on qa_mfp → 422 NAMESPACE_CONFLICT "Namespace conflict: namespace "qa_mfp" is already owned by package "com.example.qa-manifest-probe-a", so package "com.example.qa-manifest-probe-b" cannot be installed alongside it. …" (namespace, owner, incoming id all named), GET probe-b 404; (b) plain re-POST A → 409 RESOURCE_CONFLICT, ?overwrite=true → 201 (no namespace conflict); (c) base/system/sys each 201, and a SECOND claimant of base and of sys also 201 (exemption discriminated)
    platform-core.manifest-install-contract 5 pass com.example.leave (no ns): POST echo leave, GET read-back manifest.namespace leave; com.example.holiday ns timeoff read-back timeoff; com.example.averyveryverylongpackagesegmentname → averyveryverylongpac (20 chars)
    platform-core.manifest-install-contract neg0 pass no incompatible manifest got 2xx or a row (^17, ^19, >18, <18, 17.x, ~17.0.0, >=17 <18, platform ^17/^19, legacy ^17.0.0/^19.0.0, ^20: all 422, GET 404)
    platform-core.manifest-install-contract neg1 pass no-range, workspace:*, "", *, 18.x, ^17 || ^18 all admitted 201
    platform-core.manifest-install-contract neg2 pass conflict on the wire = 422 NAMESPACE_CONFLICT, never 5xx
    platform-core.manifest-install-contract neg3 pass conflict message names namespace, current owner, incoming id
    platform-core.manifest-install-contract neg4 pass derived leave identical in POST echo and GET read-back
    platform-core.marketplace-install-local-lifecycle 0 pass boot A (no OS_CLOUD_URL, tree): plugins line lists marketplace-proxy, marketplace-install-local, cloud.connection, runtime-config; GET install-local (admin) 200 items:[]; GET /marketplace/packages 502 MARKETPLACE_PROXY_FAILED "fetch failed" (mounted passthrough, upstream unreachable); GET /cloud-connection/status 200; runtime/config 200. Boot B (OS_CLOUD_URL=off, scratch): plugins line MarketplaceInstallLocal + com.objectstack.runtime.runtime-config only; install-local 200 []; /marketplace/packages 404 ENDPOINT_NOT_FOUND; /cloud-connection/status 404; catalog install {"packageId":"com.acme.nothing"} → 503 MARKETPLACE_UNAVAILABLE (refusal, no dial-out)
    platform-core.marketplace-install-local-lifecycle 1 pass boot A features {installLocal:true, marketplace:true} and install-local 200 + browse non-404; boot B features {installLocal:true, marketplace:false} and install-local 200 + browse 404: all four cells agree
    platform-core.marketplace-install-local-lifecycle 2 pass os package install /tmp/w4c3a/R7/mkt/crm-artifact.json -r http://localhost:41424 --email … --password … exit 0: "✓ Loaded artifact (80.0 KB) — air-gapped inline install … ✓ Package installed into the running kernel · Package: com.example.crm · Version: 4.0.0"; manifest cached at scratch .objectstack/installed-packages/com.example.crm.json; GET /meta/app → [showcase_app, setup, account, crm_app]; GET /data/crm_account total 3, crm_opportunity total 12
    platform-core.marketplace-install-local-lifecycle 3 pass admin: entry with installedBy:"e95GJA…", envelope storageDir; member (own session, sys_member role member): 200, entry listed, no installedBy key, envelope has no storageDir key (omitted, not null); anon 401 UNAUTHENTICATED
    platform-core.marketplace-install-local-lifecycle 4 pass user row f_QP8codfWotiv4a "QA User Row R7" added (crm_account 3→4); purge → 200 {deleted:28, skipped:0, errors:0, withSampleData:false}; after: crm_account 1 (the user row), contact/opportunity/lead/activity 0; ledger file sampleDataPurged:true; listing withSampleData:false; 2nd purge {deleted:0, skipped:28}; restart on same DB → still 1/0/0/0/0; reseed → {inserted:28, …, withSampleData:true}, counts 4/3/12/5/5 with user row intact, listing withSampleData true, ledger sampleDataPurged:false
    platform-core.marketplace-install-local-lifecycle 5 pass member (identity proven): POST install-local (body and {}), DELETE, reseed, purge → each 403 FORBIDDEN "… requires the manage_metadata capability." (Installing a package / Uninstalling a package / Reseeding sample data / Purging sample data); anon on all five → 401 UNAUTHENTICATED; admin's install/purge/reseed/DELETE succeeded; crm_account unchanged (4) after refused calls
    platform-core.marketplace-install-local-lifecycle 6 pass DELETE → 200 {manifestId, cleanups:[{runtime.package-datasources, success:true, removed:2},{security.package-permissions, success:true, removed:0}], note:"Cached manifest removed, the package withdrawn from the running kernel, …"}, no registry.uninstallPackage failure entry; pre-restart: /data/crm_account and crm_opportunity 404 OBJECT_NOT_FOUND, /meta/app [showcase_app, setup, account], ledger dir empty; post-restart: same app list, crm_account 404, listing items:[]
    platform-core.marketplace-install-local-lifecycle neg0 pass install-local 200 under OS_CLOUD_URL=off
    platform-core.marketplace-install-local-lifecycle neg1 pass flags matched routes on both boots
    platform-core.marketplace-install-local-lifecycle neg2 pass purge left the undeclared user row
    platform-core.marketplace-install-local-lifecycle neg3 pass no 2xx for member/anon on any mutating door
    platform-core.marketplace-install-local-lifecycle neg4 pass purged rows absent after restart
    platform-core.runtime-config-boot-read 0 pass boot A anon GET /api/v1/runtime/config → 200 {"cloudUrl":"","singleEnvironment":true,"features":{"installLocal":true,"marketplace":true,"aiStudio":true,"autoPublishAiBuilds":false},"branding":{"productName":"ObjectOS","productShortName":"ObjectOS","pwaDescription":"ObjectOS — runtime console","pwaThemeColor":"#4f46e5"},"telemetry":{}} (cache-control private, no-cache)
    platform-core.runtime-config-boot-read 1 pass anon /api/v1/studio/runtime-config 200; cmp empty, both sha256 39916ba8…f44f3a (boot A); boot B alias also byte-identical
    platform-core.runtime-config-boot-read 2 pass anon 200 on boot A (default cloud arm, 41420) and boot B (OS_CLOUD_URL=off, 41424; body features marketplace:false), and again after boot B's 2nd restart
    platform-core.runtime-config-boot-read 3 pass key scan (token/secret/password/bearer/session/authorization/apikey/cipher/dsn): no hit; telemetry {} (no DSN configured); admin-session read byte-identical to anon on both boots, and admin read of the alias identical
    platform-core.runtime-config-boot-read neg0 pass no 401/403 on either path, either boot
    platform-core.runtime-config-boot-read neg1 pass alias present and byte-identical
    platform-core.runtime-config-boot-read neg2 pass no credential material
    platform-core.seed-mode-matrix 0 pass scratch package com.objectstack.qa.seedmodes (derived ns seedmodes), object seedmodes_item (name/val/note, public_read_write). upsert {A a1, B b1} → inserted 2; runtime PATCH A.note; upsert {A a1, B b2, C c1} → {inserted:1, updated:1}: A id tgFqBIs2… unchanged, updated_at 03:58:35.872 unchanged, note "runtime-edit" kept; B same id IsAOa59F…, b2, updated_at bumped; C new
    platform-core.seed-mode-matrix 1 pass update {A a2, D d1} → {inserted:0, updated:1}; total 3→3; filter name=D → total 0 (positive control name=A → 1)
    platform-core.seed-mode-matrix 2 pass ignore {A a3, E e1} → {inserted:1, updated:0}; A still a2 with updated_at 03:58:42.835 unchanged; E present; total 4
    platform-core.seed-mode-matrix 3 pass insert {A a-ins} → {inserted:1}; a second A row QIkPx60N… beside tgFqBIs2… (unconstrained object: duplication shape)
    platform-core.seed-mode-matrix 4 pass hand-inserted F (POST /data 201); replace {G} → {inserted:1, updated:0}; F survives (f-hand), G inserted, total 7. Source: writeRecord case 'replace' "Replace mode: just insert (caller should have cleared the table)"; decideWriteAction case 'replace': default: return { action: 'insert' }; no clear in seed-loader.ts / protocol-publish-batch.ts
    platform-core.seed-mode-matrix 5 pass SeedMode = z.enum(['insert','update','upsert','replace','ignore']) (5) at spec/src/data/seed.zod.ts; const mode = dataset.mode || config.defaultMode in metadata-protocol/src/seed-loader.ts; defaultMode: SeedMode.default('upsert') in seed-loader.zod.ts; packages/runtime/src/seed-loader.ts = export { SeedLoaderService } from '@objectstack/metadata-protocol'
    platform-core.seed-mode-matrix neg0 pass replay 1 of the unchanged upsert draft → {inserted:0, updated:1} (re-converged the duplicate A from a-ins to a1); replay 2 → {inserted:0, updated:0}, full row listing incl. updated_at byte-identical (sha256 ea3d6e53… both)
    platform-core.seed-mode-matrix neg1 pass update created no D; ignore did not rewrite A

    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

    Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

    Close-out for this run record (SKILL §5).

    One item failed, ai.mcp-run-action-exposure-gate, on two clauses: acceptance[4] is a stale spec, carried by the anchor's §1 re-point rows; negative[1] is a product defect, extracted. Six product defects observed beside the clauses are extracted; each card passed the pm-dispatch filing gate (class ①). A seventh observation (os build size counted in UTF-16 units) did not pass the gate and stays in the record's probe notes. The two partial-coverage gaps are parked on the wave anchor.

    Parked-on: #27377 · ai.mcp-run-action-exposure-gate · acceptance[4] stale spec after rulings 6513869839 (#27681) and 6523087116 (#27900): re-point (§1)
    Extracted: #28943 · ai.mcp-run-action-exposure-gate · negative[1]
    Extracted: #28944 · api-backend.formula-gates · observed beside the clauses
    Extracted: #28945 · api-backend.enforce-or-remove-authoring-gates · observed beside the clauses
    Extracted: #28946 · cli.qa-suite-execution · observed beside the clauses
    Extracted: #28947 · cli.migrate-meta-codemod · observed beside the clauses
    Extracted: #28951 · records-forms.validation-rule-type-matrix · observed beside the clauses
    Extracted: #28952 · platform-core.seed-mode-matrix · observed beside the clauses
    Parked-on: #27377 · cli.verify-verdict-exit-mapping · clause 3 rls-hole leg blocked (fixture: a hole cannot be staged on a healthy build)
    Parked-on: #27377 · platform-core.docs-audience-gate · clause 2 unknown-future-shape half blocked (no pin; unreachable live, knownGaps[2])
    

    Retests (§5 trigger 2): #27393, #27395, #27397, #27399 and #24782 all hold; #26478 and #26480 re-confirmed. The evidence is in the record's Retest section.

    The off-clause security observation was reported privately to the dispatching PM session; it has no card and no detail here.


    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