@classytic/primitives 0.15.0 → 0.16.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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.16.0] - 2026-07-27
7
+
8
+ ### Added — `assertApproved` + `CHAIN_INCOMPLETE` (`/approval`)
9
+
10
+ - **`ApprovalErrorCode` gains `'CHAIN_INCOMPLETE'`** — the single canonical
11
+ error code for "finalize/commit attempted before the chain reached `approved`".
12
+ Hosts route it through `rethrowApprovalError` to the `approval.chain_incomplete`
13
+ wire code so all commit/post/finalize boundaries raise the same structured error.
14
+ - **`assertApproved(chain, message?)`** — canonical finalize-time gate: asserts
15
+ the chain has reached `approved`, else throws `ApprovalError('CHAIN_INCOMPLETE')`.
16
+ A `null`/absent chain passes (no chain required). Use at every journal-entry
17
+ post, PO approve, transfer dispatch, etc. to replace per-domain hand-rolled
18
+ divergent codes with one definition.
19
+
20
+ Purely additive — no existing exports changed.
21
+
6
22
  ## [0.15.0] - 2026-07-25
7
23
 
8
24
  ### Added — `/retention`: `PurgeEvidence` value object
@@ -83,7 +83,12 @@ interface CreateStepInput {
83
83
  readonly requiredApprovals?: number;
84
84
  readonly threshold?: ApprovalThreshold;
85
85
  }
86
- type ApprovalErrorCode = 'EMPTY_STEPS' | 'DUPLICATE_STEP_ID' | 'EMPTY_APPROVERS' | 'DUPLICATE_APPROVER_ID' | 'INVALID_QUORUM' | 'UNKNOWN_STEP' | 'UNAUTHORIZED_APPROVER' | 'STEP_NOT_ACTIVE' | 'STEP_ALREADY_DECIDED_BY_APPROVER';
86
+ type ApprovalErrorCode = 'EMPTY_STEPS' | 'DUPLICATE_STEP_ID' | 'EMPTY_APPROVERS' | 'DUPLICATE_APPROVER_ID' | 'INVALID_QUORUM' | 'UNKNOWN_STEP' | 'UNAUTHORIZED_APPROVER' | 'STEP_NOT_ACTIVE' | 'STEP_ALREADY_DECIDED_BY_APPROVER'
87
+ /** A finalize/commit was attempted while the chain has not reached
88
+ * `approved` (still `pending`, or `rejected`). The single canonical
89
+ * "not yet approved" gate — hosts route it through arc-approval's
90
+ * `rethrowApprovalError` to the `approval.chain_incomplete` wire code. */
91
+ | 'CHAIN_INCOMPLETE';
87
92
  declare class ApprovalError extends Error {
88
93
  readonly name = "ApprovalError";
89
94
  readonly code: ApprovalErrorCode;
@@ -123,6 +128,16 @@ declare function applyDecision(chain: ApprovalChain, input: DecisionInput): Appr
123
128
  */
124
129
  declare function skipStep(chain: ApprovalChain, stepId: string, reason?: string): ApprovalChain;
125
130
  declare function isApproved(chain: ApprovalChain): boolean;
131
+ /**
132
+ * The canonical finalize-time gate: assert `chain` has reached `approved`,
133
+ * else throw `ApprovalError('CHAIN_INCOMPLETE')`. Use this at every
134
+ * commit/post/finalize boundary that must not proceed on an unapproved chain
135
+ * (journal-entry post, PO approve, transfer dispatch, …) so they all raise the
136
+ * SAME error instead of hand-rolling divergent codes. A `null`/absent chain is
137
+ * treated as "no chain required" and passes — callers that require a chain
138
+ * check presence separately.
139
+ */
140
+ declare function assertApproved(chain: ApprovalChain | null | undefined, message?: string): void;
126
141
  declare function isRejected(chain: ApprovalChain): boolean;
127
142
  declare function isPending(chain: ApprovalChain): boolean;
128
143
  /**
@@ -130,4 +145,4 @@ declare function isPending(chain: ApprovalChain): boolean;
130
145
  */
131
146
  declare function decisionCount(chain: ApprovalChain): number;
132
147
  //#endregion
133
- export { ApprovalChain, ApprovalDecision, ApprovalError, ApprovalErrorCode, ApprovalStatus, ApprovalStep, ApprovalThreshold, Approver, ChainOrder, CreateChainInput, CreateStepInput, DecisionInput, applyDecision, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
148
+ export { ApprovalChain, ApprovalDecision, ApprovalError, ApprovalErrorCode, ApprovalStatus, ApprovalStep, ApprovalThreshold, Approver, ChainOrder, CreateChainInput, CreateStepInput, DecisionInput, applyDecision, assertApproved, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
package/dist/approval.mjs CHANGED
@@ -115,6 +115,18 @@ function skipStep(chain, stepId, reason) {
115
115
  function isApproved(chain) {
116
116
  return chain.status === "approved";
117
117
  }
118
+ /**
119
+ * The canonical finalize-time gate: assert `chain` has reached `approved`,
120
+ * else throw `ApprovalError('CHAIN_INCOMPLETE')`. Use this at every
121
+ * commit/post/finalize boundary that must not proceed on an unapproved chain
122
+ * (journal-entry post, PO approve, transfer dispatch, …) so they all raise the
123
+ * SAME error instead of hand-rolling divergent codes. A `null`/absent chain is
124
+ * treated as "no chain required" and passes — callers that require a chain
125
+ * check presence separately.
126
+ */
127
+ function assertApproved(chain, message) {
128
+ if (chain != null && !isApproved(chain)) throw new ApprovalError("CHAIN_INCOMPLETE", message ?? `approval chain has not reached 'approved' (status='${chain.status}')`);
129
+ }
118
130
  function isRejected(chain) {
119
131
  return chain.status === "rejected";
120
132
  }
@@ -143,4 +155,4 @@ function computeChainStatus(steps) {
143
155
  return steps.every((s) => s.status === "approved" || s.status === "skipped") ? "approved" : "pending";
144
156
  }
145
157
  //#endregion
146
- export { ApprovalError, applyDecision, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
158
+ export { ApprovalError, applyDecision, assertApproved, createChain, decisionCount, isApproved, isPending, isRejected, nextPendingStep, pendingSteps, skipStep };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",