@jini-ai/cms 0.3.10 → 0.4.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.
Files changed (258) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +47 -19
  3. package/dist/content-types/agent-tools.d.ts +3 -56
  4. package/dist/content-types/agent-tools.js +22 -6
  5. package/dist/content-types/cleanup.d.ts +18 -13
  6. package/dist/content-types/cleanup.js +16 -11
  7. package/dist/content-types/errors.d.ts +57 -24
  8. package/dist/content-types/errors.js +56 -39
  9. package/dist/content-types/field-defs.d.ts +8 -4
  10. package/dist/content-types/field-defs.js +19 -14
  11. package/dist/content-types/index-provisioning.d.ts +61 -47
  12. package/dist/content-types/index-provisioning.js +90 -66
  13. package/dist/content-types/index.d.ts +4 -3
  14. package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
  15. package/dist/content-types/lifecycle-dispatch.js +5 -4
  16. package/dist/content-types/lifecycle.d.ts +23 -18
  17. package/dist/content-types/lifecycle.js +71 -64
  18. package/dist/content-types/repo.memory.d.ts +5 -4
  19. package/dist/content-types/repo.memory.js +3 -2
  20. package/dist/content-types/tool-registrations.d.ts +6 -8
  21. package/dist/content-types/tool-registrations.js +24 -28
  22. package/dist/content-types/types.d.ts +17 -13
  23. package/dist/content-types/types.js +11 -5
  24. package/dist/content-types/write-service.d.ts +20 -22
  25. package/dist/content-types/write-service.js +78 -75
  26. package/dist/core/authorization.d.ts +15 -0
  27. package/dist/core/authorization.js +2 -0
  28. package/dist/core/commands/change-set.d.ts +15 -7
  29. package/dist/core/commands/command.d.ts +45 -26
  30. package/dist/core/commands/command.js +73 -63
  31. package/dist/core/entity-liveness.d.ts +11 -7
  32. package/dist/core/entity-liveness.js +5 -4
  33. package/dist/core/index.d.ts +12 -5
  34. package/dist/core/index.js +10 -4
  35. package/dist/core/ports.d.ts +25 -24
  36. package/dist/core/ports.js +1 -0
  37. package/dist/core/tools/args.d.ts +49 -0
  38. package/dist/core/tools/args.js +46 -0
  39. package/dist/core/tools/index.d.ts +3 -0
  40. package/dist/core/tools/index.js +3 -0
  41. package/dist/entries/agent-tools.d.ts +1 -73
  42. package/dist/entries/agent-tools.js +19 -13
  43. package/dist/entries/errors.d.ts +28 -14
  44. package/dist/entries/errors.js +17 -11
  45. package/dist/entries/field-validation.d.ts +5 -3
  46. package/dist/entries/field-validation.js +7 -5
  47. package/dist/entries/index.d.ts +2 -2
  48. package/dist/entries/lifecycle-dispatch.d.ts +5 -2
  49. package/dist/entries/lifecycle-dispatch.js +2 -1
  50. package/dist/entries/list.d.ts +4 -2
  51. package/dist/entries/list.js +4 -3
  52. package/dist/entries/repo.memory.d.ts +12 -10
  53. package/dist/entries/repo.memory.js +11 -10
  54. package/dist/entries/tool-registrations.d.ts +6 -8
  55. package/dist/entries/tool-registrations.js +42 -41
  56. package/dist/entries/types.d.ts +0 -7
  57. package/dist/entries/write-service.d.ts +28 -29
  58. package/dist/entries/write-service.js +114 -105
  59. package/dist/http/settings/cms-adapter.d.ts +20 -0
  60. package/dist/http/settings/cms-adapter.js +124 -0
  61. package/dist/http/settings/contracts.d.ts +135 -0
  62. package/dist/http/settings/contracts.js +2 -0
  63. package/dist/http/settings/http.d.ts +10 -0
  64. package/dist/http/settings/http.js +211 -0
  65. package/dist/http/settings/index.d.ts +5 -0
  66. package/dist/http/settings/index.js +4 -0
  67. package/dist/http/settings/shared.d.ts +68 -0
  68. package/dist/http/settings/shared.js +54 -0
  69. package/dist/http/settings/stream.d.ts +24 -0
  70. package/dist/http/settings/stream.js +147 -0
  71. package/dist/media/agent-tools.d.ts +1 -66
  72. package/dist/media/agent-tools.js +5 -1
  73. package/dist/media/blob-gc-lock.d.ts +6 -2
  74. package/dist/media/blob-gc-lock.js +4 -2
  75. package/dist/media/blob-gc.d.ts +64 -51
  76. package/dist/media/blob-gc.js +62 -52
  77. package/dist/media/blob-key.d.ts +1 -1
  78. package/dist/media/content-type-sniffer.d.ts +3 -1
  79. package/dist/media/content-type-sniffer.js +1 -1
  80. package/dist/media/html-attributes.d.ts +9 -5
  81. package/dist/media/html-attributes.js +8 -6
  82. package/dist/media/image-transformer.js +1 -1
  83. package/dist/media/image-transformer.sharp.d.ts +6 -0
  84. package/dist/media/image-transformer.sharp.js +12 -6
  85. package/dist/media/import/fetch-image.d.ts +24 -0
  86. package/dist/media/import/fetch-image.js +137 -0
  87. package/dist/media/import/index.d.ts +3 -0
  88. package/dist/media/import/index.js +3 -0
  89. package/dist/media/import/ports.d.ts +38 -0
  90. package/dist/media/index.d.ts +7 -1
  91. package/dist/media/index.js +1 -0
  92. package/dist/media/media-service.d.ts +14 -10
  93. package/dist/media/media-service.js +57 -52
  94. package/dist/media/ports.d.ts +3 -2
  95. package/dist/media/rendition-service.d.ts +3 -3
  96. package/dist/media/rendition-service.js +38 -37
  97. package/dist/media/repo.memory.d.ts +18 -7
  98. package/dist/media/repo.memory.js +12 -6
  99. package/dist/media/tool-registrations.d.ts +16 -15
  100. package/dist/media/tool-registrations.js +84 -72
  101. package/dist/media/transform-lock.d.ts +22 -12
  102. package/dist/media/transform-lock.js +14 -13
  103. package/dist/media/transform-registry.d.ts +3 -3
  104. package/dist/media/transform-registry.js +19 -18
  105. package/dist/media/transform-types.d.ts +8 -3
  106. package/dist/media/transform-types.js +10 -5
  107. package/dist/media/types.d.ts +21 -4
  108. package/dist/media/types.js +15 -2
  109. package/dist/navigation/agent-tools.d.ts +1 -80
  110. package/dist/navigation/agent-tools.js +15 -3
  111. package/dist/navigation/contracts.d.ts +1 -1
  112. package/dist/navigation/index.d.ts +2 -2
  113. package/dist/navigation/menu-service.d.ts +57 -40
  114. package/dist/navigation/menu-service.js +80 -69
  115. package/dist/navigation/ports.d.ts +1 -1
  116. package/dist/navigation/read-model.d.ts +3 -1
  117. package/dist/navigation/read-model.js +2 -2
  118. package/dist/navigation/reconcile.d.ts +2 -2
  119. package/dist/navigation/reconcile.js +2 -1
  120. package/dist/navigation/repo.memory.d.ts +7 -3
  121. package/dist/navigation/repo.memory.js +4 -2
  122. package/dist/navigation/resolver.d.ts +4 -1
  123. package/dist/navigation/resolver.js +1 -1
  124. package/dist/navigation/tool-registrations.d.ts +6 -8
  125. package/dist/navigation/tool-registrations.js +44 -42
  126. package/dist/navigation/types.d.ts +1 -1
  127. package/dist/presentation/presentation.d.ts +11 -3
  128. package/dist/presentation/presentation.js +13 -6
  129. package/dist/presentation/repo.memory.d.ts +6 -2
  130. package/dist/presentation/repo.memory.js +3 -2
  131. package/dist/settings/agent-tools.d.ts +2 -75
  132. package/dist/settings/agent-tools.js +49 -45
  133. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  134. package/dist/settings/agent-value-write-tools.js +154 -0
  135. package/dist/settings/agent-writable-preferences.js +8 -20
  136. package/dist/settings/agent-write-denylist.d.ts +19 -0
  137. package/dist/settings/agent-write-denylist.js +15 -0
  138. package/dist/settings/change-feed.d.ts +11 -2
  139. package/dist/settings/change-feed.js +4 -4
  140. package/dist/settings/definitions-dispatch.d.ts +8 -3
  141. package/dist/settings/definitions-dispatch.js +5 -5
  142. package/dist/settings/dictionaries/index.d.ts +5 -2
  143. package/dist/settings/dictionaries/index.js +2 -2
  144. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
  145. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
  146. package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
  147. package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
  148. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
  149. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
  150. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
  151. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
  152. package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
  153. package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
  154. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
  155. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
  156. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
  157. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
  158. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
  159. package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
  160. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
  161. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
  162. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
  163. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
  164. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
  165. package/dist/settings/ensure-definitions.d.ts +5 -5
  166. package/dist/settings/errors.d.ts +7 -1
  167. package/dist/settings/errors.js +7 -1
  168. package/dist/settings/index.d.ts +4 -1
  169. package/dist/settings/index.js +1 -0
  170. package/dist/settings/ports.d.ts +6 -5
  171. package/dist/settings/principal-lookup.d.ts +9 -0
  172. package/dist/settings/principal-lookup.js +2 -0
  173. package/dist/settings/purge-service.d.ts +5 -4
  174. package/dist/settings/purge-service.js +3 -2
  175. package/dist/settings/settings.d.ts +22 -7
  176. package/dist/settings/settings.js +47 -26
  177. package/dist/settings/tool-registrations.d.ts +25 -32
  178. package/dist/settings/tool-registrations.js +66 -56
  179. package/dist/settings/types.d.ts +1 -1
  180. package/dist/settings/ui-tab-definitions.d.ts +1 -1
  181. package/dist/settings/write-service.d.ts +32 -26
  182. package/dist/settings/write-service.js +26 -20
  183. package/dist/taxonomy/agent-tools.d.ts +1 -66
  184. package/dist/taxonomy/agent-tools.js +25 -15
  185. package/dist/taxonomy/content-lookup.js +1 -1
  186. package/dist/taxonomy/index.d.ts +2 -2
  187. package/dist/taxonomy/merge-term.d.ts +7 -4
  188. package/dist/taxonomy/merge-term.js +7 -5
  189. package/dist/taxonomy/repo.memory.d.ts +42 -19
  190. package/dist/taxonomy/repo.memory.js +14 -12
  191. package/dist/taxonomy/validation-chain.d.ts +39 -18
  192. package/dist/taxonomy/validation-chain.js +35 -25
  193. package/dist/taxonomy/write-service.d.ts +106 -45
  194. package/dist/taxonomy/write-service.js +221 -179
  195. package/dist/trash/adapters/record-store.d.ts +52 -0
  196. package/dist/trash/adapters/record-store.js +36 -0
  197. package/dist/trash/cursor.d.ts +20 -0
  198. package/dist/trash/cursor.js +36 -0
  199. package/dist/trash/follow-ups.d.ts +76 -0
  200. package/dist/trash/follow-ups.js +46 -0
  201. package/dist/trash/index.d.ts +8 -0
  202. package/dist/trash/index.js +8 -0
  203. package/dist/trash/ports.d.ts +308 -0
  204. package/dist/trash/ports.js +2 -0
  205. package/dist/trash/repo.memory.d.ts +50 -0
  206. package/dist/trash/repo.memory.js +114 -0
  207. package/dist/trash/sweeper.d.ts +77 -0
  208. package/dist/trash/sweeper.js +127 -0
  209. package/dist/trash/write-service.d.ts +100 -0
  210. package/dist/trash/write-service.js +242 -0
  211. package/dist/workspace/agent-tools.d.ts +1 -59
  212. package/dist/workspace/agent-tools.js +27 -12
  213. package/dist/workspace/create.d.ts +31 -6
  214. package/dist/workspace/create.js +18 -5
  215. package/dist/workspace/delete.d.ts +18 -13
  216. package/dist/workspace/delete.js +20 -12
  217. package/dist/workspace/index.d.ts +1 -1
  218. package/dist/workspace/repo.memory.d.ts +19 -4
  219. package/dist/workspace/repo.memory.js +26 -4
  220. package/dist/workspace/tool-registrations.d.ts +2 -2
  221. package/dist/workspace/tool-registrations.js +13 -13
  222. package/dist/workspace/update.d.ts +6 -5
  223. package/dist/workspace/update.js +9 -8
  224. package/package.json +44 -38
  225. package/dist/identity/admin-crud-service.d.ts +0 -280
  226. package/dist/identity/admin-crud-service.js +0 -468
  227. package/dist/identity/agent-tool-input.d.ts +0 -58
  228. package/dist/identity/agent-tool-input.js +0 -126
  229. package/dist/identity/agent-tools.d.ts +0 -101
  230. package/dist/identity/agent-tools.js +0 -290
  231. package/dist/identity/auth-service.d.ts +0 -118
  232. package/dist/identity/auth-service.js +0 -170
  233. package/dist/identity/authorize.d.ts +0 -90
  234. package/dist/identity/authorize.js +0 -94
  235. package/dist/identity/grant-service.d.ts +0 -198
  236. package/dist/identity/grant-service.js +0 -361
  237. package/dist/identity/hasher.d.ts +0 -39
  238. package/dist/identity/hasher.js +0 -39
  239. package/dist/identity/index.d.ts +0 -62
  240. package/dist/identity/index.js +0 -54
  241. package/dist/identity/password-policy.d.ts +0 -57
  242. package/dist/identity/password-policy.js +0 -66
  243. package/dist/identity/permission-migrations.d.ts +0 -86
  244. package/dist/identity/permission-migrations.js +0 -67
  245. package/dist/identity/permissions.d.ts +0 -35
  246. package/dist/identity/permissions.js +0 -603
  247. package/dist/identity/ports.d.ts +0 -201
  248. package/dist/identity/repo.memory.d.ts +0 -164
  249. package/dist/identity/repo.memory.js +0 -224
  250. package/dist/identity/seed.d.ts +0 -54
  251. package/dist/identity/seed.js +0 -429
  252. package/dist/identity/tool-registrations.d.ts +0 -50
  253. package/dist/identity/tool-registrations.js +0 -419
  254. package/dist/identity/types.d.ts +0 -185
  255. package/dist/identity/types.js +0 -64
  256. package/dist/identity/username.d.ts +0 -12
  257. package/dist/identity/username.js +0 -14
  258. /package/dist/{identity → media/import}/ports.js +0 -0
@@ -1,34 +1,36 @@
1
- import type { ClockPort } from "../core/ports.js";
2
- import { type ActorIdentityInput, type ActorPrincipalKind, type ContentTypeFieldDef, type ContentTypeRecord, type Result } from "./types.js";
1
+ import type { Clock } from "@jini-ai/core/primitives";
2
+ import { type Result } from "@jini-ai/core/primitives";
3
+ import { type ActorIdentityInput, type ActorPrincipalKind, type ContentTypeFieldDef, type ContentTypeRecord } from "./types.js";
3
4
  /**
4
5
  * @file Write-service — `registerContentType`/`updateContentTypeFields`, the single write
5
6
  * chokepoint for the `content_types` registry.
6
7
  *
7
8
  * Purpose:
8
9
  * The ONLY path that creates or full-replaces a content type's field schema. Every export here:
9
- * `authorize()` first (fail-closed) -> the fixed definition-time guard sequence (CIC U-002-B1) ->
10
+ * `authorize` first (fail-closed) -> the fixed definition-time guard sequence ->
10
11
  * same-transaction row + revision write (+ watermark stamp when supplied) -> index provisioning.
11
12
  *
12
- * `registerContentType`'s CIC U-002-B1 guard order is binding, not incidental: key grammar ->
13
+ * `registerContentType`'s guard order is binding, not incidental: key grammar ->
13
14
  * reserved-key -> field-name grammar -> field-kind -> storage-only-not-queryable -> queryable-cap,
14
15
  * evaluation stops at the first failure. The storage-only check is numbered 4b rather than
15
16
  * renumbering 5, because the fixed order is pinned by name in the certified suite and the existing
16
17
  * five guards keep both their relative order and their identity: 4b sits strictly between the
17
18
  * field-kind check and the queryable-cap check, and can only fire on a field whose kind already
18
- * passed guard 4. `updateContentTypeFields`'s CIC U-004-B1 additionally requires `expectedVersion`
19
+ * passed guard 4. `updateContentTypeFields` additionally requires `expectedVersion`
19
20
  * to be checked BEFORE the `fields_empty` floor and before any per-field guard, so a stale
20
21
  * `expectedVersion` combined with `fields: []` reports `VERSION_CONFLICT`, never
21
22
  * `VALIDATION_ERROR(fields_empty)`.
22
23
  *
23
24
  * How it relates to the project:
24
- * `deps.watermark` (REQ-01/INV-08) and the actor-identity delegation fields
25
- * (`delegatedByWorkspaceId`/`delegatedById`, REQ-16) are both optional/additive — every
25
+ * `deps.watermark` and the actor-identity delegation fields
26
+ * (`delegatedByWorkspaceId`/`delegatedById`) are both optional/additive — every
26
27
  * pre-existing call site that omits them is unaffected; `watermark-stamping.integration.test.ts`
27
28
  * exercises both.
28
29
  *
29
30
  * Architectural role:
30
31
  * `features/content-types` domain logic. Depends only on `core/ports` and this package's own
31
32
  * `errors.ts`/`index-provisioning.ts`/`types.ts` — no adapter, no other feature.
33
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
32
34
  */
33
35
  /**
34
36
  * Matches every other feature's chokepoint `AuthorizeFn` shape structurally — no shared import,
@@ -40,16 +42,8 @@ import { type ActorIdentityInput, type ActorPrincipalKind, type ContentTypeField
40
42
  * `resource_scope_mismatch` even though the route's pre-check allowed it, and why this chokepoint —
41
43
  * not the route — is the one that must stay at least as expressive.
42
44
  */
43
- export type AuthorizeFn = (params: {
44
- principalId: string;
45
- permission: string;
46
- workspaceId: string;
47
- entityType?: string | undefined;
48
- entityId?: string | undefined;
49
- }) => Promise<{
50
- allowed: boolean;
51
- reason: string;
52
- }>;
45
+ export type { AuthorizationPort as AuthorizeFn } from '../core/authorization.js';
46
+ import type { AuthorizationPort as AuthorizeFn } from '../core/authorization.js';
53
47
  export interface ContentTypeRevisionInput {
54
48
  contentTypeKey: string;
55
49
  workspaceId: string;
@@ -74,7 +68,9 @@ export interface ContentTypeRepoPort {
74
68
  workspaceId: string;
75
69
  key: string;
76
70
  }): Promise<ContentTypeRecord | null>;
77
- transaction<T>(fn: () => Promise<T>): Promise<T>;
71
+ transaction<T>(required: {
72
+ fn: () => Promise<T>;
73
+ }): Promise<T>;
78
74
  }
79
75
  export interface IndexProvisionerPort {
80
76
  provisionIndexesForNewContentType(input: {
@@ -97,7 +93,7 @@ export interface OutboxPort {
97
93
  payload: Record<string, unknown>;
98
94
  }): Promise<void>;
99
95
  }
100
- /** Optional (REQ-01/INV-08) — advances `database_write_watermark` by exactly 1 when supplied. */
96
+ /** Optional — advances `database_write_watermark` by exactly 1 when supplied. See docs/decisions/DR-001-safe-schema-and-index-transitions.md. */
101
97
  export interface WatermarkPort {
102
98
  stampWatermark(input: {
103
99
  workspaceId: string;
@@ -105,7 +101,7 @@ export interface WatermarkPort {
105
101
  }
106
102
  export interface ContentTypeWriteServiceDeps {
107
103
  repo: ContentTypeRepoPort;
108
- clock: ClockPort;
104
+ clock: Clock;
109
105
  ids: {
110
106
  newId: () => string;
111
107
  };
@@ -124,11 +120,12 @@ export interface RegisterContentTypeRequired {
124
120
  };
125
121
  }
126
122
  /**
127
- * REQ-01..08 — registers a new content type. Guard order is CIC U-002-B1, fixed and stop-at-first-
123
+ * 08 — registers a new content type. Guard order is, fixed and stop-at-first-
128
124
  * failure: key grammar -> reserved-key -> field-name grammar -> field-kind -> queryable-cap.
129
125
  *
130
126
  * @complexity O(f) in the number of submitted fields (two guard passes over `fields`).
131
127
  * @overallScore 100
128
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
132
129
  */
133
130
  export declare function registerContentType(required: RegisterContentTypeRequired): Promise<Result<{
134
131
  contentType: ContentTypeRecord;
@@ -152,12 +149,13 @@ export interface UpdateContentTypeFieldsRequired {
152
149
  };
153
150
  }
154
151
  /**
155
- * REQ-26 — full-replace of a content type's field schema. CIC U-004-B1: `expectedVersion` is
152
+ * — full-replace of a content type's field schema. : `expectedVersion` is
156
153
  * checked BEFORE the `fields_empty` floor and before any per-field guard, so a stale version
157
154
  * combined with an empty array reports `VERSION_CONFLICT`, never `VALIDATION_ERROR`.
158
155
  *
159
156
  * @complexity O(f) in the number of submitted fields.
160
157
  * @overallScore 100
158
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
161
159
  */
162
160
  export declare function updateContentTypeFields(required: UpdateContentTypeFieldsRequired): Promise<Result<{
163
161
  contentType: ContentTypeRecord;
@@ -1,7 +1,9 @@
1
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
2
  import { EntityNotLiveError } from "../core/entity-liveness.js";
2
3
  import { ContentTypeAlreadyExistsError, ContentTypeNotFoundError, ForbiddenError, InvalidFieldKindError, InvalidFieldNameGrammarError, InvalidKeyGrammarError, QueryableFieldCapExceededError, ReservedContentTypeKeyError, StorageOnlyFieldNotQueryableError, ValidationError, VersionConflictError, } from "./errors.js";
3
4
  import { resolveFieldIndexTransition, validateIdentifierGrammar } from "./index-provisioning.js";
4
- import { isContentTypeFieldKind, isIndexableFieldKind, } from "./types.js";
5
+ import {} from "@jini-ai/core/primitives";
6
+ import { isContentTypeFieldKind, isIndexableFieldKind } from "./types.js";
5
7
  /** Reserved forever for the legacy `posts` table — an operator Collection can never take these keys. */
6
8
  /**
7
9
  * Field-name guards shared by register and update, in order: every name passes the identifier
@@ -14,14 +16,14 @@ import { isContentTypeFieldKind, isIndexableFieldKind, } from "./types.js";
14
16
  */
15
17
  function fieldNameError(fields) {
16
18
  for (const field of fields) {
17
- if (!validateIdentifierGrammar(field.name)) {
18
- return new InvalidFieldNameGrammarError(`field name '${field.name}' fails the identifier grammar gate`);
19
+ if (!validateIdentifierGrammar({ value: field.name })) {
20
+ return new InvalidFieldNameGrammarError({ message: `field name '${field.name}' fails the identifier grammar gate` });
19
21
  }
20
22
  }
21
23
  const seen = new Set();
22
24
  for (const field of fields) {
23
25
  if (seen.has(field.name)) {
24
- return new ValidationError(`field name '${field.name}' appears more than once`, "fields_duplicate_name");
26
+ return new ValidationError({ message: `field name '${field.name}' appears more than once`, reason: "fields_duplicate_name" });
25
27
  }
26
28
  seen.add(field.name);
27
29
  }
@@ -34,30 +36,30 @@ function countQueryableFields(fields) {
34
36
  return fields.filter((f) => f.queryable).length;
35
37
  }
36
38
  /**
37
- * REQ-01..08 — registers a new content type. Guard order is CIC U-002-B1, fixed and stop-at-first-
39
+ * 08 — registers a new content type. Guard order is, fixed and stop-at-first-
38
40
  * failure: key grammar -> reserved-key -> field-name grammar -> field-kind -> queryable-cap.
39
41
  *
40
42
  * @complexity O(f) in the number of submitted fields (two guard passes over `fields`).
41
43
  * @overallScore 100
44
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
42
45
  */
43
46
  export async function registerContentType(required) {
44
47
  const { deps, input } = required;
45
48
  const authResult = await deps.authorize({
46
49
  principalId: input.actorId,
47
50
  permission: "admin.collections.manage",
48
- workspaceId: input.workspaceId,
49
- entityType: "content-type",
50
- });
51
+ workspaceId: input.workspaceId
52
+ }, { entityType: "content-type" });
51
53
  if (!authResult.allowed) {
52
- return { ok: false, error: new ForbiddenError(`principal '${input.actorId}' cannot register a content type (${authResult.reason})`) };
54
+ return { ok: false, error: new ForbiddenError({ message: `principal '${input.actorId}' cannot register a content type (${authResult.reason})` }) };
53
55
  }
54
56
  // Guard 1: key grammar.
55
- if (!validateIdentifierGrammar(input.key)) {
56
- return { ok: false, error: new InvalidKeyGrammarError(`content-type key '${input.key}' fails the identifier grammar gate ^[a-z][a-z0-9_]{0,63}$`) };
57
+ if (!validateIdentifierGrammar({ value: input.key })) {
58
+ return { ok: false, error: new InvalidKeyGrammarError({ message: `content-type key '${input.key}' fails the identifier grammar gate ^[a-z][a-z0-9_]{0,63}$` }) };
57
59
  }
58
60
  // Guard 2: reserved key.
59
61
  if (RESERVED_CONTENT_TYPE_KEYS.has(input.key)) {
60
- return { ok: false, error: new ReservedContentTypeKeyError(`key '${input.key}' is permanently reserved for the legacy 'posts' table`) };
62
+ return { ok: false, error: new ReservedContentTypeKeyError({ message: `key '${input.key}' is permanently reserved for the legacy 'posts' table` }) };
61
63
  }
62
64
  // Guard 3: field-name grammar, then uniqueness (every field, before any kind/cap check).
63
65
  const nameErrorOnRegister = fieldNameError(input.fields);
@@ -65,8 +67,8 @@ export async function registerContentType(required) {
65
67
  return { ok: false, error: nameErrorOnRegister };
66
68
  // Guard 4: field-kind, closed enum.
67
69
  for (const field of input.fields) {
68
- if (!isContentTypeFieldKind(field.kind)) {
69
- return { ok: false, error: new InvalidFieldKindError(`field '${field.name}' has kind '${field.kind}', not one of the closed field-kind enum`) };
70
+ if (!isContentTypeFieldKind({ value: field.kind })) {
71
+ return { ok: false, error: new InvalidFieldKindError({ message: `field '${field.name}' has kind '${field.kind}', not one of the closed field-kind enum` }) };
70
72
  }
71
73
  }
72
74
  // Guard 4b: a storage-only kind cannot be queryable. Placed strictly between guard 4 and guard 5
@@ -74,24 +76,24 @@ export async function registerContentType(required) {
74
76
  // BEFORE the cap count and long before `resolveFieldIndexTransition` could hand it to the index
75
77
  // provisioner. This is the check that keeps a kind with no CAST target off the DDL path.
76
78
  for (const field of input.fields) {
77
- if (field.queryable && !isIndexableFieldKind(field.kind)) {
79
+ if (field.queryable && !isIndexableFieldKind({ value: field.kind })) {
78
80
  return {
79
81
  ok: false,
80
- error: new StorageOnlyFieldNotQueryableError(`field '${field.name}' has storage-only kind '${field.kind}' and cannot be queryable`),
82
+ error: new StorageOnlyFieldNotQueryableError({ message: `field '${field.name}' has storage-only kind '${field.kind}' and cannot be queryable` }),
81
83
  };
82
84
  }
83
85
  }
84
86
  // Guard 5: queryable-field cap, submitted array alone.
85
87
  if (countQueryableFields(input.fields) > QUERYABLE_FIELD_CAP) {
86
- return { ok: false, error: new QueryableFieldCapExceededError(`content type '${input.key}' submits more than ${QUERYABLE_FIELD_CAP} queryable fields`) };
88
+ return { ok: false, error: new QueryableFieldCapExceededError({ message: `content type '${input.key}' submits more than ${QUERYABLE_FIELD_CAP} queryable fields` }) };
87
89
  }
88
90
  // Row 7 — a pre-check outside the transaction. Not race-safe by itself (that's the in-tx check
89
91
  // below), but it avoids opening a transaction for the overwhelmingly common not-found case.
90
92
  const existingBeforeTx = await deps.repo.findByKey({ workspaceId: input.workspaceId, key: input.key });
91
93
  if (existingBeforeTx) {
92
- return { ok: false, error: new ContentTypeAlreadyExistsError(input.key, existingBeforeTx.status === "tombstone") };
94
+ return { ok: false, error: new ContentTypeAlreadyExistsError({ key: input.key, tombstoned: existingBeforeTx.status === "tombstone" }) };
93
95
  }
94
- const now = deps.clock.nowIso();
96
+ const now = kernelNowIso({ clock: deps.clock });
95
97
  const contentType = {
96
98
  workspaceId: input.workspaceId,
97
99
  key: input.key,
@@ -102,28 +104,28 @@ export async function registerContentType(required) {
102
104
  tombstonedAt: null,
103
105
  };
104
106
  try {
105
- await deps.repo.transaction(async () => {
106
- // The check that actually closes the race: two concurrent first-widget creates both see the
107
- // pre-check above return null, but only one of them can win this in-tx re-check before save.
108
- const existingInTx = await deps.repo.findByKey({ workspaceId: input.workspaceId, key: input.key });
109
- if (existingInTx) {
110
- throw new ContentTypeAlreadyExistsError(input.key, existingInTx.status === "tombstone");
111
- }
112
- await deps.repo.save(contentType);
113
- await deps.repo.appendRevision({
114
- contentTypeKey: input.key,
115
- workspaceId: input.workspaceId,
116
- op: "register",
117
- stateJson: contentType,
118
- actorId: input.actorId,
119
- principalKind: input.principalKind ?? null,
120
- delegatedByWorkspaceId: input.delegatedByWorkspaceId ?? null,
121
- delegatedById: input.delegatedById ?? null,
122
- recordedAt: now,
123
- });
124
- if (deps.watermark)
125
- await deps.watermark.stampWatermark({ workspaceId: input.workspaceId });
126
- });
107
+ await deps.repo.transaction({ fn: async () => {
108
+ // The check that actually closes the race: two concurrent first-widget creates both see the
109
+ // pre-check above return null, but only one of them can win this in-tx re-check before save.
110
+ const existingInTx = await deps.repo.findByKey({ workspaceId: input.workspaceId, key: input.key });
111
+ if (existingInTx) {
112
+ throw new ContentTypeAlreadyExistsError({ key: input.key, tombstoned: existingInTx.status === "tombstone" });
113
+ }
114
+ await deps.repo.save(contentType);
115
+ await deps.repo.appendRevision({
116
+ contentTypeKey: input.key,
117
+ workspaceId: input.workspaceId,
118
+ op: "register",
119
+ stateJson: contentType,
120
+ actorId: input.actorId,
121
+ principalKind: input.principalKind ?? null,
122
+ delegatedByWorkspaceId: input.delegatedByWorkspaceId ?? null,
123
+ delegatedById: input.delegatedById ?? null,
124
+ recordedAt: now,
125
+ });
126
+ if (deps.watermark)
127
+ await deps.watermark.stampWatermark({ workspaceId: input.workspaceId });
128
+ } });
127
129
  }
128
130
  catch (err) {
129
131
  if (err instanceof ContentTypeAlreadyExistsError)
@@ -140,47 +142,47 @@ export async function registerContentType(required) {
140
142
  return { ok: true, value: { contentType } };
141
143
  }
142
144
  /**
143
- * REQ-26 — full-replace of a content type's field schema. CIC U-004-B1: `expectedVersion` is
145
+ * — full-replace of a content type's field schema. : `expectedVersion` is
144
146
  * checked BEFORE the `fields_empty` floor and before any per-field guard, so a stale version
145
147
  * combined with an empty array reports `VERSION_CONFLICT`, never `VALIDATION_ERROR`.
146
148
  *
147
149
  * @complexity O(f) in the number of submitted fields.
148
150
  * @overallScore 100
151
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
149
152
  */
150
153
  export async function updateContentTypeFields(required) {
151
154
  const { deps, input } = required;
152
155
  const authResult = await deps.authorize({
153
156
  principalId: input.actorId,
154
157
  permission: "admin.collections.manage",
155
- workspaceId: input.workspaceId,
156
- entityType: "content-type",
157
- });
158
+ workspaceId: input.workspaceId
159
+ }, { entityType: "content-type" });
158
160
  if (!authResult.allowed) {
159
- return { ok: false, error: new ForbiddenError(`principal '${input.actorId}' cannot update content type '${input.key}' (${authResult.reason})`) };
161
+ return { ok: false, error: new ForbiddenError({ message: `principal '${input.actorId}' cannot update content type '${input.key}' (${authResult.reason})` }) };
160
162
  }
161
163
  const current = await deps.repo.findByKey({ workspaceId: input.workspaceId, key: input.key });
162
164
  if (!current) {
163
- return { ok: false, error: new ContentTypeNotFoundError(`content type '${input.key}' was not found in workspace '${input.workspaceId}'`) };
165
+ return { ok: false, error: new ContentTypeNotFoundError({ message: `content type '${input.key}' was not found in workspace '${input.workspaceId}'` }) };
164
166
  }
165
- // Row 15's sibling — a tombstoned type is terminal (INV-06); this check runs before the version
167
+ // Row 15's sibling — a tombstoned type is terminal; this check runs before the version. See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
166
168
  // check so a stale-version update against a tombstoned row still reports "tombstoned", not
167
169
  // "version conflict" (the tombstone tore down every queryable index, so the schema is moot).
168
170
  if (current.status === "tombstone") {
169
- return { ok: false, error: new EntityNotLiveError("content type", input.key, "tombstoned") };
171
+ return { ok: false, error: new EntityNotLiveError({ entityType: "content type", entityId: input.key, state: "tombstoned" }) };
170
172
  }
171
- // U-004-B1: expectedVersion checked FIRST — before fields_empty, before any per-field guard.
173
+ // expectedVersion checked FIRST — before fields_empty, before any per-field guard. See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
172
174
  if (input.expectedVersion !== current.version) {
173
- return { ok: false, error: new VersionConflictError(`expected version ${input.expectedVersion} for content type '${input.key}', found ${current.version}`) };
175
+ return { ok: false, error: new VersionConflictError({ message: `expected version ${input.expectedVersion} for content type '${input.key}', found ${current.version}` }) };
174
176
  }
175
177
  if (input.fields.length === 0) {
176
- return { ok: false, error: new ValidationError(`content type '${input.key}' update submitted an empty fields array`, "fields_empty") };
178
+ return { ok: false, error: new ValidationError({ message: `content type '${input.key}' update submitted an empty fields array`, reason: "fields_empty" }) };
177
179
  }
178
180
  const nameErrorOnUpdate = fieldNameError(input.fields);
179
181
  if (nameErrorOnUpdate)
180
182
  return { ok: false, error: nameErrorOnUpdate };
181
183
  for (const field of input.fields) {
182
- if (!isContentTypeFieldKind(field.kind)) {
183
- return { ok: false, error: new InvalidFieldKindError(`field '${field.name}' has kind '${field.kind}', not one of the closed field-kind enum`) };
184
+ if (!isContentTypeFieldKind({ value: field.kind })) {
185
+ return { ok: false, error: new InvalidFieldKindError({ message: `field '${field.name}' has kind '${field.kind}', not one of the closed field-kind enum` }) };
184
186
  }
185
187
  }
186
188
  // Guard 4b: a storage-only kind cannot be queryable. Placed strictly between guard 4 and guard 5
@@ -188,36 +190,36 @@ export async function updateContentTypeFields(required) {
188
190
  // BEFORE the cap count and long before `resolveFieldIndexTransition` could hand it to the index
189
191
  // provisioner. This is the check that keeps a kind with no CAST target off the DDL path.
190
192
  for (const field of input.fields) {
191
- if (field.queryable && !isIndexableFieldKind(field.kind)) {
193
+ if (field.queryable && !isIndexableFieldKind({ value: field.kind })) {
192
194
  return {
193
195
  ok: false,
194
- error: new StorageOnlyFieldNotQueryableError(`field '${field.name}' has storage-only kind '${field.kind}' and cannot be queryable`),
196
+ error: new StorageOnlyFieldNotQueryableError({ message: `field '${field.name}' has storage-only kind '${field.kind}' and cannot be queryable` }),
195
197
  };
196
198
  }
197
199
  }
198
200
  if (countQueryableFields(input.fields) > QUERYABLE_FIELD_CAP) {
199
- return { ok: false, error: new QueryableFieldCapExceededError(`content type '${input.key}' submits more than ${QUERYABLE_FIELD_CAP} queryable fields`) };
201
+ return { ok: false, error: new QueryableFieldCapExceededError({ message: `content type '${input.key}' submits more than ${QUERYABLE_FIELD_CAP} queryable fields` }) };
200
202
  }
201
203
  const before = current.fields;
202
204
  const after = input.fields;
203
- const now = deps.clock.nowIso();
205
+ const now = kernelNowIso({ clock: deps.clock });
204
206
  const updated = { ...current, fields: after, label: input.label ?? current.label, version: current.version + 1 };
205
- await deps.repo.transaction(async () => {
206
- await deps.repo.save(updated);
207
- await deps.repo.appendRevision({
208
- contentTypeKey: input.key,
209
- workspaceId: input.workspaceId,
210
- op: "field-change",
211
- stateJson: updated,
212
- actorId: input.actorId,
213
- principalKind: input.principalKind ?? null,
214
- delegatedByWorkspaceId: input.delegatedByWorkspaceId ?? null,
215
- delegatedById: input.delegatedById ?? null,
216
- recordedAt: now,
217
- });
218
- if (deps.watermark)
219
- await deps.watermark.stampWatermark({ workspaceId: input.workspaceId });
220
- });
207
+ await deps.repo.transaction({ fn: async () => {
208
+ await deps.repo.save(updated);
209
+ await deps.repo.appendRevision({
210
+ contentTypeKey: input.key,
211
+ workspaceId: input.workspaceId,
212
+ op: "field-change",
213
+ stateJson: updated,
214
+ actorId: input.actorId,
215
+ principalKind: input.principalKind ?? null,
216
+ delegatedByWorkspaceId: input.delegatedByWorkspaceId ?? null,
217
+ delegatedById: input.delegatedById ?? null,
218
+ recordedAt: now,
219
+ });
220
+ if (deps.watermark)
221
+ await deps.watermark.stampWatermark({ workspaceId: input.workspaceId });
222
+ } });
221
223
  await deps.indexProvisioner.applyFieldIndexTransitions({
222
224
  workspaceId: input.workspaceId,
223
225
  contentTypeKey: input.key,
@@ -226,12 +228,13 @@ export async function updateContentTypeFields(required) {
226
228
  return { ok: true, value: { contentType: updated } };
227
229
  }
228
230
  /**
229
- * CIC U-003-B1 composition — resolves an index transition for every field name present on either
231
+ * composition — resolves an index transition for every field name present on either
230
232
  * side of a full-replace update (union of before/after names), one before/after comparison per
231
233
  * name, and returns only the actionable (non-`"none"`) transitions.
232
234
  *
233
235
  * @complexity O(f) in the number of distinct field names across both arrays.
234
236
  * @overallScore 100
237
+ * See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
235
238
  */
236
239
  function computeFullReplaceTransitions(before, after) {
237
240
  const beforeByName = new Map(before.map((f) => [f.name, f]));
@@ -0,0 +1,15 @@
1
+ /** Caller-supplied authorization evaluator shared by content domain write services. */
2
+ export interface AuthorizationRequired {
3
+ principalId: string;
4
+ permission: string;
5
+ workspaceId: string;
6
+ }
7
+ export interface AuthorizationOptional {
8
+ entityType?: string | undefined;
9
+ entityId?: string | undefined;
10
+ }
11
+ export type AuthorizationPort = (required: AuthorizationRequired, optional?: AuthorizationOptional) => Promise<{
12
+ allowed: boolean;
13
+ reason: string;
14
+ }>;
15
+ //# sourceMappingURL=authorization.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=authorization.js.map
@@ -1,4 +1,5 @@
1
- import type { DomainEvent, ISODateTime, JsonObject, UUID } from "../ports.js";
1
+ import type { ISODateTime, JsonObject, UUID } from "@jini-ai/core/primitives";
2
+ import type { DomainEvent } from "../ports.js";
2
3
  /**
3
4
  * @file Change-set vocabulary and persistence contract.
4
5
  *
@@ -15,7 +16,7 @@ import type { DomainEvent, ISODateTime, JsonObject, UUID } from "../ports.js";
15
16
  * This is the storage shape required to land with the first persistent
16
17
  * schema. SQLite/Postgres adapters must satisfy `ChangeSetRepoPort` unchanged.
17
18
  *
18
- * `insert()`'s optional third argument lets the caller pass the fully-formed outbox event through
19
+ * `insert()`'s optional event in its second object lets the caller pass the fully-formed outbox event through
19
20
  * the SAME call, so a durable adapter can co-persist it inside the same transaction as the header
20
21
  * + items. This does NOT bring the domain write (`mutation.execute()`) into the transaction — that
21
22
  * remains covered by `executeCommand()`'s existing compensating-rollback path. See
@@ -59,10 +60,11 @@ export interface ChangeSetItemRecord {
59
60
  */
60
61
  inversePayload?: JsonObject | undefined;
61
62
  /**
62
- * Entity version *after* the mutation (REQ-08 guard input). Revert refuses
63
+ * Entity version *after* the mutation ( guard input). Revert refuses
63
64
  * unless the entity's current version still equals this — otherwise the entity
64
65
  * has moved on since. Undefined for entity types without a version.
65
- */
66
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
67
+ */
66
68
  entityVersionAtApply?: number | undefined;
67
69
  /** Apply order; revert walks positions in reverse. */
68
70
  position: number;
@@ -75,12 +77,18 @@ export interface ChangeSetWithItems {
75
77
  /** Persistence contract for change sets. */
76
78
  export interface ChangeSetRepoPort {
77
79
  /**
78
- * `event`, when present, must be durably recorded atomically with `record`/`items` (BR-04 —
80
+ * `event`, when present, must be durably recorded atomically with `record`/`items` ( —
79
81
  * see this file's header). A SQLite adapter co-persists it inside the same transaction; the
80
82
  * in-memory adapter forwards it to its injected event bus/outbox. Omit `event` for a change set
81
83
  * that has no associated domain event to deliver.
82
- */
83
- insert(record: ChangeSetRecord, items: ChangeSetItemRecord[], event?: DomainEvent): Promise<void>;
84
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
85
+ */
86
+ insert(required: {
87
+ record: ChangeSetRecord;
88
+ items: ChangeSetItemRecord[];
89
+ }, optional?: {
90
+ event?: DomainEvent | undefined;
91
+ }): Promise<void>;
84
92
  findById(required: {
85
93
  workspaceId: UUID;
86
94
  id: UUID;
@@ -1,4 +1,5 @@
1
- import type { ClockPort, IdGeneratorPort, JsonObject, OutboxPort, UUID } from "../ports.js";
1
+ import type { Clock, IdGenerator, JsonObject, UUID } from "@jini-ai/core/primitives";
2
+ import type { OutboxPort } from "../ports.js";
2
3
  import type { ChangeSetOperation, ChangeSetRepoPort } from "./change-set.js";
3
4
  /**
4
5
  * @file The single mutation write path.
@@ -58,20 +59,27 @@ export interface CommandMutation<TResult> {
58
59
  execute(): Promise<TResult>;
59
60
  /**
60
61
  * Read the entity's version from the execute result (the version *after* the
61
- * mutation, REQ-08 / AC-02). Stamped onto the item as the revert guard input.
62
+ * mutation). Stamped onto the item as the revert guard input.
62
63
  * Omit for entity types without a version.
63
- */
64
- captureEntityVersion?(result: TResult): number | null;
64
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
65
+ */
66
+ captureEntityVersion?(required: {
67
+ result: TResult;
68
+ }, optional?: Record<string, never>): number | null;
65
69
  /**
66
70
  * Compensating rollback for the memory adapter's all-or-nothing guarantee
67
- * (REQ-01 / BR-04 / EC-08 / AC-17). If the change-set record fails to persist
68
- * *after* `execute()` has applied the feature mutation, the gateway calls this
71
+ * If version capture, record preparation or persistence fails
72
+ * *after* `execute` has applied the feature mutation, the gateway calls this
69
73
  * to undo the mutation — restoring the entity to its exact pre-`execute` state
70
74
  * (verbatim, including `version`) so no "mutation without a record" survives
71
- * (INV-01). On the SQLite adapter (RT-004) a real transaction replaces this and
72
- * `rollback` becomes a no-op/omitted. Omit only for a mutation whose record step
73
- * cannot fail independently of its feature write (none in v1).
74
- */
75
+ * On the SQLite adapter a real transaction replaces this and
76
+ * `rollback` may be a no-op/omitted only when a caller-owned transaction covers
77
+ * the feature write AND all subsequent record preparation/persistence. The gateway
78
+ * opens no such transaction itself. Without rollback or that outer transaction, a
79
+ * post-mutation failure rejects but leaves the feature mutation applied (
80
+ * is not guaranteed). A rollback rejection takes precedence over the original error.
81
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
82
+ */
75
83
  rollback?(): Promise<void>;
76
84
  }
77
85
  /**
@@ -95,8 +103,8 @@ export type AuthorizeFn = (params: {
95
103
  }>;
96
104
  /** Dependencies for executeCommand. */
97
105
  export interface ExecuteCommandDeps {
98
- clock: ClockPort;
99
- idGen: IdGeneratorPort;
106
+ clock: Clock;
107
+ idGen: IdGenerator;
100
108
  changeSets: ChangeSetRepoPort;
101
109
  /** When provided, `change-set.applied` is enqueued for async consumers. */
102
110
  outbox?: OutboxPort | undefined;
@@ -126,18 +134,26 @@ export interface ExecuteCommandOptional {
126
134
  */
127
135
  export declare class DuplicateCommandError extends Error {
128
136
  readonly changeSetId: UUID;
129
- constructor(message: string, changeSetId: UUID);
137
+ constructor(requiredArgs: {
138
+ message: string;
139
+ changeSetId: UUID;
140
+ }, optionalArgs?: Record<string, never>);
130
141
  }
131
142
  /**
132
- * Thrown when `authorize()` denies the caller. Routes map
133
- * this to 403 `FORBIDDEN`. Raised before the idempotency check (INV-04) — a
143
+ * Thrown when `authorize` denies the caller. Routes map
144
+ * this to 403 `FORBIDDEN`. Raised before the idempotency check — a
134
145
  * replayed command id from a denied caller therefore never produces
135
- * `DuplicateCommandError` either (EC-08).
146
+ * `DuplicateCommandError` either.
147
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
136
148
  */
137
149
  export declare class ForbiddenError extends Error {
138
150
  readonly permission: string;
139
151
  readonly reason: string;
140
- constructor(message: string, permission: string, reason: string);
152
+ constructor({ message, permission, reason }: {
153
+ message: string;
154
+ permission: string;
155
+ reason: string;
156
+ }, _optional?: Record<string, never>);
141
157
  }
142
158
  /**
143
159
  * Execute a mutation through the command gateway.
@@ -146,20 +162,23 @@ export declare class ForbiddenError extends Error {
146
162
  * If execute throws, nothing is recorded; if inverse capture throws, the
147
163
  * mutation never runs.
148
164
  *
149
- * The feature mutation and its change-set record commit as one unit of work
150
- * (REQ-01): if `changeSets.insert` fails after `execute` has applied the
151
- * mutation, the gateway rolls the mutation back via `mutation.rollback` and
152
- * re-throws, so no change set and no outbox event survive (EC-08 / AC-17,
153
- * INV-01).
165
+ * With a working `mutation.rollback`, the feature mutation and its change-set
166
+ * record form one compensated unit of work : failures in version capture,
167
+ * record preparation or `changeSets.insert` after `execute` trigger rollback and
168
+ * rejection. When rollback is omitted, the gateway still
169
+ * rejects but cannot undo the applied feature mutation; a caller-owned outer
170
+ * transaction must supply atomicity. Record/event atomicity depends on the repo
171
+ * honoring its port contract.
154
172
  *
155
- * The `change-set.applied` event is passed as `changeSets.insert()`'s third argument, not a
156
- * separate `deps.outbox.enqueue()` call afterward — a durable adapter co-persists it inside the
173
+ * The `change-set.applied` event is passed in `changeSets.insert`'s optional object, not a
174
+ * separate `deps.outbox.enqueue` call afterward — a durable adapter co-persists it inside the
157
175
  * same transaction as the change-set record, so it can never land without a durable delivery
158
176
  * record (or vice versa). This still does NOT cover the domain mutation itself
159
- * (`mutation.execute()` stays outside any shared transaction, covered only by the compensating
177
+ * (`mutation.execute` stays outside any shared transaction, covered only by the compensating
160
178
  * rollback above), and it covers only this producer. See
161
179
  * `docs/decisions/change-set-outbox-transaction-boundary.md` for the full rationale, including the
162
- * other direct `OutboxPort.enqueue()` producers this decision deliberately leaves untouched.
180
+ * other direct `OutboxPort.enqueue` producers this decision deliberately leaves untouched.
181
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
163
182
  */
164
183
  export declare function executeCommand<TResult>(required: ExecuteCommandRequired<TResult>, _optional?: ExecuteCommandOptional): Promise<{
165
184
  result: TResult;