@affiant/core 0.1.0-alpha.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 (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +354 -0
  3. package/dist/context.d.ts +136 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +30 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/docket/entry.d.ts +421 -0
  8. package/dist/docket/entry.d.ts.map +1 -0
  9. package/dist/docket/entry.js +155 -0
  10. package/dist/docket/entry.js.map +1 -0
  11. package/dist/docket/expiry.d.ts +82 -0
  12. package/dist/docket/expiry.d.ts.map +1 -0
  13. package/dist/docket/expiry.js +106 -0
  14. package/dist/docket/expiry.js.map +1 -0
  15. package/dist/docket/memory.d.ts +163 -0
  16. package/dist/docket/memory.d.ts.map +1 -0
  17. package/dist/docket/memory.js +528 -0
  18. package/dist/docket/memory.js.map +1 -0
  19. package/dist/docket/store.d.ts +387 -0
  20. package/dist/docket/store.d.ts.map +1 -0
  21. package/dist/docket/store.js +51 -0
  22. package/dist/docket/store.js.map +1 -0
  23. package/dist/errors.d.ts +153 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +164 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/gate/coverage.d.ts +152 -0
  28. package/dist/gate/coverage.d.ts.map +1 -0
  29. package/dist/gate/coverage.js +114 -0
  30. package/dist/gate/coverage.js.map +1 -0
  31. package/dist/gate/decide.d.ts +207 -0
  32. package/dist/gate/decide.d.ts.map +1 -0
  33. package/dist/gate/decide.js +559 -0
  34. package/dist/gate/decide.js.map +1 -0
  35. package/dist/gate/gate.d.ts +212 -0
  36. package/dist/gate/gate.d.ts.map +1 -0
  37. package/dist/gate/gate.js +175 -0
  38. package/dist/gate/gate.js.map +1 -0
  39. package/dist/gate/pipeline.d.ts +285 -0
  40. package/dist/gate/pipeline.d.ts.map +1 -0
  41. package/dist/gate/pipeline.js +515 -0
  42. package/dist/gate/pipeline.js.map +1 -0
  43. package/dist/gate/policy.d.ts +272 -0
  44. package/dist/gate/policy.d.ts.map +1 -0
  45. package/dist/gate/policy.js +396 -0
  46. package/dist/gate/policy.js.map +1 -0
  47. package/dist/gate/wrap.d.ts +107 -0
  48. package/dist/gate/wrap.d.ts.map +1 -0
  49. package/dist/gate/wrap.js +164 -0
  50. package/dist/gate/wrap.js.map +1 -0
  51. package/dist/index.d.ts +95 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +111 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/model/affidavit.d.ts +354 -0
  56. package/dist/model/affidavit.d.ts.map +1 -0
  57. package/dist/model/affidavit.js +417 -0
  58. package/dist/model/affidavit.js.map +1 -0
  59. package/dist/model/amendments.d.ts +160 -0
  60. package/dist/model/amendments.d.ts.map +1 -0
  61. package/dist/model/amendments.js +183 -0
  62. package/dist/model/amendments.js.map +1 -0
  63. package/dist/model/canonical.d.ts +311 -0
  64. package/dist/model/canonical.d.ts.map +1 -0
  65. package/dist/model/canonical.js +665 -0
  66. package/dist/model/canonical.js.map +1 -0
  67. package/dist/model/money.d.ts +127 -0
  68. package/dist/model/money.d.ts.map +1 -0
  69. package/dist/model/money.js +177 -0
  70. package/dist/model/money.js.map +1 -0
  71. package/dist/model/provenance.d.ts +315 -0
  72. package/dist/model/provenance.d.ts.map +1 -0
  73. package/dist/model/provenance.js +223 -0
  74. package/dist/model/provenance.js.map +1 -0
  75. package/dist/ports.d.ts +269 -0
  76. package/dist/ports.d.ts.map +1 -0
  77. package/dist/ports.js +34 -0
  78. package/dist/ports.js.map +1 -0
  79. package/dist/store-memory.d.ts +21 -0
  80. package/dist/store-memory.d.ts.map +1 -0
  81. package/dist/store-memory.js +20 -0
  82. package/dist/store-memory.js.map +1 -0
  83. package/dist/telemetry-keys.d.ts +65 -0
  84. package/dist/telemetry-keys.d.ts.map +1 -0
  85. package/dist/telemetry-keys.js +72 -0
  86. package/dist/telemetry-keys.js.map +1 -0
  87. package/dist/telemetry.d.ts +77 -0
  88. package/dist/telemetry.d.ts.map +1 -0
  89. package/dist/telemetry.js +43 -0
  90. package/dist/telemetry.js.map +1 -0
  91. package/dist/testing.d.ts +574 -0
  92. package/dist/testing.d.ts.map +1 -0
  93. package/dist/testing.js +1291 -0
  94. package/dist/testing.js.map +1 -0
  95. package/package.json +75 -0
  96. package/telemetry-keys.json +92 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * The Docket store contract — what a store must do, stated so that the in-memory
3
+ * reference store and a production store are held to the same fixtures.
4
+ *
5
+ * **Rules served: DK-1** (idempotent filing, the guarded compare-and-set, expiry as
6
+ * queryable state, preserved amendments on a late decision, lineage), **DK-3** (a
7
+ * bounded, paged, host-scheduled sweep), **DK-4** (retention, purge and export are
8
+ * hooks the host drives, and a row reads forward), **DK-5** (rehydration order),
9
+ * **AZ-2** (every operation is tenant-scoped), **RT-2** (no unbounded operation).
10
+ *
11
+ * Three properties are worth reading the interface for.
12
+ *
13
+ * **Every operation is scoped.** There is no method that reaches a row by id alone.
14
+ * An entry id is unique *within a tenant*, and a lookup with the wrong tenant is not
15
+ * an error — it is a miss, indistinguishable from an id that does not exist.
16
+ * Anything else leaks the existence of another tenant's rows to whoever can guess an
17
+ * id, and AZ-2 exists because that check is the one hosts hand-roll and get wrong.
18
+ *
19
+ * **Every read applies expiry.** `get`, `listPending` and `rehydrate` report what a
20
+ * row *reads* as, not what it says: a `pending` entry past its deadline reads
21
+ * `expired` whether or not the host's sweep has run (DK-1). A store therefore needs
22
+ * to know the time, which is why it is built with a {@link Clock} rather than being
23
+ * handed an instant at every call — a store that took `now` as a parameter would let
24
+ * a caller answer the deadline question for it.
25
+ *
26
+ * **Every list is bounded.** No method returns "all of them". Lists are paged with
27
+ * an opaque cursor; the sweep and the retention hook take a limit and report whether
28
+ * more remain; export is an `AsyncIterable`. RT-2's envelope — a serverless isolate
29
+ * with no persistent process — has no room for a method that loads a tenant's
30
+ * Docket into memory.
31
+ *
32
+ * *Three divergences from the v0.1 design record, recorded deliberately.* The
33
+ * design's sketch of `transition` takes `(entryId, expected, patch)` and returns
34
+ * `entry | "lost-race" | "not-pending"`. First, every other method on the interface
35
+ * is scoped, and an unscoped transition would be the one door in the store through
36
+ * which a caller could move another tenant's row by guessing an id — exactly what
37
+ * AZ-2 closes; the signature here takes a {@link Scope} in the same position `get`
38
+ * does. Second, the sketch has no way to say *no such entry*: reporting a missing id
39
+ * as "not pending" would make a caller turn it into `decision-expired`, which is a
40
+ * different and misleading refusal, so {@link TransitionResult} carries a
41
+ * `"not-found"` arm as well. Third, the sketch's two refusal names describe the
42
+ * wrong halves: the arm it spells `"not-pending"` is the *expired* row, while the
43
+ * arm it spells `"lost-race"` is the row somebody else already decided — and a store
44
+ * implementer writing `UPDATE … WHERE status = 'pending'` will read "not pending" as
45
+ * the union of both. The names here say which is which: `"expired"` and
46
+ * `"already-decided"`.
47
+ *
48
+ * @packageDocumentation
49
+ */
50
+ import type { Affidavit } from "../model/affidavit.js";
51
+ import type { AmendmentMap } from "../model/amendments.js";
52
+ import type { Attestation, DecisionRecord, DocketEntry, DocketStatus, ExecutionOutcome, PreservedAmendments } from "./entry.js";
53
+ /**
54
+ * What a store operation is allowed to see.
55
+ *
56
+ * `tenantId` is mandatory: there is no cross-tenant read. `conversationId` narrows
57
+ * further, which is what a session surface asks for when it rehydrates one
58
+ * conversation rather than a whole tenant's Docket.
59
+ */
60
+ export interface Scope {
61
+ /** The tenant. Mandatory — nothing in this interface reads across tenants. */
62
+ readonly tenantId: string;
63
+ /** One conversation within the tenant, when the caller wants only that (GT-2). */
64
+ readonly conversationId?: string;
65
+ }
66
+ /**
67
+ * Where to continue and how much to take.
68
+ *
69
+ * `cursor` is **opaque**: it is produced by a store, understood only by the same
70
+ * store, and bound to the list that produced it — feeding a `listPending` cursor to
71
+ * `export` is a caller error, not a silently different answer.
72
+ */
73
+ export interface Page {
74
+ /** The `cursor` from the previous {@link PageResult}, or absent/`null` to start. */
75
+ readonly cursor?: string | null;
76
+ /** How many entries to return at most. */
77
+ readonly limit: number;
78
+ }
79
+ /** One page of a list. */
80
+ export interface PageResult<T> {
81
+ /** The entries in this page, in filing order. */
82
+ readonly items: readonly T[];
83
+ /** Pass back as {@link Page.cursor} to continue, or `null` when the list is drained. */
84
+ readonly cursor: string | null;
85
+ /** Whether another page exists. `false` when `cursor` is `null`. */
86
+ readonly more: boolean;
87
+ }
88
+ /**
89
+ * What the host's retention job is allowed to remove (DK-4).
90
+ *
91
+ * Retention is a hook rather than a policy the framework holds an opinion about:
92
+ * how long an approval record must be kept is a legal question with a different
93
+ * answer in every jurisdiction the gate runs in, and a default would be wrong
94
+ * somewhere important.
95
+ */
96
+ export interface RetentionPolicy {
97
+ /**
98
+ * Remove terminal entries whose terminal instant is strictly before this ISO 8601
99
+ * instant.
100
+ */
101
+ readonly olderThan: string;
102
+ }
103
+ /**
104
+ * What a transition out of `pending` writes.
105
+ *
106
+ * The patch is exactly the set of later facts a decision produces, and nothing
107
+ * else: it cannot touch the requirement, the deadline, the filing instant or the
108
+ * entry's own id. That is DK-4's read-forward property in the type — a row
109
+ * accumulates facts, it is never rewritten.
110
+ *
111
+ * {@link TransitionPatch.amendedAffidavit} is the one apparent exception, and it is
112
+ * not one: it is written *beside* the proposal, never over it. The row keeps
113
+ * `affidavit` as the agent proposed it and gains `amendedAffidavit` as the state the
114
+ * approval accepted, produced by `applyAmendments` from the row's own. That operation
115
+ * adds a `UserStated` tag carrying a `reviewer-act` binding on top of each amended
116
+ * field's chain and leaves the displaced tag in the chain's history (PV-2), so a
117
+ * reader can still see what the machine proposed under what the reviewer decided. A
118
+ * store cannot check that, so the rule is stated here and enforced by the gate, which
119
+ * is the only caller that builds one.
120
+ *
121
+ * `supersedes` is absent on purpose: it is fixed when an entry is filed and names
122
+ * the entry this one *replaces*, which cannot change afterwards. `supersededBy` is
123
+ * here because a caller may want to record a successor in the same write that closes
124
+ * a row, though the ordinary path writes it with
125
+ * {@link DocketStore.recordSupersession} on an already-terminal entry.
126
+ */
127
+ export interface TransitionPatch {
128
+ /** The state the entry moves to. Never back to `pending`. */
129
+ readonly status: Exclude<DocketStatus, "pending">;
130
+ /**
131
+ * The execution outcome. Required to be `"unexecuted"` (the default) when `status`
132
+ * is `"approved"`, and `null` otherwise — a store refuses a patch that contradicts
133
+ * its own status (DK-1).
134
+ */
135
+ readonly execution?: ExecutionOutcome | null;
136
+ /** What the reviewer chose and why. Absent for a sweep, which nobody decided. */
137
+ readonly decision?: DecisionRecord | null;
138
+ /**
139
+ * The amendments the approval accepted. DK-2 holds inside the map: a `null` value
140
+ * clears the field, an absent key leaves it untouched.
141
+ */
142
+ readonly amendments?: AmendmentMap | null;
143
+ /**
144
+ * The accepted state an approval's amendments produced (AF-4).
145
+ *
146
+ * Absent leaves the row carrying its proposal alone, which is what a rejection, a
147
+ * sweep and an unamended approval all want. Present, it must be the result of
148
+ * applying {@link TransitionPatch.amendments} to *this row's* `affidavit` — the
149
+ * three confidence numbers recomputed over the amended fields, and each amended
150
+ * field's provenance the reviewer's act with the machine's tag preserved beneath
151
+ * it. It is written beside `affidavit`, which a store never overwrites (DK-4).
152
+ */
153
+ readonly amendedAffidavit?: Affidavit;
154
+ /** Who agreed (AZ-1). A decision without one is a decision nobody can be held to. */
155
+ readonly attestation?: Attestation | null;
156
+ /** When the row left `pending`. Defaults to the store's clock reading. */
157
+ readonly decidedAt?: string | null;
158
+ /** What the executor reported, when the caller already knows. */
159
+ readonly executionDetail?: string | null;
160
+ /** The successor link, for a caller closing and superseding a row in one write. */
161
+ readonly lineage?: {
162
+ readonly supersededBy?: string | null;
163
+ };
164
+ }
165
+ /**
166
+ * What a guarded compare-and-set returns.
167
+ *
168
+ * The two refusals are distinct because they mean different things to a caller and
169
+ * map to different error codes, and each is named after the state it describes.
170
+ * `"already-decided"` means **someone else decided this entry** — the row is
171
+ * `approved` or `rejected`, a second decision arrived, and DK-1 requires it be
172
+ * refused rather than applied twice or silently overwritten. `"expired"` means **the
173
+ * row passed its deadline**, whether or not a sweep has recorded that yet. A caller
174
+ * turns the first into `decision-lost-race` and the second into `decision-expired`,
175
+ * and the second is the one that must also preserve the amendments the late decision
176
+ * carried ({@link DocketStore.preserveAmendments}).
177
+ *
178
+ * The names are part of the contract, not an implementation detail: the in-memory
179
+ * store is the reference a Postgres store earns the name by passing the same fixtures
180
+ * as, and a name that describes the *other* arm's state — "not pending", which is
181
+ * true of both — is one a second implementer will get backwards.
182
+ *
183
+ * Between them the two cover every state a row can be in that is not `pending`, so
184
+ * a caller never has to re-read to find out which refusal it got. `"not-found"` is
185
+ * the third answer: no entry with that id is visible in the scope — which, for a
186
+ * caller in the wrong tenant, is the only answer they get (AZ-2).
187
+ */
188
+ export type TransitionResult = DocketEntry | "not-found" | "already-decided" | "expired";
189
+ /**
190
+ * The act a refused late decision was made by: its own instant and its own principal
191
+ * (DK-1, PV-2).
192
+ *
193
+ * The same two facts {@link PreservedAmendments} carries, named separately because
194
+ * this is what a caller passes in and that is what the row holds.
195
+ */
196
+ export type PreservedAct = Omit<PreservedAmendments, "amendments">;
197
+ /** What {@link DocketStore.preserveAmendments} returns. */
198
+ export type PreserveAmendmentsResult = DocketEntry | "not-found" | "not-expired";
199
+ /**
200
+ * What {@link DocketStore.recordExecution} returns.
201
+ *
202
+ * `"execution-already-recorded"` is the execution transition's half of DK-1's
203
+ * guarded compare-and-set: the row already carries an outcome, so this report is
204
+ * refused rather than written over the first one. It is a different answer from
205
+ * `"not-approved"` — that row was never approved and has no authorised write behind
206
+ * it at all, while this one is approved and already has its outcome on the record.
207
+ */
208
+ export type RecordExecutionResult = DocketEntry | "not-found" | "not-approved" | "execution-already-recorded";
209
+ /** What {@link DocketStore.recordSupersession} returns. */
210
+ export type RecordSupersessionResult = DocketEntry | "not-found" | "not-terminal";
211
+ /**
212
+ * The durable record of every proposed write and everything that happened to it.
213
+ *
214
+ * A store is the only thing in the framework that remembers, so it is also the only
215
+ * place the DK rules can be enforced rather than merely documented: the
216
+ * idempotent file, the guarded transition, the deadline applied on read, the bounded
217
+ * sweep and the append-only later facts are all properties of *this* interface. A
218
+ * host that swaps the in-memory reference store for Postgres inherits them by
219
+ * passing the same fixtures, not by re-reading the rulebook.
220
+ */
221
+ export interface DocketStore {
222
+ /**
223
+ * File `entry`, or return the one already filed under its id (DK-1).
224
+ *
225
+ * Idempotent by `entryId` within the tenant, and idempotent in the strong sense:
226
+ * a re-file is **never** an error and **never** overwrites. It returns the stored
227
+ * entry as it stands, `created: false`, keeping its **existing** `expiresAt` — a
228
+ * re-file that refreshed the deadline would let a retrying agent hold a card open
229
+ * indefinitely, which is the .NET behaviour GT-4 corrects.
230
+ */
231
+ file(entry: DocketEntry): Promise<{
232
+ entry: DocketEntry;
233
+ created: boolean;
234
+ }>;
235
+ /**
236
+ * The entry, with expiry applied (DK-1), or `null`.
237
+ *
238
+ * `null` covers both "no such id" and "not in this scope". A caller outside the
239
+ * tenant learns nothing about whether the id exists.
240
+ */
241
+ get(entryId: string, scope: Scope): Promise<DocketEntry | null>;
242
+ /**
243
+ * Move an entry out of `pending`, if it is still `pending` (DK-1).
244
+ *
245
+ * A guarded compare-and-set: the read of the current state and the write of the
246
+ * new one happen with no interleaving point between them, so of two decisions that
247
+ * race, exactly one is applied and the other is refused. `expected` is `"pending"`
248
+ * and only `"pending"` — nothing else in the state machine has a transition out of
249
+ * it.
250
+ */
251
+ transition(entryId: string, scope: Scope, expected: "pending", patch: TransitionPatch): Promise<TransitionResult>;
252
+ /**
253
+ * Record the amendments a decision carried after the entry had already expired, so
254
+ * a resubmission can prefill them (DK-1).
255
+ *
256
+ * This is the one write that applies to a row the transition guard refused, and it
257
+ * is a separate method for that reason: folding it into `transition` would make a
258
+ * refused compare-and-set write to the row, and "applied once or not at all" is
259
+ * exactly the property DK-1's fixture asserts. Under DK-4 this is an appended
260
+ * later fact on a terminal row, not an edit of a recorded decision — it touches
261
+ * `amendments` and nothing else, never `status`, `decision` or `attestation`.
262
+ *
263
+ * `act` is the refused decision's **own** instant and principal, not the store's
264
+ * clock and not the row's deadline. A resubmission prefills these values as a
265
+ * person's correction and binds each prefilled field's tag to that act (PV-2), so
266
+ * a record that dated them to the sweep would place the correction at a moment
267
+ * nobody typed anything.
268
+ *
269
+ * Returns `"not-expired"` when the entry does not read `expired`; a caller that
270
+ * gets it has a bug, because a decision that was not refused as expired has no
271
+ * amendments to preserve.
272
+ */
273
+ preserveAmendments(entryId: string, scope: Scope, amendments: AmendmentMap, act: PreservedAct): Promise<PreserveAmendmentsResult>;
274
+ /**
275
+ * Record what the host's executor reported for an approved entry, **once**
276
+ * (DK-1, DK-4).
277
+ *
278
+ * `status` stays `approved`; only `execution` and `executionDetail` move. The
279
+ * framework never performs the write (AZ-7) — it records what the host says
280
+ * happened, so that an approved-but-failed write is distinguishable from an
281
+ * approved-and-committed one on the row.
282
+ *
283
+ * **A guarded compare-and-set, exactly like {@link DocketStore.transition}.**
284
+ * `expected` is `"unexecuted"` and only `"unexecuted"`: the execution transition
285
+ * runs once, out of the state a row is approved in, and a second report is refused
286
+ * with `"execution-already-recorded"` rather than written on top. Without the
287
+ * guard an `executed` row could be flipped to `failed` by a later caller — an edit
288
+ * in place of a recorded fact, which DK-4 forbids, leaving an approved-and-
289
+ * committed write indistinguishable from an approved-but-failed one, which DK-1
290
+ * forbids.
291
+ *
292
+ * The consequence for a host is one sentence: **a host that retries a write
293
+ * reports once, when it knows the outcome.** AZ-5 already says an outbox is a
294
+ * retry of an already-attested write and never a second authorization path; this
295
+ * is the same statement about the record — the retries are the host's business,
296
+ * the outcome is the Docket's.
297
+ *
298
+ * Returns `"not-approved"` for any row that is not `approved`, and
299
+ * `"execution-already-recorded"` for an approved row whose `execution` has already
300
+ * moved off `"unexecuted"`.
301
+ */
302
+ recordExecution(entryId: string, scope: Scope, outcome: Exclude<ExecutionOutcome, "unexecuted">, detail: string | null, expected: "unexecuted"): Promise<RecordExecutionResult>;
303
+ /**
304
+ * Record that a terminal entry has been resubmitted as `supersededBy` (DK-1).
305
+ *
306
+ * The superseded entry keeps its terminal state; only the successor link is added.
307
+ * A resubmission is a new entry, never a reopened one, which is what lets the
308
+ * history read forward (DK-4).
309
+ *
310
+ * Returns `"not-terminal"` for a row that still reads `pending`.
311
+ */
312
+ recordSupersession(entryId: string, scope: Scope, supersededBy: string): Promise<RecordSupersessionResult>;
313
+ /** Entries that read `pending` right now, in filing order, paged. */
314
+ listPending(scope: Scope, page: Page): Promise<PageResult<DocketEntry>>;
315
+ /**
316
+ * Entries that are `approved` and still `unexecuted`, in filing order, paged.
317
+ *
318
+ * The host's executor reads this: an approved write nobody has reported on is work
319
+ * outstanding, and after a restart it is the only record that the work exists.
320
+ */
321
+ listApprovedUnexecuted(scope: Scope, page: Page): Promise<PageResult<DocketEntry>>;
322
+ /**
323
+ * Mark at most `limit` entries that are due at `now` as `expired`, in filing order
324
+ * (DK-3).
325
+ *
326
+ * The host schedules this; the core owns no timer. The sweep makes the state
327
+ * durable and gives the host a list to notify on — it does not *cause* expiry,
328
+ * which every read already applies (DK-1). `more` says whether another call would
329
+ * find more, so a host drains the queue in bounded steps rather than in one
330
+ * unbounded pass.
331
+ */
332
+ expireDue(now: string, scope: Scope, limit: number): Promise<{
333
+ expired: string[];
334
+ more: boolean;
335
+ }>;
336
+ /**
337
+ * Remove at most `limit` terminal entries older than `policy.olderThan` (DK-4).
338
+ *
339
+ * Each call shrinks the eligible set, so a host drains retention by calling until
340
+ * `more` is `false` — the same bounded-step shape as the sweep, and for the same
341
+ * RT-2 reason.
342
+ */
343
+ retention(policy: RetentionPolicy, scope: Scope, limit: number): Promise<{
344
+ removed: number;
345
+ more: boolean;
346
+ }>;
347
+ /**
348
+ * Remove everything belonging to `tenantId` (DK-4).
349
+ *
350
+ * Unbounded by design and by necessity: a tenant asking for their data to be
351
+ * deleted is asking for all of it, and a partial purge is not a purge. It is the
352
+ * one operation that is not paged, and the only one that takes a tenant id rather
353
+ * than a {@link Scope} — there is no such thing as purging half a tenant.
354
+ */
355
+ purge(tenantId: string): Promise<{
356
+ removed: number;
357
+ }>;
358
+ /**
359
+ * Every entry in `scope`, in filing order, streamed (DK-4).
360
+ *
361
+ * An `AsyncIterable` rather than an array so a large Docket never has to fit in
362
+ * memory (RT-2). The portable document shape a host would export *to* is reserved
363
+ * for protocol v0.2; this yields the rows.
364
+ */
365
+ export(scope: Scope): AsyncIterable<DocketEntry>;
366
+ }
367
+ /**
368
+ * What a reconnecting client needs to see, in the order DK-5 fixes.
369
+ *
370
+ * **`pending` entries first, then `approved` and `unexecuted`**, each in filing
371
+ * order. The order is a rule rather than a preference because the two groups ask
372
+ * different things of the person reconnecting: the first still needs a decision, the
373
+ * second still needs execution, and a client that showed them interleaved would put
374
+ * work that is already agreed in front of work that is still blocked on the reader.
375
+ */
376
+ export interface SessionStore {
377
+ /**
378
+ * One page of the rehydration sequence: everything that reads `pending`, then
379
+ * everything `approved` and `unexecuted`, each in filing order.
380
+ *
381
+ * Paged like every other list (RT-2). The cursor carries the position *within the
382
+ * sequence*, so a page boundary that falls between the two groups resumes at the
383
+ * start of the second rather than restarting the first.
384
+ */
385
+ rehydrate(scope: Scope, page: Page): Promise<PageResult<DocketEntry>>;
386
+ }
387
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/docket/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The Docket store contract — what a store must do, stated so that the in-memory
3
+ * reference store and a production store are held to the same fixtures.
4
+ *
5
+ * **Rules served: DK-1** (idempotent filing, the guarded compare-and-set, expiry as
6
+ * queryable state, preserved amendments on a late decision, lineage), **DK-3** (a
7
+ * bounded, paged, host-scheduled sweep), **DK-4** (retention, purge and export are
8
+ * hooks the host drives, and a row reads forward), **DK-5** (rehydration order),
9
+ * **AZ-2** (every operation is tenant-scoped), **RT-2** (no unbounded operation).
10
+ *
11
+ * Three properties are worth reading the interface for.
12
+ *
13
+ * **Every operation is scoped.** There is no method that reaches a row by id alone.
14
+ * An entry id is unique *within a tenant*, and a lookup with the wrong tenant is not
15
+ * an error — it is a miss, indistinguishable from an id that does not exist.
16
+ * Anything else leaks the existence of another tenant's rows to whoever can guess an
17
+ * id, and AZ-2 exists because that check is the one hosts hand-roll and get wrong.
18
+ *
19
+ * **Every read applies expiry.** `get`, `listPending` and `rehydrate` report what a
20
+ * row *reads* as, not what it says: a `pending` entry past its deadline reads
21
+ * `expired` whether or not the host's sweep has run (DK-1). A store therefore needs
22
+ * to know the time, which is why it is built with a {@link Clock} rather than being
23
+ * handed an instant at every call — a store that took `now` as a parameter would let
24
+ * a caller answer the deadline question for it.
25
+ *
26
+ * **Every list is bounded.** No method returns "all of them". Lists are paged with
27
+ * an opaque cursor; the sweep and the retention hook take a limit and report whether
28
+ * more remain; export is an `AsyncIterable`. RT-2's envelope — a serverless isolate
29
+ * with no persistent process — has no room for a method that loads a tenant's
30
+ * Docket into memory.
31
+ *
32
+ * *Three divergences from the v0.1 design record, recorded deliberately.* The
33
+ * design's sketch of `transition` takes `(entryId, expected, patch)` and returns
34
+ * `entry | "lost-race" | "not-pending"`. First, every other method on the interface
35
+ * is scoped, and an unscoped transition would be the one door in the store through
36
+ * which a caller could move another tenant's row by guessing an id — exactly what
37
+ * AZ-2 closes; the signature here takes a {@link Scope} in the same position `get`
38
+ * does. Second, the sketch has no way to say *no such entry*: reporting a missing id
39
+ * as "not pending" would make a caller turn it into `decision-expired`, which is a
40
+ * different and misleading refusal, so {@link TransitionResult} carries a
41
+ * `"not-found"` arm as well. Third, the sketch's two refusal names describe the
42
+ * wrong halves: the arm it spells `"not-pending"` is the *expired* row, while the
43
+ * arm it spells `"lost-race"` is the row somebody else already decided — and a store
44
+ * implementer writing `UPDATE … WHERE status = 'pending'` will read "not pending" as
45
+ * the union of both. The names here say which is which: `"expired"` and
46
+ * `"already-decided"`.
47
+ *
48
+ * @packageDocumentation
49
+ */
50
+ export {};
51
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/docket/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The error the gate throws, and the closed set of codes it throws with.
3
+ *
4
+ * **Rules served: CV-1** (hard-fail at wire-up; there is no disable switch),
5
+ * **AZ-4** (a requirement the implementation does not run is refused, never
6
+ * silently degraded).
7
+ *
8
+ * CV-1 in one sentence: *a misconfiguration the framework can detect fails at
9
+ * wire-up with a stated error, and no option turns the gate off for a tool it
10
+ * covers.* Every such failure is an {@link AffiantError} carrying one
11
+ * {@link ErrorCode} — a machine-readable reason a host can branch on and a
12
+ * conformance fixture can assert, rather than a message string that drifts.
13
+ *
14
+ * The codes are a closed union on purpose. A refusal that cannot be named here is
15
+ * a refusal no fixture can pin, and a host that cannot distinguish "you wired this
16
+ * wrong" from "the reviewer rejected it" will paper over the first.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /**
21
+ * Every reason the gate refuses, keyed by itself.
22
+ *
23
+ * The key and the value are the same string because the string *is* the API: it is
24
+ * what a fixture asserts and what a host branches on, so there is exactly one
25
+ * spelling to remember. Read a code as `ErrorCode["substance-refused"]`, or write
26
+ * the literal — both type-check.
27
+ *
28
+ * Three codes are marked **provisional**: they are named by the v0.1 design but the
29
+ * protocol rulebook does not yet carry an `ErrorCode` registry. When it does, those
30
+ * three are the ones that may be renamed to match it; the other seven describe
31
+ * behaviour the rulebook already fixes.
32
+ */
33
+ export declare const ErrorCode: {
34
+ /**
35
+ * A requirement this implementation recognises but does not run — a `MultiParty`
36
+ * approval, a `ReferralRequired` referral — reached the pipeline. The entry is
37
+ * filed `pending` and marked blocked; every decision on it is refused. Never
38
+ * degraded to a weaker requirement (AZ-4).
39
+ *
40
+ * **Provisional** until the protocol's `ErrorCode` registry lands.
41
+ */
42
+ readonly "requirement-not-implemented": "requirement-not-implemented";
43
+ /**
44
+ * A tool the gate must cover cannot be intercepted — it is write-capable with no
45
+ * `execute` to replace, it is executed by the model provider, or it is a hosted
46
+ * MCP tool. Raised at wire-up (CV-4, CV-1), or carried on a proposal from a tool
47
+ * the host explicitly declared uncovered.
48
+ *
49
+ * **Provisional** until the protocol's `ErrorCode` registry lands.
50
+ */
51
+ readonly "coverage-refused": "coverage-refused";
52
+ /**
53
+ * A proposal reached the substance gate with nothing to swear to: no field
54
+ * carrying provenance other than `Empty`, or a non-empty value sitting under
55
+ * `Empty` provenance. Refused before anything is filed (GT-3).
56
+ */
57
+ readonly "substance-refused": "substance-refused";
58
+ /**
59
+ * A decision was refused on identity grounds: the context carried no resolved
60
+ * principal, the entry belongs to another tenant, or the host's authorization
61
+ * port said no. Refused before the store is touched (AZ-2).
62
+ */
63
+ readonly "decision-unauthorized": "decision-unauthorized";
64
+ /** A decision was made on an entry that is no longer `pending` (DK-1). */
65
+ readonly "decision-not-pending": "decision-not-pending";
66
+ /**
67
+ * A decision was made on an entry that has passed its expiry. The entry reads
68
+ * `expired` whether or not the host's sweep has run; the decision's amendments
69
+ * are preserved on the row for a resubmission (DK-1).
70
+ */
71
+ readonly "decision-expired": "decision-expired";
72
+ /**
73
+ * Two decisions raced for the same entry and this one lost the compare-and-set.
74
+ * A transition is applied once or not at all, never twice (DK-1).
75
+ */
76
+ readonly "decision-lost-race": "decision-lost-race";
77
+ /**
78
+ * The gate was built wrong in a way it can detect: no store, no authorization
79
+ * port, a policy declaring a risk threshold with no scorer to compare against.
80
+ * Thrown from `createGate`, not on the first request (CV-1).
81
+ */
82
+ readonly "wireup-invalid": "wireup-invalid";
83
+ /** No entry with that id is visible in the given scope (DK-1). */
84
+ readonly "entry-not-found": "entry-not-found";
85
+ /**
86
+ * An execution outcome was reported against a row that already carries one. The
87
+ * first report stands and the row is untouched: a decision, once recorded, is
88
+ * never edited in place, and an approved-but-failed write must stay
89
+ * distinguishable from an approved-and-committed one (DK-4, DK-1).
90
+ *
91
+ * A host that retries a write reports **once**, when it knows the outcome (AZ-5:
92
+ * an outbox is a retry of an already-attested write, not a second authorization
93
+ * path, and not a second fact about what happened).
94
+ *
95
+ * **Provisional** until the protocol's `ErrorCode` registry lands.
96
+ */
97
+ readonly "execution-already-recorded": "execution-already-recorded";
98
+ };
99
+ /** One of the reasons in {@link ErrorCode}. */
100
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
101
+ /**
102
+ * Every {@link ErrorCode} value, in registry order. Pinned as data so a runtime
103
+ * check and a fixture can use the same list the type does.
104
+ *
105
+ * **The order only ever grows at the end.** A code is added by appending it, never
106
+ * by inserting one among the codes that already shipped: the list is what a host's
107
+ * exhaustiveness check and a parity manifest read, and a reordering would look like
108
+ * a rename to both.
109
+ */
110
+ export declare const ERROR_CODES: readonly ["requirement-not-implemented", "coverage-refused", "substance-refused", "decision-unauthorized", "decision-not-pending", "decision-expired", "decision-lost-race", "wireup-invalid", "entry-not-found", "execution-already-recorded"];
111
+ /** Whether `value` is one of the codes in {@link ErrorCode}. */
112
+ export declare function isErrorCode(value: unknown): value is ErrorCode;
113
+ /**
114
+ * Structured context attached to an {@link AffiantError}: the entry id, the tool
115
+ * name, the policy that produced a verdict — whatever the throwing site can name.
116
+ *
117
+ * Values are `unknown` because the useful details differ per code. Callers narrow;
118
+ * loggers stringify. Never put a field value or an utterance in here — an error is
119
+ * not an audit record, and the audit record is the Affidavit.
120
+ */
121
+ export interface AffiantErrorDetails {
122
+ readonly [key: string]: unknown;
123
+ }
124
+ /**
125
+ * The error every refusal in this package throws.
126
+ *
127
+ * The `code` is the contract; the `message` is for a human reading a log. A host
128
+ * that branches on the message is doing it wrong, and a fixture that asserts on
129
+ * the message is asserting on prose.
130
+ */
131
+ export declare class AffiantError extends Error {
132
+ /** Why the gate refused. */
133
+ readonly code: ErrorCode;
134
+ /** Structured context for the refusal. `{}` when the throwing site supplied none. */
135
+ readonly details: AffiantErrorDetails;
136
+ /**
137
+ * @param code Why the gate refused.
138
+ * @param message A human-readable explanation. Defaults to the code itself.
139
+ * @param details Structured context for the refusal.
140
+ */
141
+ constructor(code: ErrorCode, message?: string, details?: AffiantErrorDetails);
142
+ }
143
+ /**
144
+ * Whether `value` is an {@link AffiantError}.
145
+ *
146
+ * `instanceof` first, then a structural check: a host can end up with two copies of
147
+ * this package in one process (a bundler, two versions in a dependency tree), and a
148
+ * `catch` that spans that boundary still has to give a true answer. The structural
149
+ * arm is deliberately narrow — an `Error` named `AffiantError` carrying a `code`
150
+ * from {@link ERROR_CODES}.
151
+ */
152
+ export declare function isAffiantError(value: unknown): value is AffiantError;
153
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS;IACpB;;;;;;;OAOG;;IAEH;;;;;;;OAOG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;IAEH,0EAA0E;;IAE1E;;;;OAIG;;IAEH;;;OAGG;;IAEH;;;;OAIG;;IAEH,kEAAkE;;IAElE;;;;;;;;;;;OAWG;;CAEK,CAAC;AAEX,+CAA+C;AAC/C,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAEnE;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,iPAWiB,CAAC;AAE1C,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IAEtC;;;;OAIG;gBACS,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB;CAM7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAOpE"}