@substrat-run/contracts 0.66.0 → 0.68.0

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/dist/index.d.ts CHANGED
@@ -34,4 +34,6 @@ export * from './deploy.js';
34
34
  export * from './ci.js';
35
35
  export * from './money.js';
36
36
  export * from './attachments.js';
37
+ export * from './model.js';
38
+ export * from './operations.js';
37
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -34,4 +34,6 @@ export * from './deploy.js';
34
34
  export * from './ci.js';
35
35
  export * from './money.js';
36
36
  export * from './attachments.js';
37
+ export * from './model.js';
38
+ export * from './operations.js';
37
39
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The entity registry (#697).
3
+ *
4
+ * The manifest describes permissions, events, guards, schedules, attachment
5
+ * targets, entity relations, searchables and UI contributions. It does not
6
+ * describe **entities**: `migrations` is a pointer (`journalDir` +
7
+ * `compatibleFrom`), the tables live in raw SQL the manifest never sees, and
8
+ * entity *type names* appear only as bare `z.string().min(1)` fragments across
9
+ * four unrelated, individually optional features.
10
+ *
11
+ * Nothing checks those four against each other or against the tables. A typo'd
12
+ * `parentType` in `entityRelations` parses cleanly and produces an edge that
13
+ * permission never flows along — the tuple evaluator walks a relation that does
14
+ * not exist, and a grant that should reach a child silently does not.
15
+ *
16
+ * This module gives them something to be checked against.
17
+ *
18
+ * ## What it is not
19
+ *
20
+ * Migrations do not move in here, and nothing about how tables are created
21
+ * changes. Whether the model becomes the source that migrations are *derived*
22
+ * from is #680/#685's question; the registry is a prerequisite either way.
23
+ */
24
+ import { z } from 'zod';
25
+ /**
26
+ * One entity: the table it lives in, its field schema, and its place in the
27
+ * permission graph.
28
+ */
29
+ export interface EntityDef<Names extends string = string> {
30
+ /** The physical table. Owned by this module — never another's (rule 4). */
31
+ readonly table: string;
32
+ /** The row shape. Field names are what `key`, `searchables` and events check against. */
33
+ readonly fields: z.ZodObject<z.ZodRawShape>;
34
+ /**
35
+ * The parent entity types permission may flow along (design doc §4.2 rule 3).
36
+ * Checked against the declared entities — a typo is a compile error, where it
37
+ * used to be a silently dead edge.
38
+ *
39
+ * **Plural, and an array even for one.** `entityRelations` is an ALLOWLIST,
40
+ * not an assertion: the kernel accumulates permitted parents into a *set* per
41
+ * entity type and `ctx.link` checks membership. `reservation` already hangs
42
+ * off both `resource` and `member`; `protocol` off both `workorder` and
43
+ * `employee`. Singular `parent` said "the parent", which is not what the
44
+ * kernel means and cannot express the real cases.
45
+ */
46
+ readonly parents?: readonly Names[];
47
+ /** Natural key, if any. Must name fields that exist. */
48
+ readonly key?: readonly string[];
49
+ /** Fields an erasure must be able to reach (§12). Must name fields that exist. */
50
+ readonly erasable?: readonly string[];
51
+ }
52
+ /** The field names of one entity, read off its own `fields` schema. */
53
+ export type EntityFields<E> = E extends {
54
+ fields: infer F;
55
+ } ? F extends z.ZodObject<z.ZodRawShape> ? keyof z.infer<F> & string : never : never;
56
+ /**
57
+ * Declare a module's entities.
58
+ *
59
+ * The constraint is self-referential — `parent` is checked against the map's own
60
+ * keys, and `key`/`erasable` against each entity's own fields — which is what
61
+ * makes the checks bite per-entity rather than as a union across all of them.
62
+ * Written the obvious way (an erased supertype) every one of them compiles clean
63
+ * and enforces nothing; see `test/model.test.ts`, which exists to prove they
64
+ * still bite.
65
+ */
66
+ export declare function defineEntities<T extends {
67
+ readonly [K in keyof T]: EntityDef<keyof T & string> & {
68
+ key?: readonly EntityFields<T[K]>[];
69
+ erasable?: readonly EntityFields<T[K]>[];
70
+ };
71
+ }>(entities: T): T;
72
+ /** The declared entity names. */
73
+ export type EntityName<T> = keyof T & string;
74
+ /**
75
+ * The serialisable form — the artifact of record.
76
+ *
77
+ * Everything downstream (migrations, the manifest, the route table, an ER
78
+ * diagram, a diff classifier) reads THIS, never the TypeScript. That is what
79
+ * keeps the authoring notation swappable: a different authoring layer is a new
80
+ * emitter writing the same JSON, and nothing downstream notices.
81
+ *
82
+ * Field schemas are rendered with `z.toJSONSchema`, the same conversion the
83
+ * OpenAPI builder already uses — so there is no second schema language anywhere
84
+ * in the pipeline.
85
+ */
86
+ export interface EmittedEntity {
87
+ readonly table: string;
88
+ readonly fields: Record<string, unknown>;
89
+ /** The permitted parent types, sorted. One shape, always. */
90
+ readonly parents?: readonly string[];
91
+ readonly key?: readonly string[];
92
+ readonly erasable?: readonly string[];
93
+ }
94
+ export interface EmittedModel {
95
+ readonly entities: Record<string, EmittedEntity>;
96
+ }
97
+ /**
98
+ * Render the registry to plain JSON. Deterministic: entities and their fields
99
+ * are emitted in sorted order, so the checked-in artifact diffs cleanly and a
100
+ * reordered declaration is not a spurious change.
101
+ */
102
+ export declare function emitModel<T extends Record<string, EntityDef>>(entities: T): EmittedModel;
103
+ /**
104
+ * `entityRelations` derived from the `parent` declarations, rather than written
105
+ * a second time by hand.
106
+ *
107
+ * Two descriptions of one fact is how they come to disagree — and the disagreement
108
+ * here is invisible, because a relation naming an entity that does not exist is a
109
+ * permission edge that silently never resolves.
110
+ */
111
+ export declare function entityRelationsOf<T extends Record<string, EntityDef>>(entities: T): {
112
+ entityType: string;
113
+ parentType: string;
114
+ }[];
115
+ /**
116
+ * The entity-referencing half of a manifest, narrowed to declared entities.
117
+ *
118
+ * Entity-name positions are written `keyof T & string` inline rather than as
119
+ * `EntityName<T>`. A type ALIAS is printed unresolved in diagnostics — the error
120
+ * names the alias and inlines the whole entity map — where the inline form lists
121
+ * the actual names:
122
+ *
123
+ * Type '"bkie"' is not assignable to type '"bike" | "customer"'.
124
+ */
125
+ type EntityRefs<T extends Record<string, EntityDef>, M> = {
126
+ readonly attachmentTargets?: readonly {
127
+ readonly entityType: keyof T & string;
128
+ readonly readPermission: string;
129
+ readonly writePermission?: string;
130
+ }[];
131
+ /**
132
+ * `fields` is checked against the NAMED entity's own fields — the only place
133
+ * in the manifest today where a field name appears at all, and nothing
134
+ * checked it.
135
+ */
136
+ readonly searchables?: M extends {
137
+ searchables: infer S;
138
+ } ? {
139
+ readonly [I in keyof S]: S[I] extends {
140
+ entityType: infer N;
141
+ } ? N extends keyof T & string ? {
142
+ readonly entityType: N;
143
+ readonly fields: readonly EntityFields<T[N]>[];
144
+ } : never : never;
145
+ } : never;
146
+ readonly entityViews?: readonly {
147
+ readonly entityType: keyof T & string;
148
+ readonly view: string;
149
+ }[];
150
+ /**
151
+ * The engine registries this module composes, so relation edges naming their
152
+ * entities can be checked.
153
+ */
154
+ readonly engines?: readonly Record<string, EntityDef>[];
155
+ /**
156
+ * Parent edges involving an entity this module does not own.
157
+ *
158
+ * A vertical legitimately declares these: an engine is entity-agnostic, so
159
+ * only the vertical knows that a work order hangs off a bike, or a protocol
160
+ * off a work order. **Both sides are checked** against the local entities plus
161
+ * every entity of every registry in `engines`.
162
+ *
163
+ * Local-to-local edges do not belong here — they are DERIVED from the
164
+ * entities' own `parents`, and declaring one twice is how two descriptions of
165
+ * a fact come to disagree.
166
+ *
167
+ * This replaces the `foreignChildOf` / `foreignChildren` pair, which existed
168
+ * only because foreign names were uncheckable. They are now, so the split has
169
+ * nothing left to say.
170
+ */
171
+ readonly relations?: readonly {
172
+ readonly entityType: (keyof T & string) | (M extends {
173
+ engines: readonly (infer R)[];
174
+ } ? NamesOf<R> : never);
175
+ readonly parentType: (keyof T & string) | (M extends {
176
+ engines: readonly (infer R)[];
177
+ } ? NamesOf<R> : never);
178
+ }[];
179
+ };
180
+ /** Every entity name in one registry. */
181
+ type NamesOf<R> = R extends Record<string, EntityDef> ? keyof R & string : never;
182
+ /**
183
+ * Compose the entity-referencing manifest fragments against the registry.
184
+ *
185
+ * `entityRelations` is absent by design: it is DERIVED from the entities'
186
+ * `parent` declarations (`entityRelationsOf`) rather than written a second time.
187
+ *
188
+ * Spread the result into the module's manifest:
189
+ *
190
+ * ```ts
191
+ * export const manifest = moduleManifest.parse({
192
+ * id: '@acme/vertical',
193
+ * …,
194
+ * ...manifestEntities(entities, {
195
+ * attachmentTargets: [{ entityType: 'contract', readPermission: 'x:read' }],
196
+ * searchables: [{ entityType: 'customer', fields: ['name'] }],
197
+ * }),
198
+ * });
199
+ * ```
200
+ *
201
+ * A typo in any `entityType` is now a compile error naming the declared
202
+ * entities, and a `searchables` field that the entity does not have is too.
203
+ */
204
+ export declare function manifestEntities<const T extends Record<string, EntityDef>, const M extends EntityRefs<T, M>>(entities: T, refs: M): {
205
+ attachmentTargets: NonNullable<M['attachmentTargets']> | [];
206
+ searchables: M['searchables'];
207
+ entityRelations: {
208
+ entityType: string;
209
+ parentType: string;
210
+ }[];
211
+ ui: {
212
+ entityViews: M['entityViews'];
213
+ };
214
+ };
215
+ /**
216
+ * The row type of a declared entity — what `ctx.sql.query` returns for it.
217
+ *
218
+ * `ctx.sql.query` leaves `T` to the vertical, so every handler writes its own
219
+ * row interface and the schema ends up described three times: the DDL, the
220
+ * registry, and a hand-written `interface CustomerRow`. This collapses the
221
+ * third into the second.
222
+ *
223
+ * ```ts
224
+ * export type CustomerRow = EntityRow<typeof calloutEntities, 'customer'>;
225
+ * ```
226
+ */
227
+ export type EntityRow<T extends Record<string, EntityDef>, K extends keyof T> = T[K] extends {
228
+ fields: infer F;
229
+ } ? F extends z.ZodObject<z.ZodRawShape> ? z.infer<F> : never : never;
230
+ export {};
231
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The entity registry (#697).
3
+ *
4
+ * The manifest describes permissions, events, guards, schedules, attachment
5
+ * targets, entity relations, searchables and UI contributions. It does not
6
+ * describe **entities**: `migrations` is a pointer (`journalDir` +
7
+ * `compatibleFrom`), the tables live in raw SQL the manifest never sees, and
8
+ * entity *type names* appear only as bare `z.string().min(1)` fragments across
9
+ * four unrelated, individually optional features.
10
+ *
11
+ * Nothing checks those four against each other or against the tables. A typo'd
12
+ * `parentType` in `entityRelations` parses cleanly and produces an edge that
13
+ * permission never flows along — the tuple evaluator walks a relation that does
14
+ * not exist, and a grant that should reach a child silently does not.
15
+ *
16
+ * This module gives them something to be checked against.
17
+ *
18
+ * ## What it is not
19
+ *
20
+ * Migrations do not move in here, and nothing about how tables are created
21
+ * changes. Whether the model becomes the source that migrations are *derived*
22
+ * from is #680/#685's question; the registry is a prerequisite either way.
23
+ */
24
+ import { z } from 'zod';
25
+ /**
26
+ * Declare a module's entities.
27
+ *
28
+ * The constraint is self-referential — `parent` is checked against the map's own
29
+ * keys, and `key`/`erasable` against each entity's own fields — which is what
30
+ * makes the checks bite per-entity rather than as a union across all of them.
31
+ * Written the obvious way (an erased supertype) every one of them compiles clean
32
+ * and enforces nothing; see `test/model.test.ts`, which exists to prove they
33
+ * still bite.
34
+ */
35
+ export function defineEntities(entities) {
36
+ return entities;
37
+ }
38
+ /**
39
+ * Render the registry to plain JSON. Deterministic: entities and their fields
40
+ * are emitted in sorted order, so the checked-in artifact diffs cleanly and a
41
+ * reordered declaration is not a spurious change.
42
+ */
43
+ export function emitModel(entities) {
44
+ const out = {};
45
+ for (const name of Object.keys(entities).sort()) {
46
+ const e = entities[name];
47
+ if (!e)
48
+ continue;
49
+ const { $schema: _drop, ...fields } = z.toJSONSchema(e.fields, { io: 'output', target: 'draft-2020-12' });
50
+ out[name] = {
51
+ table: e.table,
52
+ fields,
53
+ ...(e.parents?.length ? { parents: [...e.parents].sort() } : {}),
54
+ ...(e.key ? { key: [...e.key].sort() } : {}),
55
+ ...(e.erasable ? { erasable: [...e.erasable].sort() } : {}),
56
+ };
57
+ }
58
+ return { entities: out };
59
+ }
60
+ /**
61
+ * `entityRelations` derived from the `parent` declarations, rather than written
62
+ * a second time by hand.
63
+ *
64
+ * Two descriptions of one fact is how they come to disagree — and the disagreement
65
+ * here is invisible, because a relation naming an entity that does not exist is a
66
+ * permission edge that silently never resolves.
67
+ */
68
+ export function entityRelationsOf(entities) {
69
+ return Object.keys(entities)
70
+ .sort()
71
+ .flatMap((name) => {
72
+ const parents = entities[name]?.parents;
73
+ if (!parents?.length)
74
+ return [];
75
+ return [...parents].sort().map((parentType) => ({ entityType: name, parentType }));
76
+ });
77
+ }
78
+ /**
79
+ * Compose the entity-referencing manifest fragments against the registry.
80
+ *
81
+ * `entityRelations` is absent by design: it is DERIVED from the entities'
82
+ * `parent` declarations (`entityRelationsOf`) rather than written a second time.
83
+ *
84
+ * Spread the result into the module's manifest:
85
+ *
86
+ * ```ts
87
+ * export const manifest = moduleManifest.parse({
88
+ * id: '@acme/vertical',
89
+ * …,
90
+ * ...manifestEntities(entities, {
91
+ * attachmentTargets: [{ entityType: 'contract', readPermission: 'x:read' }],
92
+ * searchables: [{ entityType: 'customer', fields: ['name'] }],
93
+ * }),
94
+ * });
95
+ * ```
96
+ *
97
+ * A typo in any `entityType` is now a compile error naming the declared
98
+ * entities, and a `searchables` field that the entity does not have is too.
99
+ */
100
+ export function manifestEntities(entities, refs) {
101
+ return {
102
+ attachmentTargets: (refs.attachmentTargets ?? []),
103
+ searchables: refs.searchables,
104
+ // Derived edges first, then the ones this module cannot check.
105
+ // Local edges are derived from the entities' own `parents`; edges involving
106
+ // a composed engine's entity are declared, and both sides are checked.
107
+ entityRelations: [...entityRelationsOf(entities), ...(refs.relations ?? [])],
108
+ ui: { entityViews: refs.entityViews },
109
+ };
110
+ }
111
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqCxB;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAO5B,QAAW;IACX,OAAO,QAAQ,CAAC;AAClB,CAAC;AA8BD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAsC,QAAW;IACxE,MAAM,GAAG,GAAkC,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,CAGvG,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,GAAG;YACV,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,MAAM;YACN,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAW;IAEX,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;SACzB,IAAI,EAAE;SACN,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QACxC,IAAI,CAAC,OAAO,EAAE,MAAM;YAAE,OAAO,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;AACP,CAAC;AAuED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAI9B,QAAW,EACX,IAAO;IAOP,OAAO;QACL,iBAAiB,EAAE,CAAC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAA6C;QAC7F,WAAW,EAAE,IAAI,CAAC,WAA+B;QACjD,+DAA+D;QAC/D,4EAA4E;QAC5E,uEAAuE;QACvE,eAAe,EAAE,CAAC,GAAG,iBAAiB,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC5E,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,WAA+B,EAAE;KAC1D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The operation surface of the model (#707).
3
+ *
4
+ * #697 declared the entities. This declares what can be *done* to them, and
5
+ * checks the joins that today are unchecked strings: which permission an
6
+ * operation requires, which output field an event takes its subject from,
7
+ * whether a payload carries something an erasure must be able to reach.
8
+ *
9
+ * ## A composer, not a second `defineModel`
10
+ *
11
+ * `defineOperations` sits beside `defineEntities` rather than swallowing it.
12
+ * Each half stays independently adoptable — which is what let the entity half
13
+ * ship and be taken up by two verticals before this existed. A vertical adopts
14
+ * operations when it is ready, not as the price of adopting entities.
15
+ *
16
+ * ## `input`, and the transcription that is not here
17
+ *
18
+ * `input` is the Zod object the handler already parses — the same object, not a
19
+ * description of it. That is the whole reason the model is TypeScript (#680): a
20
+ * schema language would need the shape written twice, and transcription is what
21
+ * produced 40 wrong argument names in the one app where this was measured.
22
+ */
23
+ import { z } from 'zod';
24
+ import type { EntityDef } from './model.js';
25
+ /** `{var}` names in a literal path. */
26
+ type PathParams<S extends string> = S extends `${string}{${infer P}}${infer Rest}` ? P | PathParams<Rest> : never;
27
+ type InputKeys<O> = O extends {
28
+ input: infer I;
29
+ } ? (I extends z.ZodType ? keyof z.infer<I> & string : never) : never;
30
+ type OutputKeys<O> = O extends {
31
+ output: infer R;
32
+ } ? (R extends z.ZodType ? keyof z.infer<R> & string : never) : never;
33
+ /** Every `{var}` must name an input field, or the path type collapses. */
34
+ type CheckedPath<O> = O extends {
35
+ http: {
36
+ path: infer P;
37
+ };
38
+ } ? P extends string ? [PathParams<P>] extends [InputKeys<O>] ? P : never : never : string;
39
+ /**
40
+ * The erasable fields OF THE ENTITY THIS EVENT IS ABOUT.
41
+ *
42
+ * Resolving through `emits.entity` rather than matching field names across all
43
+ * entities is what makes the §12 check exact. A `contactPerson.email` marked
44
+ * erasable must not stop an event about an `office` carrying its own `email` —
45
+ * a rule that refuses correct code trains people to route around it, which is
46
+ * how a PII rule stops being obeyed.
47
+ */
48
+ type ErasableOf<Entities, O> = O extends {
49
+ emits: {
50
+ entity: infer N;
51
+ };
52
+ } ? N extends keyof Entities ? Entities[N] extends {
53
+ erasable: readonly (infer F)[];
54
+ } ? F & string : never : never : never;
55
+ /**
56
+ * The platform's own event invariant, moved from runtime to compile time.
57
+ * `contracts/events.ts` enforces it with a `superRefine`: *"subjectId is
58
+ * required when piiClass is 'direct' — crypto-shredding must be able to key the
59
+ * erasure"*. Classification is mandatory here for the same reason it is there:
60
+ * an unclassified event type cannot be declared.
61
+ */
62
+ type PiiShape<O, OutKeys extends string> = O extends {
63
+ emits: {
64
+ piiClass: 'none';
65
+ };
66
+ } ? {
67
+ readonly piiClass: 'none';
68
+ readonly subjectId?: never;
69
+ } : {
70
+ readonly piiClass: 'pseudonymous' | 'direct';
71
+ readonly subjectId: OutKeys;
72
+ };
73
+ /**
74
+ * An operation carries a leading `permission` OR `narrows` with a reason, never
75
+ * both and never neither (rule 5 / CRM-EFF's check 14). `narrows` is the
76
+ * per-row proof walk: a salesperson listing their own customers must get their
77
+ * list, not a denial.
78
+ */
79
+ type OpAuthority<O, PermKey extends string> = O extends {
80
+ narrows: unknown;
81
+ } ? {
82
+ readonly narrows: {
83
+ readonly reason: string;
84
+ };
85
+ readonly permission?: never;
86
+ } : {
87
+ readonly permission: PermKey;
88
+ readonly narrows?: never;
89
+ };
90
+ /**
91
+ * The per-operation constraint, self-referential in `O`.
92
+ *
93
+ * Each operation is checked against ITS OWN declared input and output rather
94
+ * than an erased supertype. Written the obvious way every check below compiles
95
+ * clean and enforces nothing — see `test/operations.test.ts`, which exists to
96
+ * prove they still bite.
97
+ */
98
+ type OperationShape<O, Entities, PermKey extends string> = {
99
+ /** One line, imperative — what invoking this does. Feeds the API document. */
100
+ readonly summary: string;
101
+ /**
102
+ * The request body — the SAME Zod object the handler parses.
103
+ *
104
+ * **Omitted means no body at all**, and the handler then takes `undefined`.
105
+ * Found by the first adopter: three of Callout's six operations take no input,
106
+ * and a required `z.object({})` cannot say so — a handler accepting only
107
+ * `undefined` is not assignable to one accepting `{}`.
108
+ *
109
+ * This mirrors `ApiOperationDoc.input` ("Omit = no body") rather than
110
+ * inventing a second vocabulary for the same fact.
111
+ */
112
+ readonly input?: z.ZodObject<z.ZodRawShape>;
113
+ /** True when the handler accepts a body but also accepts none (filter-style reads). */
114
+ readonly inputOptional?: boolean;
115
+ /**
116
+ * Declared, not inferred (#695 Ask 2). Inference documents accidents: one
117
+ * inferred return carried `contacts?: undefined`, an artefact of an early
118
+ * return, which generation would have cemented into the published API.
119
+ *
120
+ * Declare a return where a caller branches on it — a UI lane is a caller that
121
+ * branches, which is why #682/#683 depend on this.
122
+ */
123
+ readonly output: z.ZodType;
124
+ readonly http?: {
125
+ readonly method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
126
+ readonly path: CheckedPath<O>;
127
+ };
128
+ readonly emits?: {
129
+ /** The entity the event is about — a declared entity. */
130
+ readonly entity: keyof Entities & string;
131
+ /**
132
+ * Which OUTPUT field carries that entity's id.
133
+ *
134
+ * The #695 defect: 18 operations emitted `entityId: String(result.id)` on
135
+ * objects that answer with `contractId` / `runId` / `instanceId`. For a
136
+ * mutation writing a child the event is about the PARENT, so the two differ
137
+ * and nothing downstream would ever have flagged it.
138
+ */
139
+ readonly entityIdFrom: OutputKeys<O>;
140
+ readonly type: string;
141
+ readonly schemaVersion: number;
142
+ /**
143
+ * Fat payload, drawn from the output — minus anything the entity marks
144
+ * `erasable`. Immutable events are the one place in a scope an erasure
145
+ * cannot reach.
146
+ */
147
+ readonly payload?: readonly Exclude<OutputKeys<O>, ErasableOf<Entities, O>>[];
148
+ } & PiiShape<O, OutputKeys<O>>;
149
+ /**
150
+ * Per-field permission on the projection: omission, not denial. The caller
151
+ * still gets the row, without the fields they may not see.
152
+ */
153
+ readonly gates?: {
154
+ readonly [F in OutputKeys<O>]?: PermKey;
155
+ };
156
+ } & OpAuthority<O, PermKey>;
157
+ /**
158
+ * Declare a module's operations against its entities and permission keys.
159
+ *
160
+ * Curried so the entities and permissions are given explicitly while each
161
+ * operation still infers its own input and output — a callback parameter cannot
162
+ * be contextually typed by a generic being inferred from the object containing
163
+ * it.
164
+ *
165
+ * ```ts
166
+ * export const ops = defineOperations(calloutEntities, PERMISSIONS)({
167
+ * 'customer/create': {
168
+ * summary: 'Register a customer',
169
+ * permission: 'customer:manage',
170
+ * input: z.object({ name: z.string() }),
171
+ * output: z.object({ id: z.string(), number: z.string() }),
172
+ * http: { method: 'POST', path: '/customers' },
173
+ * emits: {
174
+ * entity: 'customer', entityIdFrom: 'id',
175
+ * type: 'callout.customer-created', schemaVersion: 1, piiClass: 'none',
176
+ * },
177
+ * },
178
+ * });
179
+ * ```
180
+ */
181
+ export declare function defineOperations<const Entities extends Record<string, EntityDef>, const Perms extends readonly string[]>(_entities: Entities, _permissions: Perms): <const Ops extends { readonly [K in keyof Ops]: OperationShape<Ops[K], Entities, Perms[number]>; }>(operations: Ops) => Ops;
182
+ /**
183
+ * The permission keys an operation set actually requires, for the manifest.
184
+ *
185
+ * Read structurally rather than through a `{ permission?: string }` parameter:
186
+ * a `narrows` operation has neither `permission` nor `emits`, and TypeScript's
187
+ * weak-type rule rejects an object sharing no properties with the parameter.
188
+ */
189
+ export declare function permissionsUsedBy(operations: Readonly<Record<string, object>>): string[];
190
+ /** The event types an operation set emits, for `manifest.events.emits`. */
191
+ export declare function eventsEmittedBy(operations: Readonly<Record<string, object>>): {
192
+ type: string;
193
+ schemaVersion: number;
194
+ }[];
195
+ /**
196
+ * The handler map a declared operation set requires — CRM-EFF's `satisfies Impl`
197
+ * seam, which is what makes the declaration BINDING rather than decorative.
198
+ *
199
+ * ```ts
200
+ * export const operations = { … } satisfies OperationImpl<typeof calloutOps, OperationContext>;
201
+ * ```
202
+ *
203
+ * Four things become compile errors at the exact method: a handler whose input
204
+ * disagrees with the declared `input`, one whose return disagrees with the
205
+ * declared `output`, an operation declared and not implemented, and one
206
+ * implemented and not declared.
207
+ *
208
+ * `Ctx` is a parameter rather than `OperationContext` because contracts is below
209
+ * the kernel and must not import it. The vertical supplies it.
210
+ */
211
+ export type OperationImpl<Ops, Ctx> = {
212
+ [K in keyof Ops]: Ops[K] extends {
213
+ output: infer O;
214
+ } ? O extends z.ZodType ? (ctx: Ctx, input: ImplInput<Ops[K]>) => z.infer<O> | Promise<z.infer<O>> : never : never;
215
+ };
216
+ /** No declared `input` means the handler takes `undefined`. */
217
+ type ImplInput<O> = O extends {
218
+ input: infer I;
219
+ } ? I extends z.ZodType ? O extends {
220
+ inputOptional: true;
221
+ } ? z.infer<I> | undefined : z.infer<I> : undefined : undefined;
222
+ export {};
223
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The operation surface of the model (#707).
3
+ *
4
+ * #697 declared the entities. This declares what can be *done* to them, and
5
+ * checks the joins that today are unchecked strings: which permission an
6
+ * operation requires, which output field an event takes its subject from,
7
+ * whether a payload carries something an erasure must be able to reach.
8
+ *
9
+ * ## A composer, not a second `defineModel`
10
+ *
11
+ * `defineOperations` sits beside `defineEntities` rather than swallowing it.
12
+ * Each half stays independently adoptable — which is what let the entity half
13
+ * ship and be taken up by two verticals before this existed. A vertical adopts
14
+ * operations when it is ready, not as the price of adopting entities.
15
+ *
16
+ * ## `input`, and the transcription that is not here
17
+ *
18
+ * `input` is the Zod object the handler already parses — the same object, not a
19
+ * description of it. That is the whole reason the model is TypeScript (#680): a
20
+ * schema language would need the shape written twice, and transcription is what
21
+ * produced 40 wrong argument names in the one app where this was measured.
22
+ */
23
+ import { z } from 'zod';
24
+ // ---------------------------------------------------------------------------
25
+ // The composer.
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Declare a module's operations against its entities and permission keys.
29
+ *
30
+ * Curried so the entities and permissions are given explicitly while each
31
+ * operation still infers its own input and output — a callback parameter cannot
32
+ * be contextually typed by a generic being inferred from the object containing
33
+ * it.
34
+ *
35
+ * ```ts
36
+ * export const ops = defineOperations(calloutEntities, PERMISSIONS)({
37
+ * 'customer/create': {
38
+ * summary: 'Register a customer',
39
+ * permission: 'customer:manage',
40
+ * input: z.object({ name: z.string() }),
41
+ * output: z.object({ id: z.string(), number: z.string() }),
42
+ * http: { method: 'POST', path: '/customers' },
43
+ * emits: {
44
+ * entity: 'customer', entityIdFrom: 'id',
45
+ * type: 'callout.customer-created', schemaVersion: 1, piiClass: 'none',
46
+ * },
47
+ * },
48
+ * });
49
+ * ```
50
+ */
51
+ export function defineOperations(_entities, _permissions) {
52
+ return (operations) => operations;
53
+ }
54
+ /**
55
+ * The permission keys an operation set actually requires, for the manifest.
56
+ *
57
+ * Read structurally rather than through a `{ permission?: string }` parameter:
58
+ * a `narrows` operation has neither `permission` nor `emits`, and TypeScript's
59
+ * weak-type rule rejects an object sharing no properties with the parameter.
60
+ */
61
+ export function permissionsUsedBy(operations) {
62
+ const keys = Object.values(operations).flatMap((op) => {
63
+ const permission = op.permission;
64
+ return typeof permission === 'string' ? [permission] : [];
65
+ });
66
+ return [...new Set(keys)].sort();
67
+ }
68
+ /** The event types an operation set emits, for `manifest.events.emits`. */
69
+ export function eventsEmittedBy(operations) {
70
+ const seen = new Map();
71
+ for (const op of Object.values(operations)) {
72
+ const emits = op.emits;
73
+ if (typeof emits?.type === 'string' && typeof emits.schemaVersion === 'number') {
74
+ seen.set(emits.type, emits.schemaVersion);
75
+ }
76
+ }
77
+ return [...seen.entries()]
78
+ .sort(([a], [b]) => a.localeCompare(b))
79
+ .map(([type, schemaVersion]) => ({ type, schemaVersion }));
80
+ }
81
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../src/operations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiIxB,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,gBAAgB,CAG9B,SAAmB,EAAE,YAAmB;IACxC,OAAO,CAKL,UAAe,EACV,EAAE,CAAC,UAAU,CAAC;AACvB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA4C;IAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACpD,MAAM,UAAU,GAAI,EAA+B,CAAC,UAAU,CAAC;QAC/D,OAAO,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAC7B,UAA4C;IAE5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAI,EAA8D,CAAC,KAAK,CAAC;QACpF,IAAI,OAAO,KAAK,EAAE,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;YAC/E,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;SACvB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;AAC/D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/contracts",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Substrat kernel contract schemas — Zod is the source of truth (master plan D-22); OAS/JSON Schema are emitted artifacts",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -25,13 +25,15 @@
25
25
  "zod": "^4.4.3"
26
26
  },
27
27
  "devDependencies": {
28
- "typescript": "^7.0.0"
28
+ "typescript": "^7.0.0",
29
+ "vitest": "^3.0.0"
29
30
  },
30
31
  "publishConfig": {
31
32
  "access": "public"
32
33
  },
33
34
  "scripts": {
34
35
  "build": "tsc -p tsconfig.json",
35
- "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
37
+ "test": "vitest run"
36
38
  }
37
39
  }