diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index c58a282446b..3b24c15df73 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -2273,6 +2273,76 @@ "ref": "#24850" } ] + }, + { + "id": "cli.fresh-install-migrate-apply-first", + "title": "A fresh install whose first command is `os migrate apply` boots on its first `os serve` (18.1.0): the apply creates the tables on an empty database and writes the organization-ownership birth marker (the sys_migration row `adr-0131-organization-ownership`), so the serve reaches its serving line with no ownership refusal", + "since": "v18.1", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "cli", + "personas": [ + "operator (local shell) whose deploy script runs `os migrate apply` before the first `os serve` of a fresh install" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "an EMPTY database: a file DB path under /tmp/RUN/ that does not exist yet — `test ! -e` on it and on its `-wal` and `-shm` before steps[0]. ⛔ Never a file an earlier command touched: `os dev`, `os serve` and `os migrate plan` each create tables in it, and a database that held a declared table before the apply was not created from empty, so it gets no birth marker by design", + "one environment for both commands, as a deploy script has: OS_DATABASE_URL=file:/tmp/RUN/fresh.db and OS_HOME=/tmp/RUN/home, plus OS_AUTH_SECRET and OS_SECRET_KEY (32-byte hex), because a production `os serve` composes the auth family only when an auth secret resolves and refuses to boot with no stable encryption key (RUNNER environment facts, the walled-boot entry). `os migrate apply` composes what `serve` mounts (packages/cli/src/utils/schema-migrate.ts header), so it gets the same environment. Add OS_PORT=PORT for the boot (RUNNER environment facts: the showcase's self-pinging connectors read it, `-p` alone does not set it)", + "a showcase directory no other runner boots: your own worktree, or a scratch copy. Both commands load its objectstack.config.ts and its compiled dist/objectstack.json, and both write .objectstack/data/ (the showcase_external fixture, local uploads) into the directory they run in. Delete that .objectstack after the run" + ], + "knownGaps": [ + "18.0.0 does not carry the fix. There `os migrate apply` on an empty database creates the tables and writes no birth marker, and the first `os serve` refuses the database as one 17.x or an 18.0 pre-release created: the refusal platform-core.v18-boot-refusals acceptance[1] reads, opening \"This database carries no organization-ownership marker\". A run whose subject is 18.0.0 records that refusal as 18.0.0's known behaviour, never as a FAIL of this item. The fix (#24827) is .changeset/24827-migrate-apply-birth-marker.md, a minor changeset pending on main, so it ships in 18.1.0. Read at base 688052e147: the fix commit 19484af538 is not an ancestor of the @objectstack/cli@18.0.0 tag commit 06cab4849a (exit 1; control leg, a commit 50 behind the tag, exit 0)", + "the fix's pins score no clause here (RUNNER rule 6, a pin scores only the layer it drives): packages/cli/src/commands/migrate/apply.organization-ownership-birth.integration.test.ts runs `MigrateApply` in-process and judges the guard through `assertOrganizationOwnershipMigrated` on a boot that creates nothing, and packages/cli/src/utils/schema-migrate.birth-after-flush.test.ts holds the failed-write refusal with no kernel at all. Neither spawns the CLI, so neither answers acceptance[0]'s exit code, and neither boots `os serve`, so neither reaches acceptance[1]'s serving line. The item carries no automated ref and both clauses are driven live", + "a Turso remote (`libsql://`) primary is outside this item: `os migrate apply` creates no tables there (its schema sync is skipped), so the first serving boot creates them and writes the birth marker itself, as it did before the fix" + ] + }, + "steps": [ + "in the showcase directory, with the environment of fixtures.requires[1] exported and the DB path absent: `os migrate apply --yes; echo $?`. Keep the full output and the exit code", + "before any boot, read the marker through SQLite (node:sqlite, so a `-wal` is read too; there is no sqlite3 CLI here): `SELECT id, verified_at, applied_at, blocking, advisory, details FROM sys_migration WHERE id = 'adr-0131-organization-ownership'`", + "the first boot, in the same environment: `os serve -p PORT`, started with `setsid` and its output kept in a file (RUNNER environment facts: stop its process group, never the pnpm pid). Poll GET /api/v1/health until it answers 200 or the process exits, then read the output", + "read the marker row again with the steps[1] query, then stop the server's process group and confirm the port is free" + ], + "acceptance": [ + { + "clause": "`os migrate apply --yes` on an empty database creates the tables, exits 0, and writes the organization-ownership birth marker: exactly one sys_migration row `adr-0131-organization-ownership`, verified, whose details carry the birth at this release's ownership shape generation. The apply wrote it, before any boot", + "oracle": "build", + "verify": "steps[0] exits 0 and its output reports the tables it created. steps[1] returns exactly one row with verified_at set, applied_at null, blocking 0, and details.birth.shapeGeneration equal to ORGANIZATION_OWNERSHIP_SHAPE_GENERATION (packages/spec/src/system/migration.zod.ts#ORGANIZATION_OWNERSHIP_SHAPE_GENERATION, 1 at base 688052e147). The apply's output also prints `[organization-ownership] a datastore created from empty: birth marker written (shape generation N, platform V).` (packages/runtime/src/organization-ownership-boot-guard.ts#recordOrganizationOwnershipBirth); that line is supporting evidence only, the row is the oracle. steps[3] reads the same single row unchanged, so the boot found the apply's marker and wrote none of its own", + "evidence": "the apply's exit code and its created-tables line, and both reads of the marker row" + }, + { + "clause": "the first `os serve` over that database boots: it reaches its serving line, with no organization-ownership refusal", + "oracle": "log", + "verify": "steps[2]'s output carries `✓ Server is ready` (packages/cli/src/utils/format.ts#printServerReady), not its DEGRADED variant, and GET /api/v1/health answers 200 while the process runs. The output has 0 hits for `carries no organization-ownership marker`, for `refuses to start` and for `os migrate organization-ownership`. A boot that exits 1 with that refusal is a FAIL on 18.1.0 or later, and 18.0.0's known behaviour (fixtures.knownGaps[0])", + "evidence": "the serving line and the boot's Mode row, the health status, and the three refusal greps with their counts" + } + ], + "negative": [ + "on a release that carries the fix, a fresh install whose first `os serve` after `os migrate apply` is refused as an earlier release's database is the defect #24827 fixed: FAIL", + "the other side of the birth decision, a database the apply did not create from empty, must get NO marker and stay refused until the ceremony runs. It is not this item's fixture: the fix's pin holds it (apply.organization-ownership-birth.integration.test.ts, case b), and the refusal itself is platform-core.v18-boot-refusals" + ], + "traps": [ + "destructive-in-place", + "first-boot-cold-start" + ], + "source": [ + ".changeset/24827-migrate-apply-birth-marker.md (minor, pending on main at base 688052e147: ships in 18.1.0)", + "packages/cli/src/utils/schema-migrate.ts#recordBirthOfFlushedStore", + "packages/runtime/src/organization-ownership-boot-guard.ts#recordOrganizationOwnershipBirth", + "packages/runtime/src/organization-ownership-boot-guard.ts#ORGANIZATION_OWNERSHIP_BOOT_REFUSAL_CODE", + "packages/cli/src/commands/migrate/apply.organization-ownership-birth.integration.test.ts", + "docs/adr/0131-total-organization-ownership-no-null-organization-id.md", + "#24827 (the fixed p1), #25054 (the checklist gap: QA run #24838, round 16 of wave anchor #24579)" + ], + "history": [ + { + "revision": 1, + "date": "2026-10-21", + "change": "initial, for #25054: no checklist clause drove the fresh-install order #24827 fixed (QA run #24838, 18.0 sweep round 4; confirmed in round 16 of wave anchor #24579). A new item, because no existing item's scope covers a fresh install's first `os serve` after `os migrate apply`: platform-core.v18-boot-refusals scopes a 17.x-shaped database (its revision 6 says the order is not added there), cli.organization-ownership-ceremony the upgrade of a database 17.x or an 18.0 pre-release created, cli.migrate-plan-apply-json an apply on a database a prior `os dev` boot already synced (its fixtures.requires[0]), platform-core.boot-health and cli.dev-boot-contract the `os dev` boot, which creates the tables and writes the birth marker itself, and cli.scaffold-first-run the published scaffold booted by `os start --artifact`, with no `os migrate apply` in it. First evidence, driven live at base 688052e147 (the showcase in this PR's worktree, its own file DB, OS_PORT 41873): `os migrate apply --yes` exited 0, printed '✓ Created/extended 107 table(s)' and '[organization-ownership] a datastore created from empty: birth marker written (shape generation 1, platform 18.0.0).'; the marker row read verified_at 2026-10-21T06:54:23.840Z, applied_at null, blocking 0, advisory 0, details birth shapeGeneration 1, platformVersion 18.0.0; the first `os serve -p 41873` printed '✓ Server is ready' with Mode production, /api/v1/health and /api/v1/ready answered 200 (health 10 s after the start), the three refusal greps counted 0, and the row read back unchanged after the boot", + "ref": "#25075" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index f675fd1fc18..8f6653f6ba3 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -1919,7 +1919,7 @@ "title": "All six validation-rule types enforce on the write path with their exact per-type error codes — and advisory severities flag without blocking", "since": "v15", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1933,8 +1933,7 @@ "for the UNEVALUABLE legs (negative[0], negative[1], acceptance[5]): two runtime-authored scratch objects, each carrying one script rule whose condition `int(record.title) > 1` cannot be evaluated over a non-numeric title — no seeded rule is unevaluable, so the stock doors author them. As admin: PUT /api/v1/meta/object/qa_vr_probe {name: qa_vr_probe, label: QA, sharingModel: public_read_write, fields: {title: {type: text}}, validations: [{type: script, name: qa_unevaluable_warn, severity: warning, message: x, condition: \"int(record.title) > 1\"}]} → 200, and the same body as qa_vr_probe_err with the rule named qa_unevaluable_err at severity: error → 200 (the recipe of QA run #24779 / #24782). A write with title \"abc\" then drives each rule" ], "knownGaps": [ - "no showcase rule declares severity:'info' — it shares the identical non-error branch with 'warning' (rule-validator.ts tests `severity === 'error'` and logs everything else), so the warning leg is the driven proof; author a scratch info rule only if a distinct observation is wanted, and record which severities were actually driven", - "acceptance[5] stays red until #24782 lands: the unevaluable warning rule's write answers 201 and persists (correct), but the server log prints two lines naming the rule that both say \"write rejected\" — the predicate evaluator's fail-closed sentence, emitted before severity is consulted, and the advisory branch's warn, which embeds that violation message (packages/objectql/src/validation/rule-validator.ts). Measured at 580fd4ed (QA run #24779) and again at a43405d596 (#24802). When #24782's fix merges, re-run the clause and drop this line" + "no showcase rule declares severity:'info' — it shares the identical non-error branch with 'warning' (rule-validator.ts tests `severity === 'error'` and logs everything else), so the warning leg is the driven proof; author a scratch info rule only if a distinct observation is wanted, and record which severities were actually driven" ] }, "variants": [ @@ -1987,7 +1986,7 @@ "evidence": "response + re-read + the captured log line" }, { - "clause": "an UNEVALUABLE severity:'warning' rule lets the write land, and no log line beside it reports a rejection: the write answers 2xx, the row persists, and no server-log line naming the rule says the write was rejected or that the rule failed closed — the log must not state the opposite of what the server did. Red until #24782 lands", + "clause": "an UNEVALUABLE severity:'warning' rule lets the write land, and no log line beside it reports a rejection: the write answers 2xx, the row persists, and no server-log line naming the rule says the write was rejected or that the rule failed closed — the log must not state the opposite of what the server did.", "oracle": "log", "verify": "step-7 qa_vr_probe write: status 201 and a re-read finding the row; then every log entry naming qa_unevaluable_warn, read to its end — the predicate evaluator's entry spans lines (it embeds the CEL source with a caret) and its verdict sits after the caret — none may contain \"write rejected\" or \"fails closed\" (the fail-closed sentence belongs to the error-severity path; rule-validator.ts header: an unevaluable warning/info rule is logged, never thrown). Score the absence of the rejection wording, not whichever replacement sentence #24782 picks; a line saying the rule could not be evaluated is expected and passes. Control: the qa_vr_probe_err write answers 400 VALIDATION_FAILED naming qa_unevaluable_err and persists nothing — there the rejection wording is true", "evidence": "both responses + both re-reads + every log line naming either rule, quoted" @@ -2045,6 +2044,12 @@ "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" + }, + { + "revision": 7, + "date": "2026-10-21", + "change": "fixtures.knownGaps[1] removed and acceptance[5].clause's closing 'Red until #24782 lands' dropped, as knownGaps[1] said to do once #24782's fix merged: PR #25051 (72518d20f5), named by the domain:engine pointer 6308955528 on seat post #6023. Verification re-run of acceptance[5] at base 688052e147, steps[6] on the canonical own-port boot (`objectstack dev --ui --seed-admin -p 41874 -d file:SCRATCH/data.db`, OS_PORT exported): the qa_vr_probe write {title: \"abc\"} answered 201 and the re-read found its one row; exactly one server-log line names qa_unevaluable_warn, \"Validation rule 'qa_unevaluable_warn' (warning): the rule could not be evaluated (runtime: int() type error: cannot convert to int) — write proceeded: the rule is advisory, so it was not applied to this write\", and it carries neither 'write rejected' nor 'fails closed'. Control: the qa_vr_probe_err write answered 400 VALIDATION_FAILED, fields[0].constraint {rule: qa_unevaluable_err, reason: unevaluable}, its re-read found no row, and its log lines say the write was rejected. No other clause changed", + "ref": "#25075" } ] },