@usagekit/core 0.3.0 → 0.4.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.
@@ -208,6 +208,10 @@ export type ReserveInput = {
208
208
  provider: string;
209
209
  operation: string;
210
210
  estimate: readonly Quantity[];
211
+ /** Set by a Meter that resolved the estimate from a catalog; hosts may set it themselves. */
212
+ estimateSource?: EstimateSource;
213
+ /** "<provider>:<validFrom>" of the list price row behind the estimate. */
214
+ providerPriceVersion?: string;
211
215
  correlationId?: string;
212
216
  parentOperationId?: string;
213
217
  };
@@ -260,6 +264,84 @@ export type ReserveResult = ValidationFailure | {
260
264
  outcome: "conflict";
261
265
  reason: "semantic_mismatch";
262
266
  operation: Operation;
267
+ }
268
+ /**
269
+ * The connection's tracking policy is passthrough for this operation, or the catalog marks it
270
+ * free: counted with state passthrough, never reserved. The caller may forward the call.
271
+ */
272
+ | {
273
+ outcome: "passthrough";
274
+ replayed: boolean;
275
+ operation: null;
276
+ }
277
+ /**
278
+ * No estimate was given and the catalog does not know the operation: counted with state
279
+ * unpriced, never reserved. Host policy decides whether to forward the call.
280
+ */
281
+ | {
282
+ outcome: "unpriced";
283
+ replayed: boolean;
284
+ operation: null;
285
+ };
286
+ /** Where a reserve estimate came from, in resolution order. */
287
+ export type EstimateSource = "manual" | "measured" | "list" | "unknown";
288
+ /**
289
+ * Meter reserve input. With a catalog, estimate may be omitted: the Meter resolves it from the
290
+ * catalog for the connection and the request options. Store always receives a ReserveInput.
291
+ */
292
+ export type MeterReserveInput = Omit<ReserveInput, "estimate"> & {
293
+ /** Omitted: the Meter resolves it through its catalog and the connection policy. */
294
+ estimate?: readonly Quantity[];
295
+ /** Request options that move the price, read from the request by the descriptor. */
296
+ options?: Readonly<Record<string, string>>;
297
+ };
298
+ /**
299
+ * Non-operation outcomes, counted per state and never reserved. Metered requests are the
300
+ * operations themselves and are not counted here.
301
+ */
302
+ export type RequestState = "passthrough" | "unpriced" | "cached" | "rate_limited";
303
+ /**
304
+ * One counted request. commandId is the replay key: an identical replay counts once and returns
305
+ * replayed: true; a changed payload under the same commandId is a conflict. Counts carry no
306
+ * content and never touch budgets.
307
+ */
308
+ export type CountRequestInput = {
309
+ commandId: string;
310
+ scope: Scope;
311
+ surface: Surface;
312
+ source: Source;
313
+ provider: string;
314
+ operation: string;
315
+ state: RequestState;
316
+ platformPools?: readonly string[];
317
+ };
318
+ export type CountRequestResult = ValidationFailure | {
319
+ outcome: "counted";
320
+ replayed: boolean;
321
+ } | {
322
+ outcome: "conflict";
323
+ reason: "command_mismatch";
324
+ };
325
+ /** Requests counted at store time in [from, to), with the scope and access rules of usage. */
326
+ export type RequestCountsQuery = {
327
+ scope: UsageScope;
328
+ from: string;
329
+ to: string;
330
+ connection?: string;
331
+ groupBy: readonly ("provider" | "operation" | "connection" | "source" | "day")[];
332
+ /** Rows per page, 1..1000, default 1000; truncated reports more rows. */
333
+ limit?: number;
334
+ };
335
+ /** One row per dimension combination and state, ordered by dimensions then state. */
336
+ export type RequestCountRow = {
337
+ dimensions: Readonly<Record<string, string>>;
338
+ state: RequestState;
339
+ count: bigint;
340
+ };
341
+ export type RequestCountsPage = {
342
+ rows: readonly RequestCountRow[];
343
+ asOf: string;
344
+ truncated: boolean;
263
345
  };
264
346
  export type OperationRef = Pick<Scope, "namespace" | "principal"> & {
265
347
  operationId: string;
@@ -550,7 +632,8 @@ export type ExpireReservationsResult = ValidationFailure | {
550
632
  */
551
633
  export interface Meter {
552
634
  expireReservations(input: ExpireReservationsInput): Promise<ExpireReservationsResult>;
553
- reserve(input: ReserveInput): Promise<ReserveResult>;
635
+ reserve(input: MeterReserveInput): Promise<ReserveResult>;
636
+ countRequest(input: CountRequestInput): Promise<CountRequestResult>;
554
637
  markDispatchIntent(input: DispatchIntentInput): Promise<DispatchGrant>;
555
638
  renewLease(input: LeaseRenewalInput): Promise<LeaseRenewal>;
556
639
  claimForRecovery(input: RecoveryClaimInput): Promise<RecoveryClaim>;
@@ -560,6 +643,7 @@ export interface Meter {
560
643
  getOperation(access: AccessContext, input: OperationRef): Promise<ReadResult<Operation | null>>;
561
644
  usage(access: AccessContext, query: UsageQuery): Promise<ReadResult<UsagePage>>;
562
645
  listOperations(access: AccessContext, query: OperationsQuery): Promise<ReadResult<OperationsPage>>;
646
+ requestCounts(access: AccessContext, query: RequestCountsQuery): Promise<ReadResult<RequestCountsPage>>;
563
647
  definedBudgets(access: AccessContext, query: DefinedBudgetsQuery): Promise<ReadResult<readonly Budget[]>>;
564
648
  applicableBudgets(access: AccessContext, query: ApplicableBudgetsQuery): Promise<ReadResult<readonly BudgetStatus[]>>;
565
649
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, BudgetAlert, BudgetAlertCrossed, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, OperationsQuery, OperationsPage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, } from "./contracts.js";
1
+ export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, BudgetAlert, BudgetAlertCrossed, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, OperationsQuery, OperationsPage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, MeterReserveInput, EstimateSource, RequestState, CountRequestInput, CountRequestResult, RequestCountsQuery, RequestCountRow, RequestCountsPage, } from "./contracts.js";
2
+ export { requestStates } from "./policy.js";
2
3
  export { resolveWindow } from "./windows.js";
3
4
  export type { ResolvedWindow } from "./windows.js";
4
5
  export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
@@ -7,3 +8,5 @@ export type { ValidationFailure, AdmissionPolicy, BudgetOwner } from "./contract
7
8
  export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
8
9
  export type { ExpireReservationsInput, ExpireReservationsResult } from "./contracts.js";
9
10
  export { decodeMeterJson } from "./wire.js";
11
+ export type { ProviderId, OperationId, ProviderAuth, BillingUnit, ProviderPlan, PriceRow, RequestMatch, ProviderOperation, BalanceProbe, PriceListSource, BillingExport, ProviderDescriptor, BalanceSnapshot, BillingLine, DescriptorProblem, PricingCatalog, ConnectionPolicy, TrackingPolicy, MeasuredPrice, } from "./providers.js";
12
+ export { validateDescriptor } from "./providers.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ export { requestStates } from "./policy.js";
1
2
  export { resolveWindow } from "./windows.js";
2
3
  export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
3
4
  export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
4
5
  export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
5
6
  export { decodeMeterJson } from "./wire.js";
7
+ export { validateDescriptor } from "./providers.js";
package/dist/policy.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BudgetScope, Source, Surface } from "./contracts.js";
1
+ import type { BudgetScope, RequestState, Source, Surface } from "./contracts.js";
2
2
  /** Single source of the default admission order; hosts may permute it, never shorten it. */
3
3
  export declare const defaultBudgetOrder: readonly BudgetScope["kind"][];
4
4
  export declare const tagPattern: RegExp;
@@ -8,3 +8,5 @@ export declare function normalizeTags(tags: readonly string[]): string[] | null;
8
8
  export declare const sourcesOf: (surface: Surface) => readonly Source[];
9
9
  /** True when a budget surface value bounds an operation with this surface and source. */
10
10
  export declare const surfaceMatches: (budgetSurface: Surface | Source | "any", surface: Surface, source?: Source) => boolean;
11
+ /** Every counted request state, in display order. */
12
+ export declare const requestStates: readonly RequestState[];
package/dist/policy.js CHANGED
@@ -27,3 +27,10 @@ export const sourcesOf = (surface) => surfaceSources[surface];
27
27
  export const surfaceMatches = (budgetSurface, surface, source) => budgetSurface === "any" ||
28
28
  budgetSurface === surface ||
29
29
  (source !== undefined && budgetSurface === source);
30
+ /** Every counted request state, in display order. */
31
+ export const requestStates = Object.freeze([
32
+ "passthrough",
33
+ "unpriced",
34
+ "cached",
35
+ "rate_limited",
36
+ ]);
@@ -0,0 +1,256 @@
1
+ import type { EstimateSource, Money, Quantity } from "./contracts.js";
2
+ /**
3
+ * Provider descriptors: data that tells usagekit how to price, route, meter and reconcile one
4
+ * provider. Descriptors live in `@usagekit/providers`; the core contract does not depend on
5
+ * them. See docs/PROVIDERS.md.
6
+ */
7
+ export type ProviderId = string;
8
+ /**
9
+ * Operation identity is the pricing key. Manual overrides, tracking policy and budgets per
10
+ * operation all key on OperationId, never on a raw path.
11
+ */
12
+ export type OperationId = string;
13
+ export type ProviderAuth = {
14
+ kind: "query";
15
+ param: string;
16
+ } | {
17
+ kind: "header";
18
+ name: string;
19
+ prefix?: string;
20
+ } | {
21
+ kind: "basic";
22
+ } | {
23
+ kind: "bearer";
24
+ };
25
+ export type BillingUnit = "cents" | "units" | "requests";
26
+ /**
27
+ * allowanceMode mirrors how the provider behaves past the allowance: prepaid balances and
28
+ * no-overage plans are hard, plans that bill excess are soft.
29
+ */
30
+ export type ProviderPlan = {
31
+ id: string;
32
+ label: string;
33
+ cycle: "monthly_plan" | "prepaid" | "trial" | "none";
34
+ allowance?: {
35
+ unit: BillingUnit;
36
+ value: bigint;
37
+ };
38
+ allowanceMode: "hard" | "soft";
39
+ };
40
+ /**
41
+ * One list price. The descriptor never holds negotiated prices, discounts or volume tiers beyond
42
+ * the public list; those are manual prices on the connection.
43
+ */
44
+ export type PriceRow = {
45
+ /** ISO date. */
46
+ validFrom: string;
47
+ operation: OperationId;
48
+ /** Plan id; absent means any plan. */
49
+ plan?: string;
50
+ /** Request options that select this row. Options are request data, never guessed. */
51
+ option?: Readonly<Record<string, string>>;
52
+ /** Applies past the plan allowance. */
53
+ overage?: true;
54
+ unit: BillingUnit;
55
+ /** Exact nonnegative decimal in that unit. */
56
+ perUnit: string;
57
+ /** URL of the public price source. */
58
+ source: string;
59
+ /** ISO date of the last check against the source. */
60
+ checkedAt: string;
61
+ };
62
+ export type RequestMatch = {
63
+ method: "GET" | "POST" | "PUT" | "DELETE" | "ANY";
64
+ /** Path pattern with :params (one segment) and a trailing * (any rest). */
65
+ path: string;
66
+ query?: Readonly<Record<string, string | RegExp>>;
67
+ };
68
+ /**
69
+ * Free endpoints are operations too, with billable false. They are matched, counted and never
70
+ * reserved. A descriptor may advertise an operation only when at least one fixture exists for it.
71
+ */
72
+ export type ProviderOperation = {
73
+ id: OperationId;
74
+ label: string;
75
+ billable: boolean;
76
+ match: readonly RequestMatch[];
77
+ /** Options that move the price; values are extracted from the request by `options`. */
78
+ optionKeys?: readonly string[];
79
+ /**
80
+ * Cost is known from the response body, only after a later call, or never per call.
81
+ * response: settle on stream close. deferred: settle with unknown cost, keep pending, resolve
82
+ * through the task-get operation or billing export. none: settle with the estimate as
83
+ * estimated, reconcile from balance deltas or export.
84
+ */
85
+ costEvidence: "response" | "deferred" | "none";
86
+ /** Idempotent replay is safe for this operation when the provider returns the same result. */
87
+ cacheable?: {
88
+ keyParams: readonly string[];
89
+ defaultTtlMs: number;
90
+ };
91
+ };
92
+ /** Account identity is returned to the host as data. Duplicate detection is a host decision. */
93
+ export type BalanceProbe = {
94
+ /** The free account endpoint. */
95
+ operation: OperationId;
96
+ fields: {
97
+ /** JSON pointer. */
98
+ remaining?: string;
99
+ remainingUnit?: BillingUnit;
100
+ allowance?: string;
101
+ used?: string;
102
+ resetsAt?: string;
103
+ plan?: string;
104
+ /** E-mail or login, for duplicate detection. */
105
+ accountIdentity?: string;
106
+ rateLimitPerHour?: string;
107
+ };
108
+ };
109
+ /**
110
+ * A price list from an endpoint is a refresh source for prices, not a replacement: a pull
111
+ * request updates the rows with the fetched values and the check date; runtime never fetches
112
+ * prices to decide admission.
113
+ */
114
+ export type PriceListSource = {
115
+ kind: "static";
116
+ } | {
117
+ kind: "endpoint";
118
+ operation: OperationId;
119
+ pointer: string;
120
+ mapping: "provider-specific";
121
+ };
122
+ export type BillingExport = {
123
+ kind: "task-history" | "daily-csv" | "none";
124
+ /** For API-backed history. */
125
+ operation?: OperationId;
126
+ granularity: "per-request" | "per-day";
127
+ matchKey: "providerRequestId" | "window";
128
+ };
129
+ export type ProviderDescriptor = {
130
+ id: ProviderId;
131
+ label: string;
132
+ /** Origin and optional base path, for example "https://api.example.com". */
133
+ upstream: string;
134
+ auth: ProviderAuth;
135
+ stripRequestHeaders?: readonly string[];
136
+ billing: {
137
+ unit: BillingUnit;
138
+ cycle: ProviderPlan["cycle"];
139
+ };
140
+ plans: readonly ProviderPlan[];
141
+ prices: readonly PriceRow[];
142
+ priceList: PriceListSource;
143
+ operations: readonly ProviderOperation[];
144
+ balance?: BalanceProbe;
145
+ billingExport: BillingExport;
146
+ rateLimits?: readonly {
147
+ scope: "account";
148
+ perMinute?: number;
149
+ perHour?: number;
150
+ }[];
151
+ docs: {
152
+ pricing: string;
153
+ api: string;
154
+ };
155
+ };
156
+ /** Parsed balance probe. Every field is optional because providers report different subsets. */
157
+ export type BalanceSnapshot = {
158
+ remaining?: Quantity;
159
+ allowance?: Quantity;
160
+ used?: Quantity;
161
+ /** UTC ISO timestamp of the next allowance reset. */
162
+ resetsAt?: string;
163
+ plan?: string;
164
+ accountIdentity?: string;
165
+ rateLimitPerHour?: number;
166
+ };
167
+ /** One matchable line of a provider billing export. */
168
+ export type BillingLine = {
169
+ providerRequestId?: string;
170
+ operation?: OperationId;
171
+ occurredAt: string;
172
+ cost: Money;
173
+ /** Window of a per-day line, [from, to). */
174
+ window?: {
175
+ from: string;
176
+ to: string;
177
+ };
178
+ };
179
+ /** Per-connection choice per catalog operation; the default is metered. */
180
+ export type TrackingPolicy = "metered" | "passthrough";
181
+ /** Median of a connection's settled receipts for one operation, with its sample size. */
182
+ export type MeasuredPrice = (operation: OperationId) => {
183
+ quantity: Quantity;
184
+ samples: number;
185
+ } | undefined;
186
+ /**
187
+ * Connection data a host resolves for the Meter: provider, selected plan, manual per-call prices,
188
+ * measured prices and tracking policy, all keyed by OperationId. overage is true when the host
189
+ * knows the plan allowance is exhausted.
190
+ */
191
+ export type ConnectionPolicy = {
192
+ provider: ProviderId;
193
+ plan?: string;
194
+ manualPrices?: Readonly<Record<OperationId, Quantity>>;
195
+ measured?: MeasuredPrice;
196
+ tracking?: Readonly<Record<OperationId, TrackingPolicy>>;
197
+ overage?: boolean;
198
+ };
199
+ /**
200
+ * What the Meter needs from a provider catalog. `@usagekit/providers` createCatalog satisfies
201
+ * it; the Meter does not depend on that package. operationsOf lists the operations of an
202
+ * enabled provider and nothing for a disabled or unknown one.
203
+ */
204
+ export type PricingCatalog = {
205
+ estimate(connection: Omit<ConnectionPolicy, "tracking"> & {
206
+ id: string;
207
+ }, operation: OperationId, options: Readonly<Record<string, string>>): {
208
+ quantities: Quantity[];
209
+ source: EstimateSource;
210
+ priceVersion?: string;
211
+ };
212
+ operationsOf(providerId: ProviderId): readonly ProviderOperation[];
213
+ };
214
+ export type DescriptorProblem = {
215
+ kind: "duplicate_operation";
216
+ operation: OperationId;
217
+ } | {
218
+ kind: "duplicate_plan";
219
+ plan: string;
220
+ } | {
221
+ kind: "missing_match";
222
+ operation: OperationId;
223
+ } | {
224
+ kind: "unknown_price_operation";
225
+ row: number;
226
+ operation: OperationId;
227
+ } | {
228
+ kind: "unknown_price_plan";
229
+ row: number;
230
+ plan: string;
231
+ } | {
232
+ kind: "unknown_price_option";
233
+ row: number;
234
+ option: string;
235
+ } | {
236
+ kind: "invalid_price";
237
+ row: number;
238
+ field: "perUnit" | "validFrom" | "checkedAt" | "source";
239
+ } | {
240
+ kind: "unknown_probe_operation";
241
+ operation: OperationId;
242
+ } | {
243
+ kind: "billable_probe";
244
+ operation: OperationId;
245
+ } | {
246
+ kind: "unknown_price_list_operation";
247
+ operation: OperationId;
248
+ } | {
249
+ kind: "unknown_export_operation";
250
+ operation: OperationId;
251
+ };
252
+ /**
253
+ * Structural problems of a descriptor, in a stable order: operations, plans, prices, probe,
254
+ * price list, billing export. An empty list means the catalog may load it.
255
+ */
256
+ export declare function validateDescriptor(d: ProviderDescriptor): DescriptorProblem[];
@@ -0,0 +1,54 @@
1
+ const isoDate = (text) => /^\d{4}-\d{2}-\d{2}/.test(text) && Number.isFinite(Date.parse(text));
2
+ /**
3
+ * Structural problems of a descriptor, in a stable order: operations, plans, prices, probe,
4
+ * price list, billing export. An empty list means the catalog may load it.
5
+ */
6
+ export function validateDescriptor(d) {
7
+ const problems = [];
8
+ const operations = new Map();
9
+ for (const op of d.operations) {
10
+ if (operations.has(op.id))
11
+ problems.push({ kind: "duplicate_operation", operation: op.id });
12
+ else
13
+ operations.set(op.id, op);
14
+ }
15
+ for (const op of d.operations)
16
+ if (op.match.length === 0 && operations.get(op.id) === op)
17
+ problems.push({ kind: "missing_match", operation: op.id });
18
+ const plans = new Set();
19
+ for (const plan of d.plans) {
20
+ if (plans.has(plan.id))
21
+ problems.push({ kind: "duplicate_plan", plan: plan.id });
22
+ plans.add(plan.id);
23
+ }
24
+ d.prices.forEach((row, index) => {
25
+ const op = operations.get(row.operation);
26
+ if (!op)
27
+ problems.push({ kind: "unknown_price_operation", row: index, operation: row.operation });
28
+ if (row.plan !== undefined && !plans.has(row.plan))
29
+ problems.push({ kind: "unknown_price_plan", row: index, plan: row.plan });
30
+ for (const key of Object.keys(row.option ?? {}))
31
+ if (op && !(op.optionKeys ?? []).includes(key))
32
+ problems.push({ kind: "unknown_price_option", row: index, option: key });
33
+ if (!/^\d+(\.\d+)?$/.test(row.perUnit))
34
+ problems.push({ kind: "invalid_price", row: index, field: "perUnit" });
35
+ if (!isoDate(row.validFrom))
36
+ problems.push({ kind: "invalid_price", row: index, field: "validFrom" });
37
+ if (!isoDate(row.checkedAt))
38
+ problems.push({ kind: "invalid_price", row: index, field: "checkedAt" });
39
+ if (!row.source.trim())
40
+ problems.push({ kind: "invalid_price", row: index, field: "source" });
41
+ });
42
+ if (d.balance) {
43
+ const probe = operations.get(d.balance.operation);
44
+ if (!probe)
45
+ problems.push({ kind: "unknown_probe_operation", operation: d.balance.operation });
46
+ else if (probe.billable)
47
+ problems.push({ kind: "billable_probe", operation: probe.id });
48
+ }
49
+ if (d.priceList.kind === "endpoint" && !operations.has(d.priceList.operation))
50
+ problems.push({ kind: "unknown_price_list_operation", operation: d.priceList.operation });
51
+ if (d.billingExport.operation !== undefined && !operations.has(d.billingExport.operation))
52
+ problems.push({ kind: "unknown_export_operation", operation: d.billingExport.operation });
53
+ return problems;
54
+ }
package/dist/wire.js CHANGED
@@ -23,6 +23,12 @@ export function decodeMeterJson(text) {
23
23
  typeof v.costOwner === "string" &&
24
24
  decimal(v.unknownOperations))
25
25
  return { ...v, unknownOperations: BigInt(v.unknownOperations) };
26
+ if (keys.length === 3 &&
27
+ v.dimensions &&
28
+ typeof v.dimensions === "object" &&
29
+ ["passthrough", "unpriced", "cached", "rate_limited"].includes(String(v.state)) &&
30
+ decimal(v.count))
31
+ return { ...v, count: BigInt(v.count) };
26
32
  return value;
27
33
  });
28
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagekit/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Exact quantities, ownership and pure domain types.",