@evinvest/sites-contract 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The Service-Arb sites ↔ panel contract, in TS: what a landing reads from the panel and sends
4
4
  to it. Its source is the panel — `crates/panel_core` and `contracts/proto/sa/v1` in
5
5
  [EV-Service-Arb/sites_panel](https://github.com/EV-Service-Arb/sites_panel) — and this is its
6
- only TS form. No runtime dependencies, no Node imports: it is bundled into landing pages and
6
+ only TS form. Its one dependency is `@evinvest/experiments` (zero-dep), no Node imports: it is bundled into landing pages and
7
7
  the panel's front end.
8
8
 
9
9
  ```text
@@ -15,9 +15,10 @@ src/*.ts the one TS implementation of what both sides run, hel
15
15
  price priceOf: what a page runs pricing checkPricingModel
16
16
  booking bookingRequested, isLeadRef booking-config checkBookingConfig, checkBookingUrl
17
17
  place checkPlaceLive: a place's live settings, field by field
18
- phone normalizePhone, phoneProblem
18
+ phone normalizePhone, phoneProblem message-ref messageRefOf: a ref as typed
19
19
  ingest siteEvent, ingestBody, uuidV7, isOpaqueId: the only way a site builds an event
20
- experiments parseLiveExperiments, declarationProblem
20
+ experiments parseLiveExperiments, declarationProblem; validWeights, validHoldout are
21
+ @evinvest/experiments' (what applyOverrides takes), re-exported
21
22
  ```
22
23
 
23
24
  A page bundles what it runs and not the panel's checks: they sit in modules of their own.
@@ -1,3 +1,4 @@
1
+ import { validHoldout, validWeights } from "@evinvest/experiments";
1
2
  import { EXPERIMENT_LIMITS } from "./generated/experiment-limits.js";
2
3
  import { charCount, isObject, trim } from "./text.js";
3
4
  /**
@@ -45,10 +46,9 @@ export function declarationProblem(d) {
45
46
  return "variant";
46
47
  if (new Set(d.variants).size !== d.variants.length)
47
48
  return "duplicate_variant";
48
- const weightsOk = d.weights.length === d.variants.length && d.weights.every(w => Number.isFinite(w) && w >= 0) && d.weights.reduce((a, b) => a + b, 0) > 0;
49
- if (!weightsOk)
49
+ if (validWeights(d.weights, d.variants.length) === undefined)
50
50
  return "weights";
51
- if (d.holdout !== undefined && !(Number.isFinite(d.holdout) && d.holdout >= 0 && d.holdout < 1))
51
+ if (d.holdout !== undefined && validHoldout(d.holdout) === undefined)
52
52
  return "holdout";
53
53
  if (d.summary !== undefined && charCount(trim(d.summary)) > EXPERIMENT_LIMITS.maxSummary)
54
54
  return "summary";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@ export * from "./generated/index.js";
2
2
  export { bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow, type BookingRequested } from "./booking.js";
3
3
  export { checkBookingConfig, checkBookingUrl, type BookingProblem } from "./booking-config.js";
4
4
  export { declarationProblem, parseLiveExperiments, type DeclarationProblem } from "./experiments.js";
5
+ export { validHoldout, validWeights } from "@evinvest/experiments";
5
6
  export { ingestBody, isOpaqueId, siteEvent, uuidV7, type LeadCreated, type SiteEvent, type SiteEventHead, type SiteProperties } from "./ingest.js";
7
+ export { messageRefOf } from "./message-ref.js";
6
8
  export { normalizePhone, phoneProblem } from "./phone.js";
7
9
  export { checkPlaceLive, isInstant, type PlaceProblem } from "./place.js";
8
10
  export { mulBp, priceOf, roundTo, type Price, type PriceLine, type PricingInputs, type PricingLabels } from "./price.js";
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@ export * from "./generated/index.js";
2
2
  export { bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow } from "./booking.js";
3
3
  export { checkBookingConfig, checkBookingUrl } from "./booking-config.js";
4
4
  export { declarationProblem, parseLiveExperiments } from "./experiments.js";
5
+ export { validHoldout, validWeights } from "@evinvest/experiments";
5
6
  export { ingestBody, isOpaqueId, siteEvent, uuidV7 } from "./ingest.js";
7
+ export { messageRefOf } from "./message-ref.js";
6
8
  export { normalizePhone, phoneProblem } from "./phone.js";
7
9
  export { checkPlaceLive, isInstant } from "./place.js";
8
10
  export { mulBp, priceOf, roundTo } from "./price.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A ref as an operator types or pastes it ("aq-7k3f ", "Réf. AQ-7K3F", "ref aq 7k3f", "AQ 7K3F")
3
+ * in the form the landing made it, `AQ-7K3F`, or `null` when it cannot be one
4
+ * (`contracts/vectors/message-ref.json`). In the code Crockford's look-alikes are read as it
5
+ * reads them: O is 0, I and L are 1.
6
+ */
7
+ export declare function messageRefOf(raw: string | null): string | null;
@@ -0,0 +1,22 @@
1
+ import { LEAD_LIMITS } from "./generated/lead-limits.js";
2
+ const MESSAGE_REF = /* @__PURE__ */ new RegExp(LEAD_LIMITS.messageRef);
3
+ /** A label pasted with the ref ("Réf. ", "ref ", "Ref: "): the prefilled WhatsApp line ends "Réf. AQ-7K3F". */
4
+ const REF_LABEL = /^R[EÉ]F(?:[EÉ]RENCE)?(?:[.:#]\s*|\s+)/;
5
+ /** The prefix and the code, apart: a dash, spaces, or both between them. */
6
+ const REF_PARTS = /^([A-Z]{2,4})[\s-]*([0-9A-Z]{4,8})$/;
7
+ /**
8
+ * A ref as an operator types or pastes it ("aq-7k3f ", "Réf. AQ-7K3F", "ref aq 7k3f", "AQ 7K3F")
9
+ * in the form the landing made it, `AQ-7K3F`, or `null` when it cannot be one
10
+ * (`contracts/vectors/message-ref.json`). In the code Crockford's look-alikes are read as it
11
+ * reads them: O is 0, I and L are 1.
12
+ */
13
+ export function messageRefOf(raw) {
14
+ const text = (raw ?? "").trim().toUpperCase().replace(REF_LABEL, "").trim();
15
+ const parts = REF_PARTS.exec(text);
16
+ const prefix = parts?.[1];
17
+ const code = parts?.[2];
18
+ if (prefix === undefined || code === undefined)
19
+ return null;
20
+ const ref = `${prefix}-${code.replace(/O/g, "0").replace(/[IL]/g, "1")}`;
21
+ return MESSAGE_REF.test(ref) ? ref : null;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/sites-contract",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "The Service-Arb sites ↔ panel contract in TS: the shapes and vocabularies generated from the panel's Rust and protos, and the one TS implementation of what both sides run — pricing, booking and place checks, phone numbers, the ingest events a site sends.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {
@@ -35,6 +35,9 @@
35
35
  "typecheck": "tsc --noEmit",
36
36
  "prepare": "npm run build"
37
37
  },
38
+ "dependencies": {
39
+ "@evinvest/experiments": "^0.6.1"
40
+ },
38
41
  "devDependencies": {
39
42
  "@types/node": "^22.10.0",
40
43
  "typescript": "^5.9.3",