@classytic/primitives 0.25.0 → 0.26.1

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,27 @@
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.26.1
7
+
8
+ - `./testing/stock-key-seam` — new subpath; `scanStockKeySeam`, `assertStockKeySeam`, `takesStockKeyFromLabel`, `STOCK_KEY_SEAM_FIXTURES` for conformance-scanning packages that resolve stock keys
9
+
10
+ ## [0.26.0] — 2026-08-27
11
+
12
+ ### Added
13
+
14
+ - **`./deadline` subpath** — `Deadline` value type + helpers for date-bounded
15
+ obligations (reservations, SLA windows, contract terms).
16
+ - **`./stock-level` subpath** — canonical stock-level representation and helpers.
17
+ - **`./item-facts` subpath** — cross-kernel item fact contract (weight, dimensions,
18
+ hazmat flags) that catalog declares and logistics/inventory reads.
19
+ - **`StockKey` branded type** (`./item-identity`) — prevents a bare merchandising
20
+ label or ObjectId being passed where a quant key is expected; the only source is
21
+ `stockKeyOf()` / `requireStockKey()`. Return types of both functions updated.
22
+ - **Upfront collection policy** (`./monetization`) — `UpfrontCollectionPolicy`
23
+ (`'full_payment' | 'deposit' | 'deferred'`) + `UPFRONT_COLLECTION_POLICIES`
24
+ const. Cross-kernel: catalog declares the policy, order turns it into tenders,
25
+ revenue settles them.
26
+
6
27
  ## [0.25.0] - 2026-08-24
7
28
 
8
29
  ### Added
@@ -0,0 +1,16 @@
1
+ //#region src/composition/deadline.d.ts
2
+ /**
3
+ * Race a promise against a deadline without leaking the loser.
4
+ *
5
+ * A call that can hang — no throw, no response, the underlying layer just
6
+ * never settles — must not block its caller forever. Nothing here cancels
7
+ * the original promise; it keeps running after the deadline, so its eventual
8
+ * settlement is swallowed rather than surfaced a second time to a caller
9
+ * that already moved on.
10
+ */
11
+ declare class DeadlineExceededError extends Error {
12
+ constructor(label: string, timeoutMs: number);
13
+ }
14
+ declare function withDeadline<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T>;
15
+ //#endregion
16
+ export { DeadlineExceededError, withDeadline };
@@ -0,0 +1,31 @@
1
+ //#region src/composition/deadline.ts
2
+ /**
3
+ * Race a promise against a deadline without leaking the loser.
4
+ *
5
+ * A call that can hang — no throw, no response, the underlying layer just
6
+ * never settles — must not block its caller forever. Nothing here cancels
7
+ * the original promise; it keeps running after the deadline, so its eventual
8
+ * settlement is swallowed rather than surfaced a second time to a caller
9
+ * that already moved on.
10
+ */
11
+ var DeadlineExceededError = class extends Error {
12
+ constructor(label, timeoutMs) {
13
+ super(`${label} timed out after ${timeoutMs}ms`);
14
+ this.name = "DeadlineExceededError";
15
+ }
16
+ };
17
+ function withDeadline(promise, timeoutMs, label) {
18
+ return new Promise((resolve, reject) => {
19
+ const timer = setTimeout(() => reject(new DeadlineExceededError(label, timeoutMs)), timeoutMs);
20
+ timer.unref?.();
21
+ promise.then((value) => {
22
+ clearTimeout(timer);
23
+ resolve(value);
24
+ }, (err) => {
25
+ clearTimeout(timer);
26
+ reject(err);
27
+ });
28
+ });
29
+ }
30
+ //#endregion
31
+ export { DeadlineExceededError, withDeadline };
@@ -0,0 +1,73 @@
1
+ import { ShelfLifePolicy } from "./shelf-life.mjs";
2
+ //#region src/inventory/item-facts.d.ts
3
+ /** Coarse lot/serial tracking. `trackingPolicy.mode` wins when both are set. */
4
+ type TrackingMode = "none" | "lot" | "serial";
5
+ /** The stock facts an Item contributes. Persistence and classification are the owning package's. */
6
+ interface ItemStockSource {
7
+ skuRef: string;
8
+ sku: string;
9
+ displayName: string;
10
+ /** The canonical STOCK unit — what quants and moves are balanced in. */
11
+ uom: string;
12
+ trackingMode: TrackingMode;
13
+ trackingPolicy?: ShelfLifePolicy | undefined;
14
+ /**
15
+ * Variable-weight item (deli, meat, loose produce): a measured weight is
16
+ * captured per received lot while the piece count stays the stock dimension.
17
+ */
18
+ catchWeight?: boolean | undefined;
19
+ weightUom?: string | undefined;
20
+ weight?: number | undefined;
21
+ volume?: number | undefined;
22
+ barcode?: string[] | undefined;
23
+ /** Retired items are not stockable. Independent of whether a Product sells. */
24
+ isActive: boolean;
25
+ }
26
+ /** The merchandising side. Every stock fact is optional — the Item owns those. */
27
+ interface ProductFacet {
28
+ skuRef: string;
29
+ sku: string;
30
+ displayName: string;
31
+ uom?: string | undefined;
32
+ trackingMode?: TrackingMode | undefined;
33
+ trackingPolicy?: ShelfLifePolicy | undefined;
34
+ catchWeight?: boolean | undefined;
35
+ weightUom?: string | undefined;
36
+ weight?: number | undefined;
37
+ volume?: number | undefined;
38
+ barcode?: string[] | undefined;
39
+ isActive?: boolean | undefined;
40
+ }
41
+ /** The resolved answer. `@classytic/flow`'s `SkuDetails` IS this type. */
42
+ interface ItemStockFacts {
43
+ skuRef: string;
44
+ sku: string;
45
+ displayName: string;
46
+ trackingMode: TrackingMode;
47
+ trackingPolicy?: ShelfLifePolicy;
48
+ uom: string;
49
+ catchWeight?: boolean;
50
+ weightUom?: string;
51
+ weight?: number;
52
+ volume?: number;
53
+ barcode?: string[];
54
+ isActive: boolean;
55
+ }
56
+ /**
57
+ * Merge an Item with its Product facet.
58
+ *
59
+ * - **Neither** ⇒ `null`. An unknown SKU is ordinary (a deleted line), not an error.
60
+ * - **Product only** ⇒ the product's own facts — every deployment before an Item exists.
61
+ * - **Item only** ⇒ the item's facts. A raw material nobody sells.
62
+ * - **Both** ⇒ stock facts from the Item, name and label from the Product.
63
+ *
64
+ * `isActive` follows the ITEM when one exists: a product withdrawn from sale is
65
+ * still stock on a shelf, whereas a retired item is not stockable at all. Taking
66
+ * the product's flag would make unlisting a product silently orphan its stock.
67
+ */
68
+ declare function resolveStockFacts(input: {
69
+ item?: ItemStockSource | null | undefined;
70
+ product?: ProductFacet | null | undefined;
71
+ }): ItemStockFacts | null;
72
+ //#endregion
73
+ export { ItemStockFacts, ItemStockSource, ProductFacet, TrackingMode, resolveStockFacts };
@@ -0,0 +1,53 @@
1
+ //#region src/inventory/item-facts.ts
2
+ /** Unit assumed when neither side declares one. A deployment-wide guess, and a bad one — set a real `uom` on the Item. */
3
+ const FALLBACK_UOM = "unit";
4
+ const defined = (key, value) => value === void 0 ? {} : { [key]: value };
5
+ /**
6
+ * Merge an Item with its Product facet.
7
+ *
8
+ * - **Neither** ⇒ `null`. An unknown SKU is ordinary (a deleted line), not an error.
9
+ * - **Product only** ⇒ the product's own facts — every deployment before an Item exists.
10
+ * - **Item only** ⇒ the item's facts. A raw material nobody sells.
11
+ * - **Both** ⇒ stock facts from the Item, name and label from the Product.
12
+ *
13
+ * `isActive` follows the ITEM when one exists: a product withdrawn from sale is
14
+ * still stock on a shelf, whereas a retired item is not stockable at all. Taking
15
+ * the product's flag would make unlisting a product silently orphan its stock.
16
+ */
17
+ function resolveStockFacts(input) {
18
+ const { item, product } = input;
19
+ if (!item && !product) return null;
20
+ if (!item) {
21
+ const p = product;
22
+ return {
23
+ skuRef: p.skuRef,
24
+ sku: p.sku,
25
+ displayName: p.displayName,
26
+ uom: p.uom ?? FALLBACK_UOM,
27
+ trackingMode: p.trackingMode ?? "none",
28
+ ...defined("trackingPolicy", p.trackingPolicy),
29
+ ...defined("catchWeight", p.catchWeight),
30
+ ...defined("weightUom", p.weightUom),
31
+ ...defined("weight", p.weight),
32
+ ...defined("volume", p.volume),
33
+ ...defined("barcode", p.barcode),
34
+ isActive: p.isActive ?? true
35
+ };
36
+ }
37
+ return {
38
+ skuRef: item.skuRef,
39
+ sku: product?.sku ?? item.sku,
40
+ displayName: product?.displayName ?? item.displayName,
41
+ uom: item.uom,
42
+ trackingMode: item.trackingMode,
43
+ ...defined("trackingPolicy", item.trackingPolicy),
44
+ ...defined("catchWeight", item.catchWeight),
45
+ ...defined("weightUom", item.weightUom),
46
+ ...defined("weight", item.weight),
47
+ ...defined("volume", item.volume),
48
+ ...defined("barcode", item.barcode),
49
+ isActive: item.isActive
50
+ };
51
+ }
52
+ //#endregion
53
+ export { resolveStockFacts };
@@ -1,24 +1,14 @@
1
+ import { t as Brand } from "./brand-Lj_75Y_r.mjs";
1
2
  //#region src/identity/item-identity.d.ts
2
3
  /**
3
- * `sku` is a LABEL; `skuRef` is the stock IDENTITY. Never one field.
4
+ * A resolved STOCK key — the string quants, moves and reservations are keyed by.
4
5
  *
5
- * | field | mutable? | job |
6
- * |---|---|---|
7
- * | `skuRef` | never | keys quants, moves and ledger rows |
8
- * | `sku` | operator edits it | what a human reads, types, searches |
9
- *
10
- * Use `stockKeyOf` for anything that reserves or moves stock, `skuDisplay` for
11
- * anything a human reads. Never hand-roll either: both fall back, in OPPOSITE
12
- * directions, and getting one backwards type-checks and returns a plausible
13
- * string.
14
- *
15
- * - stock key = `skuRef ?? sku` — absent `skuRef` means a pre-split producer
16
- * whose `sku` still IS the key.
17
- * - display = `skuLabel ?? sku ?? skuRef` — the ref last, and flagged, because
18
- * it can be an opaque id with no merchandising meaning.
19
- *
20
- * Pinned by `tests/unit/item-identity.test.ts`.
6
+ * Branded so a port that moves stock cannot accept a bare string: the only way
7
+ * to obtain one is {@link stockKeyOf} / {@link requireStockKey}, which apply the
8
+ * `skuRef ?? sku` rule. A merchandising label passed where a stock key belongs
9
+ * finds no quant and reports "out of stock" against a full shelf.
21
10
  */
11
+ type StockKey = Brand<string, "StockKey">;
22
12
  /** Identifier fields a record may carry — each shape populates a different subset. */
23
13
  interface SkuFields {
24
14
  /** Merchandising label — mutable, human-facing. */
@@ -40,12 +30,12 @@ interface SkuFields {
40
30
  */
41
31
  declare function stockKeyOf(item: SkuFields, fallback?: string | {
42
32
  toString(): string;
43
- }): string | undefined;
33
+ }): StockKey | undefined;
44
34
  /**
45
35
  * {@link stockKeyOf} that THROWS. Use wherever the key feeds a stock movement:
46
36
  * a reservation on `undefined` returns "out of stock" instead of failing.
47
37
  */
48
- declare function requireStockKey(item: SkuFields, context?: string): string;
38
+ declare function requireStockKey(item: SkuFields, context?: string): StockKey;
49
39
  /** What a surface should render for an item, and whether it had a real label. */
50
40
  interface SkuDisplay {
51
41
  /** Best human-facing value. `undefined` = no identifier at all; render a dash. */
@@ -68,4 +58,4 @@ declare function skuDisplay(item: SkuFields): SkuDisplay;
68
58
  */
69
59
  declare function looksLikeOpaqueId(value: string | undefined): boolean;
70
60
  //#endregion
71
- export { SkuDisplay, SkuFields, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
61
+ export { SkuDisplay, SkuFields, StockKey, looksLikeOpaqueId, requireStockKey, skuDisplay, stockKeyOf };
@@ -22,6 +22,45 @@ declare const MONETIZATION_KINDS: readonly ["free", "one_time", "subscription",
22
22
  type MonetizationKind = (typeof MONETIZATION_KINDS)[number];
23
23
  /** True when `value` is one of the canonical kinds. */
24
24
  declare function isMonetizationKind(value: unknown): value is MonetizationKind;
25
+ /**
26
+ * HOW MUCH of a price is collected at the point of sale.
27
+ *
28
+ * Distinct from every neighbouring vocabulary, which is why it is its own:
29
+ *
30
+ * - order's `PAYMENT_TERMS` (`prepaid` | `collect_on_delivery`) says WHEN.
31
+ * - `PaymentAllocationStatus` (`partial`, `allocated`…) says how much of a
32
+ * RECEIVED payment has been applied to what it settles.
33
+ * - revenue's `PARTIALLY_REFUNDED` is a transaction's own state.
34
+ *
35
+ * None of them answers "what do we charge this customer right now", and a host
36
+ * that guesses charges either nothing or everything — opposite answers to the
37
+ * same question.
38
+ *
39
+ * It lives here, beside {@link MONETIZATION_KINDS}, for the reason stated in
40
+ * catalog's barrel: monetization is a CROSS-KERNEL contract. Catalog declares
41
+ * the policy on a product, order turns it into tenders, revenue settles them.
42
+ * Three packages reading one definition, none re-exporting it.
43
+ *
44
+ * - `full_payment` — the whole price, before the sale completes.
45
+ * - `deposit` — a fixed amount now, the balance outstanding on the
46
+ * order and settled later by another tender. One provider
47
+ * line instead of two, which matters when card fees are
48
+ * charged per transaction.
49
+ * - `pay_later` — nothing now. The order still records what is owed.
50
+ *
51
+ * ABSENCE means `full_payment` — the behaviour before this existed — so no
52
+ * stored record changes meaning when a build that knows this field ships.
53
+ */
54
+ declare const UPFRONT_COLLECTION_MODES: readonly ["full_payment", "deposit", "pay_later"];
55
+ type UpfrontCollectionMode = (typeof UPFRONT_COLLECTION_MODES)[number];
56
+ /**
57
+ * Narrow an unknown value to a collection mode.
58
+ *
59
+ * Readers MUST treat `false` as UNKNOWN, never as `full_payment`: a record
60
+ * written by a build that knows a mode this one does not has not said "charge
61
+ * everything", and assuming so takes money nobody agreed to.
62
+ */
63
+ declare function isUpfrontCollectionMode(value: unknown): value is UpfrontCollectionMode;
25
64
  /** A quantity-based price break. `price` (fixed override) XOR `discountPercent`. */
26
65
  interface PriceTier {
27
66
  /** Minimum quantity to qualify. */
@@ -191,4 +230,4 @@ declare function toRevenueMonetizationType(kind: MonetizationKind): RevenueMonet
191
230
  /** Revenue wire type → canonical kind (`purchase` → `one_time`). */
192
231
  declare function fromRevenueMonetizationType(type: RevenueMonetizationType): MonetizationKind;
193
232
  //#endregion
194
- export { BundleMonetization, DurationUnit, FreeMonetization, MONETIZATION_KINDS, Monetization, MonetizationKind, MoneyByCurrency, OneTimeMonetization, OneTimePricing, PriceTier, RevenueMonetizationType, SubscriptionMonetization, SubscriptionPlan, UsageMonetization, UsageRating, UsageTier, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
233
+ export { BundleMonetization, DurationUnit, FreeMonetization, MONETIZATION_KINDS, Monetization, MonetizationKind, MoneyByCurrency, OneTimeMonetization, OneTimePricing, PriceTier, RevenueMonetizationType, SubscriptionMonetization, SubscriptionPlan, UPFRONT_COLLECTION_MODES, UpfrontCollectionMode, UsageMonetization, UsageRating, UsageTier, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUpfrontCollectionMode, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
@@ -21,6 +21,51 @@ const MONETIZATION_KIND_SET = new Set(MONETIZATION_KINDS);
21
21
  function isMonetizationKind(value) {
22
22
  return typeof value === "string" && MONETIZATION_KIND_SET.has(value);
23
23
  }
24
+ /**
25
+ * HOW MUCH of a price is collected at the point of sale.
26
+ *
27
+ * Distinct from every neighbouring vocabulary, which is why it is its own:
28
+ *
29
+ * - order's `PAYMENT_TERMS` (`prepaid` | `collect_on_delivery`) says WHEN.
30
+ * - `PaymentAllocationStatus` (`partial`, `allocated`…) says how much of a
31
+ * RECEIVED payment has been applied to what it settles.
32
+ * - revenue's `PARTIALLY_REFUNDED` is a transaction's own state.
33
+ *
34
+ * None of them answers "what do we charge this customer right now", and a host
35
+ * that guesses charges either nothing or everything — opposite answers to the
36
+ * same question.
37
+ *
38
+ * It lives here, beside {@link MONETIZATION_KINDS}, for the reason stated in
39
+ * catalog's barrel: monetization is a CROSS-KERNEL contract. Catalog declares
40
+ * the policy on a product, order turns it into tenders, revenue settles them.
41
+ * Three packages reading one definition, none re-exporting it.
42
+ *
43
+ * - `full_payment` — the whole price, before the sale completes.
44
+ * - `deposit` — a fixed amount now, the balance outstanding on the
45
+ * order and settled later by another tender. One provider
46
+ * line instead of two, which matters when card fees are
47
+ * charged per transaction.
48
+ * - `pay_later` — nothing now. The order still records what is owed.
49
+ *
50
+ * ABSENCE means `full_payment` — the behaviour before this existed — so no
51
+ * stored record changes meaning when a build that knows this field ships.
52
+ */
53
+ const UPFRONT_COLLECTION_MODES = [
54
+ "full_payment",
55
+ "deposit",
56
+ "pay_later"
57
+ ];
58
+ const UPFRONT_COLLECTION_MODE_SET = new Set(UPFRONT_COLLECTION_MODES);
59
+ /**
60
+ * Narrow an unknown value to a collection mode.
61
+ *
62
+ * Readers MUST treat `false` as UNKNOWN, never as `full_payment`: a record
63
+ * written by a build that knows a mode this one does not has not said "charge
64
+ * everything", and assuming so takes money nobody agreed to.
65
+ */
66
+ function isUpfrontCollectionMode(value) {
67
+ return typeof value === "string" && UPFRONT_COLLECTION_MODE_SET.has(value);
68
+ }
24
69
  const isFreeMonetization = (m) => m.type === "free";
25
70
  const isOneTimeMonetization = (m) => m.type === "one_time";
26
71
  const isSubscriptionMonetization = (m) => m.type === "subscription";
@@ -76,4 +121,4 @@ function fromRevenueMonetizationType(type) {
76
121
  return "one_time";
77
122
  }
78
123
  //#endregion
79
- export { MONETIZATION_KINDS, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
124
+ export { MONETIZATION_KINDS, UPFRONT_COLLECTION_MODES, compareAtPriceOf, fromRevenueMonetizationType, isBundleMonetization, isFreeMonetization, isMonetizationKind, isOneTimeMonetization, isSubscriptionMonetization, isUpfrontCollectionMode, isUsageMonetization, monetizationKindOf, toRevenueMonetizationType, unitPriceOf };
@@ -0,0 +1,59 @@
1
+ import { t as Brand } from "./brand-Lj_75Y_r.mjs";
2
+ //#region src/inventory/stock-level.d.ts
3
+ /**
4
+ * Stock quantities as NOMINAL types, so "how many are there" and "how many may
5
+ * I still promise" cannot be passed for one another.
6
+ *
7
+ * They are different questions with the same JavaScript type, and every layer
8
+ * re-derived the relationship between them: the flow ledger, the catalog
9
+ * bridge, the quant invariants, and the storefront cache each knew that
10
+ * available is on-hand minus reserved, and one of them still shipped on-hand
11
+ * into a field named `totalAvailable` — so the shop advertised units already
12
+ * held in someone else's cart and checkout refused the sale it had promised.
13
+ *
14
+ * Same fix as money's `MinorUnits<C>` brand, and for the same reason: a unit
15
+ * confusion that a reviewer must notice becomes one the compiler refuses.
16
+ * Zero runtime cost — the brands erase at compile time.
17
+ */
18
+ /** Units physically present. INCLUDES units promised to open carts. */
19
+ type OnHand = Brand<number, "OnHand">;
20
+ /** Units held against an owner (cart, order, move group) — present, not free. */
21
+ type Reserved = Brand<number, "Reserved">;
22
+ /**
23
+ * Units that may still be promised to a NEW buyer: `onHand - reserved`.
24
+ *
25
+ * Constructible only via {@link sellable}, which is what makes passing on-hand
26
+ * where sellable is required a type error rather than an oversell.
27
+ */
28
+ type Sellable = Brand<number, "Sellable">;
29
+ /** What a location holds, as the ledger records it. */
30
+ interface StockLevel {
31
+ readonly onHand: OnHand;
32
+ readonly reserved: Reserved;
33
+ }
34
+ declare const onHand: (n: number) => OnHand;
35
+ declare const reserved: (n: number) => Reserved;
36
+ /**
37
+ * The ONE definition of sellable stock.
38
+ *
39
+ * Deliberately NOT clamped at zero. Writing stock off below what is already
40
+ * reserved (goods damaged after being promised) is a real state, not an error —
41
+ * you cannot un-promise a cart. A negative reads as OVERSOLD, and every
42
+ * consumer already treats `<= 0` as unsellable; clamping would erase the only
43
+ * signal operations has that the condition exists.
44
+ */
45
+ declare const sellable: (level: StockLevel) => Sellable;
46
+ /** `true` when more is promised than is physically present. */
47
+ declare const isOversold: (level: StockLevel) => boolean;
48
+ /**
49
+ * Build a level from a ledger row, whatever it calls its fields.
50
+ *
51
+ * A quant reports all three; `available` is IGNORED on purpose and recomputed,
52
+ * so a source whose stored total has drifted cannot import that drift.
53
+ */
54
+ declare const levelOf: (row: {
55
+ quantityOnHand?: number;
56
+ quantityReserved?: number;
57
+ }) => StockLevel;
58
+ //#endregion
59
+ export { OnHand, Reserved, Sellable, StockLevel, isOversold, levelOf, onHand, reserved, sellable };
@@ -0,0 +1,27 @@
1
+ //#region src/inventory/stock-level.ts
2
+ const onHand = (n) => n;
3
+ const reserved = (n) => n;
4
+ /**
5
+ * The ONE definition of sellable stock.
6
+ *
7
+ * Deliberately NOT clamped at zero. Writing stock off below what is already
8
+ * reserved (goods damaged after being promised) is a real state, not an error —
9
+ * you cannot un-promise a cart. A negative reads as OVERSOLD, and every
10
+ * consumer already treats `<= 0` as unsellable; clamping would erase the only
11
+ * signal operations has that the condition exists.
12
+ */
13
+ const sellable = (level) => level.onHand - level.reserved;
14
+ /** `true` when more is promised than is physically present. */
15
+ const isOversold = (level) => sellable(level) < 0;
16
+ /**
17
+ * Build a level from a ledger row, whatever it calls its fields.
18
+ *
19
+ * A quant reports all three; `available` is IGNORED on purpose and recomputed,
20
+ * so a source whose stored total has drifted cannot import that drift.
21
+ */
22
+ const levelOf = (row) => ({
23
+ onHand: onHand(row.quantityOnHand ?? 0),
24
+ reserved: reserved(row.quantityReserved ?? 0)
25
+ });
26
+ //#endregion
27
+ export { isOversold, levelOf, onHand, reserved, sellable };
@@ -0,0 +1,47 @@
1
+ //#region src/testing/stock-key-seam.d.ts
2
+ interface StockKeyViolation {
3
+ /** Path relative to the scanned root. */
4
+ file: string;
5
+ /** 1-indexed line number. */
6
+ line: number;
7
+ /** The offending source line, trimmed. */
8
+ text: string;
9
+ }
10
+ /**
11
+ * Does this line take a stock key from a label?
12
+ *
13
+ * The class is "assigns `skuRef` from an expression that reads `.sku` and never
14
+ * reads `.skuRef`". Matching `.sku` alone flags the CORRECT idiom
15
+ * (`line.skuRef ?? line.sku`), and a gate that cries wolf is a gate someone
16
+ * deletes — so the ref-read is what distinguishes a fallback from a label grab.
17
+ */
18
+ declare function takesStockKeyFromLabel(line: string): boolean;
19
+ /** @deprecated Kept for callers that want the raw predicate; prefer {@link takesStockKeyFromLabel}. */
20
+ declare const STOCK_KEY_FROM_LABEL: {
21
+ test: typeof takesStockKeyFromLabel;
22
+ };
23
+ interface StockKeySeamScan {
24
+ /** How many source files were read — assert a floor, or the gate can pass on an empty tree. */
25
+ scanned: number;
26
+ violations: StockKeyViolation[];
27
+ }
28
+ /** Scan a package's `src` root. Pure: returns findings, throws nothing. */
29
+ declare function scanStockKeySeam(srcRoot: string): StockKeySeamScan;
30
+ /**
31
+ * Throwing form for a one-line test. `minFiles` is the scan-volume floor: a gate
32
+ * that quietly scanned nothing passes forever, so state what "ran" means.
33
+ */
34
+ declare function assertStockKeySeam(srcRoot: string, opts: {
35
+ minFiles: number;
36
+ }): void;
37
+ /**
38
+ * The fixtures the matcher must and must not fire on — exported so every
39
+ * adopting package falsifies the DETECTOR, not just its own current cleanliness.
40
+ * Both live defects are here verbatim.
41
+ */
42
+ declare const STOCK_KEY_SEAM_FIXTURES: {
43
+ readonly mustMatch: readonly [" skuRef: (snapshot.sku as string | undefined) ?? '',", " skuRef: snap?.sku ?? '',", " const skuRef = line.snapshot?.sku ?? line.offerId;", " skuRef: (line.snapshot?.sku ?? line.offerId) as string,"];
44
+ readonly mustNotMatch: readonly [" skuRef: stockKeyOf(snapshot) ?? '',", " skuRef: line.skuRef,", " sku: (snapshot.sku as string | undefined) ?? '',", " const label = variant.sku ?? variant.skuRef;", " const skuRef = line.skuRef ?? line.sku;", " skuRef: item.skuRef ?? item.sku,"];
45
+ };
46
+ //#endregion
47
+ export { STOCK_KEY_FROM_LABEL, STOCK_KEY_SEAM_FIXTURES, StockKeySeamScan, StockKeyViolation, assertStockKeySeam, scanStockKeySeam, takesStockKeyFromLabel };
@@ -0,0 +1,109 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ //#region src/testing/stock-key-seam.ts
4
+ /**
5
+ * Conformance scan: a STOCK KEY is never taken from a merchandising label.
6
+ *
7
+ * `skuRef: snapshot.sku` type-checks (both are `string`), reads plausibly, and
8
+ * ships. Downstream it decrements a quant nothing is stocked under — the row
9
+ * goes NEGATIVE while the real units sit under the stamped ref, and nothing
10
+ * throws because a move carrying a reservation may go negative. The rule and
11
+ * `stockKeyOf` already existed when that shipped; what was missing was a gate.
12
+ *
13
+ * Every package that resolves a stock key runs this over its own `src`, the way
14
+ * `@classytic/arc/testing`'s store contracts are run by their implementors. The
15
+ * matcher lives HERE so a fix to it reaches every consumer at once — a regex
16
+ * copied per package is the same drift one layer up.
17
+ *
18
+ * Node-only (`node:fs`), so it is a `/testing` subpath and never reachable from
19
+ * the isomorphic value-object entries.
20
+ */
21
+ /** `skuRef` being assigned — property (`skuRef:`) or binding (`const skuRef =`). Both are the same defect. */
22
+ const ASSIGNS_STOCK_KEY = /\bskuRef\s*[:=]\s*([^,;{}\n]*)/;
23
+ /** The right-hand side reads a `.sku` label. `(?!Ref)` so `.skuRef` is not a `.sku` hit. */
24
+ const READS_LABEL = /\.sku\b(?!Ref)/;
25
+ /** The right-hand side already reads the stamped ref, i.e. it IS the `skuRef ?? sku` fallback. */
26
+ const READS_REF = /\.skuRef\b/;
27
+ /**
28
+ * Does this line take a stock key from a label?
29
+ *
30
+ * The class is "assigns `skuRef` from an expression that reads `.sku` and never
31
+ * reads `.skuRef`". Matching `.sku` alone flags the CORRECT idiom
32
+ * (`line.skuRef ?? line.sku`), and a gate that cries wolf is a gate someone
33
+ * deletes — so the ref-read is what distinguishes a fallback from a label grab.
34
+ */
35
+ function takesStockKeyFromLabel(line) {
36
+ const rhs = ASSIGNS_STOCK_KEY.exec(line)?.[1];
37
+ if (rhs === void 0) return false;
38
+ return READS_LABEL.test(rhs) && !READS_REF.test(rhs);
39
+ }
40
+ /** @deprecated Kept for callers that want the raw predicate; prefer {@link takesStockKeyFromLabel}. */
41
+ const STOCK_KEY_FROM_LABEL = { test: takesStockKeyFromLabel };
42
+ /** Lines that are prose, not code. */
43
+ const isComment = (trimmed) => trimmed.startsWith("*") || trimmed.startsWith("//") || trimmed.startsWith("/*");
44
+ function tsFilesIn(dir, acc = []) {
45
+ for (const entry of readdirSync(dir)) {
46
+ if (entry === "node_modules" || entry === "dist") continue;
47
+ const full = join(dir, entry);
48
+ if (statSync(full).isDirectory()) tsFilesIn(full, acc);
49
+ else if (entry.endsWith(".ts") && !entry.endsWith(".d.ts")) acc.push(full);
50
+ }
51
+ return acc;
52
+ }
53
+ /** Scan a package's `src` root. Pure: returns findings, throws nothing. */
54
+ function scanStockKeySeam(srcRoot) {
55
+ const files = tsFilesIn(srcRoot);
56
+ const violations = [];
57
+ for (const file of files) {
58
+ const lines = readFileSync(file, "utf8").split("\n");
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const raw = lines[i] ?? "";
61
+ const trimmed = raw.trim();
62
+ if (isComment(trimmed)) continue;
63
+ if (takesStockKeyFromLabel(raw)) violations.push({
64
+ file: relative(srcRoot, file),
65
+ line: i + 1,
66
+ text: trimmed.slice(0, 120)
67
+ });
68
+ }
69
+ }
70
+ return {
71
+ scanned: files.length,
72
+ violations
73
+ };
74
+ }
75
+ /**
76
+ * Throwing form for a one-line test. `minFiles` is the scan-volume floor: a gate
77
+ * that quietly scanned nothing passes forever, so state what "ran" means.
78
+ */
79
+ function assertStockKeySeam(srcRoot, opts) {
80
+ const { scanned, violations } = scanStockKeySeam(srcRoot);
81
+ if (scanned < opts.minFiles) throw new Error(`stock-key seam REFUSING to pass: scanned only ${scanned} file(s) under ${srcRoot}, expected >= ${opts.minFiles}`);
82
+ if (violations.length > 0) {
83
+ const list = violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
84
+ throw new Error(`A stock key must come from stockKeyOf() (skuRef ?? sku), never from the .sku label:\n${list}`);
85
+ }
86
+ }
87
+ /**
88
+ * The fixtures the matcher must and must not fire on — exported so every
89
+ * adopting package falsifies the DETECTOR, not just its own current cleanliness.
90
+ * Both live defects are here verbatim.
91
+ */
92
+ const STOCK_KEY_SEAM_FIXTURES = {
93
+ mustMatch: [
94
+ " skuRef: (snapshot.sku as string | undefined) ?? '',",
95
+ " skuRef: snap?.sku ?? '',",
96
+ " const skuRef = line.snapshot?.sku ?? line.offerId;",
97
+ " skuRef: (line.snapshot?.sku ?? line.offerId) as string,"
98
+ ],
99
+ mustNotMatch: [
100
+ " skuRef: stockKeyOf(snapshot) ?? '',",
101
+ " skuRef: line.skuRef,",
102
+ " sku: (snapshot.sku as string | undefined) ?? '',",
103
+ " const label = variant.sku ?? variant.skuRef;",
104
+ " const skuRef = line.skuRef ?? line.sku;",
105
+ " skuRef: item.skuRef ?? item.sku,"
106
+ ]
107
+ };
108
+ //#endregion
109
+ export { STOCK_KEY_FROM_LABEL, STOCK_KEY_SEAM_FIXTURES, assertStockKeySeam, scanStockKeySeam, takesStockKeyFromLabel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
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",
@@ -185,6 +185,10 @@
185
185
  "types": "./dist/mixin.d.mts",
186
186
  "default": "./dist/mixin.mjs"
187
187
  },
188
+ "./deadline": {
189
+ "types": "./dist/deadline.d.mts",
190
+ "default": "./dist/deadline.mjs"
191
+ },
188
192
  "./sla-policy": {
189
193
  "types": "./dist/sla-policy.d.mts",
190
194
  "default": "./dist/sla-policy.mjs"
@@ -213,6 +217,18 @@
213
217
  "./item-identity": {
214
218
  "types": "./dist/item-identity.d.mts",
215
219
  "default": "./dist/item-identity.mjs"
220
+ },
221
+ "./stock-level": {
222
+ "types": "./dist/stock-level.d.mts",
223
+ "default": "./dist/stock-level.mjs"
224
+ },
225
+ "./item-facts": {
226
+ "types": "./dist/item-facts.d.mts",
227
+ "default": "./dist/item-facts.mjs"
228
+ },
229
+ "./testing/stock-key-seam": {
230
+ "types": "./dist/testing/stock-key-seam.d.mts",
231
+ "default": "./dist/testing/stock-key-seam.mjs"
216
232
  }
217
233
  },
218
234
  "files": [