You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Round Tier-2A of the platform-checklist regression at #9296 — the 23 unpinned P0/P1 items on the api / cli / build surfaces. No item in this lane carries an automated.ref, so every clause below was driven by hand against a live server, a live CLI invocation, or the spec's own built contract face.
23 / 23 items reached a verdict. No handoff list — nothing was left unreached.
Environment fingerprint
framework sha (subject pin)
e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd (HEAD never moved)
.objectui-sha pin
665661ab093263f39f2e660a295ea615dbcee35a
console dist stamp
665661ab093263f39f2e660a295ea615dbcee35a — equal to the pin
file:/tmp/qa-wave/t2a-*.db + per-probe scratch DBs, rm -f before each first boot
spec (checklist)
re-synced mid-round from origin/main (git checkout origin/main -- docs/qa/platform-checklist/, check-platform-checklist: OK — 15 areas, 190 items). Never committed.
runner
session_01Ai9gUdihXjg7FumzsBNSSs (Tier-2A)
repo tree at close
clean apart from the intentional, uncommitted docs/qa/platform-checklist/ sync
Spec drift handled mid-round.origin/main moved after the QA tree was synced (#9385, #9366). The sync was taken at a clean point between items, before anything in this lane was scored. Only one owned item changed: cli.qa-suite-execution rev 2 → rev 3. It was scored against rev 3, i.e. against #9385's current behaviour (record action types resolve the Data Protocol mount from /discovery), not the superseded rev-2 expectation. Every other owned item's revision was re-resolved after the sync and was unchanged.
Derived totals — 15 PASS / 6 PARTIAL / 1 FAIL / 1 BLOCKED. Item verdicts are derived from the clause table below, never hand-assigned.
Both P0 items in this lane are accounted for: cli.dev-boot-contractpassed 7/7, platform-core.seed-integritypassed 5/5. No P0 failure. No authentication or authorization hole was found — every access-control clause in this lane proved both sides of its gate (entitled presence and server-side refusal for the unentitled persona, by forged request where the door allowed one).
Per-clause verdicts
Oracle evidence is text only, as required. → reads observed.
Live audience: share_new_inquiries_with_field_ops (showcase_inquiry, OWD private, recipient unit_and_subordinates: bu_field_ops). Member D was placed in bu_west_coast — a descendant of bu_field_ops, so the subtree walk is exercised, not just flat membership; outsider E in bu_hq_finance.
#
clause
verdict
oracle evidence
1
audience member reads the MATCHING record though OWD is private
pass
D: GET /data/showcase_inquiry/<Priya> → 200; D's list → total 1 ["Priya Raman"]. The share materialised on the BU-membership insert itself (share created_at 23:08:46.921 vs member insert .902); the explicit POST /sharing/rules/<name>/evaluate then reported {matchedRecords:1, expandedUsers:1, grantsCreated:0} — already present.
2
widening is scoped — both sides
pass
Four reads: D→matching 200, D→non-matching (Lena, status closed) 404; E→matching 404, E→non-matching 404 (pre-share). Neither list leaks the other's row.
3
widening is materialised + explain names SHARING as the deciding grant
pass
sys_record_share row shr_cb01e713…{source:'rule', source_id:'srule_c4f6be61-…', reason:'rule:share_new_inquiries_with_field_ops'} — the rule row id, not the name. POST /security/explain {object,operation:'read',userId:D,recordId:Priya} → record:{visible:true, decidedBy:"sharing"}, layer sharing verdict widens with rules:[{kind:'sharing_rule', name:'srule_c4f6be61…', effect:'admits'}] while owd_baseline is narrows. Same call for Lena → record:{visible:false}, effect:'neutral'.
4
manual one-off share grants exactly the target
pass
POST /data/showcase_inquiry/<Lena>/shares {recipientType:'user',recipientId:E} → 201; E reads Lena 200, third user D reads Lena 404.
5
§A13 — an admin edit to a materialised share survives restart
blocked(fixture)
The edit is unreachable: sys_record_share is API-read-only — PATCH/DELETE → 405 OBJECT_API_METHOD_NOT_ALLOWED … allowed:["get","list","aggregate","search","export"], PUT → 405, and the record-shares routes carry no update verb. The #2909 substance was nonetheless proved independently: after a full server restart on the same DB, both shares (rule-materialised and manual) re-read byte-identical and every access decision was unchanged (D: match 200 / non-match 404 / list 1; E: match 404 / non-match 200 / list 1). The seed does not clobber shares.
Personas A/B/C = three fresh sign-ups; probe showcase_private_note (OWD private).
#
clause
verdict
oracle evidence
1
PRE-grant isolation
pass
B GET /data/showcase_private_note/<A id> → 404 RECORD_NOT_FOUND; B's list total 0.
2
manual grant lands as a sys_record_share row
pass
POST …/shares → 201{id:"shr_ba4e283e…", recipient_type:"user", recipient_id:<B>, access_level:"read", source:"manual", source_id:null, granted_by:<A>}; confirmed by a system-context sys_record_share read.
3
grant widens B both ways and stays scoped
pass
B by-id 200 and B list total 1 ["95QFJ…"]; C by-id 404, C list total 0.
4
shares list is management-gated (ADR-0111 D5)
pass
A GET …/shares → 200 with the grant. B POST …/shares → 403PERMISSION_DENIED: managing shares on showcase_private_note/95QFJ… requires record ownership or Modify All Data (ADR-0111 D1). Reading confers no re-share authority.
DELETE /data/showcase_private_note/<other id>/shares/<shareId> → 404share shr_ba4e283e… not found on showcase_private_note/1wEywE3…; the share survived (A's re-read still lists it). Run before the real revoke, so survival is meaningful.
POST /sharing/rules/share_red_projects_with_execs/evaluate → 422SHARING_NOT_ENABLED: 'showcase_project' is not under record-sharing enforcement (public sharing model or no 'owner_id' field); a share row on it would never be consulted. See the fixture-gap section — the rule is inert on stock. The evaluate contract itself was proved on the enforceable sibling rule (see sharing-rules-widen clause 3): {ruleId, matchedRecords, expandedUsers, grantsCreated, grantsUpdated, grantsRevoked} and source_id === sys_sharing_rule.id.
All 7 clauses blocked(fixture). Not re-derived — two prior independent rounds already established it; this round only re-confirmed the premise from the boot banner: Tenancy: single, sys_organization count 0 at first boot (the #8686 diagnostic quotes "organizationCount":0). ADR-0095 Layer 0 is inert, every clause vacuous; OS_TENANCY_POSTURE=isolated is correctly refused because @objectstack/organizations is not declared by the host app.
Ledger action required — the item carries no blocked marker, so checklist-select.mjs presents it as runnable (--include-blocked changes nothing). This is the fourth round to reach the same dead end. See fixture gaps.
valid trio parses; every tombstone rejected LOUDLY with the retired key named
pass
Valid trio parses with documented defaults (surface:"ask", active:true on agent and skill). All 10 tombstone/guidance probes rejected, each naming its key and its prescription: tool.permissions ("promised a capability gate on tool invocation that nothing ever enforced"), tool.active, tool.category, tool.builtIn, tool.requiresConfirmation (#3896 / #3715 + ADR-0033 §2); agent.tools ("use skills … (ADR-0064)"), agent.knowledge; skill.triggerPhrases ("phrases were never matched … activation is triggerConditions"); guidance-map probes skill.permissions and skill.trigger. Zero silent strips.
2
registry serves all three kinds under SINGULAR names
pass
GET /meta and GET /meta/types both list 34 kinds including agent, tool, skill; agents / tools / skills all absent.
3
authored items round-trip field-identical with schema defaults applied
blocked(fixture)
No stock example app ships AI seeds (the item's own knownGap), and this run writes nothing into the repo, so the build-authored round-trip could not be exercised. Runtime-authored substitute: PUT …?mode=draft → publish → GET /meta/skill/qa_probe_skill echoes every authored value plus _diagnostics:{valid:true} — but not the documented schema defaults surface:'ask' / active:true, which the contract face does apply. Recorded as the thing to check when the fixture lands.
4
runtime write door matches the declared registry posture — both sides
pass
PUT /meta/skill/qa_probe_skill?mode=draft → 200{state:"draft", seq:1}; PUT /meta/tool/…?mode=draft → 200seq:2; PUT /meta/agent/qa_probe_agent?mode=draft → 403[not_creatable] Metadata type 'agent' is code-only: the metadata-type registry declares allowRuntimeCreate=false and allowOrgOverride=false … Declare it in source (**/*.agent.ts) and redeploy. Recorded as CORRECT, not a bug. (Draft read-back is ?state=draft; ?mode=draft is the write-side param and 404s on read — a wording nit, not a defect.)
5
anonymous-deny on the AI meta surface
pass
Unauthenticated GET /meta/agent, /meta/tool, /meta/skill → 401UNAUTHENTICATED on all three.
6
tool metadata is a read-only projection
pass
POST /ai/tools/qa_probe_tool/execute → 501{code:"NOT_IMPLEMENTED", message:"Provided by @objectstack/service-ai in ObjectStack Cloud/Enterprise — no implementation ships in the open framework"}. Authored ⇏ callable.
Over MCP Streamable HTTP with a minted osk_ key (POST /api/v1/keys → 201).
#
clause
verdict
oracle evidence
1
undeclared action rejected fail-closed with the exact prescription; zero side effects
pass
run_action{actionName:'showcase_mark_done', objectName:'showcase_task', recordId:<a NOT-done task>} → isError:true, text exactly 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 todo, done false, **updated_atunchanged**. Refusal reproduced a second time,isError:true` both times.
2
list_actions returns only ai-exposed + permitted
pass
Global call and objectName:'showcase_task' call both return exactly {actions:[showcase_portfolio_snapshot], totalCount:1}. showcase_mark_done absent from both. The two doors agree.
Server log: [action-audit] MCP run_action 'showcase_portfolio_snapshot' on 'global' — body executes TRUSTED (system-elevated context, RLS/FLS-bypassing) for user 'SYkUFihutw3CQtflO98VqdK7NOWSaQqF'.
5
system objects refused wholesale
pass
run_action{objectName:'sys_user'} → isError:true, Object "sys_user" is a system object and its actions are not exposed via MCP. No side effect.
Three checklist-accuracy corrections came out of this item; none is a product defect.
#
clause
verdict
oracle evidence
1
discovery bit true exactly when /batch is mounted and tx-capable
pass
GET /discovery → capabilities.transactionalBatch = {enabled:true, description:"Atomic cross-object batch endpoint (POST {basePath}/batch) …"}, and the atomic cross-object batch (create showcase_private_note + create showcase_task in one call) succeeded end-to-end — both rows verified present afterwards. Capability read and live behaviour agree.
2
atomic:false on the cross-object door → 400 BATCH_NOT_ATOMIC
pass
POST /batch {operations:[…], atomic:false} → 400{error:"Cross-object batch is always atomic; use POST /data/:object/batch for non-atomic per-object batches", code:"BATCH_NOT_ATOMIC"} — the message points at the per-object door exactly as specified.
3
a failing member rolls the whole atomic batch back with the #4793 per-row codes
pass
Rollback proved on the cross-object door: a validation-time failure (showcase_invoice missing required fields) → 400, and a write-time failure (update of a nonexistent id) → 404 — in both cases neither flanking showcase_private_note create persisted. The #4793 per-row codes are emitted by the per-object door: POST /data/showcase_private_note/batch {options:{atomic:true}} → [{ROLLED_BACK},{VALIDATION_FAILED},{NOT_ATTEMPTED}] in exactly those positions, and the first row did not persist. Both conjuncts proved; the clause attaches them to one door.
4
oversize batch refused up front, creates nothing
pass
201 operations → 400{code:"BATCH_TOO_LARGE", count:201, max:200}; a system-context count of the probe-titled rows is 0.
5
per-object door defaults to NON-atomic per-row outcomes (ADR-0119 D4)
pass
Default (atomic unset): [{ok},{VALIDATION_FAILED},{NOT_ATTEMPTED}], succeeded 1 / failed 2, row 1 stayed written — no rollback, so atomic does default false. The "neither blocks the other" reading is the separate continueOnError dial (default also false, #7539): with options:{continueOnError:true} → [{ok},{VALIDATION_FAILED},{ok}], succeeded 2 / failed 1, both valid rows persisted.
api-backend.formula-gates · rev 2 · pass (4/4)
#
clause
verdict
oracle evidence
1
the three runtime shapes evaluate against the BUILT package
pass
Harness imported /home/user/objectstack-qa-wave/packages/formula/dist/index.js — resolved path captured, dist/, never src/. record.due_date == today() → {ok:true,value:true} for today's ISO date and {ok:true,value:false} for 2020-01-01; record.flag ? record.x : null → 42 / null; floor(3.7) → 3; ceil(3.2) → 4.
2
date arithmetic is a build-time ERROR
pass
Scratch config with Field.formula({expression:'record.end_date - record.start_date + 1'}) → os buildexit 1.
3
the build error is LOCATED and actionable
pass
• object 'qa_probe' · field 'qa_duration_days' expression: date arithmetic \google.protobuf.Duration + int` — record.start_date is a date, and CEL can't do arithmetic on dates: this faults at runtime, so the field silently evaluates to null. Use `daysBetween(a, b)` … `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)`+source: `record.end_date - record.start_date + 1`+rule: expression-invalid at object 'qa_probe' · field 'qa_duration_days' expression`.
4
the gate does not over-fire
pass
os build on the untouched showcase → exit 0 (✓ Build complete (2082ms), artifact 690.1 KB), despite its legitimate due_date == today() comparisons.
each tombstoned key REJECTED at parse/build, never silently stripped
pass
QuerySchema: cursor, joins, distinct, windowFunctions — all four rejected (control: a valid query parses). EnhancedApiErrorSchema.fieldErrors rejected. ObjectSchema display annotations helpText/displayHint/uiNote/color — all four rejected. Retired filter operators enforced at the live driver door: GET …?$filter={"name":{"$regex":"North"}} → 400.
2
every rejection carries LOCATED guidance naming the replacement
pass
cursor → "no driver ever implemented keyset pagination … QueryBuilder.cursor() was removed with it"; joins → "Related records are read through expand"; distinct → names the mis-wired total/hasMore degradation; windowFunctions → *"a SQL-driver capability behind SqlDriver.findWithWindowFunctions(…)"*; fieldErrors→ *"renamed tofieldsin spec 17 (ADR-0114 D4, #3977) — the array is unchanged, only the property name"* (andfieldsparses clean);$regex→ *"is RETIRED … Write$icontainsinstead"* with the registry'swhy` quoted verbatim. No bare "unknown key".
3
a key documented as silently-stripped-BY-DESIGN still parses clean
blocked(fixture)
No such key was locatable across the sampled surfaces. The shapes that remain open are documented .passthrough() — dashboard.zod.ts:252"declared query keys + open renderer extras", view.zod.ts Gantt config, bulk-action.zod.ts params/options — i.e. open-by-design that keeps and forwards unknown keys, the opposite of a strip. Every display annotation probed is now a loud strict rejection (#4001/#1535, ADR-0085 closed the former detail:{}.passthrough()). Scoring a keep-by-design as a strip-by-design would be ticking on a label.
Seven samples, one per family; membership checked against StandardErrorCode (53) ∪ ERROR_CODE_LEDGER (22) ∪ REGISTERED_ERROR_CODES (237), read from the built spec.
validation sample carries fields[] with FIELD-level codes
pass
POST /data/showcase_invoice {} → {error:"Invoice Number is required; Account is required", code:"VALIDATION_FAILED", fields:[{field:"name",code:"required",message:"Invoice Number is required",label:"Invoice Number"},{field:"account",code:"required",…}]}; required ∈ FieldErrorCode (28 members, lowercase snake).
3
retired fieldErrors never appears
pass
Key-absence assertion across all 7 captured bodies → absent everywhere.
4
HTTP status matches the declared mapping
pass
Status-vs-code table matches HttpStatusErrorCodeMap tier for tier (400 validation/filter/param, 401, 403, 404 ×2). No 5xx on any caller-fixable input.
5
the two 404 flavours are distinguishable
pass
Object: {error:"Object 'not_a_real_object' is not registered", code:"OBJECT_NOT_FOUND"} vs record: {error:"Record nonexistent-id-0000 not found in showcase_task", code:"RECORD_NOT_FOUND"} — different codes and different messages.
6
one sample per family variant
pass
validation · auth · authorization · not-found (object) · not-found (record) · batch registered-extension code · query-rejection = 7 families, each with its own verdict.
ACCEPTED_FILTER_COMPARAND_TYPES read live from the built spec = ["string","number","bigint","boolean","null","Date"]; FILTER_COMPARAND_BIGINT_EXACT_LIMIT = 9007199254740992.
#
clause
verdict
oracle evidence
1
every member accepted, answer set reconciles against a directly-computed expectation
pass
REST door (JSON-expressible members), each 200 with the answer set matching a directly-computed count: string total 1=1 · number {tax_rate:8} 6=6 · boolean {done:true} 2=2 · null {paid_on:null} 7=7 · Date-as-ISO {issued_on:{$gte:'2026-01-01'}} 10=10. bigint and Date are not JSON-expressible, so they were exercised at the shared contract face: 7n → accepted, narrowed to number 7; new Date(…) → accepted, narrowed to ISO 8601. Six for six.
2
a non-accepted comparand is REFUSED, located and prescriptive — never dropped
pass
Plain object at $eq → 400 Filter comparand at where.tax_rate.$eq is a plain object ({"a":1}), which no driver can compare. A comparison value must be a string, number, bigint, boolean, null or Date. Refusing rather than guessing… — path named, accepted set quoted. Also refused: array in scalar and implicit positions, undefined ("cannot be told apart from an omitted key, yet the two mean OPPOSITE things"), function, Symbol. The dangerous direction was probed explicitly: every bad predicate returned 400; none returned 200 over an unfiltered table.
3
bigint exact-integer limit enforced at the boundary
pass
2n**53n → accepted, 9007199254740992 (number). 2n**53n+1n and -(2n**53n)-1n → refused: "the bigint 9007199254740993n, whose magnitude exceeds 2^53 — it has no exact JS-number form, so a comparison built on it would silently answer the wrong rows". No rounding.
4
dotted heads CLASSIFIED and handled per contract
pass
classifyDottedFilterHead: lookup/master_detail → relation, text/summary → scalar, formula → virtual, json → null, multiple:true → null. All three reachable classes refused over REST with class-specific text: relation head owner.name → "follows the relationship … a filter reaches only columns of 'showcase_invoice' itself"; scalar head name.sub → "a 'text' field … stores a single scalar value — there is nothing beneath it"; virtual head budget_remaining.x (a real formula field) → "a dotted path whose head 'budget_remaining' is a virtual 'formula' field … computed on re[ad]" — so the virtual class was NOT blocked(fixture) here. A scalar head is never traversed as a relation.
5
the refusal is CONTRACT-level, not a transport quirk
pass
The same bad comparand through three doors — GET ?$filter=, POST /query {filters}, POST /query {where} — all 400 INVALID_FILTER with byte-identical message text, and the same refusal at the spec's normalizeFilterComparandTypes face.
Registry: retired-defs holds 54 entries — 53 at prefix 17, 1 at prefix 18.
#
clause
verdict
oracle evidence
1
every sampled retired definition REFUSED at os build
pass
Four sampled defs declared as stack collections on a scratch app: externalDataSources, widgetManifests, messageQueueConfigs, apiKeys → all non-zero exit (2, the oclif error exit for a load-time defineStack throw), each Unrecognized key(s) on this stack definition: \`. Until #8687 closed this surface (the outermost #4001 door), an unknown top-level stack key parsed green and its value was silently dropped…`. No green build with a family silently absent.
2
refusal distinguishable from generic where a prescription WAS authored
pass
Route recorded per def from each entry file: 17.data__ExternalDataSource and 17.system__MessageQueueConfig → Route 3 ("no tombstone, no D2 conversion — this table plus the D3 entry are the declaration"); 18.identity__ApiKey → #8715"disposition B: delete"; 17.ui__WidgetManifest → bare table entry, no prescription authored. All four are prescription-less by design, so per the item's own knownGap the generic refusal is the CORRECT answer — and that is what was observed: the four refusals are identical in shape to the control (totallyInventedThing), differing only in the echoed key name.
3
runtime metadata-write door refuses the same types
pass
PUT /api/v1/meta/<type>/qa_retired_probe?mode=draft for external_data_source, widget_manifest, message_queue_config, api_keyand the control totally_invented_type → 400 on all five: [invalid_request] '<type>' is not a metadata type. The platform declares no such type, and since #8586 retired 'additionalTypes' a plugin cannot declare one…. No two-door disagreement. (Registered types: 34, none of the sampled defs among them.)
4
sample spans both version prefixes
pass
17.data__ExternalDataSource, 17.ui__WidgetManifest, 17.system__MessageQueueConfig (prefix 17) + 18.identity__ApiKey (prefix 18), across four domains: data/, ui/, system/, identity/.
--seed-admin is idempotent — a later --admin-password does NOT overwrite
pass
Reboot on the SAME DB with --admin-password changed99: sign-in with the ORIGINAL admin123 → 200; sign-in with changed99 → 401. Zero-user-DB-only, confirmed.
3
DB selection honours the one shared resolution matrix (#6469)
pass
Tier for tier, from the printed Database: key-value: explicit -d → file:/tmp/qa-wave/t2a-a.db; env OS_DATABASE_URL → file:/tmp/qa-wave/env-chosen.db; -d :memory: → :memory: (no file default imposed); legacy compat-read → see below; nothing chosen → file:<project>/.objectstack/data/objectstack.db — the persistent project-anchored unified default, never :memory:, and the file was verified on disk (1.7 MB). Legacy tier: with only dev.db present, the boot read …/.objectstack/data/dev.db and printed ⚠ Reading legacy database file … — the unified default is now …/objectstack.db (#6469); migrate with: mv "…/dev.db" "…/objectstack.db" … or pin it explicitly via OS_DATABASE_URL=… — and did not create a fresh empty objectstack.db beside it. Both halves of the negative satisfied. (The "config-declared default datasource" tier is not supplied by the stock fixture — the showcase config declares no default datasource; no variant existed to run.)
4
--fresh isolates OS_HOME state in an auto-deleted tempdir
pass
🧪 Fresh OS_HOME: /tmp/objectstack-dev-z6lz37, its DB file:/tmp/objectstack-dev-z6lz37/data/objectstack.db — the SAME unified filename under the ephemeral home. After exit the directory is gone. The app-declared cwd-relative examples/app-showcase/.objectstack/data/showcase_external.db survived with an unchanged mtime — annotated as the documented #5594 carve-out, not filed as a bug.
5
a busy requested port auto-shifts AND is reported
pass
Instance B on a busy 3131 printed ↪ server bound to port 3133 (requested 3131) — and every printed URL (API, Console, MCP endpoint, MCP skill) used the ACTUAL bound port 3133, never the requested one.
6
a stale artifact is called out at boot, and still boots
pass
With dist/objectstack.json aged behind the sources: ⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build. / newest source: src/ui/views/contact.view.ts (2421d newer than the artifact) / fix: run \objectstack build` or start with `--compile`, or save a watched file to trigger a rebuild + restart.— and/api/v1/health` → 200. Warn, never gate.
7
no config and no artifact exits 1 with the remedy
pass
From an empty scratch dir: stdout ✗ Config file not found in /tmp/qa-wave/scratch-empty, stderrRun in a directory with objectstack.config.ts, pass --artifact <path|url>, or run from the monorepo root., real echo $? = 1. No hang, no silent 0.
cli.build-own-contract · rev 2 · pass (6/6)
On a scratch app outside the repo (/tmp/qa-wave/app), so no deliberate breakage touched a shared fixture.
a schema violation fails with a LOCATED error naming the failing path
pass
Bad field type → ✗ objects.0.fields.name.type: Invalid field type 'not_a_real_type'. Valid types: text, textarea, email, … — the path is named, never a bare "validation failed". Exit 1 on the --json path; 2 on the human path, which is the oclif this.error() exit that clause 5 explicitly blesses.
3
author-time rule failures exit 1; ALL failing rules report at once
pass
Two objects each missing sharingModel → ✗ Author-time rules failed (2 issues) listing both, each with where/message/hint/rule: security-owd-unset/at objects[N].sharingModel. Exit 1. Not first-failure-only.
4
--json failure/advisory shapes match os validate --json
pass
build --json → {"success":false,"error":"author-time rules failed","issues":[{severity,rule,where,path,message,hint}…]}; validate --json → {"valid":false,"errors":[{severity,rule,where,path,message,hint}…]} — identical per-issue keys. Success payloads carry warnings + conversions under the same names.
5
#4873 exit-code honesty — a STATUS, never a computed value
pass
Every observed exit ∈ {0, 1, oclif 2 from a this.error() path}. Stability proved by repetition: success repeated → 0, 0; --json success → 0; schema break repeated → 2, 2 (and 1 on --json); rule failure repeated → 1, 1. Nothing varied between identical runs; no code tracked a duration or a finding count.
6
advisories never flip the exit
pass
os build on the untouched showcase: ⚠ 26 author-time warning(s) — see above (e.g. rule: security-master-detail-ungranted at objects[6].fields.expense_report), the artifact was written (690.1 KB) and echo $? = 0. Both sides of the warn/fail line. (The specific #3786 undeclared-authoring-key advisory is no longer producible — #4001/#8687 turned undeclared keys into hard errors; the warn/fail property was proved with a live advisory class instead.) Alias: os compile on the same input behaves identically (exit 0, same artifact line).
cli.qa-suite-execution · rev 3 · pass (8/8)
Scored against the post-#9385 revision. Stock mount, so neither the crud.dataPrefix nor the api.apiPath case bites — as rev 3 states.
#
clause
verdict
oracle evidence
1
the green path closes
pass
Found 1 test suites. (N > 0, quoted as the clause demands), ✅ Scenario: health-liveness (48ms), ✅ Scenario: category-create-read-roundtrip (571ms), SUCCESS: All 2 scenarios passed., echo $? = 0.
2
state threads through the run (capture → {{var}} → later step)
pass
The roundtrip passed AND was confirmed independently by server state: showcase_category held exactly 4 rows before, 4 after, with identical names — the teardown DELETE …/{{categoryId}} actually removed the created row. Dead interpolation would have 404'd on the literal path and left a 5th row.
3
a false assertion fails the scenario and the run
pass
❌ Scenario: false-assertion-probe / Step Failed: GET health expecting degraded / Assertion failed: data.status expected degraded, got ok; exit 1.
4
#7256 — a non-evaluable assertion FAILS, it does not pass
pass
Assertion failed: data.nope cannot be evaluated by 'contains' — expected an array or a string at that path, got undefined. The path resolved to nothing — the field is absent from the result, or the path is misspelled. Use 'is_null' if asserting absence is what you meant.
qa/a-malformed.test.json is not a valid Quality Protocol suite (TestSuiteSchema): / ✗ scenarios.0.steps: Invalid input: expected array, received undefined / Expected shape: {…} / Reference: content/docs/references/qa/testing.mdx. The two sibling suites still executed after it, it counted as a failed suite, exit 1, no TypeError inside runSuite.
6
teardown runs after a failed step; a passing teardown does not rescue the verdict
pass
Staged create→capture→deliberate-fail→teardown-delete: scenario verdict stayed ❌, and QA-T2A-TEARDOWN-PROBE is absent from the showcase_category listing afterwards.
7
all 8 TestActionTypeSchema members carry their OWN verdict
pass
create_record ✅ · read_record ✅ · update_record ✅ · query_records ✅ · delete_record ✅ · api_call ✅ · wait ✅ · run_script ❌ Unsupported action type in HttpAdapter: run_script. Exactly one refusal — the honest verdict rev 3 predicts. The five record-shaped types executed (post-#7848).
Bare os test from examples/app-showcase found the shipped suite with no argument. os test 'qa/**/*.test.json' from the same dir → Found 1 test suites., SUCCESS, real 3.99s (not the pre-#7363 multi-minute walk). Zero-match glob → Found 0 test suites. / No test files found matching: qa/nothing-matches-*.test.json / Exiting 0 — an empty match is not a failure. Pass --fail-on-empty to make it one., exit 0 — the declared #7848 posture.
every planted retired spelling is REWRITTEN to its declared successor
FAIL
ui/ListView:striped (registry entry packages/spec/src/migrations/entries/retired-keys/17.ui__ListView__striped.ts, D2 conversion view-list-passthrough-keys-removed) is left untouched: os migrate meta --from 16 exits 1 without rewriting anything. Reproduction rule below.
2
the rewritten tree BUILDS
FAIL
There is no rewritten tree. os build after the codemod run produces the identical pre-migration refusal.
3
idempotent — a second run changes nothing and exits 0
pass
On an already-migrated (retired-key-free) tree: two consecutive --json runs produced byte-identical output and exit 0; md5sum -c confirmed nothing was rewritten in place (the default is a read-only replay; in-place effects need --out or --stored --apply).
4
a --from below the floor is REFUSED loudly
pass
On a loadable tree: --from 3 → exit 1, ✗ Cannot migrate from protocol 3: the chain's support floor is 10 (ADR-0087 D3). Upgrade to protocol 10 by another path first, then re-run.; --json → {"error":"unsupported_from_major","message":"…"}. Caveat recorded: on a source that carries a retired key the floor check is unreachable — the load aborts first (a consequence of clauses 1–2).
5
semantic entries surfaced as ADVISORIES, distinguished from mechanical rewrites
pass
--json on a clean tree separates them into two arrays: applied:[{conversionId:"field-required-notnull-explicit", surface:"object.fields.*.required / …", path:"objects[0].fields.name.storage.notNull"}] vs todos:[{id:"action-descriptor-is-async-retired", surface:"ActionDescriptor.isAsync …"}]. Nothing advisory is reported as "migrated".
Exit 0 with the {from:16, to:17, runtime:"17.0.0", applied:[…], todos:[…]} payload.
Registry sizes recorded as the item asks: retired-defs54, retired-keys38, semantic104. Advertised majors, grepped from the retirement messages: os migrate meta --from 16 appears 132 times, --from 174 times.
cross-owner GET/DELETE/run all 404 REPORT_NOT_FOUND
blocked(fixture)
The reports service is not installed on stock showcase: every /api/v1/reports* call by an authenticated caller answers 501 {"code":"NOT_IMPLEMENTED","message":"Reports service is not configured on this deployment"}. packages/plugins/plugin-reports exists in the repo but is absent from the showcase's 48-plugin boot list.
2
the list never leaks; ownerId cannot be spoofed
blocked(fixture)
same 501
3
anonymous access answers 401 UNAUTHENTICATED
pass
Anonymous GET /api/v1/reports and GET /api/v1/reports/:id → 401{"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}. Notably the auth gate runs before the service-availability check, so an anonymous caller learns nothing about whether the service exists — the correct anti-enumeration ordering.
the request issues a real reset artifact carrying a token
pass
POST /api/v1/auth/request-password-reset → 200. The artifact was captured as server truth rather than log text: the sys_email row for the target reads subject:"Reset your ObjectStack password", status:"sent", body containing http://localhost:3131/api/v1/auth/reset-password/qvO9EIo2noPw7MPB5p1FC3WU?callbackURL=… and link expires in 60 minutes. (Oracle substitution recorded: sys_email outranks the clause's log oracle.) Exactly one row — the unknown-email request produced none.
2
the token completes the reset and rotates the credential
pass
POST /auth/reset-password {token,newPassword} → 200 {"status":true}; sign-in NEW → 200; sign-in OLD → 401INVALID_EMAIL_OR_PASSWORD.
3
a consumed token cannot be reused
pass
Second reset with the same token → 400{"message":"Invalid token","code":"INVALID_TOKEN"}; the would-be third password does not sign in (401); the second password still does (200).
4
an expired/tampered token is refused LOUDLY, credential unchanged
pass
Garbage token and a two-character-tampered token both → 400INVALID_TOKEN; the tampered password never signs in (401) and the current one still does (200). No silent success.
5
request does not leak account existence
pass
Known and unknown emails return an identical200 {"status":true,"message":"If this email exists in our system, check your email for the reset link"}. The server-side [Better Auth]: Reset Password: User not found stays in the log and never reaches the caller.
Route divergence confirmed live, as the item's steps predict: /api/v1/auth/request-password-reset serves (200) while /api/v1/auth/forget-password — the spelling in the spec's AuthEndpointPaths — 404s.
Upstream broken without mutating any repo source: the server was launched with node removed from the child PATH, so the connector's spawn('node', ['./scripts/mcp-fixture.mjs']) fails ENOENT — an operational failure, deterministic and restart-free to reverse.
#
clause
verdict
oracle evidence
1
boot COMPLETES; the instance lands as a visible degraded husk
pass
/api/v1/health → 200 on the dead upstream. Descriptor: {name:"showcase_mcp_tools", origin:"declarative", state:"degraded", degradedReason:"connector-mcp provider: connector 'showcase_mcp_tools' could not reach its MCP server: spawn node ENOENT", actions:[]} — still listed in GET /automation/connectors (5 connectors returned). Boot warn: Connector registered DEGRADED: showcase_mcp_tools (origin: declarative) — no actions and no handlers until its upstream is reachable … (#3017).
2
degrade only for upstream-unavailable; a configuration fault stays FATAL — both sides
pass
Degrade side above. Fatal side: a boot whose connector instance declares provider:'rest' with no provider factory registered aborted — ERROR Plugin startup failed: com.objectstack.service-automation … no provider factory is registered. Install the connector plugin that supplies it … (ADR-0097) / ✗ Plugin … failed to start - rollback complete. A configuration fault, fatal at boot, carrying the factory's own validation error. (Substituted twin recorded: the item names an invalid providerConfig shape, which would require editing a tracked source; the missing-factory fault is the same classification side.)
3
dispatching a degraded instance fails FAST quoting the stored reason
pass
Creating a showcase_task returned 201 in 1.2 s (no hang); the trigger-fired run logged Node 'echo' failed: connector_action 'echo': connector 'showcase_mcp_tools' is degraded — … spawn node ENOENT. Dispatch is unavailable until its upstream recovers; the platform retries automatically (#3017). — the stored degradedReason verbatim, clearly distinct from "no such connector/action".
The node shim was placed at 23:41:21; the next scheduled retry at 23:41:39 logged Connector 'showcase_mcp_tools' replaced with no server restart. Descriptor then: state:"ready", degradedReason gone, actions:["echo_upper"] re-derived from tools/list. A subsequent record-create produced no further dispatch failure.
federated objects in /meta; fixture rows over the normal /data path; filters/$top work as native
pass
/meta/object carries showcase_ext_customer and showcase_ext_order, both datasource:"showcase_external". /data/showcase_ext_customer → 3 rows (Aurora Labs / Borealis GmbH / Cyan Pacific); /data/showcase_ext_order → 4 rows. $filter={"region":"EU"} → total 1 (Borealis); $top=2 → 2 of 3; $orderby=lifetime_value desc → 480000 / 312000 / 95000 in order. Federation is transparent to the query layer.
2
writes to a read-only external object refused with a located error
pass
POST, PATCH and DELETE all → 403{"code":"EXTERNAL_WRITE_FORBIDDEN","error":"Write '<op>' 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)."} — names the datasource, the mode and both dials with observed values. Row set byte-identical afterwards (3 rows, same names).
3
the connection is visible and health-badged
pass
Server-truth oracle (outranks the page, which renders from it): GET /api/v1/datasources → both connections with status:"ok", showcase_external carrying schemaMode:"external", origin:"code", active:true; GET /api/v1/datasources/showcase_external returns the full descriptor with hasSecret:false. Recorded substitution: the clause is worded for the Setup page in an api-surface item; the console page was not driven.
every seeded object's row count equals the authored count
pass
Counts re-derived from the app's own built seed rather than the item's stale prose baseline, then compared to GET /data/<object>?$top=1 totals. 18 of 18 objects match, zero mismatches: account 14 · contact 32 · inquiry 3 · product 4 · project 5 · task 10 · category 4 · business_unit 9 · sys_business_unit 5 · team 2 · project_membership 3 · field_zoo 2 · invoice 12 · invoice_line 5 · expense_report 4 · expense_line 13 · preference 1 · announcement 2. Sum = 130, exactly the boot banner's Seeds: com.example.showcase 130 rows.
2
seed VALUES land verbatim
pass
Specimen — Full field-by-field: f_multiselect ["red","green"] as a set · f_json {"nested":{"k":"v"},"list":[1,2,3]} structurally intact · f_lookup resolved to the real Northwind id cY2IjNHmR8eYIuvc (f_lookups → Northwind + Contoso) · f_master_detail → the Website Relaunch project id · f_composite, f_repeater, f_record, f_vector, f_location, f_address, f_tags all byte-equal to the authored literal.
3
the boot log reports every seed load as success
pass
Fresh boot: zero rejected/failed seed entries; the 3 boot warnings are the known #8686 autonumber tenancy-split notice and two SharingServicePlugin rule-backfill warnings (neither a seed load).
4
seed replay is idempotent
pass
Restart against the SAME file DB → second count sweep diffs empty against the first, all 18 objects. Invoice/invoice-line values re-read identical. (The replay boot emits 8 Field '<x>' is read-only (readonlyWhen) — ignoring incoming change warnings on the upsert path — tax_rate ×5, product/quantity/unit_price; the stored values are unchanged, so no row or value is affected. Noted, not scored as a rejection.)
5
deliberately-unseeded fields stay unseeded
pass
f_user, f_users, f_secret all null on both specimens; showcase_task.covernull on every sampled row. Recorded as expected-absent (#4891 / ADR-0104), not as findings.
Corpus: exactly one authored book — showcase_manual, audience: "public", 3 groups + a synthetic uncategorized — and 9 docs, none with an explicit audience.
#
clause
verdict
oracle evidence
1
correct refusal distinction — 401 anon vs non-public, 403 non-holder vs permissionSet
blocked(fixture)
401 half proved: the implicit per-package book com.example.showcase (audience org) → anonymous 401{"error":"This documentation requires sign-in","code":"UNAUTHENTICATED"} — not a bare 404, not an empty-but-200 tree. The 403 half is unsupplied: no {permissionSet}-gated book exists in the corpus, exactly as the item's knownGaps predict.
2
the audience vocabulary holds all three ways
blocked(fixture)
Two of three cells proved: public serves anonymously (showcase_manual → anon 200, 8 entries); org/unset serves any authenticated principal and refuses anonymous (implicit book → admin 200 / plain member 200, both 9 entries; anon 401). The {permissionSet} cell has no fixture — recorded blocked per the clause's own instruction, not passed.
3
the permissionSet branch FAILS CLOSED (ADR-0049)
pass
The spec's own pin was run: packages/spec/src/system/book.test.ts → 30/30 passed, including line 222 expect(audienceAllows({ permissionSet: 'crm_admin' }, unresolved)).toBe(false) — the exact permissionSets: undefined → false case the clause names. Direct exercise of the exported contract agrees, and an unknown future audience shape also → false.
4
gating is TWO-LAYERED — entries additionally filtered by each doc's audience
pass
Entry-set diff on the public book: admin 9, member 9, anon 8 — the anonymous tree drops setup_overview and its whole uncategorized group. Every one of the 9 entries the member's tree offers was then fetched as the member: 9/9 resolved 200, zero 401/403. No nav entry refuses on click.
5
an UNCLAIMED doc is never exposed through a public book
pass
setup_overview is the unclaimed orphan (synthetic uncategorized group, no authored membership claim). It is absent from the anonymous fetch of the public book's tree, and a direct anonymous GET /meta/doc/setup_overview → 401, while the claimed showcase_index → 200 anonymously.
6
an unknown book name resolves as the implicit per-package book, audience org
pass
GET /meta/book/com.example.showcase/tree → admin 200 with a resolved tree (9 entries, group all), anonymous 401. Not a 404.
7
doc locale resolution on the tree path; book inline translations refused at authoring
pass
With Accept-Language: zh-CN the tree renders showcase_index as "Showcase 示例" with a Chinese description, other entries falling back. BookSchema refuses inline translations: "Inline translations on a book … was removed in spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it … NOTE the near neighbour that DOES work: doc.translations is live" — the trap named explicitly.
Also confirmed from the item's negatives: a repeated ?package= query parameter is refused, not coalesced — 400 {"code":"VALIDATION_ERROR","message":"The \"package\" query parameter was supplied 2 times. Supply it at most once — this endpoint will not choose between conflicting values."}.
six variants, six distinct ledgered per-type field codes
pass
All 400 VALIDATION_FAILED, fields[0].code as ledgered: invalid_initial_state (project created status:'completed') · invalid_transition (legal planned→active 200, then illegal active→planned) · invalid_format ×2 (regex tax_id:'BADFORMAT', named-format billing_email:'not-an-email') · json_schema_violation ({tier:'gold',seats:0}) · invalid_json ('{not json') · rule_violation from three rule kinds (conditional, cross_field, script). None inferred from a sibling.
2
the error targets the declared field
pass
state_machine → status (both variants) · format → the formatted field (tax_id, billing_email) · json_schema/invalid_json → support_config · cross_field → start_date, i.e. fields[0], exactly as the spec's own comment says · script/conditional → _record (object-level, correct for a rule declaring no field).
3
no rejected write persists
pass
Post-rejection sweeps of showcase_account and showcase_project: 0 rows matching any rejected probe name. The state_machine probe project remained status: "active" after the illegal transition attempt.
4
every happy-path twin lands 2xx
pass
Six twins, all 201: tax_id:'12-3456789' · billing_email:'ok@example.com' · {tier:'premium',seats:5} · a non-churned account · start_date < end_date · spent inside budget (plus status:'planned' for the FSM entry point). The rules gate violations without blocking legitimate writes.
search.rls-both-personas · rev 2 · pass (4/4)
Persona: a fresh sign-up bound to contributor via sys_user_position; /security/explain confirms positions:["org_member","contributor","everyone"], layer rls verdict narrows, readFilter: {"owner":"<member email>"} — the RLS is genuinely in force.
#
clause
verdict
oracle evidence
1
zero hits for RLS-hidden rows for the member; the identical admin search returns them
pass
Premise guard first: member GET /data/showcase_invoice/<INV-1003> (linus's) → 404RECORD_NOT_FOUND. Then POST /data/showcase_invoice/query {search:'INV-1003'} → member total 0, records [] (reproduced twice); admin, identical query → total 1, ["INV-1003"].
2
no count leakage
pass
The member's paged totals never reveal the seeded population (12 invoices; admin's own full read is 12+). With search present the protocol computes a page-local total from the RLS-filtered find: member {search:'INV',limit:2} → total 1, n 1 (matching their 1 visible row); admin the same query → total 3, hasMore true (2 returned + 1 more). No response exposes the true population to the restricted persona, and no hit or inflation derives from an invisible row — the item's own definition of the failure. (Wording note for the checklist: the page-local total equals skip + n (+1 when hasMore), so at skip:8 the member sees total 8 with 1 visible row. The number is a function of the caller's own skip and is shaped identically for admin, so it is not a leak — but "reconcile exactly with the rows they can enumerate" only holds while skip ≤ visible count.)
3
the restriction is subtractive, not a blackout
pass
The member created their own invoice (INV-QA-RLS) and searching for it returns it — total 1, ["INV-QA-RLS"]; their full visible set is exactly that 1 row.
4
RLS composition is structural, proven by the persona split
pass
The evidence set is complete: deny (INV-1003, member 0 hits / admin 1) + allow (admin) + own-rows (member's own invoice) + the by-id premise guard + the explain-derived readFilter.
Reproduction rule — the one fail
cli.migrate-meta-codemod clauses 1 & 2 — the codemod cannot load the sources it is advertised to fix
Mechanism.os migrate meta --from <N> loads and validates the stack config through the current (post-retirement) schema before any conversion runs. A source carrying a retired key — the only input the command exists to fix — fails that load, and the command exits 1 having rewritten nothing. The refusal it prints is the very message that prescribes the command.
Steps (all outside the repo; needs only @objectstack/spec resolvable):
Create a scratch app directory with objectstack.config.ts:
src/probe.object.ts — any valid object with an explicit sharingModel. src/probe.view.ts — authored as a raw object literal (this is what a real upgrader has on disk; defineView refuses the key at authoring time, which is the same wall):
Run exactly that: os migrate meta --from 16 (with or without --out <path>).
Expected — the codemod applies view-list-passthrough-keys-removed (declared by packages/spec/src/migrations/entries/retired-keys/17.ui__ListView__striped.ts), drops striped from views[0].list, and exits 0.
Actual — → Loading configuration… then exit 1 with the same message: ✗ defineStack validation failed (1 issue): ✗ views.0.list.striped: … Run \os migrate meta --from 16` to rewrite existing sources automatically.`
os build again → the identical refusal. Nothing was rewritten.
Controls that were run, and rule out the alternatives
Reproduced twice, identical output and exit 1 both times.
The source's only validation issue is the retired key — an earlier attempt that also carried an unrelated type:'list' mistake was discarded and re-run with type:'grid' so the retired key stands alone. Same result.
--from 17 → same load-time refusal. --from 3 → same load-time refusal, i.e. even the ADR-0087 floor check is unreachable on the input that needs it (on a clean tree --from 3 does report Cannot migrate from protocol 3: the chain's support floor is 10).
--out is not implicated — reproduced without it.
On a config with no retired keys the command works: exit 0, --json → {from:16, to:17, runtime:"17.0.0", applied:[{conversionId:"field-required-notnull-explicit", …}], todos:[{id:"action-descriptor-is-async-retired", …}]}. So the command is otherwise functional; it is specifically the retired-key input it cannot ingest.
Automation self-check: no browser, no coordinate clicks, no shared tab. The dispatcher-vs-hono-route trap does not apply (CLI, not a route). Stale-dist ruled out — the CLI ran from packages/cli/dist and the refusal text matches current packages/spec/src.
Blast radius.os migrate meta --from 16 is prescribed verbatim by 132 retirement messages across packages/spec/src (--from 17 by 4 more). Every one of them currently sends an upgrader to a command that cannot load their sources. The --stored arm (replaying over sys_metadata rows) is a different code path and was not implicated by this run.
Fixture gaps
Recorded so the next sweep does not rediscover them.
access-security.no-active-org-session-semantics needs a ledger blocked marker. The item has no blocked field, so checklist-select.mjs lists it as runnable — with or without --include-blocked. Three rounds have now paid for the same dead end (deployment is tenancy: single; ADR-0095 Layer 0 inert; OS_TENANCY_POSTURE=isolated correctly refused because @objectstack/organizations is not declared by the host app). Suggested: blocked: { by: "fixture", ref: "#9334" }.
Two shipped showcase sharing rules can never enforce.showcase_project and showcase_task are both sharingModel: 'public_read_write', so share_red_projects_with_execs, share_high_value_red_projects_with_managers and share_open_tasks_with_manager are inert — the platform says so loudly at every boot (SharingServicePlugin: boot rule backfill failed … SHARING_NOT_ENABLED) and at POST /sharing/rules/<name>/evaluate (422). The platform behaviour is correct; the fixture ships three decorative rules. This blocks record-share-grant-revoke clause 7 as written, and it is why sharing-rules-widen had to be driven on share_new_inquiries_with_field_ops / showcase_inquiry instead.
The reports service is not installed on stock showcase.plugin-reports exists in the repo but is absent from the boot's 48-plugin list; every /api/v1/reports* call answers 501 NOT_IMPLEMENTED. This blocks 4 of the 5 clauses of dashboards.saved-report-ownership, whose fixtures block assumes the service is present.
No AI metadata fixture. No stock example app ships agent/tool/skill seeds, so the build-authored round-trip in ai.agent-tool-skill-metadata-roundtrip clause 3 cannot be exercised. A runtime-authored substitute was used and is recorded; note for whoever builds the fixture that the runtime meta read-back does not apply the schema defaults (surface:'ask', active:true) the clause expects.
No {permissionSet}-gated and no doc-level-audience fixture in the docs corpus — only one book (showcase_manual, public) and 9 docs with no explicit audience. Blocks the 403 half of docs-audience-gate clause 1 and the third cell of clause 2.
sys_record_share has no admin edit surface, so sharing-rules-widen §A13 cannot be performed as written (PATCH/DELETE → 405 OBJECT_API_METHOD_NOT_ALLOWED, allowed verbs are read-only; the record-shares routes have no update verb).
No key documented as silently-stripped-by-design was locatable for enforce-or-remove-authoring-gates clause 3; the remaining open shapes are documented .passthrough() (keep-and-forward), which is not the same property.
Breaking a declarative connector's upstream requires either mutating a tracked source or a workspace build. This round worked around it by removing node from the spawned child's PATH; worth writing into the item as the zero-mutation recipe, since it also makes recovery restart-free.
Checklist-accuracy findings & other observations
Moved to the first comment on this issue to keep the body under GitHub's size cap: 9 checklist-text corrections found during the run (none scored as a product failure) and 4 smaller product observations, each independently checkable.
Round complete: 23 / 23 items scored, no item left unreached. Subject pin never moved; nothing landed in the repo.
Round Tier-2A of the platform-checklist regression at #9296 — the 23 unpinned P0/P1 items on the
api/cli/buildsurfaces. No item in this lane carries anautomated.ref, so every clause below was driven by hand against a live server, a live CLI invocation, or the spec's own built contract face.23 / 23 items reached a verdict. No handoff list — nothing was left unreached.
Environment fingerprint
e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd(HEAD never moved).objectui-shapin665661ab093263f39f2e660a295ea615dbcee35a665661ab093263f39f2e660a295ea615dbcee35a— equal to the pinexamples/app-showcase, seeded (--seed-admin,admin@objectos.ai)file:/tmp/qa-wave/t2a-*.db+ per-probe scratch DBs,rm -fbefore each first bootorigin/main(git checkout origin/main -- docs/qa/platform-checklist/,check-platform-checklist: OK — 15 areas, 190 items). Never committed.session_01Ai9gUdihXjg7FumzsBNSSs(Tier-2A)docs/qa/platform-checklist/syncSpec drift handled mid-round.
origin/mainmoved after the QA tree was synced (#9385, #9366). The sync was taken at a clean point between items, before anything in this lane was scored. Only one owned item changed:cli.qa-suite-executionrev 2 → rev 3. It was scored against rev 3, i.e. against #9385's current behaviour (record action types resolve the Data Protocol mount from/discovery), not the superseded rev-2 expectation. Every other owned item'srevisionwas re-resolved after the sync and was unchanged.Scope — 23 items,
revisionpinned per itemaccess-security.sharing-rules-widenaccess-security.record-share-grant-revokeaccess-security.no-active-org-session-semanticsai.agent-tool-skill-metadata-roundtripai.mcp-run-action-exposure-gateapi-backend.batch-transactional-discoveryapi-backend.formula-gatesapi-backend.enforce-or-remove-authoring-gatesapi-backend.error-envelope-ledgerapi-backend.filter-comparand-conformanceapi-backend.retired-def-refusalcli.dev-boot-contractcli.build-own-contractcli.qa-suite-executioncli.migrate-meta-codemoddashboards.saved-report-ownershipidentity-auth.self-service-password-resetintegration-system.connector-degraded-recoveryintegration-system.external-datasource-federated-readplatform-core.seed-integrityplatform-core.docs-audience-gaterecords-forms.validation-rule-type-matrixsearch.rls-both-personasDerived totals — 15 PASS / 6 PARTIAL / 1 FAIL / 1 BLOCKED. Item verdicts are derived from the clause table below, never hand-assigned.
Both P0 items in this lane are accounted for:
cli.dev-boot-contractpassed 7/7,platform-core.seed-integritypassed 5/5. No P0 failure. No authentication or authorization hole was found — every access-control clause in this lane proved both sides of its gate (entitled presence and server-side refusal for the unentitled persona, by forged request where the door allowed one).Per-clause verdicts
Oracle evidence is text only, as required.
→reads observed.access-security.sharing-rules-widen· rev 1 · partialLive audience:
share_new_inquiries_with_field_ops(showcase_inquiry, OWDprivate, recipientunit_and_subordinates: bu_field_ops). Member D was placed inbu_west_coast— a descendant ofbu_field_ops, so the subtree walk is exercised, not just flat membership; outsider E inbu_hq_finance.GET /data/showcase_inquiry/<Priya>→ 200; D's list →total 1 ["Priya Raman"]. The share materialised on the BU-membership insert itself (sharecreated_at23:08:46.921 vs member insert .902); the explicitPOST /sharing/rules/<name>/evaluatethen reported{matchedRecords:1, expandedUsers:1, grantsCreated:0}— already present.status closed) 404; E→matching 404, E→non-matching 404 (pre-share). Neither list leaks the other's row.sys_record_sharerowshr_cb01e713…{source:'rule', source_id:'srule_c4f6be61-…', reason:'rule:share_new_inquiries_with_field_ops'}— the rule row id, not the name.POST /security/explain {object,operation:'read',userId:D,recordId:Priya}→record:{visible:true, decidedBy:"sharing"}, layersharingverdictwidenswithrules:[{kind:'sharing_rule', name:'srule_c4f6be61…', effect:'admits'}]whileowd_baselineisnarrows. Same call for Lena →record:{visible:false},effect:'neutral'.POST /data/showcase_inquiry/<Lena>/shares {recipientType:'user',recipientId:E}→ 201; E reads Lena 200, third user D reads Lena 404.sys_record_shareis API-read-only —PATCH/DELETE→405 OBJECT_API_METHOD_NOT_ALLOWED … allowed:["get","list","aggregate","search","export"],PUT→ 405, and the record-shares routes carry no update verb. The #2909 substance was nonetheless proved independently: after a full server restart on the same DB, both shares (rule-materialised and manual) re-read byte-identical and every access decision was unchanged (D: match 200 / non-match 404 / list 1; E: match 404 / non-match 200 / list 1). The seed does not clobber shares.access-security.record-share-grant-revoke· rev 2 · partialPersonas A/B/C = three fresh sign-ups; probe
showcase_private_note(OWDprivate).GET /data/showcase_private_note/<A id>→404 RECORD_NOT_FOUND; B's listtotal 0.sys_record_sharerowPOST …/shares→ 201{id:"shr_ba4e283e…", recipient_type:"user", recipient_id:<B>, access_level:"read", source:"manual", source_id:null, granted_by:<A>}; confirmed by a system-contextsys_record_shareread.total 1 ["95QFJ…"]; C by-id 404, C listtotal 0.GET …/shares→ 200 with the grant. BPOST …/shares→ 403PERMISSION_DENIED: managing shares on showcase_private_note/95QFJ… requires record ownership or Modify All Data (ADR-0111 D1). Reading confers no re-share authority.DELETE …/shares/<shareId>→ 204; B's next by-id → 404; system-contextsys_record_shareread →[].DELETE /data/showcase_private_note/<other id>/shares/<shareId>→ 404share shr_ba4e283e… not found on showcase_private_note/1wEywE3…; the share survived (A's re-read still lists it). Run before the real revoke, so survival is meaningful.share_red_projects_with_execsPOST /sharing/rules/share_red_projects_with_execs/evaluate→ 422SHARING_NOT_ENABLED: 'showcase_project' is not under record-sharing enforcement (public sharing model or no 'owner_id' field); a share row on it would never be consulted. See the fixture-gap section — the rule is inert on stock. The evaluate contract itself was proved on the enforceable sibling rule (seesharing-rules-widenclause 3):{ruleId, matchedRecords, expandedUsers, grantsCreated, grantsUpdated, grantsRevoked}andsource_id === sys_sharing_rule.id.access-security.no-active-org-session-semantics· rev 1 · blocked(fixture) · P0All 7 clauses
blocked(fixture). Not re-derived — two prior independent rounds already established it; this round only re-confirmed the premise from the boot banner:Tenancy: single,sys_organizationcount 0 at first boot (the #8686 diagnostic quotes"organizationCount":0). ADR-0095 Layer 0 is inert, every clause vacuous;OS_TENANCY_POSTURE=isolatedis correctly refused because@objectstack/organizationsis not declared by the host app.Ledger action required — the item carries no
blockedmarker, sochecklist-select.mjspresents it as runnable (--include-blockedchanges nothing). This is the fourth round to reach the same dead end. See fixture gaps.ai.agent-tool-skill-metadata-roundtrip· rev 2 · partialsurface:"ask",active:trueon agent and skill). All 10 tombstone/guidance probes rejected, each naming its key and its prescription:tool.permissions("promised a capability gate on tool invocation that nothing ever enforced"),tool.active,tool.category,tool.builtIn,tool.requiresConfirmation(#3896 / #3715 + ADR-0033 §2);agent.tools("useskills… (ADR-0064)"),agent.knowledge;skill.triggerPhrases("phrases were never matched … activation istriggerConditions"); guidance-map probesskill.permissionsandskill.trigger. Zero silent strips.GET /metaandGET /meta/typesboth list 34 kinds includingagent,tool,skill;agents/tools/skillsall absent.PUT …?mode=draft→ publish →GET /meta/skill/qa_probe_skillechoes every authored value plus_diagnostics:{valid:true}— but not the documented schema defaultssurface:'ask'/active:true, which the contract face does apply. Recorded as the thing to check when the fixture lands.PUT /meta/skill/qa_probe_skill?mode=draft→ 200{state:"draft", seq:1};PUT /meta/tool/…?mode=draft→ 200seq:2;PUT /meta/agent/qa_probe_agent?mode=draft→ 403[not_creatable] Metadata type 'agent' is code-only: the metadata-type registry declares allowRuntimeCreate=false and allowOrgOverride=false … Declare it in source (**/*.agent.ts) and redeploy. Recorded as CORRECT, not a bug. (Draft read-back is?state=draft;?mode=draftis the write-side param and 404s on read — a wording nit, not a defect.)GET /meta/agent,/meta/tool,/meta/skill→ 401UNAUTHENTICATEDon all three.POST /ai/tools/qa_probe_tool/execute→ 501{code:"NOT_IMPLEMENTED", message:"Provided by @objectstack/service-ai in ObjectStack Cloud/Enterprise — no implementation ships in the open framework"}. Authored ⇏ callable.ai.mcp-run-action-exposure-gate· rev 2 · pass (5/5)Over MCP Streamable HTTP with a minted
osk_key (POST /api/v1/keys→ 201).run_action{actionName:'showcase_mark_done', objectName:'showcase_task', recordId:<a NOT-done task>}→isError:true, text exactlyAction '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 todo,done false, **updated_atunchanged**. Refusal reproduced a second time,isError:true` both times.list_actionsreturns only ai-exposed + permittedobjectName:'showcase_task'call both return exactly{actions:[showcase_portfolio_snapshot], totalCount:1}.showcase_mark_doneabsent from both. The two doors agree.run_action{showcase_portfolio_snapshot}→{ok:true, scope:"global", accounts:19, projects:9, invoices:12}; direct REST totals → 19 / 9 / 12.[action-audit] MCP run_action 'showcase_portfolio_snapshot' on 'global' — body executes TRUSTED (system-elevated context, RLS/FLS-bypassing) for user 'SYkUFihutw3CQtflO98VqdK7NOWSaQqF'.run_action{objectName:'sys_user'}→isError:true,Object "sys_user" is a system object and its actions are not exposed via MCP. No side effect.api-backend.batch-transactional-discovery· rev 2 · pass (5/5)Three checklist-accuracy corrections came out of this item; none is a product defect.
/batchis mounted and tx-capableGET /discovery→capabilities.transactionalBatch = {enabled:true, description:"Atomic cross-object batch endpoint (POST {basePath}/batch) …"}, and the atomic cross-object batch (createshowcase_private_note+ createshowcase_taskin one call) succeeded end-to-end — both rows verified present afterwards. Capability read and live behaviour agree.atomic:falseon the cross-object door → 400BATCH_NOT_ATOMICPOST /batch {operations:[…], atomic:false}→ 400{error:"Cross-object batch is always atomic; use POST /data/:object/batch for non-atomic per-object batches", code:"BATCH_NOT_ATOMIC"}— the message points at the per-object door exactly as specified.showcase_invoicemissing required fields) → 400, and a write-time failure (update of a nonexistent id) → 404 — in both cases neither flankingshowcase_private_notecreate persisted. The #4793 per-row codes are emitted by the per-object door:POST /data/showcase_private_note/batch {options:{atomic:true}}→[{ROLLED_BACK},{VALIDATION_FAILED},{NOT_ATTEMPTED}]in exactly those positions, and the first row did not persist. Both conjuncts proved; the clause attaches them to one door.{code:"BATCH_TOO_LARGE", count:201, max:200}; a system-context count of the probe-titled rows is 0.atomicunset):[{ok},{VALIDATION_FAILED},{NOT_ATTEMPTED}],succeeded 1 / failed 2, row 1 stayed written — no rollback, soatomicdoes default false. The "neither blocks the other" reading is the separatecontinueOnErrordial (default also false, #7539): withoptions:{continueOnError:true}→[{ok},{VALIDATION_FAILED},{ok}],succeeded 2 / failed 1, both valid rows persisted.api-backend.formula-gates· rev 2 · pass (4/4)/home/user/objectstack-qa-wave/packages/formula/dist/index.js— resolved path captured,dist/, neversrc/.record.due_date == today()→{ok:true,value:true}for today's ISO date and{ok:true,value:false}for2020-01-01;record.flag ? record.x : null→42/null;floor(3.7)→3;ceil(3.2)→4.Field.formula({expression:'record.end_date - record.start_date + 1'})→os buildexit 1.• object 'qa_probe' · field 'qa_duration_days' expression: date arithmetic \google.protobuf.Duration + int` — record.start_date is a date, and CEL can't do arithmetic on dates: this faults at runtime, so the field silently evaluates to null. Use `daysBetween(a, b)` … `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)`+source: `record.end_date - record.start_date + 1`+rule: expression-invalid at object 'qa_probe' · field 'qa_duration_days' expression`.os buildon the untouched showcase → exit 0 (✓ Build complete (2082ms), artifact 690.1 KB), despite its legitimatedue_date == today()comparisons.api-backend.enforce-or-remove-authoring-gates· rev 2 · partialQuerySchema:cursor,joins,distinct,windowFunctions— all four rejected (control: a valid query parses).EnhancedApiErrorSchema.fieldErrorsrejected.ObjectSchemadisplay annotationshelpText/displayHint/uiNote/color— all four rejected. Retired filter operators enforced at the live driver door:GET …?$filter={"name":{"$regex":"North"}}→ 400.cursor→ "no driver ever implemented keyset pagination …QueryBuilder.cursor()was removed with it";joins→ "Related records are read throughexpand";distinct→ names the mis-wiredtotal/hasMoredegradation;windowFunctions→ *"a SQL-driver capability behindSqlDriver.findWithWindowFunctions(…)"*;fieldErrors→ *"renamed tofieldsin spec 17 (ADR-0114 D4, #3977) — the array is unchanged, only the property name"* (andfieldsparses clean);$regex→ *"is RETIRED … Write$icontainsinstead"* with the registry'swhy` quoted verbatim. No bare "unknown key"..passthrough()—dashboard.zod.ts:252"declared query keys + open renderer extras",view.zod.tsGantt config,bulk-action.zod.tsparams/options — i.e. open-by-design that keeps and forwards unknown keys, the opposite of a strip. Every display annotation probed is now a loud strict rejection (#4001/#1535, ADR-0085 closed the formerdetail:{}.passthrough()). Scoring a keep-by-design as a strip-by-design would be ticking on a label.QuerySchema, #4286) · error envelope (EnhancedApiErrorSchema, ADR-0114 D4) · retired filter operators (RETIRED_FILTER_OPERATORS={$regex→$icontains, $options→$icontains}, data-only by design, enforced at the driver refusal site) · object display-annotation strictness (ObjectSchema, #4001).api-backend.error-envelope-ledger· rev 1 · pass (6/6)Seven samples, one per family; membership checked against
StandardErrorCode(53) ∪ERROR_CODE_LEDGER(22) ∪REGISTERED_ERROR_CODES(237), read from the built spec.VALIDATION_FAILED(400) ·UNAUTHENTICATED(401) ·PERMISSION_DENIED(403) ·OBJECT_NOT_FOUND(404) ·RECORD_NOT_FOUND(404) ·BATCH_NOT_ATOMIC(400) ·UNSUPPORTED_QUERY_PARAM(400) — 7/7 ledgered, none unregistered.fields[]with FIELD-level codesPOST /data/showcase_invoice {}→{error:"Invoice Number is required; Account is required", code:"VALIDATION_FAILED", fields:[{field:"name",code:"required",message:"Invoice Number is required",label:"Invoice Number"},{field:"account",code:"required",…}]};required∈FieldErrorCode(28 members, lowercase snake).fieldErrorsnever appearsHttpStatusErrorCodeMaptier for tier (400 validation/filter/param, 401, 403, 404 ×2). No 5xx on any caller-fixable input.{error:"Object 'not_a_real_object' is not registered", code:"OBJECT_NOT_FOUND"}vs record:{error:"Record nonexistent-id-0000 not found in showcase_task", code:"RECORD_NOT_FOUND"}— different codes and different messages.api-backend.filter-comparand-conformance· rev 1 · pass (5/5)ACCEPTED_FILTER_COMPARAND_TYPESread live from the built spec =["string","number","bigint","boolean","null","Date"];FILTER_COMPARAND_BIGINT_EXACT_LIMIT = 9007199254740992.{tax_rate:8}6=6 · boolean{done:true}2=2 · null{paid_on:null}7=7 · Date-as-ISO{issued_on:{$gte:'2026-01-01'}}10=10.bigintandDateare not JSON-expressible, so they were exercised at the shared contract face:7n→ accepted, narrowed to number 7;new Date(…)→ accepted, narrowed to ISO 8601. Six for six.$eq→ 400Filter comparand at where.tax_rate.$eq is a plain object ({"a":1}), which no driver can compare. A comparison value must be a string, number, bigint, boolean, null or Date. Refusing rather than guessing…— path named, accepted set quoted. Also refused: array in scalar and implicit positions,undefined("cannot be told apart from an omitted key, yet the two mean OPPOSITE things"), function, Symbol. The dangerous direction was probed explicitly: every bad predicate returned 400; none returned 200 over an unfiltered table.2n**53n→ accepted,9007199254740992(number).2n**53n+1nand-(2n**53n)-1n→ refused: "the bigint 9007199254740993n, whose magnitude exceeds 2^53 — it has no exact JS-number form, so a comparison built on it would silently answer the wrong rows". No rounding.classifyDottedFilterHead:lookup/master_detail→relation,text/summary→scalar,formula→virtual,json→null,multiple:true→null. All three reachable classes refused over REST with class-specific text: relation headowner.name→ "follows the relationship … a filter reaches only columns of 'showcase_invoice' itself"; scalar headname.sub→ "a 'text' field … stores a single scalar value — there is nothing beneath it"; virtual headbudget_remaining.x(a realformulafield) → "a dotted path whose head 'budget_remaining' is a virtual 'formula' field … computed on re[ad]" — so thevirtualclass was NOT blocked(fixture) here. A scalar head is never traversed as a relation.GET ?$filter=,POST /query {filters},POST /query {where}— all 400INVALID_FILTERwith byte-identical message text, and the same refusal at the spec'snormalizeFilterComparandTypesface.api-backend.retired-def-refusal· rev 1 · pass (4/4)Registry:
retired-defsholds 54 entries — 53 at prefix17, 1 at prefix18.os buildexternalDataSources,widgetManifests,messageQueueConfigs,apiKeys→ all non-zero exit (2, the oclif error exit for a load-timedefineStackthrow), eachUnrecognized key(s) on this stack definition: \`. Until #8687 closed this surface (the outermost #4001 door), an unknown top-level stack key parsed green and its value was silently dropped…`. No green build with a family silently absent.17.data__ExternalDataSourceand17.system__MessageQueueConfig→ Route 3 ("no tombstone, no D2 conversion — this table plus the D3 entry are the declaration");18.identity__ApiKey→ #8715"disposition B: delete";17.ui__WidgetManifest→ bare table entry, no prescription authored. All four are prescription-less by design, so per the item's own knownGap the generic refusal is the CORRECT answer — and that is what was observed: the four refusals are identical in shape to the control (totallyInventedThing), differing only in the echoed key name.PUT /api/v1/meta/<type>/qa_retired_probe?mode=draftforexternal_data_source,widget_manifest,message_queue_config,api_keyand the controltotally_invented_type→ 400 on all five:[invalid_request] '<type>' is not a metadata type. The platform declares no such type, and since #8586 retired 'additionalTypes' a plugin cannot declare one…. No two-door disagreement. (Registered types: 34, none of the sampled defs among them.)17.data__ExternalDataSource,17.ui__WidgetManifest,17.system__MessageQueueConfig(prefix 17) +18.identity__ApiKey(prefix 18), across four domains:data/,ui/,system/,identity/.cli.dev-boot-contract· rev 2 · pass (7/7) · P0--ui/api/v1/health→ 200;/api/v1/ready→ 200{"status":"ready","state":"running"};/_console/→ 200 serving the console document (<!doctype html> … <base href="/_console/">).--seed-adminis idempotent — a later--admin-passworddoes NOT overwrite--admin-password changed99: sign-in with the ORIGINALadmin123→ 200; sign-in withchanged99→ 401. Zero-user-DB-only, confirmed.Database:key-value: explicit-d→file:/tmp/qa-wave/t2a-a.db; envOS_DATABASE_URL→file:/tmp/qa-wave/env-chosen.db;-d :memory:→:memory:(no file default imposed); legacy compat-read → see below; nothing chosen →file:<project>/.objectstack/data/objectstack.db— the persistent project-anchored unified default, never:memory:, and the file was verified on disk (1.7 MB). Legacy tier: with onlydev.dbpresent, the boot read…/.objectstack/data/dev.dband printed⚠ Reading legacy database file … — the unified default is now …/objectstack.db (#6469); migrate with: mv "…/dev.db" "…/objectstack.db" … or pin it explicitly via OS_DATABASE_URL=…— and did not create a fresh emptyobjectstack.dbbeside it. Both halves of the negative satisfied. (The "config-declared default datasource" tier is not supplied by the stock fixture — the showcase config declares no default datasource; no variant existed to run.)--freshisolates OS_HOME state in an auto-deleted tempdir🧪 Fresh OS_HOME: /tmp/objectstack-dev-z6lz37, its DBfile:/tmp/objectstack-dev-z6lz37/data/objectstack.db— the SAME unified filename under the ephemeral home. After exit the directory is gone. The app-declared cwd-relativeexamples/app-showcase/.objectstack/data/showcase_external.dbsurvived with an unchanged mtime — annotated as the documented #5594 carve-out, not filed as a bug.↪ server bound to port 3133 (requested 3131)— and every printed URL (API, Console, MCP endpoint, MCP skill) used the ACTUAL bound port 3133, never the requested one.dist/objectstack.jsonaged behind the sources:⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build./newest source: src/ui/views/contact.view.ts (2421d newer than the artifact)/fix: run \objectstack build` or start with `--compile`, or save a watched file to trigger a rebuild + restart.— and/api/v1/health` → 200. Warn, never gate.✗ Config file not found in /tmp/qa-wave/scratch-empty, stderrRun in a directory with objectstack.config.ts, pass --artifact <path|url>, or run from the monorepo root., realecho $?= 1. No hang, no silent 0.cli.build-own-contract· rev 2 · pass (6/6)On a scratch app outside the repo (
/tmp/qa-wave/app), so no deliberate breakage touched a shared fixture.--jsonpayload✓ Build complete,Artifact: dist/objectstack.json (1.5 KB), stats printed.--json:{"success":true,"output":"…/dist/objectstack.json","size":1531,"warnings":[],"conversions":[],"stats":{objects:1,fields:3,…},"duration":76}— every declared key present. Artifact parses.✗ objects.0.fields.name.type: Invalid field type 'not_a_real_type'. Valid types: text, textarea, email, …— the path is named, never a bare "validation failed". Exit 1 on the--jsonpath; 2 on the human path, which is the oclifthis.error()exit that clause 5 explicitly blesses.sharingModel→✗ Author-time rules failed (2 issues)listing both, each with where/message/hint/rule: security-owd-unset/at objects[N].sharingModel. Exit 1. Not first-failure-only.--jsonfailure/advisory shapes matchos validate --jsonbuild --json→{"success":false,"error":"author-time rules failed","issues":[{severity,rule,where,path,message,hint}…]};validate --json→{"valid":false,"errors":[{severity,rule,where,path,message,hint}…]}— identical per-issue keys. Success payloads carrywarnings+conversionsunder the same names.this.error()path}. Stability proved by repetition: success repeated → 0, 0;--jsonsuccess → 0; schema break repeated → 2, 2 (and 1 on--json); rule failure repeated → 1, 1. Nothing varied between identical runs; no code tracked a duration or a finding count.os buildon the untouched showcase:⚠ 26 author-time warning(s) — see above(e.g.rule: security-master-detail-ungranted at objects[6].fields.expense_report), the artifact was written (690.1 KB) andecho $?= 0. Both sides of the warn/fail line. (The specific #3786 undeclared-authoring-key advisory is no longer producible — #4001/#8687 turned undeclared keys into hard errors; the warn/fail property was proved with a live advisory class instead.) Alias:os compileon the same input behaves identically (exit 0, same artifact line).cli.qa-suite-execution· rev 3 · pass (8/8)Scored against the post-#9385 revision. Stock mount, so neither the
crud.dataPrefixnor theapi.apiPathcase bites — as rev 3 states.Found 1 test suites.(N > 0, quoted as the clause demands),✅ Scenario: health-liveness (48ms),✅ Scenario: category-create-read-roundtrip (571ms),SUCCESS: All 2 scenarios passed.,echo $?= 0.{{var}}→ later step)showcase_categoryheld exactly 4 rows before, 4 after, with identical names — the teardownDELETE …/{{categoryId}}actually removed the created row. Dead interpolation would have 404'd on the literal path and left a 5th row.❌ Scenario: false-assertion-probe/Step Failed: GET health expecting degraded/Assertion failed: data.status expected degraded, got ok; exit 1.Assertion failed: data.nope cannot be evaluated by 'contains' — expected an array or a string at that path, got undefined. The path resolved to nothing — the field is absent from the result, or the path is misspelled. Use 'is_null' if asserting absence is what you meant.qa/a-malformed.test.json is not a valid Quality Protocol suite (TestSuiteSchema):/✗ scenarios.0.steps: Invalid input: expected array, received undefined/Expected shape: {…}/Reference: content/docs/references/qa/testing.mdx. The two sibling suites still executed after it, it counted as a failed suite, exit 1, no TypeError insiderunSuite.QA-T2A-TEARDOWN-PROBEis absent from theshowcase_categorylisting afterwards.TestActionTypeSchemamembers carry their OWN verdictcreate_record✅ ·read_record✅ ·update_record✅ ·query_records✅ ·delete_record✅ ·api_call✅ ·wait✅ ·run_script❌Unsupported action type in HttpAdapter: run_script. Exactly one refusal — the honest verdict rev 3 predicts. The five record-shaped types executed (post-#7848).os testfromexamples/app-showcasefound the shipped suite with no argument.os test 'qa/**/*.test.json'from the same dir →Found 1 test suites.,SUCCESS, real 3.99s (not the pre-#7363 multi-minute walk). Zero-match glob →Found 0 test suites./No test files found matching: qa/nothing-matches-*.test.json/Exiting 0 — an empty match is not a failure. Pass --fail-on-empty to make it one., exit 0 — the declared #7848 posture.cli.migrate-meta-codemod· rev 1 · FAIL (4 pass, 2 fail)ui/ListView:striped(registry entrypackages/spec/src/migrations/entries/retired-keys/17.ui__ListView__striped.ts, D2 conversionview-list-passthrough-keys-removed) is left untouched:os migrate meta --from 16exits 1 without rewriting anything. Reproduction rule below.os buildafter the codemod run produces the identical pre-migration refusal.--jsonruns produced byte-identical output and exit 0;md5sum -cconfirmed nothing was rewritten in place (the default is a read-only replay; in-place effects need--outor--stored --apply).--frombelow the floor is REFUSED loudly--from 3→ exit 1,✗ Cannot migrate from protocol 3: the chain's support floor is 10 (ADR-0087 D3). Upgrade to protocol 10 by another path first, then re-run.;--json→{"error":"unsupported_from_major","message":"…"}. Caveat recorded: on a source that carries a retired key the floor check is unreachable — the load aborts first (a consequence of clauses 1–2).--jsonon a clean tree separates them into two arrays:applied:[{conversionId:"field-required-notnull-explicit", surface:"object.fields.*.required / …", path:"objects[0].fields.name.storage.notNull"}]vstodos:[{id:"action-descriptor-is-async-retired", surface:"ActionDescriptor.isAsync …"}]. Nothing advisory is reported as "migrated".--jsonexits 0 on success (#4873){from:16, to:17, runtime:"17.0.0", applied:[…], todos:[…]}payload.Registry sizes recorded as the item asks:
retired-defs54,retired-keys38,semantic104. Advertised majors, grepped from the retirement messages:os migrate meta --from 16appears 132 times,--from 174 times.dashboards.saved-report-ownership· rev 2 · partialREPORT_NOT_FOUND/api/v1/reports*call by an authenticated caller answers501 {"code":"NOT_IMPLEMENTED","message":"Reports service is not configured on this deployment"}.packages/plugins/plugin-reportsexists in the repo but is absent from the showcase's 48-plugin boot list.ownerIdcannot be spoofedUNAUTHENTICATEDGET /api/v1/reportsandGET /api/v1/reports/:id→ 401{"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}. Notably the auth gate runs before the service-availability check, so an anonymous caller learns nothing about whether the service exists — the correct anti-enumeration ordering.identity-auth.self-service-password-reset· rev 1 · pass (5/5)POST /api/v1/auth/request-password-reset→ 200. The artifact was captured as server truth rather than log text: thesys_emailrow for the target readssubject:"Reset your ObjectStack password",status:"sent", body containinghttp://localhost:3131/api/v1/auth/reset-password/qvO9EIo2noPw7MPB5p1FC3WU?callbackURL=…andlink expires in 60 minutes. (Oracle substitution recorded:sys_emailoutranks the clause'slogoracle.) Exactly one row — the unknown-email request produced none.POST /auth/reset-password {token,newPassword}→ 200{"status":true}; sign-in NEW → 200; sign-in OLD → 401INVALID_EMAIL_OR_PASSWORD.{"message":"Invalid token","code":"INVALID_TOKEN"}; the would-be third password does not sign in (401); the second password still does (200).INVALID_TOKEN; the tampered password never signs in (401) and the current one still does (200). No silent success.200 {"status":true,"message":"If this email exists in our system, check your email for the reset link"}. The server-side[Better Auth]: Reset Password: User not foundstays in the log and never reaches the caller.Route divergence confirmed live, as the item's steps predict:
/api/v1/auth/request-password-resetserves (200) while/api/v1/auth/forget-password— the spelling in the spec'sAuthEndpointPaths— 404s.integration-system.connector-degraded-recovery· rev 2 · pass (5/5)Upstream broken without mutating any repo source: the server was launched with
noderemoved from the childPATH, so the connector'sspawn('node', ['./scripts/mcp-fixture.mjs'])failsENOENT— an operational failure, deterministic and restart-free to reverse./api/v1/health→ 200 on the dead upstream. Descriptor:{name:"showcase_mcp_tools", origin:"declarative", state:"degraded", degradedReason:"connector-mcp provider: connector 'showcase_mcp_tools' could not reach its MCP server: spawn node ENOENT", actions:[]}— still listed inGET /automation/connectors(5 connectors returned). Boot warn:Connector registered DEGRADED: showcase_mcp_tools (origin: declarative) — no actions and no handlers until its upstream is reachable … (#3017).provider:'rest'with no provider factory registered aborted —ERROR Plugin startup failed: com.objectstack.service-automation … no provider factory is registered. Install the connector plugin that supplies it … (ADR-0097)/✗ Plugin … failed to start - rollback complete. A configuration fault, fatal at boot, carrying the factory's own validation error. (Substituted twin recorded: the item names an invalidproviderConfigshape, which would require editing a tracked source; the missing-factory fault is the same classification side.)showcase_taskreturned 201 in 1.2 s (no hang); the trigger-fired run loggedNode 'echo' failed: connector_action 'echo': connector 'showcase_mcp_tools' is degraded — … spawn node ENOENT. Dispatch is unavailable until its upstream recovers; the platform retries automatically (#3017).— the storeddegradedReasonverbatim, clearly distinct from "no such connector/action".DECLARATIVE_RETRY_BASE_MSdoubling toward the ceiling.nodeshim was placed at 23:41:21; the next scheduled retry at 23:41:39 loggedConnector 'showcase_mcp_tools' replacedwith no server restart. Descriptor then:state:"ready",degradedReasongone,actions:["echo_upper"]re-derived fromtools/list. A subsequent record-create produced no further dispatch failure.integration-system.external-datasource-federated-read· rev 1 · pass (3/3)/meta; fixture rows over the normal/datapath; filters/$topwork as native/meta/objectcarriesshowcase_ext_customerandshowcase_ext_order, bothdatasource:"showcase_external"./data/showcase_ext_customer→ 3 rows (Aurora Labs / Borealis GmbH / Cyan Pacific);/data/showcase_ext_order→ 4 rows.$filter={"region":"EU"}→ total 1 (Borealis);$top=2→ 2 of 3;$orderby=lifetime_value desc→ 480000 / 312000 / 95000 in order. Federation is transparent to the query layer.{"code":"EXTERNAL_WRITE_FORBIDDEN","error":"Write '<op>' 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)."}— names the datasource, the mode and both dials with observed values. Row set byte-identical afterwards (3 rows, same names).GET /api/v1/datasources→ both connections withstatus:"ok",showcase_externalcarryingschemaMode:"external",origin:"code",active:true;GET /api/v1/datasources/showcase_externalreturns the full descriptor withhasSecret:false. Recorded substitution: the clause is worded for the Setup page in anapi-surface item; the console page was not driven.platform-core.seed-integrity· rev 1 · pass (5/5) · P0GET /data/<object>?$top=1totals. 18 of 18 objects match, zero mismatches: account 14 · contact 32 · inquiry 3 · product 4 · project 5 · task 10 · category 4 · business_unit 9 · sys_business_unit 5 · team 2 · project_membership 3 · field_zoo 2 · invoice 12 · invoice_line 5 · expense_report 4 · expense_line 13 · preference 1 · announcement 2. Sum = 130, exactly the boot banner'sSeeds: com.example.showcase 130 rows.Specimen — Fullfield-by-field:f_multiselect ["red","green"]as a set ·f_json {"nested":{"k":"v"},"list":[1,2,3]}structurally intact ·f_lookupresolved to the real Northwind idcY2IjNHmR8eYIuvc(f_lookups→ Northwind + Contoso) ·f_master_detail→ the Website Relaunch project id ·f_composite,f_repeater,f_record,f_vector,f_location,f_address,f_tagsall byte-equal to the authored literal.Field '<x>' is read-only (readonlyWhen) — ignoring incoming changewarnings on the upsert path —tax_rate×5,product/quantity/unit_price; the stored values are unchanged, so no row or value is affected. Noted, not scored as a rejection.)f_user,f_users,f_secretallnullon both specimens;showcase_task.covernullon every sampled row. Recorded as expected-absent (#4891 / ADR-0104), not as findings.platform-core.docs-audience-gate· rev 1 · partialCorpus: exactly one authored book —
showcase_manual,audience: "public", 3 groups + a syntheticuncategorized— and 9 docs, none with an explicitaudience.com.example.showcase(audienceorg) → anonymous 401{"error":"This documentation requires sign-in","code":"UNAUTHENTICATED"}— not a bare 404, not an empty-but-200 tree. The 403 half is unsupplied: no{permissionSet}-gated book exists in the corpus, exactly as the item's knownGaps predict.audiencevocabulary holds all three wayspublicserves anonymously (showcase_manual→ anon 200, 8 entries);org/unset serves any authenticated principal and refuses anonymous (implicit book → admin 200 / plain member 200, both 9 entries; anon 401). The{permissionSet}cell has no fixture — recorded blocked per the clause's own instruction, not passed.packages/spec/src/system/book.test.ts→ 30/30 passed, including line 222expect(audienceAllows({ permissionSet: 'crm_admin' }, unresolved)).toBe(false)— the exactpermissionSets: undefined → falsecase the clause names. Direct exercise of the exported contract agrees, and an unknown future audience shape also →false.setup_overviewand its wholeuncategorizedgroup. Every one of the 9 entries the member's tree offers was then fetched as the member: 9/9 resolved 200, zero 401/403. No nav entry refuses on click.setup_overviewis the unclaimed orphan (syntheticuncategorizedgroup, no authored membership claim). It is absent from the anonymous fetch of the public book's tree, and a direct anonymousGET /meta/doc/setup_overview→ 401, while the claimedshowcase_index→ 200 anonymously.orgGET /meta/book/com.example.showcase/tree→ admin 200 with a resolved tree (9 entries, groupall), anonymous 401. Not a 404.translationsrefused at authoringAccept-Language: zh-CNthe tree rendersshowcase_indexas"Showcase 示例"with a Chinese description, other entries falling back.BookSchemarefuses inlinetranslations: "Inlinetranslationson a book … was removed in spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it … NOTE the near neighbour that DOES work:doc.translationsis live" — the trap named explicitly.Also confirmed from the item's negatives: a repeated
?package=query parameter is refused, not coalesced —400 {"code":"VALIDATION_ERROR","message":"The \"package\" query parameter was supplied 2 times. Supply it at most once — this endpoint will not choose between conflicting values."}.records-forms.validation-rule-type-matrix· rev 1 · pass (4/4)VALIDATION_FAILED,fields[0].codeas ledgered:invalid_initial_state(project createdstatus:'completed') ·invalid_transition(legalplanned→active200, then illegalactive→planned) ·invalid_format×2 (regextax_id:'BADFORMAT', named-formatbilling_email:'not-an-email') ·json_schema_violation({tier:'gold',seats:0}) ·invalid_json('{not json') ·rule_violationfrom three rule kinds (conditional,cross_field,script). None inferred from a sibling.status(both variants) · format → the formatted field (tax_id,billing_email) · json_schema/invalid_json →support_config· cross_field →start_date, i.e.fields[0], exactly as the spec's own comment says · script/conditional →_record(object-level, correct for a rule declaring no field).showcase_accountandshowcase_project: 0 rows matching any rejected probe name. The state_machine probe project remainedstatus: "active"after the illegal transition attempt.tax_id:'12-3456789'·billing_email:'ok@example.com'·{tier:'premium',seats:5}· a non-churned account ·start_date < end_date·spentinside budget (plusstatus:'planned'for the FSM entry point). The rules gate violations without blocking legitimate writes.search.rls-both-personas· rev 2 · pass (4/4)Persona: a fresh sign-up bound to
contributorviasys_user_position;/security/explainconfirmspositions:["org_member","contributor","everyone"], layerrlsverdictnarrows,readFilter: {"owner":"<member email>"}— the RLS is genuinely in force.GET /data/showcase_invoice/<INV-1003>(linus's) → 404RECORD_NOT_FOUND. ThenPOST /data/showcase_invoice/query {search:'INV-1003'}→ membertotal 0, records [](reproduced twice); admin, identical query →total 1, ["INV-1003"].searchpresent the protocol computes a page-local total from the RLS-filtered find: member{search:'INV',limit:2}→total 1, n 1(matching their 1 visible row); admin the same query →total 3, hasMore true(2 returned + 1 more). No response exposes the true population to the restricted persona, and no hit or inflation derives from an invisible row — the item's own definition of the failure. (Wording note for the checklist: the page-local total equalsskip + n(+1 whenhasMore), so atskip:8the member seestotal 8with 1 visible row. The number is a function of the caller's ownskipand is shaped identically for admin, so it is not a leak — but "reconcile exactly with the rows they can enumerate" only holds whileskip ≤ visible count.)INV-QA-RLS) and searching for it returns it —total 1, ["INV-QA-RLS"]; their full visible set is exactly that 1 row.readFilter.Reproduction rule — the one
failcli.migrate-meta-codemodclauses 1 & 2 — the codemod cannot load the sources it is advertised to fixMechanism.
os migrate meta --from <N>loads and validates the stack config through the current (post-retirement) schema before any conversion runs. A source carrying a retired key — the only input the command exists to fix — fails that load, and the command exits 1 having rewritten nothing. The refusal it prints is the very message that prescribes the command.Steps (all outside the repo; needs only
@objectstack/specresolvable):objectstack.config.ts:src/probe.object.ts— any valid object with an explicitsharingModel.src/probe.view.ts— authored as a raw object literal (this is what a real upgrader has on disk;defineViewrefuses the key at authoring time, which is the same wall):os build→ exit 2, and the refusal ends with the prescription:✗ views.0.list.striped: \view.striped` was removed in @objectstack/spec 17.0.0 ([verdict question] view.jsonlist.striped/list.bordered/list.virtualScrollare LIVE on pass-through-only reads — every measured reader copies the key, none applies it #7176, ADR-0049 enforce-or-remove) … Run `os migrate meta --from 16` to rewrite existing sources automatically.`os migrate meta --from 16(with or without--out <path>).view-list-passthrough-keys-removed(declared bypackages/spec/src/migrations/entries/retired-keys/17.ui__ListView__striped.ts), dropsstripedfromviews[0].list, and exits 0.→ Loading configuration…then exit 1 with the same message:✗ defineStack validation failed (1 issue): ✗ views.0.list.striped: … Run \os migrate meta --from 16` to rewrite existing sources automatically.`os buildagain → the identical refusal. Nothing was rewritten.Controls that were run, and rule out the alternatives
type:'list'mistake was discarded and re-run withtype:'grid'so the retired key stands alone. Same result.--from 17→ same load-time refusal.--from 3→ same load-time refusal, i.e. even the ADR-0087 floor check is unreachable on the input that needs it (on a clean tree--from 3does reportCannot migrate from protocol 3: the chain's support floor is 10).--outis not implicated — reproduced without it.--json→{from:16, to:17, runtime:"17.0.0", applied:[{conversionId:"field-required-notnull-explicit", …}], todos:[{id:"action-descriptor-is-async-retired", …}]}. So the command is otherwise functional; it is specifically the retired-key input it cannot ingest.dispatcher-vs-hono-routetrap does not apply (CLI, not a route). Stale-dist ruled out — the CLI ran frompackages/cli/distand the refusal text matches currentpackages/spec/src.Blast radius.
os migrate meta --from 16is prescribed verbatim by 132 retirement messages acrosspackages/spec/src(--from 17by 4 more). Every one of them currently sends an upgrader to a command that cannot load their sources. The--storedarm (replaying oversys_metadatarows) is a different code path and was not implicated by this run.Fixture gaps
Recorded so the next sweep does not rediscover them.
access-security.no-active-org-session-semanticsneeds a ledgerblockedmarker. The item has noblockedfield, sochecklist-select.mjslists it as runnable — with or without--include-blocked. Three rounds have now paid for the same dead end (deployment istenancy: single; ADR-0095 Layer 0 inert;OS_TENANCY_POSTURE=isolatedcorrectly refused because@objectstack/organizationsis not declared by the host app). Suggested:blocked: { by: "fixture", ref: "#9334" }.showcase_projectandshowcase_taskare bothsharingModel: 'public_read_write', soshare_red_projects_with_execs,share_high_value_red_projects_with_managersandshare_open_tasks_with_managerare inert — the platform says so loudly at every boot (SharingServicePlugin: boot rule backfill failed … SHARING_NOT_ENABLED) and atPOST /sharing/rules/<name>/evaluate(422). The platform behaviour is correct; the fixture ships three decorative rules. This blocksrecord-share-grant-revokeclause 7 as written, and it is whysharing-rules-widenhad to be driven onshare_new_inquiries_with_field_ops/showcase_inquiryinstead.plugin-reportsexists in the repo but is absent from the boot's 48-plugin list; every/api/v1/reports*call answers501 NOT_IMPLEMENTED. This blocks 4 of the 5 clauses ofdashboards.saved-report-ownership, whose fixtures block assumes the service is present.agent/tool/skillseeds, so the build-authored round-trip inai.agent-tool-skill-metadata-roundtripclause 3 cannot be exercised. A runtime-authored substitute was used and is recorded; note for whoever builds the fixture that the runtime meta read-back does not apply the schema defaults (surface:'ask',active:true) the clause expects.{permissionSet}-gated and no doc-level-audiencefixture in the docs corpus — only one book (showcase_manual,public) and 9 docs with no explicit audience. Blocks the 403 half ofdocs-audience-gateclause 1 and the third cell of clause 2.sys_record_sharehas no admin edit surface, sosharing-rules-widen§A13 cannot be performed as written (PATCH/DELETE→ 405OBJECT_API_METHOD_NOT_ALLOWED, allowed verbs are read-only; the record-shares routes have no update verb).enforce-or-remove-authoring-gatesclause 3; the remaining open shapes are documented.passthrough()(keep-and-forward), which is not the same property.nodefrom the spawned child'sPATH; worth writing into the item as the zero-mutation recipe, since it also makes recovery restart-free.Checklist-accuracy findings & other observations
Moved to the first comment on this issue to keep the body under GitHub's size cap: 9 checklist-text corrections found during the run (none scored as a product failure) and 4 smaller product observations, each independently checkable.
Round complete: 23 / 23 items scored, no item left unreached. Subject pin never moved; nothing landed in the repo.
Filed against #9296.
Generated by Claude Code