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