0.10

Every published OKModel release, newest first.

all series ยท 0.9

v0.10.0

๐Ÿ’ฅ Breaking Changes

  • DriverCapabilities no longer includes prepared or describe. Prepared-statement mode stays a connect option. A driver that can describe a statement still exposes pool.describe.
  • hidden({ filterable }) accepts only a literal true. false and a boolean variable are type errors. Call hidden() with no argument. Passing false from JavaScript still leaves the column hidden and not filterable.
  • okm migrate plan --json finding reason is an untrusted object. okm migrate baseline --json unmanaged name is an untrusted object. okm migrate status JSON id, kind, and state are untrusted objects. A database error's text is errors[].detail. summary stays a registry sentence, or A database error was returned (<kind>). when the code has no page.
  • db.batch of a versioned update throws OKM1121 before any statement; move that update into tx(). tx.batch is unchanged, and version: "any" stays allowed.
  • Bound parameters stay off an error cause unless includeValues is set. A page cursor for a hidden or sensitive orderBy carries only the primary key (a gone row is OKM1130), and OkmError is branded so instanceof matches across two copies of the class (#541).

behaviour changes

  • Indexes on a tenant table lead with the tenant key (indexes: true, the default). A unique index is unique per tenant, so two tenants may share a value. tenantIndexes: false turns the prefix off for one table. .global("reason") opts one index out (an empty reason is refused) and .scoped() opts one index in. The nearest level wins. An index that already leads with the key is unchanged, and the next okm generate writes nothing. An index that does not is replaced once: CREATE INDEX CONCURRENTLY for the new index, then DROP INDEX CONCURRENTLY for the old one. That is not a blocking rebuild, and okm migrate check accepts it (Q3, D306, #390). OKM1706 stays when prefixing is off.
  • With validation: true, an unknown key on insert, update, upsert / onConflict, first-or-create, insert.validate, insert.check, insert.pick(...).validate, insert.pick(...).check, insert.omit(...).validate, insert.omit(...).check, update.validate, or insert["~standard"].validate is OKM1120 and names the key and the table (R1, D304). A typo-only update names the key instead of saying "update set is empty." insert.check, pick(...).check, and omit(...).check return rule issues and do not throw OKM1200 for a failed rule. An unknown key throws OKM1120 on those calls too, and the rule issues are lost. The Standard Schema validate throws that OKM1120. It does not return { issues } for an unknown key. A failed rule on that surface still returns { issues }.
  • On a versioned() table, an update with version in set is OKM1120 (was OKM1190). A version string other than "any", such as "2", is OKM1120 (was OKM1121). The message says it must be an integer or "any". Insert of version stays OKM1190.
  • okm generate exits 1 when an error finding exists; the migration file is still written; stderr says okm migrate apply will refuse until it is fixed (Q8, #395). A warning-only finding stays exit 0.
  • okm pull and okm import drizzle write relative imports with the .ts extension so Node type stripping and bunx okm can load the schema (R4, #401). tsc needs allowImportingTsExtensions with noEmit, or rewriteRelativeImportExtensions.
  • An update on a versioned() table without version is a TypeScript error; the runtime OKM1120 stays when types are bypassed (Q5, D305, #392). Non-versioned tables are unchanged.
  • okm exit codes follow the outcome. 0 is done or nothing to do. 1 is a command that ran and failed. 2 is usage or configuration (unknown flag, missing config, a Node older than 22.18). 3 is a decision the command will not ask (OKM1530, and a committed step that needs a person: OKM1553, OKM1555). 4 is a guard (OKM1850, OKM1852, OKM1855, OKM1859). 5 is a target that cannot be reached or a refused feature (OKM1802, OKM1803, OKM1811, OKM1812, OKM1843, OKM1844, OKM1846, OKM1854, OKM1860, and a lost connection). Every other code, including OKM1401, planner OKM1541, OKM1554, OKM1556, OKM1851, OKM1857, and OKM1858, stays 1. A script that only checked non-zero still fails. A script that treated every failure as 1 now sees 2 through 5.
  • okm migrate plan --json, okm migrate baseline --json, and okm catalog export --json keep their previous fields inside the okm.cli/1 envelope data. parsePlanJson reads data and still accepts a bare plan document.
  • okm build and okm dev reject an unknown flag. They used to leave the rest of the line unread.
  • A derived name longer than 63 bytes (index, constraint, sequence, check, trigger) keeps the same shape, a UTF-8 prefix, _, and 8 hex characters, but the suffix is now the 32-bit FNV-1a hash of the full name, not the first 8 characters of its SHA-256. The stored name of every such identifier changes once, so the next okm generate plans a change for it. Names that fit in 63 bytes are unchanged. Two long names that shorten to the same stored name stay OKM1023 (duplicate identity) and are never merged. The featureless app startup graph drops 2,415 minified bytes because sha256.ts leaves it (90,763 to 88,348).
  • schema() resolves every cased name once: snake splits an acronym (userID is user_id), casing: "camel" is accepted, any other casing is OKM1060, and two fields that spell one SQL name are OKM1023. A drop and add that are one casing of each other is OKM1530 and names .sqlName() and .renamedFrom(), and a column rename also renames its identity sequence (#535, #548).
  • Under composite tenancy, index().unique() leads with every tenant key, an ordinary index still leads with the first, a key already named moves to the front once, and two indexes that become the same one are OKM1023. tenantIndexes on a global table, or a tenant-index mark when the schema has no tenancy, is OKM1060, and OKM1706 on a using index names index().global("reason") (#546).
  • A table named capabilities is OKM1023, because db.capabilities is reserved on the root client, a scoped client, and inside tx() (#546).
  • find, one, and page reject a plain hidden key, an unknown key, a wrong filterable value, a bad or() branch, and a hidden orderBy, including inside include, and page rejects an unknown option and relevance() (OKM1120). The same plain hidden key and a bad or() are type errors on count, exists, update, and delete, while a filterable hidden column stays valid in where and orderBy (#549, #550).

โœจ Added

contracts

  • DescribeResult adds parameters (type OIDs in $n order) and fields (name, typeOid, tableOid, attnum). columns and parameterCount stay.
  • ExecuteResult.reported is the driver command-tag count, or null when the driver reported none. Absent on fixtures that only set count. The caller-facing count stays reported ?? rows.length.

adapters

  • postgres.js and PGlite describe return those fields and do not execute the statement. node-postgres, Bun.sql, and Neon still leave describe unset.
  • resultFrom records the command tag on ExecuteResult.reported. postgres.js and Bun.sql use count. pg and Neon use rowCount. PGlite uses per-statement rowCount, and cumulative affectedRows only when rowCount is absent. An own field that is not a finite number is nothing; a later field is not a fallback.

runtime

  • A connected client exposes capabilities, the same frozen object as the pool (transactions, stream, listen, cancel).
  • update({ where, set, limit, lock: "skip", version: "any" }) on a versioned() table skips the version check, bumps version by 1, and returns the claimed rows. A miss is an empty array. version stays required on every other versioned update. version: "any" without lock: "skip" and limit, an unrecognised version string, and a version key in set are OKM1120 (Q6, O-5, #393).
  • connect({ onWrite }) observes write units (okm.observe/1): buffered write, savepoint, and tri-state outcome events after commit or rollback, from one emission point shared with hookm.onTransaction. Normative shape in docs/observer-events.md. db.flush() waits for the okmodel/observe helper (app size gate).
  • okmodel/observe exports writeObserver(), flush(), OBSERVE_BUFFER_MAX (10,000), and observerBufferOverflow() (OKM2200). The helper buffers per unit, calls exactly one of committed | rolledBack | unknown, isolates throwing and rejecting handlers via onObserverError, and settles tracked promises on flush(). Overflow drops the oldest unit without blocking the write path.
  • writeObserver({ keys }) names schema tables. Those writes append primary-key RETURNING when the statement is a simple insert, update, or delete, and put the values on WriteEvent.keys. sql() and inspect() show the extra clause. describe() labels the appended columns "observer". A table with no primary key, or a name missing from the schema, is OKM1890 at connect and at plan time. Nested RETURNING (parent guard, versioned close, archive) is not rewritten. A caller result is unchanged: observer-only rows are hidden.
  • Write events take rowCount from the command tag. Mode is changed for insert, update, and delete, returned when RETURNING ran, and unknown with rowCount: null when the driver gave no tag. An upsert stays changed and counts inserted plus updated rows.
  • find({ offset }) skips that many rows. The value is an integer from 0 up and is sent as a parameter.
  • okmodel/warnings exports warnings(). Pass it to connect({ warnings }). In development, OKM2004 (offset past 1,000), OKM2005 (a limit whose order does not end in a unique key), OKM2007 (a value list past 500), and OKM2008 (an include whose row bound passes 10,000) each report once per call site. Production reports nothing until a level is set. ignore needs a reason. onWarning replaces the console.
  • raw(db, sql, params) from okmodel/pg runs a typed raw statement. The overload is chosen when the second argument is a string; raw(handle, cls, "read" | "write", fragment) is unchanged. RawStatements (an empty interface, filled by .okm/raw.d.ts) types the params and rows, and a literal with no entry is a type error that says to run okm raw sync. The runtime looks the statement up by rawStatementKey in .okm/raw.lock.json (read on the first typed call, under catalogDir), encodes parameters and decodes rows with the codecs the typed API uses, and checks the parameter count (OKM1602). A statement missing from the lock, a lock that does not parse, or a result whose columns no longer match is OKM1601. A runtime with no file system calls registerRawLock(lock) once. A write statement runs on the primary and emits an onWrite event with source: "raw", op: "raw", table: null. onQuery sees reads and writes as op: "raw". Inside tx() both kinds use the transaction connection. The runtime is a lazy chunk: the featureless app and the runtime entry gain no bytes (F7-3, #469).
  • trusted(client, "reason") from okmodel/pg lifts the tenant check for typed raw on a for() client. The reason is required and shows as the observer tag. Without it, typed raw on a tenant client is OKM1702. Raw SQL has no archive filter and no hidden-column guard.
  • connect({ onQuery }) delivers one QueryEvent per statement, reads and writes, after the statement settles. With no observer the client does not build an event and does not read the clock. queryObserver() isolates a throwing or rejecting handler, waits on flush(), and overflows in-flight promises with OKM2200. params: true and stack: true opt in; sensitive, hidden, and tenant values are [redacted]. toOtel(event) returns { name, attributes } with the same db.* names as okmodel/otel, plus db.response.status_code (the SQLSTATE) on failure. It imports nothing from OpenTelemetry and never emits parameters or tenant ids. db.query.text is included only when statement: true.
  • writeObserver({ tag }) and queryObserver({ tag }) copy a tag onto each event. A tag is 1โ€“64 characters from [A-Za-z0-9_.:/-]. Anything else is invalid when the helper is built.
  • okmodel/session exports sessionSettings({ key: { name, type, required? } }). Types are text, uuid, int, and bool. connect(url, { schema, session }) adds db.with({ ...values }), a typed client that sets those values with one select set_config($1, $2, true), ... statement as the first statement of the transaction a unit runs in, before any savepoint. is_local is always true, so a transaction-mode pooler cannot hand a value to the next client. A nested tx() never sets again, a read uses BEGIN READ ONLY on its routed endpoint, and batch() starts with the statement. with() composes with for() and unscoped() in any order, later with() calls add to earlier ones, and different values in one transaction set again before they run (D241, D312). A name needs a dot and cannot start with app.tenant or app.unscoped or be a built-in setting. A value that is not its declared type, an unknown key, a required key that is missing, and a client that never called with() when a key is required are OKM1709 before any statement. Read the value in SQL as NULLIF(current_setting('app.user_id', true), '') before a cast.
  • rlsTenancy() and sessionSettings() share one scope pool wrapper (src/runtime/tenancy/scope-pool.ts). A queued set_config marks its scope as set, so the first statement of a for() transaction no longer sets it a second time. A rollback to a savepoint forgets the scope only when a set ran inside that savepoint. for() and with() reopen the client on the pool under every wrapper, so each composition wraps it once.
  • aggregate({ countDistinct: [fields] }) counts the different non-null values of each field per group and returns them as countDistinct.<field>. A group with only NULLs, and the one total row of an empty table, count 0. It takes the column types min and max take (D309, #506).
  • find({ distinct: true, select }) keeps one row per different combination of the selected fields, and NULL is one value. find({ distinctOn: [fields], orderBy }) is Postgres only and keeps the first row of each key; orderBy must start with the same keys. Neither takes include or search, and page and one refuse them (D310, #507).
  • aggregate({ select: { name: { sum: "field" } } }) gives each aggregate its own flat, typed result key. An entry is one of count: true, countDistinct, sum, avg, min, or max, so one call can hold several aggregates of the same function and column. The list forms keep working beside it. A name that is a group field, a bucket as, a list-form key, or a prototype key is OKM1120 (D311, #508).
  • aggregate({ groupBy, having }) keeps the groups whose aggregates match. It is the where predicate form over count and the names in select: comparisons, between, inList, notIn, not, or(), and the text patterns on a text min or max. A NULL aggregate matches no comparison. having without groupBy, an unknown name, or another operator is refused before any SQL (D313, #509).
  • aggregate({ groupBy, orderBy }) orders groups by count and by the names in select, besides group fields and bucket names, each as asc, desc, or { dir, nulls }. A min or max of a timestamp sorts as a timestamp. An unknown name is OKM1120 and lists the accepted names. Every other read that orders (find, one, page, stream, include, window helpers) already took { dir, nulls }; they now have tests with NULL rows (D314, #510).
  • connect({ limits: { find, include, page, groupBy, max } }) opts in to default limits (M-7). Each key is a positive integer, optional, and independent: include and groupBy never take find's value. A default applies only to a call that states neither limit nor .all(reason): find (with search, matches, presets, and relation filters), a to-many include (including manyThrough), the page() size, and aggregate with groupBy. max is a hard ceiling for a written limit: above it is OKM1101, and .all(reason) goes past it. .stream(), one, count, exists, an aggregate without groupBy, update and delete limits, bulk write predicates, and raw() are untouched. Without limits nothing changes. .inspect() lists the bounded rule as limits.find, limits.page, limits.groupBy, or limits.include, and QueryEvent.limit.source reads the same. A bad limits is OKM1120 at connect.
  • OKM2011 (default-limit-reached) joins the warning registry: in development, a read that used a default from limits and returned exactly that many rows warns with the call site, the key (limits.find = 50), and the fix. It never fires for a limit the call wrote, for .all(reason), or for page(). The notice has a limit field.
  • orderBy accepts one hop, a column of a to-one relation or count on a to-many, and a hidden or sensitive relation sort stays a primary-key cursor. That order with distinct or distinctOn is OKM1127, in aggregate is OKM1128, and a cursor that does not match the order is OKM1130 (#558).

tooling

  • Capability key query.countDistinct is native on Postgres (D309), query.distinct and query.distinctOn are native on Postgres (D310), query.namedAggregates is native on Postgres (D311), query.having is native on Postgres (D313), and query.orderNulls is native on Postgres (D314).
  • Capability keys writeObserver.unit (transaction | batch), writeObserver.keys (returning; closed profile none), and writeObserver.rowCount (mixed; closed profile unknown) are in the Postgres registry with proofs and closed-profile refusals.
  • queryObserver.timing is native on Postgres with cost query|decode. Pool-queue wait is not measured, so queueMs stays null. The closed profile cost is none.
  • OKM1890 refuses writeObserver({ keys }) when a named table has no primary key or is not in the schema. OKM2200 warns when the write-observer pending-event buffer overflows.
  • okm --json on every command prints one okm.cli/1 document and nothing else. --out <file> writes a large data payload (- keeps it on stdout). okm schema <command> prints the JSON Schema for that data. The same files ship under schemas/cli-1/.
  • Short flags for flags that already exist: -h help, -V version, -v verbose, -c config, -t target, -o out, -n dry-run on migrate baseline. -f is not a flag. --allow-protected, --allow-pooler, and --replace have no short form and are not read from the environment.
  • describe(schema).requirements reports minVersion.postgres (the same floor as minPostgres), the extension list, the catalog features the schema uses, and a cost line for each emulated feature. okm build, okm check, and okm doctor compare that list with the Postgres registry exported from okmodel/migrate. A refused feature stops with OKM1860 and names the dialect, the feature, the first version that would support it, and the portable alternative.
  • Global --config <path> on every okm command that loads a config; default discovery stays okmodel.config.ts. A missing file names that path and --config.
  • plan, apply, and status on okmodel/migrate share the CLI migrate engine for CI and deploy without shelling out (D298).
  • okm migrate baseline and baseline(target) from okmodel/migrate record the head snapshot as applied history on an existing database without DDL (D299). A managed mismatch is OKM1858 with the reconcile plan; unmanaged objects are reported and left alone; existing history is OKM1857; a protected target needs --allow-protected.
  • describe(schema) on okmodel/migrate reports minPostgres and extensions (each with a reason). okm doctor and okm check refuse a server below that floor (OKM1802 / OKM1803) or missing a required extension (OKM1811) (D300).
  • bun run ci:report prints job and step duration, queue time, the critical path, and re-run counts for recent main runs, and why each 0.9.1 pull failed a push.
  • bun run verify:fast checks format, lint, types, layers, purity, docs, and the readme, then runs only the tests related to files changed since the merge-base with origin/main.
  • bun run ci:fail <pull> prints the first failing job, its failing step, the last 60 log lines, and the one local command that reproduces it.
  • gate / check compares npm pack --dry-run --json to a committed snapshot of packed files and the exports map (scripts/pack-contract.ts). A forbidden install script, a dependency, a required peer, or a missing files allowlist fails the check. Update the snapshot with bun ./scripts/pack-contract.ts --update.
  • bun ./scripts/verify-release.ts <version> [--recorded <path>] downloads the published tarball and checks its version, the pack-contract file list, and npm audit signatures. With --recorded it fails unless the sha256 and file list match the record the release pack job wrote. Without --recorded, a rebuild of the tag is a warning and does not fail. The release workflow packs once, prints that sha256 in the job summary, uploads the record, publishes that tarball, and verifies against the record. v0.9.1 not being on main is a warning.
  • Postgres CI starts PgBouncer 1.26.1 (edoburu/pgbouncer v1.26.1-p0, pinned by digest) in transaction mode with pool size 1, in front of the topology, on the needs: postgres label and the full tier. Tests opt in with OKM_POOLER_URL. Suite shards keep the direct URL. A smoke test runs a query on each TCP adapter, checks that two sequential transactions share one server connection, and checks that a SET outside a transaction is visible to the next client. PgBouncer 1.26.1 supports max_prepared_statements in transaction mode (default 200).
  • okm migrate resolve <migration>:<step> records a committed step a person finished, or marks it so the next apply tries again. The operand is the 0-based id okm migrate status prints. A protected target needs the protected-target allowance. There is no skip.
  • okm migrate status JSON data adds steps[] (id, kind, state, attempt, nextAction) for committed steps that are in flight or broken. id, kind, and state are untrusted objects. The text table is unchanged, and a steps section follows when a row exists.
  • okm migrate plan --json adds operationClass on the plan and on each step, fromCatalogHash, toCatalogHash, planHash, requiresAck, and destructiveOperations. planHash is the SHA-256 of canonical JSON for the dialect, the two catalog hashes, and each step's SQL as executed. It omits the target, timestamps, and the OKModel version. requiresAck is true when the plan class is destructive. Apply does not read it.
  • Text read from a database, a database error, or a user query is an object with untrusted, origin, text, truncated, and modified. It is bounded and marked untrusted; this reduces risk and does not eliminate it. The human line is untrusted <origin>: "<text>".
  • A failed committed step is recorded in okm_steps (started, failed, or retry) and removed in the same transaction that inserts okm_history. OKM1553 means the probe cannot tell whether the step is applied. OKM1554 means resolve refused because the probe says it is not applied. OKM1555 means resolve needs explicit acceptance because that kind has no probe. OKM1556 means the statement changed after the step started.
  • okm --json errors keep the six okm.cli/1 fields and add location, fixEdit, safeAlternatives, docs, and escapeHatches when the producer or the registry has them. fix stays a string. A kind-only error such as driver stays at the six fields. schemas/cli-1/errors.json is the array schema. okm doctor <code> prints docs/errors/<code>.md.
  • okm protect add, okm protect remove, and okm protect list read and write the user fingerprint store. add records the selected target. remove requires a terminal. list prints the stored fingerprints. The JSON data is the fingerprint, with identity null, and does not include the URL.
  • Conformance cases are tagged portable or gated on a registry key or an adapter gate. A refused registry entry asserts that entry's code and alternative instead of running the body. A false adapter flag on a feature the registry still provides stays unsupported. okm driver test prints portable, gated, refusal, and unsupported counts. A test-only profile refuses every Postgres feature and turns the adapter flags off, and the portable minimum (connect, a read and write, error mapping) still passes on it.
  • renderAgentsBlock({ version, tenancy, protectedConfigured }) writes the managed AGENTS.md block: the docs paths inside the package, the safe command order (okm check, okm migrate plan, okm migrate check, okm migrate apply), and the never-list. It names only commands that exist. The block does not include a URL or a timestamp.
  • bun ./scripts/pack-size.ts fails when the packed tarball grows by more than 400 KB over tests/fixtures/pack-size.json.
  • docs:check also checks public-doc hygiene: front matter, links that stay inside the public set, a language on every code fence, and no hidden characters in the packed text. bun run typecheck compiles the public type surface and the *.test-d.ts consumer fixtures with TypeScript 7. bun run ts6:check compiles the same files with TypeScript 6.0.x (typescript-editor) and is not part of bun run check.
  • A committed step may declare -- probe: <SELECT> above its statement. The probe is one read-only SELECT that returns a single boolean: true means the step is already applied. It runs inside begin read only and is rolled back. A built-in probe for the step's kind still wins, and a declared probe has no cleanup. okm migrate resolve --applied uses that probe. The probe is not part of the step's SQL hash or the plan hash.
  • OKM1557 refuses a committed step that has no built-in probe and no declared probe, so a failure cannot tell whether the change is already in place. OKM1558 refuses an empty probe, a probe that is not one read-only SELECT, more than one statement, or a probe on a step that stays inside a transaction. -- okm-allow with a reason records that a failure in that step needs a person.
  • A direct connection describes one statement when the driver cannot, including TLS chosen from sslmode. A pooler host is still refused.
  • okm raw sync discovers static raw() calls imported from okmodel/pg, describes each statement without running it, and writes .okm/raw.lock.json and .okm/raw.d.ts. --check writes nothing and exits 1 when either file would change. okm check fails with OKM1604 when a call is missing from the lock, warns with OKM1605 when a lock entry is no longer called, and fails with OKM1601 when a target's signature no longer matches. A statement that is not a static string is OKM1603. A statement the checker cannot classify is OKM1602.
  • A described column is NOT NULL only when it comes from a base or partitioned table, attnotnull is set, the column is not generated, no NOT NULL constraint on that column is unvalidated, and the statement has none of join, union, intersect, except, lateral, with, values, rollup, cube, or grouping outside strings, comments, and quoted identifiers. as "name!" forces NOT NULL and as "name?" forces nullable. The suffix is not part of the name. PostgreSQL 15โ€“17 have no contype n row, so that check does not change them. On 18 a NOT VALID constraint sets attnotnull while NULLs can remain, and the column stays nullable.
  • okm init writes starter files for one package (okmodel.config.ts, schema, client, scripts, and .okm/init.json) and does not connect. okm generate init still writes the first migration. A different installed driver is OKM1862, a missing non-interactive choice is OKM1863, a user-modified file is OKM1864, an unusable directory is OKM1865, a plan-hash mismatch is OKM1866, and schema or database tenancy is OKM1867.
  • okm init in a workspace initialises the selected packages. One config per service, or one shared database package when the apps already depend on it. Two or more packages, or a package under an ancestor config, share lint, roles, and target names through okmodel.base.ts. That file has no database URL and no protected flag. Root scripts are added only when a task runner is already in use. okmodel is catalog: only when a pnpm catalog already lists it.
  • A command loads the nearest okmodel.config.ts and prints that path. Two files at the same distance are OKM1891. okm init with several workspaces and no terminal is OKM1892. Two packages that share one migrations directory are OKM1893 and nothing is written.
  • okm check lists each project warning ignore with its rule and reason. An ignore without a reason is a configuration error. Warning pages for OKM2004, OKM2005, OKM2007, and OKM2008 are generated with the error pages.
  • bun ./scripts/size-attribution.ts prints minified bytes and a gzip share for each source module on every gated graph from scripts/size.ts. It fails when a total differs from that script. --update rewrites tests/fixtures/size-attribution.json. The pull-request job inform / size prints the delta and is not a required check.
  • bun ./scripts/dead-exports.ts lists exports under src/ that nothing in src/, tests/, or scripts/ imports and that are not on the public API surface.
  • bun ./scripts/duplicates.ts lists duplicate and near-duplicate function bodies and repeated large literals under src/.
  • okm check and okm migrate check list every -- okm-allow line in the migrations directory: migration id, step, code, rule class, and reason. A destructive rule (a drop, a row-level-security weakening, a shortening or narrowing type change) is marked DESTRUCTIVE. okm.cli/1 data adds allows[] for both commands (additive; schemas/cli-1/check.json and migrate-check.json). The reason is marked untrusted. OKM2300 warns on a reason shorter than 12 characters, made only of stop-list words (ok, fix, test, todo, needed, temp, tmp, none, na), or made only of the code repeated. It is an audit: nothing is refused and the exit code does not change. There is no new flag.
  • okm check runs three schema warning rules offline: OKM2101 (unindexed-foreign-key, no index starts with a foreign key's columns, a composite index that starts with them counts), OKM2102 (table-without-primary-key), and OKM2103 (unindexed-filter-column, a column in a filters() allow or sort list or used by a preset with no index that starts with it; a primary key, a unique index, and the tenant prefix count). Each is medium severity, default level warn, phase check, evidence static. Findings print one per line and fill warnings[] with place set to table or table.column. Exit stays 0. warnings.levels sets off, hint, warn, or error; error exits non-zero with the rule code. A rule that cannot decide (an expression index, a preset that needs its arguments, a filters() field that is not a literal) stays silent.
  • filters() keeps nothing at runtime, so okm check reads it from source with the okm raw sync lexer: the raw.include files (default src/**), the schema file, and the tables directory. Only literal allow keys and sort strings on a receiver bound to one table("name", ...) count.
  • A project warning ignore takes an optional target (events or orders.user_id, SQL names). okm check --json adds target and status (applied or stale) to each data.ignores entry. An ignore for an OKM2101 to OKM2103 rule that matched nothing is stale and is also reported as a warning. schemas/cli-1/check.json gains those two optional fields.
  • okm doctor --bundle [--out <file>] [--force-file] writes one local JSON file (bundleFormat: 1, sorted keys, default .okm/doctor-bundle.json) that a tester sends by hand. It needs no database and makes no network call, and the envelope carries no target. It holds versions (okmodel, Node, Bun), OS and arch, package manager, dialect and adapter packages with installed versions, the config file name, .okm/ artifacts from a closed list with SHA-256, layout counts, the capability registry summary, and the codes and pass or fail per rule of the checks that need no database. It never holds a connection string, host, user, URL, table or column name, SQL text, row value, a path outside the project, or an environment variable value. An existing file is refused with exit 4 unless --force-file. --bundle takes no code and no --target. okm schema doctor and schemas/cli-1/doctor.json describe the document (X7, O-6, D307, #511).
  • okm agents status | update | remove [dir] manages the AGENTS.md block between BEGIN OKMODEL v<version> and END OKMODEL, a thin CLAUDE.md @AGENTS.md import, and the packaged okmodel skill in .claude/skills/okmodel/ and .agents/skills/okmodel/ (D308, #513). Text outside the markers is never changed. A skill is written only when its tool directory exists or --agents names it, and the SHA-256 of each written file goes into .okm/init.json.
  • okm agents status prints current, stale, missing, or modified per file, exits 1 with OKM1881 on drift, and warns when CLAUDE.local.md exists. update rewrites the block and any unmodified skill file. remove deletes the block and the files whose hash still matches. A skill file you edited is never overwritten or deleted.
  • okm init --agents <block|claude|agents|all> writes the agent files with a shown diff, and --no-agents records that the project has none so init never asks again. Init asks only on a terminal. --yes or no terminal never installs agent files unasked.
  • Agent file writes come only from the installed package. They print a plan and a diff, --dry-run writes nothing, --plan-hash pins the plan, and a change to an existing file needs --yes or an answer on a terminal (OKM1884, exit 3). Writes are atomic and go to a closed list: AGENTS.md, CLAUDE.md, the shipped skill files, and .okm/init.json. .claude/settings*.json, hooks, .vscode/tasks.json, .cursor/rules, and MCP entries are never written.
  • Damaged or repeated OKMODEL markers are refused and never repaired (OKM1882, OKM1883). New codes: OKM1881 drift, OKM1884 needs a yes, OKM1885 refused content, OKM1886 refused path, OKM1887 file changed after the plan, OKM1888 damaged .okm/init.json. schemas/cli-1/agents-status.json, agents-update.json, and agents-remove.json describe the --json data, and init.json gains an agents section.
  • The packed text scan (docs:check) covers every file under skills/, not only skills/okmodel/SKILL.md.
  • okm doctor --production checks one target for misuse and changes nothing: every query runs in one READ ONLY transaction, and the output says target <name> and never the URL, host, user, or password. Each finding has a stable id, a severity (fail, warn, ok), and a one-line fix. The exit code is 1 when any finding fails (OKM2204), 2 when several targets exist and no --target is given (OKM1853), and 5 when the target cannot be read. --json returns the okm.cli/1 envelope with data.production. Findings: target-protection (fails unless the config sets protected: true; a name or NODE_ENV is never read; OKM2202), pooler (warns, fails with --allow-pooler; OKM2203), okm-meta (fails when absent; the doctor cannot see connect() options such as requireMeta; OKM1520), role-privilege (superuser or BYPASSRLS fails; OKM1707), role-owns-tables (warns), role-config (with config.roles; OKM1707), lock-timeout and statement-timeout (a value of 0 warns; apply's own lock timeout is the one reported; OKM2201), server-version (outside 15 to 18 warns above and fails below; OKM1803), and extensions (installed versions; OKM1811). The exposure check is not built: the catalog does not record validation (D315, #523). --allow-pooler is accepted on okm doctor only with --production.
  • OKM1709 is registered with a page. session.local is native on Postgres in the capability registry and SESSION_SETTINGS reports the scope by dialect family: transaction for Postgres, session for the MySQL family, none for SQLite. assertSessionSettings("mysql" | "sqlite") refuses with OKM1860 and names column, composite, or path tenancy. docs/capabilities.md lists the table.
  • okm pull --casing snake|camel writes sqlName when the key does not spell the database name. okm import drizzle no longer requires Relations, detects casing from the flag, the builders, then the config, and keeps a docs-shaped index and column unique under those names (#557).
  • okm pull --import-ext js|none sets the relative specifier (the default stays .ts). A partial index is emitted with index(), a non-default btree operator class is kept, and .name() is written when the database index name is not the generated one (#545, #555).

docs

  • [Observer events](docs/observer-events.md) freezes okm.observe/1 (write, savepoint, outcome) for connect({ onWrite }) and documents writeObserver() on okmodel/observe. keys appends primary-key RETURNING. Row counts record the command tag each driver exposes. Postgres costs are returning and mixed. The closed profile stays none and unknown.
  • The same page documents QueryEvent, toOtel(), the tag option, and queryObserver.timing. queueMs is null because no driver reports pool-queue wait.
  • Docs: [Adopt an existing database](docs/adopt-existing.md) and the handbook page cover pull, generate, then baseline.
  • A describe spike records, per driver and Postgres 15โ€“18, whether parameter OIDs, column names, column type OIDs, and table OID plus attnum can be read without executing the statement. The notes are docs/describe-spike.md. The scripts under packages/spikes/describe/ are not packed.
  • [When a migration fails](docs/migration-failure.md) records what a failed Postgres apply does, the okm_history and okm_steps columns, the step id, and why there is no skip.
  • bun run docs:errors writes docs/errors/<code>.md for every registry code. docs:check fails when a page is missing or differs from the registry. A code that needs a server says Example: needs a database.
  • [Untrusted text](docs/untrusted.md) records the bound, the origins, and the plan hash.
  • The testing notes describe the portable and gated conformance split, the profile that refuses every capability, and what a new dialect ships before it claims a gated feature.
  • The published tarball includes the public guides, docs/errors/<code>.md, docs/index.md, and skills/okmodel/SKILL.md. Planning notes stay out of the tarball. okmVersion on each page is filled from package.json when the tarball is packed. The committed pages keep the train version, so a pre-release bump does not rewrite docs/.
  • docs/index.md is the pointer index: one line per public page, the command list, and the error-page range. bun run docs:index rewrites it. docs:check fails when the file is stale.
  • [Roles](docs/roles.md) is the least-privilege recipe for an agent: GRANT pg_read_all_data for inspection, and a separate migrator role with CREATE on the schema. It does not use default_transaction_read_only.
  • [Threat model](docs/threat-model.md) records what agent use is defended against and what is not. Each defense names the feature or the test behind it.
  • The site skill page reads skills/okmodel/SKILL.md. That file is the copy an install finds at node_modules/okmodel/skills/okmodel/SKILL.md.
  • The linter page, [When a migration fails](docs/migration-failure.md), and the OKM1557 and OKM1558 error pages describe a declared -- probe: and when a committed step must have one.
  • The CLI reference describes workspace okm init and nearest-config discovery. OKM1891, OKM1892, and OKM1893 have error pages.
  • The guide awaits userFilters.parse(request.query) and passes the result to find with a limit. parse is asynchronous because the parser loads on first use (Q10, #397).
  • [Capabilities](docs/capabilities.md) is generated from the Postgres feature registry. A feature whose proof title does not pass the registry proof check is unverified. docs:check fails when that page, a <!-- support: key --> block, or the step-kind table in [When a migration fails](docs/migration-failure.md) disagrees with the code.
  • [Warnings](docs/warnings.md) records the levels, the development defaults, and the four static rules.
  • [Size audit](docs/size-audit.md) records where the gated bytes go, every unused export, and every duplicate group. It is an internal note and is not packed.
  • [Warnings](docs/warnings.md) lists OKM2101, OKM2102, and OKM2103, the target ignore, the stale report, and when each rule stays silent. Pages for the three codes are generated with the error pages.
  • The guide, the CLI reference, the threat model, and the offline-first note describe okm doctor --bundle: what the file holds, what it never holds, and what checks does not cover (D307).
  • The guide and the CLI reference describe okm agents, okm init --agents and --no-agents, the managed block, the closed list of writable paths, and a CODEOWNERS recommendation for AGENTS.md and .claude/. OKM1881 to OKM1888 have error pages (D308).
  • The guide, docs/production.md, docs/roles.md, the CLI reference, the known limits, and the threat model describe okm doctor --production, its finding ids, and what it cannot see (D315). OKM2201 to OKM2204 have error pages.
  • docs/guide.md and the handbook page define/connect/session-settings cover okmodel/session. docs/known-limits.md says it does not authenticate: a role that can run SQL can set the same names. D312 records the decision.
  • The Limits handbook page, the OKM2011 error page, and the OKM1101 and OKM1105 pages describe connect({ limits }). [Observer events](docs/observer-events.md) and [Warnings](docs/warnings.md) cover the limit source and OKM2011. okm doctor does not report limits yet, and filters().parse takes no clamp from limits.max: both are follow-ups in the plan.

โ™ป๏ธ Changed

  • CI reads Bun 1.4.3 from .bun-version. The kill-storm wedge in Bun.sql is still there on that version.
  • bun run pack-contract stores Bun content-hash chunks as dist/shared/*.js and dist/tooling/shared/*.js. The count of each name stays exact, so a different hash passes and a new or removed chunk fails. bun run verify:fast warns when the local Bun differs and still exits 0.
  • gate / check always runs (if: always()), keys concurrency on the pull request number (or the commit on main), and requires the full tier for full-ci, a chore(release) title, a change under .github/workflows, or a push to main. A no-op label event mirrors the last gate result for that commit instead of reporting Success from a skipped job. Plan: [docs/ci-and-test-cycle-plan.md](docs/ci-and-test-cycle-plan.md).
  • The drivers CI group is three shards (drivers suite, drivers rest, drivers-conformance). The pglite conformance report and the live compatibility table check sit on the conformance shard; the fast path keeps a cheap case-list sync. Driver and error suites open PGlite from a prepared data directory instead of running initdb on every pool.
  • postgres.js kill-storm uncaught exceptions are documented as an upstream limit (B-02): a minimal postgres.js-only reproduction lives in docs/postgresjs-kill-storm.md, known-limits and the Drivers page recommend okmodel/pg/pg or okmodel/pg/bun where failover is expected, and the adapter's process listener is described as damage control rather than a fix.
  • A pull request that only changes docs/ or site/ skips the runtime test matrix, Postgres, the type-cost step, and attest. gate / check stays the required status and counts that skip as a pass only when the filter says those jobs are irrelevant.
  • CI builds dist once and uploads it. Jobs that need the build download it. Jobs that do not need it skip the build. Bun's install cache is on every install, and typecheck keeps a TypeScript incremental build.
  • The topology CI group is two shards. Topology tests open PGlite from a prepared data directory instead of running initdb on every database.
  • Tier 1 runs on every pull request push: format, lint, typecheck, the unit tests, property tests at a quarter of their count (OKM_PROPERTY_SCALE), and the size gate. Tier 2 (type cost, the editor check, attest, runtimes, the temporal matrix, package checks, Windows, Postgres 15 and 18, and the drivers-conformance group) runs with the full-ci label and on every push to main. A later commit on main does not cancel the run in progress. A failed gate / check on main opens an issue.
  • Docs: connect({ hookm }) is an array; a single observer object or a hooks key does not register observers.
  • okm migrate check prints, after ok N migrations, that an empty-table replay does not catch a step that fails only on real data: NOT NULL on a populated column, a unique index over duplicates, or a check over existing rows (Q4, #391).
  • The Release workflow runs on a v* tag. Before any build it requires the tagged commit to be an ancestor of origin/main, the tag to match package.json, and a ## v changelog section. It then runs the Postgres suite, the pack contract, and npm stage publish with provenance (npm 11.15 or newer, Node 22.14 or newer). A dispatch with publish false checks main and refuses a tag that is not on main. The publish job is the only one with id-token: write and contents: write, and it uses the release environment. No npm token is stored.
  • OKM1601 now also covers a typed raw statement missing from the lock or a lock that does not parse. OKM1602 names the typed raw parameter count. OKM1702 covers typed raw on a for() client and the trusted() escape. The doctor text and docs/errors/ pages are regenerated.

runtime

  • Thrown summary and fix text no longer names a bypass. all(), unscoped(), --allow-protected, and --allow-pooler stay on escapeHatches, which requires a human-written reason.

tooling

  • The adapter incremental size gates are raised by decision on 2026-10-21 after the size audit: postgres.js 16,000 to 17,000 and PGlite 12,010 to 12,700 minified bytes. One-time increase. The featureless app and runtime entry gates are unchanged.
  • Registry fix is the primary remedy. A bypass moves to escapeHatches.
  • The migration planner takes a dialect lowering object. Postgres plans stay the same. Feature support and refusal are questions on that object. The planner does not import the feature registry; the dialect object does.
  • Table, column, and constraint changes lower through that dialect. One logical change can be several statements. Postgres plans stay the same, including a validated check before SET NOT NULL on an existing column.
  • Index create, drop, and rename lower through that dialect. Postgres still builds and drops an index on an existing table concurrently. The planner still waits to drop a replaced index until the new one is built. Plans stay the same.
  • Enum, domain, and extension changes lower through that dialect. Adding labels, replacing an enum, and swapping a domain check can be several statements. Postgres plans stay the same, including a non-transactional ADD VALUE and an unverified extension upgrade.
  • Function, trigger, view, policy, role, and grant changes lower through that dialect. One policy can be several statements. Postgres plans stay the same, including CREATE OR REPLACE for a compatible function or view body.
  • The planner no longer imports Postgres SQL. Sequences, check swaps, and backfill UPDATEs lower through the dialect. Postgres plans stay the same.
  • A committed step that fails is not run again when Postgres cannot prove what landed. The next apply probes a concurrent index, a concurrent drop, a constraint, a constraint validation, or an enum value. An INVALID concurrent index is dropped and rebuilt. Any other kind stops until a person resolves it. Transactional steps and backfills are unchanged.

docs

  • Handbook quotes of OKM1101, OKM1102, OKM1105, OKM1701, and OKM1850 match the remedy text.

๐Ÿ”ฅ Removed

  • Deno is not a supported runtime. Portable checks run on Node and Bun, and the runtime entry is imported as an edge bundle on Node (D297).

๐Ÿ› Fixed

  • describe() keeps a numeric minPostgres already on the input (max with the computed floor), and index opclass scan reads every *_ops token case-insensitively, so describe(describe(s)) keeps 17 and 18, a description's floor does not fall to 15, and a later gist_trgm_ops after text_pattern_ops still reports pg_trgm (R5, #402).
  • okm migrate check and okm check reprint table checks (including picklists) on the connected server before planning, so an author IN (...) spelling is not permanent OKM1547 or a NOT VALID/VALIDATE swap against pg_get_constraintdef (R2, #399, D128). Check columns are left out of the plan comparison when an author check omits column handles.
  • okm check checks the Postgres major before author drift and required extensions after it, so a pending migration that creates an extension still surfaces OKM1520 instead of OKM1811. An extension an applied or pending migration creates is not refused as a missing install; when the package is absent from the server, OKM1811 points at okm migrate apply. Several targets and no --target print server checks skipped: N targets, pass --target <name> from okm doctor and okm check (#400).
  • Validated insert / update handles expose ~plan again, so batch() and isolation() work with validation: true. A failing rule inside a batch rolls everything back and reports OKM1200 with issues and batchIndex (D302, Q1).
  • The linter no longer refuses the planner's own NOT VALID then VALIDATE pair when the check is on a column added in the same migration with a constant default (or nullable without a default). A validate on an existing column, or after a volatile default, stays OKM1531 (D301).
  • The expand/contract type-change drop recipe no longer puts -- okm-allow OKM1512 on DROP TRIGGER (that code does not fire there and was OKM1510). The trigger needs no allow; the function keeps OKM1515 and the column keeps OKM1512, under a name header so the first statement is a step (B-36).
  • testing() plain-populates materialized views after migrate, and isolation() falls back to a plain refresh when a concurrent refresh says the view is not populated, so refresh: "concurrently" works inside the harness (B-34).
  • Composite tenancy factories generate a distinct uuid per key, honour an explicit full key, and refuse a partial one instead of copying one key onto another; isolation() flip inserts skip identity columns so lists.id cannot be written no longer fails (B-32).
  • Documented that LIFO PgBouncer on a non-heuristic port can pass the pid probe: mark it with ?pgbouncer=true (or a pooler / pgbouncer host). Apply does not warm-and-hold a second backend (B-41).
  • The statement parameter ceiling is 65,533 so OKM1126 fires before postgres.js's MAX_PARAMETERS_EXCEEDED at 65,534 (B-48).
  • A node-postgres call with timeout or signal removes its checkout error listener, so repeated timed calls do not stack listeners on a pooled client. A call that reaches its deadline while waiting for a connection is refused and does not run. A cancel that does not settle destroys that client, so the pool can serve the next call after a kill storm.
  • After an external-table re-stamp, okm migrate status stays current, generate stays no changes, and okm migrate check no longer fails with OKM1549 when the only catalog drift is external columns (B-45).
  • okm migrate check reprints domain checks before planning, so a non-canonical author check such as VALUE > 0 is OKM1552 only and not permanent OKM1547 when generate says no changes (B-37).
  • A trigger WHEN that names OLD and NEW no longer fails okm check / okm migrate check with "expression contains variables of more than one relation"; introspection reads the predicate from pg_get_triggerdef, and those columns are ordinary table dependencies (B-46).
  • defineConfig({ namespace }) places a trigger's called function with the catalog, so generate/apply/check no longer fail OKM1020 for a missing function in public (B-47).
  • The guide roles flow on a fresh database no longer fails with SAVEPOINT can only be used in transaction blocks during provision: schema ownership runs without a savepoint when no transaction is open, CREATEROLE is checked on the connecting user (not the managed migration role), and a failed provision drops empty bootstrap tables so a retry is clean (B-30).

contracts

  • Insert omits defaulted and nullable columns, and Update accepts any subset of the writable columns, matching insert() and update(). A missing required column and an unknown key stay type errors (R7, #404).
  • ColumnDefinition adds the optional unvalidatedNotNull (the constraint name). Introspection sets it on PostgreSQL 18 for a column whose NOT NULL constraint is NOT VALID. It is absent otherwise, so no existing catalog changes its document or hash (D316).

dialects

  • A numeric default given as a JavaScript number, such as t.numeric().default(0) on the string codec, is OKM1210 at schema(). The message says the value must be a decimal literal. A string default such as "0" still describes.
  • Introspection reads a PostgreSQL 18 NOT NULL โ€ฆ NOT VALID constraint (pg_constraint contype n, convalidated false) instead of treating attnotnull as proof. The column stays NOT NULL and records the unvalidated constraint. When the schema says NOT NULL, the plan is one VALIDATE CONSTRAINT step. When it says nullable, the plan is DROP NOT NULL. PostgreSQL 15 through 17 read as before (D316).
  • Column handles in sql and enum or domain type names go through one identifier path, so a mixed-case name, a space, or a quote stays one quoted name (#534).
  • An array picklist checks that every element is one of the values. .unique() on a gin or gist index is OKM1060 at schema() (#537, #545).

adapters

  • A Bun.sql pool that answers connection must be a PostgresSQLConnection after pg_terminate_backend is replaced. The next call dials again. Pool close() returns within a second when Bun's own close() does not (oven-sh/bun#46166). Still present on Bun 1.4.3.
  • Bun.sql stamps SQLSTATE 08006 on a dead socket (Connection closed or connection must be a PostgresSQLConnection), so the failure is unavailable. A commit already outcome_unknown stays OKM1401 (#567).
  • A node-postgres pool does not retry a read after the connection fails. The killed client is discarded (#568).
  • db.capabilities is frozen. A write to a driver flag is rejected (#546).
  • PGlite loads a declared contrib extension before wasm (#554).

runtime

  • where, orderBy (including relevance()), and search accept hidden({ filterable: true }) in the type. A plain .hidden() column stays a type error there, and the column stays off the row (Q7, #394).
  • Search of a plain hidden column names the opt-in: hidden({ filterable: true }).
  • A write observer unit is tracked per transaction, batch, or autocommit statement instead of per client. Two transactions that overlap on one client no longer mix their events or lose an outcome (G-8, #525).
  • onQuery reads inside a savepoint share the transaction's unit id, and the params of a successful write statement are redacted for hidden and sensitive columns (G-8, #525).
  • A read the topology serves from a replica is labelled { endpoint: <name>, role: "replica" } in onQuery, not primary (G-8, #525).
  • A validated row is branded for its table, so a row sealed for one table does not skip another table's rules, and a batched OKM1200 keeps the stack of the check that threw. find, one, and count reject an unknown option beside a valid one, and update with version: "any", lock, limit, and { allow } on a guarded column typechecks (#560, #562).

tooling

  • The managed AGENTS.md block and skills/okmodel/SKILL.md no longer name --accept-destructive, a flag that does not exist. They name the guards that do: --allow-protected, --accept-unverified, --allow-pooler, and -- okm-allow lines in migration files. renderAgentsBlock and docs:check now refuse a long flag no command accepts, so the text cannot drift again. The threat model, roles, and protection pages say that a destructive step is allowed only by its -- okm-allow line.
  • A protected-target refusal names the match: project config, a store fingerprint, or both.
  • okm migrate check reuses an extension that is already installed. The replay moves it into the scratch schema for the transaction, and ROLLBACK restores the schema apply left, so a declared extension still passes after apply (R6, #403).
  • okm ext check prints the extension name in the declared column when the pin is omitted. A pin still prints that version or floor (R6, #403).
  • okm migrate check refuses a uuidv7() default on PostgreSQL 15, 16, and 17 with OKM1812 and the fix uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18. PostgreSQL 18 replays that default (R6, #403).
  • lint / pull request assigns the open train milestone when Closes #N has none. 0.10.0-next.N uses milestone 0.10. The issue and the pull request both receive it.
  • A no-op CI event mirrors the last finished gate / check and skips a cancelled run, so two label events that cancel each other do not fail the gate when an earlier run succeeded.
  • A failed CREATE INDEX CONCURRENTLY is dropped before apply returns, so the target does not keep an INVALID index. When that drop fails, the error names the index and DROP INDEX CONCURRENTLY. A later apply still drops a leftover invalid index and rebuilds it (Q4, #391).
  • testing() drops the catalog it applied when the harness finishes, in reverse dependency order. An ignored object is left in place. migrate: false drops nothing (Q9, #396).
  • The packed-tarball size check runs for this repository. A fixture passed to pack-contract with --root does not need tests/fixtures/pack-size.json.
  • okm check reports a PostgreSQL 18 NOT NULL โ€ฆ NOT VALID constraint as drift (OKM1520) and names the column, even when no row holds NULL. okm pull writes that column .nullable() with a comment and lists it under not validated. A dependent catalog's okm check treats such an external column as stale (OKM1841). The linter reports its VALIDATE CONSTRAINT as OKM1531 with the reason validates a NOT NULL constraint.
  • okm push refuses a plan that drops a column or a table that holds a row (OKM1898); --allow-data-loss applies it and prints dropped <object>, while an empty column or table still applies with no dropped line. A drop .okm/pull.json lists stays OKM1897, and dropping a unique or primary-key constraint alone is applied (#563, #564).
  • okm check reports a reserved column or index name as OKM1122, as okm push does, unless .okm/pull.json already lists that name. okm pull writes that record at the project root even with --out; a scratch pull or another target's --schema leaves it, and --out of the same database refreshes it (#561, #562, #563).
  • OKM1894: testing() found a table, type, or extension that already exists. Use an empty database, or migrate: false (#542, #561).
  • OKM1895: the command line was not accepted, including --json usage and a missing config file. okm --help and okm <command> --help list what is accepted (#542, #566).
  • OKM1896: a check, picklist, domain check, generated column, or btree expression is reprinted on a temporary table, and this session cannot create one. Point the command at a writable primary; the role needs TEMP, not CREATE on the schema (#552).
  • OKM1897: okm push would drop a column or constraint that okm pull listed under not pulled yet. Those objects stay, because pull did not write them (#561, #563).
  • OKM1898: okm push would drop columns and tables that hold rows. Keep them, or pass --allow-data-loss (#563, #564).
  • OKM1899: okm migrate apply and okm push refuse a table that already owns okm_meta, okm_history, okm_backfill, or okm_steps and is not okm's ledger, before any step. The message names the schema and the table (#570). A table with every ledger column, including extra ones, is still the ledger.
  • okm migrate apply honors minPostgres and refuses a feature the registry refuses, using that entry's code when it is OKM1861 or OKM1868 through OKM1879. Any other refusal stays OKM1860, and a missing dialect method or a lowered statement the planner cannot trust is OKM1559 (#566, #568).
  • Apply of uuidv7() stays OKM1812 when the function is missing, including when minPostgres is set, and a hand-installed function still applies. A using-index reprint that fails because the operator class is not installed yet keeps the author text, so check reports the pending extension as drift (#566).
  • okm check, apply, and push reprint a check, a generated expression, and a btree index expression until the text matches what Postgres stores, so a CASE newline is not OKM1122 and an INVALID index is omitted from the live catalog. CREATE UNIQUE INDEX โ€ฆ IF NOT EXISTS still raises OKM1528 and OKM1529, and the empty-replay line also names a foreign-key validation and a cast (#545).
  • Cleanup of a failed concurrent index drops that index in the table's schema and does not treat another session's build as its own. OKM1812 step numbers count from 1, a lock timeout tells you to run okm migrate check again, and an index another session is still building says to wait for that build (#545, #566, #568).
  • The planner drops an index before the column it covers, keeps both foreign-key actions, does not drop an identity sequence when its table is renamed, and treats an added enum label as applied (a removed or reordered label stays OKM1548). A trigger WHEN is reprinted before compare, okm check reads views on the namespace search_path, and an existing unique index that covers the new one is not OKM1529 (#537).
  • A column or table rename does not rewrite a function body or a trigger WHEN in place. A function with the same signature is replaced, and a trigger whose WHEN changed is dropped and created; checks, indexes, and policies still rewrite in place (#548).
  • okm migrate check does not move an installed extension into the scratch schema, and reprint uses temporary objects instead of okm_expr* in the live schema. COMMIT, END, ROLLBACK, and ABORT, including WORK, TRANSACTION, and AND [NO] CHAIN, are OKM1551, and okm push refuses a plan that weakens row-level security as OKM1510 before any statement (the allow line is OKM1550) (#532).
  • An extension installed outside the table schema is found from a column type or an index operator class, and apply puts that schema on search_path. Unquoted CREATE EXTENSION names are folded, extension(name, { schema }) creates the schema inside check, an operator class is mapped by method, and a role that does not own the extension gets OKM1814 (#552, #554, #557).
  • okm check and okm doctor skip OKM1811 before the initial migration when the package is available, and they still report it after DROP EXTENSION on an applied database. A role that cannot install an extension named in a pending migration is OKM1811, and describe() reports uuid arrays, column tenancy, and the uuidv7 case (#552, #554, #561).
  • okm pull writes a generated-always integer primary key as t.integerIdentity(), lists a reserved-word column, constraint, or index under not pulled yet instead of aborting, and stores an unnamed Drizzle index as {table}_{columns}_index. A citext check reprints by installing the extension type and rolling that install back, with the extension schema on search_path (#561, #562).
  • okm pull lists unmanaged row-level security and does not drop it after baseline, appends Name to a binding that shadows an okmodel import, and omits a cross-schema foreign key it did not read (two schemas that share a table name are OKM1023). --schema billing rewrites a key to public.users as .references("users"); repeating --schema writes nothing (OKM1023 when the name is in both, OKM1021 when the target is outside the one namespace) (#555, #562).
  • okm exits when stderr is closed (EPIPE) instead of writing again and spinning. A dangling agent skill link is refused before okm init writes (OKM1886), okm doctor --bundle refuses a symlink that leaves the project (an existing file without --force-file is OKM1855), a second okm generate lints the pending migration again and prints each finding once, protected.json is mode 600, and writeObserver refuses an unknown option (#542).

docs

  • Known limits names postgresjs-kill-storm.md again. The name is text, not a link, and the file stays out of the published tarball.
  • Known limits, the migrate-check page, the CLI reference, and the external-catalog page describe the PostgreSQL 18 NOT NULL โ€ฆ NOT VALID state. D316 records what each command does with it.
  • The observer page says seq has gaps after a savepoint rollback, that a unit is one transaction, batch, or statement, and how a replica route is labelled (G-8, #525).

๐Ÿ”’ Security

  • A store fingerprint blocks a state-changing command when there is no terminal (OKM1859, exit 4), including when that invocation passed --allow-protected. Project-config protection still honors --allow-protected. Deleting protected: true does not delete the stored fingerprint. A terminal is friction, not proof of a human. The ceiling is [Protection](docs/protection.md).
  • A postgres.js generic or aborted-connection failure on a statement whose table has a sensitive column no longer leaves parameters or args on error.cause when the driver omits table (B-33 residual, #364).
  • SECURITY.md names the disclosure address, the report policy, and supported lines (the current 0.x minor and the previous minor, 0.10.x and 0.9.x). [Using OKModel safely](docs/using-okmodel-safely.md) covers lockfile pinning, a minimum release age, and what provenance does not prove.
  • inspect() redacts a write the way a read does, and explain() sends the real parameters and then replaces hidden and sensitive values in params and in the plan text. Batch onQuery params and a page cursor redact from the statement's own table, so a concealed value or a tenant id is not left in clear text, and an OKM1210 message for that column does not contain the value (#551, #560, #562).