@openlup/core 0.21.0 → 0.23.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
@@ -4,366 +4,47 @@ Notable changes to this public-source workspace package are recorded here.
4
4
  “Unreleased” means no npm version carries the change yet; immutable OpenLup
5
5
  source previews may already ship some changes, and their release notes say
6
6
  which. Declaration snapshots track candidate drift, not a stable API or
7
- SemVer promise.
7
+ SemVer promise. This file keeps the current release and the two previous ones;
8
+ earlier releases are recorded verbatim in `CHANGELOG-HISTORY.md`
9
+ in the source repository.
8
10
 
9
11
  ## [Unreleased]
10
12
 
11
- ## [0.21.0]
12
-
13
- - Publishable on the npm `latest` dist-tag as `0.21.0`, from tag
14
- `openlup-core-v0.21.0`.
15
-
16
- - Republish unchanged APIs, runtime behavior and shipped SQL from `0.20.0` as part of the complete `0.21.0` package set.
17
-
18
- Migration: upgrade the complete set together; core introduces no API or schema migration in this release.
19
-
20
- ## [0.20.0]
21
-
22
- - Publishable on the npm `latest` dist-tag as `0.20.0`, from tag
23
- `openlup-core-v0.20.0`.
24
-
25
- - Republish unchanged APIs, runtime behavior and shipped SQL from `0.19.0` as part of the complete `0.20.0` package set.
26
-
27
- Migration: upgrade the complete set together; core introduces no API or schema migration in this release.
28
-
29
- ## [0.19.0]
30
-
31
- - Publishable on the npm `latest` dist-tag as `0.19.0`, from tag
32
- `openlup-core-v0.19.0`.
33
-
34
- - Republish unchanged APIs, runtime behavior and shipped SQL from `0.18.0` as part of the complete `0.19.0` package set.
35
-
36
- Migration: upgrade the complete set together; core introduces no API or schema migration in this release.
37
-
38
- ## [0.18.0]
39
-
40
- - Publishable on the npm `latest` dist-tag as `0.18.0`, from tag
41
- `openlup-core-v0.18.0`.
42
-
43
- - Republish unchanged APIs, runtime behavior and shipped SQL from `0.17.0` as
44
- part of the complete `0.18.0` package set.
45
-
46
- Migration: upgrade the complete set together; core introduces no API or schema
47
- migration in this release.
48
-
49
- ## [0.17.0]
50
-
51
- - Publishable on the npm `latest` dist-tag as `0.17.0`, from tag
52
- `openlup-core-v0.17.0`.
53
-
54
- - Republish unchanged APIs, runtime behavior and shipped SQL as part of the
55
- complete `0.17.0` package set.
56
-
57
- Baseline evidence (2026-10-20): published `0.16.0` tarball 161,232 packed bytes,
58
- SHA256 `48abf4ea8ddc446b8d6e2c610801e2cfcbe5575743bd5a61fbb8b8b50a2b36a3`.
59
- The initial `0.16.0` release-gates baseline is retained under S1 option B.
60
-
61
- ## [0.16.0]
62
-
63
- - Publishable on the npm `latest` dist-tag as `0.16.0`, from tag
64
- `openlup-core-v0.16.0`.
65
-
66
- - Add optional logical module selection, dependency/entry contracts, module-qualified bindings and module diagnostics to experimental `./readiness`. Each selected module retains an independent schema requirement within one artifact.
67
-
68
- Migration: additive API; existing manifests and loaded packages that omit module fields keep byte-identical readiness reports and binding keys. To opt an artifact in, observe `LoadedPackage.modules` and supply exactly one manifest `module` per selected id, with its logical kind, provided entries and in-artifact dependencies. Bind ports, triggers and routes as `package/module:id`; keep artifact kind/version and environment keys unchanged. Mixed modes, missing dependencies/entries and duplicate module/event/schedule/route ownership refuse using existing codes. Empty selections need no contributions; disabled modules need no bindings or schema probe. Inspect optional issue `module` when diagnosing module/schema failures. No release, publication or activation is performed by this change.
69
-
70
- ## [0.15.0]
13
+ ## [0.23.0]
71
14
 
72
- - Publishable on the npm `latest` dist-tag as `0.15.0`, from tag
73
- `openlup-core-v0.15.0`.
15
+ - Publishable on the npm `latest` dist-tag as `0.23.0`, from tag
16
+ `openlup-core-v0.23.0`.
74
17
 
75
- - Republished unchanged.
18
+ - Republish unchanged APIs, runtime behavior and shipped SQL from `0.22.0` as part of the complete `0.23.0` package set.
76
19
 
77
- ## [0.14.0]
20
+ Migration: upgrade the complete set together for exact internal peers; core introduces no API or schema migration.
78
21
 
79
- - Publishable on the npm `latest` dist-tag as `0.14.0`, from tag
80
- `openlup-core-v0.14.0`.
22
+ ### Known limitations
81
23
 
82
- - Add experimental `buildGroupedPostgresSchemaProbe`, `decodeGroupedPostgresSchemaRows` and `PostgresSchemaCatalogEntry` on `./readiness` for one ordered, parameterized PostgreSQL catalog read with table, column and exact function-overload shapes. The strict decoder refuses incomplete or malformed rows without exposing catalog bodies.
24
+ The fulfillment status map retains localized adopter labels; hosts still need injected labels or internationalization keys.
83
25
 
84
- Migration: additive API; existing scalar probes, observations, readiness codes and deadlines remain unchanged. Catalog rows use jsonb text, definition-string/null aggregates and raw pg_policy values; the decoder accepts text only. Hosts retain connection, snapshot, identity, digest and admission ownership. This preparation targets the 0.14.0 MINOR set; merging it does not publish packages.
26
+ ## [0.22.0]
85
27
 
86
- ## [0.13.1]
28
+ - Publishable on the npm `latest` dist-tag as `0.22.0`, from tag
29
+ `openlup-core-v0.22.0`.
87
30
 
88
- - Publishable on the npm `latest` dist-tag as `0.13.1`, from tag
89
- `openlup-core-v0.13.1`.
31
+ - Add pure fulfillment status, tracking-progress, current-parcel and canonical
32
+ fact-parser helpers to the existing `./fulfillment` subpath. Generic fact types
33
+ retain their names; narrowed fact aliases use the `Canonical` prefix. New
34
+ fulfillment re-exports carry the same `@beta` annotation as existing facts.
90
35
 
91
- - Republish the unchanged core API and implementation with the repaired outbox publication as the complete `0.13.1` package set.
36
+ Migration: existing platform fulfillment imports remain compatible re-exports;
37
+ no application import or schema migration is required.
92
38
 
93
- ## [0.13.0]
39
+ ### Known limitations
94
40
 
95
- - Publishable on the npm `latest` dist-tag as `0.13.0`, from tag
96
- `openlup-core-v0.13.0`.
41
+ - `FULFILLMENT_STATUS_MAP` carries localized adopter labels. Replace them with host-injected labels or internationalization keys before any stable-framework claim.
97
42
 
98
- - Add experimental outbox envelope/handler descriptors, structural validation, job lease and candidate readiness contracts.
99
- - Check package inventory, event ownership/coverage, actual bindings and qualified schema observations before admitting a new candidate.
100
-
101
- Migration: existing exports remain; compose the selected rail and bind its schema probe before activation. Unknown observations refuse the new candidate and do not stop a serving version.
102
-
103
-
104
- ### Changed
105
-
106
- - `AGENTS.md` gains a required "Using this package in an application" section: do not edit or
107
- patch the installed package, change behaviour through its seams, and upgrade every
108
- `@openlup/*` package together. `docs:check` refuses a guide without it.
109
-
110
- ## [0.12.0]
111
-
112
- ### Removed
113
-
114
- - **API REMOVAL: the `./checkout` subpath.** It held only an
115
- `Idempotency-Key` header-shape helper that no checkout path used.
116
-
117
- Migration: replace
118
-
119
- ```ts
120
- import { validateCheckoutIdempotencyHeader } from "@openlup/core/checkout";
121
- ```
122
-
123
- with an application-owned validator, or delete the call if, like the
124
- reference checkout, your checkout carries its idempotency key in the
125
- request body.
126
-
127
- ### Changed
128
-
129
- - **BEHAVIOUR CHANGE for the subscription status matrix.** `cancelled` is no
130
- longer terminal: `cancelled -> active` is now an allowed edge. This is the
131
- owner win-back that the managed SQL guard
132
- `public.subscription_guard_status_transition` already admits. Only the new
133
- `reactivateSubscription` takes that edge. `resumeSubscription` still leaves
134
- `paused` only and still refuses a cancelled subscription. `completed` stays
135
- terminal. A caller that treated `cancelled` as final must now expect a
136
- reactivated subscription to renew again.
137
- - **BEHAVIOUR CHANGE: an exhausted retry ladder now has a modelled outcome.** A
138
- renewal whose refusals ran past the last rung may pause an active
139
- subscription through `pauseSubscriptionForExpiredDunning`, which is the
140
- sanctioned non-payment rule. It is the only engine path that suspends a live
141
- subscription for a reason the owner did not choose. A ladder cut short by a
142
- terminating refusal class, or a failure that still has a retry after it, is
143
- refused as `dunning_not_exhausted` and leaves the subscription active.
144
- `recordPaymentFailure` is unchanged and still never touches the subscription.
145
- - `SubscriptionEngineErrorCode` gains `missing_payment_method` and
146
- `dunning_not_exhausted`. An exhaustive `switch` over the union must handle
147
- both.
148
-
149
- - `release-gates.json` sets `packageRelease.versionRule` to `0.N.P`. Below
150
- 1.0 every `@openlup/*` package carries one set version: a minor set for any
151
- API, behaviour or schema change, a patch set for a fix only.
152
-
153
- Migration: a tool that reads `versionRule` expects the set rule. Before:
154
- `"versionRule": "0.<n>.0"`. After: `"versionRule": "0.N.P"`.
155
-
156
- - Publishable on the npm `latest` dist-tag as `0.12.0`, from tag
157
- `openlup-core-v0.12.0`.
158
- - Publishable on the npm `preview` dist-tag as `0.11.0`, for source preview
159
- `openlup-source-preview/11`.
160
- - Publishable on the npm `preview` dist-tag as `0.10.0`, for source preview
161
- `openlup-source-preview/10`.
162
- - Publishable on the npm `preview` dist-tag as `0.9.0`, for source preview
163
- `openlup-source-preview/9`.
164
- - Publishable on the npm `preview` dist-tag as `0.7.0`, for source preview
165
- `openlup-source-preview/7`.
166
- - Publishable on the npm `preview` dist-tag as `0.6.0`, for source preview
167
- `openlup-source-preview/6`. Directory `npm publish` stays refused.
168
-
169
- - **BREAKING for `slugSchema`'s accepted language.** The `./catalog` schema no
170
- longer trims its input, so a slug with leading or trailing whitespace is now
171
- rejected rather than normalised and accepted, which is what the published
172
- `catalogSlugFormat` it tests against always described. A caller that relied on
173
- that normalisation must trim before validating.
174
- - **BREAKING for the retry ladder's semantics.** `nextRetryAttemptAt` now
175
- TERMINATES past the end of the ladder instead of capping at its last slot: an
176
- attempt beyond the configured backoff length answers `null`, which is the pause
177
- signal, rather than repeating the final interval forever. The stored schedule
178
- this kernel mirrors has always terminated, so the two rails now agree; the
179
- previous capping behaviour meant a caller asking for attempt 4 (or 9) was told
180
- a fourth charge was scheduled when none was. Callers that guarded with
181
- `attempt <= maxRetryAttempts()` before calling should drop the guard — asking
182
- for the next slot IS the decision, and a second copy of the budget check is how
183
- the two rails drifted apart in the first place.
184
-
185
- ### Added
186
-
187
- - `AGENTS.md` ships in the tarball. It states the package's kind and each
188
- subpath's maturity, gives one wiring example that compiles in CI, points to
189
- the shipped sources and declarations, and lists the readiness codes the
190
- package raises (none yet).
191
- - `release-gates.json` schema 5 declares the package kind (`"kind": "kernel"`)
192
- and splits the pack budget. Code files (dist JavaScript, declarations and
193
- shipped sources) and root documentation and metadata files each have their
194
- own caps on packed bytes and files; the tarball total stays as a backstop.
195
-
196
- Migration: a tool that reads `release-gates.json` checks `schemaVersion: 5`.
197
- Before: `"pack": { "maxPackedBytes": …, "maxFiles": … }`. After: `"pack": {
198
- "code": { "maxPackedBytes": …, "maxFiles": … }, "docs": { … },
199
- "maxPackedBytes": …, "maxFiles": … }`.
200
- - Add neutral checkout recovery evidence, method, operation and action contracts
201
- to the existing payment surface. They describe guidance only and do not change
202
- payment admission, renewal classification or enabled payment methods.
203
- - Export neutral lookup normalization from `./company-identity` for extensions.
204
-
205
- - `SUBSCRIPTION_STATUS_TRANSITIONS`, the frozen status matrix, and
206
- `canTransitionSubscriptionStatus` on `./subscription`. The matrix mirrors the
207
- managed SQL guard over the engine's four statuses, and a contract test
208
- enumerates every ordered pair.
209
- - `reactivateSubscription`, the win-back from `cancelled`. It needs a stored
210
- payment method and restarts the schedule `RESTART_LEAD_DAYS` (two) days out,
211
- never earlier than the stored `nextCycleAt`.
212
- - `isDunningLadderExhausted(cycle, cadence?)` on the retry ladder. It holds when
213
- a cycle is `payment_failed`, has nothing scheduled, and its attempt is past
214
- `maxRetryAttempts(cadence)`.
215
- - `pauseSubscriptionForExpiredDunning` and
216
- `resumeSubscriptionFromExpiredDunning`. The resume is the customer's recovery
217
- from the non-payment pause: it takes an optional replacement method, skips the
218
- uncollected cycle, and restarts the schedule like a win-back.
219
- - `recordPaymentFailure` takes an optional `cadence`, so a deployment ladder
220
- reaches the schedule and the exhaustion check alike. Callers that pass none
221
- keep the shipped ladder.
222
- - `recordPaymentFailure` accepts an optional neutral `failureClass` and passes it
223
- to the canonical retry-ladder decision. Existing callers remain
224
- source-compatible. The shipped terminating-class set is exactly
225
- `["hard_do_not_retry"]`: that refusal gets no later rung, while every other
226
- class keeps the existing 24h/72h/168h schedule.
227
- - Class-aware retry termination on `./subscription`.
228
- `CycleRetryCadence` gains an optional `terminatingFailureClasses`, the new
229
- `ladderTerminatedByClass(failureClass, cadence?)` answers whether the REASON
230
- for a refusal ends the ladder, and `nextRetryAttemptAt` / `shouldScheduleRetry`
231
- take an optional trailing `failureClass`. `DEFAULT_CYCLE_RETRY_CADENCE` ships
232
- only `hard_do_not_retry`. The predicate fails open for an absent or unlisted
233
- class, for a listed class the taxonomy does not recognise, and for a listed
234
- class that `failureClassDecision` still permits. Only explicit membership plus
235
- the taxonomy's existing refusal can end the ladder early.
236
- - Injectable retry cadence on `./subscription`: `CycleRetryCadence` and the
237
- frozen `DEFAULT_CYCLE_RETRY_CADENCE` (24h/72h/168h). `nextRetryAttemptAt`,
238
- `shouldScheduleRetry` and `maxRetryAttempts` each take an optional cadence and
239
- default to the shipped one, so a deployment can publish its own ladder without
240
- forking the kernel and nothing changes for a caller that passes none.
241
-
242
- - Pack budgets raised (packed 98 KiB -> 106 KiB, unpacked 448 KiB -> 480 KiB;
243
- file count unchanged at 186): the new allocation kernel ships as one source file
244
- plus its two build artifacts, and its adopter-facing JSDoc — the floor-clamp
245
- rule, the two-line split rationale and the failure-versus-throw split — is most
246
- of the added bytes. The docs are the deliverable, so the budget moves rather
247
- than the docs. Measured, not estimated: clean base packs 98532/446519/183 and
248
- this change packs 106488/475826/186.
249
- - Experimental bundle target-price allocation on `./pricing`:
250
- `allocateBundleTargetPrice`, the pure kernel that turns ONE operator-set price
251
- for a composed bundle into exact per-component money. Largest-remainder
252
- (Hamilton) split weighted by each component's own reference subtotal, all in
253
- BigInt, ties broken by component code so output never depends on input order.
254
- Two rules bound it — a component never allocates below its minimum payable per
255
- unit (and a component priced under that minimum is held to its own reference
256
- price, because no floor may lift a component above what it is worth), and never
257
- above its reference unit price. A quantity whose allocated subtotal does not
258
- divide evenly emits TWO lines rather than a rounded unit price, so the
259
- `unitPrice x quantity === lineSubtotal` identity every quote line refines still
260
- holds. The single asserted invariant is that the emitted lines sum to the target
261
- exactly. Three refusals stay data rather than exceptions: nothing to weigh
262
- against, a target above the sum of the parts, and a target that cannot cover
263
- every minimum payable. No clock, no randomness, no I/O.
264
- - Line-scoped `bundle` component in the pricing breakdown vocabulary, with an
265
- optional `bundle_allocated_discount_minor` on the line input: positive on the
266
- way in, emitted as one negative component, absent or zero emits nothing. It is
267
- line-scoped rather than order-scoped because a bundle prices a subset of the
268
- cart, and refunding one line must return exactly what that line was charged.
269
- - Pack budgets raised (packed 90 KiB -> 94 KiB, unpacked 416 KiB -> 432 KiB,
270
- files 180 -> 186): one new kernel module ships as a source file plus its build
271
- artifacts and their maps, and its adopter-facing JSDoc is most of the added
272
- bytes. The docs are the deliverable, so the budget moves rather than the docs.
273
- - Experimental payment-method lifecycle vocabulary on `./payment`: the five
274
- transitions a rail may report about a method it holds on file
275
- (`method_registered`, `method_updated`, `method_revoked`, `method_expired`,
276
- `method_suspended`), the neutral event an adapter translates its own event
277
- names into, and the three published facts (`schemeLabel`, `lastDigits`,
278
- `expiresAt`) a transition may carry — never the instrument number. Plus two
279
- pure helpers: `endsStoredMethodUsability`, the single place that answers
280
- whether a transition means the method can no longer back a charge (a rotation
281
- is not a death), and `expiryInstantFromMonthYear`, which pins the convention
282
- that a month-and-year validity runs to the LAST instant of that month in UTC.
283
- No rail is named and no clock, network or storage is read.
284
- - Pack budgets raised (packed 88 KiB -> 90 KiB, unpacked 400 KiB -> 416 KiB): the provider-capability contracts
285
- add ~0.9 KiB of shipped declarations and their adopter-facing JSDoc; the
286
- docs are the deliverable, so the budget moves rather than the docs.
287
- - (formatting) the block-reason alias is declared single-line so the
288
- generated declaration snapshot carries no trailing whitespace.
289
- - Experimental payment-provider capability contracts on `./payment`: a
290
- descriptor an adapter publishes so renewal and dunning can decide from
291
- CAPABILITIES — whether stored consent evidence must be read and whether it
292
- is chargeable unattended, which provider flow an unattended charge uses,
293
- what payer context the request needs, and what a stored method must satisfy
294
- before a charge — plus a registry contract keyed by provider kind. Types
295
- only, with no provider named and no runtime consumer yet; branching on
296
- provider identity is unchanged.
297
- - Experimental payment-failure taxonomy on `./payment`: a pure classifier that
298
- resolves a refusal into one of seven neutral classes from optional evidence
299
- (scheme retry advice, adopter-asserted neutral hints, an opaque decline code
300
- read through an adopter-supplied hint table, and the internal reason key),
301
- with a documented precedence order, a frozen per-class retry decision record,
302
- and a scheme retry ceiling helper pinned at 15 attempts per 30 days. The
303
- kernel embeds no acquirer code tables and has no runtime consumer yet; retry
304
- scheduling is unchanged.
305
- - Experimental provider-neutral payment-decline evidence on `./payment`, with
306
- a provider code (never free-form prose) and retryability hint that lets
307
- adopters classify synchronous PSP refusals without persisting customer data.
308
- Extended with optional `declineCode` and `adviceCode`, so a refusal keeps the
309
- finer bank reason and the provider's retry recommendation — both codes, never
310
- messages — for a later classification policy to read. Extended again with
311
- optional `neutralReasonHints`, so an adapter can carry its OWN reading of a
312
- refusal — already translated out of its code vocabulary — and a classifier
313
- never has to learn which adapter refused.
314
- - `PAYMENT_FAILURE_CLASSIFICATION_SOURCES` on `./payment`: the classification
315
- sources published as a value as well as a type, so a boundary schema can
316
- validate `decidedBy` against the kernel's own vocabulary instead of restating
317
- it. The type is unchanged and still derives from the same four members.
318
- - Additive `evaluatePromotionAdjustmentsV2` API in the existing candidate
319
- `./promo` subpath. It chooses one best product and shipping adjustment,
320
- interprets product percentages as target-effective basis points against a
321
- trusted reference subtotal, and requires an adopter-defined product floor.
322
- The legacy evaluator and DB-shaped types remain unchanged for compatibility;
323
- downstream applications can adopt the package boundary before any separately
324
- flagged runtime cutover.
325
- - Experimental `./fulfillment` normalized-fact contract with explicit schema,
326
- strict N/N-1 parsing, adopter-supplied canonical-status validation,
327
- discriminated provenance, application-minted evidence references, comparable
328
- provider ordinals, deterministic fingerprint/dedupe, and self-contained
329
- transition-conflict diagnostics with strict stream/decision validation.
330
- - The then-current package identity and extracted-root governance evidence;
331
- this is historical evidence, not authorization for a separate repository or
332
- release channel.
333
- - Package-local documentation and governance material for the portability
334
- boundary.
335
- - Candidate and experimental maturity metadata for the package surface.
336
-
337
- ### Changed
338
-
339
- - Marked candidate/experimental declarations `@beta` and testing-only
340
- declarations `@internal`, without changing package exports or runtime shapes.
341
- Removed the private `./testing` neutrality matcher after moving all packaged
342
- leak classes into the packed-artifact audit.
343
- - Reclassified all declaration snapshots as private package-surface drift proof;
344
- this changes no exports, runtime behavior, dependencies, or publish refusal.
345
- - Raised the reviewed packed/unpacked artifact ceilings by 4/16 KiB to admit
346
- the additive promo v2 declarations and implementation.
347
- - Replaced public-surface and freeze language with the private package-surface
348
- inventory: 16 current exports have package smoke evidence but no public
349
- stability claim.
350
-
351
- ### Removed
352
-
353
- - Removed the unused `./external-effects` API, implementation, snapshot, and
354
- self-conformance fixture before any public release. The application
355
- one-writer cleanup left no runtime consumer, so external-effect retry
356
- semantics stay adopter-owned instead of becoming a fourth core concern.
357
-
358
- ### Security
43
+ ## [0.21.0]
359
44
 
360
- - Recorded the private vulnerability-reporting contact for this proof package.
45
+ - Publishable on the npm `latest` dist-tag as `0.21.0`, from tag
46
+ `openlup-core-v0.21.0`.
361
47
 
362
- ## Release Status
48
+ - Republish unchanged APIs, runtime behavior and shipped SQL from `0.20.0` as part of the complete `0.21.0` package set.
363
49
 
364
- Versions up to `0.11.0` were published on the npm `preview` dist-tag without
365
- sections of their own, so the `0.12.0` section also lists the changes they
366
- carried. A release-preparation pull request turns the Unreleased section into
367
- the next version's section; its release tag and npm record when it is
368
- published. No version establishes a stable package API or supported upgrade
369
- channel.
50
+ Migration: upgrade the complete set together; core introduces no API or schema migration in this release.
package/README.md CHANGED
@@ -8,6 +8,11 @@ application" section, which `docs:check` requires, tells an application's agent
8
8
  not to edit or patch the installed package and to upgrade every `@openlup/*`
9
9
  package together.
10
10
 
11
+ The prepared `0.23.0` set republishes core unchanged from `0.22.0`.
12
+ Upgrade all packages together and read [the migration and label limitation](CHANGELOG.md#0230).
13
+ The size baseline is the published `0.22.0` artifact; older releases remain in
14
+ the source-only `CHANGELOG-HISTORY.md`. Preparation establishes no npm publication.
15
+
11
16
  ## Channel and stability
12
17
 
13
18
  - **Channel:** npm, on the `latest` dist-tag. Each version is published with
@@ -80,6 +85,8 @@ ceilings. Its aggregate budgets in `release-gates.json` follow the measured
80
85
  decision its `$budgetReason` records; later growth still needs its own
81
86
  measured budget decision. Passing a local pack does not authorize publication.
82
87
 
88
+ `./fulfillment` owns pure status, tracking-progress, current-parcel and canonical fact-parser helpers.
89
+
83
90
  ## Package Surface Maturity
84
91
 
85
92
  Every current export is an internal candidate, experimental kernel, or testing
@@ -91,7 +98,7 @@ promise.
91
98
  | `./bundle` | kernel | candidate | `smoke/subscriptionBundleStandalone.test.ts` |
92
99
  | `./catalog` | kernel | candidate | `smoke/catalogStandalone.test.ts` |
93
100
  | `./company-identity` | kernel | experimental | `smoke/companyIdentityStandalone.test.ts` |
94
- | `./fulfillment` | kernel | experimental | `smoke/fulfillmentStandalone.test.ts` |
101
+ | `./fulfillment` | kernel | experimental | `smoke/fulfillmentStandalone.test.ts`<br>`test/fulfillmentStatusMap.test.ts`<br>`test/fulfillmentTrackingProgress.test.ts`<br>`test/fulfillmentCurrentParcel.test.ts`<br>`test/fulfillmentNormalizedFacts.test.ts` |
95
102
  | `./inventory` | kernel | candidate | `smoke/inventoryStandalone.test.ts` |
96
103
  | `./marketing/research` | kernel | experimental | `smoke/marketingResearchStandalone.test.ts` |
97
104
  | `./partners` | kernel | experimental | `smoke/partnersStandalone.test.ts` |
@@ -0,0 +1,16 @@
1
+ export type FulfillmentParcelRow = {
2
+ id?: unknown;
3
+ order_id?: unknown;
4
+ sequence_no?: unknown;
5
+ };
6
+ export declare function parcelSequenceNo(row: FulfillmentParcelRow): number;
7
+ export declare function compareParcelPrecedence(left: FulfillmentParcelRow, right: FulfillmentParcelRow): number;
8
+ export declare function currentParcel<Row extends FulfillmentParcelRow>(rows: readonly Row[]): Row | null;
9
+ export declare function currentParcelFor<Row extends FulfillmentParcelRow>(rows: readonly Row[], orderId: unknown): Row | null;
10
+ export declare function currentParcelPerOrder<Row extends FulfillmentParcelRow>(rows: readonly Row[]): Map<string, Row>;
11
+ export declare function currentParcelFirst<Row extends FulfillmentParcelRow>(rows: readonly Row[]): Row[];
12
+ export type ParcelAttributedRow = {
13
+ fulfillment_order_id?: unknown;
14
+ };
15
+ export declare function belongsToParcel(row: ParcelAttributedRow, parcelId: unknown): boolean;
16
+ export declare function rowsForParcel<Row extends ParcelAttributedRow>(rows: readonly Row[], parcelId: unknown): Row[];
@@ -0,0 +1,70 @@
1
+ // An order may hold more than one fulfilment row: the original at `sequence_no` 0 and a
2
+ // replacement parcel at 1, 2, ... Several reads still have to answer with exactly one, and
3
+ // they must all answer the same way, or the operator, the customer and the support agent
4
+ // each see a different parcel for the same order.
5
+ //
6
+ // The rule, fixed once here: the parcel that *currently represents the order* is the highest
7
+ // `sequence_no`, tie-broken by `id` descending. While every order has exactly one row - which
8
+ // is every order until an operator can request a replacement - each function below returns
9
+ // that row, so the reads calling them are unchanged.
10
+ //
11
+ // This module is the single owner of that rule. It was four near-copies across two adapters
12
+ // and two read models before they were merged; keep it one.
13
+ // A row read without the column, or by a query that does not select it, is the original
14
+ // parcel - `sequence_no` carries a database default of 0 for exactly the same reason.
15
+ export function parcelSequenceNo(row) {
16
+ return typeof row.sequence_no === "number" ? row.sequence_no : 0;
17
+ }
18
+ // Negative when `left` is the more current parcel, so this sorts the current parcel first.
19
+ // Plain codepoint order on `id`, never `localeCompare`: this has to agree with SQL's
20
+ // `ORDER BY id DESC` over a uuid, and a collation that folds hyphens or case would not.
21
+ export function compareParcelPrecedence(left, right) {
22
+ const bySequence = parcelSequenceNo(right) - parcelSequenceNo(left);
23
+ if (bySequence !== 0)
24
+ return bySequence;
25
+ const leftId = String(left.id ?? "");
26
+ const rightId = String(right.id ?? "");
27
+ if (leftId === rightId)
28
+ return 0;
29
+ return leftId < rightId ? 1 : -1;
30
+ }
31
+ export function currentParcel(rows) {
32
+ return rows.reduce((best, row) => (best === null || compareParcelPrecedence(row, best) < 0 ? row : best), null);
33
+ }
34
+ export function currentParcelFor(rows, orderId) {
35
+ return currentParcel(rows.filter((row) => row.order_id === orderId));
36
+ }
37
+ export function currentParcelPerOrder(rows) {
38
+ const byOrder = new Map();
39
+ for (const row of rows) {
40
+ const orderId = String(row.order_id ?? "");
41
+ const incumbent = byOrder.get(orderId);
42
+ if (!incumbent || compareParcelPrecedence(row, incumbent) < 0)
43
+ byOrder.set(orderId, row);
44
+ }
45
+ return byOrder;
46
+ }
47
+ // For consumers that take `[0]` of an order's rows after filtering by `order_id`. Grouping
48
+ // preserves each order's first-appearance position, so a result that already holds one row
49
+ // per order comes back in exactly the order it arrived.
50
+ export function currentParcelFirst(rows) {
51
+ const byOrder = new Map();
52
+ for (const row of rows) {
53
+ const key = String(row.order_id ?? "");
54
+ const group = byOrder.get(key);
55
+ if (group)
56
+ group.push(row);
57
+ else
58
+ byOrder.set(key, [row]);
59
+ }
60
+ return [...byOrder.values()].flatMap((group) => (group.length > 1 ? [...group].sort(compareParcelPrecedence) : group));
61
+ }
62
+ export function belongsToParcel(row, parcelId) {
63
+ const attributed = row.fulfillment_order_id;
64
+ if (typeof attributed !== "string" || !attributed)
65
+ return true;
66
+ return typeof parcelId === "string" && parcelId ? attributed === parcelId : true;
67
+ }
68
+ export function rowsForParcel(rows, parcelId) {
69
+ return rows.filter((row) => belongsToParcel(row, parcelId));
70
+ }
@@ -2,3 +2,19 @@
2
2
  export { FULFILLMENT_FACT_SOURCE_AUTHORITIES, FULFILLMENT_FACT_SOURCES, PROVIDER_FULFILLMENT_FACT_SOURCES, NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION, NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION, TRANSITION_CONFLICT_REASONS, InvalidFulfillmentFactError, FulfillmentFactCollisionError, UnsupportedFulfillmentFactSchemaError, canonicalFulfillmentFactFingerprint, canonicalizeFulfillmentFacts, parseNormalizedFulfillmentFact, parseTransitionConflict, } from "./facts.js";
3
3
  /** @beta */
4
4
  export type { CompatibleNormalizedFulfillmentFact, FulfillmentFactSourceAuthority, FulfillmentFactSource, FulfillmentFactProvenance, FulfillmentRawEventReference, FulfillmentProviderSequence, FulfillmentFactParserOptions, ProviderFulfillmentFactSource, NormalizedFulfillmentFact, NormalizedFulfillmentFactV0, NormalizedFulfillmentFactV1, TransitionConflict, TransitionConflictDecision, FulfillmentTransitionSnapshot, TransitionConflictReason, } from "./facts.js";
5
+ /** @beta */
6
+ export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "./statusMap.js";
7
+ /** @beta */
8
+ export type { FulfillmentStatusStage, FulfillmentStatusMap } from "./statusMap.js";
9
+ /** @beta */
10
+ export { fulfillmentProgressPresentation } from "./trackingProgress.js";
11
+ /** @beta */
12
+ export { parcelSequenceNo, compareParcelPrecedence, currentParcel, currentParcelFor, currentParcelPerOrder, currentParcelFirst, belongsToParcel, rowsForParcel } from "./currentFulfillmentParcel.js";
13
+ /** @beta */
14
+ export type { FulfillmentParcelRow, ParcelAttributedRow } from "./currentFulfillmentParcel.js";
15
+ /** @beta */
16
+ export { isCanonicalLocalFulfillmentStatus, parseFulfillmentFact, parseFulfillmentTransitionConflict } from "./normalizedFulfillmentFacts.js";
17
+ /** @beta */
18
+ export type { CanonicalNormalizedFulfillmentFact, CanonicalNormalizedFulfillmentFactV1, CanonicalNormalizedFulfillmentFactV0, CanonicalCompatibleNormalizedFulfillmentFact, CanonicalFulfillmentTransitionSnapshot, CanonicalTransitionConflict } from "./normalizedFulfillmentFacts.js";
19
+ /** @beta */
20
+ export type { CanonicalLocalFulfillmentStatus, CustomerFulfillmentStep, FulfillmentProgressInput, FulfillmentProgressPresentation } from "./types.js";
@@ -1,2 +1,10 @@
1
1
  /** @beta */
2
2
  export { FULFILLMENT_FACT_SOURCE_AUTHORITIES, FULFILLMENT_FACT_SOURCES, PROVIDER_FULFILLMENT_FACT_SOURCES, NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION, NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION, TRANSITION_CONFLICT_REASONS, InvalidFulfillmentFactError, FulfillmentFactCollisionError, UnsupportedFulfillmentFactSchemaError, canonicalFulfillmentFactFingerprint, canonicalizeFulfillmentFacts, parseNormalizedFulfillmentFact, parseTransitionConflict, } from "./facts.js";
3
+ /** @beta */
4
+ export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "./statusMap.js";
5
+ /** @beta */
6
+ export { fulfillmentProgressPresentation } from "./trackingProgress.js";
7
+ /** @beta */
8
+ export { parcelSequenceNo, compareParcelPrecedence, currentParcel, currentParcelFor, currentParcelPerOrder, currentParcelFirst, belongsToParcel, rowsForParcel } from "./currentFulfillmentParcel.js";
9
+ /** @beta */
10
+ export { isCanonicalLocalFulfillmentStatus, parseFulfillmentFact, parseFulfillmentTransitionConflict } from "./normalizedFulfillmentFacts.js";
@@ -0,0 +1,14 @@
1
+ import { type CompatibleNormalizedFulfillmentFact as CoreCompatibleNormalizedFulfillmentFact, type FulfillmentTransitionSnapshot as CoreFulfillmentTransitionSnapshot, type NormalizedFulfillmentFact as CoreNormalizedFulfillmentFact, type NormalizedFulfillmentFactV0 as CoreNormalizedFulfillmentFactV0, type NormalizedFulfillmentFactV1 as CoreNormalizedFulfillmentFactV1, type TransitionConflict as CoreTransitionConflict } from "./facts.js";
2
+ import type { CanonicalLocalFulfillmentStatus } from "./types.js";
3
+ /** Application binding: core evidence narrowed to the single Layer B status canon. */
4
+ export type CanonicalNormalizedFulfillmentFact = CoreNormalizedFulfillmentFact<CanonicalLocalFulfillmentStatus>;
5
+ export type CanonicalNormalizedFulfillmentFactV1 = CoreNormalizedFulfillmentFactV1<CanonicalLocalFulfillmentStatus>;
6
+ export type CanonicalNormalizedFulfillmentFactV0 = CoreNormalizedFulfillmentFactV0<CanonicalLocalFulfillmentStatus>;
7
+ export type CanonicalCompatibleNormalizedFulfillmentFact = CoreCompatibleNormalizedFulfillmentFact<CanonicalLocalFulfillmentStatus>;
8
+ export type CanonicalFulfillmentTransitionSnapshot = CoreFulfillmentTransitionSnapshot<CanonicalLocalFulfillmentStatus>;
9
+ export type CanonicalTransitionConflict = CoreTransitionConflict<CanonicalLocalFulfillmentStatus>;
10
+ export declare function isCanonicalLocalFulfillmentStatus(value: string): value is CanonicalLocalFulfillmentStatus;
11
+ /** Strict unknown-input parser bound to the existing status canon. */
12
+ export declare function parseFulfillmentFact(value: unknown): CanonicalNormalizedFulfillmentFact;
13
+ /** Strict conflict parser bound to the same status canon. */
14
+ export declare function parseFulfillmentTransitionConflict(value: unknown): CanonicalTransitionConflict;
@@ -0,0 +1,18 @@
1
+ import { parseNormalizedFulfillmentFact, parseTransitionConflict, } from "./facts.js";
2
+ import { CANONICAL_LOCAL_FULFILLMENT_STATUSES, } from "./statusMap.js";
3
+ const CANONICAL_STATUSES = new Set(CANONICAL_LOCAL_FULFILLMENT_STATUSES);
4
+ export function isCanonicalLocalFulfillmentStatus(value) {
5
+ return CANONICAL_STATUSES.has(value);
6
+ }
7
+ /** Strict unknown-input parser bound to the existing status canon. */
8
+ export function parseFulfillmentFact(value) {
9
+ return parseNormalizedFulfillmentFact(value, {
10
+ isCanonicalStatus: isCanonicalLocalFulfillmentStatus,
11
+ });
12
+ }
13
+ /** Strict conflict parser bound to the same status canon. */
14
+ export function parseFulfillmentTransitionConflict(value) {
15
+ return parseTransitionConflict(value, {
16
+ isCanonicalStatus: isCanonicalLocalFulfillmentStatus,
17
+ });
18
+ }