@everystack/model 0.4.16 → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,213 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@everystack/model` are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - automate the version-drift check — a manual note is not a gate
12
+
13
+ ## [0.4.18] - 2026-09-16
14
+
15
+ ### Added
16
+
17
+ - auto-generated changelogs from conventional commits
18
+ - version-guard the Model-derived wire payloads (provision, converge-ownership, reconcile)
19
+
20
+ ### Fixed
21
+
22
+ - the shape-pin also covers enum value domains, not just field names
23
+ - symmetric v-prefix in the mismatch message, restore db:provision's --direct detail
24
+
25
+ ## [0.4.17] - 2026-08-15
26
+
27
+ ### Fixed
28
+
29
+ - compile owner-scoped `manage` to write policies + add `internal` flag for functions
30
+
31
+ ### Documentation
32
+
33
+ - softDelete was documented nowhere a model author reads
34
+
35
+ ## [0.4.16] - 2026-08-12
36
+
37
+ ### Fixed
38
+
39
+ - the softDelete guard asked about visibility, not about DELETE
40
+ - close the gate findings — the rule was still one grant short
41
+
42
+ ## [0.4.15] - 2026-08-10
43
+
44
+ ### Added
45
+
46
+ - defineFunction takes an owner
47
+ - keep the index name the database already has
48
+ - keep the foreign key constraint name too
49
+ - capture GENERATED ALWAYS AS IDENTITY through the round trip
50
+ - a view's owner is a privilege boundary too — declare it, build it
51
+
52
+ ### Fixed
53
+
54
+ - db:pull reads the search_path, it does not invent one
55
+
56
+ ## [0.4.14] - 2026-08-08
57
+
58
+ ### Added
59
+
60
+ - rls: false declares a grants-only table — row security as the database holds it
61
+
62
+ ## [0.4.13] - 2026-08-06
63
+
64
+ ### BREAKING CHANGES
65
+
66
+ - a grant with no policy is spellable, and no longer renders as an ability
67
+
68
+ ### Added
69
+
70
+ - a live EXECUTE grant is a fact, so let a function record one
71
+ - db:pull renders trigger bindings, as standalone descriptors
72
+ - record a relation's inexpressible grants instead of skipping the object
73
+
74
+ ## [0.4.12] - 2026-08-04
75
+
76
+ ### Added
77
+
78
+ - writtenBy: 'functions' must have functions, checked at composition
79
+
80
+ ## [0.4.11] - 2026-08-04
81
+
82
+ ### Added
83
+
84
+ - policyRoles — the policy's audience, not its grantees
85
+ - field.pgType — a verbatim type is its own identity
86
+ - make column grants first-class
87
+
88
+ ### Fixed
89
+
90
+ - one definition of a public read, and writtenBy stops guessing
91
+
92
+ ## [0.4.10] - 2026-08-02
93
+
94
+ ### Added
95
+
96
+ - the model can say REFERENCES, TRIGGER, TRUNCATE
97
+
98
+ ## [0.4.9] - 2026-08-02
99
+
100
+ ### Fixed
101
+
102
+ - db:pull dropped the owner branch of a `public OR mine` read
103
+
104
+ ## [0.4.8] - 2026-08-01
105
+
106
+ ### Added
107
+
108
+ - the authz vocabulary compiles what it declares
109
+ - a function is identified by its name AND its argument types
110
+ - a role the models cannot name keeps its grants, and is named on every run
111
+ - stripping an undeclared grantee is destructive, and db:check verifies the baseline artifact (cli 0.4.41, model 0.4.7)
112
+
113
+ ### Fixed
114
+
115
+ - soft-delete visibility is declared, never inferred from a column name
116
+
117
+ ## [0.4.5] - 2026-07-23
118
+
119
+ ### Added
120
+
121
+ - db schema-move primitive — movedFrom marker → ALTER TABLE SET SCHEMA
122
+
123
+ ## [0.4.4] - 2026-07-17
124
+
125
+ ### Added
126
+
127
+ - field.json() — the Postgres json type, distinct from jsonb
128
+
129
+ ## [0.4.3] - 2026-07-16
130
+
131
+ ### Added
132
+
133
+ - canonical-sync artifact verbs (gap B)
134
+
135
+ ## [0.4.2] - 2026-07-16
136
+
137
+ ### Fixed
138
+
139
+ - array field .default() takes a JS array, not a PG literal
140
+
141
+ ## [0.4.1] - 2026-07-10
142
+
143
+ ### Added
144
+
145
+ - the loud undefined gate — a missing import is named at define time, never silently dropped
146
+
147
+ ## [0.4.0] - 2026-07-10
148
+
149
+ ### Added
150
+
151
+ - field.defaultSql() — the raw-default escape hatch (Brick C)
152
+ - the declarative derived layer — descriptor API + rejection rules (Brick B1a)
153
+ - index identity sees everything CREATE INDEX can say (Brick E, read-model-everywhere)
154
+ - typed reads — declared fields emit the derived layer into the generated schema (Brick B6a, read-model-everywhere)
155
+ - descriptors go live in the verbs — the B3 wiring (read-model-everywhere)
156
+ - the B2 gates — the read model is enforced, not just expressible (read-model-everywhere)
157
+ - live drift + doctor — what the content hashes can't see (Brick B4, read-model-everywhere)
158
+ - db:pull renders the derived layer — the consumer's migration tool (Brick B5, read-model-everywhere)
159
+ - retire db/sql — descriptors are the single home (Brick B7, read-model-everywhere)
160
+ - db:generate --dry-run + the journal remembers schema-out (Brick D, read-model-everywhere)
161
+ - db:reconcile --rebaseline — the defHash-verified migration for source re-renders (red-team blocker, read-model-everywhere)
162
+
163
+ ### Fixed
164
+
165
+ - adoption-path fixes from the red team (read-model-everywhere)
166
+ - reconcile apply hardening — session guard, real CONCURRENTLY fallback (red team, read-model-everywhere)
167
+ - reachability gate mirrors the grant compiler — private models reach their granted roles (red team, read-model-everywhere)
168
+
169
+ ## [0.3.6] - 2026-07-04
170
+
171
+ ### Added
172
+
173
+ - losing data should be hard — classification, deprecated(), role-gated destructive (brick 9)
174
+
175
+ ## [0.3.5] - 2026-07-04
176
+
177
+ ### Added
178
+
179
+ - via: transitive-owner RLS — own a child through its parent
180
+ - toDrizzleTable — derive the runtime drizzle table from a Model
181
+ - writtenBy + the FORCE axiom — RLS posture is a model property
182
+ - per-model schema — a model declares the Postgres schema it lives in
183
+ - private — a model can be generated without being an API resource
184
+ - own dist.blobs as a v3 Model — schema + authz
185
+ - own the notification_* tables as Models + array vocabulary
186
+ - derive the FORCE-RLS carveouts from writtenBy — drop the hand-list
187
+ - column-scoped grants + cross-schema FK / index emission
188
+ - db:doctor functions teeth + derive every FORCE carveout
189
+ - defineModule — a package's full DB slice (models + extensions + functions)
190
+ - field.serial, field.real, field.doublePrecision; model 0.3.4
191
+ - table-level renamedFrom — rename intent through the diff (brick 6)
192
+
193
+ ### Documentation
194
+
195
+ - v3 (0.3.0) documentation review — accurate READMEs + guides
196
+ - correct comments + README to the shipped 0.3.0 reality
197
+
198
+ ## [0.3.0] - 2026-06-27
199
+
200
+ ### Added
201
+
202
+ - defineModel + handler-config derivation (v3 Phase 0)
203
+ - renamedFrom — the opt-in column rename, so the diff preserves data
204
+ - P3 field vocabulary — numeric, varchar, date
205
+ - P3 enum — CREATE TYPE, pg_enum introspection, round-trip
206
+ - P4a FK referential actions + tougher constraint parser
207
+ - P2.1 table-level constraints — composite unique, multi-col check
208
+ - P2.2 indexes — index() builder, round-trip
209
+ - P4b foreignKey() table-level builder for composite FKs
210
+
211
+ ### Documentation
212
+
213
+ - P7 add README — the v3 Model on-ramp
package/README.md CHANGED
@@ -110,6 +110,50 @@ defineModel('posts', {
110
110
 
111
111
  `privileges` takes only those three, and it is additive — it widens what a role holds and can never narrow it. DML stays in `can()`, where the grant and the row policy are decided together. `db:pull --abilities live` writes this key from the live grants, so an existing database that grants its admin role all three keeps them.
112
112
 
113
+ ## Soft delete
114
+
115
+ A `deletedAt` column is a column. `softDelete` is the decision about what it MEANS, and it sits
116
+ on the model beside `fields`:
117
+
118
+ ```typescript
119
+ export const Upload = defineModel('uploads', {
120
+ fields: {
121
+ id: field.uuid().primaryKey().defaultRandom(),
122
+ accountId: field.uuid().notNull().references(() => Account),
123
+ s3Key: field.text().notNull(),
124
+ deletedAt: field.timestamptz(),
125
+ },
126
+ softDelete: true,
127
+ abilities: [can('read', { owner: 'accountId' }), can('delete', { owner: 'accountId' })],
128
+ });
129
+ ```
130
+
131
+ It changes two different things, and neither default is safe:
132
+
133
+ - **Durability, on every model** — `true` makes a `DELETE` mark `deleted_at` instead of removing
134
+ the row, and hides marked rows from the data API's reads and updates. `false` makes `DELETE`
135
+ permanent.
136
+ - **Visibility, only where there is a public read** — `true` AND-s `deleted_at IS NULL` into the
137
+ anon policy. On an owner-scoped or admin-only model the compiled SQL is identical either way,
138
+ so durability is the axis that matters there.
139
+
140
+ **`defineModel` refuses to guess** when a model has `deletedAt` AND either a public read or a way
141
+ to DELETE (`can('delete')`, `can('manage')`, or a `privileges` DELETE grant). Omit the line on
142
+ such a model and it throws at import, naming which branch it tripped. Where nothing reads
143
+ publicly and nothing deletes, the flag provably changes neither the SQL nor the handler, so
144
+ nothing is required.
145
+
146
+ Pick `true` if the row is meant to survive its own deletion — anything with a cleanup or purge
147
+ job, an audit trail, or rows other tables reference. Pick `false` if `deleted_at` here is
148
+ audit-only and a `DELETE` really should remove the row. It is not cosmetic: a table whose S3
149
+ cleanup job finds work by `deleted_at IS NOT NULL` orphans every file it should have purged if
150
+ the row is hard-deleted instead.
151
+
152
+ A column named `deleted_at` is not a statement of intent — it is carried for audit, for
153
+ app-layer filtering, for half-finished migrations. This used to be inferred from the field's
154
+ presence, which made the compiler author a security rule nobody wrote. `db:pull` writes the
155
+ flag explicitly for the same reason.
156
+
113
157
  ## Relations
114
158
 
115
159
  ```typescript
@@ -137,13 +181,18 @@ everystack db:pull --stage dev # reverse an existing database into model s
137
181
 
138
182
  ### Handler config
139
183
 
140
- `deriveHandlerConfig(models)` compiles the same Models into the authorization config `createHandler` consumes (exposed tables, hidden/protected columns, relations, row ownership) — so the HTTP handler and the database enforce the *same* declared rules, with no second source of truth.
184
+ `deriveHandlerConfig(models)` compiles the same Models into the authorization config `createHandler` consumes (exposed tables, hidden/protected columns, relations, row ownership, **`softDelete.tables`**) — so the HTTP handler and the database enforce the *same* declared rules, with no second source of truth.
141
185
 
142
186
  ```typescript
143
187
  import { deriveHandlerConfig } from '@everystack/model';
144
188
  const handler = createHandler(db, schema, deriveHandlerConfig(models));
145
189
  ```
146
190
 
191
+ Do not hand-write the per-table keys beside it. Each one is a view of the models, and a
192
+ hand-kept copy drifts silently: `softDelete.tables` losing two entries is what let a table start
193
+ hard-deleting rows whose S3 objects a purge job keyed on `deleted_at IS NOT NULL`. Handler-level
194
+ options that are not per-table — `auth`, `pgSettings`, `rpc`, limits — stay explicit.
195
+
147
196
  ## Programmatic API (`@everystack/cli/model`)
148
197
 
149
198
  The same compiler the CLI uses is importable for your own CI and tests — point it at your live DB and assert the declaration and the database agree, or red-team the deployed authorization:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/model",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Declare a table's schema, validation, and authorization once — compiled to migrations, RLS, and handler config",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
@@ -8,16 +8,24 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
- "README.md"
11
+ "README.md",
12
+ "CHANGELOG.md"
12
13
  ],
13
14
  "exports": {
14
15
  ".": {
16
+ "source": "./src/index.ts",
15
17
  "types": "./src/index.ts",
16
18
  "default": "./src/index.ts"
17
19
  },
18
20
  "./drizzle": {
21
+ "source": "./src/drizzle.ts",
19
22
  "types": "./src/drizzle.ts",
20
23
  "default": "./src/drizzle.ts"
24
+ },
25
+ "./wire": {
26
+ "source": "./src/wire.ts",
27
+ "types": "./src/wire.ts",
28
+ "default": "./src/wire.ts"
21
29
  }
22
30
  },
23
31
  "peerDependencies": {
package/src/derived.ts CHANGED
@@ -416,6 +416,15 @@ export interface FunctionDefinition {
416
416
  * PUBLIC-executable. Opinion lives in the linter, never in the parser.
417
417
  */
418
418
  privileges?: Record<string, string[]>;
419
+ /**
420
+ * Owner-only by design — no app role may ever EXECUTE this function.
421
+ *
422
+ * Parallel to `private` on views/matviews. An `internal` function is a helper called only
423
+ * by other SECURITY DEFINER functions running as the owner. The compiler still emits
424
+ * `REVOKE ALL … FROM PUBLIC`; `internal` tells the lint (`db:check`) the absence of callers
425
+ * is reviewed and permanent, not an oversight. Contradicts `abilities` and `privileges`.
426
+ */
427
+ internal?: boolean;
419
428
  comment?: string;
420
429
  body: RawSql;
421
430
  }
@@ -436,6 +445,7 @@ export interface FunctionDescriptor {
436
445
  abilities: Ability[];
437
446
  /** Recorded EXECUTE grants — see {@link FunctionDefinition.privileges}. */
438
447
  privileges: Record<string, string[]>;
448
+ internal: boolean;
439
449
  comment?: string;
440
450
  body: string;
441
451
  }
@@ -487,6 +497,12 @@ export function defineFunction(name: string, def: FunctionDefinition): FunctionD
487
497
  if (seen.has(a.name)) throw new Error(`function '${name}': duplicate argument '${a.name}'.`);
488
498
  seen.add(a.name);
489
499
  }
500
+ if (def.internal && (def.abilities?.length ?? 0) > 0) {
501
+ throw new Error(`function '${name}': internal: true declares it owner-only — it cannot also declare abilities. Drop one.`);
502
+ }
503
+ if (def.internal && Object.keys(def.privileges ?? {}).length > 0) {
504
+ throw new Error(`function '${name}': internal: true declares it owner-only — it cannot also declare privileges. Drop one.`);
505
+ }
490
506
  for (const a of def.abilities ?? []) {
491
507
  if (a.action !== 'execute') {
492
508
  throw new Error(`function '${name}': can('${a.action}') is not a function ability — functions carry can('execute', { role }) only.`);
@@ -526,6 +542,7 @@ export function defineFunction(name: string, def: FunctionDefinition): FunctionD
526
542
  dependsOn: def.dependsOn ?? [],
527
543
  abilities: def.abilities ?? [],
528
544
  privileges,
545
+ internal: def.internal ?? false,
529
546
  ...(def.comment ? { comment: def.comment } : {}),
530
547
  body: def.body.sql,
531
548
  };
package/src/wire.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @everystack/model/wire — the Model-derived WIRE CONTRACT.
3
+ *
4
+ * Three privileged ops actions take a payload the CLI computes FROM the Models barrel and ships to
5
+ * a deployed stage's ops Lambda: `db:provision`, `db:converge-ownership` (owner assignments) and
6
+ * `db:reconcile` (derived-object descriptors). The CLI on an operator's machine and the
7
+ * `@everystack/cli` bundled inside that Lambda are SEPARATE installs on independent release
8
+ * trains, so their idea of those payload shapes can drift — a field renamed, reinterpreted, or
9
+ * added with new required meaning rides through silently and lands as wrong ownership or a wrong
10
+ * derived object on a real database.
11
+ *
12
+ * This module is the fix's two halves:
13
+ *
14
+ * 1. THE SHAPES, declared ONCE. `@everystack/model` is the neutral package both sides already
15
+ * reach (the CLI depends on it directly; the server reaches it through
16
+ * `@everystack/cli/wire`, which re-exports this file). Before this, the CLI built these
17
+ * payloads as inline object literals and the server declared its own matching interfaces —
18
+ * two independent types that agreed by convention, plus one (`db:reconcile`'s `declared`)
19
+ * that was `any[]` on the wire. Now there is one definition and both sides import it.
20
+ *
21
+ * 2. THE VERSION, `MODEL_WIRE_CONTRACT`. Types alone cannot police two independently installed
22
+ * packages, so every one of those payloads carries `modelContract`, the server refuses on
23
+ * anything but EXACT equality, and the CLI refuses before dispatch when a stage's `_health`
24
+ * does not advertise the same integer. See MODEL_WIRE_CONTRACT below for the bump rule.
25
+ *
26
+ * TYPES AND THE CONSTANT ONLY — no logic lives here. The refusals are enforced where they are
27
+ * decided: `packages/server/src/model-wire.ts` (the server gate) and
28
+ * `packages/cli/src/cli/model-wire-lane.ts` (the CLI preflight).
29
+ *
30
+ * This is deliberately SEPARATE from `APPLY_CONTRACT_VERSION` (`@everystack/cli/apply`), which
31
+ * versions db:apply's PERSISTED plan artifact — written by `db:plan --out <file>` and applied
32
+ * later, possibly much later. This versions an EPHEMERAL payload computed and consumed in one
33
+ * round trip. Different lifecycles, different numbers; neither may be folded into the other.
34
+ */
35
+
36
+ /**
37
+ * The Model-derived wire-contract version. Hand-bumped, checked for EXACT equality on both sides.
38
+ *
39
+ * BUMP IT whenever any type in this file changes shape — a field added, removed, renamed, retyped,
40
+ * or given a new required meaning. `__tests__/wire-shape.test.ts` pins every field of every type
41
+ * here and FAILS with that instruction if you change one without bumping, so the bump cannot be
42
+ * forgotten. That test is what makes exact-equality sustainable.
43
+ *
44
+ * Exact equality, never `>=`: a false refusal costs one redeploy (minutes, reversible), a false
45
+ * accept risks wrong ownership or a wrong derived object on a production database.
46
+ *
47
+ * v1 — the first version. Shapes as they stood when the guard was introduced.
48
+ */
49
+ export const MODEL_WIRE_CONTRACT = 1;
50
+
51
+ /**
52
+ * Every Model-derived wire payload carries this. The server refuses a payload whose `modelContract`
53
+ * is missing or unequal BEFORE it opens a connection; the CLI refuses to dispatch to a stage whose
54
+ * `_health` does not advertise the same integer.
55
+ *
56
+ * Missing is a REFUSAL, never a default: a caller that does not send the field is by definition on
57
+ * a version that predates the field, which is exactly the skew being guarded.
58
+ */
59
+ export interface ModelWireEnvelope {
60
+ /** This CLI's MODEL_WIRE_CONTRACT. Absent = a CLI older than the handshake. */
61
+ modelContract?: number;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Ownership — the db:provision / db:converge-ownership payloads.
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * One (owner, schema) pair the Models declare for a function/view/matview — the ENACT unit.
70
+ * Provision grants the operator membership in `owner` and grants `owner` CREATE on `schema`.
71
+ */
72
+ export interface DeclaredOwner {
73
+ owner: string;
74
+ schema: string;
75
+ }
76
+
77
+ /** The derived-object kinds ownership can be reassigned for. */
78
+ export type AdopteeKind = 'function' | 'view' | 'materialized view';
79
+
80
+ /**
81
+ * One declared object to ADOPT — a function/view/matview the Models declare that already exists
82
+ * live under the wrong owner. Reassigning a SECURITY DEFINER object changes its runs-as identity
83
+ * at call time, which is why `secdef` rides the wire and why adoption needs `confirm`.
84
+ */
85
+ export interface Adoptee {
86
+ /** Reassign target: `api.foo(integer)` for a function, `api.v_report` for a view/matview. */
87
+ identity: string;
88
+ schema: string;
89
+ name: string;
90
+ kind: AdopteeKind;
91
+ /** SECURITY DEFINER — functions only; always false for a view/matview. */
92
+ secdef: boolean;
93
+ /** The live owner (e.g. `postgres`). */
94
+ currentOwner: string;
95
+ /** The owner the Models declare. */
96
+ declaredOwner: string;
97
+ body?: string;
98
+ }
99
+
100
+ /** The `db:provision` payload's Model-derived half (the credentials half is server-local). */
101
+ export interface ProvisionWirePayload extends ModelWireEnvelope {
102
+ /** The declared schema set, ordered and `public`-trailing — set as the ROLE default search_path. */
103
+ searchPath?: string[];
104
+ /** Accepted by the action's payload type; convergence itself runs as db:converge-ownership. */
105
+ declaredOwners?: DeclaredOwner[];
106
+ declaredObjects?: Adoptee[];
107
+ confirm?: boolean;
108
+ }
109
+
110
+ /** The `db:converge-ownership` payload — ENACT + ADOPT + the reader's read grants. */
111
+ export interface OwnershipWirePayload extends ModelWireEnvelope {
112
+ declaredOwners?: DeclaredOwner[];
113
+ declaredObjects?: Adoptee[];
114
+ confirm?: boolean;
115
+ adminRole?: string;
116
+ /** Present = also converge the reader's read grants over `searchPath`. */
117
+ readerRole?: string;
118
+ searchPath?: string[];
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Derived layer — the db:reconcile payload.
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /** The declared derived kinds. `trigger` and `sql` carry the extra fields their edges need. */
126
+ export type DerivedKind = 'function' | 'view' | 'materialized view' | 'trigger' | 'sql';
127
+
128
+ /** A statement that belongs to a derived object: its index, comment, or grant. */
129
+ export interface DerivedAttachment {
130
+ kind: 'index' | 'comment' | 'grant';
131
+ sql: string;
132
+ }
133
+
134
+ /**
135
+ * One compiled derived-object descriptor — the unit `db:reconcile` ships. The CLI's `SourceObject`
136
+ * IS this type (`packages/cli/src/cli/derived-source.ts` re-exports it), so there is one
137
+ * definition, not two that agree by convention. Field-by-field documentation lives with the
138
+ * compiler that produces them; what matters here is that the SHAPE is versioned by
139
+ * MODEL_WIRE_CONTRACT.
140
+ */
141
+ export interface DerivedDescriptor {
142
+ kind: DerivedKind;
143
+ /** Schema name, defaulted to `public` when unqualified. */
144
+ schema: string;
145
+ name: string;
146
+ /** `schema.name` — the reconciler's join key against the live catalog.
147
+ * (Triggers: `schema.table.name` — compound identity, joined against pg_trigger.) */
148
+ identity: string;
149
+ /** The CREATE statement as authored (no trailing semicolon). */
150
+ sql: string;
151
+ /** Indexes / comments / grants targeting this object, in source order. */
152
+ attachments: DerivedAttachment[];
153
+ /** sha256 of the normalized CREATE + attachments — comment/whitespace edits do not change it. */
154
+ hash: string;
155
+ /**
156
+ * sha256 of the normalized CREATE + attachments EXCEPT plain (non-column-scoped) grants — the
157
+ * "would a rebuild be required" hash. Equal across an authz-only change (plain grants moved, body
158
+ * unchanged), so the reconciler can route that to a bare GRANT delta instead of a matview rebuild.
159
+ * Column-scoped grants stay IN (attacl not introspected → they must rebuild). `hash` stays the
160
+ * full/stable formula for provenance compat; this is the additional discriminator.
161
+ */
162
+ bodyHash: string;
163
+ /** Source file this object came from (descriptors: the declared-models marker). */
164
+ file: string;
165
+ /** Position in the concatenated source — a valid dependency order by convention. */
166
+ seq: number;
167
+ /**
168
+ * kind 'function' only: the declared owning role.
169
+ *
170
+ * A SECURITY DEFINER function executes with its OWNER's rights, so this is a privilege
171
+ * boundary. The applier enacts it by running the CREATE under `SET ROLE <owner>` rather
172
+ * than following it with `ALTER … OWNER TO`: measured on PG16, once ownership has moved the
173
+ * builder gets "must be owner of function" on the next `CREATE OR REPLACE`, so the
174
+ * alter-after form works exactly once and breaks every re-run.
175
+ */
176
+ owner?: string;
177
+ /**
178
+ * kind 'function' only: the declared `SET search_path`, or the sentinel `'unpinned'`.
179
+ *
180
+ * It is already inside `sql`, so this is NOT in the hash and changes no fingerprint. It rides
181
+ * structurally so the differ can compare the declared pin against the LIVE catalog directly
182
+ * rather than only against a recorded hash. Without that, `--baseline` records a declared pin
183
+ * the database does not have and every later check reports zero actions, forever: measured,
184
+ * and it is how a model came to claim `search_path=pg_catalog` about a database with none
185
+ * while every gate stayed green.
186
+ *
187
+ * `undefined` means NOT DECLARED — a parser-era db/sql object, or an invoker function with no
188
+ * pin. The comparison is skipped there; absent evidence is not evidence.
189
+ */
190
+ searchPath?: string[] | 'unpinned';
191
+ /** kind 'sql' only: the declared PostgreSQL object kind (`'aggregate'`, …). */
192
+ objectKind?: string;
193
+ /** kind 'sql' only: the explicit DROP for kinds whose drop isn't derivable from the name. */
194
+ drop?: string;
195
+ /** kind 'trigger' only: the (possibly schema-qualified) table/view the trigger rides. */
196
+ table?: string;
197
+ /**
198
+ * Views/matviews: every declared `dependsOn` ref as an identity (models AND derived),
199
+ * sorted. B4's dependency drift verifies it against the live catalog's actual edges —
200
+ * "declared, then verified". Absent on kinds whose live edges pg_depend can't see.
201
+ */
202
+ declaredDeps?: string[];
203
+ /**
204
+ * kind 'function' only: the relations this function's SIGNATURE names — a `setof(ref)`
205
+ * return, or an argument/return type spelled as a declared table, view or matview.
206
+ *
207
+ * Distinct from `dependsOn`, and the distinction is load-bearing for the from-scratch build.
208
+ * A body reference is a FORWARD reference the build can defer (`check_function_bodies = off`
209
+ * is exactly that escape), so a function whose body reads a not-yet-created table still
210
+ * creates. A SIGNATURE reference is not deferrable: PostgreSQL resolves the return and
211
+ * argument types when the function is created, so `RETURNS SETOF stats_view.x_row` needs
212
+ * that view to exist already. `db:build` runs functions ahead of state (an RLS policy's
213
+ * predicate must resolve the function it calls) and uses this to hold back the ones that
214
+ * cannot go first; the compiler also orders the function after them.
215
+ *
216
+ * RELATIONS only — tables, views, matviews. A signature naming a declared ENUM, domain or
217
+ * composite type has the same problem and is NOT covered here, so it still fails in the
218
+ * ahead-of-state pass. Absent/empty means "names no declared relation", never "is safe".
219
+ *
220
+ * Not in the hash — it is derived from `sql`, so carrying it moves no fingerprint.
221
+ */
222
+ signatureDeps?: string[];
223
+ }
224
+
225
+ /** The `db:reconcile` payload — the declared derived layer plus the run's mode flags. */
226
+ export interface ReconcileWirePayload extends ModelWireEnvelope {
227
+ /** The declared derived objects. Was `any[]` on the wire before this contract existed. */
228
+ declared?: DerivedDescriptor[];
229
+ /** `{ [newName]: oldName }` — table renames the reconciler must follow. */
230
+ renamedTables?: Record<string, string>;
231
+ apply?: boolean;
232
+ baseline?: boolean;
233
+ rebaseline?: boolean;
234
+ rebuild?: boolean;
235
+ overwriteDrift?: boolean;
236
+ only?: string[];
237
+ actor?: string | null;
238
+ gitRef?: string | null;
239
+ }