@amboras-dev/capability-kit 0.1.0 → 0.2.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/README.md CHANGED
@@ -5,7 +5,8 @@ and the orchestrator (which exposes them as AI tools). One definition per
5
5
  merchant action: id, zod input/output, policy and agent text. The kit checks it
6
6
  once, converts money at one boundary, answers errors in one shape and emits a
7
7
  hashed manifest the other repo re-checks on load. CommonJS + ESM + types; Node >= 20 and
8
- browsers. No runtime dependencies besides the `zod` peer (`^3.25.76 || ^4.0.0`,
8
+ browsers. One runtime dependency, `@amboras-dev/money` (the storage rule, conversion
9
+ and money view; the kit pins its exact version), and the `zod` peer (`^3.25.76 || ^4.0.0`,
9
10
  imported only as `zod/v4`; zod 3.25.0 ships without `dist/` and cannot load `zod/v4`).
10
11
 
11
12
  ```ts
@@ -83,18 +84,27 @@ count, or a count that is not a non-negative integer.
83
84
  - **Major units at every boundary.** `money()` is the only money field type; its
84
85
  description is the one unit sentence. Storage is Medusa's: ×1000 for KWD, BHD,
85
86
  OMR, TND, JOD, IQD, LYD and ×100 for everything else, zero-decimal codes
86
- included. The currency sets are copied from Medusa `src/common/utils/money.ts`;
87
- a Medusa parity spec pins `storageScale` to `divisorFor`.
87
+ included. The rule has one owner, `@amboras-dev/money`: `storageScale`,
88
+ `fractionDigits`, `toStorageAmount`, `fromStorageAmount` and `moneyView` are thin
89
+ wrappers over its `storageScale`, `storageDigits`, `fromMajor`, `toMajorNumber` and
90
+ `/ai` `moneyView`. Codes are validated by the package's `currencyCode`: an unknown
91
+ code (`ZZZ`) throws `currency_invalid`; retired codes (BGN) still read. A stored
92
+ amount must be a safe integer.
88
93
  - **Excess decimals throw, float noise rounds.** `19.99 × 100 = 1998.999…` stores
89
94
  1999 and `0.1 + 0.2` stores 30; `19.999 USD` and `5.5 JPY` throw. The digit
90
95
  check reads the number's decimal text, so it holds at any magnitude; amounts
91
96
  above 2^46 units of the currency's precision (USD 703 billion) refuse as too large.
92
97
  - **`MoneyError` is an `invalid_input` `CapabilityError`** (rules `money_precision`,
93
- `money_invalid`, `currency_invalid`). `toStorageAmount(major, currency, field)`
98
+ `money_invalid`, `currency_invalid`), mapped from the package's `MoneyError` codes
99
+ (`precision` → `money_precision`; `amount_invalid`, `overflow` → `money_invalid`;
100
+ `currency_invalid` → `currency_invalid`). `toStorageAmount(major, currency, field)`
94
101
  names the field, so an adapter answers 400 with it. A bad `money()` option is
95
102
  a `CapabilityDefinitionError` (boot).
96
103
  - **Display digits are pinned**, not read from the runtime's ICU data (HUF/IDR
97
- differ between Node images).
104
+ differ between Node images). `moneyView` shows the currency's display digits from
105
+ the package registry, widened to keep a finer stored value: HUF 435000 is
106
+ `HUF 4,350` (was `HUF 4,350.00`), HUF 435050 `HUF 4,350.5`, IQD 1500 `IQD 1.5`
107
+ (was `IQD 1.500`); `amount` is always exact.
98
108
  - **Chaining on `money()` is safe; `.describe()` is not.** `money()` adds a
99
109
  no-op refinement as a brand. zod copies checks into every clone, so the brand
100
110
  survives `.positive()`, `.max()`, `.int()`, `.optional()`, `.nullish()`, even
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let zod_v4 = require("zod/v4");
3
+ let _amboras_dev_money = require("@amboras-dev/money");
4
+ let _amboras_dev_money_ai = require("@amboras-dev/money/ai");
3
5
  //#region src/canonical-json.ts
4
6
  function serialize(value, seen) {
5
7
  if (value !== null && typeof value === "object" && typeof value.toJSON === "function") value = value.toJSON();
@@ -197,38 +199,6 @@ function isRole(value) {
197
199
  }
198
200
  //#endregion
199
201
  //#region src/money.ts
200
- const ZERO_DECIMAL = /* @__PURE__ */ new Set([
201
- "JPY",
202
- "KRW",
203
- "VND",
204
- "CLP",
205
- "PYG",
206
- "XAF",
207
- "XOF",
208
- "BIF",
209
- "DJF",
210
- "GNF",
211
- "ISK",
212
- "KMF",
213
- "MGA",
214
- "RWF",
215
- "UGX",
216
- "VUV",
217
- "XPF"
218
- ]);
219
- const THREE_DECIMAL = /* @__PURE__ */ new Set([
220
- "KWD",
221
- "BHD",
222
- "OMR",
223
- "TND",
224
- "JOD",
225
- "IQD",
226
- "LYD"
227
- ]);
228
- /** The one unit sentence every money field carries, for the model and for people. */
229
- const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
230
- /** JSON Schema keyword marking a money field. */
231
- const MONEY_META_KEY = "x-amboras-money";
232
202
  /**
233
203
  * A bad amount or currency code. It is an `invalid_input` CapabilityError, so an
234
204
  * adapter that maps CapabilityError answers 400 with the rule (and the field,
@@ -276,7 +246,7 @@ function money(opts) {
276
246
  const { exclusiveMinimum, max } = opts;
277
247
  if (exclusiveMinimum !== void 0 && !isBound(exclusiveMinimum)) throw new CapabilityDefinitionError("money(): exclusiveMinimum must be a finite number ≥ 0");
278
248
  if (max !== void 0 && (!isBound(max) || max <= (exclusiveMinimum ?? -1))) throw new CapabilityDefinitionError("money(): max must be a finite number ≥ 0 and above exclusiveMinimum");
279
- const description = opts.describe ? `${opts.describe.trim()} ${MONEY_UNIT_SENTENCE}` : MONEY_UNIT_SENTENCE;
249
+ const description = opts.describe ? `${opts.describe.trim()} ${_amboras_dev_money_ai.MONEY_UNIT_SENTENCE}` : _amboras_dev_money_ai.MONEY_UNIT_SENTENCE;
280
250
  let schema = exclusiveMinimum === void 0 ? zod_v4.z.number().min(0) : zod_v4.z.number().gt(exclusiveMinimum);
281
251
  if (max !== void 0) schema = schema.max(max);
282
252
  schema = schema.refine(alwaysValid);
@@ -287,64 +257,82 @@ function money(opts) {
287
257
  });
288
258
  return schema.meta({
289
259
  description,
290
- [MONEY_META_KEY]: marker
260
+ [_amboras_dev_money_ai.MONEY_META_KEY]: marker
291
261
  });
292
262
  }
293
- function normalizeCode(currency) {
294
- if (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency)) throw new MoneyError(`Invalid currency code: ${JSON.stringify(currency)}`, "currency_invalid");
295
- return currency.toUpperCase();
263
+ const RULE_FOR_CODE = new Map(Object.entries({
264
+ precision: "money_precision",
265
+ amount_invalid: "money_invalid",
266
+ overflow: "money_invalid",
267
+ parse: "money_invalid",
268
+ rate_invalid: "money_invalid",
269
+ wire_refused: "money_invalid",
270
+ currency_invalid: "currency_invalid",
271
+ currency_missing: "currency_invalid",
272
+ currency_mismatch: "currency_invalid",
273
+ wire_unsupported: "currency_invalid"
274
+ }));
275
+ /** The caller's code as a known currency (`currencyCode` from @amboras-dev/money), or a kit `currency_invalid`. */
276
+ function kitCurrency(currency) {
277
+ try {
278
+ return (0, _amboras_dev_money.currencyCode)(currency);
279
+ } catch (error) {
280
+ throw toCapabilityMoneyError(error);
281
+ }
296
282
  }
297
- /** ISO 4217 minor-unit digits: 0 for JPY/KRW/VND…, 3 for KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2 (also for unknown codes). */
283
+ /**
284
+ * Internal: an error from @amboras-dev/money as the kit's MoneyError, keyed by its `code`
285
+ * (not `instanceof`, so a second copy of the package still maps). Anything else (never
286
+ * expected) is `money_invalid`.
287
+ */
288
+ function toCapabilityMoneyError(error, field) {
289
+ const code = error?.code;
290
+ const rule = RULE_FOR_CODE.get(code) ?? "money_invalid";
291
+ return new MoneyError(error instanceof Error ? error.message : String(error), rule, field);
292
+ }
293
+ /**
294
+ * Storage digits (`storageDigits` in @amboras-dev/money): 0 for JPY/KRW/VND…, 3 for
295
+ * KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2. An unknown code throws `currency_invalid`.
296
+ */
298
297
  function fractionDigits(currency) {
299
- const code = normalizeCode(currency);
300
- if (ZERO_DECIMAL.has(code)) return 0;
301
- if (THREE_DECIMAL.has(code)) return 3;
302
- return 2;
298
+ return (0, _amboras_dev_money.storageDigits)(kitCurrency(currency));
303
299
  }
304
300
  /**
305
- * Amboras storage rule: ×1000 for the seven three-decimal codes, ×100 for every
306
- * other code INCLUDING zero-decimal ones (¥5 is stored as 500). By design; see
307
- * Medusa `divisorFor`.
301
+ * Amboras storage rule (`storageScale` in @amboras-dev/money): ×1000 for the seven
302
+ * three-decimal codes, ×100 for every other code INCLUDING zero-decimal ones (¥5 is
303
+ * stored as 500). An unknown code throws `currency_invalid`.
308
304
  */
309
305
  function storageScale(currency) {
310
- return THREE_DECIMAL.has(normalizeCode(currency)) ? 1e3 : 100;
311
- }
312
- const MAX_UNITS = 2 ** 46;
313
- /** Decimal places in the shortest text form of `n` (what JSON carries): 19.99 → 2, 1e-7 → 7, 1.5e+21 → 0. */
314
- function decimalPlaces(n) {
315
- const [mantissa, exponent = "0"] = String(Math.abs(n)).split("e");
316
- const fraction = mantissa.split(".")[1] ?? "";
317
- return Math.max(0, fraction.length - Number(exponent));
306
+ return (0, _amboras_dev_money.storageScale)(kitCurrency(currency));
318
307
  }
319
308
  /**
320
- * Major → stored minor integer. Throws MoneyError (`invalid_input`) when the
321
- * amount has more fraction digits than the currency allows (19.999 USD, 5.5 JPY),
322
- * is not finite, or does not fit a safe integer. Pass `field` to name the input
323
- * field in the error.
324
- *
325
- * An amount is accepted when its decimal text has at most the allowed digits
326
- * (exact at any magnitude), or when it is within float noise of such an amount
327
- * (0.1 + 0.2 → 0.30). Noise is bounded absolutely, so a real extra digit is never
328
- * rounded away, however large the amount.
309
+ * Major → stored minor integer: `fromMajor(major, currency).amount` from
310
+ * @amboras-dev/money. Throws MoneyError (`invalid_input`) when the amount has more
311
+ * fraction digits than the currency allows (19.999 USD, 5.5 JPY), is not a finite
312
+ * number, or does not fit (2^46 units of the currency's digits). Float noise is
313
+ * accepted (0.1 + 0.2 → 0.30); a real extra digit never is. Pass `field` to name the
314
+ * input field in the error.
329
315
  */
330
316
  function toStorageAmount(major, currency, field) {
331
- const digits = fractionDigits(currency);
332
- const scale = storageScale(currency);
333
- const code = currency.toUpperCase();
334
- if (typeof major !== "number" || !Number.isFinite(major)) throw new MoneyError(`Amount must be a finite number, got ${String(major)}`, "money_invalid", field);
335
- const inDigits = major * 10 ** digits;
336
- const whole = Math.round(inDigits);
337
- const noise = Math.abs(inDigits - whole);
338
- if (!(decimalPlaces(major) <= digits) && noise > Math.min(8 * Number.EPSILON * Math.max(1, Math.abs(inDigits)), 1e-6)) throw new MoneyError(`${major} has more than ${digits} decimal places, the most ${code} allows`, "money_precision", field);
339
- const stored = whole * (scale / 10 ** digits);
340
- if (Math.abs(whole) > MAX_UNITS || !Number.isSafeInteger(stored)) throw new MoneyError(`${major} ${code} is too large to store`, "money_invalid", field);
341
- return stored === 0 ? 0 : stored;
317
+ const code = kitCurrency(currency);
318
+ if (typeof major !== "number") throw new MoneyError(`Amount must be a finite number, got ${String(major)}`, "money_invalid", field);
319
+ try {
320
+ return (0, _amboras_dev_money.fromMajor)(major, code).amount;
321
+ } catch (error) {
322
+ throw toCapabilityMoneyError(error, field);
323
+ }
342
324
  }
343
- /** Stored minor amount → major units. */
325
+ /**
326
+ * Stored minor amount → major units: `toMajorNumber(money(minor, currency))` from
327
+ * @amboras-dev/money. The stored amount must be a safe integer.
328
+ */
344
329
  function fromStorageAmount(minor, currency) {
345
- const scale = storageScale(currency);
346
- if (typeof minor !== "number" || !Number.isFinite(minor)) throw new MoneyError(`Stored amount must be a finite number, got ${String(minor)}`, "money_invalid");
347
- return minor / scale;
330
+ const code = kitCurrency(currency);
331
+ try {
332
+ return (0, _amboras_dev_money.toMajorNumber)((0, _amboras_dev_money.money)(minor, code));
333
+ } catch (error) {
334
+ throw toCapabilityMoneyError(error);
335
+ }
348
336
  }
349
337
  /** Output schema for a MoneyView (what `moneyView` returns), for capability outputs. */
350
338
  const moneyViewSchema = zod_v4.z.object({
@@ -353,25 +341,18 @@ const moneyViewSchema = zod_v4.z.object({
353
341
  display: zod_v4.z.string().describe("Formatted for people, e.g. \"$19.99\".")
354
342
  });
355
343
  /**
356
- * A stored amount as the merchant reads it. Fraction digits are pinned (not left
357
- * to the runtime's ICU data): ISO digits, widened only to show a stored value
358
- * that is not a whole minor unit (¥5.5 stays visible).
344
+ * A stored amount as the merchant reads it: `moneyView` from @amboras-dev/money/ai.
345
+ * `amount` is exact; `display` shows the currency's display digits, widened to keep a
346
+ * stored value finer than them (¥5.5 stays visible, HUF 435000 is `HUF 4,350`). An
347
+ * unknown code throws `currency_invalid`.
359
348
  */
360
349
  function moneyView(minor, currency) {
361
- const code = normalizeCode(currency);
362
- const amount = fromStorageAmount(minor, code);
363
- const digits = fractionDigits(code);
364
- const scaleDigits = storageScale(code) === 1e3 ? 3 : 2;
365
- return {
366
- amount,
367
- currency: code,
368
- display: new Intl.NumberFormat("en", {
369
- style: "currency",
370
- currency: code,
371
- minimumFractionDigits: digits,
372
- maximumFractionDigits: Math.max(digits, scaleDigits)
373
- }).format(amount)
374
- };
350
+ const code = kitCurrency(currency);
351
+ try {
352
+ return (0, _amboras_dev_money_ai.moneyView)((0, _amboras_dev_money.money)(minor, code));
353
+ } catch (error) {
354
+ throw toCapabilityMoneyError(error);
355
+ }
375
356
  }
376
357
  //#endregion
377
358
  //#region src/json-schema.ts
@@ -389,7 +370,7 @@ function toJsonSchema(schema, io) {
389
370
  const brand = moneyBrandOf(ctx.zodSchema);
390
371
  if (!brand) return;
391
372
  const node = ctx.jsonSchema;
392
- node[MONEY_META_KEY] = { ...brand.marker };
373
+ node[_amboras_dev_money_ai.MONEY_META_KEY] = { ...brand.marker };
393
374
  if (typeof node.description !== "string") node.description = brand.description;
394
375
  }
395
376
  });
@@ -552,11 +533,11 @@ function checkFieldName(id, name, schema) {
552
533
  }
553
534
  function checkMoneyNode(id, input, node) {
554
535
  const description = node.description;
555
- if (typeof description !== "string" || !description.includes("Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.")) fail(id, "a money() field lost its unit sentence; pass money({ ..., describe }) instead of .describe()/.meta()");
556
- const marker = node[MONEY_META_KEY];
536
+ if (typeof description !== "string" || !description.includes(_amboras_dev_money_ai.MONEY_UNIT_SENTENCE)) fail(id, "a money() field lost its unit sentence; pass money({ ..., describe }) instead of .describe()/.meta()");
537
+ const marker = node[_amboras_dev_money_ai.MONEY_META_KEY];
557
538
  const field = isRecord(marker) ? marker.currencyField : void 0;
558
539
  if (isRecord(marker) && marker.currency === "store" && field === void 0) return;
559
- if (typeof field !== "string") fail(id, `a money() field has a malformed ${MONEY_META_KEY} marker`);
540
+ if (typeof field !== "string") fail(id, `a money() field has a malformed ${_amboras_dev_money_ai.MONEY_META_KEY} marker`);
560
541
  const found = resolveField(input, field);
561
542
  if (found.length === 0) fail(id, `money() currencyField "${field}" is not a field at the input root`);
562
543
  for (const { parent, name } of found) {
@@ -578,7 +559,7 @@ function checkInputSchema(id, input) {
578
559
  const keys = finiteKeys(node);
579
560
  if (keys) for (const name of keys) checkFieldName(id, name, node.additionalProperties);
580
561
  if (node.additionalProperties !== void 0 && node.additionalProperties !== false && !keys || "patternProperties" in node || "propertyNames" in node && !keys) fail(id, "input objects must declare their keys (no z.record with open keys, z.looseObject or .passthrough())");
581
- if ("x-amboras-money" in node) checkMoneyNode(id, input, node);
562
+ if (_amboras_dev_money_ai.MONEY_META_KEY in node) checkMoneyNode(id, input, node);
582
563
  });
583
564
  }
584
565
  function checkHideFields(id, input, hideFields) {
@@ -1039,8 +1020,8 @@ exports.CapabilityDefinitionError = CapabilityDefinitionError;
1039
1020
  exports.CapabilityError = CapabilityError;
1040
1021
  exports.CapabilityManifestError = CapabilityManifestError;
1041
1022
  exports.IDEMPOTENCY_KEY_PATTERN = IDEMPOTENCY_KEY_PATTERN;
1042
- exports.MONEY_META_KEY = MONEY_META_KEY;
1043
- exports.MONEY_UNIT_SENTENCE = MONEY_UNIT_SENTENCE;
1023
+ exports.MONEY_META_KEY = _amboras_dev_money_ai.MONEY_META_KEY;
1024
+ exports.MONEY_UNIT_SENTENCE = _amboras_dev_money_ai.MONEY_UNIT_SENTENCE;
1044
1025
  exports.MoneyError = MoneyError;
1045
1026
  exports.ROLE_RANK = ROLE_RANK;
1046
1027
  exports.areaOf = areaOf;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod/v4";
2
+ import { MONEY_META_KEY, MONEY_UNIT_SENTENCE } from "@amboras-dev/money/ai";
2
3
  //#region src/canonical-json.d.ts
3
4
  /** Sorted keys, no whitespace. Throws on BigInt and circular values; `undefined` at the top serializes as `null`. */
4
5
  export declare function canonicalJson(v: unknown): string;
@@ -210,10 +211,6 @@ export declare function emitManifest(defs: readonly AnyCapabilityDefinition[], s
210
211
  export declare function parseManifest(json: unknown): CapabilityManifest;
211
212
  //#endregion
212
213
  //#region src/money.d.ts
213
- /** The one unit sentence every money field carries, for the model and for people. */
214
- export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
215
- /** JSON Schema keyword marking a money field. */
216
- export declare const MONEY_META_KEY = "x-amboras-money";
217
214
  /**
218
215
  * A bad amount or currency code. It is an `invalid_input` CapabilityError, so an
219
216
  * adapter that maps CapabilityError answers 400 with the rule (and the field,
@@ -250,27 +247,30 @@ type MoneyOptions = MoneyCurrency & {
250
247
  * rejects: pass `describe` here instead.
251
248
  */
252
249
  export declare function money(opts: MoneyOptions): z.ZodNumber;
253
- /** ISO 4217 minor-unit digits: 0 for JPY/KRW/VND…, 3 for KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2 (also for unknown codes). */
250
+ /**
251
+ * Storage digits (`storageDigits` in @amboras-dev/money): 0 for JPY/KRW/VND…, 3 for
252
+ * KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2. An unknown code throws `currency_invalid`.
253
+ */
254
254
  export declare function fractionDigits(currency: string): 0 | 2 | 3;
255
255
  /**
256
- * Amboras storage rule: ×1000 for the seven three-decimal codes, ×100 for every
257
- * other code INCLUDING zero-decimal ones (¥5 is stored as 500). By design; see
258
- * Medusa `divisorFor`.
256
+ * Amboras storage rule (`storageScale` in @amboras-dev/money): ×1000 for the seven
257
+ * three-decimal codes, ×100 for every other code INCLUDING zero-decimal ones (¥5 is
258
+ * stored as 500). An unknown code throws `currency_invalid`.
259
259
  */
260
260
  export declare function storageScale(currency: string): 100 | 1000;
261
261
  /**
262
- * Major → stored minor integer. Throws MoneyError (`invalid_input`) when the
263
- * amount has more fraction digits than the currency allows (19.999 USD, 5.5 JPY),
264
- * is not finite, or does not fit a safe integer. Pass `field` to name the input
265
- * field in the error.
266
- *
267
- * An amount is accepted when its decimal text has at most the allowed digits
268
- * (exact at any magnitude), or when it is within float noise of such an amount
269
- * (0.1 + 0.2 → 0.30). Noise is bounded absolutely, so a real extra digit is never
270
- * rounded away, however large the amount.
262
+ * Major → stored minor integer: `fromMajor(major, currency).amount` from
263
+ * @amboras-dev/money. Throws MoneyError (`invalid_input`) when the amount has more
264
+ * fraction digits than the currency allows (19.999 USD, 5.5 JPY), is not a finite
265
+ * number, or does not fit (2^46 units of the currency's digits). Float noise is
266
+ * accepted (0.1 + 0.2 → 0.30); a real extra digit never is. Pass `field` to name the
267
+ * input field in the error.
271
268
  */
272
269
  export declare function toStorageAmount(major: number, currency: string, field?: string): number;
273
- /** Stored minor amount → major units. */
270
+ /**
271
+ * Stored minor amount → major units: `toMajorNumber(money(minor, currency))` from
272
+ * @amboras-dev/money. The stored amount must be a safe integer.
273
+ */
274
274
  export declare function fromStorageAmount(minor: number, currency: string): number;
275
275
  interface MoneyView {
276
276
  amount: number;
@@ -280,9 +280,10 @@ interface MoneyView {
280
280
  /** Output schema for a MoneyView (what `moneyView` returns), for capability outputs. */
281
281
  export declare const moneyViewSchema: z.ZodType<MoneyView>;
282
282
  /**
283
- * A stored amount as the merchant reads it. Fraction digits are pinned (not left
284
- * to the runtime's ICU data): ISO digits, widened only to show a stored value
285
- * that is not a whole minor unit (¥5.5 stays visible).
283
+ * A stored amount as the merchant reads it: `moneyView` from @amboras-dev/money/ai.
284
+ * `amount` is exact; `display` shows the currency's display digits, widened to keep a
285
+ * stored value finer than them (¥5.5 stays visible, HUF 435000 is `HUF 4,350`). An
286
+ * unknown code throws `currency_invalid`.
286
287
  */
287
288
  export declare function moneyView(minor: number, currency: string): MoneyView;
288
289
  //#endregion
@@ -310,4 +311,4 @@ export declare function evaluatePolicy(policy: CapabilityPolicy, ctx: Capability
310
311
  */
311
312
  export declare function confirmationRequired(policy: Pick<CapabilityPolicy, 'confirm'>, records?: number): boolean;
312
313
  //#endregion
313
- export type { Actor, AnyCapabilityDefinition, Area, CapabilityAgentSpec, CapabilityCallContext, CapabilityDefinition, CapabilityErrorBody, CapabilityErrorCode, CapabilityErrorInit, CapabilityId, CapabilityManifest, CapabilityPolicy, FromZodErrorOptions, JsonSchema, ManifestEntry, MoneyCurrency, MoneyOptions, MoneyView, PolicyDecision, PolicyEnforcement, Role, Surface };
314
+ export { type Actor, type AnyCapabilityDefinition, type Area, type CapabilityAgentSpec, type CapabilityCallContext, type CapabilityDefinition, type CapabilityErrorBody, type CapabilityErrorCode, type CapabilityErrorInit, type CapabilityId, type CapabilityManifest, type CapabilityPolicy, type FromZodErrorOptions, type JsonSchema, MONEY_META_KEY, MONEY_UNIT_SENTENCE, type ManifestEntry, type MoneyCurrency, type MoneyOptions, type MoneyView, type PolicyDecision, type PolicyEnforcement, type Role, type Surface };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod/v4";
2
+ import { MONEY_META_KEY, MONEY_UNIT_SENTENCE } from "@amboras-dev/money/ai";
2
3
  //#region src/canonical-json.d.ts
3
4
  /** Sorted keys, no whitespace. Throws on BigInt and circular values; `undefined` at the top serializes as `null`. */
4
5
  export declare function canonicalJson(v: unknown): string;
@@ -210,10 +211,6 @@ export declare function emitManifest(defs: readonly AnyCapabilityDefinition[], s
210
211
  export declare function parseManifest(json: unknown): CapabilityManifest;
211
212
  //#endregion
212
213
  //#region src/money.d.ts
213
- /** The one unit sentence every money field carries, for the model and for people. */
214
- export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
215
- /** JSON Schema keyword marking a money field. */
216
- export declare const MONEY_META_KEY = "x-amboras-money";
217
214
  /**
218
215
  * A bad amount or currency code. It is an `invalid_input` CapabilityError, so an
219
216
  * adapter that maps CapabilityError answers 400 with the rule (and the field,
@@ -250,27 +247,30 @@ type MoneyOptions = MoneyCurrency & {
250
247
  * rejects: pass `describe` here instead.
251
248
  */
252
249
  export declare function money(opts: MoneyOptions): z.ZodNumber;
253
- /** ISO 4217 minor-unit digits: 0 for JPY/KRW/VND…, 3 for KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2 (also for unknown codes). */
250
+ /**
251
+ * Storage digits (`storageDigits` in @amboras-dev/money): 0 for JPY/KRW/VND…, 3 for
252
+ * KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2. An unknown code throws `currency_invalid`.
253
+ */
254
254
  export declare function fractionDigits(currency: string): 0 | 2 | 3;
255
255
  /**
256
- * Amboras storage rule: ×1000 for the seven three-decimal codes, ×100 for every
257
- * other code INCLUDING zero-decimal ones (¥5 is stored as 500). By design; see
258
- * Medusa `divisorFor`.
256
+ * Amboras storage rule (`storageScale` in @amboras-dev/money): ×1000 for the seven
257
+ * three-decimal codes, ×100 for every other code INCLUDING zero-decimal ones (¥5 is
258
+ * stored as 500). An unknown code throws `currency_invalid`.
259
259
  */
260
260
  export declare function storageScale(currency: string): 100 | 1000;
261
261
  /**
262
- * Major → stored minor integer. Throws MoneyError (`invalid_input`) when the
263
- * amount has more fraction digits than the currency allows (19.999 USD, 5.5 JPY),
264
- * is not finite, or does not fit a safe integer. Pass `field` to name the input
265
- * field in the error.
266
- *
267
- * An amount is accepted when its decimal text has at most the allowed digits
268
- * (exact at any magnitude), or when it is within float noise of such an amount
269
- * (0.1 + 0.2 → 0.30). Noise is bounded absolutely, so a real extra digit is never
270
- * rounded away, however large the amount.
262
+ * Major → stored minor integer: `fromMajor(major, currency).amount` from
263
+ * @amboras-dev/money. Throws MoneyError (`invalid_input`) when the amount has more
264
+ * fraction digits than the currency allows (19.999 USD, 5.5 JPY), is not a finite
265
+ * number, or does not fit (2^46 units of the currency's digits). Float noise is
266
+ * accepted (0.1 + 0.2 → 0.30); a real extra digit never is. Pass `field` to name the
267
+ * input field in the error.
271
268
  */
272
269
  export declare function toStorageAmount(major: number, currency: string, field?: string): number;
273
- /** Stored minor amount → major units. */
270
+ /**
271
+ * Stored minor amount → major units: `toMajorNumber(money(minor, currency))` from
272
+ * @amboras-dev/money. The stored amount must be a safe integer.
273
+ */
274
274
  export declare function fromStorageAmount(minor: number, currency: string): number;
275
275
  interface MoneyView {
276
276
  amount: number;
@@ -280,9 +280,10 @@ interface MoneyView {
280
280
  /** Output schema for a MoneyView (what `moneyView` returns), for capability outputs. */
281
281
  export declare const moneyViewSchema: z.ZodType<MoneyView>;
282
282
  /**
283
- * A stored amount as the merchant reads it. Fraction digits are pinned (not left
284
- * to the runtime's ICU data): ISO digits, widened only to show a stored value
285
- * that is not a whole minor unit (¥5.5 stays visible).
283
+ * A stored amount as the merchant reads it: `moneyView` from @amboras-dev/money/ai.
284
+ * `amount` is exact; `display` shows the currency's display digits, widened to keep a
285
+ * stored value finer than them (¥5.5 stays visible, HUF 435000 is `HUF 4,350`). An
286
+ * unknown code throws `currency_invalid`.
286
287
  */
287
288
  export declare function moneyView(minor: number, currency: string): MoneyView;
288
289
  //#endregion
@@ -310,4 +311,4 @@ export declare function evaluatePolicy(policy: CapabilityPolicy, ctx: Capability
310
311
  */
311
312
  export declare function confirmationRequired(policy: Pick<CapabilityPolicy, 'confirm'>, records?: number): boolean;
312
313
  //#endregion
313
- export type { Actor, AnyCapabilityDefinition, Area, CapabilityAgentSpec, CapabilityCallContext, CapabilityDefinition, CapabilityErrorBody, CapabilityErrorCode, CapabilityErrorInit, CapabilityId, CapabilityManifest, CapabilityPolicy, FromZodErrorOptions, JsonSchema, ManifestEntry, MoneyCurrency, MoneyOptions, MoneyView, PolicyDecision, PolicyEnforcement, Role, Surface };
314
+ export { type Actor, type AnyCapabilityDefinition, type Area, type CapabilityAgentSpec, type CapabilityCallContext, type CapabilityDefinition, type CapabilityErrorBody, type CapabilityErrorCode, type CapabilityErrorInit, type CapabilityId, type CapabilityManifest, type CapabilityPolicy, type FromZodErrorOptions, type JsonSchema, MONEY_META_KEY, MONEY_UNIT_SENTENCE, type ManifestEntry, type MoneyCurrency, type MoneyOptions, type MoneyView, type PolicyDecision, type PolicyEnforcement, type Role, type Surface };
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod/v4";
2
+ import { currencyCode, fromMajor, money as money$1, storageDigits, storageScale as storageScale$1, toMajorNumber } from "@amboras-dev/money";
3
+ import { MONEY_META_KEY, MONEY_UNIT_SENTENCE, moneyView as moneyView$1 } from "@amboras-dev/money/ai";
2
4
  //#region src/canonical-json.ts
3
5
  function serialize(value, seen) {
4
6
  if (value !== null && typeof value === "object" && typeof value.toJSON === "function") value = value.toJSON();
@@ -196,38 +198,6 @@ function isRole(value) {
196
198
  }
197
199
  //#endregion
198
200
  //#region src/money.ts
199
- const ZERO_DECIMAL = /* @__PURE__ */ new Set([
200
- "JPY",
201
- "KRW",
202
- "VND",
203
- "CLP",
204
- "PYG",
205
- "XAF",
206
- "XOF",
207
- "BIF",
208
- "DJF",
209
- "GNF",
210
- "ISK",
211
- "KMF",
212
- "MGA",
213
- "RWF",
214
- "UGX",
215
- "VUV",
216
- "XPF"
217
- ]);
218
- const THREE_DECIMAL = /* @__PURE__ */ new Set([
219
- "KWD",
220
- "BHD",
221
- "OMR",
222
- "TND",
223
- "JOD",
224
- "IQD",
225
- "LYD"
226
- ]);
227
- /** The one unit sentence every money field carries, for the model and for people. */
228
- const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
229
- /** JSON Schema keyword marking a money field. */
230
- const MONEY_META_KEY = "x-amboras-money";
231
201
  /**
232
202
  * A bad amount or currency code. It is an `invalid_input` CapabilityError, so an
233
203
  * adapter that maps CapabilityError answers 400 with the rule (and the field,
@@ -289,61 +259,79 @@ function money(opts) {
289
259
  [MONEY_META_KEY]: marker
290
260
  });
291
261
  }
292
- function normalizeCode(currency) {
293
- if (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency)) throw new MoneyError(`Invalid currency code: ${JSON.stringify(currency)}`, "currency_invalid");
294
- return currency.toUpperCase();
262
+ const RULE_FOR_CODE = new Map(Object.entries({
263
+ precision: "money_precision",
264
+ amount_invalid: "money_invalid",
265
+ overflow: "money_invalid",
266
+ parse: "money_invalid",
267
+ rate_invalid: "money_invalid",
268
+ wire_refused: "money_invalid",
269
+ currency_invalid: "currency_invalid",
270
+ currency_missing: "currency_invalid",
271
+ currency_mismatch: "currency_invalid",
272
+ wire_unsupported: "currency_invalid"
273
+ }));
274
+ /** The caller's code as a known currency (`currencyCode` from @amboras-dev/money), or a kit `currency_invalid`. */
275
+ function kitCurrency(currency) {
276
+ try {
277
+ return currencyCode(currency);
278
+ } catch (error) {
279
+ throw toCapabilityMoneyError(error);
280
+ }
295
281
  }
296
- /** ISO 4217 minor-unit digits: 0 for JPY/KRW/VND…, 3 for KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2 (also for unknown codes). */
282
+ /**
283
+ * Internal: an error from @amboras-dev/money as the kit's MoneyError, keyed by its `code`
284
+ * (not `instanceof`, so a second copy of the package still maps). Anything else (never
285
+ * expected) is `money_invalid`.
286
+ */
287
+ function toCapabilityMoneyError(error, field) {
288
+ const code = error?.code;
289
+ const rule = RULE_FOR_CODE.get(code) ?? "money_invalid";
290
+ return new MoneyError(error instanceof Error ? error.message : String(error), rule, field);
291
+ }
292
+ /**
293
+ * Storage digits (`storageDigits` in @amboras-dev/money): 0 for JPY/KRW/VND…, 3 for
294
+ * KWD/BHD/OMR/TND/JOD/IQD/LYD, else 2. An unknown code throws `currency_invalid`.
295
+ */
297
296
  function fractionDigits(currency) {
298
- const code = normalizeCode(currency);
299
- if (ZERO_DECIMAL.has(code)) return 0;
300
- if (THREE_DECIMAL.has(code)) return 3;
301
- return 2;
297
+ return storageDigits(kitCurrency(currency));
302
298
  }
303
299
  /**
304
- * Amboras storage rule: ×1000 for the seven three-decimal codes, ×100 for every
305
- * other code INCLUDING zero-decimal ones (¥5 is stored as 500). By design; see
306
- * Medusa `divisorFor`.
300
+ * Amboras storage rule (`storageScale` in @amboras-dev/money): ×1000 for the seven
301
+ * three-decimal codes, ×100 for every other code INCLUDING zero-decimal ones (¥5 is
302
+ * stored as 500). An unknown code throws `currency_invalid`.
307
303
  */
308
304
  function storageScale(currency) {
309
- return THREE_DECIMAL.has(normalizeCode(currency)) ? 1e3 : 100;
310
- }
311
- const MAX_UNITS = 2 ** 46;
312
- /** Decimal places in the shortest text form of `n` (what JSON carries): 19.99 → 2, 1e-7 → 7, 1.5e+21 → 0. */
313
- function decimalPlaces(n) {
314
- const [mantissa, exponent = "0"] = String(Math.abs(n)).split("e");
315
- const fraction = mantissa.split(".")[1] ?? "";
316
- return Math.max(0, fraction.length - Number(exponent));
305
+ return storageScale$1(kitCurrency(currency));
317
306
  }
318
307
  /**
319
- * Major → stored minor integer. Throws MoneyError (`invalid_input`) when the
320
- * amount has more fraction digits than the currency allows (19.999 USD, 5.5 JPY),
321
- * is not finite, or does not fit a safe integer. Pass `field` to name the input
322
- * field in the error.
323
- *
324
- * An amount is accepted when its decimal text has at most the allowed digits
325
- * (exact at any magnitude), or when it is within float noise of such an amount
326
- * (0.1 + 0.2 → 0.30). Noise is bounded absolutely, so a real extra digit is never
327
- * rounded away, however large the amount.
308
+ * Major → stored minor integer: `fromMajor(major, currency).amount` from
309
+ * @amboras-dev/money. Throws MoneyError (`invalid_input`) when the amount has more
310
+ * fraction digits than the currency allows (19.999 USD, 5.5 JPY), is not a finite
311
+ * number, or does not fit (2^46 units of the currency's digits). Float noise is
312
+ * accepted (0.1 + 0.2 → 0.30); a real extra digit never is. Pass `field` to name the
313
+ * input field in the error.
328
314
  */
329
315
  function toStorageAmount(major, currency, field) {
330
- const digits = fractionDigits(currency);
331
- const scale = storageScale(currency);
332
- const code = currency.toUpperCase();
333
- if (typeof major !== "number" || !Number.isFinite(major)) throw new MoneyError(`Amount must be a finite number, got ${String(major)}`, "money_invalid", field);
334
- const inDigits = major * 10 ** digits;
335
- const whole = Math.round(inDigits);
336
- const noise = Math.abs(inDigits - whole);
337
- if (!(decimalPlaces(major) <= digits) && noise > Math.min(8 * Number.EPSILON * Math.max(1, Math.abs(inDigits)), 1e-6)) throw new MoneyError(`${major} has more than ${digits} decimal places, the most ${code} allows`, "money_precision", field);
338
- const stored = whole * (scale / 10 ** digits);
339
- if (Math.abs(whole) > MAX_UNITS || !Number.isSafeInteger(stored)) throw new MoneyError(`${major} ${code} is too large to store`, "money_invalid", field);
340
- return stored === 0 ? 0 : stored;
316
+ const code = kitCurrency(currency);
317
+ if (typeof major !== "number") throw new MoneyError(`Amount must be a finite number, got ${String(major)}`, "money_invalid", field);
318
+ try {
319
+ return fromMajor(major, code).amount;
320
+ } catch (error) {
321
+ throw toCapabilityMoneyError(error, field);
322
+ }
341
323
  }
342
- /** Stored minor amount → major units. */
324
+ /**
325
+ * Stored minor amount → major units: `toMajorNumber(money(minor, currency))` from
326
+ * @amboras-dev/money. The stored amount must be a safe integer.
327
+ */
343
328
  function fromStorageAmount(minor, currency) {
344
- const scale = storageScale(currency);
345
- if (typeof minor !== "number" || !Number.isFinite(minor)) throw new MoneyError(`Stored amount must be a finite number, got ${String(minor)}`, "money_invalid");
346
- return minor / scale;
329
+ const code = kitCurrency(currency);
330
+ try {
331
+ return toMajorNumber(money$1(minor, code));
332
+ } catch (error) {
333
+ throw toCapabilityMoneyError(error);
334
+ }
347
335
  }
348
336
  /** Output schema for a MoneyView (what `moneyView` returns), for capability outputs. */
349
337
  const moneyViewSchema = z.object({
@@ -352,25 +340,18 @@ const moneyViewSchema = z.object({
352
340
  display: z.string().describe("Formatted for people, e.g. \"$19.99\".")
353
341
  });
354
342
  /**
355
- * A stored amount as the merchant reads it. Fraction digits are pinned (not left
356
- * to the runtime's ICU data): ISO digits, widened only to show a stored value
357
- * that is not a whole minor unit (¥5.5 stays visible).
343
+ * A stored amount as the merchant reads it: `moneyView` from @amboras-dev/money/ai.
344
+ * `amount` is exact; `display` shows the currency's display digits, widened to keep a
345
+ * stored value finer than them (¥5.5 stays visible, HUF 435000 is `HUF 4,350`). An
346
+ * unknown code throws `currency_invalid`.
358
347
  */
359
348
  function moneyView(minor, currency) {
360
- const code = normalizeCode(currency);
361
- const amount = fromStorageAmount(minor, code);
362
- const digits = fractionDigits(code);
363
- const scaleDigits = storageScale(code) === 1e3 ? 3 : 2;
364
- return {
365
- amount,
366
- currency: code,
367
- display: new Intl.NumberFormat("en", {
368
- style: "currency",
369
- currency: code,
370
- minimumFractionDigits: digits,
371
- maximumFractionDigits: Math.max(digits, scaleDigits)
372
- }).format(amount)
373
- };
349
+ const code = kitCurrency(currency);
350
+ try {
351
+ return moneyView$1(money$1(minor, code));
352
+ } catch (error) {
353
+ throw toCapabilityMoneyError(error);
354
+ }
374
355
  }
375
356
  //#endregion
376
357
  //#region src/json-schema.ts
@@ -551,7 +532,7 @@ function checkFieldName(id, name, schema) {
551
532
  }
552
533
  function checkMoneyNode(id, input, node) {
553
534
  const description = node.description;
554
- if (typeof description !== "string" || !description.includes("Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.")) fail(id, "a money() field lost its unit sentence; pass money({ ..., describe }) instead of .describe()/.meta()");
535
+ if (typeof description !== "string" || !description.includes(MONEY_UNIT_SENTENCE)) fail(id, "a money() field lost its unit sentence; pass money({ ..., describe }) instead of .describe()/.meta()");
555
536
  const marker = node[MONEY_META_KEY];
556
537
  const field = isRecord(marker) ? marker.currencyField : void 0;
557
538
  if (isRecord(marker) && marker.currency === "store" && field === void 0) return;
@@ -577,7 +558,7 @@ function checkInputSchema(id, input) {
577
558
  const keys = finiteKeys(node);
578
559
  if (keys) for (const name of keys) checkFieldName(id, name, node.additionalProperties);
579
560
  if (node.additionalProperties !== void 0 && node.additionalProperties !== false && !keys || "patternProperties" in node || "propertyNames" in node && !keys) fail(id, "input objects must declare their keys (no z.record with open keys, z.looseObject or .passthrough())");
580
- if ("x-amboras-money" in node) checkMoneyNode(id, input, node);
561
+ if (MONEY_META_KEY in node) checkMoneyNode(id, input, node);
581
562
  });
582
563
  }
583
564
  function checkHideFields(id, input, hideFields) {
package/package.json CHANGED
@@ -1,57 +1,60 @@
1
1
  {
2
- "name": "@amboras-dev/capability-kit",
3
- "version": "0.1.0",
4
- "description": "Capability contract for Amboras: defineCapability, major-unit money, the shared error contract, pure policy evaluation, idempotency helpers and a deterministic manifest. Peer dependency on zod only.",
5
- "license": "UNLICENSED",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/Ecomcoder/amboras-core.git",
9
- "directory": "packages/capability-kit"
2
+ "name": "@amboras-dev/capability-kit",
3
+ "version": "0.2.0",
4
+ "description": "Capability contract for Amboras: defineCapability, major-unit money (on @amboras-dev/money), the shared error contract, pure policy evaluation, idempotency helpers and a deterministic manifest. Peer dependency on zod.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Ecomcoder/amboras-core.git",
9
+ "directory": "packages/capability-kit"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.cts",
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
10
25
  },
11
- "type": "module",
12
- "main": "./dist/index.cjs",
13
- "module": "./dist/index.js",
14
- "types": "./dist/index.d.cts",
15
- "exports": {
16
- ".": {
17
- "import": {
18
- "types": "./dist/index.d.ts",
19
- "default": "./dist/index.js"
20
- },
21
- "require": {
22
- "types": "./dist/index.d.cts",
23
- "default": "./dist/index.cjs"
24
- }
25
- },
26
- "./package.json": "./package.json"
27
- },
28
- "files": [
29
- "dist"
30
- ],
31
- "sideEffects": false,
32
- "engines": {
33
- "node": ">=20"
34
- },
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
- "scripts": {
39
- "build": "tsdown",
40
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.zod3.json && tsc --noEmit -p tsconfig.zod40.json",
41
- "test": "vitest run --coverage && vitest run --config vitest.zod3.config.ts && vitest run --config vitest.zod40.config.ts"
42
- },
43
- "peerDependencies": {
44
- "zod": "^3.25.76 || ^4.0.0"
45
- },
46
- "devDependencies": {
47
- "@types/node": "22.20.5",
48
- "@vitest/coverage-v8": "3.2.7",
49
- "fast-check": "4.3.0",
50
- "tsdown": "0.23.0",
51
- "typescript": "5.9.3",
52
- "vitest": "3.2.7",
53
- "zod": "4.3.6",
54
- "zod3": "npm:zod@3.25.76",
55
- "zod40": "npm:zod@4.0.0"
56
- }
57
- }
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "sideEffects": false,
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@amboras-dev/money": "0.1.0"
40
+ },
41
+ "peerDependencies": {
42
+ "zod": "^3.25.76 || ^4.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "22.20.5",
46
+ "@vitest/coverage-v8": "3.2.7",
47
+ "fast-check": "4.3.0",
48
+ "tsdown": "0.23.0",
49
+ "typescript": "5.9.3",
50
+ "vitest": "3.2.7",
51
+ "zod": "4.3.6",
52
+ "zod3": "npm:zod@3.25.76",
53
+ "zod40": "npm:zod@4.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.zod3.json && tsc --noEmit -p tsconfig.zod40.json",
58
+ "test": "vitest run --coverage && vitest run --config vitest.zod3.config.ts && vitest run --config vitest.zod40.config.ts"
59
+ }
60
+ }