Skip to content

QA run · owed:weekly (30/30) · 0c2aeb96 · 2026-11-14 · 28 PASS / 1 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #33534

Closed

Description

@objectstack-fleet

qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

Routine regression (checklist-test §6), the owed-retest list, dispatched by qa#3 (seat #33014) on the maintainer's 2026-11-14 21:46Z choice 「只跑 P0 和欠测复测」. Labels: qa-run only.

Environment fingerprint

Scope

Selector owed:weekly: the list node scripts/qa/retest-owed.mjs computed at seating (2026-11-14 ~21:50Z) — 33 rows, minus platform-core.builtin-apps-nav-render and platform-core.console-login (judged in the priority:P0 record of this round) and smoke:npm-18.4.0·D2 (a smoke cell, re-judged at the next npm smoke). Each id resolved as a bare id (§0). For each item the runner drove the item's clauses at the pinned revision and the named fix card's own repro.

item rev surface fix card(s) re-judged last judged by verdict
access-security.permission-set-address-in-execution-context 3 api #32945 #32940 5fb79bc0 2026-11-12 PASS
access-security.security-catalog-activation-and-clone-doors 5 api #33082 #33081 8e3d4a54 2026-11-13 PASS PASS
ai.mcp-coded-refusals-and-runtime-exposure 5 api #33030 #33081 8e3d4a54 2026-11-13 PASS PASS
ai.mcp-dataset-tools 1 api #33028, #32944 #33081 8e3d4a54 2026-11-13 PASS PASS
ai.mcp-read-file-pdf-word-extraction 2 api #33032, #32945 #33025 ee9ad834 2026-11-13 FAIL PASS
ai.mcp-run-action-exposure-gate 7 api #33029, #32163 #33092 c4196099 2026-11-13 PASS PASS
ai.mcp-tool-call-interceptor 2 api #32942 #33089 8e3d4a54 2026-11-13 PASS PASS
api-backend.actions-namespace-address 3 api #32946 #33025 ee9ad834 2026-11-13 FAIL PASS
api-backend.error-sentences-and-unique-violation-wire-code 9 api #33032 #33081 8e3d4a54 2026-11-13 FAIL PASS
api-backend.package-rest-lifecycle 6 api #33084 #33081 8e3d4a54 2026-11-13 PASS PASS
api-backend.rest-metadata-config-contract 10 api #33086, #33085 #33081 8e3d4a54 2026-11-13 FAIL PASS
api-backend.second-object-exposure-and-internal-fields 4 api #33086 #33081 8e3d4a54 2026-11-13 FAIL PASS
approvals.ooo-delegation-reroute 8 mixed #33062 #33058 ee9ad834 2026-11-13 FAIL
approvals.record-page-decisions 7 browser #33060 #33093 c4196099 2026-11-13 FAIL PASS
attachments-storage.chunked-upload-integrity 9 api #33027 #33025 ee9ad834 2026-11-13 FAIL PASS
automation.package-flow-override 4 mixed #33052 #33048 ee9ad834 2026-11-13 PASS
cli.deploy-runtime-dry-run-json 3 cli #32945 #32940 5fb79bc0 2026-11-12 PASS
cli.validate-v18-authoring-verdicts 3 cli #33086 #33081 8e3d4a54 2026-11-13 FAIL PASS
platform-core.bare-name-one-holder-refusal 5 mixed #33091, #33050, #33049 #33089 8e3d4a54 2026-11-13 FAIL PASS
platform-core.docs-portal-render 3 browser #33055 #33048 ee9ad834 2026-11-13 PASS
platform-core.namespace-object-name-refusal 3 mixed #32945 #32940 5fb79bc0 2026-11-12 PASS
records-forms.field-unique-enforcement 14 api #33032 #33025 ee9ad834 2026-11-13 FAIL PASS
records-forms.home-amount-preview-states 1 browser #32941 #32940 5fb79bc0 2026-11-12 PASS
records-forms.multi-currency-invoice 2 mixed #32941 #33089 8e3d4a54 2026-11-13 FAIL PASS
records-forms.related-list-server-pagination 12 browser #33051 #33048 ee9ad834 2026-11-13 PASS
records-forms.saved-view-management 2 browser #33098 #33096 8e3d4a54 2026-11-13 FAIL PASS
studio-authoring.dataset-member-vocabulary 2 mixed #32943 #32940 5fb79bc0 2026-11-12 PASS
studio-authoring.expression-editors 10 mixed #33100 #33096 8e3d4a54 2026-11-13 PASS
studio-authoring.flow-status-matches-runtime 7 mixed #33054 #33048 ee9ad834 2026-11-13 PASS
studio-authoring.no-entry-property-matrix 9 mixed #33101 #33096 8e3d4a54 2026-11-13 PARTIAL

Fix-card repros: every named fix card's own repro passes at 0c2aeb9 — #32941, #32942, #32943, #32944, #32945, #32946, #33027, #33028, #33029, #32163, #33030, #33032, #33049, #33050, #33051, #33052, #33054, #33055, #33060, #33062, #33082, #33084, #33085, #33086, #33091, #33098, #33100, #33101. (#32163 passes under its ruling E, "list and mark, do not hide": the member sees exactly the 15 membership-reach actions marked PERMISSION_DENIED. #33030: the stdio missing-id repro was driven; its RLS-hidden-id variant was not.)

Clauses that failed last time now pass:

Per-clause verdict tables

Every acceptance and negative clause of all 30 items has a verdict with one line of text oracle evidence, the layer each pin reaches, and the fix-card repro line. They follow as three comments (body limit):

  • Comment 1 (record-OWED-clauses-1.md): group D — the 14 api items.
  • Comment 2 (record-OWED-clauses-2.md): group E — the 2 cli and 9 mixed items.
  • Comment 3 (record-OWED-clauses-3.md): group F — the 5 browser items.

Tier 1: D 13 package batches / 55 pin files, E 13 batches / 47 files, F pins for home-amount-preview (18/18, unit) and related-list (7/7, HTTP) — all green, --maxWorkers=1, 0 failed.

Failures

  • approvals.ooo-delegation-reroute · off-clause probe · security finding — detail withheld, reported to PM

No other fail; every clause of the 30 items passes.

Derived item verdicts and gaps

  • 28 PASS, 1 PARTIAL, 1 FAIL (the withheld row above; every clause of approvals.ooo-delegation-reroute and its fix card Approval delegations accept an inverted window (valid_until before valid_from) and a self-delegation, both 201 and silently inert #33062 repro pass):
  • Fixture gaps:
    • records-forms.home-amount-preview-states steps[2]: the member's invoice from the fixture recipe has no currency and no lines, so no preview can mount; the runner added currency USD and one line (100) as the member.
    • cli.deploy-runtime-dry-run-json: an os deploy to a showcase booted in the shared tree writes the runtime's install cache into the shared examples/app-showcase/.objectstack/installed-packages/; the runner moved it out after each install. Next round its target runtime should boot from a scratch copy.

Off-clause probes (RUNNER rule 8)

About 130 minutes in total across the 30 items (2–6 min each), on the same boots and personas: opposite credential (session vs osk_ key) on MCP doors; unknown / missing ids on marked actions, chunk sessions and approval requests; limit 0 / -1 / 1000 on dataset queries; retries on a completed chunked upload; restarts on the same file DB after package install, override and uninstall; uninstall followed by the bare action call; deploy dry run with the install-ledger file absent; delegation windows (equal bounds, inverted, self, open-ended, update path); rate effective-date boundaries; synonyms length/count limits; forged meta writes and forged contact slices as non-admins; unknown doc fragments. Findings that pass the filing gate — three low product defects, card drafts attached:

  1. api-backend.package-rest-lifecycle — a package's installedAt and updatedAt read as the boot time after every restart (card draft card-OWED-1.md).
  2. api-backend.actions-namespace-address — after an install-local uninstall the withdrawn package's action handler stays registered, so a bare /actions call still refuses as ambiguous until restart (card draft card-OWED-2.md).
  3. cli.deploy-runtime-dry-run-json — os deploy --dry-run reads the install-ledger file while the install gate reads the DB; with the file gone and the DB kept, the dry run plans clean and the install refuses 409 (card draft card-OWED-3.md).

Not filed (observations / questions):

  • _activation/position/platform_admin {enabled:false} answers 200 and writes a ledger row but changes nothing (platform_admin is derived from admin_full_access).
  • Wording only: MCP query_dataset refusal says "body.selection.measures"; a completed chunked upload's retry with a different eTag says "the upload stays open"; the interceptor context omits registered for registered calls; deleting the current default view leaves the object with no default list view.

Checklist-accuracy notes (for the wave anchor, not extracted)

  • access-security.permission-set-address-in-execution-context steps[3]: the package-less row does not survive the restart it needs — the cold-boot activation-ledger backfill (ADR-0150 D8 R4) binds it to the name's one holder, so acceptance[3] measures the rebound row (answers still match).
  • cli.validate-v18-authoring-verdicts steps[2] should name defineStack(x, { strict: false }); inside a strict defineStack os lint never sees the omitted required.
  • studio-authoring.expression-editors steps[1]: record.quantity * record.unit_price infers Unknown in both engine and editor, so it is not a proven-Number example.
  • automation.package-flow-override: at 1500 px the override chip sits in the header itself, not folded into More.
  • records-forms.home-amount-preview-states acceptance[2].verify quotes "2024-06-01"; the page shows the user's format "Jun 1, 2024".
  • records-forms.related-list-server-pagination steps[1] says the wide layout has no tab strip; at 1440×900 the page draws one (clauses unaffected).
  • platform-core.docs-portal-render steps[3]: a package with a single book skips its docs index and opens the book directly, by design.

Close-out lines (for the seat, after filing)

Extracted: #CARD1 · api-backend.package-rest-lifecycle · observed beside the clauses
Extracted: #CARD2 · api-backend.actions-namespace-address · observed beside the clauses
Extracted: #CARD3 · cli.deploy-runtime-dry-run-json · observed beside the clauses
Parked-on: #ANCHOR · studio-authoring.no-entry-property-matrix · rows 3–5, 8–11, 14–21, 23–25, 28–36 not driven; matrix split (knownGaps[3])

Activity

  1. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

    QA run · owed:weekly clause tables (1/3) · 0c2aeb9 · runner group D (api 14)

    ai.mcp-run-action-exposure-gate · rev 7 · PASS · api · OWED: fix #33029, #32163

    env: showcase @0c2aeb96, port 41162, db file:/tmp/qa3/D/mcp-runaction.db, --seed-admin --no-compile --no-watch. MCP HTTP /api/v1/mcp; admin osk_ key, admin session token, phone.demo osk_ key + session token. Staged qa_ai_gated (requiredPermissions manage_metadata), torn down (DELETE 200). No pin (Tier 2 only).

    clause verdict evidence (text, one line)
    acceptance[0] pass run_action showcase_mark_done on task -xuSw6ddpWfkfzuC → isError {code:"VALIDATION_ERROR",status:400,message:"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 in_progress, progress 55, updated_at unchanged 22:31:40.666Z
    acceptance[1] pass admin osk list_actions (46 rows): showcase_portfolio_snapshot present, showcase_mark_done absent, qa_ai_gated present (1 row); member phone.demo list: showcase_portfolio_snapshot present, qa_ai_gated absent; member run qa_ai_gated → 403 "requires capability [manage_metadata]"; list_actions {objectName} → -32602 "Unrecognized key(s) on this list_actions call: objectName"
    acceptance[2] pass run_action showcase_portfolio_snapshot → {ok:true,scope:"global",accounts:14,projects:5,invoices:12}; REST totals showcase_account 14, showcase_project 5, showcase_invoice 12
    acceptance[3] pass server log: "[action-audit] MCP run_action 'showcase_portfolio_snapshot' on 'global' — body executes TRUSTED (system-elevated context, RLS/FLS-bypassing) for user 'Lq1tn1LKmrXOD9s4BrPj7pfj2uR2ncX8'"
    acceptance[4] pass sys_api_key total 2 before; create_api_key with objectName → "Action "create_api_key" on system object "sys_api_key" is not exposed via MCP: a system object's action is reachable only when its author exposed it to AI (ai.exposed) and list_actions lists it for you"; without → 400 VALIDATION_ERROR "Action 'create_api_key' is on a system object ('sys_api_key') and is not exposed via MCP — …declares ai: { exposed: true, description: … } on it"; total 2 after; create_api_key absent from list; revoke_api_key listed {aiExposed:true,requiresConfirmation:true}; run via admin session token {confirm:true} → ok:true; GET sys_api_key/UDIlC3p_1UEZYfaK revoked:true
    negative[0] pass refused showcase_mark_done left the task byte-identical (status, progress, updated_at); refused create_api_key left count 2
    negative[1] pass listing and run agree on ai.exposed: listed qa_ai_gated/portfolio run for admin; unlisted mark_done/create_api_key refused; revoke_api_key under osk_ listed with unavailable PERMISSION_DENIED and refused with that same sentence

    ai.mcp-read-file-pdf-word-extraction · rev 2 · PASS · api · OWED: fix #33032, #32945

    env: showcase @0c2aeb96, port 41163, db file:/tmp/qa3/D/mcp-readfile.db, --seed-admin --no-compile --no-watch; boot 1 default budget, boot 2 same DB with OS_MCP_READ_FILE_CPU_BUDGET_MS=60, boot 3 OS_MCP_READ_FILE_CPU_BUDGET_MS=abc (own DB). Fixtures generated locally (reportlab/python-docx): two.pdf (2 text pages), doc.docx (3 paragraphs), long.pdf (180 text pages, 156 KB), uploaded via POST /storage/upload/chunked → PUT chunk/0 → complete, then PATCH showcase_invoice_line/‹id› {receipt: fileId} (lines 4ixZkRqRpnhkb2jV, 9d6RRBS7dMu11BCg, AzZg4QCncMH6qRbu). MCP HTTP with admin osk_ key.
    Tier 1: @objectstack/mcp mcp-file-tools.test.ts 47✓, 0 failed at 0c2aeb9 (plugin unit).

    clause verdict evidence (text, one line)
    acceptance[0] pass read_file two.pdf: block 1 JSON {fileId:"121b4984-…", name:"two.pdf", mimeType:"application/pdf", size:1869, field:"receipt", maxBytes:10485760, download:{url,expiresAt}, content:"text", format:"application/pdf"}; block 2 "QA two-page PDF page one: alpha bravo charlie QA two-page PDF page two: delta echo foxtrot"
    acceptance[1] pass read_file doc.docx: facts content "text", format "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; text block "QA Word paragraph one: golf hotel. QA Word paragraph two: india juliet. QA Word paragraph three: kilo lima."
    acceptance[2] pass field+attachmentId → isError "Name one file: either field (with fileId for a multi-file field) or attachmentId, not both."; fileId without field → isError "fileId names a file within a field: send field with it."
    acceptance[3] pass (was FAIL at #33025 — the failed clause now passes) boot with budget 60: long.pdf facts content "text", truncated:true, note "Reading stopped at the CPU-time budget for one file (60 ms) after page 4 of 180, so the text is partial. Download the file with the link for the rest." (text 16166 chars); second read "after page 9 of 180"; no zero-page shape at 60 ms. (Default-budget boot: truncated:true at the 100000-char cap, not the budget — control)
    acceptance[4] pass OS_MCP_READ_FILE_CPU_BUDGET_MS=abc: boot exits 1, log "✗ OS_MCP_READ_FILE_CPU_BUDGET_MS must be a positive integer; got "abc". Unset it for the default (2000), or set a whole number."; health 000
    acceptance[5] pass mcp-file-tools.test.ts 47✓ 0 failed (503 over concurrency cap, >10 MiB facts+link, scanned PDF) — plugin unit, oracle test
    negative[0] pass long reads answered in 128–343 ms with text and the note; no hang
    negative[1] pass malformed variable refused at boot (exit 1), never defaulted

    ai.mcp-dataset-tools · rev 1 · PASS · api · OWED: fix #33028, #32944

    env: showcase @0c2aeb96, port 41161, db file:/tmp/qa3/D/mcp-dataset.db, --seed-admin --no-compile --no-watch; MCP HTTP /api/v1/mcp with admin osk_ key; member persona phone.demo osk_ key for probes.
    Tier 1: @objectstack/service-analytics tests/dataset-tools.test.ts 32✓ (1 file, 0 failed; unit), @objectstack/mcp mcp-dataset-tools.test.ts 21✓ (plugin unit), @objectstack/runtime mcp-dataset-tools-bridge.test.ts 6✓ (unit) — at 0c2aeb9.

    clause verdict evidence (text, one line)
    acceptance[0] pass live tools/list: list_datasets + query_dataset listed; list_datasets description: "Selection rule: one match on a measure wins, so answer through query_dataset… Several matches (two datasets or two members answer to the word): ask the user…"
    acceptance[1] pass live list_datasets {}: showcase_invoice_metrics {object:"showcase_invoice", dimensions status/region/issued_on/account, measures invoice_count 'Invoices'/total_home_sum/paid_count/paid_rate, each with label}; no filter/sql/join key anywhere in the answer
    acceptance[2] pass live terms ['invoices','status','qa_no_word']: invoice matches [{term:"invoices",member:"measures.invoice_count",matchedOn:"label"},{term:"status",member:"dimensions.status",matchedOn:"name"}]; qa_no_word in no dataset's matches
    acceptance[3] pass live query_dataset: rows Draft 2 / Paid 5 / Sent 5; provenance {kind:"dataset",dataset:{name:"showcase_invoice_metrics",label:"Invoice Metrics"},measures:[{name:"invoice_count",label:"Invoices"}],dimensions:[{name:"status",label:"Status"}]}; rowCount 3 == rows.length; truncated false; no sql key
    acceptance[4] pass live limit 1: rows 1, rowCount 1, truncated true (3 groups exist)
    acceptance[5] pass live qa_no_such_dataset → isError {error:{code:"NOT_FOUND",status:404,message:"Dataset "qa_no_such_dataset" not found."}}
    acceptance[6] pass live dataset key → -32602 "Unrecognized key(s)… dataset. Did you mean dataset → datasetName?"; top-level measures → -32602 "Selection members go inside selection, not beside it…"
    acceptance[7] pass pins 32✓/21✓/6✓ as above, 0 failed (hidden=missing NOT_FOUND, degraded 503, no-data:read token registers neither tool)
    negative[0] pass no sql key in any query_dataset answer (default, limit 1, limit 2)
    negative[1] pass live member (phone.demo): hidden showcase_invoice_metrics → NOT_FOUND "Dataset "showcase_invoice_metrics" not found." 404, byte-identical shape to qa_no_such_dataset
    negative[2] pass limit-1 truncated true with 3 groups

    ai.mcp-coded-refusals-and-runtime-exposure · rev 5 · PASS · api · OWED: fix #33030

    env: showcase @0c2aeb96, port 41160, db file:/tmp/qa3/D/mcp-coded.db, --seed-admin --no-compile --no-watch; restarted once on the same file DB. MCP HTTP /api/v1/mcp, admin osk_ key (POST /api/v1/keys), auditor.demo osk_ key.

    clause verdict evidence (text, one line)
    acceptance[0] pass @objectstack/mcp: plugin.record-resource-exposure 8✓, mcp-action-refusal-details 14✓, mcp-coded-refusal 178✓ (7 files 326 passed, 0 failed, plugin unit); @objectstack/objectql plugin-runtime-object-metadata-copy 10✓ (3 files 66 passed); @objectstack/qa-dogfood mcp-stdio-runtime-object-exposure 10✓ (10 files 121 passed, 0 failed) (engine layer)
    acceptance[1] pass live HTTP door (pin is plugin unit → live leg driven): tools/call get_record {objectName:showcase_task, recordId:qa_missing_id_123} → isError {error:{code:"RECORD_NOT_FOUND",message:"Record qa_missing_id_123 not found in showcase_task",status:404}}; same after restart. id arg → -32602 "Did you mean id → recordId?"
    acceptance[2] pass live HTTP: PUT /meta/object/qa_noapi {enable:{apiEnabled:false},sharingModel:private} 200; query_records → {error:{code:"OBJECT_API_DISABLED",status:404}} before AND after restart; DELETE /meta/object/qa_noapi 200 → list_objects no longer contains qa_noapi (was present before delete)
    acceptance[3] pass live: staged qa_ai_mark; auditor.demo run_action on task 4RF3oAKjXeLO0lrS → {error:{code:"PERMISSION_DENIED",message:"You do not have permission to perform this action. Contact your administrator…",status:403}} — keys code/message/status only, no details; task progress stays 0. Control admin run → ok:true, progress 50
    • fix-card MCP stdio get_record answers a missing or hidden id with uncoded error text, while the HTTP door answers RECORD_NOT_FOUND 404 #33030 repro: pass — stdio door (scratch copy app-icpt, os start OS_MCP_STDIO_ENABLED=true + admin osk_ key, port 41164): tools/call get_record {showcase_task, qa_missing_id_123} → isError {error:{code:"RECORD_NOT_FOUND",message:"Record qa_missing_id_123 not found in showcase_task",status:404}} (was uncoded text); update_record same id → same coded RECORD_NOT_FOUND. The RLS-hidden-id-under-member-key variant was not driven.
    • probe: 3 min · doors get_record/query_records over HTTP MCP · classes: empty recordId (→ 404 RECORD_NOT_FOUND "Record not found"), unknown object (→ 404 OBJECT_NOT_FOUND), query on the deleted object (→ 404 OBJECT_NOT_FOUND) · findings: none
    • notes: teardown DELETE /meta/action/qa_ai_mark 200.

    ai.mcp-tool-call-interceptor · rev 2 · PASS · api · OWED: fix #32942

    env: scratch copy /tmp/qa3/D/ws/examples/app-icpt (showcase @0c2aeb96 + the fixture's inline qa-mcp-interceptor plugin, as any), port 41164, db file:/tmp/qa3/D/icpt.db. HTTP legs on os dev --seed-admin --no-watch (scratch compile); stdio leg on os start (same DB) with OS_MCP_STDIO_ENABLED=true + OS_MCP_STDIO_API_KEY=admin osk_, calls sent after /api/v1/health 200, both pipes captured in full.
    Tier 1: @objectstack/mcp tool-call-interceptor.test.ts 49✓, tests/plugin-execution-context.test.ts 9✓ (0 failed at 0c2aeb9; plugin unit).

    clause verdict evidence (text, one line)
    acceptance[0] pass list_objects normal result (objects list); log [qa-interceptor] {"door":"mcp","transport":"http","toolName":"list_objects","userId":"5c9STJ04jzkjcHpBXqsi3PauIQmeLVKS"} = admin key owner
    acceptance[1] pass delete_record → isError {error:{code:"PERMISSION_DENIED",message:"QA interceptor refuses delete_record",status:403}}; GET /data/showcase_task/2lbL5U2rjMl89Bi- → 200, record present
    acceptance[2] pass describe_object → isError {error:{code:"INTERNAL_ERROR",message:"Internal server error",status:500}}; log WARN "[MCP] The installed tool-call interceptor answered "describe_object" with neither a tool result nor a { outcome: 'deny', code, message } refusal, so the call was refused as a server fault."
    acceptance[3] pass log [qa-interceptor] {…"toolName":"qa_no_such_tool","registered":false…}; client gets "MCP error -32602: Tool qa_no_such_tool not found"
    acceptance[4] pass stdio list_objects → result on stdout; stderr [qa-interceptor] {"door":"mcp","transport":"stdio","toolName":"list_objects","userId":"5c9STJ04…"}
    acceptance[5] pass pins 49✓ + 9✓, 0 failed (plugin unit, oracle test)
    negative[0] pass denied delete_record: task still 200 afterwards
    negative[1] pass malformed verdict → isError 500 INTERNAL_ERROR, never success
    negative[2] pass unregistered name → SDK's own "Tool qa_no_such_tool not found", nothing else
    • fix-card os start with the stdio MCP transport drops a host plugin console.log written after boot: it reaches neither stdout nor stderr #32942 repro: pass — os start stdio: one [qa-interceptor] … "transport":"stdio" line per call on STDERR (list_objects, get_record, update_record); stdout carried only the JSON-RPC answers (ids 1–4, no stray lines).
    • probe: 2 min · doors stdio get_record/update_record on a missing id · classes: missing id over the other transport · findings: none. Observation: for registered calls the logged context omits registered (only the unregistered call carries registered:false) — not a clause, not filed.
    • notes: no OS_HOME ledger concern (scratch copy).

    access-security.security-catalog-activation-and-clone-doors · rev 5 · PASS · api · OWED: fix #33082

    env: showcase @0c2aeb96, port 41165, db file:/tmp/qa3/D/catalog-act.db, --seed-admin --no-compile --no-watch, OS_PLATFORM_OWNER_EMAIL unset. Member = auditor.demo (sys_user_position showcase_auditor). SET = showcase_auditor (holder com.example.showcase).
    Tier 1 (vitest --maxWorkers=1 at 0c2aeb9): @objectstack/qa-dogfood catalog-activation-door.dogfood 10✓, permission-set-clone-door.dogfood 11✓ (HTTP, in-process booted showcase); @objectstack/runtime catalog-activation-door 91✓, permission-set-clone 53✓, permission-set-package-doors 40✓ (domain handler over fakes); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass all 5 pin files ≥1 test, 0 failed (counts above)
    acceptance[1] pass live: POST /security/_activation/position/showcase_auditor {enabled:false} → 200; ledger row {position, showcase_auditor, com.example.showcase, active:false}; auditor me/permissions positions org_member,everyone and sets lose showcase_auditor; {enabled:true} → 200, positions/sets restored
    acceptance[2] pass everyone → 400 VALIDATION_ERROR "Position 'everyone' is an audience anchor…"; guest → 400 same shape; qa_no_such_position → 404 RESOURCE_NOT_FOUND "…has no definition"; ledger total stayed 1
    acceptance[3] pass clone {name:qa_clone_set,label} → 200 {permissionSet{name,label,objects…}, notice "The copy was saved in package 'com.example.showcase.custom'…"}, keys name,label,objects — no isDefault/adminScope/packageId; same name → 409 RESOURCE_CONFLICT; source's own name → 409; missing label → 400 VALIDATION_FAILED; extra key → 400 VALIDATION_FAILED "Unknown key extra"; bad name → 400; unknown source → 404; member → 403 PERMISSION_DENIED "…requires the manage_metadata capability"; anonymous → 401 UNAUTHENTICATED; none of qa_clone_2..6 saved
    acceptance[4] pass ?package=com.example.showcase OFF/ON → 200 with its own row (permission, showcase_auditor, com.example.showcase), as the bare write (bare OFF/ON → same row, same answers); ?package=com.objectstack.plugin-security → 409 RESOURCE_CONFLICT "…is not held by package "com.objectstack.plugin-security"… it is held by package "com.example.showcase"", details.candidates present; ?foo=1 → 400 VALIDATION_ERROR (permission: "Supported parameters: package."; position: "This endpoint takes no query parameters."); package= blank → 400; package repeated → 400 "supplied 2 times"; ?package on a position → 400; ledger total 2 before and after every refusal; clone ?package=NOT_HOLDER → 409, ?foo=1 → 400, neither saved; clone ?package=holder → 200
    acceptance[5] pass pin-only clause: runtime permission-set-package-doors.test.ts 40✓ 0 failed (domain handler over a stand-in registry)
    • fix-card Switching admin_full_access off through POST /security/_activation is accepted for the last platform admin, and nothing in the product can switch it back on #33082 repro: pass — seeded admin (only admin_full_access holder; positions platform_admin, org_owner…): POST /security/_activation/permission/admin_full_access {enabled:false} → 403 PERMISSION_DENIED "Refusing this activation-ledger write: switching the 'admin_full_access' permission set off would end the platform-administrator standing of '2owBqkw…', who is the last platform administrator this system has that is not already banned…"; me/permissions unchanged (admin_full_access still held); no ledger row written. Same refusal with ?package=com.objectstack.plugin-security.
    • probe: 6 min · doors _activation position/permission as admin · classes: built-in positions (platform_admin, org_owner), built-in set organization_admin_no_bypass, empty name (position/ → 404 ENDPOINT_NOT_FOUND), non-boolean enabled ("no" → 400 VALIDATION_FAILED) · findings: one observation, not filed — POST _activation/position/platform_admin {enabled:false} answers 200 and writes a ledger row (active:false) but the admin's positions still list platform_admin (it is derived from the admin_full_access grant, resolve-authz-context.ts ~2722), so the door accepts a switch it does not honour; everyone/guest anchors are refused 400 instead. No open issue found (search "platform_admin activation"). Possible card for the PM to weigh: "the activation door accepts switching the derived platform_admin position off (200 + ledger row) and nothing changes". All switched rows restored to active:true afterwards.

    api-backend.package-rest-lifecycle · rev 6 · PASS · api · OWED: fix #33084

    env: showcase @0c2aeb96, port 41166, db file:/tmp/qa3/D/pkg-life.db, --seed-admin --no-compile --no-watch; restarted twice on the same file DB. Admin session. No pin (Tier 2). Tree install ledger examples/app-showcase/.objectstack/installed-packages/ absent before and after (POST /packages is not install-local).

    clause verdict evidence (text, one line)
    acceptance[0] pass POST /api/v1/packages {manifest{id com.example.qa-pkg-lifecycle-probe, 1.0.0, scope project, type app}} → 201 {manifest…, namespace "qa_pkg_lifecycle_pro", status installed, enabled true}; GET /packages/‹id› → 200 same manifest
    acceptance[1] pass same POST again → 409 RESOURCE_CONFLICT "Package 'com.example.qa-pkg-lifecycle-probe' already exists"; re-read manifest unchanged (name QA Probe, 1.0.0)
    acceptance[2] pass PATCH {name:'QA Probe Renamed',description:'edited',version:'1.1.0'} → 200; GET: the three changed; id/scope/type and enabled true/status installed/installedAt 22:38:40.151Z identical to before (updatedAt advanced)
    acceptance[3] pass PATCH {name:''} → 400 VALIDATION_ERROR "name must not be empty"; {version:'not-semver'} → 400 VALIDATION_ERROR "version must be a SemVer 2.0.0 string…"; {} → 400 VALIDATION_ERROR "…nothing to update"
    acceptance[4] pass POST ?overwrite=true (2.0.0, name QA Probe v2, description overwritten) → 201; GET /packages lists exactly 1 row for the id; version 2.0.0
    acceptance[5] pass POST /api/v1/packages installed (201, not a publish-shape 400; scope 'custom' → 400 VALIDATION_ERROR naming manifest.scope); PATCH /packages/‹id› → 200, unknown id → 404 RESOURCE_NOT_FOUND "Package 'qa.nope.missing' not found" (not a routing 404)
    acceptance[6] pass after restart on the same DB: GET → name "QA Probe v2", description "overwritten", version 2.0.0, dependencies equal to the pre-restart read (4/4 equal); list rows for the id = 1
    negative[0] pass duplicate POST → 409, manifest survived
    negative[1] pass PATCH kept enabled/status/installedAt
    negative[2] pass one list row after overwrite
    negative[3] pass restart served the last write (2.0.0), not 1.1.0/1.0.0
    • fix-card Package registry: after a restart the server serves an older manifest than the last overwrite re-install (rollback, or several writes within one second) #33084 repro: pass — rollback: com.example.qa-rollback 1.0.0 → (≥1 s) 2.0.0 overwrite → (≥1 s) 1.0.0 overwrite: live 1.0.0, after restart 1.0.0 (was 2.0.0), list 1 row 1.0.0; same-second: qa-samesec 1.0.0→2.0.0→3.0.0 within one second: live 3.0.0, after restart 3.0.0 (was 1.0.0), list 1 row. Held across a second restart.
    • probe: 4 min · doors GET/PATCH /packages/:id across two restarts · classes: unknown id, scope outside the enum, second restart · findings: 1 (low) — every package's installedAt and updatedAt are re-stamped to the boot time on each restart (probe package: 22:38:41.421Z pre-restart → 22:39:02.508Z after restart 1 → 22:39:27.809Z after restart 2; com.example.showcase likewise), though acceptance[2]/negative[1] treat installedAt as a lifecycle field a PATCH must preserve. Reproduced on two restarts. No open issue found ("installedAt restart", "installedAt package registry").
    • CARD DRAFT (probe, product defect, low): title "Package registry: installedAt and updatedAt read as the boot time after every restart — the stored install time is not served" / body: Repro (stock showcase, file DB, admin): 1) POST /api/v1/packages {manifest:{id:"com.example.qa-pkg-lifecycle-probe",name:"QA Probe",version:"1.0.0",scope:"project",type:"app"}} → 201, installedAt T0. 2) Restart the server on the same DB. 3) GET /api/v1/packages/com.example.qa-pkg-lifecycle-probe → installedAt and updatedAt equal the restart time T1, not T0; again T2 after a second restart. Expected: installedAt (and updatedAt) served as stored (the PATCH contract already preserves installedAt). Mechanism (not proven): the boot hydration re-installs each sys_packages row through registry.installPackage (packages/services/service-package/src/index.ts), which stamps fresh lifecycle times. QA-source: #RECORD · api-backend.package-rest-lifecycle · observed beside the clauses

    attachments-storage.chunked-upload-integrity · rev 9 · PASS · api · OWED: fix #33027

    env: showcase @0c2aeb96, port 41167, db file:/tmp/qa3/D/chunked.db, --seed-admin --no-compile --no-watch, local storage adapter; admin session. (The door's chunkSize floor is 5242880, so the 3-chunk fixture is 5 MiB + 5 MiB + 10 B.) Note: prior record #33025 lists this item PASS 4/0/0/0; items-D's "FAIL" refers to the record overall.
    Tier 1 (0c2aeb9, --maxWorkers=1): @objectstack/service-storage chunked-upload-integrity 23✓, chunk-part-record-concurrency 24✓ (unit); @objectstack/qa-dogfood storage-chunked-resume-integrity.dogfood 4✓ (HTTP, in-process); @objectstack/organizations storage-upload-organization-change.wall 10✓ (HTTP in-process on a booted organization wall); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass all four pin files ≥1 test, 0 failed (above)
    acceptance[1] pass live: PUT chunks 0 and 2, complete → 409 RESOURCE_CONFLICT "…does not hold chunk 1 of the 3 it declared…", details.missingChunks [1]; progress status in_progress, uploadedChunks 2
    acceptance[2] pass live: PUT chunk 0 again → 200; progress uploadedChunks 2, uploadedSize 5242890 (unchanged, counted once); then PUT 1 + complete → 200, progress completed 3/3
    acceptance[3] pass (pin, HTTP in-process) — live leg blocked(fixture) stock single boot holds one organization (RUNNER "A stock single boot holds exactly one organization"); the wall pin drives chunk PUT 409 / complete 409 / progress 200→409-at-expiry after an org switch on a booted wall, 10✓. Off-clause: set-active null on the single boot leaves chunk PUT/complete 200 (no wall; not this clause's A→B switch)
    • fix-card Retrying a chunked-upload completion after it succeeded answers 500 INTERNAL and re-stamps the completed session 'failed' #33027 repro: pass — 1-chunk r.bin: complete → 200 {fileId bbba79f5…}; identical retry → 200 with the same fileId/key (twice); progress stays completed (never failed); PUT chunk/0 to the completed session → 409 RESOURCE_CONFLICT "This chunked upload has already completed and its file is committed, so it takes no more chunks…"; file URL 200.
    • probe: 4 min · doors complete/chunk/progress · classes: retry with a different eTag on a completed session, null active organization on single posture, chunkSize below floor (400 INVALID_REQUEST ">=5242880") · findings: none filed. Observation (wording, low): a completed session's complete-retry listing a different eTag answers 409 "…Nothing was assembled; the upload stays open until it expires." although the session is completed and stays completed.

    records-forms.field-unique-enforcement · rev 14 · PASS · api · OWED: fix #33032

    env: showcase @0c2aeb96, port 41168, db file:/tmp/qa3/D/uniq.db (stock SQLite), --seed-admin --no-compile --no-watch; admin session. Fixture: POST /packages {com.objectstack.qa.unique, namespace qauniq, enableOnInstall} 201; PUT /meta/object/qauniq_item?package=… (sku unique:true label 'SKU', gsku unique:'global' label 'Global SKU', nick) 200.
    Tier 1 (0c2aeb9): @objectstack/qa-dogfood field-unique-rest-refusal.dogfood 5✓ (HTTP, booted stack on SQLite); @objectstack/driver-memory memory-unique-constraint 27✓ (driver); @objectstack/driver-sql sql-driver-8577-tenant-scoped-declared-unique 39✓, sql-driver-declared-index-organization-respelling 26✓ (SqlDriver); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass (was FAIL at #33025 — the failed clause now passes) live POST {name:r2,sku:A-1} → 409 {"error":"A record with this SKU already exists","code":"UNIQUE_VIOLATION","developerMessage":"Duplicate record refused on 'QA Unique Item': a unique constraint on 'SKU' already holds this value. No record was written.","field":"sku","object":"qauniq_item"}; gsku → 409 "…Global SKU…", field "gsku"; count 1 before and after; + pin 5✓
    acceptance[1] pass two POSTs with no sku → 201, 201; count 3
    acceptance[2] pass PATCH r2 {sku:'A-1'} → 409 UNIQUE_VIOLATION, re-read sku B-1; PATCH r1 {nick} → 200; PATCH r1 re-sending its own sku A-1 → 200
    acceptance[3] pass POST /data/qauniq_item/updateMany {records:[r1→C-1, r2→C-1], options:{atomic:true}} → 207 succeeded 0 / failed 2, results[0] ROLLED_BACK, results[1] UNIQUE_VIOLATION 409; both rows re-read byte-identical (REST door = the record-list updateMany; the predicate half is the memory pin's)
    acceptance[4] pass oracle test: memory-unique-constraint 27✓ + sql-driver-8577 39✓ + respelling 26✓ (driver layer, as the clause prescribes); live two-org probe not run (single posture, knownGaps)
    acceptance[5] pass oracle test: memory-unique-constraint.test.ts 'the refusal is recognisable to the ENGINE, so the autonumber resync converges' ✓
    negative[0] pass colliding creates never 2xx; count unchanged
    negative[1] pass no driver prefix, no 'UNIQUE constraint failed', no bound values in any refusal body
    negative[2] pass atomic updateMany: zero rows changed
    negative[3] pass (pin) SQL-family/memory scope legs green in the pins; live two-org leg blocked(fixture), single posture

    api-backend.rest-metadata-config-contract · rev 10 · PASS · api · OWED: fix #33086, #33085

    env: showcase @0c2aeb96, port 41169, db file:/tmp/qa3/D/restmeta.db, --seed-admin --no-compile --no-watch; OS_ALLOW_UNMASKED_OBJECT_METADATA unset (env and boot log). Restricted persona: qa.liaison@example.com via POST /auth/admin/create-user + change-password, bound with POST /data/sys_user_position {position: showcase_client_liaison} 201 (me/permissions positions org_member, showcase_client_liaison, everyone). Non-default clauses scored with oracle test (knownGaps ruling #15543).
    Tier 1 (0c2aeb9): @objectstack/rest rest-sub-config-parse-not-cast 25✓, discovery-services-route-follows-mount 10✓, meta-object-fls 147✓, rest-config-mount-table.pin 24✓ (route-handler / mount-table harness); @objectstack/qa-dogfood rest-config-contract-live.dogfood 7✓ (HTTP, booted showcase), rest-config-contract-harness 7✓ (real RestServer); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass live: GET /api/v1/meta, /meta/types, /meta/diagnostics, /meta/_drafts, /meta/object, /meta/object/showcase_task, …/references, …/layers, /meta/book/showcase_manual/tree → all 200, none equal to the control (GET /api/v1/qa-no-such-route-xyz → 404 ENDPOINT_NOT_FOUND); + live pin 7✓
    acceptance[1] pass harness pin rest-config-contract-harness ✓ (prefix '/metadata': whole table re-based, nothing under /meta, discovery /api/v1/metadata) + discovery-services-route-follows-mount 10✓; live default: discovery routes.metadata and services.metadata.route = /api/v1/meta
    acceptance[2] pass (was FAIL at #33081 — the failed clause now passes) mount-table pin: "metadata.endpoints.types … exactly 2 route(s)", "items … exactly 1", "maintenance … exactly 4" (incl. POST _migrate-stored, POST _migrate-record-currency-codes), "item … exactly 14" (incl. GET :type/:ns/:name, app bundle), each asserted as absence in both directions; the clause now spells 4 and 14
    acceptance[3] pass live: liaison member GET /meta/object/showcase_project → 20 fields vs admin 23; missing exactly budget, spent, budget_remaining; no "budget"/"spent" text anywhere in the member document (even account.relatedListColumns drops budget); every other field identical; harness: maskObjectFields:false preserved and pass-through masker (meta-object-fls 147✓, parse-not-cast §D)
    acceptance[4] pass harness pin rest-config-contract-harness ✓: enableCache true→getMetaItemCached, false→getMetaItem, and each carve-out (app, dashboard, ?state=draft, ?preview=draft, ?package=, ?member=, book, doc) calls getMetaItem with the switch on
    acceptance[5] pass harness pin ✓: cacheTtl 3600, 0, -1 and endpoints.schema refused naming the key, MetadataEndpointsConfigSchema and the replacement; parse-not-cast §E ✓
    negative[0] pass no masked field in the liaison's document (no SECURITY finding)
    negative[1] pass masking scored from the member's document against the admin's, never the admin's alone
    negative[2] pass mount-table pin: every switch's routes absent when false

    api-backend.second-object-exposure-and-internal-fields · rev 4 · PASS · api · OWED: fix #33086

    env: showcase @0c2aeb96, port 41170, db file:/tmp/qa3/D/secobj.db, --seed-admin --no-compile --no-watch. Live fixture authored at runtime (no shared-file edit): qa_sox_target (enable.apiEnabled:false), qa_sox_getonly (enable.apiMethods:['get']), qa_sox_source (lookups t_ref→target, g_ref→getonly, internal_f internal:true). Personas: admin and qa.member (admin create-user + change-password).
    Tier 1 (0c2aeb9): @objectstack/metadata-protocol protocol.relation-condition-target-exposure 13✓, protocol.expand-target-exposure 15✓, protocol.data-door-internal-field-delegation 6✓; @objectstack/objectql protocol-data-door-internal-field-positions 50✓, internal-field-positions 6✓ (protocol/engine); @objectstack/qa-dogfood second-object-exposure.dogfood 48✓, internal-field-data-door-positions.dogfood 15✓ (HTTP on a booted fixture app); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass (was FAIL at #33081 — the failed clause now passes) every automated.ref file exists and reports ≥1 test, 0 failed, incl. the re-pointed packages/objectql/src/protocol-data-door-internal-field-positions.test.ts 50✓ and the delegation twin 6✓
    acceptance[1] pass live, admin and member identical: GET ?filter={"t_ref":{"name":"x"}} and POST /query same filter → 404 OBJECT_API_DISABLED "…the condition on it at 'filter.t_ref' was not evaluated…", object qa_sox_target; g_ref (apiMethods ['get']) → 405 OBJECT_API_METHOD_NOT_ALLOWED at 'filter.g_ref', allowed ["get"]; dotted form → 400 prescribing the nested form; + HTTP pin 48✓
    acceptance[2] pass (pin, HTTP) second-object-exposure.dogfood ✓ (withheld $expand returns the bare id). Live: by-id ?expand=t_ref → 200 as admin (fixture row has no t_ref, since the target takes no API write, so the live leg does not discriminate); member 403 PERMISSION_DENIED on the runtime object (permission, not exposure)
    acceptance[3] pass live, admin and member identical: filter, sort, POST /query groupBy, aggregations[0].field on internal_f → 400 {code:"INVALID_FIELD",field:"internal_f",object:"qa_sox_source"} each, sentence "…declared internal: true… refused rather than evaluated"; sys_api_key ?filter key → 400 INVALID_FIELD field key; no 500
    negative[0] pass internal-field filters refused 400 before any rows are served (no row-count oracle)

    api-backend.error-sentences-and-unique-violation-wire-code · rev 9 · PASS · api · OWED: fix #33032

    env: Tier 1 at 0c2aeb9 (--maxWorkers=1) + one live leg on showcase port 41171, db file:/tmp/qa3/D/errsent.db, --seed-admin --no-compile --no-watch (runtime-authored qa_es_contact with email unique:true and a declarative update action qa_es_dup; no shared file edited).
    Tier 1: @objectstack/rest rest-write-route-hook-refusal-sentence.ledger 673✓, rest-hook-script-fault-envelope 18✓ (unit); @objectstack/runtime dispatcher-unique-violation-wire-code 7✓ (engine: HttpDispatcher over ObjectQL + SqlDriver); @objectstack/service-storage storage-routes.server-fault-withheld 18✓ (unit); @objectstack/qa-dogfood error-sentences-live.dogfood 4✓ (HTTP via @objectstack/verify bootStack on the scratch fixture app); 0 failed.

    clause verdict evidence (text, one line)
    acceptance[0] pass all five pin files ≥1 test, 0 failed (above)
    acceptance[1] pass (pin, HTTP) error-sentences-live ✓ "POST /data/OBJECT: 400 with the hook's own sentence, HOOK_ABORTED, noth[ing written]" and "…/createMany: 400 … HOOK_ABORTED"; nested-envelope rows 400 HOOK_ABORTED in the ledger pin 673✓ (unit, pin-only by the clause)
    acceptance[2] pass (was FAIL at #33025/#33081 — the failed clause now passes) live: POST /api/v1/actions/qa_es_contact/qa_es_dup/‹id› → 409 {code:"UNIQUE_VIOLATION",message:"Duplicate record refused on 'QA ES Contact': a unique constraint on 'Email' already holds this value. No record was written.",details:{developerMessage,field:"email",object:"qa_es_contact"}} — key set exactly [developerMessage, field, object]; row keeps b@example.com; pin ✓ "409 UNIQUE_VIOLATION, details exactly { developerMessage, field, …" (HTTP) and engine pin 7✓
    acceptance[3] pass (pin, HTTP) error-sentences-live ✓ "POST /storage/upload/chunked: 500 INTERNAL with the generic sentence, never [the caught text]"; storage-routes.server-fault-withheld 18✓

    api-backend.actions-namespace-address · rev 3 · PASS · api · OWED: fix #32946

    env: showcase @0c2aeb96, port 41172, db file:/tmp/qa3/D/actions-ns.db, --seed-admin --no-compile --no-watch, OS_HOME=/tmp/qa3/D/home-act (install ledger stayed there; tree ledger examples/app-showcase/.objectstack/installed-packages absent). Scratch packages /tmp/qa3/D/ws/examples/qa-a (com.qa.addr-a, ns qa_a: object qa_a_ticket with embedded script action escalate, object-less qa_a_ping, permission set qa_a_reader) and qa-b (com.qa.addr-b, ns qa_b: object qa_b_note, standalone script action escalate objectName qa_a_ticket; defineStack {strict:false} since qa_a_ticket is another package's), each objectstack build then os package install DIR/dist/objectstack.json (admin). Handlers return {ran: ‹package id›}. RID 3EnXZFk2VRRCIwHd; env action qa_env_ping PUT /meta/action without ?package= 200.
    Tier 1 (0c2aeb9): @objectstack/runtime domains/actions-addressed-route 47✓, action-package-addressing 118✓, action-execution-addressed-exposure-refusal 16✓ (domain/runtime unit); 0 failed. All clauses also driven live.

    clause verdict evidence (text, one line)
    acceptance[0] pass (was FAIL at #33025 — the failed clause now passes) bare POST /actions/qa_a_ticket/escalate/RID → 409 RESOURCE_CONFLICT "The action "escalate" is held by 2 packages in this environment and the caller names no package: package "com.qa.addr-a"; package "com.qa.addr-b". Nothing picks one of them: address it by its package.", details.candidates both; no handler result (twice)
    acceptance[1] pass /actions/qa_a/qa_a_ticket/escalate/RID → 200 {ran:"com.qa.addr-a"}; /actions/qa_b/qa_a_ticket/escalate/RID → 200 {ran:"com.qa.addr-b"}
    acceptance[2] pass /actions/qa_a/global/qa_a_ping → 200 {ran:"com.qa.addr-a",action:"qa_a_ping"}; /actions/qa_b/global/qa_a_ping → 404 RESOURCE_NOT_FOUND "Action 'qa_a_ping' not found on object 'global' in namespace 'qa_b'"
    acceptance[3] pass /actions/_env/global/qa_env_ping → 200 {ran:"environment"}; /actions/_env/global/qa_a_ping → 404 "Action 'qa_a_ping' not found on object 'global' in the environment"
    acceptance[4] pass /actions/qa_a/_activation/qa_a_ticket → 404 "An addressed action path is /actions/:ns/:object/:action[/:recordId] or /actions/:ns/global/:action (ADR-0150 D5)"; /actions/qa_a/qa_a_ticket/escalate/RID/extra → 404 ENDPOINT_NOT_FOUND; record byte-identical after all calls
    negative[0] pass bare call ran neither handler (409, no data)
    negative[1] pass qa_b global miss → 404, no fallback to qa_a's
    negative[2] pass with both holders the bare call is 409 (never 200 with qa_a's result); with qa_b removed AND the server restarted, the bare call → 200 {ran:"com.qa.addr-a"}
    • fix-card qa(checklist): re-point three stale clauses from the 18.4.0 acceptance (#32940): actions two-holder 409, flow bare-name, co-owner install door #32946 repro (item 1): pass — the clause and negative now expect the 409 naming both packages, which is what the door answers.
    • probe: 5 min · doors bare/addressed /actions, install-local uninstall · classes: unknown namespace segment (→ 404 ROUTE_NOT_FOUND), bare global (/actions/global/qa_a_ping → 200 qa_a; /actions/global/qa_env_ping → 200 env), addressed call without recordId (→ 200), live uninstall of the second holder · findings: 1 — after DELETE /api/v1/marketplace/install-local/com.qa.addr-b (200, note "…the package withdrawn from the running kernel…"; GET /packages/com.qa.addr-b → 404; /actions/qa_b/… → 404) the bare call still answers 409 "Action handler 'qa_a_ticket:escalate' is ambiguous for a call that names no package: 2 owners register one ('app:com.qa.addr-a', 'app:com.qa.addr-b')…", i.e. the withdrawn package's handler stays registered until a restart (after restart → 200 qa_a). Reproduced twice (fresh install/uninstall cycle). No open issue found.
    • CARD DRAFT (probe, product defect, low): title "An install-local uninstall leaves the package's action handlers registered: a bare /actions call still refuses as ambiguous, naming the withdrawn package, until restart" / body: Repro (stock showcase, OS_HOME set, admin): build and os package install two scratch packages — com.qa.addr-a (object qa_a_ticket, embedded script action escalate) and com.qa.addr-b (standalone script action escalate, objectName qa_a_ticket); create one qa_a_ticket RID. 1) POST /api/v1/actions/qa_a_ticket/escalate/RID → 409 naming both packages (correct). 2) DELETE /api/v1/marketplace/install-local/com.qa.addr-b → 200, note "Cached manifest removed, the package withdrawn from the running kernel, …"; GET /api/v1/packages/com.qa.addr-b → 404. 3) Repeat step 1 → 409 RESOURCE_CONFLICT "Action handler 'qa_a_ticket:escalate' is ambiguous for a call that names no package: 2 owners register one ('app:com.qa.addr-a', 'app:com.qa.addr-b')…". Expected: one holder left, so 200 with com.qa.addr-a's result — which is what the same call answers after a restart. Mechanism (not proven): the uninstall withdraws the registry entry and metadata but not the handler registered under owner 'app:com.qa.addr-b' (the bare-call ambiguity check in packages/runtime/src/action-execution.ts reads the handler registry). QA-source: #RECORD · api-backend.actions-namespace-address · observed beside the clauses

    access-security.permission-set-address-in-execution-context · rev 3 · PASS · api · OWED: fix #32945

    env: showcase @0c2aeb96, port 41173, db file:/tmp/qa3/D/psa.db, --seed-admin --no-compile --no-watch, OS_HOME=/tmp/qa3/D/home-psa (ledger stayed there); the same scratch packages as actions-namespace-address (com.qa.addr-a: qa_a_ticket + qa_a_reader read-only on it; com.qa.addr-b: no permission set) installed with os package install. M = qa.m@example.com (admin create-user + change-password), granted via POST /data/sys_user_permission_set {user_id:M, permission_set:'qa_a_reader'} 201; one qa_a_ticket. Server stopped/restarted 4 times on the same file DB; ledger edits with better-sqlite3 while stopped.
    Tier 1 (0c2aeb9): @objectstack/plugin-security permission-set-address.test.ts 24✓ incl. "a holder of a grant the user does not hold switches nothing off for the one they do" ✓; @objectstack/core security/request-execution-context.test.ts 21✓; 0 failed (unit).

    clause verdict evidence (text, one line)
    acceptance[0] pass M me/permissions sets include qa_a_reader; GET /data/qa_a_ticket → 200, 1 row; explain contributor {kind:permission_set,name:qa_a_reader,via:"direct grant"}; grant row permission_set_package = com.qa.addr-a
    acceptance[1] pass stale row (permission, com.qa.addr-b, qa_a_reader, active 0) inserted, restart: qa_a_reader gone from M's sets, GET /data/qa_a_ticket → 403 PERMISSION_DENIED, explain {name:qa_a_reader,via:"held — deactivated",state:"deactivated"}; row deleted + restart: all step-1 answers back (sets include it, 200 1 row, "direct grant"); pin case ✓ (24✓)
    acceptance[2] pass POST /security/_activation/permission/qa_a_reader {enabled:false} → 200; ledger row {permission, qa_a_reader, package_id com.qa.addr-a, active false}; M loses qa_a_reader, data 403 PERMISSION_DENIED, explain "held — deactivated", state deactivated
    acceptance[3] pass row package_id set NULL while stopped, restart: answers as acceptance[2] (no qa_a_reader, 403, deactivated); {enabled:true} → 200, M lists qa_a_reader again and reads 1 row ("direct grant"). See notes: the cold-boot backfill rebound the NULL row to com.qa.addr-a before the read
    acceptance[4] pass (source reading, as the clause prescribes) packages/core/src/security/assemble-execution-context.ts:349 permissionSetAddresses: agent ? undefined : authz.permissionSetAddresses, with the comment "An agent principal's permissions are its scope-derived ceiling names, which carry no address and resolve by D3 with no caller" — OAuth agent arm carries none; the account arm carries authz.permissionSetAddresses. No pin tells the arms apart (FOLLOW-UPS §16b)
    negative[0] pass the stale com.qa.addr-b row switched M's set off (not ignored)
    negative[1] pass step 2's foreign row was deleted before step 3; the door wrote com.qa.addr-a's own row
    • fix-card qa(checklist): five assertion defects from the 18.4.0 acceptance (#32940): permission-set address, read_file budget, deploy confirm, two namespace refusals #32945 repro (item 1): pass — the stale-column row (package holding no set of the name) now reads as the D3 holder's switch per the re-pointed clause: M loses qa_a_reader, exactly as the card measured; the two-holder case is scored from the pin.

    • probe: 3 min · doors explain / me-permissions / data across restarts · classes: the row read back over GET /data/sys_metadata_activation after the NULL edit · findings: none

    • notes: checklist-accuracy — steps[3]'s package-less row does not survive the restart it needs: the cold-boot backfill (packages/objectql/src/activation-ledger-backfill.ts, ADR-0150 D8 R4) binds a package-less row to the name's one holder, so the DB row read com.qa.addr-a again after boot and acceptance[3] measures the backfilled row, not readDisabledCatalogNames' package-less arm. The answers still match the clause; a package-less live read would need a name two packages hold (not installable, knownGaps).

    • group notes: Tier 1 ran 13 vitest batches by package with npx vitest run FILES --maxWorkers=1 from each package dir (brief's rule; item text says --maxWorkers=2 via pnpm --filter), 22:28–22:33Z, all 55 pin files green, 0 failed. Every boot used --no-compile --no-watch in the shared tree except the interceptor scratch copy (own compile under /tmp/qa3/D/ws). Install-local items used their own OS_HOME; the tree ledger examples/app-showcase/.objectstack/installed-packages stayed absent throughout. No GitHub writes. No security finding.

  2. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

    QA run · owed:weekly clause tables (3/3) · 0c2aeb9 · runner group F (browser 5)

    approvals.record-page-decisions · rev 7 · PASS · browser · OWED: fix #33060

    env: showcase boot :41190, -d file:/tmp/qa3/F/approvals.db --seed-admin --no-compile, console dist 22:08Z (this tree). Projects created planned (budget 50000, spent 1000) then PATCHed: A/B/D → 200000, C → 600000. Member = qa-fixture.mjs contributor-member. No automated.ref (Tier 2 by hand).

    clause verdict evidence (text, one line)
    acceptance[0] pass screenshot: bar above page body; DOM order Approve(bg-primary) · Reject(destructive) · Reassign · Send back · Request info · Recall · Reply, none inside ‹header›; no Send reminder/Resubmit; header has only Edit + overflow. First load: POST /api/v1/data/showcase_project/A/page 200 (no GET ?object&recordId until refresh); own GET → row pending manager_review viewer {can_act:true,is_submitter:true,can_override:true}
    acceptance[1] pass click declared-action-approval_reject → 1 dialog, title "Reject", desc "Reject this request? A rejection is final for every approver.", 1 textarea + 1 file input; 0 POSTs before Confirm; after Confirm exactly one decision POST /api/v1/actions/sys_approval_request/approval_reject 200 body {"recordId":"areq_d688de54…","params":{"comment":"QA-F reject comment A"}}, 0 dialogs remain
    acceptance[2] pass reject: GET /approvals/requests/areq_d688… → status rejected; /actions lists {action:"reject",comment:"QA-F reject comment A"}; approve (B): one dialog "Approve"+declared desc → POST …/approval_approve 200 recordId=areq_326b… params.comment → re-read status approved, action row comment "QA-F approve B"; bar draws 0 declared actions after reject (refreshAfter GET ?object&recordId fired)
    acceptance[3] pass A (manager_review, lock_record:false): Edit enabled, 5 pencils (aria-label "Double-click to edit"), band "In approval · editable"; C after manager approve → new pending exec_review lock_record:true, on reload: Edit rendered+disabled, 0 pencils, band "Locked for approval" (screenshot amber lock band, faded Edit)
    acceptance[4] pass member: project read 200, approvals read {"data":[]}, page rendered (h1 "QA-F appr C"), 0 declared-action-*; forged POST /api/v1/approvals/requests/areq_497a…/approve ×2 → 404 {"code":"REQUEST_NOT_FOUND"}; admin re-read pending. Admin off-slate (D reassigned via bar picker to usr_showcase_auditor_demo, POST …/approval_reassign 200): row viewer {can_act:false,is_submitter:true,can_override:true}; status line "Pending · Waiting on Ada Auditor (demo)"; inline only Reply; declared-actions-override-menu aria-label "Admin override" → menuitems Override Approve/Override Reject/Override Reassign (data-override-decision=true) + Recall (plain); no Send back/Request info
    negative[0] pass single dialog, its Confirm posted the reject (no confirm→second-dialog chain)
    negative[1] pass locked C: 0 inline-edit pencils, Edit disabled
    negative[2] pass member (no viewer block / no row) offered 0 declared actions; D-admin offered only override-framed levers per its viewer block
    • previously-failed clause (acceptance[4] at QA run · touched:8e3d4a54 (25/52) · c4196099 · 2026-11-13 · 22 PASS / 1 PARTIAL / 2 FAIL / 0 BLOCKED / 27 NOT-RUN #33093, 403 expected vs 404 actual): now PASSES — the rev-7 clause expects 404 REQUEST_NOT_FOUND and the server answers exactly that (stale-spec re-point landed, product unchanged).
    • fix-card Re-point approvals.record-page-decisions acceptance[4]: a forged decision by a caller who cannot read the request answers 404 REQUEST_NOT_FOUND, not 403 FORBIDDEN #33060 repro: pass — contributor member who reads the project but not its pending exec_review request: POST /approvals/requests/:id/approve → 404 REQUEST_NOT_FOUND ×2, request still pending; also the action door (POST /api/v1/actions/sys_approval_request/approval_approve recordId=that id) → 404 REQUEST_NOT_FOUND. The card's "rest of the clause held" (empty read, no buttons, override folding) also re-held.
    • probe: 3 min · doors /approvals/requests/:id/approve, /actions/sys_approval_request/approval_approve, /approvals/requests/:id(/actions), /data/sys_approval_request · classes unknown id (404 REQUEST_NOT_FOUND), decided request (409 INVALID_STATE), empty recordId (400 VALIDATION_ERROR), member reads by id (404 both), member raw data read (200, 0 rows) · findings: none
    • notes: automation self-check — first Reassign attempt clicked the lookup option's centre (it hit the option's email link; no POST sent); keyboard Enter on the highlighted option selected it. Driving artifact, not a product defect.

    platform-core.docs-portal-render · rev 3 · PASS · browser · OWED: fix #33055

    env: showcase boot :41191, -d file:/tmp/qa3/F/docs.db --seed-admin --no-compile; console dist 22:08Z carries the fix (DocsSlug chunk Navigate to={pathname…,hash} present). Member = invite (role member) + invited sign-up, no position (member_default). No automated.ref (Tier 2 by hand).

    clause verdict evidence (text, one line)
    acceptance[0] pass screenshot: /_console/docs index renders 2 cards (Showcase Manual "8 articles", Setup com.objectstack.setup "1 article"); DocsLayout fired GET /meta/doc 200 + /meta/book 200 once; API (admin) = 1 book showcase_manual + 9 docs; rendered tree across both books = the same 9 docs (8 manual + setup_overview in the implicit per-package book) — empty diff both directions
    acceptance[1] pass /docs/manual/showcase_index body renders (h1 "Showcase"); clicking intra-corpus link "Data" (href /_console/docs/showcase_tour_data) → lands /_console/docs/manual/showcase_tour_data, h1 "Guided tour — Data" inside the portal
    acceptance[2] pass nav trace /_console/docs/setup_overview → /_console/docs/com.objectstack.setup/setup_overview (h1 "Setup overview"); /_console/docs/showcase_tour_security → /_console/docs/manual/showcase_tour_security; doc-load 200, client-side replace redirect, never a 404
    acceptance[3] pass /console/apps/com.example.showcase/docs (single book → AppDocsIndex lands on /apps/com.example.showcase/docs/manual/showcase_index, by design AppDocsIndex.tsx:48) lists only showcase* (8 docs, no setup_overview); /apps/com.objectstack.setup/docs lists only setup_overview; opened entry /apps/com.example.showcase/docs/manual/showcase_tour_ui → h1 "Guided tour — UI" in the app container
    acceptance[4] pass member /home screenshot → Help & Documentation button → menu "All documentation · Online documentation · About" → /_console/docs; member API = 1 book + 9 docs; member rendered tree = 9 docs, diff empty both ways; each of 9 entries opened: h1 rendered, 0 responses 401/403, GET /meta/doc/‹name› 200 each. Admin's Help menu offers the same entry
    acceptance[5] pass screenshots: index (2 cards), book landing/reader (sidebar + body), app-scoped (sidebar + body) all content; unknown doc renders a NAMED empty state "Documentation not found — No book or document named no_such_doc_qa is installed."
    negative[0] pass member rendered tree not wider than member API subset (equal sets)
    negative[1] pass 0 entries 401/403 on click (member session)
    negative[2] pass flat URLs redirect (2 of 2 tried), none 404
    negative[3] pass screenshot-first, settled (networkidle+1.5s) before every read; no empty-index read
    • fix-card Docs portal: a section cross-link through a flat permalink loses its #fragment on the redirect, so the doc opens at the top #33055 repro: pass — /_console/docs/manual/showcase_index → click "cross-reference section" (href /_console/docs/showcase_docs_guide#cross-references) → final URL /_console/docs/manual/showcase_docs_guide#cross-references (2/2 fresh loads). Scroll: at 1440×380 the #cross-references H2 sits at top 158 px vs 417 px on the same doc opened without the hash, so the page scrolled to the section (at 1440×900 the whole doc fits, nothing to scroll). Same holds in the app container (/apps/com.example.showcase/docs/… → …/manual/showcase_docs_guide#cross-references, top 158).
    • probe: 4 min · doors flat redirect (both container variants), DocPage, AppDocsIndex, Help menu · classes unknown fragment (#no-such-section: keeps hash, renders doc top, no error), book + unknown doc (named empty state), member opening an app-scoped docs index of an app they cannot open (/apps/com.objectstack.setup/docs → "You don't have access to this app", meta/app 403 — the app gate; the same doc stays readable via /docs/com.objectstack.setup/setup_overview, consistent with the docs API serving it) · findings: none
    • notes: AppDocsIndex skips its listing for a single-book package (redirect to the book) — step 4's "lists ONLY that package's docs" is read off the book sidebar there; checklist wording could name that.

    records-forms.home-amount-preview-states · rev 1 · PASS · browser · OWED: fix #32941

    env: showcase boot :41192, -d file:/tmp/qa3/F/homeamt.db --seed-admin --no-compile; console dist 22:08Z carries the fix (ui-components chunk: read-only CurrencyField branch mounts the preview when !disabled && currencyMode==='record' && homeAmount). Member = qa-fixture.mjs contributor-member (own invoice 2vxVLFm1nUCyRv1q). Rates (admin GET /data/sys_exchange_rate): EUR 0.92@2025-01-01, 0.9@2026-10-15; JPY 151.5@2025-01-01, 149.25@2026-10-15; home USD.
    Tier 1 pin (unit, objectui component layer — render over a stub data source, no HTTP): ui/ vitest run packages/fields/src/__tests__/homeAmountPreview-27196.test.tsx packages/plugin-form/src/__tests__/sectionFormHomeAmount-27196.test.tsx --maxWorkers=1 → 2 files, 18 tests passed. Below the dom oracle on the served bundle, so every clause was also driven live.

    clause verdict evidence (text, one line)
    acceptance[0] pass INV-1002 (USD draft, 198) Edit dialog: home-amount-preview data-state=same "≈ $198.00 (home currency)"; Currency→EUR: data-state=converted "≈ $220.00 at 0.9 on Nov 14, 2026" (198÷0.9=220.00, rate row EUR 0.9 eff. 2026-10-15 read via GET /data/sys_exchange_rate?…effective_date<=2026-11-14 → 200)
    acceptance[1] pass Invoice date cleared → data-state=awaitingDate "Set the document date to see the home amount"
    acceptance[2] pass Invoice date 06/01/2024 → data-state=awaitingRate "No EUR rate on or before Jun 1, 2024: saving is refused until one is added"; same refusal on save: PATCH invoice_date 2024-06-01 → 400 "…no exchange rate for EUR has an…" (date rendered locale-formatted, see notes)
    acceptance[3] pass member own invoice, Currency EUR + 03/15/2025 → data-state=refused "Converted to the home currency when you save", no digit in the line; network: GET /data/sys_exchange_rate?…currency=EUR… → 403 (×2); member direct rate read 403 PERMISSION_DENIED. Reproduced on 2 fresh contexts
    negative[0] pass preview "≈ $220.00" then Update → stored amount_home 220, amount_rate 0.9 (GET /data/showcase_invoice/:id); member line carries no number
    • fix-card Edit form shows no home-amount preview for a record-mode amount computed by an expression (showcase invoice): the read-only branch of CurrencyField skips it #32941 repro: pass — INV-1004 (EUR 450) → Edit: [data-testid=home-amount-preview] count 1, data-state=converted "≈ $500.00 at 0.9 on Nov 14, 2026" (card's actual was 0, plain €450.00); Currency→JPY + Invoice Date 01/15/2026: still mounted, converted "≈ $2.97 at 151.5 on Jan 15, 2026" (450÷151.5). 2/2 fresh loads.
    • probe: 3 min · doors PATCH /data/showcase_invoice (save-side conversion), sys_exchange_rate read · classes rate effective-date boundary (invoice_date = 2025-01-01, the first EUR rate's own date → 200, stored amount_home 215.22 at 0.92 = 198÷0.92), date before any rate (400 refused, matches awaitingRate), unreadable-rate member (403, refused state) · findings: none
    • notes: (1) fixture: the recipe's own invoice has currency_code null and no lines, so its record-mode amount (expression → null without a currency) is drawn as an EmptyValue and no preview can mount; I gave it currency USD (member PATCH 200) and one line (member POST showcase_invoice_line 2×50 → 201) so amount=100 before driving step 3. checklist-accuracy: steps[2]/provisioning should say the member's invoice needs a currency and a line. (2) acceptance[2].verify quotes "on or before 2024-06-01"; the served line formats the date for the locale ("Jun 1, 2024") — the clause's substance holds; the verify literal could name the formatted date.

    records-forms.related-list-server-pagination · rev 12 · PASS · browser · OWED: fix #33051

    env: showcase boot :41193, -d file:/tmp/qa3/F/related.db --seed-admin --no-compile, 1440×900. Northwind = PtaK1MoDuE2R1uNW. Personas: qa-fixture.mjs contributor-member (showcase_contributor) and … --position showcase_manager (contributor + manager; the child-read-entitled persona).
    Tier 1 pins (HTTP layer — @objectstack/verify bootStack: full REST stack through Hono request injection, no port): packages/qa/dogfood vitest run test/related-list-server-paging-and-gate.dogfood.test.ts test/record-page-related-slices-concurrent-read-scope.dogfood.test.ts --maxWorkers=1 → 2 files, 7 tests passed (A1/N1, A4, A5/N2; #33051 controls + order-independence).

    clause verdict evidence (text, one line)
    acceptance[0] pass enlarged Contacts (?related=showcase_contact.account): page 1 GET /api/v1/data/showcase_contact?…&top=25&skip=0&filter=["account","=","PtaK1MoDuE2R1uNW"] 200; page 2 …top=25&skip=25&filter=… 200; record page POST …/showcase_account/:id/page related[] = contact/field_zoo×2/invoice/project each top 3; invoice/project tables GET …top=10&skip=0&filter=…; 0 child GETs without a top bound (pin: HTTP $top/$skip paging)
    acceptance[1] pass pager "Page 1 of 2" → Next → "Page 2 of 2"; page-1 ids 25 + page-2 ids 1, overlap 0, union 26 = total 26 (response total 26); pin: $top=10 at skip 0/10/20 → 10/10/6, total 26
    acceptance[2] pass screenshot at 1440: Invoices and Projects body tables in the details column, Contacts card (26, 3 rows) in the related column — no page config
    acceptance[3] pass Invoices body table title "Invoices", headers Invoice Number · Status · Total · Issued On (+ row Actions) = declared relatedListColumns [name,status,total,issued_on]
    acceptance[4] pass click "Prospect 24" (a href …/showcase_contact/record/0VpHC9I7OLVJtbmH) in the enlarged list → record drawer "Prospect 24"; GET /api/v1/data/showcase_contact/0VpHC9I7OLVJtbmH 200 Prospect 24, prospect24@northwind.example, account PtaK1MoDuE2R1uNW (pin: HTTP re-read by id)
    acceptance[5] pass contributor: screenshot no Contacts card/strip entry, its /page related[] omits showcase_contact; forged GET /data/showcase_contact?$filter=[["account","=",NW]] → 403 PERMISSION_DENIED. manager: Contacts card renders (26, 3 rows) and the same query → 200 total 26, 26 rows (pin agrees at HTTP)
    negative[0] pass no child-list request without top on the detail page or the enlarged list
    negative[1] pass pager total 26 = direct filtered count 26
    negative[2] pass no Contacts section for contributor; server refuses the child query 403 with no rows
    • fix-card Record page related lists under-fill for a non-admin when the slice is not the last one in the page request's related[] #33051 repro: pass — as the manager member POST /api/v1/data/showcase_account/NW/page with [contact top 3, field_zoo f_lookup top 3] → contact slice total 26 / 3 records; swapped order → 3; contacts-only → 3; admin 3 in every order; 2 repetitions each. Console leg: the manager's Northwind page (its own /page lists Contacts first) draws the Contacts card "26" with 3 rows (Prospect 24 / 13 / 07), same as the admin — the card's actual was 1 row. Pin record-page-related-slices-concurrent-read-scope.dogfood.test.ts 4/4 at HTTP.
    • probe: 5 min · doors POST /data/showcase_account/:id/page related[], GET /data/showcase_contact · classes contributor forging a contact slice on /page (200 envelope, slice carries error PERMISSION_DENIED 403, no rows — gate holds), top 1000/100000 (400 VALIDATION_FAILED max_value), slice with no top (400), unknown relationship field (400 invalid_relationship), skip past end (200, 0 rows, total 26), top=0 (200, 0 rows) · findings: none
    • notes: checklist-accuracy — steps[1] says the column-tier page "draws no tab strip", but at 1440×900 the page draws a role=tab strip (Overview · Activity 0 · Contacts 26 · Field Zoo …) above the two columns; the clauses read the column bodies and are unaffected. The row-click opens the console's record drawer (URL stays on the account with ?related=…), which the clause's API re-read covers.

    records-forms.saved-view-management · rev 2 · PASS · browser · OWED: fix #33098

    env: showcase boot :41194, -d file:/tmp/qa3/F/savedview.db --seed-admin --no-compile; console dist 22:08Z carries the fix (index chunk: handleSetDefaultView for (let n of …) await Promise.all(…[n]); data-adapter chunk: updateView → ensureCustomization / "records no version"). Non-admin = invite (member) + invited sign-up (member_default, no position). No automated.ref (Tier 2 by hand).

    clause verdict evidence (text, one line)
    acceptance[0] pass switcher → New → kanban, groupByField Status, name "QA F Board" → PUT /api/v1/meta/view/showcase_task.qa_f_board?mode=draft 200 body {type:"kanban",kanban:{groupByField:"status",…}}; landed …/view/showcase_task.qa_f_board?preview=draft; plain GET before publish 404 RESOURCE_NOT_FOUND; draft-preview-publish → POST /api/v1/meta/view/showcase_task.qa_f_board/publish 200; reload without ?preview: no preview bar, plain GET 200 type kanban groupBy status, in GET /meta/view?object=showcase_task, board renders status lanes (BACKLOG …)
    acceptance[1] pass rename: PUT /meta/view/showcase_task.qa_f_board (no ?mode) 200 label "QA F Board Renamed" → reload row label holds; set-default: POST /packages/com.example.showcase/ensure-customization 201 → PUT /meta/view/showcase_task.default?package=com.example.showcase.custom 200 (isDefault:false) → PUT /meta/view/showcase_task.qa_f_board 200 (isDefault:true) → reload: one DEFAULT badge (qa_f_board), API isDefault = [qa_f_board] only, shipped view reads isDefault false + overrides {package:com.example.showcase,basedOn:0.1.0}; pin: PUT 200 → reload isPinned true; delete: confirm "Delete view" → DELETE …?state=draft 200 + DELETE /meta/view/showcase_task.qa_f_board 200 → reload row ABSENT, API gone. No error toasts; each write's only 404 is updateView's GET …?state=draft probe
    acceptance[2] pass member screenshot: Tasks list rendered (switcher "QA X · 10"); switcher menu has no list-view-switcher-new and no list-view-switcher-manage (testids present: search/all/rows/star/pin/save-mine); no create-view dialog. The row star/pin are per-user workspace preferences (useWorkspacePreference pinnedViewChoice), not meta writes
    acceptance[3] pass member forged PUT /meta/view/showcase_task.qa_member_forged → 403 FORBIDDEN "Saving a metadata item requires the manage_metadata capability."; PUT over existing qa_x → 403; PUT ?mode=draft → 403; DELETE qa_x → 403 "Resetting a metadata item requires…"; admin re-read: forged view 404 (published and draft), qa_x unchanged
    negative[0] pass every mutation survived reload (re-read via GET /meta/view and the Manage Views rows)
    negative[1] pass non-admin persisted nothing (4 forged writes 403, admin re-reads 404/unchanged)
    negative[2] pass after set-default exactly one list view flagged default (API and badges), twice (qa_f_board, then qa_x)
    • previously-failed clauses (acceptance[1] set-default half + negative[2] at QA run · touched:ee9ad834 (59/174) · 8e3d4a54 · 2026-11-13 · 51 PASS / 4 PARTIAL / 4 FAIL / 0 BLOCKED / 115 NOT-RUN #33096: two defaults when the old default was the package-shipped All Tasks): now PASS — the clear of the shipped default lands in the customization pair (?package=com.example.showcase.custom, 200), one default remains.
    • fix-card Set as Default on a list view leaves two default views when the previous default is a package-shipped view (its unset PUT is refused 403, the new default still lands) #33098 repro: pass — (a) console-created+published view: Set as Default over the shipped All Tasks → ensure-customization 201, shipped clear 200 into the pair, new default 200, no "Failed to save" toast, reload → DEFAULT on one view only; (b) the card's API path: All Tasks restored as default through Manage Views (PUT …default?package=…custom isDefault:true 200), then PUT /meta/view/showcase_task.qa_x {grid} 200 over the API, Manage Views → Set as Default on qa_x → shipped clear 200 (pair) then qa_x isDefault:true 200 → reload: badge + API isDefault = [qa_x] only; bare /showcase_task route opens "QA X".
    • probe: 3 min · doors PUT/DELETE /meta/view/:name (published + ?mode=draft), Manage Views default on a view already in the customization pair · classes non-admin on an existing name vs new name vs draft vs delete (all 403), re-defaulting the shipped view after it was customized (writes the pair again, single default) · findings: none. Observation: deleting the view that is the default leaves the object with zero default list views (shipped All Tasks stays isDefault:false in the pair); not asserted by any clause, not filed.
  3. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

    QA run · owed:weekly clause tables (2/3) · 0c2aeb9 · runner group E (cli+mixed 11)

    cli.deploy-runtime-dry-run-json · rev 3 · PASS · cli · OWED: fix #32945

    clause verdict evidence (text, one line)
    acceptance[0] pass log: os deploy --dry-run @1.0.0 exit 0, plan "Objects + qa_deploy_item", "Nothing in this plan asks for a confirmation: os deploy installs it as it is.", "✓ Dry run: nothing was installed into http://localhost:41175"; GET /api/v1/packages → 0 × "com.qa.deploy"
    acceptance[1] pass log: --dry-run --json exit 0; stdout JSON.parse OK, keys url,result,package,plan,environment,wouldStop,refusal,confirm,error,target = {target:"runtime",url:"http://localhost:41175",result:"planned",package:{manifestId:"com.qa.deploy",version:"1.0.0",installedVersion:null},wouldStop:[],refusal:null,confirm:null,error:null}; the 36 human lines all on stderr
    acceptance[2] pass log: os deploy </dev/null exit 0, "✓ Live: http://localhost:41175 now serves com.qa.deploy 1.0.0."; packages lists com.qa.deploy ×1; GET /api/v1/data/qa_deploy_item → 200 {records:[],total:0}; 2 POSTs with note → 201-shaped records
    acceptance[3] pass log: v1.1.0 (note→memo, no renamedFrom) dry run exit 0 "Version: 1.1.0 (installed: 1.0.0)", "⚠ Deploying this build would stop for your confirmation — 1 finding(s): • qa_deploy_item.note (text) is removed while qa_deploy_item.memo (text) is added, and no renamedFrom links them…"; deploy --json </dev/null exit 1, result "refused", refusal 409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED (verbatim message, 2 rows), confirm "os deploy --confirm-upgrade schema-upgrade:9996319b…"; no question on stderr (0 prompt lines)
    acceptance[4] pass test (cli unit, subject 0c2aeb9): packages/cli/test/deploy.test.ts 47 tests / 0 failed (vitest --maxWorkers=1)
    negative[0] pass dry run left packages without com.qa.deploy; --json stdout parsed as one document with no human line
    negative[1] pass off-TTY --json returned exit 1 in <5 s without waiting; fixture is the rename shape (not a bare drop)
    • fix-card qa(checklist): five assertion defects from the 18.4.0 acceptance (#32940): permission-set address, read_file budget, deploy confirm, two namespace refusals #32945 repro (item 3): pass — the rename upgrade stops with 409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED and the confirm command, as the card says; item rev 3 now plants the rename.
    • probe: 4 min · deploy --dry-run with the runtime host's install ledger file absent (DB row present) · boundary/opposite-posture · finding (low, not filed): with ‹app›/.objectstack/installed-packages/com.qa.deploy.json absent, the v1.1.0 dry run read "Version: 1.1.0 (not installed)", "Objects + qa_deploy_item", "Nothing in this plan asks for a confirmation" (exit 0), while the real deploy --json against the same runtime refused 409 for the rename (installedVersion "1.0.0" from the DB). The dry run reads the on-disk ledger (cloud-connection marketplace-install-local-plugin.ts answerUpgradeDryRun → this.ledger.read), the install gate reads the DB. Arose because I moved the cache file out of the shared tree (see notes); a host whose DB persists but whose .objectstack/ does not would see the same mismatch. Re-run with the file restored: plan correct.
    • notes: environment/fixture gap — os deploy to a showcase booted in the SHARED tree writes the runtime's install cache into /home/user/objectstack/examples/app-showcase/.objectstack/installed-packages/com.qa.deploy.json ("re-registers on every boot"), which would leak com.qa.deploy into every other runner's showcase boot. I moved it out after each install and removed the (empty, newly created) installed-packages dir; next round the item's fixture should boot the target runtime from a scratch copy. HOME isolated to /tmp/qa3/E/home for use-runtime + deploy (rev 3 fixture). Port 41175, db /tmp/qa3/E/deploy.db.

    cli.validate-v18-authoring-verdicts · rev 3 · PASS · cli · OWED: fix #33086 (item 3)

    clause verdict evidence (text, one line)
    acceptance[0] pass test (unit: spec/formula/lint, subject 0c2aeb9, vitest --maxWorkers=1): spec define-seed-record-keys 13/0 · formula validate-field-existence-spellings 31/0 · spec stack-duplicate-entry 138/0 · lint data-model-rules.master-detail-required 30/0 · spec stack-cbp-extension-master 13/0 · lint validate-component-props 121/0 · spec stack-email-sms 24/0 (tests/failed)
    acceptance[1] pass build (scratch showcase copy /tmp/qa3/E/ws/examples/app-v18, one arm at a time, reverted): seed created_atx → validate exit 1 "defineSeed('showcase_task'): unknown field(s) in records — created_atx … Did you mean 'created_at'?", build exit 2 · record["typo"] → exit 1 expression-invalid at objects[16].validations[0].condition "unknown field typo", build exit 1 "Author-time rules failed" · previous["typo"] → same rule/path, build 1 · two datasources alike → exit 1 "Datasource 'showcase_external' is declared twice in datasources: stack.datasources[0] and stack.datasources[1]", build 2 · cbp no master_detail → exit 1 at objects.24.sharingModel, build 2 · cbp two required inline → exit 1 at objects.24.fields "2 required master_detail fields (header, other)", build 2 · second required master via objectExtensions → exit 1 at objectExtensions.1.fields, build 2 · object-grid defaultFilters → exit 1 component-props-invalid (error) at pages[19].regions[1].components[2].properties.defaultFilters, build 1 · email {apiKey:'x'} → exit 1 "email: Unrecognized key(s) on the stack email block: apiKey" (prescribes OS_EMAIL_API_KEY), build 2; no arm printed "Validation passed"
    acceptance[2] pass build: seed created_at (pristine) validate 0 / build 0 · record.name == "record.typo" 0/0 · email {provider:'smtp'} 0/0 · cbp whose only master_detail is required:false 0/0
    acceptance[3] pass build (scaffolded blank app /tmp/qa3/E/ws/examples/app-raw, defineStack({...}, { strict: false }), cbp child whose only master_detail omits required): os lint exit 1 "✗ master_detail "qarl_child.header" → qarl_parent declares no required: write true … or false (an optional master)" relationship/master-detail-required at objects[1].fields.header.required; os validate --json exit 0 valid:true; os build exit 0
    • fix-card checklist: re-point three api clauses that lag the product at 8e3d4a54 (QA 18.4.0 R2 part B, B-api slice) #33086 repro (item 3, the cbp arm): pass — the failed clause (acceptance[1], cbp arm) now PASSES: rev 3 plants no-master and two-required (inline + objectExtensions), each refused by validate and build; the optional-master shape that rev 2 expected refused is now a passing control (acceptance[2]), and the lint-only shape is acceptance[3].
    • probe: 3 min · os lint / validate / build on the omitted-required cbp body in three wrappers · opposite posture · no product finding: default (strict) defineStack → lint 0 / validate 0 / build 0 (the parse writes required:false before lint sees it, as spec/data/controlled-by-parent-master.ts documents); bare object export → lint 1 (same finding) but validate/build exit 1/2 "the default export was not built by defineStack"; only { strict: false } gives the clause's three exits.
    • notes: checklist-accuracy — steps[2] says "a plain object literal (not through ObjectSchema.create())" but does not say how the literal reaches os lint unparsed: inside the showcase's (strict) defineStack config the lint finding never appears (lint exit 0), and a bare export is refused by validate/build. Name defineStack(x, { strict: false }) in steps[2]. Also: arm record["typo"] here replaced the project rule's null guards, so validate reported a second expression-invalid (null <) beside the typo finding — fixture artifact of the edit, not a verdict. HOME/OS_HOME=/tmp/qa3/E/home.

    platform-core.bare-name-one-holder-refusal · rev 5 · PASS · mixed (api+log) · OWED: fix #33091, #33050, #33049

    clause verdict evidence (text, one line)
    acceptance[0] pass api (HTTP, scratch showcase copy :41176): complement of NAMESPACE_SCOPED_BARE_NAME_TYPES in BARE_NAME_ONE_HOLDER_TYPES re-read from packages/core dist = [connector]; os package install qa_a → exit 0 installed; qa_b → exit 1 "Install failed (422 PLUGIN_REGISTER_FAILED): … package "com.qa.addr-b" declares the connector "qa_conn", already held by package "com.qa.addr-a" — rename it to "qa_b_qa_conn" …"; GET /api/v1/packages lists com.qa.addr-a ×1, com.qa.addr-b ×0
    acceptance[1] pass api: qa_b rebuilt without qa_conn → install ✓ "Package installed into the running kernel"; packages list addr-a ×1 + addr-b ×1; package-less GET /meta/webhook/qa_hook, /meta/flow/qa_sync, /meta/agent/qa_helper each → 409 RESOURCE_CONFLICT naming both packages; ?package=com.qa.addr-b flow → 200 "QA Sync (qa_b)"
    acceptance[2] pass api: qa_a (holding email_template auth.password_reset) installed exit 0 with no refusal; GET /meta/email_template/auth.password_reset?package=com.qa.addr-a → 200 subject "QA reset {{url}}" _packageId com.qa.addr-a
    acceptance[3] pass log (scratch app /tmp/qa3/E/ws/examples/app-cold, defineStack packages[] = com.qa.addr-c + com.qa.addr-d each with connector qa_conn, :41177): boot health 200; exactly 1 × "WARN Bare-name conflict at boot: … package "com.qa.addr-d" declares the connector "qa_conn", already held by package "com.qa.addr-c" — rename it to "qa_d_qa_conn" … The boot continues and changes nothing about which holder each caller is served"
    negative[0] pass second install answered 422, not 200
    negative[1] pass cold boot with the clash completed (health 200)
    negative[2] pass rebuilt qa_b 200 beside qa_a's flow qa_sync read as per-namespace (bare address 409 naming both), not scored a defect

    platform-core.namespace-object-name-refusal · rev 3 · PASS · mixed (build+api) · OWED: fix #32945

    clause verdict evidence (text, one line)
    acceptance[0] pass build (scratch /tmp/qa3/E/ws/pkgs/qa_k_compose, defineStack packages[] = com.qa.k {object qa_k_case} + com.qa.k-case {ns qa_k_case}): os validate exit 1, objectstack build exit 2, both "✗ Namespace 'qa_k_case' of package 'com.qa.k-case' equals the object 'qa_k_case' of package 'com.qa.k' … (ADR-0150 D6). Rename the namespace or the object."; each directory alone builds exit 0. Pins (subject 0c2aeb9): cli namespace-object-names 7/0 · spec stack-namespace-object-name 7/0
    acceptance[1] pass api (HTTP, scratch showcase copy :41178 with qa_k installed): os package install qa_k_case → exit 1 "Install failed (422 PLUGIN_REGISTER_FAILED): Failed to register imported manifest: Namespace 'qa_k_case' of package 'com.qa.k-case' equals the object 'qa_k_case' of package 'com.qa.k' …"; GET /api/v1/packages lists com.qa.k only (0 × com.qa.k-case). Pin objectql registry-namespace-object-name 7/0 (engine)
    acceptance[2] pass api: POST /api/v1/packages com.qa.w → 201; install-local com.qa.w-desk (ns qa_w_desk) 200; PUT /meta/object/qa_w_desk?package=com.qa.w → 422 NAMESPACE_CONFLICT "The object 'qa_w_desk' of package 'com.qa.w' equals the installed namespace 'qa_w_desk' of package 'com.qa.w-desk' …"; same PUT &mode=draft → 200 state draft; POST …/qa_w_desk/publish?package=com.qa.w → 422 NAMESPACE_CONFLICT (same sentence); POST /api/v1/packages/com.qa.w/publish-drafts → 200 {success:false,outcome:"refused",publishedCount:0,failed:[{type:"object",name:"qa_w_desk",code:"NAMESPACE_CONFLICT",error:‹same sentence›}]}; GET ?state=draft → 200, published read → 404. Pins metadata-protocol protocol.namespace-object-name-doors 7/0 · runtime package-door-namespace-object-name 5/0
    acceptance[3] pass build: scratch qa_k_custom (ns qa_k, object qa_k_custom) os validate exit 1 "The object 'qa_k_custom' of package 'com.qa.k-custom' is short-named 'custom'. The paired customization package of namespace 'qa_k' takes the namespace 'qa_k_custom' (ADR-0146) …"
    negative[0] pass the clash was refused at install (422), never accepted
    negative[1] pass every refusal names both spellings (namespace + object, with both packages); publish-drafts' 200 carried outcome refused + failed[] and the draft stayed a draft
    • fix-card qa(checklist): five assertion defects from the 18.4.0 acceptance (#32940): permission-set address, read_file budget, deploy confirm, two namespace refusals #32945 repro (item 5): pass — POST /api/v1/packages/com.qa.w/publish → 200 {success:false,itemsPublished:0,validationErrors:[{message:"No metadata items found for package 'com.qa.w'"}]}; the drafts door POST …/publish-drafts → 200 outcome refused, failed[0].code NAMESPACE_CONFLICT, draft kept; the clause now names that route (and clause 0 composes both packages).
    • probe: 3 min · POST /api/v1/packages + PUT /meta/object · boundary/opposite posture · none: object named its own namespace qa_w → 422 NAMESPACE_PREFIX (prefix rule fires first); object showcase → 422 NAMESPACE_PREFIX; package create with namespace qa_k_case (equals installed object) → 422 NAMESPACE_CONFLICT naming both; PUT with empty name → 405 METHOD_NOT_ALLOWED.
    • notes: scratch showcase copy (/tmp/qa3/E/ws/examples/app-v18, own compile), db /tmp/qa3/E/nsobj.db; its install cache from the previous item was moved aside before this boot.

    approvals.ooo-delegation-reroute · rev 8 · FAIL · mixed (api) · OWED: fix #33062

    clause verdict evidence (text, one line)
    acceptance[0] pass api (HTTP, stock showcase :41179, runtime flow qa_ooo_flow via POST /api/v1/automation, start showcase_field_zoo record-after-create, approvers [{type:user,value:A}]): baseline request areq_f9bedf23… pending_approvers ["D706j8Qz…" = A]; /actions = [submit] only, no ooo_substitute
    acceptance[1] pass api: POST /data/sys_approval_delegation as A {delegate_id:usr_showcase_auditor_demo, now-1h…now+24h, 'Annual leave'} → stamped delegator_id A; next request areq_72d5f793… pending_approvers ["usr_showcase_auditor_demo"]; /actions has exactly one ooo_substitute row with no actor_id key (system) and comment "D706j8Qz… → usr_showcase_auditor_demo — Annual leave"
    acceptance[2] pass api: GET /api/v1/notifications with B's own bearer → {type:"approval.ooo_substituted", title:"Approval routed to you (out-of-office cover)", actionUrl:"/system/approvals?request=areq_72d5f793…"}
    acceptance[3] pass api: POST /approvals/requests/areq_72d5f793…/approve with Ada's own bearer (sign-in auditor.demo@example.com / showcase123) → status approved, pending []; approve row actor_id "usr_showcase_auditor_demo" (never A)
    acceptance[4] pass api: PATCH delegation valid_until → now-5min (200); next request areq_787692a6… pending_approvers [A], /actions [submit] only, no ooo_substitute
    negative[0] pass approve row attributed to B, not A
    negative[1] pass expired window routed back to A at resolution
    negative[2] pass runtime flow with approvers [{type:position,value:showcase_manager}] (A holds it) while an open-ended A→B delegation is active → pending_approvers [A], no ooo_substitute
    negative[3] pass admin POST with delegator_id=Ada → 403 FORBIDDEN "you may only manage out-of-office delegations where you are the delegator ('D706j8Qz…')"; Ada (bearer) POST with delegator_id=A → 403 PERMISSION_DENIED
    • fix-card Approval delegations accept an inverted window (valid_until before valid_from) and a self-delegation, both 201 and silently inert #33062 repro: pass — POST with valid_from later than valid_until → 400 VALIDATION_FAILED fields[0].field "valid_until" "Valid Until must be later than Valid From…"; POST with delegate = the caller → 400 VALIDATION_FAILED fields[0].field "delegate_id" "Delegate must be someone other than the Delegator…". Pin plugin-approvals sys-approval-delegation-inert-shapes.integration 12/0.
    • probe: 6 min · sys_approval_delegation write + approval resolution · boundary / opposite posture / readers · window validation holds (valid_from == valid_until → 400; inverted window or self on PATCH → 400 VALIDATION_FAILED; open-ended → 201). Off-clause: approvals.ooo-delegation-reroute · off-clause probe · security finding — detail withheld, reported to PM.
    • notes: B = Ada Auditor provisioned at boot (no setup needed); runtime flows deleted after use. db /tmp/qa3/E/ooo.db.

    records-forms.multi-currency-invoice · rev 2 · PASS · mixed (api+browser) · OWED: fix #32941

    clause verdict evidence (text, one line)
    (fixture) ok GET /api/settings/localization → currency.value "USD"; rates EUR 0.92@2025-01-01, 0.9@2026-10-15; JPY 151.5@2025-01-01, 149.25@2026-10-15 (manual)
    acceptance[0] pass api (stock showcase :41180): INV-1004 EUR 2026-11-14 450 → rate 0.9, home 500 (450÷0.9) · INV-1009 EUR 2026-08-06 2016 → 0.92, 2191.3 · INV-1010 EUR 2026-08-31 276 → 0.92, 300 · INV-1008 JPY 2026-11-09 237864 → 149.25, 1593.73 · INV-1007 JPY 2026-09-15 95400 → 151.5, 629.7 — every rate the latest row on/before the date, every home = amount ÷ rate, 2 dp
    acceptance[1] pass api: all 7 USD seed invoices amount_rate 1, amount_home = amount (e.g. INV-1001 1749.56/1749.56, INV-1003 323.94/323.94)
    acceptance[2] pass api: POST showcase_invoice {name QA-NOCUR, account, status draft} → 201; read-back currency_code null, amount null, amount_home null, amount_rate null
    acceptance[3] pass api: INV-1002 before {USD, 2026-11-14, 198, 198, 1}; PATCH currency_code EUR → 200; after {EUR, 198, home 220, rate 0.9} (198÷0.9)
    acceptance[4] pass api: POST {currency_code JPY, invoice_date 2024-06-01} → 400 ERR_EXCHANGE_RATE_MISSING "Insert on 'showcase_invoice' was REFUSED: 'amount' is in JPY … no exchange rate for JPY has an effective date on or before 2024-06-01. The row was not written…"; list by name QA-JPY-OLD → total 0
    acceptance[5] pass screenshot then dom (Playwright, form sign-in, own context; served /_console bundle built 22:08Z carries the #32941 change — ui-components-*.js holds return!c.disabled&&l?.currencyConfig?.currencyMode==='record'&&l.currencyConfig.homeAmount?…space-y-1…[t,VI(...)]): INV-1004 detail shows "$500.00 / Converted at 0.9 on Nov 14, 2026"; Edit dialog: 1 × [data-testid=home-amount-preview] data-state "converted" "≈ $500.00 at 0.9 on Nov 14, 2026"; Invoice Date → 01/15/2026: "≈ $489.13 at 0.92 on Jan 15, 2026"; Currency → JPY: "≈ $2.97 at 151.5 on Jan 15, 2026"; nothing saved (server read after: EUR 450/500/0.9)
    negative[0] pass no foreign invoice at rate 1 or a later rate, no multiplication; currency-less accepted with blanks; missing-rate write stored no row
    • prior FAIL: acceptance[5] (no preview on the read-only expression-computed amount) now PASSES: the preview mounts and follows date and currency before save, reproduced on two fresh browser contexts (mc2.js, mc3.js).
    • fix-card Edit form shows no home-amount preview for a record-mode amount computed by an expression (showcase invoice): the read-only branch of CurrencyField skips it #32941 repro: pass — INV-1004 Edit shows 1 preview, data-state converted; after Currency JPY + Invoice Date 2026-01-15 it re-reads at 151.5 on Jan 15, 2026. Pins (ui jsdom): fields homeAmountPreview-27196 10/0 · plugin-form sectionFormHomeAmount-27196 8/0.
    • probe: 4 min · showcase_invoice write door · boundary values · none: invoice dated exactly the 2026-10-15 effective date → 0.9 (inclusive); 2026-10-14 → 0.92; PATCH an existing EUR invoice's date to 2024-12-31 → 400 ERR_EXCHANGE_RATE_MISSING, row unchanged; PATCH currency_code null → amount/home/rate all null; currency_code "XXX" → 400 VALIDATION_FAILED value_domain.
    • notes: screenshots at deviceScaleFactor 1 per the BRIEF (clause names 2); screenshots used only for judgment. db /tmp/qa3/E/multicur.db.

    studio-authoring.dataset-member-vocabulary · rev 2 · PASS · mixed (api+network+dom) · OWED: fix #32943

    clause verdict evidence (text, one line)
    acceptance[0] pass api (stock showcase :41181): PUT synonyms ['bill count','Bill_Count'] → 422 INVALID_METADATA "measures.0.synonyms.1 [custom]" "synonym "Bill_Count" … repeats synonyms[0] "bill count" once case, width, spacing, _ and - are folded". Pin spec dataset-member-vocabulary 18/0 (parse layer)
    acceptance[1] pass api: ['status'] → 422 at measures.0.synonyms.0 "…is also the name of dimension "status" in this dataset…"
    acceptance[2] pass api: [' bill'] → 422 measures.0.synonyms.0 [invalid_format] "…no leading or trailing whitespace… The schema refuses…"; 61-char synonym → 422 [too_big] "a synonym is at most 60 characters…"; 501-char description → 422 measures.0.description "…is 501 characters; … at most 500 per locale"
    acceptance[3] pass api: GET after each of the 5 refusals → 404; valid PUT → 200 "Saved dataset 'qa_vocab_metrics' (env-wide, state=active)"; GET → synonyms ["bill count","账单数"], description "Count of invoices, every status."
    acceptance[4] pass network (MCP HTTP POST /api/v1/mcp, osk_ key, initialize → tools/call list_datasets {terms:['Bill-Count','账单数']}): qa_vocab_metrics matches [{term:"Bill-Count",member:"measures.invoice_count",matchedOn:"synonym"},{term:"账单数",member:"measures.invoice_count",matchedOn:"synonym"}]; invoice_count lists synonyms + description. Pin service-analytics dataset-term-matcher 5/0
    acceptance[5] pass screenshot then dom (/_console/studio/~all/metadata/dataset/qa_vocab_metrics, measure row's "Advanced" opened): input[data-testid=dataset-measures-synonyms] value "bill count, 账单数"; [data-testid=dataset-measures-description] value "Count of invoices, every status."; help "…Matched by the dataset AI tools, never shown. At most 20, each at most 60 characters; a word may answer to only one member of this dataset". Pin ui DatasetDefaultInspector 24/0
    negative[0] pass no refused body stored (404 after each); ' bill' refused, not trimmed
    negative[1] pass page text before and after opening Advanced contains 0 × "bill count" outside the synonyms input; chart/table label reads "Invoices"
    • fix-card Studio dataset designer offers no Synonyms or Description on a measure or dimension row: DatasetDefaultInspector replaces the dataset form #32943 repro: pass — the measure row now offers Synonyms and Description (DatasetMemberVocabulary in the inspector's Advanced fold) holding the API-stored values, with the limits help text; the served /_console bundle (built 22:08Z from 0c2aeb9, which contains 83bd36d) is what was driven. Setup-app route of the card not re-driven.
    • probe: 3 min · PUT /meta/dataset · boundary values · none: 60-char synonym 200; 20 synonyms 200, 21 → 422 measures.0.synonyms [too_big]; "" → 422 [invalid_format]; a dimension synonym "Totals" equal to the measure synonym "totals" → 422 (cross-member, folded); fullwidth "bill" vs "bill" → 422 (width folded).
    • notes: db /tmp/qa3/E/vocab.db; the dataset is env-wide in the runtime DB only.

    studio-authoring.expression-editors · rev 10 · PASS · mixed (screenshot+dom+api+test) · OWED: fix #33100

    clause verdict evidence (text, one line)
    acceptance[0] pass screenshot then dom (stock showcase :41182, authored package com.qa.x ns qax, object qax_job; Studio Data → Form → "Line total" → data-field-inspector, FORMULA (CEL)): record.est_hours * 1.1 → "Valid CEL · Result type: Number"; record.a + record.b → "Result type: Unknown — the engine cannot prove a single type. Wrap operands in double() / int() / string() to pin it…"
    acceptance[1] pass api: f_sibling and f_total published with returnType "text"; after editing f_sibling in the inspector (autosave PUT …/meta/object/qax_job?mode=draft&package=com.qa.x 200), GET ?state=draft → f_sibling {expression "record.est_hours * 1.1", returnType "number"}, un-edited f_total {returnType "text"} unchanged
    acceptance[2] pass api+dom: engine (MCP validate_expression site formula/validation on qax_job) — record.est_hourz → ok:false "unknown field est_hourz on qax_job — did you mean est_hours?"; bare est_hours → ok:false "bare reference est_hours …"; editor shows the same two messages, textarea aria-invalid=true, no autosave PUT while they stand. RLS USING quantity * 2 > unit_price → editor error "RLS using is not lowerable (unsupported operand "*"), so the policy is dropped and grants no access"; engine: the same predicate PUT as draft then POST /meta/permission/qax_rls/publish → 422 INVALID_METADATA rule rls-predicate-unenforceable (publish-drafts outcome refused)
    acceptance[3] pass screenshot then dom (Notes field VISIBLE WHEN editor): record. → listbox [name,status,quantity,unit_price,est_hours,a,b,f_sibling] (8 = MAX); record.est → [est_hours]; previous. → field catalog; previous.s → [status], Enter inserts "previous.status"; current_user. → no listbox; parent. → no listbox; bare est_hours > 1 → aria-invalid, "bare reference est_hours …"
    acceptance[4] pass screenshot then dom/network (Studio Access → QA RLS → Row-level security, package door): non-pushdownable USING → error + perm-autosave-held "Fix the expression errors before saving.", 0 PUT; parse fault owner_id == ( → "Unexpected token: EOF" error, held, 0 PUT; formula-field probe f_sibling > 5 → "Valid CEL" before save, first save PUT 200, re-read shows cel-server-error "RLS using (INVALID_FIELD / 400): … 'f_sibling', a virtual formula field on 'qax_job'" (draft _diagnostics code rls-predicate-unenforceable); edit beside the guard (policy name) → held, 0 PUT; new guard text f_sibling > 6 → 1 PUT 200; POST /meta/permission/qax_rls/publish → 422 INVALID_METADATA [rls-predicate-unenforceable]; live set still owner_id == current_user.id
    acceptance[5] pass screenshot then dom (Test → CelTestRunDialog, USING owner_id == current_user.id, current_user.id "user-1"): owner_id "user-1" → lucide-shield-check "Allowed"; "user-2" → lucide-shield-x "Denied"; predicate owner_id → "Non-boolean result "user-1" — a row filter should evaluate to true or false."; direct engine ExpressionEngine.evaluate (packages/formula dist, same ctx shape) → true / false / "user-1"
    acceptance[6] pass per-probe parity above: formula Number/unknown/error/error and validation-site errors equal validate_expression (MCP); RLS error tiers equal the publish gate; test-run equals engine evaluate. Engine side not re-proved here
    acceptance[7] pass test (ui jsdom, subject 0c2aeb9, cd ui && npx vitest run --maxWorkers=1): celAuthoring.test.ts 40/0 (incl. 'graceful degradation'); CelPredicateField 24/0 · CelTestRunDialog 6/0 · PermissionAdvancedFacets.cel 10/0
    negative[0] pass no probe where editor and engine tiers differed
    negative[1] pass formula parse fault record.est_hours * 1.1 * ( → aria-invalid, "Unexpected token: EOF", 0 PUT (draft read 404); RLS parse fault 0 PUT; formula-field guard: no PUT for the edit beside, publish 422
    negative[2] pass current_user. / parent. offered no qax_job fields; bare refs flagged
    • fix-card validate_expression and the object publish gate accept a formula that reads another formula field, and that formula reads null on every row #33100 repro: pass — card's x_job shape (here qax_job, namespace rule) published; row {quantity 2, unit_price 5} reads f_sibling 10, f_total 20 (API and the Studio Records grid "10 | 20"); self-reference f_self = record.f_self * 2 → draft _diagnostics valid:false "formula 'f_self' reads its own value through the formulas it reads ('f_self' reads 'f_self')…", publish-drafts refused [expression-invalid]; a 2-cycle f1↔f2 likewise ("'f1' reads 'f2', 'f2' reads 'f1'"); validate_expression record.f_sibling * 2 → ok, number, now consistent with runtime. Pins objectql engine-formula-dependency-order 11/0 · lint runtime-gate.object-formula-cycle 5/0 · formula formula-read-order 5/0.
    • probe: (folded into the validate_expression and the object publish gate accept a formula that reads another formula field, and that formula reads null on every row #33100 cycle legs above) 4 min · object save/publish door · boundary · none beyond the card.
    • notes: checklist-accuracy — steps[1] offers record.quantity * record.unit_price as a proven-number formula, but both the engine (validate_expression inferredType "unknown") and the editor ("Result type: Unknown") read it unknown; only record.est_hours * 1.1 proves Number. The item's predicates use record.FORMULA_FIELD; the RLS site here binds bare field names (showcase policies are written owner == current_user.email). Package created with namespace qax (the card's x is refused: namespace 2–20 chars). db /tmp/qa3/E/expr.db.

    studio-authoring.flow-status-matches-runtime · rev 7 · PASS · mixed (api+dom) · OWED: fix #33054

    clause verdict evidence (text, one line)
    acceptance[0] pass api+dom, two boots. Default boot (:41183, shared tree): _status qafs_on_create {enabled:false,bound:false} ↔ rail "Switched off" (title "Switched off — will not run"), header "Status: Switched off"; qafs_manual {enabled:true,bound:false, no trigger} ↔ "Switched on" ("Enabled · no trigger (run manually)"); qafs_sched {enabled:true,bound:false,unboundCause:deployment_policy} ↔ "Not running here"; after enable+publish qafs_on_create {true,true} ↔ rail/header "Switched on" ("Enabled · bound to its trigger"). Scheduled-work boot (:41184, OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1, fresh DB, package re-authored): qafs_sched {enabled:true,bound:true} ↔ rail/header "Switched on"; on_create "Switched off"; manual "Switched on". No row disagrees
    acceptance[1] pass screenshot then dom (default boot): schedule flow rail title and header pill title = "This deployment keeps scheduled work that comes with a package switched off. Whoever operates this deployment can switch it on." (worded deployment_policy); Problems [data-testid=flow-run-status-note] "Not running here
    acceptance[2] pass api: header switch on qafs_on_create → draft PUT, toast "Switched on — publish to apply"; publish-drafts published; POST /data/qafs_item rec-1 → GET /automation/qafs_on_create/runs 1 run status completed trigger record-after-create; switch off → toast "Switched off — publish to apply", publish, _status {enabled:false,bound:false}; POST rec-2 → still 1 run
    acceptance[3] pass api: scheduled-work boot, qafs_sched (interval 60000 ms) _status bound:true, Studio "Switched on"; first run completed at 23:29:47Z, trigger {type:"schedule"} (~60 s after publish)
    negative[0] pass the unbound schedule flow's dot/pill read "Not running here" (gray, not green "Switched on"), and Problems counts it as a note, not a problem

    studio-authoring.no-entry-property-matrix · rev 9 · PARTIAL · mixed (api) · OWED: fix #33101

    clause verdict evidence (text, one line)
    acceptance[0] partial api (stock showcase :41185, authored package com.qa.m ns qam, one fresh object/flow per row; draft PUTs 200, publish-drafts published 11): live reads carry each key as written — r0 managedBy "append-only" · r1 ownership "org" · r2 systemFields {audit:false} · r6 activityMilestones [{field:stage,value:won,summary:"QA milestone: deal won"}] · r7 displayNameField "code" · r12 num.format "GD-{0000}" · r13 must.storage {notNull:true} · r22 secret.internal true · r26c parent.deleteBehavior "restrict" · f27 description/successMessage/errorMessage. Draft byte-equal reads were not captured before the publish (publish followed directly). 11 rows of 37 driven
    acceptance[1] partial api, same 11 rows: managedBy append-only → POST 403 PERMISSION_DENIED, DELETE 403 · ownership org → 201 row with no owner_id · systemFields audit:false → no created_by/updated_by (organization_id kept) · activityMilestones → PATCH stage won writes sys_activity summary "QA milestone: deal won" · displayNameField code → sys_activity record_label "CODE-777" (not the name "Deal seven") · autonumber → GD-0001, GD-0002, GD-0003 · storage.notNull → insert without it 400 VALIDATION_FAILED, with it 201 · internal → key absent from the read, also with ?select=name,secret and ?fields=name,secret · deleteBehavior restrict → DELETE parent 409 DELETE_RESTRICTED · flow successMessage → manual trigger 200 carries "QA success message". The other 26 rows not reached (knownGaps[3]: one runner's budget does not reach the matrix)
    acceptance[2] partial api: none of the 11 driven rows stored-and-inert; the #33101 shapes (a key naming no field) are now refused at publish (below). Rows not driven: not scored
    negative[0] pass no driven row was stored, served and ignored
    • fix-card Publish accepts an activityMilestones[].field or displayNameField that names no field; both are served and do nothing #33101 repro: pass — PUT draft with activityMilestones[{field:'nope'}] → 200 but draft _diagnostics valid:false "activityMilestones[0].field "nope" is not a field on object "qam_probe1". Did you mean "name"?" (object-field-ref-unknown); publish-drafts → outcome refused INVALID_METADATA. Same for displayNameField:'nope' ("displayNameField "nope" is not a field …", refused) and nameField:'nope' (refused). Pin lint validate-object-field-refs 103/0.
    • probe: 2 min · object publish door · boundary values · none beyond the card (nameField twin also refused).
    • notes: rows not reached this round: 3–5, 8–11, 14–21, 23–25, 28–36 (Studio-control census of steps[3] not taken for the driven rows). The split of the matrix owed by knownGaps[3] is still owed. db /tmp/qa3/E/matrix.db.

    automation.package-flow-override · rev 4 · PASS · mixed (network+api+dom) · OWED: fix #33052

    clause verdict evidence (text, one line)
    acceptance[0] pass network (Playwright, scratch showcase copy :41186, fresh DB): Studio automations → Project Budget Approval → studio-customize press sends nothing (0 writes); label edit "Manager Review (QA)" → POST /api/v1/packages/com.example.showcase/ensure-customization 201, then PUT /api/v1/meta/flow/showcase_budget_approval?mode=draft&package=com.example.showcase.custom 200 with body name showcase_budget_approval, overrides {package:"com.example.showcase",basedOn:"0.1.0"}; no /toggle POST, no activation write (the only two writes)
    acceptance[1] pass api: after publish-drafts of the pair (published, no restart) _status lists showcase_budget_approval once {enabled:true,bound:true}; package-less GET /meta/flow/showcase_budget_approval serves the override (overrides block, nodes without needs_exec/exec_review, "Manager Review (QA)"); next run (budget 130000) step log [start, manager_review] → completed with no needs_exec (the override's topology). Pin service-automation flow-sync-one-precedence 16/0 (engine)
    acceptance[2] pass api: run_1ab02e9d… started on the base before the customization (paused at manager_review); after the override was published, approving its request completed it with steps [start, manager_review, needs_exec, exec_review(skipped)] — the base's topology (the override removed needs_exec), so the waiting run finished on the base. The override was a topology change (manager_review→approved, needs_exec/exec_review removed, written to the pair draft via PUT …?mode=draft&package=com.example.showcase.custom) per knownGaps[2]
    acceptance[3] pass api+dom: header draws one switch (role=switch count 1, auto-ledger-switch); off → POST /automation/showcase_budget_approval/toggle {enabled:false} 200, _status enabled:false, budget 140000 starts no run (runs stayed 2); on → toggle {enabled:true} 200, budget 150000 starts run_f509d0f1… on the override; sys_metadata_activation row {type flow, name showcase_budget_approval, package_id com.example.showcase} (the base's)
    acceptance[4] pass api+dom: chip popover studio-override-restore → studio-restore-original-dialog "Restore ObjectStack Showcase's “Project Budget Approval”? …" → confirm: DELETE …/meta/flow/showcase_budget_approval?state=draft&package=com.example.showcase.custom 200 and DELETE …?package=com.example.showcase.custom 200; GET /meta/flow/showcase_budget_approval → base, no overrides key, needs_exec back; next run (160000) steps [start, manager_review, needs_exec, exec_review]
    acceptance[5] pass network: New similar (auto-item-more → menuitem → auto-new-similar-dialog) on the base → POST /api/v1/automation/showcase_budget_approval/clone {name:"qa_budget_copy",label:"QA Budget copy"} (no package) 200, flow status "obsolete"; copy opens at ?surface=flow:qa_budget_copy listed "In com.example.showcase.custom"; GET /meta/flow/qa_budget_copy?package=com.example.showcase.custom → _packageId com.example.showcase.custom, obsolete, no overrides; base stays armed (_status enabled/bound true). Pin runtime automation-flow-clone-custom-package 39/0
    acceptance[6] pass network+api: customized and published again, New similar on the live override → clone 200 {name qa_budget_copy2}, dialog closed, copy opened in the pair; GET /meta/flow/qa_budget_copy2?package=com.example.showcase.custom → manager_review label "Manager Review (QA)" (the override's), status obsolete, no overrides key; GET /meta/flow/showcase_budget_approval still serves the override with its overrides block; then restored (two DELETEs 200)
    negative[0] pass each budget change produced exactly one run (runs 1→2→2(off)→3→4)
    negative[1] pass override armed by publish with no restart; restore re-armed the base at once
    • fix-card Studio Automations: a flow copy opened by "New similar" reads "Status: Unpublished draft" although it is stored live, until the page is reloaded #33052 repro: pass — both New similar copies opened with header "Status: Switched off" immediately (no reload); GET /meta/flow/qa_budget_copy?state=draft → 404 (stored live), _status row {enabled:false,bound:false}. Pins ui AutomationsPillar.newSimilar-28567 11/0 · customizeFlow-28570 14/0 (jsdom).
    • probe: 3 min · ensure-customization twice, clone on a live override · opposite posture · none: the second ensure answered 200 (pair exists) where the first was 201; the copy of a live override carries the override's definition without its overrides block.
    • notes: checklist-accuracy — steps[3]/[7] say the override chip is folded into auto-item-more (studio-item-header-folded-chips); at a 1500 px viewport the chip (studio-override-chip "Overrides ObjectStack Showcase") sits in the header itself and the folded-chips container is absent; the fold applies at narrower widths. The topology edit was written over the metadata door, the label edit through Studio. Scratch showcase copy used (customization pair is DB-only, but the copy keeps the shared tree untouched). db /tmp/qa3/E/pfo.db.
  4. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

    Close-out 2026-11-14T23:54Z.

    All 28 named fix cards' own repros pass at 0c2aeb9; every clause that failed last round passes. The one FAIL row is the withheld security row, handed to the security lane privately.

    Extracted: #33535 · api-backend.package-rest-lifecycle · observed beside the clauses
    Extracted: #33536 · api-backend.actions-namespace-address · observed beside the clauses
    Extracted: #33537 · cli.deploy-runtime-dry-run-json · observed beside the clauses
    Parked-on: #33353 · studio-authoring.no-entry-property-matrix · rows 3–5, 8–11, 14–21, 23–25, 28–36 not driven; matrix split (knownGaps[3])
    

    Checklist-accuracy notes and fixture gaps went to #33353 (comment 6886186625).

  5. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    qa#3 · os-tesla · session_01L9qibzj9DrVWXkq7joYHGB

    Closed 2026-11-14T23:56Z: the owed retests at 0c2aeb9 are judged (28 fix repros pass); close-out in comment 6886197946.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions