@nodaro/shared 1.24.0 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "1.24.0",
3
+ "version": "2.0.1",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { CREDIT_BASE_USD } from "../model-constants.js"
3
+ import { usdToCredits, creditsToUsd } from "../credit-conversion.js"
4
+
5
+ describe("usdToCredits", () => {
6
+ it("converts exact multiples of the base without rounding up", () => {
7
+ // Stated against the constant, not a literal: this is an INVARIANT of the
8
+ // conversion, so it must hold at whatever CREDIT_BASE_USD currently is.
9
+ for (const multiple of [1, 2, 50, 500]) {
10
+ expect(usdToCredits(multiple * CREDIT_BASE_USD)).toBe(multiple)
11
+ }
12
+ })
13
+
14
+ it("rounds fractional cost up to a whole credit", () => {
15
+ expect(usdToCredits(CREDIT_BASE_USD * 1.5)).toBe(2)
16
+ expect(usdToCredits(CREDIT_BASE_USD / 2)).toBe(1)
17
+ })
18
+
19
+ it("returns 0 for zero cost", () => {
20
+ expect(usdToCredits(0)).toBe(0)
21
+ })
22
+
23
+ // The reason this helper exists rather than a bare ceil(usd / base).
24
+ it("absorbs IEEE-754 division noise (bare ceil would over-charge)", () => {
25
+ // 0.02 here is a LITERAL on purpose: it reproduces the historical bug the
26
+ // guard was written for (0.14 / 0.02 = 7.000000000000001, so a bare ceil
27
+ // charged 8 instead of 7), which stays meaningful after the base moves.
28
+ expect(0.14 / 0.02).toBeGreaterThan(7)
29
+ expect(Math.ceil(0.14 / 0.02)).toBe(8) // the bug
30
+ // The guard's own property: an exact multiple never rounds up, at any base.
31
+ for (const usd of [0.28, 0.56, 1.12, 2.22, 2.24, 4.44, 4.48, 4.94, 4.98]) {
32
+ expect(usdToCredits(usd)).toBe(Math.round(usd / CREDIT_BASE_USD))
33
+ }
34
+ })
35
+
36
+ it("rejects non-finite input rather than emitting NaN credits", () => {
37
+ expect(() => usdToCredits(Number.NaN)).toThrow(/finite/)
38
+ expect(() => usdToCredits(Number.POSITIVE_INFINITY)).toThrow(/finite/)
39
+ expect(() => usdToCredits(-0.5)).toThrow(/negative/)
40
+ })
41
+ })
42
+
43
+ describe("creditsToUsd", () => {
44
+ it("is the inverse of the base", () => {
45
+ expect(creditsToUsd(1)).toBeCloseTo(CREDIT_BASE_USD, 10)
46
+ expect(creditsToUsd(50)).toBeCloseTo(50 * CREDIT_BASE_USD, 10)
47
+ expect(creditsToUsd(0)).toBe(0)
48
+ })
49
+
50
+ it("rejects non-finite input", () => {
51
+ expect(() => creditsToUsd(Number.NaN)).toThrow(/finite/)
52
+ })
53
+ })
@@ -87,9 +87,10 @@ describe("bare video-analysis node-type credit id", () => {
87
87
  expect(credits, `${id} exceeds the bare-id ceiling ${ceiling}`).toBeLessThanOrEqual(ceiling)
88
88
  }
89
89
  // The migration writes this number; keep them in lockstep (277 wrote 200,
90
- // 279 wrote 739, 280 writes 346 — every tier now dispatches one identical
91
- // analyzer pass, so the whole schedule flattened and the ceiling fell with it).
92
- expect(ceiling).toBe(346)
90
+ // 279 wrote 739, 283 wrote 346, 284 wrote 350 `smart` owns the ceiling and
91
+ // gained the continuity pass and the credit re-denomination scaled it x10
92
+ // to 3500, the value 288 now writes).
93
+ expect(ceiling).toBe(3500)
93
94
  })
94
95
 
95
96
  it("the bare id still bounds the default tier at the ceiling bucket", () => {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The ONLY place credit⇄USD arithmetic is written.
3
+ *
4
+ * Every credit figure derives from {@link CREDIT_BASE_USD}. Dividing by a
5
+ * hardcoded copy of that number instead makes the base impossible to change
6
+ * without a multi-file hunt, so route conversions through here.
7
+ *
8
+ * MILLI-CREDIT GUARD — do not remove. `usd / base` is IEEE-754 division and
9
+ * lands a hair above an integer for many exact-multiple inputs
10
+ * (0.14 / 0.02 = 7.000000000000001), so a bare `Math.ceil` rounds up a whole
11
+ * credit that was not owed. Rounding to milli-credits first absorbs that noise
12
+ * at 1000x finer resolution than any credit-level decision.
13
+ *
14
+ * The guard matters at every base, not just the current one — a finer base does
15
+ * not make it unnecessary, it only moves which inputs trip it. Do not
16
+ * "simplify" it away if {@link CREDIT_BASE_USD} ever changes.
17
+ */
18
+ import { CREDIT_BASE_USD } from "./model-constants.js"
19
+
20
+ /** Milli-credits per credit — the intermediate rounding resolution. */
21
+ export const CREDIT_ROUNDING_RESOLUTION = 1000
22
+
23
+ function assertUsableUsd(usd: number): void {
24
+ if (!Number.isFinite(usd)) throw new Error(`usdToCredits: cost must be finite, got ${usd}`)
25
+ if (usd < 0) throw new Error(`usdToCredits: cost must not be negative, got ${usd}`)
26
+ }
27
+
28
+ /** A USD amount → whole credits at the current base. Rounds up. */
29
+ export function usdToCredits(usd: number): number {
30
+ assertUsableUsd(usd)
31
+ const milliCredits = Math.round((usd / CREDIT_BASE_USD) * CREDIT_ROUNDING_RESOLUTION)
32
+ return Math.ceil(milliCredits / CREDIT_ROUNDING_RESOLUTION)
33
+ }
34
+
35
+ /** Whole credits → their USD value at the current base. Display/reporting only. */
36
+ export function creditsToUsd(credits: number): number {
37
+ if (!Number.isFinite(credits)) throw new Error(`creditsToUsd: credits must be finite, got ${credits}`)
38
+ return credits * CREDIT_BASE_USD
39
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,8 @@ export { DEFAULT_LABEL_BY_SOURCE } from "./types.js"
13
13
 
14
14
  export * from "./freecut-protocol.js"
15
15
 
16
+ export { usdToCredits, creditsToUsd, CREDIT_ROUNDING_RESOLUTION } from "./credit-conversion.js"
17
+
16
18
  export {
17
19
  CREDIT_BASE_USD,
18
20
  IMAGE_PROMPT_MAX,