@aldus-runtime/gate-engine 0.2.0-next.2 → 0.2.0-next.21

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/dist/spend.js CHANGED
@@ -11,13 +11,14 @@
11
11
  * `GateDecision` stores only digests. Carrying the limit in a record beside the decision would
12
12
  * let someone raise the ceiling without touching the approval.
13
13
  *
14
- * So a grant does both: it holds the values, and {@link grantLimitsDigest} is included among the
14
+ * So a grant does both: it holds the values, and {@link grantTermsDigest} is included among the
15
15
  * gate's bound subjects. Raising a limit changes that digest, which drifts from `subjectHashes`,
16
16
  * which voids the authorization exactly as §13.2 requires. The ceiling cannot move without an
17
17
  * operator re-approving it.
18
18
  */
19
+ import { reservationExposureIsBounded, reservationIsActive, } from "@aldus-runtime/core";
19
20
  import { digestSubjectValue } from "./binding.js";
20
- import { addMoney, compareMoney, formatMoney, isNegativeMoney, subtractMoney, zeroMoney, } from "./money.js";
21
+ import { addMoney, compareMoney, formatMoney, isNegativeMoney, isPositiveMoney, subtractMoney, zeroMoney, } from "./money.js";
21
22
  /**
22
23
  * The subject key under which a grant's limits are bound.
23
24
  *
@@ -27,13 +28,21 @@ import { addMoney, compareMoney, formatMoney, isNegativeMoney, subtractMoney, ze
27
28
  */
28
29
  export const SPEND_LIMIT_SUBJECT_KEY = "spendLimit";
29
30
  /**
30
- * The digest a gate must bind for the grant's limits to be tamper-evident.
31
+ * The digest a gate must bind for the grant's **terms** to be tamper-evident (§13.2).
31
32
  *
32
- * Only the limits are digested, not the grant's identity: re-issuing an identical ceiling under a
33
- * new `grantId` should not read as the operator having approved something different.
33
+ * Was `grantTermsDigest`, covering the ceilings alone. Scope is now a term too: changing a grant
34
+ * from agent-only to TTS-capable widens what an approval permits exactly as raising its ceiling
35
+ * does, and an approval that survives that change did not bind what it appeared to bind.
36
+ *
37
+ * Only the terms are digested, not the grant's identity: re-issuing an identical grant under a new
38
+ * `grantId` should not read as the operator having approved something different.
34
39
  */
35
- export function grantLimitsDigest(grant) {
40
+ export function grantTermsDigest(grant) {
36
41
  return digestSubjectValue({
42
+ // Sorted, because the *set* of authorized operations is the term, not the order an adopter
43
+ // listed them in — the same reason ADR-0033 sorts release input hashes.
44
+ scope: { operations: [...grant.scope.operations].sort() },
45
+ unestimatedExecution: grant.unestimatedExecution ?? "refuse",
37
46
  maxTotal: grant.maxTotal,
38
47
  maxPerRequest: grant.maxPerRequest ?? null,
39
48
  });
@@ -62,6 +71,16 @@ export function costRecordDraw(record, currency) {
62
71
  return zeroMoney(currency);
63
72
  return record.actual ?? record.estimated ?? zeroMoney(currency);
64
73
  }
74
+ /**
75
+ * Whether a record is a charge of unknown size (§19.3; #150).
76
+ *
77
+ * An estimate does not resolve it. An estimate is evidence about what a request was expected to
78
+ * cost, and the ruling on #150 is explicit that it does not confirm the final charge — so a record
79
+ * carrying both an estimate and `billingStatus: "unknown"` is still unresolved.
80
+ */
81
+ export function isUnresolvedUnknownCharge(record) {
82
+ return record.billingStatus === "unknown";
83
+ }
65
84
  /**
66
85
  * Total what has been drawn against a grant.
67
86
  *
@@ -85,12 +104,122 @@ export function computeLedger(grant, costs) {
85
104
  }
86
105
  const headroom = subtractMoney(grant.maxTotal, consumed);
87
106
  const overspent = isNegativeMoney(headroom);
107
+ const unresolvedUnknown = counted.filter(isUnresolvedUnknownCharge);
88
108
  return {
89
109
  consumed,
90
110
  remaining: overspent ? zeroMoney(currency) : headroom,
91
111
  overspent,
92
112
  counted,
93
113
  excluded,
114
+ unresolvedUnknown,
115
+ remainingIsDeterminate: unresolvedUnknown.length === 0,
116
+ };
117
+ }
118
+ /**
119
+ * Derive what a grant still authorizes (§19.3; ADR-0044).
120
+ *
121
+ * Composes with #150 rather than replacing it. A charge of unknown size still makes the grant
122
+ * indeterminate — what a reservation adds is the possibility of a *bound*, and only when the
123
+ * execution that produced the charge was actually dispatched under an enforced ceiling. A backend
124
+ * that declares enforcement today is not evidence about a request dispatched by an earlier version
125
+ * (ADR-0030).
126
+ */
127
+ export function availableAuthorization(grant, costs, reservations = []) {
128
+ const currency = grant.maxTotal.currency;
129
+ const ledger = computeLedger(grant, costs);
130
+ // Per grant, never per decision. The grant is the budget pool; the decision is who authorized
131
+ // its terms. One decision may establish several grants — an episode-level ceiling above an agent
132
+ // grant and a TTS grant — and deriving availability per decision would silently aggregate them.
133
+ const mine = reservations.filter((reservation) => reservation.grantId === grant.grantId);
134
+ const active = mine.filter(reservationIsActive);
135
+ // Settled reservations are already represented by the cost records that settled them. Counting
136
+ // both would double-count the same money against the ceiling.
137
+ let reserved = zeroMoney(currency);
138
+ const reservedUnknownByCurrency = new Map();
139
+ for (const reservation of active) {
140
+ if (reservation.reserved.currency === currency) {
141
+ reserved = addMoney(reserved, reservation.reserved);
142
+ }
143
+ if (reservation.status === "billing_unknown") {
144
+ const running = reservedUnknownByCurrency.get(reservation.reserved.currency) ??
145
+ zeroMoney(reservation.reserved.currency);
146
+ reservedUnknownByCurrency.set(reservation.reserved.currency, addMoney(running, reservation.reserved));
147
+ }
148
+ }
149
+ const unboundedReservations = active.filter((reservation) => reservation.status === "billing_unknown" && !reservationExposureIsBounded(reservation));
150
+ // #150's rule, narrowed by what a reservation can now establish. A record of unknown size whose
151
+ // reservation is bounded is accounted for; one with no reservation is not, and that is every
152
+ // such record written before this protocol.
153
+ const boundedReservationIds = new Set(active
154
+ .filter((reservation) => reservationExposureIsBounded(reservation))
155
+ .map((reservation) => reservation.reservationId));
156
+ // `computeLedger` filters cost records by `authorizationId`, which is the legacy link and stays
157
+ // correct: a record written before reservations existed has no `reservationId` to reach a grant
158
+ // through, and its decision is the only thing tying it to an authorization (#155).
159
+ const unreservedUnknownCharges = ledger.counted.filter((record) => isUnresolvedUnknownCharge(record) &&
160
+ (record.reservationId === undefined || !boundedReservationIds.has(record.reservationId)));
161
+ const headroom = subtractMoney(subtractMoney(grant.maxTotal, ledger.consumed), reserved);
162
+ return {
163
+ authorized: grant.maxTotal,
164
+ settled: ledger.consumed,
165
+ reserved,
166
+ available: isNegativeMoney(headroom) ? zeroMoney(currency) : headroom,
167
+ determinate: unboundedReservations.length === 0 && unreservedUnknownCharges.length === 0,
168
+ indeterminate: { unboundedReservations, unreservedUnknownCharges },
169
+ reservedUnknownByCurrency: Object.fromEntries([...reservedUnknownByCurrency.entries()]
170
+ .sort(([a], [b]) => a.localeCompare(b))
171
+ .map(([code, money]) => [code, money.amount])),
172
+ };
173
+ }
174
+ /**
175
+ * Whether a grant can truthfully permit an unestimated execution (ADR-0044; #155).
176
+ *
177
+ * `reserve_max_per_request` promises to reserve the per-request ceiling. A grant that permits it
178
+ * and states no such ceiling promises an amount it does not have — and reserving zero instead
179
+ * would make unestimated executions invisible to concurrency control, which is the case most
180
+ * likely to be dispatched in a loop.
181
+ *
182
+ * Checked at decode **and** before dispatch: a grant assembled from configuration never passes
183
+ * through the constructor that would have caught it.
184
+ */
185
+ export function unestimatedPolicyIsSatisfiable(grant) {
186
+ if ((grant.unestimatedExecution ?? "refuse") !== "reserve_max_per_request")
187
+ return undefined;
188
+ const ceiling = grant.maxPerRequest;
189
+ if (ceiling === undefined) {
190
+ return (`Grant "${grant.grantId}" permits unestimated execution by reserving its per-request ` +
191
+ "ceiling, and states no ceiling. There is no truthful amount to reserve, so this cannot be " +
192
+ "dispatched (§13.2, §19.3).");
193
+ }
194
+ if (!isPositiveMoney(ceiling)) {
195
+ return (`Grant "${grant.grantId}" permits unestimated execution by reserving its per-request ` +
196
+ `ceiling, and that ceiling is ${formatMoney(ceiling)}. Reserving zero would make an ` +
197
+ "unestimated execution invisible to concurrency control.");
198
+ }
199
+ if (ceiling.currency !== grant.maxTotal.currency) {
200
+ return (`Grant "${grant.grantId}" states a per-request ceiling in ${ceiling.currency} and a total ` +
201
+ `in ${grant.maxTotal.currency}. A reservation is denominated in the grant's currency, and ` +
202
+ "converting implicitly is not something this runtime does (ADR-0044).");
203
+ }
204
+ return undefined;
205
+ }
206
+ /**
207
+ * Whether a grant authorizes an operation (§13.2, §4.2; #155).
208
+ *
209
+ * Checked before reserving, so handing the wrong grant to an execution gateway cannot authorize
210
+ * unrelated work. Without it a grant is a pool of money with no statement about what it is for,
211
+ * and the only thing separating agent spend from a synthesis ledger is which decision each
212
+ * happened to name.
213
+ */
214
+ export function checkSpendScope(grant, operation) {
215
+ if (grant.scope.operations.includes(operation))
216
+ return undefined;
217
+ return {
218
+ operation,
219
+ authorized: [...grant.scope.operations],
220
+ explanation: `Grant "${grant.grantId}" authorizes ${grant.scope.operations.map((entry) => `"${entry}"`).join(", ") || "nothing"} ` +
221
+ `and this operation is "${operation}". A grant states what it may be spent on as well as ` +
222
+ "how much, so passing the wrong one to a gateway cannot authorize unrelated work (§13.2).",
94
223
  };
95
224
  }
96
225
  /**
@@ -116,6 +245,23 @@ export function checkSpend(grant, costs, request) {
116
245
  "cost record, not as a negative draw, so that the audit trail keeps the original charge.",
117
246
  };
118
247
  }
248
+ // Before the arithmetic, because the arithmetic is what cannot be trusted. While a charge of
249
+ // unknown size stands against this grant, `remaining` is the total over what is *known* — and
250
+ // spending against it would treat an unresolved charge as a zero draw, which is the one thing
251
+ // the ruling on #150 forbids (§19.3).
252
+ if (!ledger.remainingIsDeterminate) {
253
+ return {
254
+ allowed: false,
255
+ reason: "billing-unconfirmed",
256
+ ledger,
257
+ explanation: `${ledger.unresolvedUnknown.length} charge(s) against this authorization have an ` +
258
+ "unconfirmed amount, so the remaining budget is indeterminate rather than " +
259
+ `${formatMoney(ledger.remaining)}. Automatic spend is refused until the amount is ` +
260
+ "reconciled or an operator issues a new authorization: an unknown charge is neither free " +
261
+ "nor zero, and drawing against a figure nobody can establish is how a ceiling is exceeded " +
262
+ "without any single decision being wrong (§19.3).",
263
+ };
264
+ }
119
265
  if (ledger.overspent) {
120
266
  return {
121
267
  allowed: false,
package/dist/spend.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"spend.js","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spend.js","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aldus-runtime/gate-engine",
3
- "version": "0.2.0-next.2",
3
+ "version": "0.2.0-next.21",
4
4
  "type": "module",
5
5
  "description": "Aldus Gate Engine — hash-bound human gates, cascading invalidation, and spend authorization.",
6
6
  "license": "Apache-2.0",
@@ -33,10 +33,10 @@
33
33
  "typecheck:test": "tsc -p tsconfig.test.json"
34
34
  },
35
35
  "dependencies": {
36
- "@aldus-runtime/core": "0.2.0-next.2"
36
+ "@aldus-runtime/core": "0.2.0-next.21"
37
37
  },
38
38
  "devDependencies": {
39
- "@aldus-runtime/testkit": "0.2.0-next.2",
39
+ "@aldus-runtime/testkit": "0.2.0-next.21",
40
40
  "@types/node": "^26.2.0",
41
41
  "typescript": "^7.0.2",
42
42
  "vitest": "^4.1.10"
package/src/definition.ts CHANGED
@@ -15,6 +15,14 @@
15
15
  */
16
16
 
17
17
  import type { ActorKind } from "@aldus-runtime/core";
18
+ import {
19
+ QUALITY_ENFORCEMENTS,
20
+ QUALITY_LEVELS,
21
+ validateQualityClaim,
22
+ type PromotionEvidence,
23
+ type QualityEnforcement,
24
+ type QualityLevel,
25
+ } from "@aldus-runtime/core";
18
26
 
19
27
  import { GateEngineErrorCodes, gateEngineError } from "./errors.js";
20
28
 
@@ -26,32 +34,29 @@ import { GateEngineErrorCodes, gateEngineError } from "./errors.js";
26
34
  * does not, and a model-assisted review may do either depending on whether it has been
27
35
  * calibrated (§12.1).
28
36
  */
29
- export const GATE_LEVELS = [
30
- /** Blocks on an objectively testable failure (§12 level 1). */
31
- "hard_gate",
32
- /** Reports a possible issue without blocking (§12 level 2). */
33
- "advisory_signal",
34
- /** Evaluates meaning, stance, style, or claims under uncertainty (§12 level 3). */
35
- "model_assisted",
36
- /** A human owns the judgement, because it is subjective or asymmetric-risk (§12 level 4). */
37
- "human_oracle",
38
- ] as const;
37
+ /**
38
+ * @deprecated in name only the concept moved to Core (#115) and this is the gate-shaped alias.
39
+ *
40
+ * §12's levels are a property of any quality mechanism, not of gates. They lived here alone until
41
+ * an adopter's blocking evaluator turned out to be a Stage, which could declare none of this. The
42
+ * canonical definition is `QUALITY_LEVELS`; this name is kept because published code consumes it.
43
+ */
44
+ export const GATE_LEVELS = QUALITY_LEVELS;
39
45
 
40
46
  /** @see GATE_LEVELS */
41
- export type GateLevel = (typeof GATE_LEVELS)[number];
47
+ export type GateLevel = QualityLevel;
42
48
 
43
49
  /**
44
- * Whether a gate stops work or merely reports.
50
+ * Whether a gate stops work or merely reports. The gate-shaped alias of `QUALITY_ENFORCEMENTS`.
45
51
  *
46
- * Deliberately a two-state enumeration rather than a boolean. Contract §12.1 permits an
47
- * evaluator to *become* blocking only after calibration, which makes this a promotion with
48
- * evidence behind it — and a field named `blocking: boolean` invites someone to flip it in a
49
- * config file without producing any.
52
+ * Still a two-state enumeration rather than a boolean, for the reason Core records: §12.1 makes
53
+ * blocking a promotion with evidence behind it, and `blocking: boolean` invites someone to flip it
54
+ * in a config file without producing any.
50
55
  */
51
- export const GATE_ENFORCEMENTS = ["blocking", "advisory"] as const;
56
+ export const GATE_ENFORCEMENTS = QUALITY_ENFORCEMENTS;
52
57
 
53
58
  /** @see GATE_ENFORCEMENTS */
54
- export type GateEnforcement = (typeof GATE_ENFORCEMENTS)[number];
59
+ export type GateEnforcement = QualityEnforcement;
55
60
 
56
61
  /**
57
62
  * Evidence that a model-assisted evaluator was calibrated before it was allowed to block.
@@ -65,14 +70,7 @@ export type GateEnforcement = (typeof GATE_ENFORCEMENTS)[number];
65
70
  * scope among what promotion must consider, because an evaluator calibrated on one host says
66
71
  * nothing about another. Dimensions are caller-supplied (§4.2), consistent with WP-09's packs.
67
72
  */
68
- export interface PromotionEvidence {
69
- /** Identifier of the calibration report that justified promotion (WP-10). */
70
- reportRef: string;
71
- /** Scope the calibration covers, e.g. `{ host: "example-host", voice: "voice-a" }`. */
72
- scope: Record<string, string>;
73
- /** Known blind spots recorded at promotion time (§12.1, §9.3). */
74
- knownBlindSpots?: string[];
75
- }
73
+ export type { PromotionEvidence };
76
74
 
77
75
  /**
78
76
  * One configured gate.
@@ -196,15 +194,15 @@ export function validateGateDefinition(definition: GateDefinition): ResolvedGate
196
194
  fail("A gate cannot depend on itself.");
197
195
  }
198
196
 
199
- if (definition.level === "model_assisted" && definition.enforcement === "blocking") {
200
- if (definition.promotionEvidence === undefined) {
201
- fail(
202
- "A model-assisted gate may only block once it has been calibrated against human-labeled " +
203
- "examples (contract §12.1). Set `promotionEvidence`, or leave the gate advisory. " +
204
- "Contract §12 forbids presenting a machine pass as semantic correctness.",
205
- { level: definition.level, enforcement: definition.enforcement },
206
- );
207
- }
197
+ // The shared rule, not a second copy of it (#115). A gate and an evaluator Stage making the same
198
+ // claim are refused for the same reasons in the same words; two implementations of one clause
199
+ // is how §12 came to be enforced in one place and not the other.
200
+ for (const problem of validateQualityClaim({
201
+ level: definition.level,
202
+ enforcement: definition.enforcement,
203
+ promotionEvidence: definition.promotionEvidence,
204
+ })) {
205
+ fail(problem.message, { level: definition.level, enforcement: definition.enforcement });
208
206
  }
209
207
 
210
208
  const permittedActorKinds =
package/src/engine.ts CHANGED
@@ -35,7 +35,7 @@ import { GateEngineErrorCodes, gateEngineError } from "./errors.js";
35
35
  import type { CostReader, GateDecisionStore, GateEventSink } from "./ports.js";
36
36
  import {
37
37
  checkSpend,
38
- grantLimitsDigest,
38
+ grantTermsDigest,
39
39
  type SpendCheck,
40
40
  type SpendGrant,
41
41
  type SpendRequest,
@@ -73,6 +73,13 @@ export const GATE_STATES = [
73
73
  export type GateState = (typeof GATE_STATES)[number];
74
74
 
75
75
  /** The evaluated state of one gate. */
76
+ /** `"a"`, `"a and b"`, `"a, b and c"` — for a sentence an operator reads, not a log line. */
77
+ function formatKeys(keys: readonly string[]): string {
78
+ const quoted = keys.map((key) => `"${key}"`);
79
+ if (quoted.length <= 1) return quoted[0] ?? "";
80
+ return `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1] as string}`;
81
+ }
82
+
76
83
  export interface GateStatus {
77
84
  gateId: string;
78
85
  state: GateState;
@@ -86,6 +93,17 @@ export interface GateStatus {
86
93
  drift?: SubjectDrift;
87
94
  /** Gates that blocked this one, when `state` is `blocked_upstream`. */
88
95
  blockedBy?: string[];
96
+ /**
97
+ * Bound values nobody has supplied yet, when the gate is otherwise decidable (#91).
98
+ *
99
+ * §13.2 requires an authorization to bind every listed value, so a gate whose subjects are
100
+ * only partly supplied cannot be approved — `assertSubjectsCover` refuses it. Reported here so
101
+ * that a caller can say why before offering the decision, rather than recommending a command
102
+ * that will be rejected.
103
+ *
104
+ * Absent when every bound value is present, which is the ordinary case.
105
+ */
106
+ missingSubjects?: string[];
89
107
  /** Operator-facing explanation of why work may not proceed past this gate. */
90
108
  explanation?: string;
91
109
  /**
@@ -314,12 +332,24 @@ export class GateEngine {
314
332
  // machines with disagreeing clocks must not be able to reorder which approval is current.
315
333
  const latest = [...decisions].reverse().find((entry) => entry.gateId === gate.gateId);
316
334
 
335
+ const suppliedKeys = new Set(subjects.map((entry) => entry.key));
336
+ const missingSubjects = gate.binds.filter((key) => !suppliedKeys.has(key));
337
+
317
338
  if (latest === undefined) {
318
339
  return {
319
340
  ...base,
320
341
  state: "pending",
321
342
  blocking: blocks("pending"),
322
- explanation: `Gate "${gate.gateId}" has no recorded decision.`,
343
+ // Naming the missing values rather than only the absence of a decision: an operator told
344
+ // "no recorded decision" goes looking for who forgot to approve, when the answer is that
345
+ // nothing has produced what the approval would bind (§13.2).
346
+ explanation:
347
+ missingSubjects.length > 0
348
+ ? `Gate "${gate.gateId}" has no recorded decision, and ` +
349
+ `${formatKeys(missingSubjects)} ${missingSubjects.length === 1 ? "has" : "have"} ` +
350
+ "not been supplied, so it cannot be decided yet."
351
+ : `Gate "${gate.gateId}" has no recorded decision.`,
352
+ ...(missingSubjects.length > 0 ? { missingSubjects } : {}),
323
353
  };
324
354
  }
325
355
 
@@ -521,7 +551,7 @@ export class GateEngine {
521
551
  };
522
552
  }
523
553
 
524
- if (!decision.subjectHashes.includes(grantLimitsDigest(grant))) {
554
+ if (!decision.subjectHashes.includes(grantTermsDigest(grant))) {
525
555
  return {
526
556
  authorized: false,
527
557
  statuses: [status],
package/src/index.ts CHANGED
@@ -66,11 +66,16 @@ export {
66
66
 
67
67
  export {
68
68
  SPEND_LIMIT_SUBJECT_KEY,
69
+ availableAuthorization,
70
+ checkSpendScope,
71
+ unestimatedPolicyIsSatisfiable,
69
72
  checkSpend,
70
73
  computeLedger,
71
74
  consumesBudget,
72
75
  costRecordDraw,
73
- grantLimitsDigest,
76
+ grantTermsDigest,
77
+ type SpendAvailability,
78
+ type SpendScopeRefusal,
74
79
  type SpendCheck,
75
80
  type SpendGrant,
76
81
  type SpendLedger,