@zhin.js/harness 0.0.0-stage → 0.0.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/lib/authority.d.ts +37 -0
  4. package/lib/authority.js +161 -0
  5. package/lib/budget.d.ts +48 -0
  6. package/lib/budget.js +154 -0
  7. package/lib/contracts.d.ts +643 -0
  8. package/lib/contracts.js +86 -0
  9. package/lib/guard.d.ts +25 -0
  10. package/lib/guard.js +213 -0
  11. package/lib/harness.d.ts +197 -0
  12. package/lib/harness.js +731 -0
  13. package/lib/index.d.ts +8 -0
  14. package/lib/index.js +8 -0
  15. package/lib/journal.d.ts +86 -0
  16. package/lib/journal.js +712 -0
  17. package/lib/mailbox.d.ts +6 -0
  18. package/lib/mailbox.js +422 -0
  19. package/lib/policies/admission.d.ts +48 -0
  20. package/lib/policies/admission.js +118 -0
  21. package/lib/policies/groups.d.ts +47 -0
  22. package/lib/policies/groups.js +91 -0
  23. package/lib/policies/index.d.ts +5 -0
  24. package/lib/policies/index.js +5 -0
  25. package/lib/policies/live.d.ts +28 -0
  26. package/lib/policies/live.js +44 -0
  27. package/lib/policies/markers.d.ts +23 -0
  28. package/lib/policies/markers.js +42 -0
  29. package/lib/policies/merge.d.ts +49 -0
  30. package/lib/policies/merge.js +101 -0
  31. package/package.json +39 -3
  32. package/src/authority.ts +194 -0
  33. package/src/budget.ts +181 -0
  34. package/src/contracts.ts +762 -0
  35. package/src/guard.ts +305 -0
  36. package/src/harness.ts +975 -0
  37. package/src/index.ts +8 -0
  38. package/src/journal.ts +833 -0
  39. package/src/mailbox.ts +467 -0
  40. package/src/policies/admission.ts +164 -0
  41. package/src/policies/groups.ts +137 -0
  42. package/src/policies/index.ts +5 -0
  43. package/src/policies/live.ts +68 -0
  44. package/src/policies/markers.ts +58 -0
  45. package/src/policies/merge.ts +121 -0
package/src/budget.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * In-memory conserved budget ledger (ADR 0048 §3.4 invariant 2).
3
+ *
4
+ * Semantics, per budget key:
5
+ * - consumed(g) = g.ownSpent + Σ rolledUp(closed child), where
6
+ * rolledUp(c) = min(consumed(c), granted(c)).
7
+ * Overspend beyond a grant stays visible on that grant's own
8
+ * snapshot but never inflates the parent's accounting, so
9
+ * a subtree can never consume more than it was granted
10
+ * from above (conservation).
11
+ * - reserved(g) = Σ granted(c) over OPEN children c.
12
+ * - remaining(g) = granted(g) - consumed(g) - reserved(g) (may go negative
13
+ * after an overspending charge).
14
+ * - Snapshot: `spent` = consumed(g) (own + closed children), `reservedForChildren`
15
+ * = reserved(g), `remaining` as above.
16
+ *
17
+ * grant(): requested keys above the parent's remaining fail whole with
18
+ * `budget_exceeded` (no partial grant, no clamping). Omitted keys default to
19
+ * floor(parent remaining / 2), never below 0. The child's granted amount is
20
+ * reserved on the parent immediately.
21
+ *
22
+ * charge(): always records the usage on an open grant; reports
23
+ * `budget_exceeded` (with the exhausted keys) when any remaining < 0.
24
+ *
25
+ * close(): closes open descendants first (deepest first), then the grant.
26
+ * The parent's reservation is released and the child's consumed amount is
27
+ * rolled up, i.e. unspent returns to the parent. Idempotent.
28
+ */
29
+ import {
30
+ BUDGET_KEYS,
31
+ type BudgetAmounts,
32
+ type BudgetKey,
33
+ type BudgetLedger,
34
+ type BudgetResult,
35
+ type BudgetSnapshot,
36
+ type GrantId,
37
+ } from './contracts.js';
38
+
39
+ type Mutable = { -readonly [K in BudgetKey]: number };
40
+
41
+ interface Node {
42
+ readonly id: GrantId;
43
+ readonly parent?: Node;
44
+ readonly granted: BudgetAmounts;
45
+ readonly ownSpent: Mutable;
46
+ readonly rolledUp: Mutable;
47
+ readonly children: Node[];
48
+ closed: boolean;
49
+ }
50
+
51
+ type FailCode = 'budget_exceeded' | 'grant_closed' | 'unknown_grant' | 'invalid_amount';
52
+
53
+ const zero = (): Mutable => ({ modelCalls: 0, tokens: 0, wallMs: 0, sideEffects: 0 });
54
+ const valid = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 0;
55
+
56
+ export interface InMemoryBudgetLedgerOptions {
57
+ readonly idFactory?: () => GrantId;
58
+ }
59
+
60
+ export class InMemoryBudgetLedger implements BudgetLedger {
61
+ private readonly nodes = new Map<GrantId, Node>();
62
+ private readonly idFactory: () => GrantId;
63
+
64
+ constructor(options: InMemoryBudgetLedgerOptions = {}) {
65
+ let n = 0;
66
+ this.idFactory = options.idFactory ?? (() => `g${++n}`);
67
+ }
68
+
69
+ openRoot(amounts: BudgetAmounts): GrantId {
70
+ for (const k of BUDGET_KEYS) {
71
+ if (!valid(amounts?.[k])) throw new TypeError(`invalid budget amount for ${k}: ${String(amounts?.[k])}`);
72
+ }
73
+ return this.create(undefined, { ...amounts }).id;
74
+ }
75
+
76
+ grant(parentId: GrantId, request: Partial<BudgetAmounts>): BudgetResult<GrantId> {
77
+ const parent = this.nodes.get(parentId);
78
+ if (!parent) return fail('unknown_grant', `unknown grant ${parentId}`);
79
+ if (parent.closed) return fail('grant_closed', `grant ${parentId} is closed`);
80
+ const bad = invalidKey(request);
81
+ if (bad) return fail('invalid_amount', `invalid amount for ${bad}`);
82
+ const rem = this.remaining(parent);
83
+ const exhausted = BUDGET_KEYS.filter((k) => request[k] !== undefined && (request[k] as number) > rem[k]);
84
+ if (exhausted.length > 0) {
85
+ return fail('budget_exceeded', `requested more than parent remaining for: ${exhausted.join(', ')}`, exhausted);
86
+ }
87
+ const amounts = zero();
88
+ for (const k of BUDGET_KEYS) amounts[k] = request[k] ?? Math.max(0, Math.floor(rem[k] / 2));
89
+ return { ok: true, value: this.create(parent, amounts).id };
90
+ }
91
+
92
+ charge(grantId: GrantId, usage: Partial<BudgetAmounts>): BudgetResult<BudgetSnapshot> {
93
+ const node = this.nodes.get(grantId);
94
+ if (!node) return fail('unknown_grant', `unknown grant ${grantId}`);
95
+ const bad = invalidKey(usage);
96
+ if (bad) return fail('invalid_amount', `invalid amount for ${bad}`);
97
+ if (node.closed) return fail('grant_closed', `grant ${grantId} is closed`);
98
+ for (const k of BUDGET_KEYS) node.ownSpent[k] += usage[k] ?? 0;
99
+ const rem = this.remaining(node);
100
+ const exhausted = BUDGET_KEYS.filter((k) => rem[k] < 0);
101
+ if (exhausted.length > 0) {
102
+ return fail('budget_exceeded', `budget exceeded for: ${exhausted.join(', ')}`, exhausted);
103
+ }
104
+ return { ok: true, value: this.snap(node) };
105
+ }
106
+
107
+ close(grantId: GrantId): BudgetResult<BudgetSnapshot> {
108
+ const node = this.nodes.get(grantId);
109
+ if (!node) return fail('unknown_grant', `unknown grant ${grantId}`);
110
+ this.closeNode(node);
111
+ return { ok: true, value: this.snap(node) };
112
+ }
113
+
114
+ snapshot(grantId: GrantId): BudgetSnapshot | undefined {
115
+ const node = this.nodes.get(grantId);
116
+ return node ? this.snap(node) : undefined;
117
+ }
118
+
119
+ private create(parent: Node | undefined, granted: BudgetAmounts): Node {
120
+ const id = this.idFactory();
121
+ const node: Node = { id, parent, granted, ownSpent: zero(), rolledUp: zero(), children: [], closed: false };
122
+ this.nodes.set(id, node);
123
+ parent?.children.push(node);
124
+ return node;
125
+ }
126
+
127
+ private closeNode(node: Node): void {
128
+ if (node.closed) return;
129
+ for (const child of node.children) this.closeNode(child);
130
+ node.closed = true;
131
+ if (node.parent) {
132
+ const consumed = this.consumed(node);
133
+ for (const k of BUDGET_KEYS) node.parent.rolledUp[k] += Math.min(consumed[k], node.granted[k]);
134
+ }
135
+ }
136
+
137
+ private consumed(node: Node): Mutable {
138
+ const out = zero();
139
+ for (const k of BUDGET_KEYS) out[k] = node.ownSpent[k] + node.rolledUp[k];
140
+ return out;
141
+ }
142
+
143
+ private reserved(node: Node): Mutable {
144
+ const out = zero();
145
+ for (const c of node.children) if (!c.closed) for (const k of BUDGET_KEYS) out[k] += c.granted[k];
146
+ return out;
147
+ }
148
+
149
+ private remaining(node: Node): Mutable {
150
+ const c = this.consumed(node);
151
+ const r = this.reserved(node);
152
+ const out = zero();
153
+ for (const k of BUDGET_KEYS) out[k] = node.granted[k] - c[k] - r[k];
154
+ return out;
155
+ }
156
+
157
+ private snap(node: Node): BudgetSnapshot {
158
+ return Object.freeze({
159
+ grantId: node.id,
160
+ ...(node.parent ? { parentId: node.parent.id } : {}),
161
+ granted: Object.freeze({ ...node.granted }),
162
+ spent: Object.freeze(this.consumed(node)),
163
+ reservedForChildren: Object.freeze(this.reserved(node)),
164
+ remaining: Object.freeze(this.remaining(node)),
165
+ closed: node.closed,
166
+ });
167
+ }
168
+ }
169
+
170
+ function invalidKey(amounts: Partial<BudgetAmounts>): BudgetKey | undefined {
171
+ if (amounts === null || typeof amounts !== 'object') return BUDGET_KEYS[0];
172
+ return BUDGET_KEYS.find((k) => amounts[k] !== undefined && !valid(amounts[k]));
173
+ }
174
+
175
+ function fail(
176
+ code: FailCode,
177
+ message: string,
178
+ exhausted?: readonly BudgetKey[],
179
+ ): { readonly ok: false; readonly code: FailCode; readonly exhausted?: readonly BudgetKey[]; readonly message: string } {
180
+ return exhausted ? { ok: false, code, exhausted, message } : { ok: false, code, message };
181
+ }