diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 298f8136d1a..2036d0ec68f 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -3114,6 +3114,672 @@ "ref": "#23169" } ] + }, + { + "id": "access-security.anonymous-floor-v18-doors", + "title": "The anonymous floor answers 401 FIRST on the doors v18 added to it — /notifications (was 501), non-POST /keys (was 405), share-link management (was 501), /openapi.json and /docs, every /i18n face and the settings family — while the token faces and the public-form submit stay anonymous and the submit echoes only {id}", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "anonymous caller (no cookie, no bearer, no x-api-key)", + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the showcase's stock public form `contact-us` (anon GET /api/v1/forms/contact-us resolves it; fields name/email/company/message)" + ], + "knownGaps": [ + "the dispatcher `/ui` domain (also floored by 23225) is NOT mounted over HonoServer on a stock `os dev` / `os serve` — GET /api/v1/ui/... answers 404 ENDPOINT_NOT_FOUND to the admin too — so it is not driven here; it is reachable only on a dispatcher-only (hosted) kernel and is pinned by the dogfood sweep", + "the dispatcher mounts only POST /keys on HonoServer (packages/runtime/src/dispatcher-plugin.ts), so a non-POST /keys never reaches the domain's floor on a stock boot: the HonoServer fallback answers it 405 METHOD_NOT_ALLOWED (packages/plugins/plugin-hono-server/src/adapter.ts unmatchedResponse) — run 2026-10-16 measured exactly that; the clause stays as the 23225 contract states it, so a 405 there is a FAIL, not a re-point" + ] + }, + "steps": [ + "with NO credentials call: GET /api/v1/notifications, GET /api/v1/keys, GET /api/v1/share-links, GET /api/v1/openapi.json, GET /api/v1/docs, GET /api/v1/i18n/locales, GET /api/settings/branding", + "with no credentials call a token face: GET /api/v1/share-links/BOGUS/resolve", + "with no credentials POST /api/v1/forms/contact-us/submit with the four whitelisted fields plus a forged owner_id and a non-whitelisted status", + "repeat two of the 401 calls as admin to prove the door itself serves" + ], + "acceptance": [ + { + "clause": "the anonymous floor is the first statement on every listed v18 door and the form submit answers {id} only — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "every listed door answers the anonymous caller 401 (UNAUTHENTICATED / AUTH_REQUIRED in that family's envelope) — never the 501/405/400 it answered before the floor", + "oracle": "api", + "verify": "status + error.code per door, anonymous", + "evidence": "one status+code line per door" + }, + { + "clause": "the share-link token face is NOT behind the floor (it answers its own invalid-token refusal, not 401)", + "oracle": "api", + "verify": "status + code on the bogus token resolve", + "evidence": "the response line" + }, + { + "clause": "an anonymous public-form submit answers 201 with a body that is exactly {id} — no stored, default or hook-stamped field echoed", + "oracle": "api", + "verify": "keys of the response body", + "evidence": "the response body" + } + ], + "negative": [ + "an anonymous 200 from any listed door is a FAIL (and an authorization finding — RUNNER rule 2 carve-out applies to its reproduction)" + ], + "traps": [ + "auth-state-leak", + "dispatcher-vs-hono-route" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts + packages/rest/src/rest-api-description-anonymous-deny.test.ts + packages/runtime/src/domains/i18n-anonymous-deny.test.ts + packages/services/service-settings/src/settings-routes.anonymous-floor.pin.test.ts + packages/rest/src/public-form-submit-answer.test.ts" + }, + "source": [ + "packages/core/src/security/anonymous-deny.ts#shouldDenyAnonymous", + "packages/rest/src/discovery-routes.ts#registerOpenApiEndpoints", + "packages/services/service-settings/src/settings-routes.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23225-domain-floor-anonymous-deny, 23225-ui-data-domain-floor, 22430-api-description-anonymous-deny, 22432-i18n-anonymous-deny, 23142-settings-anonymous-floor, 22437-public-form-submit-answers-id" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; extends access-security.anonymous-deny-surfaces (v15), which enumerates data/meta/actions/automation/batch/security/analytics only", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.walled-environment-metadata-operator-only", + "title": "Under a walled posture (group / isolated) environment metadata and the security catalog belong to the platform operator: an org owner holding manage_metadata is refused 403 on every /meta write verb, every /packages write, install-local of a catalog-carrying package, _migrate-stored and POST /automation — and the single posture is unchanged", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P0", + "surface": "api", + "personas": [ + "org owner holding manage_metadata who is NOT platform admin — walled-posture legs only", + "platform operator (holds the platform_admin position) — only for the walled-posture legs", + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", + "on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position" + ] + }, + "steps": [ + "walled boot, as the org owner: PUT /api/v1/meta/object/qa_walled_probe (a minimal object body); PUT /api/v1/meta/permission/showcase_member_access (any body); POST /api/v1/meta/view/SOME_VIEW/publish; DELETE /api/v1/packages/com.example.showcase", + "walled boot, as the org owner: POST /api/v1/security/permission-sets/SOME_SET/clone", + "walled boot, as the operator: repeat the object PUT and confirm it is admitted (then delete the probe)", + "single-posture boot, as admin: the same object PUT is admitted (control)" + ], + "acceptance": [ + { + "clause": "every environment-metadata and catalog write door refuses a non-operator under a walled posture — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "each org-owner write on the walled boot answers 403 (FORBIDDEN on /meta, PERMISSION_DENIED on the dispatcher /packages and /security doors) and stores nothing (GET of the item afterwards is unchanged/404)", + "oracle": "api", + "verify": "status + error.code per call + a read-back", + "evidence": "one line per call + the read-backs" + }, + { + "clause": "the operator's identical write is admitted on the walled boot, and the single-posture admin write is admitted", + "oracle": "api", + "verify": "status 2xx and read-back present", + "evidence": "both write answers" + } + ], + "negative": [ + "an org owner on a walled boot succeeding at ANY environment-metadata write is a P0 FAIL (cross-tenant blast radius) — authorization carve-out: publish no reproduction" + ], + "traps": [ + "wrong-persona", + "auth-state-leak" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/rest/src/meta-migrate-stored-walled-refusal.test.ts + packages/runtime/src/domains/packages-walled-write-refusal.test.ts + packages/runtime/src/domains/automation-walled-definition-write.test.ts + packages/runtime/src/domains/meta-walled-catalog-refusal.test.ts + packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts + packages/qa/dogfood/test/wall-catalog-refusal.dogfood.test.ts + packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts" + }, + "source": [ + "packages/metadata-core/src/meta-write-capability.ts#walledCatalogWriteVerdict", + "ADR-0131", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23231-walled-environment-metadata-writes, 15204-walled-catalog-refusal, 15204-walled-catalog-package-refusal, 15204-walled-catalog-package-verbs" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.meta-authoring-trail-and-audience-gates", + "title": "/meta read gates added on v18: the authoring-trail doors (history / audit / diff / package commits) refuse a setup.access-only caller, and a list view or dashboard carrying requiredPermissions is withheld from a caller without them on EVERY alternate door", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a scratch list view (PUT /api/v1/meta/view/showcase_task.qa_gated_view — view is overlay-allowed on stock showcase; a view name is dotted OBJECT.NAME) with requiredPermissions naming a capability the member lacks", + "a member granted setup.access but not studio.access / manage_metadata (a scratch permission set assigned through sys_user_permission_set)" + ] + }, + "steps": [ + "as the setup.access-only member: GET /api/v1/meta/object/showcase_task/history, /audit, /diff; GET /api/v1/packages/com.example.showcase/commits", + "as a member without the capability: GET /api/v1/meta/view (list) and GET /api/v1/meta/view/showcase_task.qa_gated_view, plus /published, /layers, ?state=draft, /history", + "as admin: the same reads (control); then delete the scratch view" + ], + "acceptance": [ + { + "clause": "trail doors demand authoring access and the view/dashboard audience gate holds on every alternate door — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the setup.access-only caller gets 403 FORBIDDEN on history/audit/diff and 403 PERMISSION_DENIED on /packages/:id/commits", + "oracle": "api", + "verify": "status + error.code per door", + "evidence": "four status lines" + }, + { + "clause": "the gated view is absent from the member's list and every by-name door answers 403 PERMISSION_DENIED; the admin reads it on all of them", + "oracle": "api", + "verify": "list membership + status per door, both personas", + "evidence": "the list ids + per-door lines" + } + ], + "negative": [ + "a 304 Not Modified replaying the admin's body to the member (stale ETag) is a FAIL of the gate" + ], + "traps": [ + "wrong-persona", + "cache-staleness" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/rest/src/meta-history-diff-authoring-door.test.ts + packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts + packages/rest/src/meta-view-dashboard-audience-gate.test.ts" + }, + "source": [ + "packages/rest/src/meta-item-read-gate.ts#holdsRequiredPermissions", + "packages/runtime/src/domains/packages.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23546-trail-doors-setup-access, 22611-list-view-dashboard-required-permissions, 22639-lint-capability-references-list-views, 22639-rest-meta-read-gate-audience, 22639-runtime-meta-legacy-object-exit, 22639-spec-audience-gate-enforced" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.security-catalog-activation-and-clone-doors", + "title": "Security-catalog doors new on v18: POST /security/_activation/:type/:name switches a position or permission set off and on (holders lose and regain its grants), and POST /security/permission-sets/:name/clone copies a set with its refusal arms", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)", + "anonymous caller (no cookie, no bearer, no x-api-key)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase position the member holds (GET /api/v1/data/sys_user_position as admin)" + ] + }, + "steps": [ + "as admin POST /api/v1/security/_activation/position/POSITION {enabled:false}; as the member re-read GET /api/v1/auth/me/permissions (or a record the position grants)", + "POST the same with {enabled:true}; re-read", + "POST /api/v1/security/_activation/position/everyone {enabled:false}; and for an unknown name", + "POST /api/v1/security/permission-sets/SOURCE/clone {name:\"qa_clone_set\", label:\"QA clone\"}; repeat with the same name; with the source's own name; with a bad body; as the member; anonymously" + ], + "acceptance": [ + { + "clause": "the activation door and the clone door with every refusal arm — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "disabling a position answers 200, writes a sys_metadata_activation row and removes the holder's grants from it; re-enabling restores them", + "oracle": "api", + "verify": "activation answer + the member's permissions before/after each switch", + "evidence": "the three permission reads" + }, + { + "clause": "everyone / guest → 400; an unknown name → 404", + "oracle": "api", + "verify": "status + code", + "evidence": "refusal bodies" + }, + { + "clause": "clone → 200 {permissionSet, notice} carrying the facets but not isDefault / adminScope / packageId; name taken (including the source's) → 409; bad body → 400; member without manage_metadata → 403; anonymous → 401", + "oracle": "api", + "verify": "status + code per arm and the cloned set's keys", + "evidence": "one line per arm + the clone body" + } + ], + "traps": [ + "wrong-persona" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/runtime/src/domains/catalog-activation-door.test.ts + packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts + packages/runtime/src/domains/permission-set-clone.test.ts + packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts" + }, + "source": [ + "packages/runtime/src/domains/catalog-activation.ts#handleCatalogActivationWrite", + "packages/runtime/src/domains/permission-set-clone.ts#handlePermissionSetCloneWrite", + "packages/runtime/src/domains/permission-set-clone.ts#PERMISSION_SET_CLONE_DROPPED_KEYS", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 15204-s2c-catalog-activation-door, 15204-catalog-activation-environment-authored, 15204-s2b-plugin-sharing-ledger-deactivation, 15204-permission-set-clone-door" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.security-catalog-one-holder-and-boot-report", + "title": "The security catalog has exactly one holder per name: two packages declaring one permission-set / position name refuse boot (422 NAMESPACE_CONFLICT on install-local), a body packageId that disagrees with its package is refused, `os migrate security-catalog-overlays` clears environment shadows, and boot reports grants naming undefined catalog entries", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a second scratch package declaring a permission set with the same name as a showcase set (for the install-local leg)" + ] + }, + "steps": [ + "POST /api/v1/marketplace/install-local with the conflicting package; GET /api/v1/packages afterwards", + "PUT /api/v1/meta/permission/SET?package=com.example.showcase with a body whose packageId names another package", + "in the scratch app run `os migrate security-catalog-overlays` (preview), then `--apply --yes --json`", + "plant a sys_user_permission_set row naming an undefined set (scratch DB), reboot, read the [security] boot line" + ], + "acceptance": [ + { + "clause": "one-holder refusal, owner binding, the overlay migration and the catalog-reference report — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "install-local of a package redeclaring a catalog name → 422 NAMESPACE_CONFLICT (or PLUGIN_REGISTER_FAILED) with conflicts[], and nothing is recorded as installed", + "oracle": "api", + "verify": "status + code + conflicts; package list unchanged", + "evidence": "refusal body + the list" + }, + { + "clause": "a permission-set save whose body packageId disagrees with the target package → 400 VALIDATION_ERROR at packageId", + "oracle": "api", + "verify": "status + error located path", + "evidence": "refusal body" + }, + { + "clause": "`os migrate security-catalog-overlays` previews and exits 1 when rows exist; --apply --yes deletes and exits 0; --json reports listed/deleted/rows", + "oracle": "build", + "verify": "exit codes + JSON", + "evidence": "both CLI outputs" + }, + { + "clause": "a grant naming an undefined set is listed in the [security] error line at boot (per org, ≤50 names); a clean boot logs one info line", + "oracle": "log", + "verify": "boot log grep for [security]", + "evidence": "the log lines" + } + ], + "traps": [ + "destructive-in-place" + ], + "automated": { + "kind": "unit", + "ref": "packages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts + packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts + packages/cli/src/commands/migrate/security-catalog-overlays.integration.test.ts + packages/plugins/plugin-security/src/catalog-reference-report.test.ts + packages/spec/src/security/permission-set-owner-binding.test.ts + packages/objectql/src/permission-set-owner-registration.test.ts" + }, + "source": [ + "packages/objectql/src/registry.ts#SecurityCatalogNameConflictError", + "packages/cli/src/commands/migrate/security-catalog-overlays.ts", + "packages/plugins/plugin-security/src/catalog-reference-report.ts#reportCatalogReferences", + "packages/spec/src/security/permission-set-owner-binding.ts#judgePermissionSetOwnerBinding", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22135-security-catalog-one-holder, 22307-cold-boot-catalog-refusal, 22371-security-catalog-overlays-step, 15204-s3-boot-report, 23259-permission-set-owner-mismatch-refused, 23312-permission-set-owner-binding-build-and-registration" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.grant-table-write-contract", + "title": "Grant-table write contract (sys_user_permission_set / sys_user_position): the retired permission_set_id column is refused, a duplicate grant is 409, an inverted validity window is 400, granted_by is stamped with the caller, and (walled) a grant to a non-member is refused", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the member's user id and a showcase permission-set NAME" + ] + }, + "steps": [ + "as admin POST /api/v1/data/sys_user_permission_set {user_id: MEMBER, permission_set: NAME} (201), then the same body again", + "POST with permission_set_id instead of permission_set", + "POST with valid_until earlier than valid_from", + "POST with granted_by naming the member; read the stored row's granted_by", + "walled boot only: as an org admin grant in org A to a user who is not a member of A" + ], + "acceptance": [ + { + "clause": "the grant-table write contract — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "permission_set_id → 400 INVALID_FIELD; a second identical grant → 409 UNIQUE_VIOLATION", + "oracle": "api", + "verify": "status + code + located field per arm", + "evidence": "two refusal bodies" + }, + { + "clause": "an INSERT with valid_until <= valid_from is refused as the rule's VIOLATION: 400 VALIDATION_FAILED at valid_until with constraint.rule validity_window_order, constraint.reason NOT `unevaluable`, and the rule's authored message (\"Valid Until must be later than Valid From …\") — an unevaluable-predicate refusal is a FAIL even though it also answers 400", + "oracle": "api", + "verify": "fields[0].constraint + message on the inverted-window insert; a valid window inserts 201 (control)", + "evidence": "the refusal body + the control" + }, + { + "clause": "a non-system insert naming someone else as granted_by stores the CALLER", + "oracle": "api", + "verify": "read-back of granted_by", + "evidence": "the stored row" + }, + { + "clause": "walled: a grant to a non-member → 400 VALIDATION_FAILED with fields.user_id reference_not_found", + "oracle": "api", + "verify": "status + field code", + "evidence": "refusal body" + } + ], + "automated": { + "kind": "dogfood", + "ref": "packages/plugins/plugin-security/src/grant-permission-set-name.test.ts + packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts + packages/qa/dogfood/test/permission-set-assignment-window.dogfood.test.ts + packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts + packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts" + }, + "source": [ + "packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts", + "packages/plugins/plugin-security/src/grant-holder-membership-refusal.ts#registerGrantHolderMembershipRefusal", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23430-grant-permission-set-id-retired, 9272-grant-window-order-refused, 9272-user-page-grant-window-columns, 22201-granted-by-provenance, 22226-grant-holder-must-be-organization-member" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.parent-and-organization-write-guards", + "title": "Write guards tightened on v18: a walled update cannot null out organization_id, a detail write naming an unreadable header is reference_not_found, a validation rule reading an unreadable header answers like a missing one, and a controlled_by_parent child's attachments / comments / explain follow the master's edit grant", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase controlled_by_parent detail object with a master the member can read but not edit (or a scratch pair in the scratch app)", + "for the organization leg: the walled boot" + ] + }, + "steps": [ + "walled: PATCH a record with {organization_id: null}", + "as the member POST a detail row naming a header id the member cannot read, and one naming a non-existent id", + "as the member POST /api/v1/data/sys_attachment on the child; DELETE another user's file / comment on it", + "as the member POST /api/v1/security/explain for an update on the child; compare with the PATCH answer" + ], + "acceptance": [ + { + "clause": "organization-removal refusal, unreadable-header parity and the cbp attach/comment/explain gates — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "walled: an update leaving organization_id null → 403 PERMISSION_DENIED; an insert with an empty org is still stamped", + "oracle": "api", + "verify": "status + code; read-back", + "evidence": "refusal + stamped row" + }, + { + "clause": "an unreadable header and a non-existent header answer the SAME 400 reference_not_found (no existence oracle)", + "oracle": "api", + "verify": "compare the two bodies field by field", + "evidence": "both bodies" + }, + { + "clause": "on a cbp child whose master the member cannot edit: attach → 403 ATTACHMENT_PARENT_ACCESS, deleting another's file → 403 ATTACHMENT_DELETE_DENIED, and explain reports not-writable decided by sharing — matching the PATCH", + "oracle": "api", + "verify": "status + code per call; explain verdict vs PATCH status", + "evidence": "per-call lines" + } + ], + "negative": [ + "an unreadable header answering differently from a missing one is an existence-oracle FAIL — authorization carve-out applies" + ], + "traps": [ + "wrong-persona" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts + packages/objectql/src/engine-unreadable-master-detail-header.test.ts + packages/objectql/src/engine-predicate-related-master-detail-header.test.ts + packages/qa/dogfood/test/cbp-parent-attachment-comment-gates.dogfood.test.ts + packages/qa/dogfood/test/cbp-explain-master-write.dogfood.test.ts + packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts" + }, + "source": [ + "packages/plugins/plugin-security/src/security-plugin.ts#SecurityPlugin", + "packages/services/service-storage/src/attachment-access-hooks.ts", + "packages/plugins/plugin-security/src/explain-engine.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22278-tenant-wall-judges-emptied-organization, 22519-unreadable-header-write, 22892-predicate-related-master-detail, 22455-cbp-parent-gates-judge-master, 22455-security-serves-controlled-by-parent-write, 22464-security-service-controlled-by-parent-write, 22514-explain-cbp-update-master-check, 23093-cbp-middle-master-floor" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.deployment-tables-platform-only", + "title": "Seven deployment tables (sys_job, sys_job_run, sys_job_queue, sys_flow_dispatch, sys_migration, sys_migration_journal, sys_presence) carry no organization column on v18 and are readable only with manage_platform_settings — an org admin who is not platform admin is refused even on single", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a member promoted to org admin (sys_member role admin) who is not platform admin" + ] + }, + "steps": [ + "as the org admin GET /api/v1/data/sys_job and /api/v1/data/sys_migration", + "as the platform admin repeat", + "as the platform admin GET /api/v1/data/sys_job?filter=[[\"organization_id\",\"=\",\"x\"]]" + ], + "acceptance": [ + { + "clause": "the seven tables are platform-level and gated by manage_platform_settings — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the org admin gets 403 PERMISSION_DENIED on each; the platform admin gets 200", + "oracle": "api", + "verify": "status per persona per table", + "evidence": "status lines" + }, + { + "clause": "organization_id is an unknown field on these tables (400 INVALID_FIELD on a filter)", + "oracle": "api", + "verify": "status + code", + "evidence": "refusal body" + } + ], + "traps": [ + "wrong-persona" + ], + "automated": { + "kind": "unit", + "ref": "packages/platform-objects/src/deployment-level-no-tenant-column.test.ts + packages/plugins/plugin-security/src/authz-identity-objects-boot-refusal.test.ts" + }, + "source": [ + "packages/platform-objects/src/audit/sys-job.object.ts", + "packages/services/service-realtime/src/objects/sys-presence.object.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 15207-deployment-plumbing-no-organization-column" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "access-security.share-link-combined-gates", + "title": "Share links with two gates combined: signed_in + password answers an anonymous caller 401 SIGN_IN_REQUIRED whatever password is sent, email + password answers a uniform 404 INVALID_OR_EXPIRED, and /messages answers exactly like /resolve", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "anonymous caller (no cookie, no bearer, no x-api-key)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "two share links minted by admin on a showcase record: (a) audience signed_in + password, (b) audience email + password" + ] + }, + "steps": [ + "anonymously GET /api/v1/share-links/TOKEN_A/resolve with no password, a wrong one, the right one (UTF-8 header form)", + "anonymously resolve TOKEN_B with no password and with the right password but no matching email session", + "repeat each call on /messages" + ], + "acceptance": [ + { + "clause": "combined share-link gates and resolve/messages parity — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "link A answers anonymous 401 SIGN_IN_REQUIRED for every password variant (the password is never evaluated first)", + "oracle": "api", + "verify": "status + code per variant", + "evidence": "three lines" + }, + { + "clause": "link B answers the same 404 INVALID_OR_EXPIRED body for every non-entitled variant", + "oracle": "api", + "verify": "byte-compare the bodies", + "evidence": "the bodies" + }, + { + "clause": "/messages answers identically to /resolve on every call", + "oracle": "api", + "verify": "status+code pairs", + "evidence": "the paired lines" + } + ], + "negative": [ + "a password-correct anonymous caller reading link A is an authorization FAIL — carve-out applies" + ], + "automated": { + "kind": "conformance", + "ref": "packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts + packages/plugins/plugin-sharing/src/share-link-envelope.conformance.test.ts" + }, + "source": [ + "packages/plugins/plugin-sharing/src/share-link-routes.ts#registerShareLinkRoutes", + "packages/runtime/src/domains/share-links.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23328-share-link-refusal-order, 23372-share-link-email-audience-uniform-refusal, 23371-share-link-messages-audience-inputs" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } \ No newline at end of file diff --git a/docs/qa/platform-checklist/areas/ai.json b/docs/qa/platform-checklist/areas/ai.json index 987fec62d5a..ca7976c1c6b 100644 --- a/docs/qa/platform-checklist/areas/ai.json +++ b/docs/qa/platform-checklist/areas/ai.json @@ -792,6 +792,74 @@ "ref": "#22073" } ] + }, + { + "id": "ai.mcp-coded-refusals-and-runtime-exposure", + "title": "MCP on v18: a coded refusal on the data tools, list_actions and the record resource renders {error:{code,message,status}} (not the bare message), both MCP doors honour a runtime-authored object's apiEnabled:false including after a restart, and run_action withholds PERMISSION_DENIED details", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "MCP enabled on the boot (ai.mcp-http-surface names the switch)", + "a runtime-authored scratch object (PUT /api/v1/meta/object on a scratch boot) with apiEnabled:false" + ] + }, + "steps": [ + "call the MCP get_record tool for a missing id; call query_records on the apiEnabled:false object", + "restart the boot (same file DB) and repeat", + "call run_action on an action the caller may not run" + ], + "acceptance": [ + { + "clause": "coded MCP refusals and runtime-object exposure — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "get_record for a missing id renders {error:{code:\"RECORD_NOT_FOUND\",status:404}}", + "oracle": "api", + "verify": "tool result body", + "evidence": "the excerpt" + }, + { + "clause": "query_records on the runtime-authored apiEnabled:false object is refused before and after restart, and a deleted object leaves list_objects", + "oracle": "api", + "verify": "tool results across the restart", + "evidence": "the excerpts" + }, + { + "clause": "run_action PERMISSION_DENIED carries no internal details", + "oracle": "api", + "verify": "tool result keys", + "evidence": "the excerpt" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/mcp/src/plugin.record-resource-exposure.test.ts + packages/mcp/src/mcp-action-refusal-details.test.ts + packages/objectql/src/plugin-runtime-object-metadata-copy.test.ts" + }, + "source": [ + "packages/mcp/src/mcp-http-tools.ts", + "packages/mcp/src/stdio-data-bridge.ts#enforceApiExposure", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23509-mcp-coded-refusal, 23661-runtime-object-metadata-copy, 23689-env-kernel-runtime-object-copy, 23529-mcp-action-tools-withhold-permission-details" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 0e6edd5a502..550f8b941c6 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -2504,6 +2504,320 @@ "ref": "#14961" } ] + }, + { + "id": "api-backend.second-object-exposure-and-internal-fields", + "title": "Every read that reaches a NON-addressed object asks that object's own `enable` block (nested-relation filter → 404/405, $expand withheld to the bare id, search skips it), and an `internal: true` field is refused 400 INVALID_FIELD in every evaluate position (filter, sort, groupBy, aggregate, $expand where/orderBy)", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "in the scratch app: an object T with apiEnabled:false (and a second with apiMethods:[\"get\"]), a lookup from a showcase-shaped object S into T, and an internal:true field on S; boot the scratch app" + ] + }, + "steps": [ + "GET /api/v1/data/S?filter=[[\"t_ref.name\",\"=\",\"x\"]] (nested-relation filter into T)", + "GET /api/v1/data/S/ID?$expand=t_ref", + "GET /api/v1/search?q=TERM and confirm T never appears", + "GET /api/v1/data/S?filter=[[\"internal_f\",\"=\",\"x\"]], ?sort=internal_f, POST /api/v1/data/S/query with groupBy / aggregate on internal_f", + "repeat as admin AND member — the answers must be identical (exposure is not a permission)" + ], + "acceptance": [ + { + "clause": "second-object exposure and internal-field refusal at the protocol and engine — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "a nested-relation filter into an apiEnabled:false target → 404 OBJECT_API_DISABLED naming where.t_ref (405 OBJECT_API_METHOD_NOT_ALLOWED for a whitelist without the verb), for admin and member alike", + "oracle": "api", + "verify": "status + code + located path, both personas", + "evidence": "the four lines" + }, + { + "clause": "$expand into the unexposed target returns the bare id, not the record", + "oracle": "api", + "verify": "shape of t_ref in the response", + "evidence": "the response excerpt" + }, + { + "clause": "every evaluate position on the internal field → 400 INVALID_FIELD naming object+field (never 500, never a filtered answer that leaks its value)", + "oracle": "api", + "verify": "status + code per position", + "evidence": "one line per position" + } + ], + "negative": [ + "a 200 whose row count differs between filter-on-internal and no-filter is an oracle leak — FAIL (silent-coercion)" + ], + "traps": [ + "silent-coercion", + "wrong-persona" + ], + "automated": { + "kind": "unit", + "ref": "packages/metadata-protocol/src/protocol.relation-condition-target-exposure.test.ts + packages/metadata-protocol/src/protocol.expand-target-exposure.test.ts + packages/metadata-protocol/src/protocol.data-door-internal-field-positions.test.ts + packages/objectql/src/internal-field-positions.test.ts" + }, + "source": [ + "packages/spec/src/data/api-derivation.ts#canServeApiOperation", + "packages/spec/src/data/api-derivation.ts#apiExposureDenialReason", + "packages/metadata-protocol/src/protocol.ts#assertNoInternalFieldEvaluated", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22640-search-api-exposure, 22661-second-object-exposure, 22737-relation-condition-target-exposure, 22737-spec-ledger-metadata-protocol-exposure-codes, 22737-types-exposure-arm-names-target, 22777-core-relation-condition-sites, 22777-mcp-stdio-relation-condition-target-exposure, 22823-calldata-fallback-relation-exposure, 22823-spec-ledger-runtime-exposure-codes, 22738-lookup-title-exposure, 22739-import-reference-target-exposure, 22785-import-user-field-target, 22800-identity-import-exposure, 22646-internal-field-data-door-positions" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "api-backend.data-request-shape-and-query-arity", + "title": "Request-shape gates on v18: a /data write with a null / array / scalar body or an empty version token is refused 400 (never an unguarded 200), a repeated single-valued query name is refused 400 on the dispatcher doors (a doubled keepData no longer drops tables), and a stale If-Match on an unreadable row answers 404", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "one showcase_task row id and its current version token" + ] + }, + "steps": [ + "PATCH /api/v1/data/showcase_task/ID with body `null`, then `[]`, then `5`; re-read updated_at", + "PATCH with {\"expectedVersion\": \"\"} and with an empty If-Match header", + "POST /api/v1/data/showcase_task/query with body `null`", + "GET /api/v1/packages?status=a&status=b; DELETE /api/v1/packages/com.example.showcase?keepData=true&keepData=true (expect refusal BEFORE any work — run on a throwaway boot only)", + "as the member PATCH with a stale If-Match a row the member cannot read" + ], + "acceptance": [ + { + "clause": "request-schema gate, empty-token refusal, dispatcher query arity and conflict caller visibility — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "null / array / scalar PATCH bodies and an empty version token each → 400 VALIDATION_FAILED, and updated_at is unchanged", + "oracle": "api", + "verify": "status + code per call; updated_at before/after", + "evidence": "per-call lines + the two timestamps" + }, + { + "clause": "a null /query body → 400, never the full row set", + "oracle": "api", + "verify": "status + row count", + "evidence": "the answer" + }, + { + "clause": "a repeated single-valued query name → 400 VALIDATION_ERROR (\"supplied N times\"); the doubled keepData DELETE is refused and the package and its tables remain", + "oracle": "api", + "verify": "status; GET /api/v1/packages + a data read afterwards", + "evidence": "refusal + the reads" + }, + { + "clause": "a stale If-Match on a row the caller cannot read → 404, not 409 with a currentRecord", + "oracle": "api", + "verify": "status + body", + "evidence": "the answer" + } + ], + "negative": [ + "the doubled keepData being honoured as false (tables dropped) is a data-loss FAIL" + ], + "traps": [ + "destructive-in-place", + "silent-coercion" + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/request-schema-gate.conformance.test.ts + packages/runtime/src/domains/data-write-request-gate.test.ts + packages/runtime/src/domains/data-delete-expected-version.test.ts + packages/runtime/src/domains/dispatcher-query-arity.test.ts + packages/runtime/src/domains/packages-write-flag-arity.test.ts + packages/runtime/src/domains/data-conflict-caller-visibility.test.ts" + }, + "source": [ + "packages/rest/src/meta-item-read-gate.ts#metaQueryArityRefusal", + "packages/rest/src/query-multiplicity.ts#assertFilterParamSuppliedOnce", + "packages/metadata-protocol/src/protocol.ts#ConcurrentUpdateError", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23491-patch-data-request-gate, 23492-empty-version-token-refused, 23574-data-request-contract-remainder, 23214-dispatcher-query-arity, 23214-hono-query-multiplicity, 23586-occ-conflict-caller-visibility, 23531-hono-conflict-body, 23531-hono-conflict-current-record" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "api-backend.meta-read-doors-v18", + "title": "/meta read doors on v18: GET /meta/app/PACKAGE_ID returns that package's app, `?member=` selects one bundle member on by-name doors, an unknown query parameter on any by-name door is refused 400, `?package=all` folds to the no-parameter answer, and the object definition marks an FLS-denied field unsortable for that caller", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase email template with two locale members (or one authored through PUT /api/v1/meta/email_template/NAME?member=zh-CN)" + ] + }, + "steps": [ + "GET /api/v1/meta/app/com.example.showcase; GET /api/v1/meta/app/com.unknown.pkg", + "GET /api/v1/meta/email_template/NAME?member=zh-CN; GET /api/v1/meta/object/showcase_task?member=x", + "GET /api/v1/meta/object/showcase_task?memeber=x (typo)", + "GET /api/v1/meta/view?package=all vs GET /api/v1/meta/view", + "as a member whose FLS hides a field F: GET /api/v1/meta/object/showcase_task and read sortability for F; GET /api/v1/data/showcase_task?sort=F" + ], + "acceptance": [ + { + "clause": "app-by-package-id, member selector, closed query sets, package=all fold and caller-FLS sortability — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "/meta/app/com.example.showcase returns the app whose name is the showcase app; an unknown package → the same 404 as an unknown name", + "oracle": "api", + "verify": "status + name", + "evidence": "both answers" + }, + { + "clause": "?member= reads only that member; on a non-bundled type → 400 VALIDATION_ERROR; an unknown parameter → 400 naming it", + "oracle": "api", + "verify": "status + code + body", + "evidence": "three lines" + }, + { + "clause": "?package=all answers byte-identically to no parameter", + "oracle": "api", + "verify": "compare item ids", + "evidence": "the two id lists" + }, + { + "clause": "for the FLS-restricted caller F is marked unsortable and the data sort on F is 403 — the two agree", + "oracle": "api", + "verify": "sortability entry + data answer", + "evidence": "both excerpts" + } + ], + "traps": [ + "cache-staleness" + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/meta-object-sortability-caller-fls.test.ts + packages/runtime/src/domains/meta-object-sortability-caller-fls.test.ts + packages/metadata-protocol/src/protocol.app-package-id-key.test.ts + packages/runtime/src/domains/meta-app-package-id-key.test.ts + packages/rest/src/rest-server-meta-member-selector.test.ts + packages/rest/src/rest-server-meta-door-closed-sets.test.ts + packages/runtime/src/domains/meta-package-all-fold.test.ts" + }, + "source": [ + "packages/spec/src/api/protocol.zod.ts#isAppPackageIdKey", + "packages/rest/src/meta-item-read-gate.ts#metaItemQueryRefusal", + "packages/rest/src/meta-item-read-gate.ts#metaMemberSelector", + "packages/metadata-core/src/object-schema-fls.ts#resolveObjectSchemaRuntimeView", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23183-app-item-read-by-package-id, 22877-member-selector, 22877-overlay-locale-member, 23072-dispatcher-meta-query-decision, 23072-meta-doors-closed-sets, 23073-list-member, 22250-meta-fls-runtime-sortability, 22128-draft-head-resolution" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "api-backend.error-sentences-and-unique-violation-wire-code", + "title": "Error answers made honest on v18: a hook refusal on a hand-built write route answers 400 with the hook's own sentence, a crash answers a generic 500 that withholds the caught message, and a unique-constraint refusal is 409 UNIQUE_VIOLATION with details at the action door, MCP and sandboxed bodies too", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "in the scratch app: a beforeInsert hook on sys_sharing_rule (or another hand-built write route's object) that throws a business sentence, and an object with a unique field plus a script action inserting a duplicate" + ] + }, + "steps": [ + "POST the hand-built write route whose hook refuses; read the body", + "invoke the script action that inserts a duplicate value: POST /api/v1/actions/OBJECT/ACTION", + "force a server fault on a storage route (e.g. a malformed adapter config on the scratch boot) and read error.message" + ], + "acceptance": [ + { + "clause": "hook-refusal sentences, withheld 500 text and the unique-violation wire code — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the hook refusal answers 400 VALIDATION_ERROR carrying the hook's sentence — not 500", + "oracle": "api", + "verify": "status + message", + "evidence": "the body" + }, + { + "clause": "the duplicate insert through the action door → 409 UNIQUE_VIOLATION with details naming the field", + "oracle": "api", + "verify": "status + code + details", + "evidence": "the body" + }, + { + "clause": "a forced 500 carries the route's generic sentence, never the caught internal message or a stack", + "oracle": "api", + "verify": "error.message text", + "evidence": "the body" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts + packages/rest/src/rest-hook-script-fault-envelope.test.ts + packages/runtime/src/dispatcher-unique-violation-wire-code.test.ts + packages/services/service-storage/src/storage-routes.server-fault-withheld.test.ts" + }, + "source": [ + "packages/types/src/data-error-classification.ts#sandboxBusinessMessage", + "packages/types/src/data-error-classification.ts#isSandboxOwnFault", + "packages/types/src/response-envelope.ts#sendServerFault", + "packages/runtime/src/http-dispatcher.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22719-rest-write-hook-refusal-sentence, 22764-rest-write-fault-withheld, 22885-sandbox-own-fault-rest-write-routes, 22588-nested-refusal-sentence-zero-row-total, 22694-import-row-hook-sentence, 22718-import-row-sandbox-fault, 22868-sandbox-fault-not-unknown-object, 23718-sandbox-plain-object-throw, 23686-dispatcher-unique-violation, 23701-mcp-unique-violation, 23702-sandbox-relay-duplicate, 23587-hono-structured-arm-fields, 23587-hono-unique-violation, 23174-nested-fault-writer" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index b70babf9fff..b31322261e9 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1447,6 +1447,153 @@ "history": [ { "revision": 1, "date": "2026-08-30", "change": "initial — the inbox's keyboard surface (documented in the page's own header and advertised by its hint bar) had no checklist coverage; the load-bearing halves are that r/a land the same server transition as the buttons and that quick-decidability gates hotkeys off rows whose decisions require typed outputs", "ref": "#sweep-2026-08-30" } ] + }, + { + "id": "approvals.request-page-reply-and-reader-visibility", + "title": "Approval requests on v18: the request record page serves record:approval_decision and a Reply action that lands in its timeline, `enable.approvalsVisibleToReaders` lets a plain record reader see the record's requests read-only (and only then), the generic /data door serves request rows by approvals visibility, and titles shown to a reader use only what that reader may read", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a pending showcase approval request the member is NOT an approver on", + "for the reader leg: a scratch object (or showcase object via the scratch app) with enable.approvalsVisibleToReaders:true" + ], + "knownGaps": [ + "the console clause needs the vendored console — blocked(environment) while the .objectui-sha pin does not build against current spec (see records-forms.v18-semantic-defaults)" + ] + }, + "steps": [ + "GET /api/v1/meta/page/sys_approval_request_detail and find record:approval_decision", + "as the pending approver POST /api/v1/approvals/requests/ID/comment {body}; read the timeline", + "as the member (non-participant) GET /api/v1/data/sys_approval_request/ID and list with a total", + "flip approvalsVisibleToReaders on the scratch object; as a plain reader list that record's requests; flip it off and repeat without restart" + ], + "acceptance": [ + { + "clause": "request page, reply, reader opt-in, generic-door read gate and reader-scoped titles — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the request page carries record:approval_decision and the reply lands in the timeline (pending approver or submitter only)", + "oracle": "api", + "verify": "page body + timeline read", + "evidence": "excerpts" + }, + { + "clause": "a non-participant's by-id read on /data/sys_approval_request → 404 and list totals exclude it", + "oracle": "api", + "verify": "status + total", + "evidence": "answers" + }, + { + "clause": "the reader sees requests read-only with no actions only while the flag is on; the inbox is not widened", + "oracle": "api", + "verify": "list per flag state", + "evidence": "two lists" + }, + { + "clause": "the console decision panel renders on the request page", + "oracle": "screenshot", + "verify": "request page after load", + "evidence": "one-line description" + } + ], + "traps": [ + "wrong-persona", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts + packages/spec/src/ui/component-record-approval-decision-22472.test.ts + packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts + packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts + packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts" + }, + "source": [ + "packages/plugins/plugin-approvals/src/sys-approval-request.page.ts", + "packages/plugins/plugin-approvals/src/request-read-gate.ts#bindRequestReadGate", + "packages/plugins/plugin-approvals/src/approval-service.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22472-spec-record-approval-decision, 22473-approval-request-page, 22560-platform-objects-approvals-visible-to-readers-form-row, 22560-plugin-approvals-declared-record-reader-tier, 22560-spec-approvals-visible-to-readers, 22559-approval-request-read-gate, 22589-approval-sibling-read-gates, 23594-subject-title-reader-fls, 23618-approval-title-family-served-fields, 23637-lookup-target-title-row-access" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "approvals.act-door-and-admin-rescue-slate", + "title": "Approvals on v18: the email action door /approvals/act works through the dispatcher exactly as the self-hosted mount (GET renders, POST redeems, PUT 405 Allow: GET, POST, a dead link a 200 page, a policy-held session not 403'd before the token), and an approval over an unstaffed position opens on the org administrators as ordinary approvers", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "an approval email action token (dev mail transport capture) for a pending request", + "an approval process whose approver is a position nobody holds (scratch process, default onEmptyApprovers)" + ] + }, + "steps": [ + "GET /api/v1/approvals/act?token=T; POST the same; PUT the same; GET with a consumed token", + "submit a record into the unstaffed process; as the org admin GET /api/v1/approvals/requests (My Pending); decide it" + ], + "acceptance": [ + { + "clause": "the dispatcher act door and the admin_rescue slate — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "GET renders the action page, POST redeems, PUT → 405 with Allow: GET, POST, a dead token → a 200 page (not an error envelope)", + "oracle": "api", + "verify": "status + headers per verb", + "evidence": "four lines" + }, + { + "clause": "the unstaffed request appears in each org admin's pending list and the decision is recorded via_override:false", + "oracle": "api", + "verify": "pending list + decision record", + "evidence": "excerpts" + } + ], + "automated": { + "kind": "dogfood", + "ref": "packages/runtime/src/domains/approvals-act.test.ts + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts + packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts + packages/qa/dogfood/test/approval-admin-rescue-slate.dogfood.test.ts" + }, + "source": [ + "packages/runtime/src/domains/approvals.ts", + "packages/core/src/security/auth-gate.ts", + "packages/plugins/plugin-approvals/src/approval-service.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22575-spec-approvals-action-page-member, 22577-approvals-act-allowlist, 22578-approvals-action-page-member, 22824-admin-rescue-named-slate" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; approvals.approver-resolution-matrix still says admin_rescue is decidable only via the privileged override — stale against 22824 (reported, not edited here)", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index 56ad4afe2e5..aad9ea7c983 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -891,6 +891,163 @@ "history": [ { "revision": 1, "date": "2026-08-20", "change": "new item from the scoped scan-functionality sweep (扫描功能): the server-side re-enforcement of field accept/maxSize had NO coverage on either lane — the only pinned guard is the client dialog's (records-forms.upload-guard-blocks-confirm), which the enforcement source itself calls 'a convenience rather than a control'. Authored with the qa-media-constraints recipe (no stock showcase field declares either key), the two source-documented holes (missing metadata, dotless-name vs extension-only accept) as documented-boundary passes, the declaration-based content boundary recorded so runs do not score sniffing absence as a leak, and the per-source wire-envelope analysis (ERR_FILE_CONSTRAINT ledgered but status-less → sanitized 500 expected) carried as a knownGap for the run to measure rather than rediscover", "ref": "claude/new-session-0pv25p" } ] + }, + { + "id": "attachments-storage.upload-limits-scope-and-ownership", + "title": "Upload contract on v18: a declared size over the limit (100 MiB default, moved live by the max_upload_mb setting) is 413 with nothing written, an unknown scope is 400 naming the allowed values (public is retired), only the uploader may complete or poll an upload, an unclaimed private file is 401 to anonymous, and a file id copied from another record hydrates as {id, metadataRefused:true}", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)", + "anonymous caller (no cookie, no bearer, no x-api-key)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)" + ] + }, + "steps": [ + "POST /api/v1/storage/upload/presigned declaring size 101 MiB; then set the storage max_upload_mb setting lower and retry a smaller declared size without restarting", + "POST presigned with scope \"avatars\", \"PUBLIC\", \"public\", null; and with scope omitted", + "as the admin start an upload; as the member POST /api/v1/storage/upload/complete and GET the chunked progress for it", + "anonymously GET the download URL of an unclaimed private file", + "as the member GET a record whose file field holds an id copied from another record" + ], + "acceptance": [ + { + "clause": "limits, scope vocabulary, uploader-only doors, unclaimed-download floor and file hydration — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "over-limit → 413 PAYLOAD_TOO_LARGE with no sys_file row; the saved max_upload_mb takes effect without restart", + "oracle": "api", + "verify": "status + sys_file count; the second refusal", + "evidence": "answers + counts" + }, + { + "clause": "an unknown or retired scope → 400 INVALID_REQUEST naming the allowed values (never 500); omitted → user", + "oracle": "api", + "verify": "status + message per variant", + "evidence": "per-variant lines" + }, + { + "clause": "the member's complete/progress on the admin's upload → 403 PERMISSION_DENIED (no admin exception)", + "oracle": "api", + "verify": "status + code", + "evidence": "both answers" + }, + { + "clause": "anonymous download of an unclaimed private file → 401 AUTH_REQUIRED with no signed URL", + "oracle": "api", + "verify": "status + absence of Location", + "evidence": "the answer" + }, + { + "clause": "a copied file id hydrates as {id, metadataRefused:true}; the record's own file hydrates fully", + "oracle": "api", + "verify": "field value shapes", + "evidence": "both excerpts" + } + ], + "negative": [ + "an anonymous caller receiving a signed URL for a private file is an authorization FAIL — carve-out applies" + ], + "traps": [ + "wrong-persona", + "auth-state-leak" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/services/service-storage/src/storage-limits.test.ts + packages/services/service-storage/src/upload-scope-vocabulary.test.ts + packages/qa/dogfood/test/storage-unclaimed-download.dogfood.test.ts + packages/services/service-storage/src/upload-door-ownership.test.ts + packages/services/service-storage/src/storage-routes.raw-token-refusal.test.ts + packages/objectql/src/engine-file-hydrate-refused.test.ts + packages/objectql/src/engine-file-hydrate-parent-derived.test.ts" + }, + "source": [ + "packages/services/service-storage/src/storage-limits.ts#resolveStorageLimits", + "packages/spec/src/api/storage.zod.ts#UploadScopeSchema", + "packages/services/service-storage/src/upload-ownership.ts#isFileUploader", + "packages/spec/src/data/field-value.zod.ts#FileRefusedValueSchema", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22283-storage-limits-honoured, 22314-storage-payload-too-large-provenance, 22470-upload-scope-vocabulary, 22443-storage-scope-public-retired, 22443-sys-file-public-scope-rewrite, 22431-unclaimed-download-signed-in, 22046-upload-doors-uploader-only, 23143-storage-token-refusal-401, 22593-file-field-hydration-refused, 22637-file-field-metadata-parent-derived" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "attachments-storage.chunked-upload-integrity", + "title": "Chunked upload integrity on v18: complete with a missing chunk is 409 with details.missingChunks and the session stays in_progress, a re-sent chunk counts once, parallel chunks all record, and an org switch mid-upload answers 409 on complete / chunk / progress", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)" + ] + }, + "steps": [ + "start a chunked upload of 3 chunks; PUT chunks 1 and 3; POST complete", + "PUT chunk 1 again; GET progress", + "PUT chunk 2; POST complete", + "start another, switch active organization (POST /api/v1/auth/organization/set-active) and PUT a chunk" + ], + "acceptance": [ + { + "clause": "chunk accounting, missing-chunk refusal and the org-switch guard — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "complete with a missing chunk → 409 RESOURCE_CONFLICT with details.missingChunks [2]; progress still in_progress", + "oracle": "api", + "verify": "status + details + progress", + "evidence": "answers" + }, + { + "clause": "a re-sent chunk is counted once in progress", + "oracle": "api", + "verify": "received count", + "evidence": "the progress body" + }, + { + "clause": "after the org switch chunk PUT / complete / progress → 409", + "oracle": "api", + "verify": "status per call", + "evidence": "three lines" + } + ], + "automated": { + "kind": "dogfood", + "ref": "packages/services/service-storage/src/chunked-upload-integrity.test.ts + packages/qa/dogfood/test/storage-chunked-resume-integrity.dogfood.test.ts + packages/services/service-storage/src/chunk-part-record-concurrency.test.ts" + }, + "source": [ + "packages/services/service-storage/src/storage-routes.ts#registerStorageRoutes", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22313-chunked-completion-assembles-held-parts, 22175-upload-org-change-4xx, 22218-chunk-progress-org-change-4xx, 22332-chunk-part-record-compare-and-set" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 040c7312b7a..0ca1f172ccc 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1875,6 +1875,345 @@ "ref": "#23169" } ] + }, + { + "id": "automation.flow-registration-verdict-on-read", + "title": "A stored flow the engine refused to register no longer reads as running: GET /meta/flow/:name carries registration {state:\"refused\", reason} beside the authored status, the list carries registrations keyed by name, and a good flow reads registered", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a scratch flow the engine refuses (store it on a scratch boot with OS_ALLOW_UNLINTED_METADATA_WRITES=1, e.g. a node whose config is refused by contract)" + ] + }, + "steps": [ + "GET /api/v1/meta/flow/SHOWCASE_FLOW and read registration", + "GET /api/v1/meta/flow/qa_refused_flow and read registration and status", + "GET /api/v1/meta/flow and read registrations" + ], + "acceptance": [ + { + "clause": "the registration verdict on the flow read and list — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "a showcase flow reads registration.state registered; the refused scratch flow reads refused with a reason message while status stays as authored", + "oracle": "api", + "verify": "registration objects", + "evidence": "both excerpts" + }, + { + "clause": "the list carries registrations keyed by flow name, including the refused one", + "oracle": "api", + "verify": "registrations map", + "evidence": "the map" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/meta-flow-cached-registration.test.ts + packages/services/service-automation/src/flow-refused-registration-read.integration.test.ts" + }, + "source": [ + "packages/spec/src/api/protocol.zod.ts#FlowRegistrationSchema", + "packages/metadata-protocol/src/protocol.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23316-refused-flow-registration-verdict" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "automation.flow-authoring-refusals-v18", + "title": "Flow authoring contract on v18, refused at `os validate` AND at PUT /meta/flow: single-brace {…} value tokens (CEL only; text slots use {{ }}), $-prefixed bound variable names, builtin node config keys/values the executor does not declare, dangling or repeated edges, `record`/`user.id` roots a flow cannot bind, retryPolicy.maxRetry, and an anonymous endpoint targeting a runAs:\"system\" flow", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)", + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "one scratch flow file per refusal arm, each otherwise valid (copy a showcase flow)" + ] + }, + "steps": [ + "for each arm run `os validate` in the scratch app and record exit code + rule id + located path: fields:{owner:\"{record.owner}\"}; notify title \"{record.name}\"; screen field named \"$x\"; create_record outputVariable:42; notify bogusKey; an edge to a removed node; a REST-triggered flow reading record.x; `user.id` in CEL; job retryPolicy:{maxRetry:3}; an authRequired:false flow endpoint targeting a runAs:\"system\" flow", + "PUT the same flow bodies to a fresh showcase boot at /api/v1/meta/flow/qa_ARM (scratch names)", + "controls: the CEL envelope {dialect:\"cel\",source:\"record.owner\"}, a notify title \"Case {{ record.name }}\", and a query placeholder {current_user_id} in a CRUD filter operand all PASS" + ], + "acceptance": [ + { + "clause": "every flow authoring refusal at the spec and lint layers — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "each arm fails `os validate` (exit ≠ 0) with its rule (expression-invalid / node-config-refused-by-contract / unrecognized_keys with did-you-mean) at the right path, and the message names the canonical spelling", + "oracle": "build", + "verify": "exit code + JSON issues per arm", + "evidence": "one line per arm" + }, + { + "clause": "the same bodies at PUT /meta/flow → 422 INVALID_METADATA and nothing stored", + "oracle": "api", + "verify": "status + read-back 404", + "evidence": "per-arm lines" + }, + { + "clause": "the three controls validate and save", + "oracle": "build", + "verify": "exit 0 / 200", + "evidence": "three lines" + } + ], + "negative": [ + "an arm that validates clean but fails only at run time is a FAIL of the build-door contract" + ], + "automated": { + "kind": "unit", + "ref": "packages/spec/src/automation/flow-value-slot-template.test.ts + packages/spec/src/automation/flow-text-slot-template.test.ts + packages/spec/src/automation/flow-builtin-node-config-keys.test.ts + packages/spec/src/automation/flow-builtin-node-config-values.test.ts + packages/spec/src/automation/flow-bound-variable-name.test.ts + packages/spec/src/automation/flow.test.ts + packages/lint/src/validate-expressions.flow-cel-root.test.ts + packages/spec/src/shared/retry-policy.test.ts + packages/spec/src/api/anonymous-elevated-flow-gate.test.ts" + }, + "source": [ + "packages/spec/src/automation/flow-value-slot-template.ts#flowNodeValueTemplateRefusals", + "packages/spec/src/automation/flow-text-slot-template.ts#textSlotTemplateRefusal", + "packages/spec/src/automation/flow-node-config-refusals.ts#flowNodeConfigRefusals", + "packages/spec/src/api/endpoint-publish-gate.ts#anonymousElevatedFlowPairs", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 19939-flow-callee-input-value-slots, 19939-flow-collection-value-slots, 19939-flow-crud-filter-operands, 19939-flow-http-slots, 19939-flow-notify-value-slots, 19939-flow-screen-value-slots, 19939-flow-single-brace-resolver-deleted, 19939-flow-value-slot-date-macros-refused, 19939-flow-value-slot-run-user-refused, 19939-flow-value-slot-template-dialect-refused, 22110-flow-text-slot-double-brace, 22054-notify-title-template-input, 21898-flow-builtin-node-config-values-refused, 21982-flow-builtin-node-config-undeclared-keys-refused, 21982-flow-script-subflow-config-undeclared-keys-refused, 22088-flow-edge-unresolved-or-repeated-refused, 22642-flow-cel-record-binding, 22677-flow-cel-record-entrance, 22636-flow-cel-record-trigger-no-object-opens, 22477-flow-text-slot-dollar-root-refused, 22502-flow-binding-variable-dollar-name-refused, 22572-flow-binding-name-dollar-refused, 22565-flow-cel-unbound-root-refused, 22343-retry-policy-try-catch-undeclared-keys-refused, 23260-builtin-token-value-judged, 23225-d2b-anonymous-elevated-flow-refused" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "automation.flow-run-semantics-v18", + "title": "Flow run semantics on v18: current_user is bound in CEL (null on a schedule run), `record` exists only when the run was handed one, a non-system caller starting a runAs:\"system\" self-triggered flow is refused 403 with nothing run, and a decision with overlapping true edges takes the FIRST", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "scratch flows: a record-triggered flow writing current_user.id; an autolaunched runAs:\"system\" flow; a decision node whose two edges are both true" + ] + }, + "steps": [ + "as the member update a record that fires the record-triggered flow; read the stamped field", + "as the member POST /api/v1/automation/qa_elevated/trigger", + "trigger the decision flow and read which branch ran (run log)" + ], + "acceptance": [ + { + "clause": "current_user binding, record binding, elevated-start refusal and first-match decisions — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the record-triggered run stamps the triggering member's id", + "oracle": "api", + "verify": "read-back of the field", + "evidence": "the row" + }, + { + "clause": "the member's start of the elevated flow → 403 PERMISSION_DENIED and no run row exists", + "oracle": "api", + "verify": "status + flow run list", + "evidence": "refusal + empty run list" + }, + { + "clause": "the decision takes the first true edge only", + "oracle": "log", + "verify": "run log / run record", + "evidence": "the branch line" + } + ], + "negative": [ + "a non-system caller successfully running a runAs:\"system\" flow is a privilege-escalation FAIL — carve-out applies" + ], + "traps": [ + "wrong-persona", + "eventual-consistency" + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-automation/src/flow-cel-current-user.test.ts + packages/services/service-automation/src/flow-cel-record-binding.test.ts + packages/runtime/src/action-flow-elevated-door.test.ts + packages/runtime/src/endpoint-flow-elevated-door.test.ts + packages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts" + }, + "source": [ + "packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart", + "packages/services/service-automation/src/engine.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22310-elevated-flow-trigger-door, 22407-flow-door-elevated-start, 19939-flow-value-slot-run-user-refused" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; D3 entry flow-decision-edge-branching-first-match", + "ref": "#23879" + } + ] + }, + { + "id": "automation.inbound-hook-timestamped-signature", + "title": "Inbound flow hooks accept ONLY the timestamped signature (t=,v1=): a body-only sha256= HMAC, a stale timestamp, an unknown flow and a wrong hook id all answer the SAME 401, and nothing is enqueued", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "anonymous caller (no cookie, no bearer, no x-api-key)", + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase flow with an inbound hook and its secret (automation.trigger-type-matrix names the fixture)" + ] + }, + "steps": [ + "POST /api/v1/automation/hooks/FLOW/HOOK with a valid t=,v1= signature (202)", + "POST with a body-only sha256= HMAC; with t older than 300 s; to an unknown flow; with a wrong hook id", + "list flow runs before/after" + ], + "acceptance": [ + { + "clause": "the timestamped-signature-only hook door — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the valid signature → 202 {accepted, messageId}", + "oracle": "api", + "verify": "status + body", + "evidence": "the answer" + }, + { + "clause": "the four refusal arms answer byte-identical 401 INVALID_SIGNATURE bodies and enqueue nothing", + "oracle": "api", + "verify": "byte-compare bodies; run count unchanged", + "evidence": "the bodies + counts" + } + ], + "traps": [ + "clock-skew" + ], + "automated": { + "kind": "unit", + "ref": "packages/triggers/trigger-api/src/inbound-hook-member.test.ts + packages/core/src/security/http-signature.test.ts + packages/runtime/src/domains/automation-hooks.test.ts" + }, + "source": [ + "packages/triggers/trigger-api/src/api-trigger.ts", + "packages/core/src/security/http-signature.ts#verifyHttpSignature", + "packages/runtime/src/domains/automation-hooks.ts#handleInboundHookRequest", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22769-versioned-http-signature, 22772-spec-trigger-api-inbound-hook-member, 22773-automation-hooks-domain, 22774-inbound-hook-member, 22806-uniform-hook-refusal, 22826-refuse-body-only-signature" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; automation.trigger-type-matrix deferred the body-only refusal to a follow-up that has since landed", + "ref": "#23879" + } + ] + }, + { + "id": "automation.scheduled-work-opt-in", + "title": "Scheduled work is OFF on a stock v18 boot unless OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true: schedule triggers bind and fire only with the flag, and the boot says which (D3 schedule-flow-acting-organization-required)", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a second boot with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true" + ] + }, + "steps": [ + "boot without the flag; read the boot log scheduled-work line; wait two schedule intervals of a showcase scheduled flow / job and list its runs", + "boot with the flag; repeat" + ], + "acceptance": [ + { + "clause": "the per-kernel scheduled-work policy — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "without the flag no scheduled run occurs and the boot log states scheduled work is off", + "oracle": "log", + "verify": "boot line + run list after two intervals", + "evidence": "line + empty list" + }, + { + "clause": "with the flag the scheduled flow runs on its interval", + "oracle": "api", + "verify": "run list after two intervals", + "evidence": "the runs" + } + ], + "traps": [ + "eventual-consistency", + "clock-skew" + ], + "automated": { + "kind": "unit", + "ref": "packages/triggers/trigger-schedule/src/per-kernel-scheduled-work-policy.test.ts + packages/services/service-automation/src/per-kernel-scheduled-work-policy.test.ts" + }, + "source": [ + "packages/types/src/env.ts", + "packages/spec/src/migrations/entries/semantic/18.schedule-flow-acting-organization-required.ts" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; automation.trigger-type-matrix, automation.time-relative-trigger and platform-core.notification-center assume scheduled work fires on a stock boot — they need the flag (reported, not edited here)", + "ref": "#23879" + } + ] } ] } \ No newline at end of file diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 8970969813f..20c882e9501 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -1715,6 +1715,359 @@ "history": [ { "revision": 1, "date": "2026-08-30", "change": "new item (sweep 2026-08-30, angle 2): the autoMigrate off|safe policy had no coverage although it is the one path that mutates a dev database at boot. Grounded verify-first, correcting the register's sketch on two points: (1) 'safe' is the LOOSEN-only subset (relax NOT NULL, widen varchar, index ops, clean-probe tighten) — not 'additive' diffs; (2) the register's sql-driver.ts/4652-4654 cites the config/field declarations, but the enforcement lives in reconcileAndWarnDrift with the managed-mode gate, the production force-off warning, and allowDestructive:false — all now pinned as clauses. The dev-injection arm (storage-driver.ts / serve.ts, 'safe' only when isDev) was found while grounding and added as its own clause so a keyless dev boot's self-heal is not misread as a driver default. enumSource pins SqlAutoMigrateSchema expect 2", "ref": "#sweep-2026-08-30" } ] + }, + { + "id": "cli.migrate-meta-write-stored-and-all", + "title": "`os migrate meta` on v18: `--from 17 --write` rewrites the provable mechanical edits in place (byte-preserving otherwise), lists what it cannot write with a reason and restores + exits 1 when a re-run disagrees; `--stored` lists a TODO per refused stored shape; and provably-absent semantic notices collapse into one line that `--all` expands", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a scratch app carrying v17 spellings (e.g. an element with a flat object/filter data-binding, an object-grid defaultFilters)", + "a scratch DB with a stored refused shape for the --stored leg" + ] + }, + "steps": [ + "checksum the sources; `os migrate meta --from 17` (report only); checksum again", + "`os migrate meta --from 17 --write --json`; read write.manual[]; diff the sources; re-run (idempotent)", + "`os migrate meta --from 17` vs `--all` on a stack without jobs/agents", + "`os migrate meta --stored --json` against the scratch DB" + ], + "acceptance": [ + { + "clause": "--write, --stored TODOs and the relevance predicate — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the report-only run leaves every source byte-identical", + "oracle": "build", + "verify": "checksums before == after", + "evidence": "the checksums" + }, + { + "clause": "--write applies the mechanical edits, leaves the rest byte-identical, lists manual sites with a kind, and a second run makes no edits", + "oracle": "build", + "verify": "git diff of the scratch tree + JSON + second run", + "evidence": "diffstat + JSON excerpt" + }, + { + "clause": "absent notices are one line naming --all; --all lists them", + "oracle": "build", + "verify": "both outputs", + "evidence": "excerpts" + }, + { + "clause": "--stored prints a TODO per refused stored shape instead of counting it canonical", + "oracle": "build", + "verify": "output", + "evidence": "excerpt" + } + ], + "traps": [ + "destructive-in-place" + ], + "automated": { + "kind": "unit", + "ref": "packages/cli/test/migrate-meta-write.test.ts + packages/cli/src/commands/migrate/meta.report-order.test.ts + packages/spec/src/migrations/semantic-relevance.test.ts + packages/services/service-automation/src/canonicalize-stored-flow.test.ts + packages/spec/src/conversions/refused-shapes.test.ts" + }, + "source": [ + "packages/cli/src/commands/migrate/meta.ts", + "packages/spec/src/migrations/chain.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 9591-migrate-meta-write, 23269-v18-stored-row-fates, 23317-stored-preview-gaps, 22072-migrate-meta-relevance-predicate" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; cli.migrate-meta-codemod still asserts that no --write exists (its restart condition has fired) — reported, not edited here", + "ref": "#23879" + } + ] + }, + { + "id": "cli.organization-ownership-ceremony", + "title": "The v18 upgrade ceremony `os migrate organization-ownership`: a bare run writes a plan and changes nothing, --apply demands a reviewed --plan-file (and --backup-taken), refuses a mismatched plan, an open SQLite holder (database-busy) and an unresolved dialect, checkpoints its steps, and `os migrate apply --allow-destructive` holds every organization_id drop until the ceremony marker is verified; then `os migrate comment-reactions --apply` converts reactions idempotently — end to end, plan → backup → apply → v18 boot", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P0", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a COPY of a v17-shaped SQLite DB (platform-core.v18-boot-refusals names how to get one) — never the original" + ] + }, + "steps": [ + "`os migrate organization-ownership` on the copy; read the plan file; checksum the DB before/after", + "`--apply` without --plan-file; with a plan for another DB; with the DB open in a second process", + "`--apply --plan-file PLAN --backup-taken`; read sys_migration row adr-0131-organization-ownership", + "`os migrate apply --allow-destructive` before and after the marker is verified", + "`os migrate comment-reactions --apply` twice", + "`os serve` on the migrated copy — it must boot (platform-core.v18-boot-refusals is the before-state)" + ], + "acceptance": [ + { + "clause": "the ceremony's plan, refusals, checkpoints, drop gate and reactions conversion — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the bare run writes organization-ownership-plan-*.json and the DB checksum is unchanged", + "oracle": "build", + "verify": "file + checksums", + "evidence": "plan name + checksums" + }, + { + "clause": "--apply without a plan → plan-file-required; a mismatched plan → plan-*-mismatch; an open holder → database-busy (exit 1, nothing applied)", + "oracle": "build", + "verify": "exit code + reason per arm", + "evidence": "per-arm lines" + }, + { + "clause": "the reviewed apply checkpoints in sys_migration and the drop gate withholds organization_id drops until then", + "oracle": "build", + "verify": "sys_migration row + apply output", + "evidence": "excerpts" + }, + { + "clause": "the migrated copy boots under v18", + "oracle": "log", + "verify": "boot output reaches listening", + "evidence": "the line" + } + ], + "negative": [ + "any ceremony arm that alters the DB while refusing is a data-integrity FAIL" + ], + "traps": [ + "destructive-in-place" + ], + "automated": { + "kind": "unit", + "ref": "packages/cli/src/commands/migrate/organization-ownership.apply-flags.test.ts + packages/cli/src/utils/organization-ownership-apply.test.ts + packages/cli/src/commands/migrate/organization-ownership.occupancy.integration.test.ts + packages/cli/src/commands/migrate/organization-ownership.dialect.test.ts + packages/cli/src/commands/migrate/apply-organization-ownership-gate.integration.test.ts + packages/cli/src/commands/migrate/comment-reactions.integration.test.ts + packages/cli/src/utils/organization-ownership-host-supplement.integration.test.ts" + }, + "source": [ + "packages/cli/src/commands/migrate/organization-ownership.ts", + "packages/spec/src/system/fate-supplement.zod.ts#OrganizationOwnershipSupplementSchema", + "packages/cli/src/utils/organization-ownership-drop-gate.ts", + "docs/adr/0131-total-organization-ownership-no-null-organization-id.md", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22617-cli-organization-ownership-plan, 22937-cli-ceremony-promotions, 22937-cli-organization-ownership-apply, 22937-core-email-template-row-definition, 22937-core-row-definition-mappers, 15211-ceremony-metadata-promotion, 15211-cli-apply-reviewed-plan, 23280-host-fate-supplement, 23400-ownership-occupancy-gate, 23429-sys-metadata-family-organization-column-retired, 23558-ceremony-dialect-resolver, 23684-drop-gate-host-tables, 23281-comment-reactions-upgrade-conversion" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "cli.meta-package-flags-and-explain", + "title": "CLI authoring commands on v18: `os meta delete|register --package ID` address one package's row (an empty --package is refused before send), `os meta register --force` passes a 409 DESTRUCTIVE_CHANGE, a no-op delete prints \"Nothing deleted\", and `os explain RULE_ID` explains a lint rule (unknown id exits 1)", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)", + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the CLI pointed at the fresh boot (OS_URL / --url and a login token)" + ] + }, + "steps": [ + "`os meta register view --data qa_view.json --package com.example.showcase`; `os meta delete view qa_view --package \"\"`", + "`os meta register object --data destructive.json` then with `--force`", + "`os meta delete view qa_absent_view`", + "`os explain field-no-consumers`, `os explain field-no-consumers --json`, `os explain`, `os explain no_such_rule`" + ], + "acceptance": [ + { + "clause": "package addressing, --force, the no-op line and rule explain — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "--package sends ?package= and an empty one is refused before any request", + "oracle": "build", + "verify": "CLI output + server log", + "evidence": "excerpts" + }, + { + "clause": "without --force the destructive register fails with a hint; with it, it passes", + "oracle": "build", + "verify": "exit codes + output", + "evidence": "both outputs" + }, + { + "clause": "`os explain` prints {rule, covers, paragraphs} with --json, lists ids with no argument, exits 1 on an unknown id", + "oracle": "build", + "verify": "exit codes + output", + "evidence": "per-call lines" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/cli/src/commands/meta/register-package.test.ts + packages/cli/src/commands/meta/delete-reset-carriers.test.ts + packages/cli/src/commands/meta/register-force.test.ts + packages/cli/test/explain-rule-id.test.ts + packages/cli/src/commands/meta/delete-noop-line.test.ts" + }, + "source": [ + "packages/cli/src/commands/explain.ts", + "packages/lint/src/rule-explanations.ts#RULE_EXPLANATIONS", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23293-client-delete-package, 23350-cli-meta-package-flag, 23480-cli-meta-register-force, 23658-client-publish-package, 23479-meta-delete-noop-line, 22161-functional-completeness-one-line, 22161-lint-slice-10-one-line, 22161-lint-slice-11-one-line-fix, 22161-lint-slice-12-one-line-fix, 22161-lint-slice-13-one-line-fix, 22161-lint-slice-14-one-line-fix, 22161-lint-slice-15-one-line-fix, 22161-lint-slice-16-one-line-fix, 22161-lint-slice-2-one-line, 22161-lint-slice-3-one-line, 22161-lint-slice-4-one-line, 22161-lint-slice-5-one-line, 22161-lint-slice-6-one-line, 22161-lint-slice-7-one-line, 22161-lint-slice-8-one-line, 22161-lint-slice-9-one-line, 22161-rule-message-one-line, 22161-s17-widget-binding-fix-lines, 22161-spec-approver-type-describe" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "cli.validate-v18-authoring-verdicts", + "title": "`os validate` / `os build` verdicts new on v18: an unknown seed record key (with did-you-mean), a misspelt record[…] / previous[…] field, a duplicate stack entry name, a controlled_by_parent object without exactly one required master_detail (also through objectExtensions), a component-props finding (now an error), and a credential in stack email/sms config each fail the build", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "one scratch edit per arm, reverted between arms" + ] + }, + "steps": [ + "for each arm run `os validate --json` and `os build`, recording exit code + rule id + path: seed key created_atx; record[\"typo\"] in a validation; two datasources named alike; a cbp object whose master_detail is not required; an object-grid with defaultFilters; email:{apiKey:\"x\"}", + "controls: seed key created_at, record.name == \"record.typo\", email.provider — all pass" + ], + "acceptance": [ + { + "clause": "each new build verdict — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "each arm fails validate (exit ≠ 0, no \"Validation passed\") with its rule at the right path and build refuses", + "oracle": "build", + "verify": "exit codes + issue per arm", + "evidence": "per-arm lines" + }, + { + "clause": "the controls pass", + "oracle": "build", + "verify": "exit 0", + "evidence": "three lines" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/spec/src/data/define-seed-record-keys.test.ts + packages/formula/src/validate-field-existence-spellings.test.ts + packages/spec/src/stack-duplicate-entry.test.ts + packages/lint/src/data-model-rules.master-detail-required.test.ts + packages/spec/src/stack-cbp-extension-master.test.ts + packages/lint/src/validate-component-props.test.ts + packages/spec/src/stack-email-sms.test.ts" + }, + "source": [ + "packages/spec/src/data/seed.zod.ts#defineSeed", + "packages/spec/src/stack.zod.ts#collectDuplicateEntryErrors", + "packages/spec/src/data/controlled-by-parent-master.ts#controlledByParentMasterRefusal", + "packages/spec/src/system/email-config.zod.ts#StackEmailConfigSchema", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22149-define-seed-record-keys, 22212-lint-sibling-spellings, 22428-field-existence-every-spelling, 22386-spec-object-attached-on-read, 22386-validator-attached-on-read-leaf, 22902-stack-functions-array-duplicate-name-refused, 22934-stack-collection-duplicate-entry-refused, 23210-cbp-extension-merged-master, 9139-cbp-master-detail-required-error, 23100-cbp-one-required-master, 23026-retired-component-prop-refused, 23147-component-props-every-finding-refused, 22748-stack-email-sms-door" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "cli.verify-derived-proof-families", + "title": "`os verify` gains five derived proof families on v18 — --reference-shapes, --delete-behaviors, --unique-scopes, --parent-write-reach, --dashboard-windows — each clean (exit 0) on the showcase with its report in --json, and non-zero on a planted defect", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot" + ] + }, + "steps": [ + "in examples/app-showcase run `os verify --reference-shapes --json` and each of the other four flags", + "in the scratch copy plant a wrong deleteBehavior and rerun --delete-behaviors" + ], + "acceptance": [ + { + "clause": "the five verify families — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "each family exits 0 with hardFailures 0 on the showcase and its report is present in --json", + "oracle": "build", + "verify": "exit code + JSON key per family", + "evidence": "five lines" + }, + { + "clause": "the planted defect makes --delete-behaviors exit non-zero naming it", + "oracle": "build", + "verify": "exit code + output", + "evidence": "excerpt" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/cli/test/verify-reference-shapes.test.ts + packages/cli/test/verify-delete-behaviors.test.ts + packages/cli/test/verify-unique-scopes.test.ts + packages/cli/test/verify-parent-write-reach.test.ts + packages/cli/test/verify-dashboard-windows.test.ts" + }, + "source": [ + "packages/cli/src/commands/verify.ts", + "packages/verify/src/delete-behavior.ts#runDeleteBehaviorProofs", + "packages/verify/src/reference-shape.ts#runReferenceShapeProofs", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 15953-verify-dashboard-window-family, 15953-verify-dashboard-window-now-removed, 15953-verify-delete-behavior-family, 15953-verify-parent-write-reach-family, 15953-verify-reference-shape-family, 15953-verify-unique-scope-family" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json index 9bd22087025..332a5f2e593 100644 --- a/docs/qa/platform-checklist/areas/dashboards.json +++ b/docs/qa/platform-checklist/areas/dashboards.json @@ -1194,6 +1194,75 @@ } ], "retiredReason": "Capability removed (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-25, #20102): the saved-report schedule dispatcher this item drove left with the rest of the saved-report stack (sys_report_schedule, the /api/v1/reports schedule routes, @objectstack/plugin-reports), which had zero consumers. The platform has no scheduled delivery of a report today; scheduled delivery for `report` metadata is explicitly NOT built by that ruling — a real need is a new card on the metadata kind, and a new item with it." + }, + { + "id": "dashboards.analytics-exposure-gate-and-measure-aggregate", + "title": "Analytics on v18 asks each object's `enable` block and internal flags: a cube or dataset over an apiEnabled:false object is 404, an internal field as dimension / measure / filter is 400 even for admin, an unexposed configured cube is warned at boot and absent from /analytics/meta, and a dataset answer's measure column states its aggregate", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "in the scratch app: an apiEnabled:false object with a cube over it and an internal:true field on an exposed object" + ] + }, + "steps": [ + "POST /api/v1/analytics/query on the cube over the unexposed object", + "POST /api/v1/analytics/query using the internal field as a dimension, then as a filter", + "GET /api/v1/analytics/meta and the boot log", + "POST /api/v1/analytics/dataset/query {datasetName: \"sys_user_metrics\", selection: {measures: [\"user_count\"]}} (a saved dataset is named by `datasetName`; `dataset` is an INLINE definition object); read fields[]" + ], + "acceptance": [ + { + "clause": "the analytics exposure door, registration refusal, label withholding and measure aggregate — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the unexposed cube query → 404 OBJECT_API_DISABLED; the internal field → 400 INVALID_FIELD (admin too)", + "oracle": "api", + "verify": "status + code", + "evidence": "answers" + }, + { + "clause": "the unexposed cube is warned at boot and absent from /analytics/meta", + "oracle": "log", + "verify": "boot line + meta list", + "evidence": "line + list" + }, + { + "clause": "the count measure column carries aggregate:\"count\"; dimension columns carry none", + "oracle": "api", + "verify": "fields[] entries", + "evidence": "the excerpt" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-analytics/src/__tests__/api-exposure-door.test.ts + packages/services/service-analytics/src/__tests__/unexposed-definition-registration.test.ts + packages/services/service-analytics/src/__tests__/dimension-label-exposure.test.ts + packages/services/service-analytics/src/__tests__/preview-column-enrichment.test.ts" + }, + "source": [ + "packages/services/service-analytics/src/api-exposure-door.ts#assertObjectsExposed", + "packages/services/service-analytics/src/analytics-service.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22634-analytics-exposure-gate, 22634-spec-ledger-analytics-exposure-codes, 22663-analytics-unexposed-cube-registration, 21995-measure-column-aggregate" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } \ No newline at end of file diff --git a/docs/qa/platform-checklist/areas/i18n.json b/docs/qa/platform-checklist/areas/i18n.json index 22966666ea6..78d6ae0cd1e 100644 --- a/docs/qa/platform-checklist/areas/i18n.json +++ b/docs/qa/platform-checklist/areas/i18n.json @@ -489,6 +489,83 @@ { "revision": 1, "date": "2026-08-07", "change": "new item: the i18n build lane — drift gate, undeclared-key gate, ratchet, and their provable red paths", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-11", "change": "step 4 was a no-op as written: 'perturb one committed bundle VALUE' is green by design under merge mode, where the committed file is its own baseline. Reworded to the STRUCTURAL perturbation (remove/rename a KEY) that actually bites, split into a do/don't pair, with the #7640 symlink-farm technique that drives it without touching a repo file; the drift-bites clause and both negatives are reconciled so a green value edit can no longer be read (or filed) as a gate defect", "ref": "#7686" } ] + }, + { + "id": "i18n.flow-copy-translation-and-host-override", + "title": "Flow copy is translatable on v18: a flow refusal message and a paused screen's title/description resolve in the caller's Accept-Language with holes filled, `os i18n extract` scaffolds the refusal / screen / option keys, a translated screen title is judged by the {{ }} text-slot rule, and a host translation bundle overrides a platform key in either load order", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a showcase (or scratch) flow with an end-node refusal message and a screen, plus a zh-CN translation bundle carrying their keys" + ] + }, + "steps": [ + "POST /api/v1/automation/FLOW/trigger with Accept-Language: zh-CN and read the refusal message", + "start the screen flow with Accept-Language: zh-CN and read the paused screen title", + "in the scratch app run `os i18n extract` and inspect the scaffolded keys; add a translated screen title \"Hi {name}\" and run `os validate`", + "in the scratch app give the host bundle a key the platform also ships (e.g. a sys_user label) and boot; GET /api/v1/meta/object/sys_user with Accept-Language for that locale" + ], + "acceptance": [ + { + "clause": "flow refusal / screen translation, extract scaffolding, translated text-slot rule and host override — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the refusal and the screen title come back in zh-CN with their holes filled; record-trigger refusals stay in the source language", + "oracle": "api", + "verify": "message text per call", + "evidence": "the two answers" + }, + { + "clause": "`os i18n extract` writes the refusal, screen, option and toast keys", + "oracle": "build", + "verify": "diff of the bundle file", + "evidence": "the added keys" + }, + { + "clause": "a translated screen title with single braces fails `os validate` naming the {{ }} spelling", + "oracle": "build", + "verify": "exit code + rule", + "evidence": "the issue line" + }, + { + "clause": "the host bundle's value wins for the overlapping platform key", + "oracle": "api", + "verify": "label in the served definition", + "evidence": "the excerpt" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-automation/src/end-node-refusal-translation.test.ts + packages/runtime/src/flow-run-locale.test.ts + packages/spec/src/automation/flow-screen-option-key.test.ts + packages/spec/src/system/translation.test.ts + packages/platform-objects/src/plugin.translations.test.ts" + }, + "source": [ + "packages/spec/src/system/i18n-resolver.ts#flowRefusalMessageKey", + "packages/spec/src/system/i18n-resolver.ts#flowScreenCopyKey", + "packages/spec/src/automation/flow-screen-option-key.ts#flowScreenFieldOptionKey", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22450-flow-refusal-translation, 22507-flow-options-toasts-translation, 22507-screen-copy-engine-pick-automation, 22507-screen-copy-engine-pick-cli, 22507-screen-copy-engine-pick-lint, 22507-screen-copy-engine-pick-spec, 22627-flow-screen-title-translation-text-slot, 22972-host-bundle-wins" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 0f245846dc2..48e0f96bfc8 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -2116,6 +2116,369 @@ "ref": "#23169" } ] + }, + { + "id": "identity-auth.auth-gate-held-session-raw-host-families", + "title": "A session held by the ADR-0069 credential policy (password expired, forced reset, MFA not enrolled) is refused with the gate's own 403 code on EVERY raw-host route family, and an API key whose owner holds an account-scope posture is refused too", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a member whose session the policy holds — the cheapest: create the member with POST /api/v1/auth/admin/create-user, which lands mustChangePassword:true, and sign it in BEFORE it changes its password (held with PASSWORD_RESET_REQUIRED); an expiry / MFA-required policy in auth settings holds it with the other two codes", + "an API key minted by the member before the hold (POST /api/v1/keys)" + ] + }, + "steps": [ + "sign the member in and confirm the hold server-side: GET /api/v1/auth/get-session carries the gate (never infer it from the console overlay)", + "with the held session, call one route of each raw-host family: GET /api/settings/NAMESPACE, GET /api/v1/datasources, GET /api/v1/share-links, POST /api/v1/storage/upload/presigned, POST /api/v1/webhooks/redeliver, POST /api/v1/packages/:id/publish", + "repeat one read with an unheld session (the admin) and confirm it is answered normally", + "with the member's API key (x-api-key) call GET /api/v1/data/showcase_task while the member holds MFA_REQUIRED (account scope), then while the member holds only PASSWORD_EXPIRED (credential scope)" + ], + "acceptance": [ + { + "clause": "the auth gate runs before route logic on every raw-host family, outage answers 503, and the API-key account posture is enforced at REST, the dispatcher and install-local — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "every raw-host family answers the held session 403 with the gate's OWN code (PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED / MFA_REQUIRED) — never 200, never a route-specific 404/400 that ran logic first", + "oracle": "api", + "verify": "status + error.code per family from the held session; an unheld session gets the normal answer on the same call", + "evidence": "one status+code line per family for both sessions" + }, + { + "clause": "an admin-forced change answers PASSWORD_RESET_REQUIRED (not PASSWORD_EXPIRED)", + "oracle": "api", + "verify": "error.code on the first held call after the admin forced the reset", + "evidence": "the refusal body" + }, + { + "clause": "an API key whose owner holds MFA_REQUIRED is refused 403 MFA_REQUIRED; a credential-scope posture (PASSWORD_EXPIRED) does NOT hold the key", + "oracle": "api", + "verify": "the two x-api-key calls", + "evidence": "both status lines" + } + ], + "negative": [ + "an unheld session or an anonymous caller must not see a 403 auth-gate code — anonymous keeps its own 401 floor" + ], + "traps": [ + "auth-state-leak", + "wrong-persona" + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-storage/src/storage-routes.auth-gate.pin.test.ts + packages/services/service-settings/src/settings-routes.auth-gate.pin.test.ts + packages/plugins/plugin-sharing/src/share-link-routes.auth-gate.pin.test.ts + packages/plugins/plugin-webhooks/src/webhook-redeliver-auth-gate.pin.test.ts + packages/services/service-datasource/src/__tests__/admin-routes-auth-gate.pin.test.ts + packages/cloud-connection/src/cloud-connection-auth-gate.pin.test.ts + packages/runtime/src/http-dispatcher.auth-gate-outage.pin.test.ts + packages/verify/src/held-session-vendor-admin-doors.pin.test.ts + packages/rest/src/rest-api-key-account-posture.pin.test.ts + packages/runtime/src/http-dispatcher.api-key-account-posture.pin.test.ts + packages/core/src/security/api-key-account-posture.test.ts" + }, + "source": [ + "packages/core/src/security/auth-gate.ts#evaluateAuthGate", + "packages/core/src/security/auth-gate.ts#normalizeAuthGate", + "packages/plugins/plugin-auth/src/platform-admin-gate.ts#judgePlatformAdmin", + "packages/spec/src/kernel/execution-context.zod.ts#AUTH_GATE_SCOPES", + "ADR-0069", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23178-auth-gate-raw-host-families, 23178-settings-auth-gate, 23190-auth-gate-cli-sites, 23245-dispatcher-auth-gate-outage, 23335-held-admin-vendor-doors-refused, 23335-held-platform-admin-refused, 23287-api-key-account-posture, 23455-key-admission-posture-required" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "identity-auth.api-key-expiry-lifecycle", + "title": "API-key lifecycle policy: a mint with no expiry gets the default (90 days) echoed back, an expiry past the ceiling is refused 400 API_KEY_EXPIRY_TOO_LONG, and an expired / inactive / revoked key is refused with its own 403 code", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the four `auth` settings keys (api_key_default_expiry_days, api_key_max_expiry_days, api_key_inactivity_days, api_key_policy_started_at) at their defaults unless a step says otherwise" + ] + }, + "steps": [ + "POST /api/v1/keys with no expires_at; read the response", + "POST /api/v1/keys with expires_at 400 days ahead", + "revoke a key (the admin key action) and call GET /api/v1/data/showcase_task with it", + "mint a key with a near expiry (or lower api_key_default_expiry_days through the settings door) and call with it after it lapses" + ], + "acceptance": [ + { + "clause": "default expiry, ceiling refusal and the three distinct 403 codes — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "a mint with no expires_at returns an expires_at about 90 days ahead (the configured default), echoed in the response", + "oracle": "api", + "verify": "response expires_at minus the server clock", + "evidence": "the mint response" + }, + { + "clause": "an expires_at beyond the ceiling (365 days by default) is refused 400 API_KEY_EXPIRY_TOO_LONG and no key is minted", + "oracle": "api", + "verify": "status + error.code; GET /api/v1/keys count unchanged", + "evidence": "refusal body + the two key counts" + }, + { + "clause": "a revoked key answers 403 API_KEY_REVOKED and an expired one 403 API_KEY_EXPIRED — distinct codes, not one generic 401", + "oracle": "api", + "verify": "status + error.code per key", + "evidence": "both refusal bodies" + } + ], + "traps": [ + "clock-skew" + ], + "automated": { + "kind": "dogfood", + "ref": "packages/rest/src/rest-api-key-lifecycle.pin.test.ts + packages/runtime/src/api-key-lifecycle.pin.test.ts + packages/qa/dogfood/test/api-key-lifecycle.dogfood.test.ts" + }, + "source": [ + "packages/plugins/plugin-auth/src/auth-manager.ts#resolveApiKeyLifecyclePolicy", + "packages/services/service-settings/src/manifests/auth.manifest.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23367-api-key-lifecycle" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; identity-auth.api-key-ui-lifecycle covers mint/revoke/restore only, not expiry, ceiling or inactivity", + "ref": "#23879" + } + ] + }, + { + "id": "identity-auth.cookie-session-renewal-in-process", + "title": "A cookie session past its updateAge is renewed ONLY by GET /auth/get-session: in-process reads (data, meta, me/permissions, SSO admin) leave sys_session.expires_at unchanged and set no cookie, while bearer-only calls still renew", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a session aged past `updateAge` — either a boot with a short session updateAge or a session row whose updated_at is moved back in the scratch DB" + ] + }, + "steps": [ + "read sys_session.expires_at for the session (GET /api/v1/data/sys_session as admin, or the scratch DB)", + "with the cookie only, call GET /api/v1/data/showcase_task and GET /api/v1/meta/object/showcase_task; re-read expires_at; inspect response headers for Set-Cookie", + "call GET /api/v1/auth/get-session with the cookie; re-read expires_at and headers" + ], + "acceptance": [ + { + "clause": "in-process session reads never renew a cookie session; get-session does — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "cookie-carrying data/meta reads leave expires_at unchanged and return no Set-Cookie", + "oracle": "api", + "verify": "expires_at before/after + the response headers", + "evidence": "the two expires_at values and the header dump" + }, + { + "clause": "GET /auth/get-session renews expires_at and re-issues the cookie", + "oracle": "api", + "verify": "expires_at moves forward and Set-Cookie is present", + "evidence": "the header dump + new expires_at" + } + ], + "traps": [ + "clock-skew", + "eventual-consistency" + ], + "automated": { + "kind": "unit", + "ref": "packages/runtime/src/in-process-session-renewal.pin.test.ts + packages/rest/src/in-process-session-read.pin.test.ts" + }, + "source": [ + "packages/types/src/in-process-session-read.ts#inProcessSessionReadInput", + "packages/types/src/in-process-session-read.ts#carriesSessionCookie", + "packages/plugins/plugin-auth/src/in-process-redispatch.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22258-in-process-session-read-no-renewal-behind-cookie, 22258-services-in-process-session-read, 22398-plugin-auth-session-redispatch, 22384-auth-session-api-getsession-input-query" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "identity-auth.default-organization-boot-invariant", + "title": "ADR-0131 single posture: the Default Organization exists from boot BEFORE any sign-up — the first sign-up becomes its owner, later sign-ups its members, every session carries it as active org, and seeded org-columned rows carry it instead of NULL", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P0", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)" + ] + }, + "steps": [ + "on a fresh DB, confirm the slug `default` sys_organization row exists before the first user: read the boot log ordering (the Default Organization is ensured before --seed-admin signs the admin up) or boot WITHOUT --seed-admin and read the scratch DB", + "let --seed-admin create the first user, then create a second with POST /api/v1/auth/admin/create-user (self-registration is closed on the stock showcase); GET /api/v1/auth/get-session for each", + "as admin GET /api/v1/data/sys_member?filter organization = default", + "as admin GET /api/v1/data/sys_business_unit and read organization_id on the seeded rows" + ], + "acceptance": [ + { + "clause": "ensureDefaultOrganization runs at boot and the identity objects stamp the org — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the Default Organization exists before the first sign-up", + "oracle": "log", + "verify": "boot log line / sys_organization row read before any POST /auth/sign-up", + "evidence": "the row (id, slug) and the timestamp ordering against the first sign-up" + }, + { + "clause": "the first user is owner, the second is member, and both sessions carry the Default Organization as activeOrganizationId", + "oracle": "api", + "verify": "sys_member roles + get-session payloads", + "evidence": "the two member rows and two session payloads" + }, + { + "clause": "seeded rows of org-columned objects (sys_business_unit) carry the Default Organization id, never NULL", + "oracle": "api", + "verify": "organization_id on every seeded row", + "evidence": "the row list with organization_id" + } + ], + "negative": [ + "a seeded org-columned row with organization_id NULL is a FAIL of ADR-0131 total ownership (it also unblocks access-security.crud-permission-matrix A1 — re-run that clause when this passes)" + ], + "traps": [ + "first-boot-cold-start", + "seed-data-thin" + ], + "automated": { + "kind": "unit", + "ref": "packages/plugins/plugin-auth/src/ensure-default-organization.test.ts + packages/runtime/src/identity-objects-default-organization.pin.test.ts" + }, + "source": [ + "packages/plugins/plugin-auth/src/default-organization-invariant.ts#ensureDefaultOrganizationExists", + "packages/plugins/plugin-auth/src/ensure-default-organization.ts#ensureDefaultOrganization", + "docs/adr/0131-total-organization-ownership-no-null-organization-id.md", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 15195-plugin-auth-default-organization-boot-invariant, 15195-objectql-no-organization-refused, 15195-metadata-protocol-seed-organization-stamp" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "identity-auth.import-invitation-and-sso-domain-answers", + "title": "Identity import sends the dedicated account-invitation mail whose link opens the console set-password page, and the SSO domain-verification doors answer 404 for an unknown provider when enabled and 400 DOMAIN_VERIFICATION_DISABLED when off", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the dev mail transport (boot log mail capture) to read the sent message", + "OS_SSO_DOMAIN_VERIFICATION toggled per leg (two boots)" + ] + }, + "steps": [ + "POST /api/v1/auth/admin/import-users with one new user under the automatic-invite policy; read the captured mail", + "open the mailed link: it must be /_console/reset-password?token=… and accept a new password", + "trigger a self-service password reset for an existing user and read that mail", + "POST /api/v1/auth/admin/sso/request-domain-verification with an unknown providerId on a boot with OS_SSO_DOMAIN_VERIFICATION on, then on one with it off" + ], + "acceptance": [ + { + "clause": "import sends auth.account_invitation with a working link; the SSO domain doors answer by flag — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the import invite uses the auth.account_invitation template (inviter, address, set-password link, real expiry) — not auth.password_reset; a self-requested reset still uses password_reset", + "oracle": "log", + "verify": "the two captured mails: template id / subject", + "evidence": "both mail headers + the link" + }, + { + "clause": "the invite link targets /_console/reset-password?token= and works (no ?error=INVALID_TOKEN)", + "oracle": "api", + "verify": "follow the link / POST the reset with its token", + "evidence": "the reset answer" + }, + { + "clause": "unknown providerId: 404 RESOURCE_NOT_FOUND with the flag on, 400 DOMAIN_VERIFICATION_DISABLED with it off", + "oracle": "api", + "verify": "status + error.code per boot", + "evidence": "both refusal bodies" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/plugins/plugin-auth/src/sso-domain-verification-unknown-provider.pin.test.ts + packages/plugins/plugin-auth/src/admin-import-users-invite-link.test.ts + packages/plugins/plugin-email/src/auth-account-invitation-template.test.ts" + }, + "source": [ + "packages/plugins/plugin-email/src/templates/auth-templates.ts#AUTH_ACCOUNT_INVITATION_TEMPLATE", + "packages/plugins/plugin-auth/src/admin-import-users.ts#runAdminImportUsers", + "packages/plugins/plugin-auth/src/auth-manager.ts#isSsoDomainVerificationEnabled", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23343-plugin-auth-import-account-invitation, 23343-plugin-email-account-invitation-template, 23152-import-invite-redirect-to, 22463-sso-domain-verification-unknown-provider, 23598-plugin-auth-import-invite-delivery-failure" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index d1883b15a81..18fc07152b7 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1914,6 +1914,149 @@ "ref": "#23169" } ] + }, + { + "id": "integration-system.inbound-mailbox-association", + "title": "Inbound mail (new on v18): with requires:[\"mailbox\"] and an IMAP mailbox, a synced message is stored as sys_mail_message and associated with the record whose mailAssociation email field matches (inline forwards by an org member match on the original participants), attachments land as sys_file + sys_attachment, and an unmatched message is visible only to manage_mailboxes holders", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a scratch app with requires:[\"mailbox\"], a contact-like object with an email field marked mailAssociation, and an IMAP mailbox the sync job can reach (a local IMAP double) — there is NO HTTP ingest door, so this needs the IMAP sync composed" + ], + "knownGaps": [ + "no in-repo app composes the mailbox service and no IMAP double ships with a stock boot — the live clauses are blocked(fixture) until one does; the Tier-1 pins compose a kernel with an IMAP double and are the evidence in the meantime" + ] + }, + "steps": [ + "configure a sys_mailbox and run the mailbox:imap-sync job", + "deliver one message from a contact address, one inline-forwarded by an org member, one with an attachment, one from nobody", + "GET /api/v1/data/sys_mail_message and /sys_mail_association as admin and member" + ], + "acceptance": [ + { + "clause": "mailbox ingest, association, inline-forward matching and attachments — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the contact message is associated with that contact; the inline forward matches the original participants", + "oracle": "api", + "verify": "association rows", + "evidence": "the rows" + }, + { + "clause": "the attachment is a sys_file + sys_attachment on the message", + "oracle": "api", + "verify": "attachment rows", + "evidence": "the rows" + }, + { + "clause": "the unmatched message → 404 RECORD_NOT_FOUND for the member, visible to the manage_mailboxes holder", + "oracle": "api", + "verify": "status per persona", + "evidence": "two lines" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-mailbox/src/mailbox-service-kernel.test.ts + packages/services/service-mailbox/src/mailbox-sync-kernel.test.ts + packages/services/service-mailbox/src/ingest/inline-forward.test.ts + packages/services/service-mailbox/src/mailbox-attachments-kernel.test.ts + packages/spec/src/data/field-mail-association.test.ts" + }, + "source": [ + "packages/services/service-mailbox/src/mailbox-service-plugin.ts#MailboxServicePlugin", + "packages/services/service-mailbox/src/ingest/mail-ingest-service.ts#MailIngestService", + "packages/services/service-mailbox/src/ingest/inline-forward.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22870-field-mail-association, 22870-service-mailbox, 22871-imap-mailbox-sync, 22871-mail-association-live, 22871-mail-ingest-seam, 23215-inline-forward-parse, 23307-mail-attachments, 23306-storage-attach-verb" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "integration-system.notification-org-and-redeliver-eligibility", + "title": "Messaging and integration answers on v18: a notify from a system-context run stores the organization of the record it is about (never NULL), mark-read writes a receipt only for notifications delivered to the caller, redelivering for an inactive or deleted webhook subscription is 409 DELIVERY_NOT_ELIGIBLE, and a declarative MCP stdio connector with a relative command boots healthy from another directory", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase notify flow (integration-system.notify-inbox-delivery names one)", + "the showcase outbound webhook (shipped inactive) with one delivery" + ] + }, + "steps": [ + "fire the notify flow from a system-context run; GET /api/v1/data/sys_notification and read organization_id", + "as the member POST /api/v1/notifications/read with the ADMIN's notification id; read readCount", + "POST /api/v1/webhooks/redeliver for a delivery of the inactive webhook", + "from the repo root run `os verify --app examples/app-showcase` (or boot from another cwd) and read the MCP connector health" + ], + "acceptance": [ + { + "clause": "notification org stamping, recipient-only receipts, redeliver eligibility and MCP stdio cwd — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the stored notification carries the record's organization_id and no \"[notify] no organization in scope\" warning is logged", + "oracle": "api", + "verify": "organization_id + boot log grep", + "evidence": "row + grep" + }, + { + "clause": "the foreign mark-read answers readCount 0 and writes no receipt", + "oracle": "api", + "verify": "readCount + receipt rows", + "evidence": "answer + rows" + }, + { + "clause": "redeliver for the inactive webhook → 409 DELIVERY_NOT_ELIGIBLE", + "oracle": "api", + "verify": "status + code", + "evidence": "the answer" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-messaging/src/notification-organization-follows-event.integration.test.ts + packages/services/service-messaging/src/messaging-service.test.ts + packages/plugins/plugin-webhooks/src/webhook-redeliver-member.test.ts + packages/connectors/connector-mcp/src/mcp-stdio-cwd.test.ts" + }, + "source": [ + "packages/services/service-messaging/src/messaging-service.ts", + "packages/plugins/plugin-webhooks/src/redeliver-guard.ts", + "packages/connectors/connector-mcp/src/mcp-provider.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23284-notification-takes-record-organization, 22026-read-receipt-recipient-only, 22804-redeliver-inactive-veto, 22754-spec-webhooks-redeliver-member, 22756-webhooks-redeliver-member, 22423-mcp-stdio-package-cwd, 22434-connector-provider-package-path" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } \ No newline at end of file diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 87ea4d2ed4d..603e1511a91 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -2878,6 +2878,312 @@ "ref": "#21932" } ] + }, + { + "id": "platform-core.v18-boot-refusals", + "title": "A v18 server refuses to boot rather than serve wrongly: against a 17.x-shaped database (a physical organization_id on a non-tenant table, no verified ADR-0131 ceremony marker) it stops before listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED naming the tables and the command, and with security on but no identity objects it stops with AuthzIdentityObjectsMissingError", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P0", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a v17-shaped SQLite file: a DB created by a 17.x showcase boot (npm `@objectstack/cli@17.7.0`) — or the guard pin's fixture shape planted into a scratch DB — copied, never the original" + ] + }, + "steps": [ + "checksum the v17 DB; run `os serve` (v18) against it with -d file:COPY.db; read the output and exit code; checksum again", + "run `os dev` on a scratch config with services:{auth:false} and security on" + ], + "acceptance": [ + { + "clause": "the organization-ownership boot guard and the identity-objects boot refusal — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the v18 serve against the v17 DB exits before kernel:listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED, naming the tables and `os migrate organization-ownership`", + "oracle": "log", + "verify": "boot output + exit code + no listening line", + "evidence": "the refusal excerpt" + }, + { + "clause": "the refused boot did not alter the database", + "oracle": "build", + "verify": "checksum before == after", + "evidence": "the two checksums" + }, + { + "clause": "the auth-less security boot is refused naming createIdentityObjectsPlugin(), not served with 503s", + "oracle": "log", + "verify": "boot output", + "evidence": "the excerpt" + } + ], + "negative": [ + "a v18 boot that serves a v17-shaped DB (or half-migrates it) is a P0 data-integrity FAIL" + ], + "traps": [ + "destructive-in-place", + "first-boot-cold-start" + ], + "automated": { + "kind": "unit", + "ref": "packages/runtime/src/organization-ownership-boot-guard.test.ts + packages/runtime/src/organization-ownership-boot-guard.kernel.test.ts + packages/plugins/plugin-security/src/authz-identity-objects-boot-refusal.test.ts" + }, + "source": [ + "packages/runtime/src/organization-ownership-boot-guard.ts#ORGANIZATION_OWNERSHIP_BOOT_REFUSAL_CODE", + "packages/plugins/plugin-security/src/security-plugin.ts#AuthzIdentityObjectsMissingError", + "docs/adr/0131-total-organization-ownership-no-null-organization-id.md", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23429-v18-boot-refusal, 22074-auth-identity-objects-plugin, 22074-security-identity-objects-boot-refusal" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "platform-core.protocol-18-handshake-and-stored-replay", + "title": "Protocol 18 upgrade surfaces: a package declaring engines.protocol \"^17\" is refused 422 OS_PROTOCOL_INCOMPATIBLE naming `os migrate meta --from 17` (\"^18\" is admitted), a stored v17-shaped row is served canonical while a PUT of the same old shape is refused at parse, and a \"^18\" artifact built before a retirement boots with one warn per replayed conversion", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "a scratch package manifest with engines.protocol \"^17\" and one with \"^18\"", + "a stored v17-shaped row (e.g. an element using a retired flat data-binding key) planted into the scratch DB" + ] + }, + "steps": [ + "POST /api/v1/marketplace/install-local (or POST /api/v1/packages) with the ^17 manifest; then the ^18 one", + "GET /api/v1/meta/page/PLANTED and compare with the planted body; PUT the planted (old-shape) body back", + "boot (`os start`) a ^18 dist/objectstack.json built before a retirement; read the boot warns" + ], + "acceptance": [ + { + "clause": "the protocol handshake, stored-row replay and artifact forward conversion — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "^17 → 422 OS_PROTOCOL_INCOMPATIBLE with migrateCommand naming --from 17; ^18 admitted", + "oracle": "api", + "verify": "status + code + migrateCommand", + "evidence": "both answers" + }, + { + "clause": "the planted row is served in the canonical v18 shape; PUT of the old shape → 422 with the tombstone prescription", + "oracle": "api", + "verify": "GET body vs planted; PUT status + message", + "evidence": "both excerpts" + }, + { + "clause": "the old artifact boots with one \"converted N site(s) … ID\" warn per conversion", + "oracle": "log", + "verify": "boot log grep", + "evidence": "the lines" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/runtime/src/domains/packages-install-protocol-incompatible.test.ts + packages/cli/test/migrate-meta-protocol-range.test.ts + packages/spec/src/conversions/stored.test.ts + packages/metadata-protocol/src/protocol.stored-migration.test.ts + packages/metadata/src/plugin-artifact-forward-conversion-major-step.test.ts" + }, + "source": [ + "packages/spec/src/kernel/protocol-version.ts#PROTOCOL_VERSION", + "packages/metadata-core/src/protocol-handshake.ts#checkProtocolCompat", + "packages/spec/src/conversions/stored.ts#applyConversionsToStoredItem", + "packages/metadata-core/src/artifact-forward-conversion.ts#applyArtifactForwardConversions", + "ADR-0087", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22130-protocol-18, 23001-prerelease-artifact-window" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "platform-core.settings-organization-isolation-and-platform-rung", + "title": "Settings on v18: a global-scope key is stored in sys_platform_setting (readable through /data only with manage_platform_settings), and under a walled posture one organization's tenant save or lock does not change or lock another's, with a data-API read of sys_setting filtered to readable namespaces", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)", + "org owner holding manage_metadata who is NOT platform admin — walled-posture legs only" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", + "on the walled boot: two organizations A and B with an admin each" + ] + }, + "steps": [ + "as platform admin write a global-scope key through PUT /api/settings/NAMESPACE; GET /api/v1/data/sys_platform_setting and /sys_setting?filter scope=global", + "as the member GET /api/v1/data/sys_platform_setting", + "walled: as A's admin save a tenant key and lock it; as B's admin read and write the same key", + "walled: write a tenant key with no organization in scope" + ], + "acceptance": [ + { + "clause": "the platform rung and per-organization settings isolation — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the global key lands in sys_platform_setting and sys_setting has no scope=global row; the member is refused that table", + "oracle": "api", + "verify": "row reads + member status", + "evidence": "excerpts" + }, + { + "clause": "walled: A's save and lock leave B's value and lock untouched", + "oracle": "api", + "verify": "B's reads before/after", + "evidence": "reads" + }, + { + "clause": "a tenant key with no organization → 400 SETTINGS_VALIDATION", + "oracle": "api", + "verify": "status + code", + "evidence": "the answer" + } + ], + "traps": [ + "wrong-persona", + "cache-staleness" + ], + "automated": { + "kind": "unit", + "ref": "packages/services/service-settings/src/settings-organization-isolation.pin.test.ts + packages/services/service-settings/src/settings-read-door.pin.test.ts + packages/services/service-settings/src/settings-admission-tenancy-posture.test.ts + packages/services/service-settings/src/config-change-audit.test.ts" + }, + "source": [ + "packages/services/service-settings/src/settings-read-door.ts#registerSettingsReadDoor", + "packages/services/service-settings/src/config-change-audit.ts#CONFIG_CHANGE_GLOBAL_OBJECT_NAME", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22261-settings-organization-row-identity, 15207-settings-global-rung-platform-setting" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "platform-core.serve-composition-v18", + "title": "`os serve` composition on v18: unclaimed top-level items of a packages[] config are served under manifest.id with a warn line, an app shipping its own default sys_organization record page boots, a host datasourceMapping is installed before the first schema sync, a production host with no storage refuses to boot naming three remedies, and /_console/ answers 503 with the remedy when the console is not built", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "cli", + "personas": [ + "metadata author running the CLI in a scratch copy of the showcase app (no server needed)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "per leg a scratch config: packages[] plus one top-level view; an isDefault:true sys_organization record page; a datasourceMapping (examples/app-crm declares one); NODE_ENV=production with no storage service" + ], + "knownGaps": [ + "test/serve-organization-record-page.e2e.test.ts is a `*.e2e.test.ts` file, which packages/cli collects ONLY under OS_TEST_TIERS=nightly (vitest.config.ts nightly tiers): run it as `OS_TEST_TIERS=nightly npx vitest run test/serve-organization-record-page.e2e.test.ts` in packages/cli — a plain run of the automated.ref list silently collects 25 of its 26 cli files" + ] + }, + "steps": [ + "`os serve` each scratch config on its own port and file DB; read the boot output", + "GET /api/v1/meta/view and find the top-level view's package", + "boot a tree whose console is not built and GET /_console/" + ], + "acceptance": [ + { + "clause": "the serve composition guards and fallbacks — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the unclaimed top-level view is served under manifest.id with one warn line", + "oracle": "api", + "verify": "item package + boot log", + "evidence": "excerpt + line" + }, + { + "clause": "the custom sys_organization default page boots (no DEFAULT_RECORD_PAGE_CONFLICT) and is the only default", + "oracle": "log", + "verify": "boot output + GET /meta/page list", + "evidence": "excerpt" + }, + { + "clause": "platform tables land in the mapped datasource (no `no such table`)", + "oracle": "log", + "verify": "boot output", + "evidence": "excerpt" + }, + { + "clause": "the production no-storage boot exits non-zero naming the three remedies", + "oracle": "log", + "verify": "exit code + output", + "evidence": "excerpt" + }, + { + "clause": "GET /_console/ on an unbuilt console → 503 with the build remedy", + "oracle": "api", + "verify": "status + body", + "evidence": "the answer" + } + ], + "automated": { + "kind": "unit", + "ref": "packages/cli/test/serve-config-boot-residual.integration.test.ts + packages/cli/test/serve-organization-record-page.e2e.test.ts + packages/cli/test/serve-datasource-mapping-before-sync.integration.test.ts + packages/runtime/src/datasource-mapping-before-schema-sync.test.ts + packages/cli/test/serve-no-storage-boot-guard.integration.test.ts + packages/cli/src/utils/console.not-built.test.ts" + }, + "source": [ + "packages/metadata/src/plugin.ts", + "packages/core/src/stack-organization-record-page.ts#stackDeclaresOrganizationRecordPage", + "packages/cli/src/utils/console.ts#createConsoleNotBuiltPlugin", + "packages/cli/src/commands/serve.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22521-config-boot-residual, 23254-auth-org-record-page-option, 23310-serve-org-record-page, 23649-datasource-mapping-before-sync, 23573-serve-no-storage-loud, 23636-no-storage-hydration-error, 22225-console-not-built-loud, 22420-console-drift-responder" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 5777847ee98..6d1da7e72b2 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -4582,6 +4582,309 @@ "ref": "#21596" } ] + }, + { + "id": "records-forms.write-time-option-and-element-shape-judgement", + "title": "Write-time value judgement on v18: picking an option whose stored visibleWhen cannot be evaluated is refused (reason unevaluable), an import with createMissingOptions keeps an unmatched select value while a later PATCH sending it back is refused, an import dry run admits exactly what the write admits, and a malformed element in a multi-valued lookup is refused with its index", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "in the scratch app: a select field whose option carries a visibleWhen that faults at runtime (stored with OS_ALLOW_UNLINTED_METADATA_WRITES=1 on the scratch boot), and a multiple:true lookup field" + ] + }, + "steps": [ + "POST a row choosing the faulting option", + "POST /api/v1/data/OBJ/import with createMissingOptions:true and an unmatched select value; repeat as dryRun; then PATCH another row sending that value", + "dry-run import a matched row whose requiredWhen reads an omitted column; compare with the real import", + "POST a row with the multi lookup set to [{\"id\":\"x\"}], then [false], then [42]" + ], + "acceptance": [ + { + "clause": "option-visibility enforcement, kept options, dry-run parity and multi-reference element shape — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "choosing the faulting option → 400 VALIDATION_FAILED rule_violation with constraint.reason unevaluable", + "oracle": "api", + "verify": "status + reason", + "evidence": "the body" + }, + { + "clause": "the import stores the unmatched value and its dry run agrees; a later PATCH sending it back → invalid_option", + "oracle": "api", + "verify": "stored value + dry-run row + PATCH answer", + "evidence": "three excerpts" + }, + { + "clause": "the dry run admits the matched row exactly where the write admits it", + "oracle": "api", + "verify": "dry-run row verdict vs import row verdict", + "evidence": "both rows" + }, + { + "clause": "each malformed multi-lookup element → 400 VALIDATION_FAILED invalid_type naming the index", + "oracle": "api", + "verify": "status + located index", + "evidence": "three bodies" + } + ], + "traps": [ + "silent-coercion" + ], + "automated": { + "kind": "unit", + "ref": "packages/objectql/src/validation/rule-validator.option-visibility.test.ts + packages/objectql/src/engine-field-predicate-fault.test.ts + packages/objectql/src/import-kept-option-values.test.ts + packages/objectql/src/validation/record-validator.kept-option-values.test.ts + packages/objectql/src/validate-update-stored-row.test.ts + packages/objectql/src/validate-preview-parent.test.ts + packages/objectql/src/engine-multi-reference-element-shape.test.ts" + }, + "source": [ + "packages/objectql/src/validation/rule-validator.ts", + "packages/core/src/utils/import-coerce.ts", + "packages/objectql/src/validation/record-validator.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22402-option-gate-fails-closed, 22183-import-kept-option-values, 22445-update-preview-stored-row, 22474-preview-binds-parent, 22908-multi-reference-element-shape" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "records-forms.write-answer-advisories-rollback-and-tree-delete", + "title": "Write semantics changed on v18: a warning-severity validation hit comes back in the 2xx write answer as warnings[], a blocking after* hook abort rolls the whole write back, a `tree` field (with or without an explicit reference) honours its deleteBehavior, and a cascade delete is distinguishable to the child hook", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "the showcase project object whose severity:\"warning\" rule fires on a health jump (records-forms.validation-rule-type-matrix names it)", + "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", + "in the scratch app: an object with a referenceless `tree` field per deleteBehavior (set_null / cascade / restrict) and an afterInsert hook that aborts" + ] + }, + "steps": [ + "PATCH a showcase project to trigger the warning rule; read the answer body", + "scratch: POST a row on the object whose afterInsert aborts; GET the list afterwards", + "scratch: build a parent/child tree and DELETE the parent under each deleteBehavior" + ], + "acceptance": [ + { + "clause": "advisory write answer, after-hook rollback, tree delete semantics and the cascade signal — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the warning-rule PATCH answers 2xx with warnings:[{rule,severity,field,code,message}]; a write with no hit carries no warnings key", + "oracle": "api", + "verify": "response body keys", + "evidence": "both bodies" + }, + { + "clause": "the aborting afterInsert refuses the caller AND no row is stored", + "oracle": "api", + "verify": "status + list count before/after", + "evidence": "answer + counts" + }, + { + "clause": "tree delete: set_null clears the children, restrict → 409 DELETE_RESTRICTED, cascade removes the subtree", + "oracle": "api", + "verify": "child reads after each delete", + "evidence": "per-behavior lines" + } + ], + "negative": [ + "an aborted after* write whose row is still readable is a durability FAIL" + ], + "automated": { + "kind": "unit", + "ref": "packages/objectql/src/validation/advisory-write-answer.test.ts + packages/objectql/src/engine-after-hook-abort-unit.test.ts + packages/objectql/src/engine-cascade-delete.test.ts + packages/objectql/src/engine-cascade-delete-tree.test.ts + packages/verify/src/delete-behavior-referenceless-tree.test.ts" + }, + "source": [ + "packages/objectql/src/engine.ts", + "packages/spec/src/data/object.zod.ts#materializeTreeSelfReferences", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22726-write-answer-warnings, 22782-objectql-after-hook-abort-rolls-back, 22782-spec-hook-onerror-abort-text, 22856-log-hook-unevaluable-condition, 22957-tree-delete-behavior, 23065-referenceless-tree, 23221-cascade-delete-hook-signal" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; records-forms.validation-rule-type-matrix still says the advisory is server-log only — that clause is stale against 22726 (reported, not edited here)", + "ref": "#23879" + } + ] + }, + { + "id": "records-forms.comment-reactions-and-edit-stamps", + "title": "Discussion on v18: reactions are their own records (sys_comment_reaction — 201 with user_id forced, duplicate 409, unreadable comment 403, no update, no deleting another member's), the retired sys_comment.reactions column is refused, a comment edit is platform-stamped, and milestone activity is withheld whole from a reader missing a watched field", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase record both personas can read, with one comment by the admin" + ] + }, + "steps": [ + "as the member POST /api/v1/data/sys_comment_reaction {comment_id, emoji, user_id: ADMIN_ID}; read user_id; POST again", + "POST a reaction to a comment on a record the member cannot read", + "PATCH the member's reaction; DELETE the admin's reaction as the member", + "PATCH /api/v1/data/sys_comment/ID with {reactions:[...]}; GET with filter on reactions", + "as the admin PATCH the comment body with is_edited:false, edited_at:\"2000-01-01\"; read back" + ], + "acceptance": [ + { + "clause": "reaction records, retired column, edit stamps and milestone redaction — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "a reaction answers 201 with user_id forced to the caller; a duplicate → 409 UNIQUE_VIOLATION; an unreadable comment → 403 RECORD_NOT_ACCESSIBLE; update and foreign delete refused", + "oracle": "api", + "verify": "status + code per call", + "evidence": "per-call lines" + }, + { + "clause": "sys_comment.reactions on write or filter → 400 INVALID_FIELD and is absent from reads", + "oracle": "api", + "verify": "status + read keys", + "evidence": "both answers" + }, + { + "clause": "a body edit sets is_edited:true and a server edited_at; client-sent values are dropped", + "oracle": "api", + "verify": "read-back vs sent", + "evidence": "the stored row" + } + ], + "traps": [ + "wrong-persona" + ], + "automated": { + "kind": "unit", + "ref": "packages/plugins/plugin-audit/src/objects/sys-comment-reaction.object.test.ts + packages/plugins/plugin-security/src/comment-reaction-own-record-floor.test.ts + packages/plugins/plugin-audit/src/comment-edit-stamp.integration.test.ts + packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts + packages/plugins/plugin-audit/src/activity-field-redaction.test.ts" + }, + "source": [ + "packages/plugins/plugin-audit/src/objects/sys-comment-reaction.object.ts", + "packages/plugins/plugin-audit/src/comment-access-hooks.ts#installCommentAccessHooks", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22566-comment-reaction-record, 22573-sys-comment-reactions-retired, 23124-comment-edit-stamp, 22771-milestone-activity-kind, 22786-withhold-milestone-row" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "records-forms.v18-semantic-defaults", + "title": "Defaults that changed meaning on v18 (ADR-0087 D3 18.* entries): an omitted list-view page size is 50 (was 25), an omitted editInline means editable in place, an autonumber with no `unique` is unique per organization, and a field-heavy form opens as a bounded modal on desktop", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "mixed", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot", + "org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as `member`, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a showcase list view with no pagination.pageSize and no userActions.editInline (GET /api/v1/meta/view lists them)", + "a showcase object with more than 50 rows (or seed scratch rows)", + "the console bundle for the browser legs — see knownGaps" + ], + "knownGaps": [ + "the browser clauses need the vendored console; at framework 94114c1f the pinned objectui (.objectui-sha daeb77fc) does not build against current spec (MISSING_EXPORT checkPageComponentViewData) — score those clauses blocked(environment) until the pin is bumped" + ] + }, + "steps": [ + "GET /api/v1/meta/view/VIEW and read the resolved pageSize / editInline; open the list in the console and count the first page", + "as a member who may update, open the list and confirm the inline-edit affordance; as a read-only member confirm it is absent", + "create two rows of an autonumber object in the same org and read their numbers", + "open New on a field-heavy object on a desktop viewport, then on a mobile viewport" + ], + "acceptance": [ + { + "clause": "the semantic defaults at the spec layer — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the first page of a list view with no pageSize shows 50 rows", + "oracle": "dom", + "verify": "count rendered rows after a screenshot confirms the list painted", + "evidence": "row count + one-line screenshot description" + }, + { + "clause": "inline edit is offered to an updater and absent for a read-only member with editInline omitted", + "oracle": "screenshot", + "verify": "per-persona affordance", + "evidence": "two one-line descriptions" + }, + { + "clause": "the heavy create form is a bounded modal on desktop and full-screen on mobile", + "oracle": "screenshot", + "verify": "surface shape per viewport", + "evidence": "two one-line descriptions" + } + ], + "traps": [ + "hydration-race", + "stale-console-bundle" + ], + "automated": { + "kind": "unit", + "ref": "packages/spec/src/data/field-autonumber-default-unique.test.ts + packages/spec/src/ui/view.test.ts + packages/spec/src/data/record-surface.test.ts" + }, + "source": [ + "packages/spec/src/data/record-surface.ts#deriveRecordFlowSurface", + "packages/spec/src/ui/view.zod.ts#UserActionsConfigSchema", + "ADR-0087", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22605-spec-list-view-edit-inline-default-on, 23734-heavy-flow-bounded-modal" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; D3 entries: view-pagination-page-size-default-50, list-view-edit-inline-default-on, autonumber-default-unique-organization", + "ref": "#23879" + } + ] } ] } \ No newline at end of file diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index 0b6bfdea53e..84319ec94ec 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -1573,6 +1573,236 @@ "history": [ { "revision": 1, "date": "2026-10-06", "change": "new — drafted with objectui#10202 against the measured run of its PR; filed by the seat. Authored from that draft with these changes: since v17.7, because 17.6.0 shipped before objectui PR 11669 merged; the picker clause split in two, one oracle each; the picklist fixture spelled with its export, its stack keys and the reload it needs; the console fixture naming the merge it must carry; the spec citations written as anchors the gate resolves", "ref": "#21957" } ] + }, + { + "id": "studio-authoring.meta-save-preconditions-and-draft-rows", + "title": "Metadata save preconditions on v18: the item read serves its `version` and sending it back as If-Match saves, `If-None-Match: *` refuses an existing item 409 METADATA_CONFLICT with currentVersion, any other If-None-Match is 400, pending-draft rows carry the draft's label, and every stored row records its acting user", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "a scratch view (view is overlay-allowed on stock showcase; a view name is dotted OBJECT.NAME): copy the body of GET /api/v1/meta/view/showcase_task.in_progress (item, minus underscore keys) to showcase_task.qa_occ_view", + "the token lives on the DRAFT read: the plain read is the cached published-value branch and serves no `version` (22114 says so), so drive the precondition legs on a draft row (?mode=draft save, ?state=draft read)" + ] + }, + "steps": [ + "PUT /api/v1/meta/view/showcase_task.qa_occ_view?mode=draft; GET it with ?state=draft and read version; PUT ?mode=draft with If-Match: VERSION (200); PUT again with the same, now stale, version (409)", + "PUT /api/v1/meta/view/showcase_task.qa_occ_view (active) with If-None-Match: * once the row exists", + "PUT with If-None-Match: \"abc\", and with both If-Match and If-None-Match", + "PUT with ?mode=draft a label change; GET /api/v1/meta/_drafts and read the row's label", + "read the stored row's actor (GET /api/v1/meta/view/showcase_task.qa_occ_view/history → events[].actor); delete the scratch view" + ], + "acceptance": [ + { + "clause": "version-token OCC, create-only precondition, draft rows and row actor — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "the ?state=draft read serves `version`; If-Match with it saves; a stale one → 409; If-None-Match:* on an existing item → 409 METADATA_CONFLICT with currentVersion; any other If-None-Match (or one beside If-Match) → 400 and nothing written", + "oracle": "api", + "verify": "status + code per call; version before/after", + "evidence": "per-call lines" + }, + { + "clause": "the pending-drafts row carries the draft body's label, never the machine name", + "oracle": "api", + "verify": "label field", + "evidence": "the row" + }, + { + "clause": "created_by / updated_by on the stored row name the acting admin", + "oracle": "api", + "verify": "history entry actor fields", + "evidence": "the entry" + } + ], + "traps": [ + "cache-staleness" + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/meta-item-version-token-occ.test.ts + packages/rest/src/meta-draft-head-package-inheritance.test.ts + packages/runtime/src/domains/meta-save-preconditions-parity.test.ts + packages/objectql/src/sys-metadata-repository-list-drafts.test.ts + packages/objectql/src/protocol-metadata-row-actor.test.ts" + }, + "source": [ + "packages/rest/src/meta-save-request.ts#metaSaveRequestOptions", + "packages/spec/src/api/protocol.zod.ts#MetadataConflictErrorSchema", + "packages/metadata-protocol/src/sys-metadata-repository.ts", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 22114-meta-read-version-token, 22141-rest-meta-save-request-options, 22141-runtime-meta-save-preconditions, 22188-rest-meta-package-all-fold, 22188-runtime-meta-package-all-fold, 22200-drafts-header-label, 23033-metadata-row-actor, 23086-actor-family, 23068-migrate-actor" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] + }, + { + "id": "studio-authoring.multi-package-item-rows", + "title": "One item name, one stored row PER PACKAGE (ADR-0048) on v18: a package-less save resolves its owning package and an ambiguous one is 409, a dropStorage delete while another package's row remains is 409 and keeps the table, a save into the wrong package is 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, a sealed packaged name answers one 403 NOT_OVERRIDABLE sentence, and revert discards a Studio package's drafts", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "two scratch Studio-authored packages A and B (POST /api/v1/packages), each saving a view named showcase_task.qa_shared_view (?package=A / ?package=B)" + ] + }, + "steps": [ + "PUT /api/v1/meta/view/showcase_task.qa_shared_view with no ?package= — expect 409 (two holders)", + "DELETE /api/v1/meta/view/showcase_task.qa_shared_view?package=A; then re-save without ?package= — expect it to update B's row", + "PUT /api/v1/meta/object/showcase_task?package=A (a package that does not own it)", + "PUT a sealed packaged name (any showcase object) as a create; read the 403 sentence", + "publish package A, edit to a draft, POST /api/v1/packages/A/revert; then revert a never-published package" + ], + "acceptance": [ + { + "clause": "multi-package row addressing, drop-storage and owner-mismatch refusals, the sealed sentence and stored-package revert — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "an ambiguous package-less save or delete → 409 RESOURCE_CONFLICT and nothing changes; once one holder remains the package-less save updates THAT row (no second row)", + "oracle": "api", + "verify": "status + GET /meta/view/showcase_task.qa_shared_view/layers before/after", + "evidence": "answers + layer lists" + }, + { + "clause": "a save into a package that does not own the object → 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, nothing stored", + "oracle": "api", + "verify": "status + code; read-back", + "evidence": "refusal + read-back" + }, + { + "clause": "a sealed packaged name answers 403 NOT_OVERRIDABLE with ONE sentence naming TYPE/NAME as managed and the list door", + "oracle": "api", + "verify": "error.message is a single sentence", + "evidence": "the body" + }, + { + "clause": "revert serves the published body and removes a draft-only item; a never-published package → 409 RESOURCE_CONFLICT", + "oracle": "api", + "verify": "item reads + status", + "evidence": "reads + refusal" + } + ], + "traps": [ + "destructive-in-place" + ], + "automated": { + "kind": "unit", + "ref": "packages/rest/src/meta-delete-drop-storage-other-rows.test.ts + packages/runtime/src/package-uninstall-shared-object.integration.test.ts + packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts + packages/objectql/src/protocol-sentinel-owner-package-save-refusal.test.ts + packages/runtime/src/package-promote-runtime-owner-refusal.integration.test.ts + packages/rest/src/rest-meta-sealed-first-draft-remedy.test.ts + packages/metadata-protocol/src/protocol.packageless-save-address.test.ts + packages/metadata-protocol/src/protocol.sealed-remedy-kind.test.ts + packages/runtime/src/package-revert-stored-members.integration.test.ts" + }, + "source": [ + "packages/metadata-protocol/src/protocol.ts", + "packages/metadata-protocol/src/sys-metadata-repository.ts", + "ADR-0048", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23297-package-axis-delete-draft, 23415-drop-storage-other-rows, 23473-uninstall-shared-object-refusal, 23416-runtime-owner-overlay-refusal, 23514-sentinel-owner-save, 23630-promote-doors-d99, 23495-draft-doors-one-address, 23657-draft-save-one-package-draft, 23715-strict-draft-read-address, 23577-sealed-write-one-sentence, 23165-direct-save-resolves-package, 22591-sealed-remedy-kind, 22090-metadata-protocol-revert-stored-package, 22090-runtime-revert-studio-package, 22113-revert-package-code-shipped-members, 23505-extension-refusal-409" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it; the sealed-name sentence was rewritten twice on the line (22591 → 23406 → 23577): assert the 23577 one-sentence form", + "ref": "#23879" + } + ] + }, + { + "id": "studio-authoring.save-door-runs-authoring-rules", + "title": "The metadata save door runs the build's authoring rules on v18 — a page with an unknown component prop, a page with both details and tabs slots, a second default record page, a chart list view with no dataset, an object predicate that does not compile, a `protection` block or a lock change, and a non-http(s) URL target are each refused 4xx at PUT (drafts still admitted where the rule says so)", + "since": "v18", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", + "scratch names only (qa_*) so nothing shipped is touched; view/page are written on stock showcase, object writes need the scratch app or OS_METADATA_WRITABLE on a scratch boot" + ] + }, + "steps": [ + "PUT /api/v1/meta/page/qa_props_page with a page:header block carrying properties.titel", + "PUT a record page with both slots.details and slots.tabs", + "PUT a second isDefault:true record page for showcase_task", + "PUT /api/v1/meta/view/showcase_task.qa_chart_view as a chart list view with no dataset binding", + "PUT an object with an option visibleWhen reading `parent` and a validation condition `sqrt(record.amount)`; then the same with ?mode=draft", + "PUT any item with a `protection` block; PUT with _lock:\"full\" on an unlocked item", + "PUT /api/v1/meta/action/qa_bad_url with target `javascript:alert(1)`" + ], + "acceptance": [ + { + "clause": "the runtime authoring gate on the page, view, object and action save doors — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", + "oracle": "test", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "evidence": "the vitest summary line per pin file, with the package name and the subject sha" + }, + { + "clause": "each refused body answers 422 INVALID_METADATA (400 VALIDATION_ERROR for protection / _lock*, 422 DEFAULT_RECORD_PAGE_CONFLICT naming both pages) with the rule id and located path, and a read-back shows nothing stored", + "oracle": "api", + "verify": "status + code + rule id + path per PUT; GET afterwards", + "evidence": "one line per PUT" + }, + { + "clause": "component-props refusal offers the did-you-mean (titel → title)", + "oracle": "api", + "verify": "message text", + "evidence": "the body" + }, + { + "clause": "a draft save (?mode=draft) of the bad object predicate is admitted and its publish is refused", + "oracle": "api", + "verify": "draft status then publish status", + "evidence": "both answers" + } + ], + "negative": [ + "a refused body that is nevertheless readable afterwards (stored despite the 4xx) is a FAIL" + ], + "automated": { + "kind": "unit", + "ref": "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts + packages/metadata-protocol/src/protocol.save-door-protection.test.ts + packages/metadata-protocol/src/protocol.save-door-lock-family.test.ts + packages/objectql/src/default-record-page-save-door.test.ts + packages/spec/src/ui/page-slot-pair.test.ts + packages/rest/src/meta-view-chart-binding.test.ts + packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts + packages/lint/src/validate-component-props-url-target.test.ts" + }, + "source": [ + "packages/metadata-protocol/src/runtime-authoring-gate.ts", + "packages/metadata-protocol/src/protocol.ts#protectionBlockRefusal", + "packages/spec/src/ui/page.zod.ts#checkPageSlotPair", + "packages/spec/src/ui/view.zod.ts#checkListViewChartBinding", + "packages/lint/src/validate-expressions.ts#validateStackExpressions", + "v18 changesets (.changeset/pre/, consumed into 18.0.0-next.*): 23388-page-component-rules-runtime-door, 23357-page-component-view-data-api-refused, 23332-page-action-row-operation, 23427-page-action-row-declarative-update-rules, 23096-save-door-protection, 23119-save-door-lock-envelope, 23015-one-default-record-page-per-object, 22568-page-slots-details-beside-tabs-refused, 22491-chart-list-view-binds-a-dataset, 22157-option-visible-when-parent, 22274-option-visible-when-members, 22394-option-visible-when-evaluser-members, 22019-object-save-door-formula-verdict, 22019-objectql-formula-fault-log, 22032-object-save-door-action-predicates, 22032-object-save-door-field-rule-slots, 22032-object-save-door-option-visible-when, 22032-object-save-door-validation-predicates, 22042-nested-validation-predicate-verdict, 23010-url-action-target-scheme-set, 23077-url-target-doors-scheme-set, 23103-page-action-row-on-success-and-executor, 23168-page-action-row-name-leg-url-target, 23168-page-action-row-on-success-beside-executor" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-16", + "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", + "ref": "#23879" + } + ] } ] }