@sentropic/artifacts 0.1.4 → 0.2.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 (69) hide show
  1. package/README.md +17 -0
  2. package/dist/adapters/postgres-commit.d.ts +15 -0
  3. package/dist/adapters/postgres-commit.js +80 -0
  4. package/dist/adapters/postgres.d.ts +5 -2
  5. package/dist/adapters/postgres.js +39 -0
  6. package/dist/http.d.ts +41 -30
  7. package/dist/http.js +168 -507
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/mcp-core.d.ts +29 -39
  11. package/dist/mcp-core.js +83 -244
  12. package/dist/mcp.js +5 -5
  13. package/dist/profile/contracts.d.ts +67 -0
  14. package/dist/profile/contracts.js +31 -0
  15. package/dist/profile/index.d.ts +6 -0
  16. package/dist/profile/index.js +117 -0
  17. package/dist/profile/json.d.ts +10 -0
  18. package/dist/profile/json.js +76 -0
  19. package/dist/profile/radar.d.ts +3 -0
  20. package/dist/profile/radar.js +21 -0
  21. package/dist/profile/schemas.d.ts +6 -0
  22. package/dist/profile/schemas.js +76 -0
  23. package/dist/profile/validation.d.ts +5 -0
  24. package/dist/profile/validation.js +104 -0
  25. package/dist/profile/validators.d.ts +3 -0
  26. package/dist/profile/validators.js +51 -0
  27. package/dist/server/authorized-replay.d.ts +33 -0
  28. package/dist/server/authorized-replay.js +31 -0
  29. package/dist/server/command-identity.d.ts +52 -0
  30. package/dist/server/command-identity.js +71 -0
  31. package/dist/server/commit/brands.d.ts +7 -0
  32. package/dist/server/commit/brands.js +3 -0
  33. package/dist/server/commit/coordinator.d.ts +2 -0
  34. package/dist/server/commit/coordinator.js +729 -0
  35. package/dist/server/commit/types.d.ts +379 -0
  36. package/dist/server/commit/types.js +94 -0
  37. package/dist/server/commit-contract.d.ts +4 -0
  38. package/dist/server/commit-contract.js +2 -0
  39. package/dist/server/failures.d.ts +84 -0
  40. package/dist/server/failures.js +133 -0
  41. package/dist/server/gc.d.ts +42 -1
  42. package/dist/server/gc.js +168 -5
  43. package/dist/server/idempotency.d.ts +4 -0
  44. package/dist/server/idempotency.js +14 -0
  45. package/dist/server/lifecycle.d.ts +2 -1
  46. package/dist/server/lifecycle.js +15 -1
  47. package/dist/server/object-intents.d.ts +14 -1
  48. package/dist/server/object-intents.js +31 -0
  49. package/dist/server/outcome-contract.d.ts +3 -0
  50. package/dist/server/outcome-contract.js +3 -0
  51. package/dist/server/outcome-projection.d.ts +21 -0
  52. package/dist/server/outcome-projection.js +29 -0
  53. package/dist/server/preparation-cleanup.d.ts +31 -0
  54. package/dist/server/preparation-cleanup.js +106 -0
  55. package/dist/server/publication.d.ts +76 -0
  56. package/dist/server/publication.js +270 -31
  57. package/dist/server/service.d.ts +60 -0
  58. package/dist/server/service.js +181 -0
  59. package/dist/testing.d.ts +50 -0
  60. package/dist/testing.js +86 -0
  61. package/docs/migration-l6.md +129 -0
  62. package/examples/radar-commit-setup.ts +299 -0
  63. package/l0-freeze/command-identity-contract.ts +303 -0
  64. package/l0-freeze/commit-contract.ts +421 -0
  65. package/l0-freeze/failure-contract.ts +174 -0
  66. package/l0-freeze/profile-contract.ts +219 -0
  67. package/migrations/0003_coordinator.sql +28 -0
  68. package/package.json +27 -1
  69. package/src/contracts.ts +125 -0
@@ -11,6 +11,9 @@ import { assertArtifactAccess } from './authorization.js';
11
11
  import { createBindingService } from './bindings.js';
12
12
  import { createIdempotencyStore } from './idempotency.js';
13
13
  import { manifestDigest } from '../codecs.js';
14
+ import { ArtifactProfileInputError } from '../profile/index.js';
15
+ import { createArtifactCommand, } from './command-identity.js';
16
+ import { projectEnsureReceipt, replayEnsureGuarded, } from './authorized-replay.js';
14
17
  /** Create the artifact service over explicit injected ports (no defaults). */
15
18
  export function createArtifactService(ports) {
16
19
  const bindings = createBindingService(ports.kv, ports.uuid);
@@ -35,3 +38,181 @@ export function createArtifactService(ports) {
35
38
  },
36
39
  };
37
40
  }
41
+ /** Private causes stay on the failure object but never serialize: projection picks fields. */
42
+ const conceal = (failure) => {
43
+ Object.defineProperty(failure, 'cause', { value: failure.cause, enumerable: false });
44
+ return Object.freeze(failure);
45
+ };
46
+ const profileEntryFailure = (error, invocationId) => {
47
+ if (error.kind === 'operational') {
48
+ return conceal({
49
+ kind: 'operational', category: 'capability_unselected', variant: error.variant,
50
+ httpStatus: error.httpStatus, publicCode: error.publicCode, retriable: false,
51
+ invocationId, operation: 'ensure', phase: error.phase, commitState: 'not_committed', cause: error,
52
+ });
53
+ }
54
+ return conceal({
55
+ kind: 'ordinary', category: 'invalid_request',
56
+ httpStatus: error.httpStatus, publicCode: error.publicCode, retriable: false,
57
+ invocationId, operation: 'ensure', phase: error.phase, commitState: 'not_committed', cause: error,
58
+ });
59
+ };
60
+ const internalFailure = (invocationId, phase, cause) => conceal({
61
+ kind: 'operational', category: 'provider_error', variant: 'unclassified',
62
+ httpStatus: 500, publicCode: 'internal_error', retriable: false,
63
+ invocationId, operation: 'ensure', phase, commitState: 'not_committed', cause,
64
+ });
65
+ const unavailableFailure = (invocationId, phase, cause) => conceal({
66
+ kind: 'operational', category: 'provider_unavailable', variant: 'dispatch_failure',
67
+ httpStatus: 503, publicCode: 'capability_unavailable', retriable: true,
68
+ invocationId, operation: 'ensure', phase, commitState: 'not_committed', cause,
69
+ });
70
+ const anonymousFailure = (invocation) => conceal({
71
+ kind: 'admission', category: 'anonymous',
72
+ httpStatus: 401, publicCode: 'anonymous', retriable: false,
73
+ invocationId: invocation.invocationId, operation: 'ensure', phase: 'admit',
74
+ commitState: 'not_committed', cause: { invocation: invocation.invocationId },
75
+ });
76
+ const coordinatorFailure = (failure) => conceal({
77
+ kind: 'operational', category: failure.category, variant: failure.variant,
78
+ httpStatus: failure.httpStatus, publicCode: failure.publicCode, retriable: failure.retriable,
79
+ invocationId: failure.invocationId, operation: failure.operation, phase: failure.phase,
80
+ commitState: failure.commitState, cause: failure.cause,
81
+ });
82
+ const ensureResult = (receipt, replayed, invocationId) => ({
83
+ ok: true,
84
+ result: { artifactId: receipt.artifactId, created: receipt.created, replayed },
85
+ invocationId,
86
+ commitState: 'committed',
87
+ replayed,
88
+ });
89
+ export function createGuardedEnsureService(deps) {
90
+ const finalizeRequest = (invocation, command, policyRevision) => {
91
+ const binding = {
92
+ ownerId: invocation.actorId,
93
+ storageScopeId: invocation.storageScopeId,
94
+ bindingName: command.payload.bindingName,
95
+ attemptId: invocation.invocationId,
96
+ };
97
+ const captured = {};
98
+ const request = {
99
+ scopeId: invocation.storageScopeId,
100
+ internalKey: command.identity.internalKey,
101
+ commandHash: command.identity.commandHash,
102
+ codecVersion: command.identity.codecVersion,
103
+ invocationId: invocation.invocationId,
104
+ actorId: invocation.actorId,
105
+ operation: 'ensure',
106
+ get artifactId() { return captured.artifactId ?? null; },
107
+ bindingName: binding.bindingName,
108
+ policyRevision,
109
+ locks: { 'binding-identity': [async (client) => {
110
+ await deps.host.lockBinding(client, binding);
111
+ const receipt = await deps.host.resolveBinding(client, binding);
112
+ captured.artifactId = receipt.artifactId;
113
+ captured.created = receipt.created;
114
+ }] },
115
+ validateReplayReceipt: (receipt) => captured.created === false &&
116
+ projectEnsureReceipt(receipt).artifactId === captured.artifactId,
117
+ noteAbandonedAttempt: (attemptId) => deps.host.abandonAttempt(attemptId),
118
+ attemptId: invocation.invocationId,
119
+ guard: invocation.authorizeCommit,
120
+ receiptData: captured,
121
+ };
122
+ return { request, captured };
123
+ };
124
+ return {
125
+ async ensure(invocation, args) {
126
+ if (invocation.actorId.length === 0 || invocation.storageScopeId.length === 0) {
127
+ return { ok: false, failure: anonymousFailure(invocation) };
128
+ }
129
+ let command;
130
+ try {
131
+ const { envelopeKey, ...commandArgs } = args;
132
+ command = createArtifactCommand(deps.profile, 'artifacts_ensure', commandArgs, { storageScopeId: invocation.storageScopeId, actorId: invocation.actorId }, deps.derivation, envelopeKey);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof ArtifactProfileInputError) {
136
+ return { ok: false, failure: profileEntryFailure(error, invocation.invocationId) };
137
+ }
138
+ return { ok: false, failure: internalFailure(invocation.invocationId, 'entry', error) };
139
+ }
140
+ let policyRevision;
141
+ try {
142
+ policyRevision = await deps.host.readPolicyRevision();
143
+ }
144
+ catch (error) {
145
+ return { ok: false, failure: unavailableFailure(invocation.invocationId, 'acquire', error) };
146
+ }
147
+ const { request } = finalizeRequest(invocation, command, policyRevision);
148
+ const outcome = await deps.coordinator.finalizePrepared(request);
149
+ if (!outcome.ok)
150
+ return { ok: false, failure: coordinatorFailure(outcome.failure) };
151
+ let receipt;
152
+ try {
153
+ receipt = projectEnsureReceipt(outcome.result);
154
+ }
155
+ catch (error) {
156
+ return { ok: false, failure: internalFailure(invocation.invocationId, 'resolve', error) };
157
+ }
158
+ return ensureResult(receipt, outcome.replayed, outcome.invocationId);
159
+ },
160
+ async replay(invocation, args) {
161
+ if (invocation.actorId.length === 0 || invocation.storageScopeId.length === 0) {
162
+ return { ok: false, failure: anonymousFailure(invocation) };
163
+ }
164
+ try {
165
+ deps.profile.assertSelected('artifacts_ensure');
166
+ if (!args.binding || args.binding.ownerId !== invocation.actorId ||
167
+ args.binding.storageScopeId !== invocation.storageScopeId || !args.binding.bindingName) {
168
+ throw new ArtifactProfileInputError();
169
+ }
170
+ }
171
+ catch (error) {
172
+ if (error instanceof ArtifactProfileInputError) {
173
+ return { ok: false, failure: profileEntryFailure(error, invocation.invocationId) };
174
+ }
175
+ return { ok: false, failure: internalFailure(invocation.invocationId, 'entry', error) };
176
+ }
177
+ let policyRevision;
178
+ try {
179
+ policyRevision = await deps.host.readPolicyRevision();
180
+ }
181
+ catch (error) {
182
+ return { ok: false, failure: unavailableFailure(invocation.invocationId, 'acquire', error) };
183
+ }
184
+ let artifactId;
185
+ let missingTarget = false;
186
+ const binding = args.binding;
187
+ const outcome = await replayEnsureGuarded(deps.coordinator, {
188
+ invocation,
189
+ internalKey: args.internalKey,
190
+ commandHash: args.commandHash,
191
+ policyRevision,
192
+ get artifactId() { return artifactId; },
193
+ bindingName: binding?.bindingName,
194
+ validateReplayReceipt: (receipt) => !missingTarget &&
195
+ projectEnsureReceipt(receipt).artifactId === artifactId,
196
+ noteAbandonedAttempt: (attemptId) => deps.host.abandonAttempt(attemptId),
197
+ locks: binding === undefined ? undefined : {
198
+ 'binding-identity': [async (client) => {
199
+ await deps.host.lockBinding(client, binding);
200
+ const target = await deps.host.resolveBinding(client, binding);
201
+ missingTarget = target.created;
202
+ artifactId = target.artifactId;
203
+ }],
204
+ },
205
+ });
206
+ if (!outcome.ok)
207
+ return { ok: false, failure: coordinatorFailure(outcome.failure) };
208
+ let receipt;
209
+ try {
210
+ receipt = projectEnsureReceipt(outcome.result);
211
+ }
212
+ catch (error) {
213
+ return { ok: false, failure: internalFailure(invocation.invocationId, 'resolve', error) };
214
+ }
215
+ return ensureResult(receipt, outcome.replayed, outcome.invocationId);
216
+ },
217
+ };
218
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * L3 (immo #861) — test-only commit barrier (spec r2 §3.2).
3
+ *
4
+ * Test seam for pausing finalization at deterministic points with
5
+ * invocation correlation. `waitUntilReached` resolves after arrival;
6
+ * `release` resumes only that invocation/point pair. The coordinator
7
+ * rechecks authority after a resumed barrier; a barrier can never mark
8
+ * an operation authorized or successful.
9
+ *
10
+ * Pass `observer` as the coordinator `testObserver`. Register a wait before
11
+ * starting the operation to pause that invocation/point; other arrivals
12
+ * are observed without blocking. The three preparation points are fired by L4
13
+ * preparation adapters holding this same observer, never by the
14
+ * coordinator (arming one throws at construction).
15
+ */
16
+ import type { InspectableDeletionClaimStore } from './server/gc.js';
17
+ import type { PublicationBlobStore } from './server/publication.js';
18
+ export declare const COMMIT_BARRIER_POINTS: readonly ["before_connection_acquire", "before_key_lock", "before_blob_put", "after_blob_put_before_verify", "before_blob_readback", "before_finalization_row_lock", "after_finalization_locks_before_guard", "after_replay_lookup_before_guard", "before_commit"];
19
+ export type CommitBarrierPoint = (typeof COMMIT_BARRIER_POINTS)[number];
20
+ export type CommitBarrierObserver = (point: CommitBarrierPoint, invocationId: string) => Promise<void>;
21
+ export interface CommitArrivalObserver {
22
+ arrive(point: CommitBarrierPoint, invocationId: string): void;
23
+ }
24
+ /** Explicit host binding: purge must use the supplied deletion adapter for this call only. */
25
+ export interface PurgeTestHost {
26
+ readonly claims: InspectableDeletionClaimStore;
27
+ readonly storageScopeId: string;
28
+ readonly blobs: PublicationBlobStore;
29
+ purge(artifactId: string, deleting: PublicationBlobStore): Promise<void>;
30
+ }
31
+ export declare function inspectDeletionClaims(host: Pick<PurgeTestHost, 'claims' | 'storageScopeId'>): Promise<Array<{
32
+ claimId: string;
33
+ state: string;
34
+ blobKeys: string[];
35
+ }>>;
36
+ /** Start one correlated purge and pause immediately before its first external DELETE. */
37
+ export declare function pausePurgeBeforeDelete(host: PurgeTestHost, artifactId: string): Promise<{
38
+ claimId: string;
39
+ blobKeys: string[];
40
+ release(): void;
41
+ finished: Promise<void>;
42
+ }>;
43
+ /** Create a correlated test barrier for one finalization run. */
44
+ export declare function createArtifactCommitBarrier(options?: {
45
+ readonly points: readonly CommitBarrierPoint[];
46
+ }): {
47
+ observer: CommitBarrierObserver & CommitArrivalObserver;
48
+ waitUntilReached(point: CommitBarrierPoint, invocationId: string): Promise<void>;
49
+ release(point: CommitBarrierPoint, invocationId: string): void;
50
+ };
@@ -0,0 +1,86 @@
1
+ export const COMMIT_BARRIER_POINTS = [
2
+ 'before_connection_acquire',
3
+ 'before_key_lock',
4
+ 'before_blob_put',
5
+ 'after_blob_put_before_verify',
6
+ 'before_blob_readback',
7
+ 'before_finalization_row_lock',
8
+ 'after_finalization_locks_before_guard',
9
+ 'after_replay_lookup_before_guard',
10
+ 'before_commit',
11
+ ];
12
+ export async function inspectDeletionClaims(host) {
13
+ return (await host.claims.listClaims()).map((claim) => ({ claimId: claim.claimId, state: claim.state,
14
+ blobKeys: [`artifacts/${host.storageScopeId}/blobs/sha256/${claim.digest.slice(7)}`] }));
15
+ }
16
+ /** Start one correlated purge and pause immediately before its first external DELETE. */
17
+ export async function pausePurgeBeforeDelete(host, artifactId) {
18
+ let release;
19
+ const gate = new Promise((resolve) => { release = resolve; });
20
+ let reached;
21
+ let fail;
22
+ const arrival = new Promise((resolve, reject) => {
23
+ reached = resolve;
24
+ fail = reject;
25
+ });
26
+ const deleting = { ...host.blobs,
27
+ putBlob: (key, bytes) => host.blobs.putBlob(key, bytes),
28
+ getBlob: (key) => host.blobs.getBlob(key), listKeys: (prefix) => host.blobs.listKeys(prefix),
29
+ async deleteBlob(key) {
30
+ const claims = (await host.claims.listClaims()).filter((claim) => claim.holder === `purge:${artifactId}` && claim.state === 'pending');
31
+ const matching = claims.find((claim) => key.endsWith(`/sha256/${claim.digest.slice(7)}`));
32
+ if (matching === undefined)
33
+ return host.blobs.deleteBlob(key);
34
+ reached({ claimId: matching.claimId,
35
+ blobKeys: claims.map((claim) => `artifacts/${host.storageScopeId}/blobs/sha256/${claim.digest.slice(7)}`) });
36
+ await gate;
37
+ await host.blobs.deleteBlob(key);
38
+ },
39
+ };
40
+ const finished = host.purge(artifactId, deleting);
41
+ void finished.then(() => fail(new Error('purge finished without external DELETE')), fail);
42
+ return { ...await arrival, release, finished };
43
+ }
44
+ /** Create a correlated test barrier for one finalization run. */
45
+ export function createArtifactCommitBarrier(options) {
46
+ const gates = new Map();
47
+ const gateFor = (point, invocationId) => {
48
+ const key = `${point}:${invocationId}`;
49
+ const existing = gates.get(key);
50
+ if (existing !== undefined)
51
+ return existing;
52
+ const created = { arrivals: 0, armed: options?.points.includes(point) ?? false, waiters: [], releases: [] };
53
+ gates.set(key, created);
54
+ return created;
55
+ };
56
+ const arrive = (point, invocationId) => {
57
+ const gate = gateFor(point, invocationId);
58
+ gate.arrivals += 1;
59
+ for (const waiter of gate.waiters.splice(0))
60
+ waiter();
61
+ if (!gate.armed)
62
+ return Promise.resolve();
63
+ return new Promise((resolve) => {
64
+ gate.releases.push(resolve);
65
+ });
66
+ };
67
+ return {
68
+ observer: Object.assign(arrive, { arrive }),
69
+ waitUntilReached: (point, invocationId) => {
70
+ const gate = gateFor(point, invocationId);
71
+ if (gate.arrivals > 0)
72
+ return Promise.resolve();
73
+ gate.armed = true;
74
+ return new Promise((resolve) => {
75
+ gate.waiters.push(resolve);
76
+ });
77
+ },
78
+ release: (point, invocationId) => {
79
+ const gate = gateFor(point, invocationId);
80
+ const resume = gate.releases.shift();
81
+ if (resume === undefined)
82
+ throw new Error(`no paused barrier at ${point} for ${invocationId}`);
83
+ resume();
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,129 @@
1
+ # Artifacts 0.2.0 transport migration
2
+
3
+ The registry baseline checked for this change is `@sentropic/artifacts@0.1.4`.
4
+ Radar 0.1.2 and upstream 0.1.4 hosts must migrate their composition before
5
+ loading the 0.2.0 HTTP or MCP surface. Publication is a separate owner-approved
6
+ release step; this branch does not publish packages.
7
+
8
+ ## Required host composition
9
+
10
+ Compile an explicit immutable profile with `compileArtifactsProfile`. Radar
11
+ selects `artifacts_ensure`, `artifacts_publish`, `artifacts_list` and
12
+ `artifacts_get`. Pass the same compiled profile to `createHttpArtifactApp`,
13
+ the MCP adapter/executor and `createArtifactsRegistryDefinition`. Empty
14
+ selection exposes nothing. Missing configuration fails construction.
15
+
16
+ Supply a raw PostgreSQL pool to `createPostgresCommitExecutor` and a
17
+ transaction-bound policy participant factory to `createArtifactCommitCoordinator`.
18
+ The coordinator owns BEGIN, key locking, COMMIT, rollback and release.
19
+ Remove session advisory-lock wrappers, pool-routing proxies, nested BEGIN
20
+ and no-op release wrappers from the old provider composition.
21
+
22
+ Create one trusted `ArtifactInvocationContext` for every request, containing
23
+ the verified actor, trusted scope, invocation ID and `authorizeCommit` guard.
24
+ Pass it to `app.execute(request, invocation)` or `app.handle(request, invocation)`.
25
+ Accounting hosts call `execute` once and project its typed outcome themselves.
26
+ Do not call both methods for the same operation. The MCP adapter requires
27
+ `createInvocation` and verifies its actor against `ctx.principal.sub`.
28
+ HTTP and MCP must use the identical canonical verified actor string.
29
+
30
+ Supply one qualified `createRadarKeyDerivation` binding to both transports.
31
+ Caller arguments contain the original caller key; actor, scope, internal key,
32
+ hash and guard come from the host. An optional platform envelope must exactly
33
+ match the argument key. Keep publication URLs under one shared
34
+ `publicationUrls` policy so HTTP and MCP return identical receipts.
35
+
36
+ ## Public coordinator example and cleanup
37
+
38
+ The packed `examples/radar-commit-setup.ts` uses public entrypoints only:
39
+ `@sentropic/artifacts/adapters/postgres-commit`, `/server/commit-contract`
40
+ and `/mcp-core`. The commit contract exports `CommitExecutor`,
41
+ `CommitCoordinatorOptions`, `ArtifactPolicyDriver` and `CoordinatorTestPoint`.
42
+ The example compiles the frozen `ArtifactProviderProfile` shape, binds policy
43
+ reads/quota to the acquired PostgreSQL transaction and mints its guard fence.
44
+
45
+ Before activation, configure `cleanupWindowMs` on guarded publication and
46
+ start a cleanup scheduler; the example supplies a 30-second preparation
47
+ lifetime and a one-second serialized sweep. Each sweep processes at most
48
+ 100 attempts. Capacity, sweep cadence and storage availability determine
49
+ whether the configured deadline is met; failed cleanup remains observable debt.
50
+ Reads are limited to the same batch size, with unsettled debt first. Settled
51
+ attempts receive metadata-only existence checks for 60 seconds after their
52
+ deadline; subsequent orphan recovery uses the host's ordinary GC. No cleanup
53
+ or inspection downloads blob contents. Invalid metadata is retained as failed
54
+ debt, excluded from subsequent sweeps, and returned in inspection's
55
+ `invalidIntents`; the sweep reports it after processing the remaining batch.
56
+
57
+ `/server/object-intents` exposes `inspectPreparations(host, artifactId)` and
58
+ `scheduleBoundedPreparationCleanup(host, throughMs)`. Bind `host.intents`
59
+ to the PostgreSQL artifact store, plus `blobs`, `refs` and `claims`.
60
+ Keep the claim store's transaction open through external DELETE, then commit
61
+ or roll back and release it, as `startRadarPreparationCleanup` demonstrates.
62
+ Inspection returns `attempts`, `protectedKeys` and `abandonedKeys`; every
63
+ attempt retains its ID, invocation, blob keys, preparation time and deadline.
64
+ Cleanup invalidates expired preparations before removing their pins, retains
65
+ other references/pins, and never promotes an abandoned attempt to success.
66
+
67
+ `/testing` exports `pausePurgeBeforeDelete(host, artifactId)` and
68
+ `inspectDeletionClaims(host)`. Bind the test host's `storageScopeId`, `blobs`,
69
+ inspectable `claims`, and `purge(artifactId, deletingBlobs)` callback. The
70
+ callback must commit its durable `purge:<artifactId>` claims and use the
71
+ supplied deletion adapter for that purge only. The pause returns `claimId`,
72
+ `blobKeys`, `release()` and `finished`; release it in a `finally` block.
73
+ Inspection reads durable claim history without releasing exclusion or
74
+ granting publication authority. The seams do not alter L3 ports or profiles.
75
+
76
+ ## Domain adapters and transaction boundaries
77
+
78
+ `ensureHost.lockBinding` and `resolveBinding` receive the coordinator's
79
+ transaction client. Resolve the binding and write ACL/lifecycle state on that
80
+ same client. Concurrent commands must serialize on the binding identity,
81
+ including when their caller keys differ. Replay resolves current identity
82
+ under this lock and compares it to the stored receipt. Missing or changed
83
+ targets deny before the host guard; explicit replay requires a binding.
84
+
85
+ Use `createGuardedEnsureService` and `createGuardedPublicationService` for
86
+ custom services. The canonical coordinator exposes `finalizeOperation` and
87
+ `replayOperation` with the frozen L0 inputs. Bind fresh domain request plans
88
+ through its `operations` option; no process-local replay cache is used.
89
+ Provider adapters use `finalizePrepared` and `replayPrepared` on that same
90
+ coordinator. Guards run after all blocking waits and must be side-effect-free.
91
+ Policy/quota registration is trusted construction, never a public argument.
92
+
93
+ `ArtifactCommitParticipant.readAdmission` reads current approval/revision on
94
+ the leased transaction. Keep policy locks through COMMIT. Quota reservation
95
+ is provisional and rolls back with denial. Object authorization also runs
96
+ for replay; a stored success alone confers no authority.
97
+
98
+ Use `createPostgresGcService(executor, refsForTransaction)` for PostgreSQL GC.
99
+ The reference-store factory must bind its reads to the supplied client.
100
+ GC keeps the transaction and digest exclusion through external deletion and
101
+ claim completion. Do not pass a raw pool to transaction-scoped deletion claims.
102
+
103
+ ## Durable state and failures
104
+
105
+ Stop old writers before enabling the new composition. Apply migrations
106
+ `0001_artifacts.sql`, `0002_lifecycle.sql` and additive `0003_coordinator.sql`.
107
+ Preserve historical rows and objects. Supply `legacyRecords`, using
108
+ `createPostgresLegacyArtifactRecords(pool)` for PostgreSQL. An empty inventory
109
+ reader is appropriate only for a verified fresh store.
110
+
111
+ Ambiguous legacy keys and unknown command codecs return a stable
112
+ 503 `capability_unavailable`, non-retriable migration condition. They are
113
+ never promoted to newly authorized successes and never trigger allocation
114
+ under another key. Reconcile actual pilot inventory separately; unknown
115
+ historical array order cannot be recovered from a sorted manifest.
116
+
117
+ New receipts store the ordered `artifacts-command/v1` identity in
118
+ `artifacts.operation_receipts`. Manifest digests retain their historical
119
+ codec. A denied attempt has no success receipt; reauthorization may retry the
120
+ same key. An unknown COMMIT outcome requires the same key and a fresh guarded
121
+ lookup. Private failure causes remain internal. HTTP and MCP project the same
122
+ public code and retry classification; status 503 alone does not imply retry.
123
+
124
+ Public host imports are `@sentropic/artifacts/server/commit-contract`,
125
+ `@sentropic/artifacts/server/outcome-contract` and `@sentropic/artifacts/testing`.
126
+ The `/mcp-core` runtime stays independent of the MCP platform runtime.
127
+ For tests that subscribe after arrival, explicitly arm barrier points with
128
+ `createArtifactCommitBarrier({ points: ['before_commit'] })`. Other arrivals
129
+ remain observational; a barrier cannot grant authority or certify success.