@intelligo-dev/core 1.0.0-beta.3 → 1.0.0-beta.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 +44 -0
- package/dist/conversations/service.d.ts +3 -4
- package/dist/conversations/service.d.ts.map +1 -1
- package/dist/conversations/service.js +3 -4
- package/dist/conversations/service.js.map +1 -1
- package/dist/db/schema/agents.js +4 -4
- package/dist/db/schema/agents.js.map +1 -1
- package/dist/db/schema/ai.d.ts +7 -8
- package/dist/db/schema/ai.d.ts.map +1 -1
- package/dist/db/schema/ai.js +13 -14
- package/dist/db/schema/ai.js.map +1 -1
- package/dist/db/schema/rag.d.ts +3 -4
- package/dist/db/schema/rag.d.ts.map +1 -1
- package/dist/db/schema/rag.js +3 -4
- package/dist/db/schema/rag.js.map +1 -1
- package/dist/db/schema/usage.js +2 -2
- package/dist/documents/classifier.d.ts +3 -2
- package/dist/documents/classifier.d.ts.map +1 -1
- package/dist/documents/classifier.js +16 -7
- package/dist/documents/classifier.js.map +1 -1
- package/dist/identity/service.d.ts +8 -8
- package/dist/identity/service.js +8 -8
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +112 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +217 -0
- package/dist/money.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +92 -0
- package/dist/registry.js.map +1 -0
- package/dist/request-context.d.ts +66 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +99 -0
- package/dist/request-context.js.map +1 -0
- package/package.json +34 -9
package/dist/identity/service.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Privacy-facing reads and mutations over the user identity graph:
|
|
5
5
|
* fact listing/deletion, a full-identity data export, and the
|
|
6
6
|
* memory-audit trail that records every mutation to a user's facts,
|
|
7
|
-
* memories, and synthesized profile snapshot. Ported from
|
|
8
|
-
* `actions/identity.ts` (ADR-0008/ADR-0009 precedent — see
|
|
7
|
+
* memories, and synthesized profile snapshot. Ported from the first
|
|
8
|
+
* product's `actions/identity.ts` (ADR-0008/ADR-0009 precedent — see
|
|
9
9
|
* `../documents/service.ts` and `../conversations/service.ts` for the
|
|
10
10
|
* same move): the tables (`user_facts`, `user_memories`,
|
|
11
11
|
* `user_profile_snapshots`, `user_memory_audit`) always lived in
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
*
|
|
17
17
|
* What did NOT come with it (stays product/agents-side):
|
|
18
18
|
* - `updateMyFactImportance` and the `synthesizeProfile()` re-trigger
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* the product's `deleteMyFact` ran after deleting — synthesis is
|
|
20
|
+
* the product's AI code (ADR-0003), and this package cannot depend
|
|
21
|
+
* on it. `deleteFact` below does not touch the cached snapshot; a
|
|
22
|
+
* caller that also owns a synthesis engine re-triggers it after
|
|
23
|
+
* calling this.
|
|
24
24
|
*
|
|
25
25
|
* Callers pass a resolved actor (workspaceId, userId) rather than this
|
|
26
26
|
* module resolving one itself — `@intelligo-dev/core` cannot depend on
|
|
@@ -132,7 +132,7 @@ export async function deleteFact(actor, factId) {
|
|
|
132
132
|
* Aggregate the actor's full identity graph — facts, memories, latest
|
|
133
133
|
* profile snapshot, and full audit trail — for a data-export flow.
|
|
134
134
|
* Records its own audit row (action "export"): the export operation
|
|
135
|
-
* audits itself, same as the ported
|
|
135
|
+
* audits itself, same as the implementation it was ported from.
|
|
136
136
|
*/
|
|
137
137
|
export async function exportIdentity(actor) {
|
|
138
138
|
// Recorded before the reads so the export's own audit row is part of
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,cAAc,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export const version = "0.0.0"
|
|
1
|
+
// There was an `export const version = "0.0.0"` here, on a package
|
|
2
|
+
// published as 1.0.0-beta.3, read by nothing. A version constant that
|
|
3
|
+
// has to be kept in step with the manifest by hand will not be; the
|
|
4
|
+
// manifest is the one place that cannot drift, and consumers who need
|
|
5
|
+
// it can read it from there.
|
|
2
6
|
// Re-export database client
|
|
3
7
|
export * from "./db/index.js";
|
|
4
8
|
// Email module -- use subpath import: @intelligo-dev/core/email
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,sEAAsE;AACtE,oEAAoE;AACpE,sEAAsE;AACtE,6BAA6B;AAE7B,4BAA4B;AAC5B,cAAc,MAAM,CAAC;AAErB,gEAAgE;AAChE,gFAAgF;AAChF,gFAAgF;AAChF,wEAAwE;AACxE,sEAAsE;AACtE,2DAA2D;AAC3D,qDAAqD"}
|
package/dist/money.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money, in one unit, with the currency attached.
|
|
3
|
+
*
|
|
4
|
+
* The framework used to denominate every amount in one deployment's
|
|
5
|
+
* currency: `balance_mnt`, `chargedMnt`, `estimatedMnt`, a
|
|
6
|
+
* `DEFAULT_USD_TO_MNT_RATE` constant, and a `formatPrice` that returned
|
|
7
|
+
* a tugrik glyph — in the public port types of a package meant to
|
|
8
|
+
* underpin products that do not sell in tugrik. This module is the
|
|
9
|
+
* replacement. It imports nothing, and must keep importing nothing:
|
|
10
|
+
* `@intelligo-dev/executions/pricing` is a deliberate zero-import leaf
|
|
11
|
+
* that client bundles reach, and it needs these types without dragging
|
|
12
|
+
* the rest of core behind them. `core/registry` holds the same line for
|
|
13
|
+
* the same reason.
|
|
14
|
+
*
|
|
15
|
+
* Two decisions worth stating, because both look arbitrary and neither
|
|
16
|
+
* is.
|
|
17
|
+
*
|
|
18
|
+
* **Micros, not minor units.** A typical chat turn on a cheap model
|
|
19
|
+
* costs about $0.0019 of provider time, which is 0.74 cents once the
|
|
20
|
+
* margin is applied. Rounding that to whole cents overcharges by more
|
|
21
|
+
* than a third on every request. Six decimal places was in fact what
|
|
22
|
+
* the old `× 3450` tugrik scaling bought — this keeps the precision and
|
|
23
|
+
* drops the pretence that the number is a currency.
|
|
24
|
+
*
|
|
25
|
+
* **Currency on the amount, not in a global.** The bug this is built
|
|
26
|
+
* against was a credit pack whose grant was authored in tokens and
|
|
27
|
+
* spent as ledger units: 100,000 of one unit sold for $1.01 of another.
|
|
28
|
+
* Nothing in the type system objected, because the number was a
|
|
29
|
+
* `number`. Here the currency travels with the amount and `add` refuses
|
|
30
|
+
* to mix two.
|
|
31
|
+
*/
|
|
32
|
+
declare const isoTag: unique symbol;
|
|
33
|
+
declare const microsTag: unique symbol;
|
|
34
|
+
/** An ISO-4217 alphabetic code, upper-cased and validated. */
|
|
35
|
+
export type CurrencyCode = string & {
|
|
36
|
+
readonly [isoTag]: true;
|
|
37
|
+
};
|
|
38
|
+
/** An integer number of millionths of one major currency unit. */
|
|
39
|
+
export type Micros = number & {
|
|
40
|
+
readonly [microsTag]: true;
|
|
41
|
+
};
|
|
42
|
+
/** An amount and the currency it is denominated in. */
|
|
43
|
+
export type Money = {
|
|
44
|
+
readonly amount: Micros;
|
|
45
|
+
readonly currency: CurrencyCode;
|
|
46
|
+
};
|
|
47
|
+
/** Thrown for every misuse in this module, so callers can catch one type. */
|
|
48
|
+
export declare class MoneyError extends Error {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
constructor(code: string, message: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate and normalise an ISO-4217 code.
|
|
54
|
+
*
|
|
55
|
+
* Shape only — a real registry would go stale, and a deployment
|
|
56
|
+
* selling in a code this file has never heard of should not be blocked
|
|
57
|
+
* by a list. `minorExponent` is where an unknown code takes its
|
|
58
|
+
* conservative default.
|
|
59
|
+
*/
|
|
60
|
+
export declare function currency(code: string): CurrencyCode;
|
|
61
|
+
/**
|
|
62
|
+
* An integer count of micros.
|
|
63
|
+
*
|
|
64
|
+
* The upper bound is `Number.MAX_SAFE_INTEGER`, which at six decimal
|
|
65
|
+
* places is about 9 billion major units — far above any real balance,
|
|
66
|
+
* and low enough to catch a value that arrived in the wrong unit.
|
|
67
|
+
*/
|
|
68
|
+
export declare function micros(value: number): Micros;
|
|
69
|
+
export declare function money(amount: number, code: string | CurrencyCode): Money;
|
|
70
|
+
/** How many decimal places this currency's minor unit has. */
|
|
71
|
+
export declare function minorExponent(code: CurrencyCode): 0 | 2 | 3;
|
|
72
|
+
/** `12.34` USD → `12_340_000` micros. */
|
|
73
|
+
export declare function fromMajor(value: number, code: string | CurrencyCode): Money;
|
|
74
|
+
/** `1234` cents → `12_340_000` micros. */
|
|
75
|
+
export declare function fromMinor(value: number, code: string | CurrencyCode): Money;
|
|
76
|
+
/**
|
|
77
|
+
* The integer a payment provider wants: whole units for a zero-decimal
|
|
78
|
+
* currency, hundredths for most, thousandths for a few.
|
|
79
|
+
*
|
|
80
|
+
* Rounds up. A fraction of a minor unit is money the deployment has
|
|
81
|
+
* already spent, and rounding it away means eating the difference on
|
|
82
|
+
* every transaction.
|
|
83
|
+
*/
|
|
84
|
+
export declare function toMinor(value: Money): number;
|
|
85
|
+
/** The amount as a plain decimal number, for display and for charts. */
|
|
86
|
+
export declare function toMajor(value: Money): number;
|
|
87
|
+
export declare const zero: (code: string | CurrencyCode) => Money;
|
|
88
|
+
export declare function add(a: Money, b: Money): Money;
|
|
89
|
+
export declare function subtract(a: Money, b: Money): Money;
|
|
90
|
+
/** Scale by a plain factor — a margin multiplier, a percentage, a count. */
|
|
91
|
+
export declare function multiply(value: Money, factor: number): Money;
|
|
92
|
+
export declare function compare(a: Money, b: Money): number;
|
|
93
|
+
export declare const isZero: (value: Money) => boolean;
|
|
94
|
+
export declare const isNegative: (value: Money) => boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Convert at an explicit rate, expressed in micros so the rate itself
|
|
97
|
+
* carries no float error: `3_450_000_000` is 3450.0.
|
|
98
|
+
*
|
|
99
|
+
* There is no ambient rate and no default. A framework that guesses an
|
|
100
|
+
* exchange rate is inventing money.
|
|
101
|
+
*/
|
|
102
|
+
export declare function convert(value: Money, to: string | CurrencyCode, rateMicros: number): Money;
|
|
103
|
+
/**
|
|
104
|
+
* Format for a human, in their locale.
|
|
105
|
+
*
|
|
106
|
+
* `Intl` owns the symbol, its position, the grouping separator and the
|
|
107
|
+
* decimal separator — none of which belong in framework source. This
|
|
108
|
+
* is the function that replaces a `₮${n.toLocaleString()}` template.
|
|
109
|
+
*/
|
|
110
|
+
export declare function formatMoney(value: Money, locale: string, options?: Intl.NumberFormatOptions): string;
|
|
111
|
+
export {};
|
|
112
|
+
//# sourceMappingURL=money.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,CAAC;AACpC,OAAO,CAAC,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AAEvC,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEhE,kEAAkE;AAClE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAE7D,uDAAuD;AACvD,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;CACjC,CAAC;AAEF,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAyCD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CASnD;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAc5C;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,KAAK,CAExE;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAE3D;AAED,yCAAyC;AACzC,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,KAAK,CAK3E;AAED,0CAA0C;AAC1C,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,KAAK,CAI3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAG5C;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE5C;AAED,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,YAAY,KAAG,KAGjD,CAAC;AAWH,wBAAgB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAG7C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAGlD;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAQ5D;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,MAAM,CAGlD;AAED,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,KAAG,OAA6B,CAAC;AACpE,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,KAAG,OAA2B,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,MAAM,GAAG,YAAY,EACzB,UAAU,EAAE,MAAM,GACjB,KAAK,CAqBP;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,IAAI,CAAC,mBAAwB,GACrC,MAAM,CASR"}
|
package/dist/money.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money, in one unit, with the currency attached.
|
|
3
|
+
*
|
|
4
|
+
* The framework used to denominate every amount in one deployment's
|
|
5
|
+
* currency: `balance_mnt`, `chargedMnt`, `estimatedMnt`, a
|
|
6
|
+
* `DEFAULT_USD_TO_MNT_RATE` constant, and a `formatPrice` that returned
|
|
7
|
+
* a tugrik glyph — in the public port types of a package meant to
|
|
8
|
+
* underpin products that do not sell in tugrik. This module is the
|
|
9
|
+
* replacement. It imports nothing, and must keep importing nothing:
|
|
10
|
+
* `@intelligo-dev/executions/pricing` is a deliberate zero-import leaf
|
|
11
|
+
* that client bundles reach, and it needs these types without dragging
|
|
12
|
+
* the rest of core behind them. `core/registry` holds the same line for
|
|
13
|
+
* the same reason.
|
|
14
|
+
*
|
|
15
|
+
* Two decisions worth stating, because both look arbitrary and neither
|
|
16
|
+
* is.
|
|
17
|
+
*
|
|
18
|
+
* **Micros, not minor units.** A typical chat turn on a cheap model
|
|
19
|
+
* costs about $0.0019 of provider time, which is 0.74 cents once the
|
|
20
|
+
* margin is applied. Rounding that to whole cents overcharges by more
|
|
21
|
+
* than a third on every request. Six decimal places was in fact what
|
|
22
|
+
* the old `× 3450` tugrik scaling bought — this keeps the precision and
|
|
23
|
+
* drops the pretence that the number is a currency.
|
|
24
|
+
*
|
|
25
|
+
* **Currency on the amount, not in a global.** The bug this is built
|
|
26
|
+
* against was a credit pack whose grant was authored in tokens and
|
|
27
|
+
* spent as ledger units: 100,000 of one unit sold for $1.01 of another.
|
|
28
|
+
* Nothing in the type system objected, because the number was a
|
|
29
|
+
* `number`. Here the currency travels with the amount and `add` refuses
|
|
30
|
+
* to mix two.
|
|
31
|
+
*/
|
|
32
|
+
/** Thrown for every misuse in this module, so callers can catch one type. */
|
|
33
|
+
export class MoneyError extends Error {
|
|
34
|
+
constructor(code, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.name = "MoneyError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const MICROS_PER_UNIT = 1000000;
|
|
41
|
+
/**
|
|
42
|
+
* Currencies whose minor unit is not the usual hundredth.
|
|
43
|
+
*
|
|
44
|
+
* Only the exceptions are listed; everything else is 2. Getting this
|
|
45
|
+
* wrong is not cosmetic — it is the difference between charging ¥1,000
|
|
46
|
+
* and ¥100,000, because Stripe takes zero-decimal currencies in whole
|
|
47
|
+
* units and every other one in hundredths.
|
|
48
|
+
*/
|
|
49
|
+
const MINOR_EXPONENT = {
|
|
50
|
+
BIF: 0,
|
|
51
|
+
CLP: 0,
|
|
52
|
+
DJF: 0,
|
|
53
|
+
GNF: 0,
|
|
54
|
+
ISK: 0,
|
|
55
|
+
JPY: 0,
|
|
56
|
+
KMF: 0,
|
|
57
|
+
KRW: 0,
|
|
58
|
+
MGA: 0,
|
|
59
|
+
MNT: 0,
|
|
60
|
+
PYG: 0,
|
|
61
|
+
RWF: 0,
|
|
62
|
+
UGX: 0,
|
|
63
|
+
UYI: 0,
|
|
64
|
+
VND: 0,
|
|
65
|
+
VUV: 0,
|
|
66
|
+
XAF: 0,
|
|
67
|
+
XOF: 0,
|
|
68
|
+
XPF: 0,
|
|
69
|
+
BHD: 3,
|
|
70
|
+
IQD: 3,
|
|
71
|
+
JOD: 3,
|
|
72
|
+
KWD: 3,
|
|
73
|
+
LYD: 3,
|
|
74
|
+
OMR: 3,
|
|
75
|
+
TND: 3,
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Validate and normalise an ISO-4217 code.
|
|
79
|
+
*
|
|
80
|
+
* Shape only — a real registry would go stale, and a deployment
|
|
81
|
+
* selling in a code this file has never heard of should not be blocked
|
|
82
|
+
* by a list. `minorExponent` is where an unknown code takes its
|
|
83
|
+
* conservative default.
|
|
84
|
+
*/
|
|
85
|
+
export function currency(code) {
|
|
86
|
+
const upper = code.toUpperCase();
|
|
87
|
+
if (!/^[A-Z]{3}$/.test(upper)) {
|
|
88
|
+
throw new MoneyError("invalid_currency", `"${code}" is not an ISO-4217 alphabetic code (three letters).`);
|
|
89
|
+
}
|
|
90
|
+
return upper;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An integer count of micros.
|
|
94
|
+
*
|
|
95
|
+
* The upper bound is `Number.MAX_SAFE_INTEGER`, which at six decimal
|
|
96
|
+
* places is about 9 billion major units — far above any real balance,
|
|
97
|
+
* and low enough to catch a value that arrived in the wrong unit.
|
|
98
|
+
*/
|
|
99
|
+
export function micros(value) {
|
|
100
|
+
if (!Number.isInteger(value)) {
|
|
101
|
+
throw new MoneyError("not_an_integer", `Micros must be a whole number; received ${value}. Convert with fromMajor/fromMinor rather than multiplying by hand.`);
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isSafeInteger(value)) {
|
|
104
|
+
throw new MoneyError("out_of_range", `${value} micros exceeds the safe integer range.`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
export function money(amount, code) {
|
|
109
|
+
return { amount: micros(amount), currency: currency(code) };
|
|
110
|
+
}
|
|
111
|
+
/** How many decimal places this currency's minor unit has. */
|
|
112
|
+
export function minorExponent(code) {
|
|
113
|
+
return MINOR_EXPONENT[code] ?? 2;
|
|
114
|
+
}
|
|
115
|
+
/** `12.34` USD → `12_340_000` micros. */
|
|
116
|
+
export function fromMajor(value, code) {
|
|
117
|
+
return {
|
|
118
|
+
amount: micros(Math.round(value * MICROS_PER_UNIT)),
|
|
119
|
+
currency: currency(code),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** `1234` cents → `12_340_000` micros. */
|
|
123
|
+
export function fromMinor(value, code) {
|
|
124
|
+
const c = currency(code);
|
|
125
|
+
const scale = MICROS_PER_UNIT / 10 ** minorExponent(c);
|
|
126
|
+
return { amount: micros(Math.round(value * scale)), currency: c };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The integer a payment provider wants: whole units for a zero-decimal
|
|
130
|
+
* currency, hundredths for most, thousandths for a few.
|
|
131
|
+
*
|
|
132
|
+
* Rounds up. A fraction of a minor unit is money the deployment has
|
|
133
|
+
* already spent, and rounding it away means eating the difference on
|
|
134
|
+
* every transaction.
|
|
135
|
+
*/
|
|
136
|
+
export function toMinor(value) {
|
|
137
|
+
const scale = MICROS_PER_UNIT / 10 ** minorExponent(value.currency);
|
|
138
|
+
return Math.ceil(value.amount / scale);
|
|
139
|
+
}
|
|
140
|
+
/** The amount as a plain decimal number, for display and for charts. */
|
|
141
|
+
export function toMajor(value) {
|
|
142
|
+
return value.amount / MICROS_PER_UNIT;
|
|
143
|
+
}
|
|
144
|
+
export const zero = (code) => ({
|
|
145
|
+
amount: 0,
|
|
146
|
+
currency: currency(code),
|
|
147
|
+
});
|
|
148
|
+
function sameCurrency(a, b, op) {
|
|
149
|
+
if (a.currency !== b.currency) {
|
|
150
|
+
throw new MoneyError("currency_mismatch", `Cannot ${op} ${a.currency} and ${b.currency}. Convert one first — an exchange rate is a decision, not a coercion.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function add(a, b) {
|
|
154
|
+
sameCurrency(a, b, "add");
|
|
155
|
+
return { amount: micros(a.amount + b.amount), currency: a.currency };
|
|
156
|
+
}
|
|
157
|
+
export function subtract(a, b) {
|
|
158
|
+
sameCurrency(a, b, "subtract");
|
|
159
|
+
return { amount: micros(a.amount - b.amount), currency: a.currency };
|
|
160
|
+
}
|
|
161
|
+
/** Scale by a plain factor — a margin multiplier, a percentage, a count. */
|
|
162
|
+
export function multiply(value, factor) {
|
|
163
|
+
if (!Number.isFinite(factor)) {
|
|
164
|
+
throw new MoneyError("invalid_factor", `Factor must be finite.`);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
amount: micros(Math.ceil(value.amount * factor)),
|
|
168
|
+
currency: value.currency,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function compare(a, b) {
|
|
172
|
+
sameCurrency(a, b, "compare");
|
|
173
|
+
return a.amount === b.amount ? 0 : a.amount < b.amount ? -1 : 1;
|
|
174
|
+
}
|
|
175
|
+
export const isZero = (value) => value.amount === 0;
|
|
176
|
+
export const isNegative = (value) => value.amount < 0;
|
|
177
|
+
/**
|
|
178
|
+
* Convert at an explicit rate, expressed in micros so the rate itself
|
|
179
|
+
* carries no float error: `3_450_000_000` is 3450.0.
|
|
180
|
+
*
|
|
181
|
+
* There is no ambient rate and no default. A framework that guesses an
|
|
182
|
+
* exchange rate is inventing money.
|
|
183
|
+
*/
|
|
184
|
+
export function convert(value, to, rateMicros) {
|
|
185
|
+
const target = currency(to);
|
|
186
|
+
if (!Number.isInteger(rateMicros) || rateMicros <= 0) {
|
|
187
|
+
throw new MoneyError("invalid_rate", `Rate must be a positive integer number of micros; received ${rateMicros}.`);
|
|
188
|
+
}
|
|
189
|
+
if (target === value.currency) {
|
|
190
|
+
if (rateMicros !== MICROS_PER_UNIT) {
|
|
191
|
+
throw new MoneyError("invalid_rate", `Converting ${value.currency} to itself at ${rateMicros / MICROS_PER_UNIT} is not a conversion.`);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
amount: micros(Math.ceil((value.amount * rateMicros) / MICROS_PER_UNIT)),
|
|
197
|
+
currency: target,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Format for a human, in their locale.
|
|
202
|
+
*
|
|
203
|
+
* `Intl` owns the symbol, its position, the grouping separator and the
|
|
204
|
+
* decimal separator — none of which belong in framework source. This
|
|
205
|
+
* is the function that replaces a `₮${n.toLocaleString()}` template.
|
|
206
|
+
*/
|
|
207
|
+
export function formatMoney(value, locale, options = {}) {
|
|
208
|
+
const digits = minorExponent(value.currency);
|
|
209
|
+
return new Intl.NumberFormat(locale, {
|
|
210
|
+
style: "currency",
|
|
211
|
+
currency: value.currency,
|
|
212
|
+
minimumFractionDigits: digits,
|
|
213
|
+
maximumFractionDigits: digits,
|
|
214
|
+
...options,
|
|
215
|
+
}).format(toMajor(value));
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registries that survive a bundler duplicating the module they live in.
|
|
3
|
+
*
|
|
4
|
+
* A framework registry is a `Map` at module scope: the composition root
|
|
5
|
+
* writes to it once at startup and every later reader reads through it.
|
|
6
|
+
* That holds exactly as long as there is one instance of the module.
|
|
7
|
+
* Next.js does not guarantee that — its server build splits code into
|
|
8
|
+
* several bundles, and a package imported from two of them can be
|
|
9
|
+
* instantiated twice. The composition root then writes to one copy and
|
|
10
|
+
* a page reads the other.
|
|
11
|
+
*
|
|
12
|
+
* The failure is quiet, which is what makes it expensive. The first
|
|
13
|
+
* product to hit it saw `"No billing product configured"` logged by a
|
|
14
|
+
* page that rendered perfectly well: quota state came back null,
|
|
15
|
+
* feature checks fell through to their closed defaults, and nothing
|
|
16
|
+
* threw. Its workaround was a `server-only` side-effect module that
|
|
17
|
+
* every server file had to import, plus an architecture test naming the
|
|
18
|
+
* ten functions that read a registry — ADR-0005 forbids exactly that
|
|
19
|
+
* kind of import-side-effect registration, so the product had to
|
|
20
|
+
* violate the rule inside files the framework had shipped it.
|
|
21
|
+
*
|
|
22
|
+
* Keying the storage off `Symbol.for()` moves the map out of the module
|
|
23
|
+
* and into the realm's global symbol registry, which the bundler cannot
|
|
24
|
+
* duplicate. Every copy of the module then finds the same map, and the
|
|
25
|
+
* composition root can go back to running once.
|
|
26
|
+
*
|
|
27
|
+
* The limit, stated honestly: `globalThis` is per process. Node and
|
|
28
|
+
* Edge runtimes are separate processes and each still needs the
|
|
29
|
+
* composition root to run. This fixes duplicate modules, not duplicate
|
|
30
|
+
* runtimes.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* A `Map` shared by every copy of the module that asks for the same
|
|
34
|
+
* key.
|
|
35
|
+
*
|
|
36
|
+
* Use it wherever a registry is written by the composition root and
|
|
37
|
+
* read by request-time code:
|
|
38
|
+
*
|
|
39
|
+
* const plans = createRegistry<PlanMap>("billing/plans");
|
|
40
|
+
*
|
|
41
|
+
* `key` is namespaced into the global symbol registry, so it needs to
|
|
42
|
+
* be unique across the framework — `"<package>/<registry>"` is the
|
|
43
|
+
* convention. The value type is not checked across copies: two modules
|
|
44
|
+
* that disagree about `T` for one key is a programming error this
|
|
45
|
+
* cannot catch, which is why the keys live next to their registries
|
|
46
|
+
* rather than in a shared list someone could reuse by accident.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createRegistry<T>(key: string): Map<string, T>;
|
|
49
|
+
/**
|
|
50
|
+
* A single shared value, for the registries that are not maps.
|
|
51
|
+
*
|
|
52
|
+
* `defaultProductSlug` is the one that motivated this: a `let` at
|
|
53
|
+
* module scope has exactly the duplication problem a `Map` does, and
|
|
54
|
+
* it is the value whose absence produces "No billing product
|
|
55
|
+
* configured".
|
|
56
|
+
*/
|
|
57
|
+
export declare function createRegistryRef<T>(key: string, initial: T): {
|
|
58
|
+
get(): T;
|
|
59
|
+
set(value: T): void;
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAWH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CA2B7D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,GACT;IAAE,GAAG,IAAI,CAAC,CAAC;IAAC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,CASnC"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registries that survive a bundler duplicating the module they live in.
|
|
3
|
+
*
|
|
4
|
+
* A framework registry is a `Map` at module scope: the composition root
|
|
5
|
+
* writes to it once at startup and every later reader reads through it.
|
|
6
|
+
* That holds exactly as long as there is one instance of the module.
|
|
7
|
+
* Next.js does not guarantee that — its server build splits code into
|
|
8
|
+
* several bundles, and a package imported from two of them can be
|
|
9
|
+
* instantiated twice. The composition root then writes to one copy and
|
|
10
|
+
* a page reads the other.
|
|
11
|
+
*
|
|
12
|
+
* The failure is quiet, which is what makes it expensive. The first
|
|
13
|
+
* product to hit it saw `"No billing product configured"` logged by a
|
|
14
|
+
* page that rendered perfectly well: quota state came back null,
|
|
15
|
+
* feature checks fell through to their closed defaults, and nothing
|
|
16
|
+
* threw. Its workaround was a `server-only` side-effect module that
|
|
17
|
+
* every server file had to import, plus an architecture test naming the
|
|
18
|
+
* ten functions that read a registry — ADR-0005 forbids exactly that
|
|
19
|
+
* kind of import-side-effect registration, so the product had to
|
|
20
|
+
* violate the rule inside files the framework had shipped it.
|
|
21
|
+
*
|
|
22
|
+
* Keying the storage off `Symbol.for()` moves the map out of the module
|
|
23
|
+
* and into the realm's global symbol registry, which the bundler cannot
|
|
24
|
+
* duplicate. Every copy of the module then finds the same map, and the
|
|
25
|
+
* composition root can go back to running once.
|
|
26
|
+
*
|
|
27
|
+
* The limit, stated honestly: `globalThis` is per process. Node and
|
|
28
|
+
* Edge runtimes are separate processes and each still needs the
|
|
29
|
+
* composition root to run. This fixes duplicate modules, not duplicate
|
|
30
|
+
* runtimes.
|
|
31
|
+
*/
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* A `Map` shared by every copy of the module that asks for the same
|
|
35
|
+
* key.
|
|
36
|
+
*
|
|
37
|
+
* Use it wherever a registry is written by the composition root and
|
|
38
|
+
* read by request-time code:
|
|
39
|
+
*
|
|
40
|
+
* const plans = createRegistry<PlanMap>("billing/plans");
|
|
41
|
+
*
|
|
42
|
+
* `key` is namespaced into the global symbol registry, so it needs to
|
|
43
|
+
* be unique across the framework — `"<package>/<registry>"` is the
|
|
44
|
+
* convention. The value type is not checked across copies: two modules
|
|
45
|
+
* that disagree about `T` for one key is a programming error this
|
|
46
|
+
* cannot catch, which is why the keys live next to their registries
|
|
47
|
+
* rather than in a shared list someone could reuse by accident.
|
|
48
|
+
*/
|
|
49
|
+
export function createRegistry(key) {
|
|
50
|
+
const symbol = Symbol.for(`@intelligo-dev/registry/${key}`);
|
|
51
|
+
const globals = globalThis;
|
|
52
|
+
const existing = globals[symbol];
|
|
53
|
+
if (existing) {
|
|
54
|
+
// A second copy of the module reaching the same slot is the
|
|
55
|
+
// condition this function exists to survive — it is not an error,
|
|
56
|
+
// and the map it returns is the right one. It is worth saying once
|
|
57
|
+
// per key, because it also means every *other* module-scope value
|
|
58
|
+
// in that file is duplicated too, and the next one to matter will
|
|
59
|
+
// not announce itself.
|
|
60
|
+
if (existing.origin !== import.meta.url && !seen.has(key)) {
|
|
61
|
+
seen.add(key);
|
|
62
|
+
console.warn(`[intelligo] registry "${key}" is being read from a second module ` +
|
|
63
|
+
`instance (${import.meta.url}, first seen from ${existing.origin}). ` +
|
|
64
|
+
`The registry itself is shared, so this is safe — but any other ` +
|
|
65
|
+
`module-scope state in that file is now duplicated.`);
|
|
66
|
+
}
|
|
67
|
+
return existing.map;
|
|
68
|
+
}
|
|
69
|
+
const map = new Map();
|
|
70
|
+
globals[symbol] = { map, origin: import.meta.url };
|
|
71
|
+
return map;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A single shared value, for the registries that are not maps.
|
|
75
|
+
*
|
|
76
|
+
* `defaultProductSlug` is the one that motivated this: a `let` at
|
|
77
|
+
* module scope has exactly the duplication problem a `Map` does, and
|
|
78
|
+
* it is the value whose absence produces "No billing product
|
|
79
|
+
* configured".
|
|
80
|
+
*/
|
|
81
|
+
export function createRegistryRef(key, initial) {
|
|
82
|
+
const box = createRegistry(`ref/${key}`);
|
|
83
|
+
if (!box.has("value"))
|
|
84
|
+
box.set("value", initial);
|
|
85
|
+
return {
|
|
86
|
+
get: () => box.get("value"),
|
|
87
|
+
set: (value) => {
|
|
88
|
+
box.set("value", value);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AASH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;AAE/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,cAAc,CAAI,GAAW;IAC3C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,UAA4D,CAAC;IAE7E,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,QAAQ,EAAE,CAAC;QACb,4DAA4D;QAC5D,kEAAkE;QAClE,mEAAmE;QACnE,kEAAkE;QAClE,kEAAkE;QAClE,uBAAuB;QACvB,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,CAAC,IAAI,CACV,yBAAyB,GAAG,uCAAuC;gBACjE,aAAa,MAAM,CAAC,IAAI,CAAC,GAAG,qBAAqB,QAAQ,CAAC,MAAM,KAAK;gBACrE,iEAAiE;gBACjE,oDAAoD,CACvD,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,CAAC;IACtB,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,EAAa,CAAC;IACjC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACnD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAW,EACX,OAAU;IAEV,MAAM,GAAG,GAAG,cAAc,CAAI,OAAO,GAAG,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO;QACL,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAM;QAChC,GAAG,EAAE,CAAC,KAAQ,EAAE,EAAE;YAChB,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where request-scoped context comes from.
|
|
3
|
+
*
|
|
4
|
+
* `@intelligo-dev/auth` needs the incoming request's headers to resolve
|
|
5
|
+
* a session, and it got them by importing `next/headers` directly in
|
|
6
|
+
* five files. That made a package whose subject is authentication —
|
|
7
|
+
* not rendering — unusable outside Next: a queue worker that wants to
|
|
8
|
+
* check a session, a Hono API, a product on another framework, a test
|
|
9
|
+
* that is not running inside a request. ADR-0005 named the fix in its
|
|
10
|
+
* consequences; this is it.
|
|
11
|
+
*
|
|
12
|
+
* The framework asks for headers through `getRequestHeaders()`. The
|
|
13
|
+
* application says where they come from, once, from its composition
|
|
14
|
+
* root:
|
|
15
|
+
*
|
|
16
|
+
* import { setRequestContextSource } from "@intelligo-dev/core/request-context";
|
|
17
|
+
* import { nextRequestContext } from "@intelligo-dev/next";
|
|
18
|
+
*
|
|
19
|
+
* setRequestContextSource(nextRequestContext);
|
|
20
|
+
*
|
|
21
|
+
* `@intelligo-dev/next` is the only package in the framework that
|
|
22
|
+
* imports `next/*`, and an architecture test keeps it that way. This
|
|
23
|
+
* module imports nothing but `./registry`, so the contract is reachable
|
|
24
|
+
* from any runtime the adapter is not.
|
|
25
|
+
*
|
|
26
|
+
* Nothing self-registers. An unbound source throws where the headers
|
|
27
|
+
* are needed, naming the two lines that fix it — the alternative is a
|
|
28
|
+
* framework that guesses at the request, which is how a session gets
|
|
29
|
+
* read from the wrong one.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Produces the current request's headers.
|
|
33
|
+
*
|
|
34
|
+
* Async because Next's accessor is, and sync sources satisfy it too —
|
|
35
|
+
* `getRequestHeaders` awaits either.
|
|
36
|
+
*/
|
|
37
|
+
export type RequestContextSource = () => Headers | Promise<Headers>;
|
|
38
|
+
export declare class RequestContextUnavailableError extends Error {
|
|
39
|
+
readonly code = "request_context_unavailable";
|
|
40
|
+
constructor();
|
|
41
|
+
}
|
|
42
|
+
export declare function setRequestContextSource(next: RequestContextSource): void;
|
|
43
|
+
/** Forget the bound source. For tests composing a fresh root. */
|
|
44
|
+
export declare function clearRequestContextSource(): void;
|
|
45
|
+
export declare function hasRequestContextSource(): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The current request's headers.
|
|
48
|
+
*
|
|
49
|
+
* @throws {RequestContextUnavailableError} when nothing is bound.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getRequestHeaders(): Promise<Headers>;
|
|
52
|
+
/**
|
|
53
|
+
* Run `fn` with these headers, whatever the ambient source says.
|
|
54
|
+
*
|
|
55
|
+
* For the callers that have a request but are not inside the
|
|
56
|
+
* framework's request scope: a background job replaying a webhook, a
|
|
57
|
+
* script acting as a user, an integration test that wants a real
|
|
58
|
+
* session without a server. Restores the previous value afterwards, so
|
|
59
|
+
* nesting behaves.
|
|
60
|
+
*
|
|
61
|
+
* Not `AsyncLocalStorage`: the value is set and restored around one
|
|
62
|
+
* awaited call, and ALS would make the package require a Node built-in
|
|
63
|
+
* that Edge runtimes only partly provide.
|
|
64
|
+
*/
|
|
65
|
+
export declare function withRequestHeaders<T>(headers: Headers, fn: () => Promise<T> | T): Promise<T>;
|
|
66
|
+
//# sourceMappingURL=request-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpE,qBAAa,8BAA+B,SAAQ,KAAK;IACvD,QAAQ,CAAC,IAAI,iCAAiC;;CAU/C;AAmBD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAExE;AAED,iEAAiE;AACjE,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC,CAQZ"}
|