@jini-ai/admin 0.3.0 → 0.3.6

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.
@@ -0,0 +1,302 @@
1
+ /**
2
+ * @file `AdminEntityPort` — the one port that is generic over the entity it fronts.
3
+ *
4
+ * ## Why this port is shaped differently from every other one here
5
+ *
6
+ * Every other port in this directory names a fixed capability with a fixed row shape:
7
+ * `AdminMembersPort` returns `AdminMember`, `AdminSeoPort` returns `AdminSeoMeta`. That works
8
+ * because the capability is the same everywhere it is wired — a member is a member.
9
+ *
10
+ * An application's own entities are not like that. A recipe app has recipes and authors; a
11
+ * warehouse app has pallets and SKUs. There is no fixed row shape to write down, and force-fitting
12
+ * one onto an existing port is lossy in a way that shows up immediately: mapping an app's `Author`
13
+ * onto `AdminMember` has to invent a `status`, and drops every field (`bio`) the app actually has.
14
+ *
15
+ * So this port carries its own schema. An adapter declares an {@link AdminEntityDescriptor}, and
16
+ * the row type is *derived* from it ({@link AdminEntityRow}) rather than declared twice. A host
17
+ * screen reads the descriptor to decide what to render, which is what makes one screen able to
18
+ * render any entity with no per-entity code.
19
+ *
20
+ * ## What this port deliberately does NOT carry
21
+ *
22
+ * - **No `workspaceId`, and no other host-specific field.** If a field cannot be produced by an app
23
+ * with no database, it does not belong on a generic row. Multi-tenancy is the adapter's business,
24
+ * held in its own closure, invisible here. This is the single rule that keeps an adapter for a
25
+ * plain in-memory array as short as an adapter for a real backend.
26
+ * - **No response envelope.** `list` returns {@link AdminEntityPage}, `get` returns the row itself
27
+ * — never `{ entity }` or `{ items }`-plus-metadata. {@link AdminEntityPage} is not a
28
+ * counterexample: `nextCursor` is pagination state that has nowhere else to live, not a wrapper
29
+ * around the payload.
30
+ * - **No `expectedVersion` on `update`.** Every other write in this package carries one. It is
31
+ * omitted here because an in-memory store has no version column, and requiring one would make the
32
+ * smallest honest adapter impossible to write. An entity that wants optimistic concurrency
33
+ * declares a `version` field of kind `"integer"` and enforces it inside its own `update`. This is
34
+ * a real weakening relative to the other ports, stated rather than hidden, and worth revisiting
35
+ * the first time a genuinely multi-writer entity appears.
36
+ * - **No publication lifecycle.** `AdminEntry`-style `draft`/`published` belongs to a CMS entry, not
37
+ * to an arbitrary entity. An app that wants one declares a `status` field of kind `"text"`.
38
+ * - **No per-operation authorization seam** (no `canCreate`/`canDelete`). Consistent with every
39
+ * other port here, and with `AdminPanel.permissions` being affordance-only — but worth naming,
40
+ * because a generic CRUD surface over arbitrary application data is where that gap starts to
41
+ * matter more than it does on a fixed screen.
42
+ *
43
+ * Types only, like every file in this directory. The runtime half — registration, descriptor
44
+ * validation, and the erasure a generic screen consumes — lives in `../entities/rules.ts`.
45
+ */
46
+ /**
47
+ * The closed set of field kinds an entity may declare.
48
+ *
49
+ * **Closed deliberately**, unlike the reference-implementation-derived unions elsewhere in this
50
+ * directory (see `README.md`'s `T | (string & {})` idiom). A kind is not a label: each member here
51
+ * is a promise that a rendering layer knows how to display and edit it, and that a persistence
52
+ * layer knows its storage class. A host cannot add a seventh kind by widening a string — it would
53
+ * render as nothing. Adding a kind is a change to this package.
54
+ *
55
+ * `"media"` and `"enum"` are deliberately absent. `"media"` is the same shape as `"relation"` at
56
+ * rest (a foreign id stored as text) and differs only in needing a media *picker*, which needs
57
+ * `AdminMediaPort` wired; adding it before that picker exists would ship a kind indistinguishable
58
+ * from `"text"`. `"enum"` has a real unresolved question (does a queryable enum index the label or
59
+ * the ordinal?) and no forcing case.
60
+ */
61
+ export type AdminEntityFieldKind = 'text' | 'integer' | 'real' | 'boolean' | 'datetime' | 'relation' | 'json';
62
+ /** Fields common to every kind. `relation` adds `target`; `json` narrows `queryable`. */
63
+ interface AdminEntityFieldBase {
64
+ /**
65
+ * The key this field occupies on a row.
66
+ *
67
+ * **No identifier grammar is imposed here, and that is deliberate.** The identifier grammar that
68
+ * gates a CMS content-type key (`^[a-z][a-z0-9_]{0,63}$`) is load-bearing where a field name
69
+ * reaches SQL DDL — it does not reach it here. Applying it to this descriptor would reject
70
+ * `authorId` and `createdAt`, i.e. would require an application to rename its own fields in order
71
+ * to get an admin. An app that is *also* persisted through the CMS maps names in its adapter,
72
+ * where the mapping is visible, rather than in its descriptor.
73
+ *
74
+ * `"id"` is reserved: every row already has one (see {@link AdminEntityRow}).
75
+ */
76
+ readonly name: string;
77
+ /** Human-readable column/label text. Falls back to {@link AdminEntityFieldBase.name}. */
78
+ readonly label?: string;
79
+ /** When true, the row type makes this field non-optional — see {@link AdminEntityRow}. */
80
+ readonly required?: boolean;
81
+ /**
82
+ * Whether a backend can filter or order on this field without a full scan.
83
+ *
84
+ * **Currently advisory in both directions, and nothing enforces either half.** A screen is
85
+ * expected to offer filter/sort controls only for `queryable` fields, and an adapter is expected
86
+ * to reject a filter on a non-queryable one rather than silently scanning — but this is a
87
+ * type-only port, so both are obligations rather than guarantees. An in-memory adapter (every
88
+ * app's first one) filters anything happily, so a filter that is a full scan in production looks
89
+ * correct in development. Read this flag as documentation of intent, not as a capability.
90
+ */
91
+ readonly queryable?: boolean;
92
+ }
93
+ /** A value stored inline: text, a number, a flag, or an ISO-8601 timestamp. */
94
+ export type AdminEntityScalarField = AdminEntityFieldBase & {
95
+ readonly kind: 'text' | 'integer' | 'real' | 'boolean' | 'datetime';
96
+ };
97
+ /** A foreign id pointing at another registered entity. */
98
+ export type AdminEntityRelationField = AdminEntityFieldBase & {
99
+ readonly kind: 'relation';
100
+ /**
101
+ * The {@link AdminEntityDescriptor.name} this field points at. Checked against the registry at
102
+ * composition time (`createAdminEntityRegistry`), not by the type system — a descriptor does not
103
+ * know which other descriptors it will be registered beside.
104
+ *
105
+ * **No referential integrity is implied anywhere.** Nothing validates that the stored id names a
106
+ * live row: not this port, not a rendering layer, not a persistence layer. A deleted target
107
+ * leaves a dangling reference, and a screen's honest rendering of that is the raw id, visibly
108
+ * marked as unresolved — not a blank cell.
109
+ */
110
+ readonly target: string;
111
+ };
112
+ /**
113
+ * An arbitrary JSON value stored verbatim.
114
+ *
115
+ * Exists because no scalar kind can hold a list or a nested object, and an application's own
116
+ * entities routinely have one (a recipe's ingredients). Storage-only: never indexed, never
117
+ * filtered, never ordered — which is why `queryable` is typed `false | undefined` here, making
118
+ * `queryable: true` on a json field a compile error rather than a runtime rejection.
119
+ */
120
+ export type AdminEntityJsonField = Omit<AdminEntityFieldBase, 'queryable'> & {
121
+ readonly kind: 'json';
122
+ readonly queryable?: false;
123
+ };
124
+ /**
125
+ * One declared field. A discriminated union rather than a flat interface with optional extras, so
126
+ * `relation` can *require* its `target` instead of documenting that it needs one.
127
+ */
128
+ export type AdminEntityField = AdminEntityScalarField | AdminEntityRelationField | AdminEntityJsonField;
129
+ /** What an entity is: a name, labels, a title field, and its fields. */
130
+ export interface AdminEntityDescriptor {
131
+ /**
132
+ * Stable machine name — the URL segment a generic screen routes on, and the value a
133
+ * {@link AdminEntityRelationField.target} names. Must equal the key this descriptor's port is
134
+ * registered under (enforced by `createAdminEntityRegistry`), so there is exactly one name for
135
+ * an entity rather than a registry key and a descriptor name that can drift apart.
136
+ */
137
+ readonly name: string;
138
+ readonly labelSingular: string;
139
+ readonly labelPlural: string;
140
+ /**
141
+ * Which field renders as a row's human-readable title — in lists, and in every relation picker
142
+ * that points at this entity.
143
+ *
144
+ * **Cannot be constrained to "a `text` field on this same descriptor" by the type system** without
145
+ * a conditional type heavy enough to make every descriptor's errors unreadable, so it is checked
146
+ * at registration instead. A typo here is not a small bug: it makes every relation picker
147
+ * pointing at this entity show `undefined` for every row.
148
+ */
149
+ readonly titleField: string;
150
+ readonly fields: readonly AdminEntityField[];
151
+ }
152
+ /** The runtime type each kind's value takes on a row. */
153
+ interface AdminEntityKindValue {
154
+ text: string;
155
+ datetime: string;
156
+ integer: number;
157
+ real: number;
158
+ boolean: boolean;
159
+ /** A foreign id — same storage class as `text`. */
160
+ relation: string;
161
+ /** Storage-only, so nothing may consume it without narrowing first. */
162
+ json: unknown;
163
+ }
164
+ type AdminEntityFieldValue<F extends AdminEntityField> = F extends {
165
+ kind: infer K extends AdminEntityFieldKind;
166
+ } ? K extends keyof AdminEntityKindValue ? AdminEntityKindValue[K] : never : never;
167
+ type AdminEntityRequiredNames<F extends AdminEntityField> = F extends {
168
+ required: true;
169
+ } ? F['name'] : never;
170
+ /**
171
+ * The row shape a descriptor describes — derived, never hand-written.
172
+ *
173
+ * Requires the descriptor to be declared `as const satisfies AdminEntityDescriptor`: without
174
+ * `as const` the field names widen to `string` and every key collapses. Fields declared
175
+ * `required: true` are non-optional; everything else is optional. `id` is always present and is
176
+ * never a declared field.
177
+ *
178
+ * @example
179
+ * const recipe = {
180
+ * name: 'recipe', labelSingular: 'Recipe', labelPlural: 'Recipes', titleField: 'title',
181
+ * fields: [
182
+ * { name: 'title', kind: 'text', required: true },
183
+ * { name: 'authorId', kind: 'relation', target: 'author', required: true },
184
+ * { name: 'ingredients', kind: 'json' },
185
+ * ],
186
+ * } as const satisfies AdminEntityDescriptor;
187
+ * // AdminEntityRow<typeof recipe> is
188
+ * // { id: string; title: string; authorId: string; ingredients?: unknown }
189
+ */
190
+ export type AdminEntityRow<D extends AdminEntityDescriptor> = {
191
+ readonly id: string;
192
+ } & {
193
+ readonly [F in D['fields'][number] as F['name'] & AdminEntityRequiredNames<F>]: AdminEntityFieldValue<F>;
194
+ } & {
195
+ readonly [F in D['fields'][number] as F['name'] & Exclude<F['name'], AdminEntityRequiredNames<F>>]?: AdminEntityFieldValue<F>;
196
+ };
197
+ /** One filter clause. `field` should name a `queryable` field — see that flag's own caveat. */
198
+ export interface AdminEntityFilter {
199
+ readonly field: string;
200
+ readonly op: 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte';
201
+ readonly value: string | number | boolean;
202
+ }
203
+ export interface AdminEntityListQuery {
204
+ /** Maximum rows in the returned page. An adapter may return fewer; it must not return more. */
205
+ readonly limit?: number;
206
+ /**
207
+ * Opaque forward cursor, taken verbatim from a prior page's {@link AdminEntityPage.nextCursor}.
208
+ *
209
+ * **Not an offset.** An offset cannot be made stable under concurrent writes — a row inserted
210
+ * before the current position shifts every later page by one, silently duplicating or skipping a
211
+ * row. Every backend this port is likely to front can express keyset pagination over
212
+ * `(sortField, id)` instead.
213
+ *
214
+ * **Forward-only, by decision rather than by accident.** There is no `prevCursor`, because a
215
+ * backwards keyset walk needs a reversed comparator this port does not express. A screen that
216
+ * wants a "Previous" control holds the cursors it has already visited itself; that history is
217
+ * screen state and does not survive a reload.
218
+ */
219
+ readonly cursor?: string;
220
+ readonly filter?: readonly AdminEntityFilter[];
221
+ /**
222
+ * Ordering, applied by the adapter over the whole collection *before* the page is cut.
223
+ *
224
+ * Ordering is a server concern and this is the only place it may be expressed. Sorting a page
225
+ * after it has been cut reorders the rows on screen and nothing else — correct-looking at one
226
+ * page, meaningless at two, and no value-comparison test detects the difference.
227
+ */
228
+ readonly sort?: {
229
+ readonly field: string;
230
+ readonly direction: 'asc' | 'desc';
231
+ };
232
+ }
233
+ export interface AdminEntityPage<TRow> {
234
+ readonly items: readonly TRow[];
235
+ /**
236
+ * Cursor for the next page, or `null` on the last one.
237
+ *
238
+ * Required rather than optional — matching `AdminFormSubmissionPage`, this package's other
239
+ * cursor-paginated shape — because forgetting it silently caps every list at one page, which
240
+ * looks entirely correct until a collection outgrows one. `null` is a decision an adapter has to
241
+ * type out.
242
+ */
243
+ readonly nextCursor: string | null;
244
+ /**
245
+ * Total rows matching the query, when the adapter can count cheaply.
246
+ *
247
+ * Genuinely optional, unlike `nextCursor`: omitting it degrades one label, and forcing a count
248
+ * query on every list read to satisfy the type would be worse than not having the label.
249
+ */
250
+ readonly total?: number;
251
+ }
252
+ /**
253
+ * CRUD over one application-defined entity.
254
+ *
255
+ * `D` is the descriptor *value's* type (`typeof recipeDescriptor`), not `AdminEntityDescriptor` —
256
+ * that is what lets `list`/`get`/`create` return rows typed from the declaration.
257
+ *
258
+ * Note that `AdminEntityPort<SomeSpecificDescriptor>` is **not** assignable to
259
+ * `AdminEntityPort<AdminEntityDescriptor>`: `create`/`update` take the row type as a parameter and
260
+ * are therefore contravariant in it. A screen generic over all entities consumes
261
+ * {@link AdminErasedEntityPort} instead, reached through `eraseEntityPort`.
262
+ */
263
+ export interface AdminEntityPort<D extends AdminEntityDescriptor> {
264
+ readonly descriptor: D;
265
+ list(query?: AdminEntityListQuery): Promise<AdminEntityPage<AdminEntityRow<D>>>;
266
+ get(id: string): Promise<AdminEntityRow<D> | null>;
267
+ create(input: Omit<AdminEntityRow<D>, 'id'>): Promise<AdminEntityRow<D>>;
268
+ update(id: string, patch: Partial<Omit<AdminEntityRow<D>, 'id'>>): Promise<AdminEntityRow<D>>;
269
+ /**
270
+ * Optional. An entity that cannot be deleted omits it, and a generic screen renders no delete
271
+ * affordance — the same "absence means the capability does not exist" convention
272
+ * `AdminMembersPort` uses for its deliberately missing `enableMember`.
273
+ */
274
+ remove?(id: string): Promise<void>;
275
+ }
276
+ /** A row with its field types erased, as a screen generic over all entities sees it. */
277
+ export type AdminEntityRowData = Record<string, unknown> & {
278
+ readonly id: string;
279
+ };
280
+ /**
281
+ * {@link AdminEntityPort} with its descriptor generic erased — what a screen that renders *any*
282
+ * entity actually holds. Produced only by `eraseEntityPort`; never implemented directly.
283
+ */
284
+ export interface AdminErasedEntityPort {
285
+ readonly descriptor: AdminEntityDescriptor;
286
+ list(query?: AdminEntityListQuery): Promise<AdminEntityPage<AdminEntityRowData>>;
287
+ get(id: string): Promise<AdminEntityRowData | null>;
288
+ create(input: Record<string, unknown>): Promise<AdminEntityRowData>;
289
+ update(id: string, patch: Record<string, unknown>): Promise<AdminEntityRowData>;
290
+ remove?(id: string): Promise<void>;
291
+ }
292
+ /**
293
+ * The assembled registry: entity name -> port, with each port's own type preserved.
294
+ *
295
+ * Same inference shape as `AdminClient` — a host's entities and any Jini-shipped ones would be
296
+ * indistinguishable here, and registering one is a line in an object literal.
297
+ */
298
+ export type AdminEntityRegistry<TPorts extends Record<string, AdminEntityPort<AdminEntityDescriptor>>> = {
299
+ readonly [K in keyof TPorts]: TPorts[K];
300
+ };
301
+ export {};
302
+ //# sourceMappingURL=entities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../../src/core/ports/entities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,SAAS,GACT,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,CAAC;AAEX,yFAAyF;AACzF,UAAU,oBAAoB;IAC5B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,+EAA+E;AAC/E,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;CACrE,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,wBAAwB,GAAG,oBAAoB,GAAG;IAC5D,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACxB,sBAAsB,GACtB,wBAAwB,GACxB,oBAAoB,CAAC;AAEzB,wEAAwE;AACxE,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC9C;AAED,yDAAyD;AACzD,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,KAAK,qBAAqB,CAAC,CAAC,SAAS,gBAAgB,IAAI,CAAC,SAAS;IACjE,IAAI,EAAE,MAAM,CAAC,SAAS,oBAAoB,CAAC;CAC5C,GACG,CAAC,SAAS,MAAM,oBAAoB,GAClC,oBAAoB,CAAC,CAAC,CAAC,GACvB,KAAK,GACP,KAAK,CAAC;AAEV,KAAK,wBAAwB,CAAC,CAAC,SAAS,gBAAgB,IAAI,CAAC,SAAS;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GACpF,CAAC,CAAC,MAAM,CAAC,GACT,KAAK,CAAC;AAEV;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,qBAAqB,IAAI;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IACtF,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,wBAAwB,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC;CACzG,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC;CAC9E,CAAC;AAEF,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;CAChF;AAED,MAAM,WAAW,eAAe,CAAC,IAAI;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,qBAAqB;IAC9D,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F;;;;OAIG;IACH,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,wFAAwF;AACxF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnF;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,IAAI,CAAC,KAAK,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACjF,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChF,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,CAC7B,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,qBAAqB,CAAC,CAAC,IACnE;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;CACxC,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @file `AdminEntityPort` — the one port that is generic over the entity it fronts.
3
+ *
4
+ * ## Why this port is shaped differently from every other one here
5
+ *
6
+ * Every other port in this directory names a fixed capability with a fixed row shape:
7
+ * `AdminMembersPort` returns `AdminMember`, `AdminSeoPort` returns `AdminSeoMeta`. That works
8
+ * because the capability is the same everywhere it is wired — a member is a member.
9
+ *
10
+ * An application's own entities are not like that. A recipe app has recipes and authors; a
11
+ * warehouse app has pallets and SKUs. There is no fixed row shape to write down, and force-fitting
12
+ * one onto an existing port is lossy in a way that shows up immediately: mapping an app's `Author`
13
+ * onto `AdminMember` has to invent a `status`, and drops every field (`bio`) the app actually has.
14
+ *
15
+ * So this port carries its own schema. An adapter declares an {@link AdminEntityDescriptor}, and
16
+ * the row type is *derived* from it ({@link AdminEntityRow}) rather than declared twice. A host
17
+ * screen reads the descriptor to decide what to render, which is what makes one screen able to
18
+ * render any entity with no per-entity code.
19
+ *
20
+ * ## What this port deliberately does NOT carry
21
+ *
22
+ * - **No `workspaceId`, and no other host-specific field.** If a field cannot be produced by an app
23
+ * with no database, it does not belong on a generic row. Multi-tenancy is the adapter's business,
24
+ * held in its own closure, invisible here. This is the single rule that keeps an adapter for a
25
+ * plain in-memory array as short as an adapter for a real backend.
26
+ * - **No response envelope.** `list` returns {@link AdminEntityPage}, `get` returns the row itself
27
+ * — never `{ entity }` or `{ items }`-plus-metadata. {@link AdminEntityPage} is not a
28
+ * counterexample: `nextCursor` is pagination state that has nowhere else to live, not a wrapper
29
+ * around the payload.
30
+ * - **No `expectedVersion` on `update`.** Every other write in this package carries one. It is
31
+ * omitted here because an in-memory store has no version column, and requiring one would make the
32
+ * smallest honest adapter impossible to write. An entity that wants optimistic concurrency
33
+ * declares a `version` field of kind `"integer"` and enforces it inside its own `update`. This is
34
+ * a real weakening relative to the other ports, stated rather than hidden, and worth revisiting
35
+ * the first time a genuinely multi-writer entity appears.
36
+ * - **No publication lifecycle.** `AdminEntry`-style `draft`/`published` belongs to a CMS entry, not
37
+ * to an arbitrary entity. An app that wants one declares a `status` field of kind `"text"`.
38
+ * - **No per-operation authorization seam** (no `canCreate`/`canDelete`). Consistent with every
39
+ * other port here, and with `AdminPanel.permissions` being affordance-only — but worth naming,
40
+ * because a generic CRUD surface over arbitrary application data is where that gap starts to
41
+ * matter more than it does on a fixed screen.
42
+ *
43
+ * Types only, like every file in this directory. The runtime half — registration, descriptor
44
+ * validation, and the erasure a generic screen consumes — lives in `../entities/rules.ts`.
45
+ */
46
+ export {};
47
+ //# sourceMappingURL=entities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entities.js","sourceRoot":"","sources":["../../../src/core/ports/entities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG"}
@@ -18,4 +18,5 @@ export type { AdminSeoAnalysis, AdminSeoIssue, AdminSeoMeta, AdminSeoOpenGraph,
18
18
  export type { AdminRedirect, AdminRedirectCreateInput, AdminRedirectHitStats, AdminRedirectImportFailure, AdminRedirectImportResult, AdminRedirectListFilter, AdminRedirectsPort, AdminRedirectUpdatePatch, RedirectMatchType, RedirectSource, RedirectStatus, RedirectStatusCode, } from './redirects.js';
19
19
  export type { AdminAssignMenuLocationResult, AdminDeleteMenuResult, AdminMenu, AdminMenuBinding, AdminMenuCreateInput, AdminMenuCustomTarget, AdminMenuEntryTarget, AdminMenuItem, AdminMenuItemAttrs, AdminMenuRouteTarget, AdminMenusPort, AdminMenuTarget, AdminMenuTermTarget, AdminMenuUpdateTreeInput, AdminMenuUrlTarget, MenuStatus, NavTargetKind, } from './menus.js';
20
20
  export type { AdminFormCreateInput, AdminFormDefinition, AdminFormField, AdminFormNotifyConfig, AdminFormsPort, AdminFormSubmission, AdminFormSubmissionPage, AdminFormUpdatePatch, FormDefinitionStatus, FormFieldType, } from './forms.js';
21
+ export type { AdminEntityDescriptor, AdminEntityField, AdminEntityFieldKind, AdminEntityFilter, AdminEntityJsonField, AdminEntityListQuery, AdminEntityPage, AdminEntityPort, AdminEntityRegistry, AdminEntityRelationField, AdminEntityRow, AdminEntityRowData, AdminEntityScalarField, AdminErasedEntityPort, } from './entities.js';
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/ports/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,wBAAwB,EACxB,+BAA+B,EAC/B,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAClG,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,SAAS,GACV,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,aAAa,EACb,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,6BAA6B,EAC7B,qBAAqB,EACrB,SAAS,EACT,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,GACd,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/ports/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,wBAAwB,EACxB,+BAA+B,EAC/B,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAClG,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,SAAS,GACV,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,aAAa,EACb,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EACvB,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,6BAA6B,EAC7B,qBAAqB,EACrB,SAAS,EACT,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,qBAAqB,EACrB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,eAAe,CAAC"}
@@ -25,6 +25,20 @@
25
25
  * `kind` values (with `AdminMenuCustomTarget` as the `default` arm) is the straightforward way to
26
26
  * stay correct.
27
27
  *
28
+ * **Migration hazard, measured 2026-09-05.** This is not merely a style preference — it means an
29
+ * exhaustive `switch` with NO `default` arm cannot compile against `AdminMenuTarget` at all, and
30
+ * that is exactly the shape a host arrives with. Both existing admin apps' `targetForKind`
31
+ * (`apps/admin/src/features/menus/MenuEditor.tsx`) switch over their own local CLOSED four-member
32
+ * union with no `default`, and rely on exhaustiveness for the return type. Compiling that function
33
+ * against this port's union emits `TS2366` (lacks ending return statement) plus a `TS2339` per
34
+ * field access. Confirmed by compiling the real function against this union directly.
35
+ *
36
+ * The hazard is pre-existing and belongs to `AdminMenuCustomTarget`, not to any later change: the
37
+ * identical probe run against the union with and without an extra member emits a character-for-
38
+ * character identical error set. Recorded here because it will surface the first time anyone wires
39
+ * a host onto this port, and the natural instinct will be to blame whatever change is in flight at
40
+ * that moment. The fix is a `default` arm in the host, not a change to this file.
41
+ *
28
42
  * **This is a deliberately deferred decision, not a settled one — record it as such.** `entryRef`/
29
43
  * `termRef` encode a host content model (an "entry" and a "taxonomy term" are the reference
30
44
  * implementation's own vocabulary for its content system, not a vendor-neutral abstraction this
@@ -39,6 +53,35 @@
39
53
  * instead of one, and a genuine vendor-neutral reference shape (rather than another bespoke
40
54
  * `kind`-per-host member) becomes the right call.
41
55
  *
56
+ * ### Status of that trigger, re-checked 2026-09-05 — NOT fired
57
+ *
58
+ * A second host now exists in this workspace, and that is NOT the trigger. The trigger is a second
59
+ * host needing a fifth KIND. Recording the difference here because a promotion was very nearly
60
+ * approved on the weaker reading. Four findings, all verified against source rather than assumed:
61
+ *
62
+ * 1. **Zero importers.** Nothing anywhere imports `AdminMenuTarget`, `NavTargetKind`, or any target
63
+ * variant from this package. In this repo they are re-exported by `core/index.ts` and
64
+ * `core/ports/index.ts` and consumed by nothing.
65
+ * 2. **Both admin apps declare their own.** Each ships a local, *closed*
66
+ * `{ kind: "entryRef" | "termRef" | "url" | "route" }` in its own `src/lib/api.ts` — an HTTP
67
+ * DTO, not this port. The two are byte-identical to each other: the second is a fork of the
68
+ * first, so they are one data point, not two.
69
+ * 3. **The other `NavTargetKind` is a different type.** `@jini-ai/cms/navigation` exports its own
70
+ * `NavTargetKind` (closed, plus a separate `ReservedNavTargetKind`). A host importing that one
71
+ * is not exercising this port.
72
+ * 4. **The union is already open.** `AdminMenuCustomTarget`'s `kind: string & {}` already lets any
73
+ * host express any kind today. Nothing is blocked while the decision stays deferred.
74
+ *
75
+ * So no fifth kind has been needed, and adding a named generic variant now would be designing from
76
+ * zero validating examples — the exact failure the paragraph above exists to prevent. Considered
77
+ * and rejected on 2026-09-05 for that reason. Re-check this section, not the prose above it,
78
+ * before concluding the trigger has fired.
79
+ *
80
+ * Note for whoever revisits: wiring `AdminMenusPort` into a host is the experiment that ANSWERS the
81
+ * type question, and it is not blocked by it. A host that wires the port and finds
82
+ * `entryRef`/`termRef` fit vindicates the deferral; one that needs a product/collection kind fires
83
+ * the trigger with a real second example to design from.
84
+ *
42
85
  * ## `deleteMenu` is a two-rung ladder driven by call count, not by `force` alone
43
86
  *
44
87
  * A single method models a two-step deletion flow: the FIRST call against a non-trashed menu
@@ -1 +1 @@
1
- {"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../../src/core/ports/menus.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../../src/core/ports/menus.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,20 @@
25
25
  * `kind` values (with `AdminMenuCustomTarget` as the `default` arm) is the straightforward way to
26
26
  * stay correct.
27
27
  *
28
+ * **Migration hazard, measured 2026-09-05.** This is not merely a style preference — it means an
29
+ * exhaustive `switch` with NO `default` arm cannot compile against `AdminMenuTarget` at all, and
30
+ * that is exactly the shape a host arrives with. Both existing admin apps' `targetForKind`
31
+ * (`apps/admin/src/features/menus/MenuEditor.tsx`) switch over their own local CLOSED four-member
32
+ * union with no `default`, and rely on exhaustiveness for the return type. Compiling that function
33
+ * against this port's union emits `TS2366` (lacks ending return statement) plus a `TS2339` per
34
+ * field access. Confirmed by compiling the real function against this union directly.
35
+ *
36
+ * The hazard is pre-existing and belongs to `AdminMenuCustomTarget`, not to any later change: the
37
+ * identical probe run against the union with and without an extra member emits a character-for-
38
+ * character identical error set. Recorded here because it will surface the first time anyone wires
39
+ * a host onto this port, and the natural instinct will be to blame whatever change is in flight at
40
+ * that moment. The fix is a `default` arm in the host, not a change to this file.
41
+ *
28
42
  * **This is a deliberately deferred decision, not a settled one — record it as such.** `entryRef`/
29
43
  * `termRef` encode a host content model (an "entry" and a "taxonomy term" are the reference
30
44
  * implementation's own vocabulary for its content system, not a vendor-neutral abstraction this
@@ -39,6 +53,35 @@
39
53
  * instead of one, and a genuine vendor-neutral reference shape (rather than another bespoke
40
54
  * `kind`-per-host member) becomes the right call.
41
55
  *
56
+ * ### Status of that trigger, re-checked 2026-09-05 — NOT fired
57
+ *
58
+ * A second host now exists in this workspace, and that is NOT the trigger. The trigger is a second
59
+ * host needing a fifth KIND. Recording the difference here because a promotion was very nearly
60
+ * approved on the weaker reading. Four findings, all verified against source rather than assumed:
61
+ *
62
+ * 1. **Zero importers.** Nothing anywhere imports `AdminMenuTarget`, `NavTargetKind`, or any target
63
+ * variant from this package. In this repo they are re-exported by `core/index.ts` and
64
+ * `core/ports/index.ts` and consumed by nothing.
65
+ * 2. **Both admin apps declare their own.** Each ships a local, *closed*
66
+ * `{ kind: "entryRef" | "termRef" | "url" | "route" }` in its own `src/lib/api.ts` — an HTTP
67
+ * DTO, not this port. The two are byte-identical to each other: the second is a fork of the
68
+ * first, so they are one data point, not two.
69
+ * 3. **The other `NavTargetKind` is a different type.** `@jini-ai/cms/navigation` exports its own
70
+ * `NavTargetKind` (closed, plus a separate `ReservedNavTargetKind`). A host importing that one
71
+ * is not exercising this port.
72
+ * 4. **The union is already open.** `AdminMenuCustomTarget`'s `kind: string & {}` already lets any
73
+ * host express any kind today. Nothing is blocked while the decision stays deferred.
74
+ *
75
+ * So no fifth kind has been needed, and adding a named generic variant now would be designing from
76
+ * zero validating examples — the exact failure the paragraph above exists to prevent. Considered
77
+ * and rejected on 2026-09-05 for that reason. Re-check this section, not the prose above it,
78
+ * before concluding the trigger has fired.
79
+ *
80
+ * Note for whoever revisits: wiring `AdminMenusPort` into a host is the experiment that ANSWERS the
81
+ * type question, and it is not blocked by it. A host that wires the port and finds
82
+ * `entryRef`/`termRef` fit vindicates the deferral; one that needs a product/collection kind fires
83
+ * the trigger with a real second example to design from.
84
+ *
42
85
  * ## `deleteMenu` is a two-rung ladder driven by call count, not by `force` alone
43
86
  *
44
87
  * A single method models a two-step deletion flow: the FIRST call against a non-trashed menu
@@ -1 +1 @@
1
- {"version":3,"file":"menus.js","sourceRoot":"","sources":["../../../src/core/ports/menus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG"}
1
+ {"version":3,"file":"menus.js","sourceRoot":"","sources":["../../../src/core/ports/menus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8GG"}