v0.10.0
๐ฅ Breaking Changes
DriverCapabilitiesno longer includespreparedordescribe. Prepared-statement mode stays a connect option. A driver that can describe a statement still exposespool.describe.hidden({ filterable })accepts only a literaltrue.falseand a boolean variable are type errors. Callhidden()with no argument. Passingfalsefrom JavaScript still leaves the column hidden and not filterable.okm migrate plan --jsonfindingreasonis an untrusted object.okm migrate baseline --jsonunmanagednameis an untrusted object.okm migrate statusJSONid,kind, andstateare untrusted objects. A database error's text iserrors[].detail.summarystays a registry sentence, orA database error was returned (<kind>).when the code has no page.db.batchof a versioned update throws OKM1121 before any statement; move that update intotx().tx.batchis unchanged, andversion: "any"stays allowed.- Bound parameters stay off an error cause unless
includeValuesis set. A page cursor for a hidden or sensitiveorderBycarries only the primary key (a gone row is OKM1130), andOkmErroris branded soinstanceofmatches 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: falseturns 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 nextokm generatewrites nothing. An index that does not is replaced once:CREATE INDEX CONCURRENTLYfor the new index, thenDROP INDEX CONCURRENTLYfor the old one. That is not a blocking rebuild, andokm migrate checkaccepts it (Q3, D306, #390). OKM1706 stays when prefixing is off. - With
validation: true, an unknown key oninsert,update, upsert /onConflict, first-or-create,insert.validate,insert.check,insert.pick(...).validate,insert.pick(...).check,insert.omit(...).validate,insert.omit(...).check,update.validate, orinsert["~standard"].validateis OKM1120 and names the key and the table (R1, D304). A typo-onlyupdatenames the key instead of saying "update set is empty."insert.check,pick(...).check, andomit(...).checkreturn 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 Schemavalidatethrows 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 withversioninsetis 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 ofversionstays OKM1190. okm generateexits 1 when an error finding exists; the migration file is still written; stderr saysokm migrate applywill refuse until it is fixed (Q8, #395). A warning-only finding stays exit 0.okm pullandokm import drizzlewrite relative imports with the.tsextension so Node type stripping andbunx okmcan load the schema (R4, #401).tscneedsallowImportingTsExtensionswithnoEmit, orrewriteRelativeImportExtensions.- An
updateon aversioned()table withoutversionis a TypeScript error; the runtime OKM1120 stays when types are bypassed (Q5, D305, #392). Non-versioned tables are unchanged. okmexit codes follow the outcome.0is done or nothing to do.1is a command that ran and failed.2is usage or configuration (unknown flag, missing config, a Node older than 22.18).3is a decision the command will not ask (OKM1530, and a committed step that needs a person: OKM1553, OKM1555).4is a guard (OKM1850, OKM1852, OKM1855, OKM1859).5is 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, stays1. A script that only checked non-zero still fails. A script that treated every failure as1now sees2through5.okm migrate plan --json,okm migrate baseline --json, andokm catalog export --jsonkeep their previous fields inside theokm.cli/1envelopedata.parsePlanJsonreadsdataand still accepts a bare plan document.okm buildandokm devreject 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 nextokm generateplans 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 becausesha256.tsleaves it (90,763 to 88,348). schema()resolves every cased name once:snakesplits an acronym (userIDisuser_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.tenantIndexeson a global table, or a tenant-index mark when the schema has no tenancy, is OKM1060, and OKM1706 on ausingindex namesindex().global("reason")(#546). - A table named
capabilitiesis OKM1023, becausedb.capabilitiesis reserved on the root client, a scoped client, and insidetx()(#546). find,one, andpagereject a plain hidden key, an unknown key, a wrong filterable value, a bador()branch, and a hiddenorderBy, including insideinclude, andpagerejects an unknown option andrelevance()(OKM1120). The same plain hidden key and a bador()are type errors oncount,exists,update, anddelete, while a filterable hidden column stays valid inwhereandorderBy(#549, #550).
โจ Added
contracts
DescribeResultaddsparameters(type OIDs in$norder) andfields(name,typeOid,tableOid,attnum).columnsandparameterCountstay.ExecuteResult.reportedis the driver command-tag count, or null when the driver reported none. Absent on fixtures that only setcount. The caller-facingcountstaysreported ?? rows.length.
adapters
- postgres.js and PGlite
describereturn those fields and do not execute the statement. node-postgres, Bun.sql, and Neon still leavedescribeunset. resultFromrecords the command tag onExecuteResult.reported. postgres.js and Bun.sql usecount. pg and Neon userowCount. PGlite uses per-statementrowCount, and cumulativeaffectedRowsonly whenrowCountis 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 aversioned()table skips the version check, bumpsversionby 1, and returns the claimed rows. A miss is an empty array.versionstays required on every other versioned update.version: "any"withoutlock: "skip"andlimit, an unrecognised version string, and aversionkey insetare 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 withhookm.onTransaction. Normative shape indocs/observer-events.md.db.flush()waits for theokmodel/observehelper (app size gate).okmodel/observeexportswriteObserver(),flush(),OBSERVE_BUFFER_MAX(10,000), andobserverBufferOverflow()(OKM2200). The helper buffers per unit, calls exactly one ofcommitted|rolledBack|unknown, isolates throwing and rejecting handlers viaonObserverError, and settles tracked promises onflush(). Overflow drops the oldest unit without blocking the write path.writeObserver({ keys })names schema tables. Those writes append primary-keyRETURNINGwhen the statement is a simple insert, update, or delete, and put the values onWriteEvent.keys.sql()andinspect()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. NestedRETURNING(parent guard, versioned close, archive) is not rewritten. A caller result is unchanged: observer-only rows are hidden.- Write events take
rowCountfrom the command tag. Mode ischangedfor insert, update, and delete,returnedwhenRETURNINGran, andunknownwithrowCount: nullwhen the driver gave no tag. An upsert stayschangedand 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/warningsexportswarnings(). Pass it toconnect({ 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.ignoreneeds a reason.onWarningreplaces the console.raw(db, sql, params)fromokmodel/pgruns 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 runokm raw sync. The runtime looks the statement up byrawStatementKeyin.okm/raw.lock.json(read on the first typed call, undercatalogDir), 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 callsregisterRawLock(lock)once. Awritestatement runs on the primary and emits anonWriteevent withsource: "raw",op: "raw",table: null.onQuerysees reads and writes asop: "raw". Insidetx()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")fromokmodel/pglifts the tenant check for typed raw on afor()client. The reason is required and shows as the observertag. Without it, typed raw on a tenant client is OKM1702. Raw SQL has no archive filter and no hidden-column guard.connect({ onQuery })delivers oneQueryEventper 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 onflush(), and overflows in-flight promises with OKM2200.params: trueandstack: trueopt in; sensitive, hidden, and tenant values are[redacted].toOtel(event)returns{ name, attributes }with the samedb.*names asokmodel/otel, plusdb.response.status_code(the SQLSTATE) on failure. It imports nothing from OpenTelemetry and never emits parameters or tenant ids.db.query.textis included only whenstatement: true.writeObserver({ tag })andqueryObserver({ tag })copy a tag onto each event. A tag is 1โ64 characters from[A-Za-z0-9_.:/-]. Anything else isinvalidwhen the helper is built.okmodel/sessionexportssessionSettings({ key: { name, type, required? } }). Types aretext,uuid,int, andbool.connect(url, { schema, session })addsdb.with({ ...values }), a typed client that sets those values with oneselect set_config($1, $2, true), ...statement as the first statement of the transaction a unit runs in, before any savepoint.is_localis always true, so a transaction-mode pooler cannot hand a value to the next client. A nestedtx()never sets again, a read usesBEGIN READ ONLYon its routed endpoint, andbatch()starts with the statement.with()composes withfor()andunscoped()in any order, laterwith()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 withapp.tenantorapp.unscopedor 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 calledwith()when a key is required are OKM1709 before any statement. Read the value in SQL asNULLIF(current_setting('app.user_id', true), '')before a cast.rlsTenancy()andsessionSettings()share one scope pool wrapper (src/runtime/tenancy/scope-pool.ts). A queuedset_configmarks its scope as set, so the first statement of afor()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()andwith()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 ascountDistinct.<field>. A group with only NULLs, and the one total row of an empty table, count 0. It takes the column typesminandmaxtake (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;orderBymust start with the same keys. Neither takesincludeorsearch, andpageandonerefuse them (D310, #507).aggregate({ select: { name: { sum: "field" } } })gives each aggregate its own flat, typed result key. An entry is one ofcount: true,countDistinct,sum,avg,min, ormax, 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 bucketas, a list-form key, or a prototype key is OKM1120 (D311, #508).aggregate({ groupBy, having })keeps the groups whose aggregates match. It is thewherepredicate form overcountand the names inselect: comparisons,between,inList,notIn,not,or(), and the text patterns on a textminormax. A NULL aggregate matches no comparison.havingwithoutgroupBy, an unknown name, or another operator is refused before any SQL (D313, #509).aggregate({ groupBy, orderBy })orders groups bycountand by the names inselect, besides group fields and bucket names, each asasc,desc, or{ dir, nulls }. Aminormaxof 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:includeandgroupBynever takefind's value. A default applies only to a call that states neitherlimitnor.all(reason):find(withsearch,matches, presets, and relation filters), a to-manyinclude(includingmanyThrough), thepage()size, andaggregatewithgroupBy.maxis a hard ceiling for a written limit: above it is OKM1101, and.all(reason)goes past it..stream(),one,count,exists, anaggregatewithoutgroupBy, update and delete limits, bulk write predicates, andraw()are untouched. Withoutlimitsnothing changes..inspect()lists theboundedrule aslimits.find,limits.page,limits.groupBy, orlimits.include, andQueryEvent.limit.sourcereads the same. A badlimitsis OKM1120 at connect.- OKM2011 (
default-limit-reached) joins the warning registry: in development, a read that used a default fromlimitsand 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 forpage(). The notice has alimitfield. orderByaccepts one hop, a column of a to-one relation orcounton a to-many, and a hidden or sensitive relation sort stays a primary-key cursor. That order withdistinctordistinctOnis OKM1127, inaggregateis OKM1128, and a cursor that does not match the order is OKM1130 (#558).
tooling
- Capability key
query.countDistinctis native on Postgres (D309),query.distinctandquery.distinctOnare native on Postgres (D310),query.namedAggregatesis native on Postgres (D311),query.havingis native on Postgres (D313), andquery.orderNullsis native on Postgres (D314). - Capability keys
writeObserver.unit(transaction | batch),writeObserver.keys(returning; closed profilenone), andwriteObserver.rowCount(mixed; closed profileunknown) are in the Postgres registry with proofs and closed-profile refusals. queryObserver.timingis native on Postgres with costquery|decode. Pool-queue wait is not measured, soqueueMsstays null. The closed profile cost isnone.- 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 --jsonon every command prints oneokm.cli/1document and nothing else.--out <file>writes a largedatapayload (-keeps it on stdout).okm schema <command>prints the JSON Schema for thatdata. The same files ship underschemas/cli-1/.- Short flags for flags that already exist:
-hhelp,-Vversion,-vverbose,-cconfig,-ttarget,-oout,-ndry-run onmigrate baseline.-fis not a flag.--allow-protected,--allow-pooler, and--replacehave no short form and are not read from the environment. describe(schema).requirementsreportsminVersion.postgres(the same floor asminPostgres), the extension list, the catalog features the schema uses, and a cost line for each emulated feature.okm build,okm check, andokm doctorcompare that list with the Postgres registry exported fromokmodel/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 everyokmcommand that loads a config; default discovery staysokmodel.config.ts. A missing file names that path and--config. plan,apply, andstatusonokmodel/migrateshare the CLI migrate engine for CI and deploy without shelling out (D298).okm migrate baselineandbaseline(target)fromokmodel/migraterecord 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)onokmodel/migratereportsminPostgresandextensions(each with a reason).okm doctorandokm checkrefuse a server below that floor (OKM1802 / OKM1803) or missing a required extension (OKM1811) (D300).bun run ci:reportprints job and step duration, queue time, the critical path, and re-run counts for recentmainruns, and why each 0.9.1 pull failed a push.bun run verify:fastchecks format, lint, types, layers, purity, docs, and the readme, then runs only the tests related to files changed since the merge-base withorigin/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 / checkcomparesnpm pack --dry-run --jsonto a committed snapshot of packed files and theexportsmap (scripts/pack-contract.ts). A forbidden install script, a dependency, a required peer, or a missingfilesallowlist fails the check. Update the snapshot withbun ./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, andnpm audit signatures. With--recordedit 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.1not being on main is a warning.- Postgres CI starts PgBouncer 1.26.1 (
edoburu/pgbouncerv1.26.1-p0, pinned by digest) in transaction mode with pool size 1, in front of the topology, on theneeds: postgreslabel and the full tier. Tests opt in withOKM_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 aSEToutside a transaction is visible to the next client. PgBouncer 1.26.1 supportsmax_prepared_statementsin 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 idokm migrate statusprints. A protected target needs the protected-target allowance. There is no skip.okm migrate statusJSONdataaddssteps[](id,kind,state,attempt,nextAction) for committed steps that are in flight or broken.id,kind, andstateare untrusted objects. The text table is unchanged, and astepssection follows when a row exists.okm migrate plan --jsonaddsoperationClasson the plan and on each step,fromCatalogHash,toCatalogHash,planHash,requiresAck, anddestructiveOperations.planHashis 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.requiresAckis true when the plan class isdestructive. 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, andmodified. It is bounded and marked untrusted; this reduces risk and does not eliminate it. The human line isuntrusted <origin>: "<text>". - A failed committed step is recorded in
okm_steps(started,failed, orretry) and removed in the same transaction that insertsokm_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 --jsonerrors keep the sixokm.cli/1fields and addlocation,fixEdit,safeAlternatives,docs, andescapeHatcheswhen the producer or the registry has them.fixstays a string. A kind-only error such asdriverstays at the six fields.schemas/cli-1/errors.jsonis the array schema.okm doctor <code>printsdocs/errors/<code>.md.okm protect add,okm protect remove, andokm protect listread and write the user fingerprint store.addrecords the selected target.removerequires a terminal.listprints the stored fingerprints. The JSON data is the fingerprint, withidentitynull, 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 testprints 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.tsfails when the packed tarball grows by more than 400 KB overtests/fixtures/pack-size.json.docs:checkalso 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 typecheckcompiles the public type surface and the*.test-d.tsconsumer fixtures with TypeScript 7.bun run ts6:checkcompiles the same files with TypeScript 6.0.x (typescript-editor) and is not part ofbun run check.- A committed step may declare
-- probe: <SELECT>above its statement. The probe is one read-onlySELECTthat returns a single boolean: true means the step is already applied. It runs insidebegin read onlyand is rolled back. A built-in probe for the step's kind still wins, and a declared probe has no cleanup.okm migrate resolve --applieduses 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-allowwith 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 syncdiscovers staticraw()calls imported fromokmodel/pg, describes each statement without running it, and writes.okm/raw.lock.jsonand.okm/raw.d.ts.--checkwrites nothing and exits 1 when either file would change.okm checkfails 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,
attnotnullis set, the column is not generated, no NOT NULL constraint on that column is unvalidated, and the statement has none ofjoin,union,intersect,except,lateral,with,values,rollup,cube, orgroupingoutside strings, comments, and quoted identifiers.as "name!"forces NOT NULL andas "name?"forces nullable. The suffix is not part of the name. PostgreSQL 15โ17 have nocontypenrow, so that check does not change them. On 18 aNOT VALIDconstraint setsattnotnullwhile NULLs can remain, and the column stays nullable. okm initwrites starter files for one package (okmodel.config.ts, schema, client, scripts, and.okm/init.json) and does not connect.okm generate initstill 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 initin 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 throughokmodel.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.okmodeliscatalog:only when a pnpm catalog already lists it.- A command loads the nearest
okmodel.config.tsand prints that path. Two files at the same distance are OKM1891.okm initwith several workspaces and no terminal is OKM1892. Two packages that share one migrations directory are OKM1893 and nothing is written. okm checklists 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.tsprints minified bytes and a gzip share for each source module on every gated graph fromscripts/size.ts. It fails when a total differs from that script.--updaterewritestests/fixtures/size-attribution.json. The pull-request jobinform / sizeprints the delta and is not a required check.bun ./scripts/dead-exports.tslists exports undersrc/that nothing insrc/,tests/, orscripts/imports and that are not on the public API surface.bun ./scripts/duplicates.tslists duplicate and near-duplicate function bodies and repeated large literals undersrc/.okm checkandokm migrate checklist every-- okm-allowline 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 markedDESTRUCTIVE.okm.cli/1data addsallows[]for both commands (additive;schemas/cli-1/check.jsonandmigrate-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 checkruns 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 afilters()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 levelwarn, phasecheck, evidencestatic. Findings print one per line and fillwarnings[]withplaceset totableortable.column. Exit stays 0.warnings.levelssetsoff,hint,warn, orerror;errorexits non-zero with the rule code. A rule that cannot decide (an expression index, a preset that needs its arguments, afilters()field that is not a literal) stays silent.filters()keeps nothing at runtime, sookm checkreads it from source with theokm raw synclexer: theraw.includefiles (defaultsrc/**), the schema file, and thetablesdirectory. Only literalallowkeys andsortstrings on a receiver bound to onetable("name", ...)count.- A project warning ignore takes an optional
target(eventsororders.user_id, SQL names).okm check --jsonaddstargetandstatus(appliedorstale) to eachdata.ignoresentry. An ignore for an OKM2101 to OKM2103 rule that matched nothing isstaleand is also reported as a warning.schemas/cli-1/check.jsongains 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 notarget. 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.--bundletakes no code and no--target.okm schema doctorandschemas/cli-1/doctor.jsondescribe the document (X7, O-6, D307, #511).okm agents status | update | remove [dir]manages theAGENTS.mdblock betweenBEGIN OKMODEL v<version>andEND OKMODEL, a thinCLAUDE.md@AGENTS.mdimport, and the packagedokmodelskill 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--agentsnames it, and the SHA-256 of each written file goes into.okm/init.json.okm agents statusprintscurrent,stale,missing, ormodifiedper file, exits 1 with OKM1881 on drift, and warns whenCLAUDE.local.mdexists.updaterewrites the block and any unmodified skill file.removedeletes 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-agentsrecords that the project has none so init never asks again. Init asks only on a terminal.--yesor no terminal never installs agent files unasked.- Agent file writes come only from the installed package. They print a plan and a diff,
--dry-runwrites nothing,--plan-hashpins the plan, and a change to an existing file needs--yesor 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
OKMODELmarkers 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, andagents-remove.jsondescribe the--jsondata, andinit.jsongains anagentssection. - The packed text scan (
docs:check) covers every file underskills/, not onlyskills/okmodel/SKILL.md. okm doctor --productionchecks one target for misuse and changes nothing: every query runs in oneREAD ONLYtransaction, and the output saystarget <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--targetis given (OKM1853), and 5 when the target cannot be read.--jsonreturns theokm.cli/1envelope withdata.production. Findings:target-protection(fails unless the config setsprotected: true; a name orNODE_ENVis never read; OKM2202),pooler(warns, fails with--allow-pooler; OKM2203),okm-meta(fails when absent; the doctor cannot seeconnect()options such asrequireMeta; OKM1520),role-privilege(superuser or BYPASSRLS fails; OKM1707),role-owns-tables(warns),role-config(withconfig.roles; OKM1707),lock-timeoutandstatement-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), andextensions(installed versions; OKM1811). The exposure check is not built: the catalog does not recordvalidation(D315, #523).--allow-pooleris accepted onokm doctoronly with--production.- OKM1709 is registered with a page.
session.localis native on Postgres in the capability registry andSESSION_SETTINGSreports the scope by dialect family:transactionfor Postgres,sessionfor the MySQL family,nonefor SQLite.assertSessionSettings("mysql" | "sqlite")refuses with OKM1860 and names column, composite, or path tenancy.docs/capabilities.mdlists the table. okm pull --casing snake|camelwritessqlNamewhen the key does not spell the database name.okm import drizzleno 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|nonesets the relative specifier (the default stays.ts). A partial index is emitted withindex(), 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) forconnect({ onWrite })and documentswriteObserver()onokmodel/observe.keysappends primary-keyRETURNING. Row counts record the command tag each driver exposes. Postgres costs arereturningandmixed. The closed profile staysnoneandunknown. - The same page documents
QueryEvent,toOtel(), thetagoption, andqueryObserver.timing.queueMsis 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 underpackages/spikes/describe/are not packed. - [When a migration fails](docs/migration-failure.md) records what a failed Postgres apply does, the
okm_historyandokm_stepscolumns, the step id, and why there is no skip. bun run docs:errorswritesdocs/errors/<code>.mdfor every registry code.docs:checkfails when a page is missing or differs from the registry. A code that needs a server saysExample: 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, andskills/okmodel/SKILL.md. Planning notes stay out of the tarball.okmVersionon each page is filled frompackage.jsonwhen the tarball is packed. The committed pages keep the train version, so a pre-release bump does not rewritedocs/. docs/index.mdis the pointer index: one line per public page, the command list, and the error-page range.bun run docs:indexrewrites it.docs:checkfails when the file is stale.- [Roles](docs/roles.md) is the least-privilege recipe for an agent:
GRANT pg_read_all_datafor inspection, and a separate migrator role withCREATEon the schema. It does not usedefault_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 atnode_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 initand nearest-config discovery. OKM1891, OKM1892, and OKM1893 have error pages. - The guide awaits
userFilters.parse(request.query)and passes the result tofindwith a limit.parseis 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:checkfails 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
targetignore, 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 whatchecksdoes not cover (D307). - The guide and the CLI reference describe
okm agents,okm init --agentsand--no-agents, the managed block, the closed list of writable paths, and aCODEOWNERSrecommendation forAGENTS.mdand.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 describeokm doctor --production, its finding ids, and what it cannot see (D315). OKM2201 to OKM2204 have error pages. docs/guide.mdand the handbook pagedefine/connect/session-settingscoverokmodel/session.docs/known-limits.mdsays 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 doctordoes not report limits yet, andfilters().parsetakes no clamp fromlimits.max: both are follow-ups in the plan.
โป๏ธ Changed
- CI reads Bun
1.4.3from.bun-version. The kill-storm wedge in Bun.sql is still there on that version. bun run pack-contractstores Bun content-hash chunks asdist/shared/*.jsanddist/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:fastwarns when the local Bun differs and still exits 0.gate / checkalways runs (if: always()), keys concurrency on the pull request number (or the commit onmain), and requires the full tier forfull-ci, achore(release)title, a change under.github/workflows, or a push tomain. 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 recommendokmodel/pg/pgorokmodel/pg/bunwhere 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/orsite/skips the runtime test matrix, Postgres, the type-cost step, and attest.gate / checkstays the required status and counts that skip as a pass only when the filter says those jobs are irrelevant. - CI builds
distonce 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 thedrivers-conformancegroup) runs with thefull-cilabel and on every push tomain. A later commit onmaindoes not cancel the run in progress. A failedgate / checkonmainopens an issue. - Docs:
connect({ hookm })is an array; a single observer object or ahookskey does not register observers. okm migrate checkprints, afterok N migrations, that an empty-table replay does not catch a step that fails only on real data:NOT NULLon 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 oforigin/main, the tag to matchpackage.json, and a## vchangelog section. It then runs the Postgres suite, the pack contract, andnpm stage publishwith provenance (npm 11.15 or newer, Node 22.14 or newer). A dispatch withpublishfalse checksmainand refuses a tag that is not onmain. The publish job is the only one withid-token: writeandcontents: write, and it uses thereleaseenvironment. 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 thetrusted()escape. The doctor text anddocs/errors/pages are regenerated.
runtime
- Thrown
summaryandfixtext no longer names a bypass.all(),unscoped(),--allow-protected, and--allow-poolerstay onescapeHatches, 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
fixis the primary remedy. A bypass moves toescapeHatches. - 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 NULLon 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 VALUEand 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 REPLACEfor 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 numericminPostgresalready on the input (maxwith the computed floor), and index opclass scan reads every*_opstoken case-insensitively, sodescribe(describe(s))keeps 17 and 18, a description's floor does not fall to 15, and a latergist_trgm_opsaftertext_pattern_opsstill reportspg_trgm(R5, #402).okm migrate checkandokm checkreprint table checks (including picklists) on the connected server before planning, so an authorIN (...)spelling is not permanent OKM1547 or a NOT VALID/VALIDATE swap againstpg_get_constraintdef(R2, #399, D128). Checkcolumnsare left out of the plan comparison when an author check omits column handles.okm checkchecks 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 atokm migrate apply. Several targets and no--targetprintserver checks skipped: N targets, pass --target <name>fromokm doctorandokm check(#400).- Validated
insert/updatehandles expose~planagain, sobatch()andisolation()work withvalidation: true. A failing rule inside a batch rolls everything back and reports OKM1200 withissuesandbatchIndex(D302, Q1). - The linter no longer refuses the planner's own
NOT VALIDthenVALIDATEpair 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 OKM1512onDROP 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, andisolation()falls back to a plain refresh when a concurrent refresh says the view is not populated, sorefresh: "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 solists.id cannot be writtenno longer fails (B-32). - Documented that LIFO PgBouncer on a non-heuristic port can pass the pid probe: mark it with
?pgbouncer=true(or apooler/pgbouncerhost). 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_EXCEEDEDat 65,534 (B-48). - A node-postgres call with
timeoutorsignalremoves its checkouterrorlistener, 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 statusstayscurrent,generatestaysno changes, andokm migrate checkno longer fails with OKM1549 when the only catalog drift is external columns (B-45). okm migrate checkreprints domain checks before planning, so a non-canonical author check such asVALUE > 0is OKM1552 only and not permanent OKM1547 whengeneratesays no changes (B-37).- A trigger
WHENthat namesOLDandNEWno longer failsokm check/okm migrate checkwith "expression contains variables of more than one relation"; introspection reads the predicate frompg_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 inpublic(B-47).- The guide roles flow on a fresh database no longer fails with
SAVEPOINT can only be used in transaction blocksduring provision: schema ownership runs without a savepoint when no transaction is open,CREATEROLEis 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
Insertomits defaulted and nullable columns, andUpdateaccepts any subset of the writable columns, matchinginsert()andupdate(). A missing required column and an unknown key stay type errors (R7, #404).ColumnDefinitionadds the optionalunvalidatedNotNull(the constraint name). Introspection sets it on PostgreSQL 18 for a column whose NOT NULL constraint isNOT 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 atschema(). 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 VALIDconstraint (pg_constraintcontypen,convalidatedfalse) instead of treatingattnotnullas proof. The column stays NOT NULL and records the unvalidated constraint. When the schema says NOT NULL, the plan is oneVALIDATE CONSTRAINTstep. When it says nullable, the plan isDROP NOT NULL. PostgreSQL 15 through 17 read as before (D316). - Column handles in
sqland 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 atschema()(#537, #545).
adapters
- A Bun.sql pool that answers
connection must be a PostgresSQLConnectionafterpg_terminate_backendis replaced. The next call dials again. Poolclose()returns within a second when Bun's ownclose()does not (oven-sh/bun#46166). Still present on Bun 1.4.3. - Bun.sql stamps SQLSTATE 08006 on a dead socket (
Connection closedorconnection must be a PostgresSQLConnection), so the failure isunavailable. A commit alreadyoutcome_unknownstays OKM1401 (#567). - A node-postgres pool does not retry a read after the connection fails. The killed client is discarded (#568).
db.capabilitiesis frozen. A write to a driver flag is rejected (#546).- PGlite loads a declared contrib extension before wasm (#554).
runtime
where,orderBy(includingrelevance()), and search accepthidden({ 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).
onQueryreads inside a savepoint share the transaction's unit id, and theparamsof 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" }inonQuery, notprimary(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, andcountreject an unknown option beside a valid one, andupdatewithversion: "any",lock,limit, and{ allow }on a guarded column typechecks (#560, #562).
tooling
- The managed
AGENTS.mdblock andskills/okmodel/SKILL.mdno longer name--accept-destructive, a flag that does not exist. They name the guards that do:--allow-protected,--accept-unverified,--allow-pooler, and-- okm-allowlines in migration files.renderAgentsBlockanddocs:checknow 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-allowline. - A protected-target refusal names the match: project config, a store fingerprint, or both.
okm migrate checkreuses an extension that is already installed. The replay moves it into the scratch schema for the transaction, andROLLBACKrestores the schema apply left, so a declared extension still passes after apply (R6, #403).okm ext checkprints the extension name in the declared column when the pin is omitted. A pin still prints that version or floor (R6, #403).okm migrate checkrefuses auuidv7()default on PostgreSQL 15, 16, and 17 with OKM1812 and the fixuuidv7() needs PostgreSQL 18 or higher. minPostgres: 18.PostgreSQL 18 replays that default (R6, #403).lint / pull requestassigns the open train milestone whenCloses #Nhas none.0.10.0-next.Nuses milestone0.10. The issue and the pull request both receive it.- A no-op CI event mirrors the last finished
gate / checkand 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 CONCURRENTLYis dropped before apply returns, so the target does not keep an INVALID index. When that drop fails, the error names the index andDROP 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. Anignoredobject is left in place.migrate: falsedrops nothing (Q9, #396).- The packed-tarball size check runs for this repository. A fixture passed to
pack-contractwith--rootdoes not needtests/fixtures/pack-size.json. okm checkreports a PostgreSQL 18NOT NULL โฆ NOT VALIDconstraint as drift (OKM1520) and names the column, even when no row holds NULL.okm pullwrites that column.nullable()with a comment and lists it undernot validated. A dependent catalog'sokm checktreats such an external column as stale (OKM1841). The linter reports itsVALIDATE CONSTRAINTas OKM1531 with the reasonvalidates a NOT NULL constraint.okm pushrefuses a plan that drops a column or a table that holds a row (OKM1898);--allow-data-lossapplies it and printsdropped <object>, while an empty column or table still applies with no dropped line. A drop.okm/pull.jsonlists stays OKM1897, and dropping a unique or primary-key constraint alone is applied (#563, #564).okm checkreports a reserved column or index name as OKM1122, asokm pushdoes, unless.okm/pull.jsonalready lists that name.okm pullwrites that record at the project root even with--out; a scratch pull or another target's--schemaleaves it, and--outof the same database refreshes it (#561, #562, #563).- OKM1894:
testing()found a table, type, or extension that already exists. Use an empty database, ormigrate: false(#542, #561). - OKM1895: the command line was not accepted, including
--jsonusage and a missing config file.okm --helpandokm <command> --helplist 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, notCREATEon the schema (#552). - OKM1897:
okm pushwould drop a column or constraint thatokm pulllisted under not pulled yet. Those objects stay, because pull did not write them (#561, #563). - OKM1898:
okm pushwould drop columns and tables that hold rows. Keep them, or pass--allow-data-loss(#563, #564). - OKM1899:
okm migrate applyandokm pushrefuse a table that already ownsokm_meta,okm_history,okm_backfill, orokm_stepsand 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 applyhonorsminPostgresand 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 whenminPostgresis 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 aCASEnewline is not OKM1122 and an INVALID index is omitted from the live catalog.CREATE UNIQUE INDEX โฆ IF NOT EXISTSstill 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 checkagain, 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
WHENis reprinted before compare,okm checkreads views on the namespacesearch_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
WHENin place. A function with the same signature is replaced, and a trigger whoseWHENchanged is dropped and created; checks, indexes, and policies still rewrite in place (#548). okm migrate checkdoes not move an installed extension into the scratch schema, and reprint uses temporary objects instead ofokm_expr*in the live schema.COMMIT,END,ROLLBACK, andABORT, includingWORK,TRANSACTION, andAND [NO] CHAIN, are OKM1551, andokm pushrefuses 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. UnquotedCREATE EXTENSIONnames 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 checkandokm doctorskip OKM1811 before the initial migration when the package is available, and they still report it afterDROP EXTENSIONon an applied database. A role that cannot install an extension named in a pending migration is OKM1811, anddescribe()reports uuid arrays, column tenancy, and theuuidv7case (#552, #554, #561).okm pullwrites a generated-always integer primary key ast.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 onsearch_path(#561, #562).okm pulllists unmanaged row-level security and does not drop it after baseline, appendsNameto 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 billingrewrites a key topublic.usersas.references("users"); repeating--schemawrites nothing (OKM1023 when the name is in both, OKM1021 when the target is outside the one namespace) (#555, #562).okmexits when stderr is closed (EPIPE) instead of writing again and spinning. A dangling agent skill link is refused beforeokm initwrites (OKM1886),okm doctor --bundlerefuses a symlink that leaves the project (an existing file without--force-fileis OKM1855), a secondokm generatelints the pending migration again and prints each finding once,protected.jsonis mode 600, andwriteObserverrefuses an unknown option (#542).
docs
- Known limits names
postgresjs-kill-storm.mdagain. 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 VALIDstate. D316 records what each command does with it. - The observer page says
seqhas 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. Deletingprotected: truedoes 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
parametersorargsonerror.causewhen the driver omitstable(B-33 residual, #364). SECURITY.mdnames 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, andexplain()sends the real parameters and then replaces hidden and sensitive values inparamsand in the plan text. BatchonQueryparams 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).