@classytic/arc 2.25.0 → 2.28.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 (48) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController--M8EMzt0.mjs → BaseController-B65zbz9m.mjs} +4 -2
  3. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  4. package/dist/cleanup/index.d.mts +313 -0
  5. package/dist/cleanup/index.mjs +1267 -0
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/core/index.d.mts +1 -1
  8. package/dist/core/index.mjs +4 -3
  9. package/dist/core-BGa8TXJR.mjs +221 -0
  10. package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-C-N7z2EL.mjs} +2 -2
  11. package/dist/{preloadResources-DfyngIN9.mjs → createApp-Du5G54TM.mjs} +2 -74
  12. package/dist/{core-EUNu0u07.mjs → defineResource-2zvVVpxg.mjs} +6 -224
  13. package/dist/docs/index.mjs +1 -1
  14. package/dist/events/index.d.mts +28 -2
  15. package/dist/events/index.mjs +20 -4
  16. package/dist/factory/index.d.mts +4 -3
  17. package/dist/factory/index.mjs +3 -1
  18. package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +2 -1
  19. package/dist/index-CZY4EGga.d.mts +107 -0
  20. package/dist/{index-f4PTrk3B.d.mts → index-CjVg-Epg.d.mts} +1 -1
  21. package/dist/index.d.mts +2 -2
  22. package/dist/index.mjs +6 -5
  23. package/dist/integrations/mcp/index.mjs +1 -1
  24. package/dist/integrations/mcp/testing.mjs +1 -1
  25. package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-Cs6nRGCt.mjs} +3 -3
  26. package/dist/module-_-M7C5ES.mjs +29 -0
  27. package/dist/{openapi-DrLtog_B.mjs → openapi-BLV4OMXb.mjs} +1 -1
  28. package/dist/permissions/index.mjs +1 -1
  29. package/dist/{pipeline-BN5G05rd.mjs → pipeline-BGRnV-To.mjs} +1 -1
  30. package/dist/plugins/index.d.mts +2 -2
  31. package/dist/plugins/index.mjs +1 -1
  32. package/dist/plugins/tracing-entry.mjs +1 -1
  33. package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-CM4uUaFv.d.mts} +2 -1
  34. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  35. package/dist/{realtime-Cuw_deA-.mjs → realtime-DEuDu9io.mjs} +1 -1
  36. package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-DTp18Ah_.mjs} +1 -1
  37. package/dist/registry/index.d.mts +1 -1
  38. package/dist/registry/index.mjs +1 -1
  39. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  40. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  41. package/dist/testing/cleanupStoreContract.mjs +375 -0
  42. package/dist/testing/index.d.mts +3 -2
  43. package/dist/testing/index.mjs +4 -2
  44. package/dist/testing/outboxStoreContract.d.mts +13 -0
  45. package/dist/testing/outboxStoreContract.mjs +247 -0
  46. package/dist/types-C_ZZRcD6.d.mts +450 -0
  47. package/dist/{index-KUoxjIq1.d.mts → types-Cu6vVJmh.d.mts} +1 -105
  48. package/package.json +27 -12
@@ -0,0 +1,450 @@
1
+ import { PurgeActor, PurgeEvidence, PurgeResourceResult, PurgeStrategyKind as PurgeStrategyKind$1, PurgeVerificationSummary } from "@classytic/primitives/retention";
2
+ //#region src/cleanup/types.d.ts
3
+ /** Ambient context every recipe method receives. Host-injected — never global. */
4
+ interface CleanupContext {
5
+ /** Who requested the operation. */
6
+ readonly actor: PurgeActor;
7
+ /** Injected clock — recipes never call `new Date()` directly (testability). */
8
+ readonly now: Date;
9
+ /** Cooperative cancellation — checked between chunks/steps. */
10
+ readonly signal?: AbortSignal | undefined;
11
+ /** Optional structured logger. */
12
+ readonly logger?: CleanupLogger | undefined;
13
+ /**
14
+ * Opaque host-provided ambient values (e.g. the resolved company/branch
15
+ * scope). MUST be JSON-serializable — it is persisted on the run so a worker
16
+ * (different process) can rebuild the exact operation context. Recipes read
17
+ * what they declared they need; the framework never inspects it.
18
+ */
19
+ readonly ambient?: Readonly<Record<string, unknown>> | undefined;
20
+ }
21
+ interface CleanupLogger {
22
+ info(msg: string, meta?: Record<string, unknown>): void;
23
+ warn(msg: string, meta?: Record<string, unknown>): void;
24
+ error(msg: string, meta?: Record<string, unknown>): void;
25
+ }
26
+ /**
27
+ * Execution-time context — adds progress reporting + the run/operation ids +
28
+ * cooperative cancellation.
29
+ */
30
+ interface CleanupExecutionContext extends CleanupContext {
31
+ /** Durable run id this execution belongs to. */
32
+ readonly runId: string;
33
+ /** Stable operation id threaded through logs, audit, and evidence. */
34
+ readonly operationId: string;
35
+ /**
36
+ * Report one committed chunk. Updates the run's BOUNDED progress summary
37
+ * (counts / current resource / cursor / heartbeat) — it does NOT retain
38
+ * every step in memory. Also refreshes the cancellation view: after a chunk
39
+ * commits, call this so the executor learns of a cancel request promptly.
40
+ */
41
+ onStep(step: CleanupStepResult): Promise<void>;
42
+ /**
43
+ * Throw `CleanupCancelled` if a cancel has been requested for this run. Cheap
44
+ * to call between chunks; backed by the durable `cancelRequested` flag (the
45
+ * source of truth) plus the in-process `signal`.
46
+ */
47
+ throwIfCancelled(): Promise<void>;
48
+ }
49
+ /** Whether a recipe may run right now, and why not. */
50
+ interface Availability {
51
+ readonly available: boolean;
52
+ /** Required when `available` is false — surfaced to the operator. */
53
+ readonly reason?: string | undefined;
54
+ }
55
+ /** Operator-supplied recipe parameters (branch, module set, before-date, …). */
56
+ interface CleanupInput {
57
+ readonly parameters?: Readonly<Record<string, unknown>> | undefined;
58
+ }
59
+ /** One logical line in the preview — a business record class, never a collection. */
60
+ interface CleanupPlanItem {
61
+ /** Logical module / resource label, e.g. `'orders'`, `'journal entries'`. */
62
+ readonly resource: string;
63
+ /** Estimated records affected. */
64
+ readonly estimated: number;
65
+ /** What is retained for this line (e.g. `'measures kept, PII redacted'`). */
66
+ readonly retained?: string | undefined;
67
+ /** Domain blockers preventing this line (e.g. `'OPEN_TRANSFER'`). */
68
+ readonly blockers?: readonly string[] | undefined;
69
+ }
70
+ /**
71
+ * A recipe's plan BEFORE the framework seals it — the recipe returns this; the
72
+ * service computes {@link CleanupPlan.digest} + stamps {@link CleanupPlan.recipeId}.
73
+ */
74
+ interface CleanupPlanDraft {
75
+ readonly items: readonly CleanupPlanItem[];
76
+ /** Top-level categories the recipe explicitly RETAINS. */
77
+ readonly retains?: readonly string[] | undefined;
78
+ /**
79
+ * Top-level blockers. A NON-EMPTY blockers list is a HARD STOP — the service
80
+ * refuses `execute` with `CLEANUP_BLOCKED`. Preview surfaces them so the
81
+ * operator resolves them (or the recipe stops listing them) before running.
82
+ */
83
+ readonly blockers?: readonly string[] | undefined;
84
+ /** Projection/scaffolding rebuilds the recipe will perform after cleanup. */
85
+ readonly rebuildActions?: readonly string[] | undefined;
86
+ /** Non-blocking warnings for the operator. */
87
+ readonly warnings?: readonly string[] | undefined;
88
+ /**
89
+ * The exact phrase the operator must type to confirm a destructive run.
90
+ * Defaults (in the service) to the recipe id when omitted.
91
+ */
92
+ readonly confirmationPhrase?: string | undefined;
93
+ }
94
+ /** A sealed, digest-stamped plan. The digest makes confirmation tamper-evident. */
95
+ interface CleanupPlan {
96
+ readonly recipeId: string;
97
+ readonly parameters: Readonly<Record<string, unknown>>;
98
+ readonly items: readonly CleanupPlanItem[];
99
+ readonly retains: readonly string[];
100
+ readonly blockers: readonly string[];
101
+ readonly rebuildActions: readonly string[];
102
+ readonly warnings: readonly string[];
103
+ readonly estimatedTotal: number;
104
+ readonly confirmationPhrase: string;
105
+ /** Deterministic checksum of the plan's material content — see `computePlanDigest`. */
106
+ readonly digest: string;
107
+ }
108
+ /** One committed step's outcome — reported via `onStep`, folded into the summary. */
109
+ interface CleanupStepResult {
110
+ readonly resource: string;
111
+ readonly processed: number;
112
+ readonly ok: boolean;
113
+ readonly error?: string | undefined;
114
+ /** Opaque resume cursor the recipe may surface for observability. */
115
+ readonly cursor?: string | undefined;
116
+ readonly startedAt?: Date | undefined;
117
+ readonly completedAt?: Date | undefined;
118
+ }
119
+ /**
120
+ * BOUNDED progress summary persisted on the run (design "Node.js memory"): a
121
+ * fixed-size aggregate, never an unbounded per-chunk array. Detailed steps, if a
122
+ * host wants them, go to a separate capped sink — not this document.
123
+ */
124
+ interface CleanupProgressSummary {
125
+ /** Running total of processed records across all steps. */
126
+ readonly processed: number;
127
+ /** Count of steps reported so far. */
128
+ readonly steps: number;
129
+ /** Resource the last step touched. */
130
+ readonly currentResource?: string | undefined;
131
+ /** Last resume cursor reported. */
132
+ readonly lastCursor?: string | undefined;
133
+ /** When the last step landed — liveness/heartbeat. */
134
+ readonly heartbeatAt?: Date | undefined;
135
+ /** Count of failed steps (a run with any is never a clean success). */
136
+ readonly failed?: number | undefined;
137
+ }
138
+ /** One verification invariant's outcome. */
139
+ interface VerificationCheck {
140
+ readonly name: string;
141
+ readonly ok: boolean;
142
+ readonly detail?: string | undefined;
143
+ }
144
+ /** The recipe's own post-checks — a delete count alone is never success (§9). */
145
+ interface VerificationResult {
146
+ readonly ok: boolean;
147
+ readonly checks: readonly VerificationCheck[];
148
+ }
149
+ type CleanupOutcomeStatus = "completed" | "partial" | "failed" | "cancelled";
150
+ /** What `execute` returns — the per-step results + a terminal status. */
151
+ interface CleanupResult {
152
+ readonly status: Exclude<CleanupOutcomeStatus, "cancelled">;
153
+ readonly results: readonly CleanupStepResult[];
154
+ }
155
+ /**
156
+ * The finalization payload persisted ON the run BEFORE it enters `finalizing`
157
+ * (same CAS patch). This is what makes a `finalizing` crash recoverable
158
+ * WITHOUT re-executing the recipe: a recovering worker replays ONLY
159
+ * `evidenceStore.finalize()` + the terminal CAS from this durable snapshot —
160
+ * never `recipe.execute()` again.
161
+ */
162
+ interface CleanupFinalizationPayload {
163
+ readonly status: CleanupOutcomeStatus;
164
+ readonly results: readonly CleanupStepResult[];
165
+ readonly verification: VerificationResult;
166
+ /** Count of verification checks dropped by `maxChecks` truncation. */
167
+ readonly checksTruncated?: number | undefined;
168
+ readonly failureReason?: string | undefined;
169
+ }
170
+ /**
171
+ * A cleanup recipe — the unit operators choose. Pure provider: the four
172
+ * methods are the only surface arc calls. A recipe delegates the actual
173
+ * domain work to kernel-provided steps; it never runs a raw Mongo cascade.
174
+ */
175
+ interface CleanupRecipe {
176
+ /** Stable machine id, e.g. `'cleanup.pre-live-reset'`. Unique in a registry. */
177
+ readonly id: string;
178
+ /** Plain-language name for the UI. */
179
+ readonly label: string;
180
+ /** Whether this recipe removes data (gates confirmation + single-run fence). */
181
+ readonly destructive: boolean;
182
+ /**
183
+ * Recipe/plan-schema version. Persisted on the run; a retry that finds a
184
+ * different current version requires a fresh preview rather than replaying an
185
+ * incompatible sealed plan. Defaults to `'1'`.
186
+ */
187
+ readonly version?: string;
188
+ /**
189
+ * The purge strategy this recipe's evidence records (`'hard'` deletion,
190
+ * `'soft'` deactivation, `'anonymize'` redaction). Defaults to `'hard'`.
191
+ * A pure projection-rebuild recipe should declare `'soft'` (nothing about the
192
+ * SOURCE data is removed) or, better, the closest honest label — evidence
193
+ * must never overstate destructiveness.
194
+ */
195
+ readonly evidenceStrategy?: PurgeStrategyKind$1;
196
+ /** Whether this recipe may run right now (e.g. pre-go-live only). */
197
+ available(ctx: CleanupContext): Promise<Availability>;
198
+ /** Compute a preview WITHOUT mutating anything. Idempotent + side-effect-free. */
199
+ plan(input: CleanupInput, ctx: CleanupContext): Promise<CleanupPlanDraft>;
200
+ /** Execute the sealed plan. Must be chunked, idempotent, and cancellation-aware. */
201
+ execute(plan: CleanupPlan, ctx: CleanupExecutionContext): Promise<CleanupResult>;
202
+ /** Verify the post-state — the recipe owns what "clean" means. */
203
+ verify(plan: CleanupPlan, ctx: CleanupContext): Promise<VerificationResult>;
204
+ }
205
+ type CleanupRunStatus = "planned" | "queued" | "running" | "finalizing" | "completed" | "failed" | "cancelled";
206
+ /** Terminal states — no further transition is valid. */
207
+ declare const CLEANUP_TERMINAL_STATUSES: readonly CleanupRunStatus[];
208
+ interface CleanupRun {
209
+ readonly id: string;
210
+ readonly recipeId: string;
211
+ readonly recipeVersion: string;
212
+ readonly status: CleanupRunStatus;
213
+ readonly planDigest: string;
214
+ /** The immutable sealed plan — replayed verbatim by the worker / a retry. */
215
+ readonly sealedPlan: CleanupPlan;
216
+ /** The operator-supplied parameters (also inside sealedPlan; kept explicit). */
217
+ readonly parameters: Readonly<Record<string, unknown>>;
218
+ /** The authorizing actor — persisted so a worker attributes the op correctly. */
219
+ readonly actor: PurgeActor;
220
+ /** Serializable ambient scope captured at request time (branch/company). */
221
+ readonly ambient?: Readonly<Record<string, unknown>> | undefined;
222
+ readonly requestedBy: string;
223
+ readonly reason: string;
224
+ readonly operationId: string;
225
+ /**
226
+ * Whether the recipe was destructive at request time — persisted so a
227
+ * GENERIC store can enforce the single-destructive-run policy without
228
+ * consulting the registry (and so non-destructive rebuilds need not
229
+ * serialize behind it).
230
+ */
231
+ readonly destructive: boolean;
232
+ /**
233
+ * Admission-control key the store serializes on (e.g. `'global-destructive'`).
234
+ * Absent for runs that need no mutual exclusion. Hosts may shard it later
235
+ * (`'branch:<id>'`) without an arc change.
236
+ */
237
+ readonly concurrencyKey?: string | undefined;
238
+ /** How many times a worker has claimed this run (1 = first execution). */
239
+ readonly attempt: number;
240
+ /**
241
+ * Exclusive worker lease. A run in `running`/`finalizing` with an UNEXPIRED
242
+ * lease is owned — `claim` refuses it. Progress + status writes carry the
243
+ * token so a stalled ex-owner that wakes past expiry cannot clobber the new
244
+ * owner's writes.
245
+ */
246
+ readonly leaseToken?: string | undefined;
247
+ readonly leaseExpiresAt?: Date | undefined;
248
+ readonly progress: CleanupProgressSummary;
249
+ /** Durable cancellation request — the source of truth the executor polls. */
250
+ readonly cancelRequested: boolean;
251
+ /** Who requested the cancel + why + when (audit — §8, cancel evidence). */
252
+ readonly cancelRequestedBy?: PurgeActor | undefined;
253
+ readonly cancelReason?: string | undefined;
254
+ readonly cancelRequestedAt?: Date | undefined;
255
+ /** Durable finalization snapshot — see {@link CleanupFinalizationPayload}. */
256
+ readonly finalization?: CleanupFinalizationPayload | undefined;
257
+ readonly queuedAt?: Date | undefined;
258
+ readonly startedAt?: Date | undefined;
259
+ readonly completedAt?: Date | undefined;
260
+ readonly failureReason?: string | undefined;
261
+ }
262
+ /** Immutable proof of a completed run (§5) — deterministically checksummed. */
263
+ interface CleanupManifest {
264
+ readonly runId: string;
265
+ readonly recipeId: string;
266
+ readonly recipeVersion: string;
267
+ readonly operationId: string;
268
+ readonly planDigest: string;
269
+ readonly status: CleanupOutcomeStatus;
270
+ readonly actor: PurgeActor;
271
+ readonly reason: string;
272
+ readonly results: readonly CleanupStepResult[];
273
+ readonly verification: VerificationResult;
274
+ /**
275
+ * Count of verification checks DROPPED by the `maxChecks` cap. Present and
276
+ * non-zero whenever `verification.checks` is not the complete set — evidence
277
+ * must say so rather than silently show only the retained checks.
278
+ */
279
+ readonly checksTruncated?: number | undefined;
280
+ readonly completedAt: Date;
281
+ /**
282
+ * Integrity CHECKSUM of the manifest content (not tamper-PROOF: plain SHA-256
283
+ * does not stop an attacker with write access from replacing the record and
284
+ * recomputing the digest — pair with access control / keyed signatures for
285
+ * that). Detects accidental corruption + drift.
286
+ */
287
+ readonly manifestDigest: string;
288
+ }
289
+ type CleanupRunCreateResult = {
290
+ readonly created: true;
291
+ } | {
292
+ readonly created: false;
293
+ readonly activeRunId: string;
294
+ };
295
+ /** Fields a status transition may also patch. */
296
+ interface CleanupRunTransitionPatch {
297
+ readonly queuedAt?: Date;
298
+ readonly startedAt?: Date;
299
+ readonly completedAt?: Date;
300
+ readonly failureReason?: string;
301
+ readonly progress?: CleanupProgressSummary;
302
+ /** Reset the cancellation flag — used when a terminal run is re-armed on retry. */
303
+ readonly cancelRequested?: boolean;
304
+ /** Persist the durable finalization snapshot (set entering `finalizing`). */
305
+ readonly finalization?: CleanupFinalizationPayload;
306
+ }
307
+ /** An exclusive worker lease on a run. */
308
+ interface CleanupLease {
309
+ /** Opaque token identifying THIS worker's ownership of the run. */
310
+ readonly token: string;
311
+ /** When the lease lapses and the run becomes claimable again. */
312
+ readonly expiresAt: Date;
313
+ }
314
+ /** Metadata persisted with a durable cancel request (audit). */
315
+ interface CleanupCancelRequest {
316
+ readonly actor?: PurgeActor | undefined;
317
+ readonly reason?: string | undefined;
318
+ readonly requestedAt: Date;
319
+ }
320
+ /**
321
+ * Durable store for cleanup runs. The host owns persistence (Mongo/SQL/…) AND
322
+ * the atomicity of these operations — the framework's safety rests on them:
323
+ *
324
+ * - `createIfPermitted` MUST be atomic: for a run with a `concurrencyKey` it
325
+ * inserts ONLY if no other run with the same key is in a non-terminal
326
+ * state, backed by a unique partial index / conditional insert / lock. A
327
+ * check-then-create is NOT sufficient. Runs WITHOUT a key are always
328
+ * admitted.
329
+ * - `claim` MUST be atomic and EXCLUSIVE: grant the lease ONLY if the run is
330
+ * `queued`, OR is `running`/`finalizing` with an ABSENT or EXPIRED lease
331
+ * (`leaseExpiresAt <= now`). On grant it sets `leaseToken`/`leaseExpiresAt`,
332
+ * increments `attempt`, and returns the run (status UNCHANGED — the service
333
+ * branches on it: `finalizing` recovers finalization only, never
334
+ * re-executes). A run whose lease is live is OWNED — return `null`.
335
+ * - `compareAndTransition` MUST be a compare-and-set: apply the transition
336
+ * ONLY if the current status is one of `expected` AND, when `leaseToken`
337
+ * is given, the run's current lease token matches. This is the ONLY way
338
+ * status changes — it makes cancel-vs-complete AND stale-ex-owner races
339
+ * safe.
340
+ * - `reArmIfPermitted` MUST apply the same admission policy as
341
+ * `createIfPermitted` atomically with the terminal→`queued` transition
342
+ * (a retry must not slip past the single-destructive-run guard).
343
+ */
344
+ interface CleanupRunStore {
345
+ /** Atomic conditional insert (admission control by `concurrencyKey`, §8). */
346
+ createIfPermitted(run: CleanupRun): Promise<CleanupRunCreateResult>;
347
+ get(id: string): Promise<CleanupRun | null>;
348
+ /**
349
+ * Atomically acquire the exclusive worker lease (see contract above).
350
+ * Returns the claimed run with the lease applied, or `null` if the run is
351
+ * owned, terminal, or missing.
352
+ */
353
+ claim(id: string, lease: CleanupLease): Promise<CleanupRun | null>;
354
+ /**
355
+ * CAS the status: set `status=to` (+ `patch`) ONLY if current status ∈
356
+ * `expected` and (when given) `leaseToken` matches the run's current lease.
357
+ * Returns the updated run, or `null` if the CAS lost the race.
358
+ */
359
+ compareAndTransition(id: string, expected: readonly CleanupRunStatus[], to: CleanupRunStatus, patch?: CleanupRunTransitionPatch, leaseToken?: string): Promise<CleanupRun | null>;
360
+ /**
361
+ * Atomically re-arm a terminal (`failed`/`cancelled`) run back to `queued`
362
+ * under the SAME admission policy as `createIfPermitted` (its
363
+ * `concurrencyKey` must not collide with another non-terminal run). Clears
364
+ * the cancel flag/metadata + lease. Returns `{created:false}`-style refusal
365
+ * via `null` + the blocking run id in the second tuple slot is NOT modeled —
366
+ * a `null` simply means "not permitted or not in a terminal state."
367
+ */
368
+ reArmIfPermitted(id: string, patch: CleanupRunTransitionPatch): Promise<CleanupRun | null>;
369
+ /**
370
+ * Set `cancelRequested = true` (+ audit metadata, idempotent — first request
371
+ * wins). Does NOT change `status`.
372
+ */
373
+ requestCancel(id: string, request?: CleanupCancelRequest): Promise<CleanupRun | null>;
374
+ /**
375
+ * Overwrite the small bounded progress summary (frequent, non-status).
376
+ * When `lease` is given, ALSO extend `leaseExpiresAt` (heartbeat renewal) —
377
+ * and apply ONLY if the run's current lease token matches `lease.token`.
378
+ */
379
+ saveProgress(id: string, progress: CleanupProgressSummary, lease?: CleanupLease): Promise<void>;
380
+ }
381
+ /**
382
+ * Durable store for the evidence + manifest a run leaves behind. `finalize`
383
+ * MUST be idempotent by `evidence.operationId` — a retry / restart that
384
+ * re-finalizes the same operation is a no-op (or an upsert), never a duplicate.
385
+ * Ideally the host writes both in one transaction (with the terminal run
386
+ * transition); the framework tolerates a `finalizing` intermediate + re-run.
387
+ */
388
+ interface CleanupEvidenceStore {
389
+ finalize(input: {
390
+ evidence: PurgeEvidence;
391
+ manifest: CleanupManifest;
392
+ }): Promise<void>;
393
+ }
394
+ /** A serializable job — everything the worker needs is the run id. */
395
+ interface CleanupJob {
396
+ readonly runId: string;
397
+ }
398
+ /**
399
+ * Durable job queue (§8). `execute()` enqueues a `{ runId }`; a worker later
400
+ * dequeues and calls `service.processRun(runId)` — a DIFFERENT process can do
401
+ * this because the run carries the whole sealed operation. The default in-proc
402
+ * queue defers to a microtask (still off the request path within one process);
403
+ * production injects BullMQ / SQS / a repo-backed queue for cross-process
404
+ * durability, claim/lease, and restart recovery.
405
+ */
406
+ interface CleanupJobQueue {
407
+ enqueue(job: CleanupJob): Promise<void>;
408
+ }
409
+ /**
410
+ * A company/target write fence (§8) — blocks new writes for the duration of a
411
+ * destructive run. Acquire/release keyed by the operation id so a crashed run's
412
+ * fence is recoverable by op id.
413
+ *
414
+ * `acquire` MAY return a fencing token; when it does, the service passes the
415
+ * SAME token back to `release`, and the fence MUST refuse a release carrying a
416
+ * stale token. This closes the distributed race where a stalled ex-owner wakes
417
+ * after lease expiry and releases the fence out from under the new owner.
418
+ * Implementations without cross-process workers may return `undefined` and
419
+ * ignore the token.
420
+ */
421
+ interface CleanupWriteFence {
422
+ acquire(operationId: string): Promise<string | undefined | void>;
423
+ release(operationId: string, token?: string): Promise<void>;
424
+ }
425
+ interface CleanupPermissions {
426
+ /** May preview/observe runs. */
427
+ view: CleanupPermissionCheck;
428
+ /** May execute a run. Superadmin in the reference host. */
429
+ execute: CleanupPermissionCheck;
430
+ /** May cancel/retry a run. Defaults to `execute` when omitted. */
431
+ manage?: CleanupPermissionCheck;
432
+ }
433
+ /** Host permission predicate — receives whatever the host passes as the request scope. */
434
+ type CleanupPermissionCheck = (scope: unknown) => boolean | Promise<boolean>;
435
+ /** Framework size caps that keep runs + documents bounded (design "memory"). */
436
+ interface CleanupLimits {
437
+ /** Max plan items. Default 500. */
438
+ readonly maxPlanItems: number;
439
+ /** Max per-run result rows folded into the manifest. Default 1000. */
440
+ readonly maxResults: number;
441
+ /** Max verification checks. Default 200. */
442
+ readonly maxChecks: number;
443
+ /** Max reason string length. Default 2000. */
444
+ readonly maxReasonLength: number;
445
+ /** Max nesting depth of the parameters object. Default 8. */
446
+ readonly maxParamDepth: number;
447
+ }
448
+ declare const DEFAULT_CLEANUP_LIMITS: CleanupLimits;
449
+ //#endregion
450
+ export { CleanupWriteFence as A, CleanupResult as C, CleanupRunStore as D, CleanupRunStatus as E, PurgeStrategyKind$1 as F, PurgeVerificationSummary as I, VerificationCheck as L, PurgeActor as M, PurgeEvidence as N, CleanupRunTransitionPatch as O, PurgeResourceResult as P, VerificationResult as R, CleanupRecipe as S, CleanupRunCreateResult as T, CleanupPermissions as _, CleanupEvidenceStore as a, CleanupPlanItem as b, CleanupInput as c, CleanupLease as d, CleanupLimits as f, CleanupPermissionCheck as g, CleanupOutcomeStatus as h, CleanupContext as i, DEFAULT_CLEANUP_LIMITS as j, CleanupStepResult as k, CleanupJob as l, CleanupManifest as m, CLEANUP_TERMINAL_STATUSES as n, CleanupExecutionContext as o, CleanupLogger as p, CleanupCancelRequest as r, CleanupFinalizationPayload as s, Availability as t, CleanupJobQueue as u, CleanupPlan as v, CleanupRun as w, CleanupProgressSummary as x, CleanupPlanDraft as y };
@@ -587,108 +587,4 @@ type ArcModuleInput<TExports = unknown> = ArcModule<TExports> | Promise<ArcModul
587
587
  */
588
588
  interface ArcModuleRegistry {}
589
589
  //#endregion
590
- //#region src/factory/module/contributions.d.ts
591
- /**
592
- * Collect every module's `healthChecks` in the caller's order. Fails at boot
593
- * on a duplicate check `name` across modules, naming BOTH owning modules.
594
- * The host's app-level checks are appended AFTER this (modules-first,
595
- * host-last) by the caller.
596
- */
597
- declare function collectModuleHealthChecks(modules: readonly ArcModule[]): HealthCheck[];
598
- /**
599
- * Collect every module's `workflows`. Resolves factory contributions (so a
600
- * factory can close over a container the caller has already decorated on
601
- * `fastify`) and flattens to a single opaque array. Arc core deliberately
602
- * does NOT validate or interpret the values — the streamline integration
603
- * owns name/shape checks and registration. Called by the integration at init
604
- * time, not by arc's boot pipeline (workflows depend on the integration's
605
- * container).
606
- */
607
- declare function collectModuleWorkflows(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<unknown[]>;
608
- /**
609
- * Collect every module's `scheduledJobs`. Resolves factory contributions
610
- * (after bootstraps) and fails at boot on a duplicate job `name` across
611
- * modules. The caller passes the returned definitions to the canonical
612
- * schedules plugin; factories must therefore be resolved only once.
613
- */
614
- declare function collectModuleScheduledJobs(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<ScheduleDefinition[]>;
615
- //#endregion
616
- //#region src/factory/module/lifecycle.d.ts
617
- /**
618
- * Subscribe every module's `eventHandlers` in dependency order (pass the
619
- * `orderModules`-sorted list). Returns the ordered list of unsubscribe
620
- * functions so the caller can tear them down at shutdown BEFORE module
621
- * `onClose` (while module deps are still alive). Fails at boot on:
622
- * - a duplicate NAMED handler across modules (attributing both owners);
623
- * - a module declaring handlers while the event subsystem is unavailable.
624
- * Resolves factory contributions (after bootstraps) so a handler can close over
625
- * booted engines rather than a global getter.
626
- */
627
- declare function subscribeModuleEventHandlers(fastify: FastifyInstance, modules: readonly ArcModule[]): Promise<Array<() => void | Promise<void>>>;
628
- //#endregion
629
- //#region src/factory/module/order.d.ts
630
- /**
631
- * Order modules for composition by their `dependsOn` edges — a STABLE
632
- * topological sort. Called once at the start of the bootstrap phase; every
633
- * subsequent phase (bootstrap, resources, afterResources, and reverse-order
634
- * onClose) iterates the returned list, so a module's declared dependencies are
635
- * always composed before it.
636
- *
637
- * "Stable" = modules with no edge between them keep their original list order,
638
- * so declaring `dependsOn` on one module never silently reorders an unrelated
639
- * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
640
- * returned unchanged.
641
- *
642
- * Fail-fast — throws (never reorders past a broken contract) on:
643
- * - duplicate module names (the name is the graph key)
644
- * - a `dependsOn` name not present in the composed set
645
- * - a self-reference (`dependsOn` includes the module's own name)
646
- * - a dependency cycle (reports the concrete `a → b → … → a` path)
647
- */
648
- declare function orderModules(modules: readonly ArcModule[]): ArcModule[];
649
- //#endregion
650
- //#region src/factory/module/resolve.d.ts
651
- /**
652
- * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
653
- * (this is where a dynamic `import()` fires); a promise (or plain module) is
654
- * awaited. Called once per module at the start of the bootstrap phase.
655
- */
656
- declare function resolveModule(input: ArcModuleInput): Promise<ArcModule>;
657
- /** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
658
- declare function resolveContribution<T>(contribution: ModuleContribution<T> | undefined, fastify: FastifyInstance): Promise<readonly T[]>;
659
- //#endregion
660
- //#region src/factory/module/index.d.ts
661
- /**
662
- * Identity helper for authoring a typed module — mirrors `defineResource`.
663
- * Gives inference + a single obvious construction site; does no work.
664
- *
665
- * `TExports` is inferred from the `bootstrap` return value, so a module
666
- * author gets a typed public export for free:
667
- *
668
- * ```ts
669
- * export const accountingModule = (deps: Deps) =>
670
- * defineModule({
671
- * name: "accounting",
672
- * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
673
- * });
674
- * ```
675
- */
676
- declare function defineModule<TExports = unknown>(module: ArcModule<TExports>): ArcModule<TExports>;
677
- /**
678
- * Typed accessor for a module's public export (its `bootstrap` return value),
679
- * recorded at `fastify.arc.modules[name]`.
680
- *
681
- * Throwing, in line with the fail-fast contract (and `requireOrgId`-style
682
- * accessors): a missing entry means the module either isn't composed, is
683
- * composed AFTER the caller (list order = init order), or returned nothing —
684
- * all wiring bugs that must surface at boot, not as `undefined` downstream.
685
- *
686
- * The type parameter is an assertion, not a proof. Two ways to type it:
687
- * - augment `ArcModuleRegistry` once — then the name alone infers the type:
688
- * `getModuleExports(f, "accounting")` → `AccountingEngine`.
689
- * - or pass it inline: `getModuleExports<AccountingEngine>(f, "accounting")`.
690
- */
691
- declare function getModuleExports<K extends keyof ArcModuleRegistry>(fastify: FastifyInstance, name: K): ArcModuleRegistry[K];
692
- declare function getModuleExports<TExports = unknown>(fastify: FastifyInstance, name: string): TExports;
693
- //#endregion
694
- export { schedulesPlugin as C, healthPlugin as D, _default$1 as E, _default as S, HealthOptions as T, ResourceModule as _, orderModules as a, ScheduleStats as b, collectModuleScheduledJobs as c, ArcModuleInput as d, ArcModuleRegistry as f, ResourceLike as g, LoadResourcesOptions as h, resolveModule as i, collectModuleWorkflows as l, ModuleContribution as m, getModuleExports as n, subscribeModuleEventHandlers as o, EventHandlerDefinition as p, resolveContribution as r, collectModuleHealthChecks as s, defineModule as t, ArcModule as u, loadResources as v, HealthCheck as w, SchedulesPluginOptions as x, ScheduleDefinition as y };
590
+ export { _default$1 as _, ModuleContribution as a, ResourceModule as c, ScheduleStats as d, SchedulesPluginOptions as f, HealthOptions as g, HealthCheck as h, EventHandlerDefinition as i, loadResources as l, schedulesPlugin as m, ArcModuleInput as n, LoadResourcesOptions as o, _default as p, ArcModuleRegistry as r, ResourceLike as s, ArcModule as t, ScheduleDefinition as u, healthPlugin as v };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.25.0",
3
+ "version": "2.28.0",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -128,6 +128,10 @@
128
128
  "types": "./dist/testing/storageContract.d.mts",
129
129
  "default": "./dist/testing/storageContract.mjs"
130
130
  },
131
+ "./testing/cleanup": {
132
+ "types": "./dist/testing/cleanupStoreContract.d.mts",
133
+ "default": "./dist/testing/cleanupStoreContract.mjs"
134
+ },
131
135
  "./factory": {
132
136
  "types": "./dist/factory/index.d.mts",
133
137
  "default": "./dist/factory/index.mjs"
@@ -211,6 +215,14 @@
211
215
  "./usage": {
212
216
  "types": "./dist/usage/index.d.mts",
213
217
  "default": "./dist/usage/index.mjs"
218
+ },
219
+ "./cleanup": {
220
+ "types": "./dist/cleanup/index.d.mts",
221
+ "default": "./dist/cleanup/index.mjs"
222
+ },
223
+ "./testing/outbox": {
224
+ "types": "./dist/testing/outboxStoreContract.d.mts",
225
+ "default": "./dist/testing/outboxStoreContract.mjs"
214
226
  }
215
227
  },
216
228
  "main": "./dist/index.mjs",
@@ -227,6 +239,7 @@
227
239
  ".": "stable",
228
240
  "./permissions": "stable",
229
241
  "./cache": "stable",
242
+ "./cleanup": "stable",
230
243
  "./types": "stable",
231
244
  "./types/storage": "stable",
232
245
  "./core": "stable",
@@ -255,6 +268,7 @@
255
268
  "./events/redis-stream": "stable",
256
269
  "./testing": "stable",
257
270
  "./testing/storage": "stable",
271
+ "./testing/cleanup": "stable",
258
272
  "./factory": "stable",
259
273
  "./schemas": "stable",
260
274
  "./migrations": "stable",
@@ -275,7 +289,8 @@
275
289
  "./scim": "stable",
276
290
  "./auth/audit": "stable",
277
291
  "./sync": "experimental",
278
- "./usage": "experimental"
292
+ "./usage": "experimental",
293
+ "./testing/outbox": "stable"
279
294
  }
280
295
  },
281
296
  "files": [
@@ -327,8 +342,8 @@
327
342
  "node": ">=22"
328
343
  },
329
344
  "peerDependencies": {
330
- "@classytic/primitives": ">=0.14.0",
331
- "@classytic/repo-core": ">=0.14.0",
345
+ "@classytic/primitives": ">=0.15.0",
346
+ "@classytic/repo-core": ">=0.17.0",
332
347
  "@classytic/streamline": ">=2.8.0",
333
348
  "@fastify/cors": ">=11.0.0",
334
349
  "@fastify/helmet": ">=13.0.0",
@@ -446,13 +461,13 @@
446
461
  "@arethetypeswrong/cli": "^0.18.5",
447
462
  "@better-auth/drizzle-adapter": "^1.6.19",
448
463
  "@better-auth/mongo-adapter": "^1.6.19",
449
- "@biomejs/biome": "^2.5.0",
464
+ "@biomejs/biome": "^2.5.5",
450
465
  "@classytic/dev-tools": "^0.2.0",
451
466
  "@classytic/gitkit": ">=0.1.0",
452
- "@classytic/mongokit": "^3.25.0",
467
+ "@classytic/mongokit": "^3.28.0",
453
468
  "@classytic/pgkit": "^0.1.0",
454
- "@classytic/primitives": "^0.14.0",
455
- "@classytic/repo-core": "^0.14.0",
469
+ "@classytic/primitives": "^0.15.0",
470
+ "@classytic/repo-core": "^0.17.0",
456
471
  "@classytic/sqlitekit": "^0.6.0",
457
472
  "@classytic/streamline": "^2.8.2",
458
473
  "@electric-sql/pglite": "^0.5.4",
@@ -471,9 +486,9 @@
471
486
  "@types/qs": "^6.15.1",
472
487
  "@vitest/coverage-v8": "^4.1.9",
473
488
  "ajv": "^8.20.0",
474
- "better-auth": "^1.6.19",
489
+ "better-auth": "^1.6.25",
475
490
  "better-sqlite3": "^12.11.1",
476
- "bullmq": "^5.79.0",
491
+ "bullmq": "^5.81.2",
477
492
  "dotenv": "^17.4.2",
478
493
  "drizzle-orm": "^0.45.2",
479
494
  "fast-check": "^4.8.0",
@@ -488,10 +503,10 @@
488
503
  "mongodb-memory-server": "^11.2.0",
489
504
  "mongoose": "^9.7.1",
490
505
  "publint": "^0.3.21",
491
- "tsdown": "^0.22.5",
506
+ "tsdown": "^0.22.14",
492
507
  "typebox": "^1.3.6",
493
508
  "typescript": "^7.0.2",
494
- "vitest": "^4.1.9",
509
+ "vitest": "^4.1.10",
495
510
  "ws": "^8.21.0",
496
511
  "zod": "^4.4.3"
497
512
  },