@dbsp/cli 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { Command as Command12, CommanderError } from "commander";
19
19
 
20
20
  // src/commands/apply.ts
21
21
  import { AsyncLocalStorage } from "async_hooks";
22
- import { createHash as createHash3 } from "crypto";
22
+ import { createHash } from "crypto";
23
23
  import { readFile } from "fs/promises";
24
24
  import { createInterface } from "readline/promises";
25
25
  import {
@@ -29,6 +29,8 @@ import {
29
29
  createPgTransitionPack as createPgTransitionPack2,
30
30
  escapeDiagnosticText as escapeDiagnosticText3,
31
31
  preparePgExecutionSession,
32
+ readPgLedgerAddressChain as readPgLedgerAddressChain2,
33
+ readPgLedgerReservationsForExecution,
32
34
  readTransitionJournal,
33
35
  TransitionRunIdentityMismatchError,
34
36
  validatePgManagedLedgerCurrency,
@@ -45,7 +47,7 @@ import {
45
47
  validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest3
46
48
  } from "@dbsp/core";
47
49
  import { mintDurablyLoadedRun } from "@dbsp/core/internal";
48
- import { REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
50
+ import { ledgerAddressKey as ledgerAddressKey2, REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
49
51
  import { Command as Command2 } from "commander";
50
52
 
51
53
  // src/utils/output.ts
@@ -59,13 +61,29 @@ function printCliJson(value) {
59
61
  console.log(serializeCliJson(value));
60
62
  }
61
63
 
64
+ // src/commands/execution-ids.ts
65
+ function executionIdsForRun(journal) {
66
+ const executionIds = /* @__PURE__ */ new Set([journal.run.runId]);
67
+ for (const event of journal.events) {
68
+ const record2 = event.record;
69
+ if (event.event === "intent" && "executionId" in record2 && typeof record2.executionId === "string")
70
+ executionIds.add(record2.executionId);
71
+ if (event.event === "observed" && "intent" in record2 && record2.intent && typeof record2.intent.executionId === "string")
72
+ executionIds.add(record2.intent.executionId);
73
+ }
74
+ if ("generator" in journal.plan)
75
+ executionIds.add(`dbsp.generator.execution.${journal.run.runId}`);
76
+ return [...executionIds];
77
+ }
78
+
62
79
  // src/commands/generator-execution.ts
63
- import { createHash, randomUUID } from "crypto";
80
+ import { randomUUID } from "crypto";
64
81
  import { isDeepStrictEqual } from "util";
65
82
  import {
83
+ assertGeneratedPostconditionSession,
66
84
  compareSchemata,
67
85
  createPgsqlAdapter,
68
- dbTypesEqual,
86
+ decodeGeneratedPostconditionPayload,
69
87
  executePgAdmittedOperation,
70
88
  executePgDeclaredAdoption,
71
89
  executePgPersistedTableReaddress,
@@ -73,9 +91,18 @@ import {
73
91
  readPgCatalogueIdentity,
74
92
  readPgLedgerAddressChain,
75
93
  readPgLedgerScopeCurrency,
76
- readPgRemovalEffectsClosure
94
+ readPgRemovalEffectsClosure,
95
+ toGeneratedPostconditionBindingAddress,
96
+ verifyGeneratedCheckPostcondition,
97
+ verifyGeneratedColumnPostcondition,
98
+ verifyGeneratedIdentityPostcondition,
99
+ verifyGeneratedIndexPostcondition,
100
+ verifyGeneratedTablePostcondition,
101
+ withGeneratedPostconditionSession
77
102
  } from "@dbsp/adapter-pgsql";
78
103
  import {
104
+ canonicalJson,
105
+ canonicalJsonDigest,
79
106
  outcomeClaimEventId,
80
107
  outcomeClaimId,
81
108
  projectLedgerChain,
@@ -194,261 +221,180 @@ function observed(address) {
194
221
  digest: `generator:${address.kind}:${address.name}`
195
222
  };
196
223
  }
197
- function normalizedDefinition(value) {
198
- return value.replaceAll('"', "").replace(/\s+/gu, " ").replace(/;$/u, "").trim().toLowerCase();
199
- }
200
224
  function generatedPayload(value) {
225
+ const encoded = canonicalJson(value);
226
+ const normalized = JSON.parse(encoded);
201
227
  return {
202
- value,
203
- digest: createHash("sha256").update(JSON.stringify(value)).digest("hex")
228
+ value: normalized,
229
+ digest: canonicalJsonDigest(normalized),
230
+ payloadKind: "generated-structural-observation"
204
231
  };
205
232
  }
206
- function textList(value) {
207
- return Array.isArray(value) ? value.map((item) => String(item)) : [];
208
- }
209
- function foreignKeyAction(code) {
210
- switch (code) {
211
- case "a":
212
- return "NO ACTION";
213
- case "r":
214
- return "RESTRICT";
215
- case "c":
216
- return "CASCADE";
217
- case "n":
218
- return "SET NULL";
219
- case "d":
220
- return "SET DEFAULT";
221
- default:
222
- return void 0;
223
- }
233
+ async function identityObserved(executor, postcondition, address, kind) {
234
+ const verified = await verifyGeneratedIdentityPostcondition({
235
+ session: executor,
236
+ postcondition,
237
+ address,
238
+ kind
239
+ });
240
+ const value = JSON.parse(
241
+ canonicalJson({
242
+ kind: "identity-observed",
243
+ observedKind: verified.kind,
244
+ address: {
245
+ scope: address.scope,
246
+ ...address.schema === void 0 ? {} : { schema: address.schema },
247
+ name: address.name
248
+ },
249
+ identity: verified.identity,
250
+ structuralSemantics: "unverified"
251
+ })
252
+ );
253
+ return {
254
+ catalogueIdentity: verified.catalogueIdentity,
255
+ observed: {
256
+ value,
257
+ digest: canonicalJsonDigest(value),
258
+ payloadKind: "generated-identity-observation"
259
+ }
260
+ };
224
261
  }
225
262
  function generatedPostcondition(step, address) {
226
- const value = step.expectedDeclaration?.value;
227
- if (!value || typeof value !== "object" || Array.isArray(value))
263
+ const declaration = step.expectedDeclaration;
264
+ if (!declaration?.value || typeof declaration.value !== "object" || Array.isArray(declaration.value))
228
265
  throw new Error(
229
- `generated ${address.kind} step ${step.stepKey} has no structural postcondition`
266
+ `generated ${address.kind} step ${step.stepKey} has no decodable generated declaration`
230
267
  );
231
- const postcondition = value;
232
- return postcondition;
233
- }
234
- async function readGeneratedPostcondition(executor, step, address) {
235
- const parent = address.parent?.name;
236
- if (address.kind === "column" && parent && address.schema) {
237
- const postcondition = generatedPostcondition(step, address);
238
- if (postcondition.kind !== "column")
239
- throw new Error(
240
- `generated column ${address.name} has a non-column structural postcondition`
241
- );
242
- const expected = postcondition.column;
243
- if (expected.name !== address.name)
244
- throw new Error(
245
- `generated column ${address.name} structural postcondition names another column`
246
- );
247
- const row = (await executor.query(
248
- `SELECT pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS column_type, attribute.attnotnull AS is_not_null, pg_catalog.pg_get_expr(default_value.adbin, default_value.adrelid) AS column_default, collation.collname AS collation_name, attribute.attidentity AS identity_kind FROM pg_catalog.pg_attribute attribute JOIN pg_catalog.pg_class relation ON relation.oid = attribute.attrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_attrdef default_value ON default_value.adrelid = attribute.attrelid AND default_value.adnum = attribute.attnum LEFT JOIN pg_catalog.pg_collation collation ON collation.oid = attribute.attcollation WHERE namespace.nspname = $1 AND relation.relname = $2 AND attribute.attname = $3 AND attribute.attnum > 0 AND NOT attribute.attisdropped`,
249
- [address.schema, parent, address.name]
250
- )).rows[0];
251
- if (!row) throw new Error(`generated column ${address.name} is absent`);
252
- const type = String(row.column_type ?? "");
253
- const nullable = row.is_not_null === true ? false : row.is_not_null === false ? true : void 0;
254
- const actualDefault = row.column_default == null ? void 0 : String(row.column_default);
255
- const actualCollation = row.collation_name == null ? null : String(row.collation_name);
256
- const actualIdentity = row.identity_kind === "a" ? "always" : row.identity_kind === "d" ? "byDefault" : null;
257
- if (expected.type !== void 0 && !dbTypesEqual(type, expected.type))
258
- throw new Error(
259
- `generated column ${address.name} type postcondition differs`
260
- );
261
- if (expected.nullable !== void 0 && nullable !== expected.nullable)
262
- throw new Error(
263
- `generated column ${address.name} nullability postcondition differs`
264
- );
265
- if (expected.hasDefault === true && (actualDefault === void 0 || expected.default === void 0 || actualDefault !== expected.default))
266
- throw new Error(
267
- `generated column ${address.name} default postcondition differs`
268
- );
269
- if (expected.hasDefault === false && actualDefault !== void 0)
270
- throw new Error(
271
- `generated column ${address.name} default postcondition differs`
272
- );
273
- if (expected.collation !== void 0 && actualCollation !== expected.collation)
274
- throw new Error(
275
- `generated column ${address.name} collation postcondition differs`
276
- );
277
- if (expected.identity !== void 0 && actualIdentity !== expected.identity)
278
- throw new Error(
279
- `generated column ${address.name} identity postcondition differs`
280
- );
281
- return generatedPayload({
282
- kind: "column",
283
- type,
284
- nullable,
285
- default: actualDefault
286
- });
287
- }
288
- if (address.kind === "constraint" && parent && address.schema) {
289
- const postcondition = generatedPostcondition(step, address);
290
- if (postcondition.kind !== "constraint")
291
- throw new Error(
292
- `generated constraint ${address.name} has a non-constraint structural postcondition`
293
- );
294
- const expected = postcondition.constraint;
295
- const row = (await executor.query(
296
- `SELECT constraint_item.contype AS constraint_type, pg_catalog.pg_get_constraintdef(constraint_item.oid, true) AS constraint_definition, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.conkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.conrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS key_columns, referenced_namespace.nspname AS referenced_schema, referenced_relation.relname AS referenced_table, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.confkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.confrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS referenced_columns, constraint_item.confdeltype AS on_delete, constraint_item.confupdtype AS on_update, constraint_item.condeferrable AS is_deferrable, constraint_item.condeferred AS is_deferred, constraint_item.convalidated AS is_validated FROM pg_catalog.pg_constraint constraint_item JOIN pg_catalog.pg_class relation ON relation.oid = constraint_item.conrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_class referenced_relation ON referenced_relation.oid = constraint_item.confrelid LEFT JOIN pg_catalog.pg_namespace referenced_namespace ON referenced_namespace.oid = referenced_relation.relnamespace WHERE namespace.nspname = $1 AND relation.relname = $2 AND constraint_item.conname = $3`,
297
- [address.schema, parent, address.name]
298
- )).rows[0];
299
- if (!row) throw new Error(`generated constraint ${address.name} is absent`);
300
- const definition = String(row.constraint_definition ?? "");
301
- if (String(row.constraint_type) !== expected.type)
302
- throw new Error(
303
- `generated constraint ${address.name} postcondition differs`
304
- );
305
- if ((expected.type === "p" || expected.type === "u") && JSON.stringify(textList(row.key_columns)) !== JSON.stringify(expected.columns))
306
- throw new Error(
307
- `generated constraint ${address.name} postcondition differs`
308
- );
309
- if (expected.type === "c") {
310
- if (normalizedDefinition(definition) !== normalizedDefinition(expected.definition) || row.is_validated === true === expected.notValid)
311
- throw new Error(
312
- `generated constraint ${address.name} postcondition differs`
313
- );
268
+ return decodeGeneratedPostconditionPayload(declaration, step.stepKey);
269
+ }
270
+ async function readGeneratedV3Postcondition(executor, postcondition, address) {
271
+ const bindingAddress = toGeneratedPostconditionBindingAddress(address);
272
+ switch (postcondition.declaration.kind) {
273
+ case "column": {
274
+ const verified = await verifyGeneratedColumnPostcondition({
275
+ session: executor,
276
+ postcondition,
277
+ address: bindingAddress
278
+ });
279
+ return {
280
+ catalogueIdentity: verified.catalogueIdentity,
281
+ observed: generatedPayload({
282
+ kind: "column",
283
+ type: verified.projection.type,
284
+ nullable: verified.projection.nullable,
285
+ ...verified.projection.default === void 0 ? {} : { default: verified.projection.default },
286
+ ...verified.projection.collation === void 0 ? {} : { collation: verified.projection.collation },
287
+ ...verified.projection.identity === void 0 ? {} : { identity: verified.projection.identity }
288
+ })
289
+ };
314
290
  }
315
- if (expected.type === "f") {
316
- if (JSON.stringify(textList(row.key_columns)) !== JSON.stringify(expected.columns) || String(row.referenced_table ?? "") !== expected.references.table || expected.references.schema !== void 0 && String(row.referenced_schema ?? "") !== expected.references.schema || JSON.stringify(textList(row.referenced_columns)) !== JSON.stringify(expected.references.columns) || foreignKeyAction(row.on_delete) !== expected.onDelete || foreignKeyAction(row.on_update) !== expected.onUpdate || (row.is_deferrable === true && row.is_deferred === true) !== expected.deferred || row.is_validated === true === expected.notValid)
317
- throw new Error(
318
- `generated constraint ${address.name} postcondition differs`
319
- );
291
+ case "check": {
292
+ const verified = await verifyGeneratedCheckPostcondition({
293
+ session: executor,
294
+ postcondition,
295
+ address: bindingAddress
296
+ });
297
+ return {
298
+ catalogueIdentity: verified.catalogueIdentity,
299
+ observed: generatedPayload({
300
+ kind: verified.kind,
301
+ type: "c",
302
+ expression: verified.projection.expression,
303
+ validated: verified.projection.validated,
304
+ noInherit: verified.projection.noInherit,
305
+ enforced: verified.projection.enforced,
306
+ isLocal: verified.projection.isLocal,
307
+ inheritanceCount: verified.projection.inheritanceCount,
308
+ parentId: verified.projection.parentId
309
+ })
310
+ };
320
311
  }
321
- return generatedPayload({
322
- kind: "constraint",
323
- type: String(row.constraint_type ?? ""),
324
- definition
325
- });
326
- }
327
- if (address.kind === "index" && parent && address.schema) {
328
- const postcondition = generatedPostcondition(step, address);
329
- if (postcondition.kind !== "index")
330
- throw new Error(
331
- `generated index ${address.name} has a non-index structural postcondition`
312
+ case "constraint": {
313
+ return identityObserved(
314
+ executor,
315
+ postcondition,
316
+ bindingAddress,
317
+ "constraint"
332
318
  );
333
- const row = (await executor.query(
334
- `SELECT index_relation.relname AS index_name, index_meta.indisunique AS is_unique, pg_catalog.pg_get_indexdef(index_meta.indexrelid, 0, true) AS index_definition FROM pg_catalog.pg_index index_meta JOIN pg_catalog.pg_class relation ON relation.oid = index_meta.indrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace JOIN pg_catalog.pg_class index_relation ON index_relation.oid = index_meta.indexrelid WHERE namespace.nspname = $1 AND relation.relname = $2 AND index_relation.relname = $3`,
335
- [address.schema, parent, address.name]
336
- )).rows[0];
337
- if (!row) throw new Error(`generated index ${address.name} is absent`);
338
- const definition = String(row.index_definition ?? "");
339
- if (normalizedDefinition(definition) !== normalizedDefinition(postcondition.definition))
340
- throw new Error(`generated index ${address.name} postcondition differs`);
341
- return generatedPayload({
342
- kind: "index",
343
- unique: row.is_unique === true,
344
- definition
345
- });
346
- }
347
- if (address.kind === "table" && address.schema) {
348
- const postcondition = generatedPostcondition(step, address);
349
- if (postcondition.kind !== "table")
350
- throw new Error(
351
- `generated table ${address.name} has a non-table structural postcondition`
352
- );
353
- const rows = (await executor.query(
354
- `SELECT attribute.attname AS column_name, pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS column_type, attribute.attnotnull AS is_not_null, pg_catalog.pg_get_expr(default_value.adbin, default_value.adrelid) AS column_default FROM pg_catalog.pg_attribute attribute JOIN pg_catalog.pg_class relation ON relation.oid = attribute.attrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_attrdef default_value ON default_value.adrelid = attribute.attrelid AND default_value.adnum = attribute.attnum WHERE namespace.nspname = $1 AND relation.relname = $2 AND attribute.attnum > 0 AND NOT attribute.attisdropped ORDER BY attribute.attnum`,
355
- [address.schema, address.name]
356
- )).rows;
357
- const expected = postcondition.columns;
358
- if (expected.length === 0 || rows.length !== expected.length)
359
- throw new Error(
360
- `generated table ${address.name} column postcondition differs: expected ${JSON.stringify(expected)}, live ${JSON.stringify(rows)}`
361
- );
362
- for (const specification of expected) {
363
- const actual = rows.find((row) => row.column_name === specification.name);
364
- if (!actual || specification.type !== void 0 && !dbTypesEqual(
365
- String(actual.column_type ?? ""),
366
- specification.type
367
- ) || specification.nullable !== void 0 && actual.is_not_null !== true !== specification.nullable || specification.hasDefault === true && (specification.default === void 0 || actual.column_default == null || String(actual.column_default) !== specification.default) || specification.hasDefault === false && actual.column_default != null)
368
- throw new Error(
369
- `generated table ${address.name} column postcondition differs: expected ${JSON.stringify(specification)}, live ${JSON.stringify(actual)}`
370
- );
371
319
  }
372
- return generatedPayload({
373
- kind: "table",
374
- columns: rows.map((row) => ({
375
- name: String(row.column_name),
376
- type: String(row.column_type),
377
- nullable: row.is_not_null !== true,
378
- default: row.column_default == null ? void 0 : String(row.column_default)
379
- }))
380
- });
381
- }
382
- if (address.kind === "enum" && address.schema) {
383
- const postcondition = generatedPostcondition(step, address);
384
- if (postcondition.kind !== "enum")
385
- throw new Error(
386
- `generated enum ${address.name} has a non-enum structural postcondition`
320
+ case "index": {
321
+ const verified = await verifyGeneratedIndexPostcondition({
322
+ session: executor,
323
+ postcondition,
324
+ address: bindingAddress
325
+ });
326
+ return {
327
+ catalogueIdentity: verified.catalogueIdentity,
328
+ observed: generatedPayload({
329
+ kind: verified.kind,
330
+ projection: verified.projection
331
+ })
332
+ };
333
+ }
334
+ case "table": {
335
+ const verified = await verifyGeneratedTablePostcondition({
336
+ session: executor,
337
+ postcondition,
338
+ address: bindingAddress
339
+ });
340
+ return {
341
+ catalogueIdentity: verified.catalogueIdentity,
342
+ observed: generatedPayload({
343
+ kind: verified.kind,
344
+ columns: verified.projection.columns.map((column) => ({
345
+ name: column.name,
346
+ type: column.type,
347
+ nullable: column.nullable,
348
+ ...column.default === void 0 ? {} : { default: column.default },
349
+ ...column.collation === void 0 ? {} : { collation: column.collation },
350
+ ...column.identity === void 0 ? {} : { identity: column.identity }
351
+ }))
352
+ })
353
+ };
354
+ }
355
+ case "enum": {
356
+ return identityObserved(executor, postcondition, bindingAddress, "enum");
357
+ }
358
+ case "sequence": {
359
+ return identityObserved(
360
+ executor,
361
+ postcondition,
362
+ bindingAddress,
363
+ "sequence"
387
364
  );
388
- const rows = (await executor.query(
389
- `SELECT enum_label.enumlabel AS label FROM pg_catalog.pg_type type JOIN pg_catalog.pg_namespace namespace ON namespace.oid = type.typnamespace JOIN pg_catalog.pg_enum enum_label ON enum_label.enumtypid = type.oid WHERE namespace.nspname = $1 AND type.typname = $2 ORDER BY enum_label.enumsortorder`,
390
- [address.schema, address.name]
391
- )).rows;
392
- const labels = rows.map((row) => String(row.label));
393
- if (JSON.stringify(labels) !== JSON.stringify(postcondition.labels))
394
- throw new Error(`generated enum ${address.name} postcondition differs`);
395
- return generatedPayload({ kind: "enum", labels });
396
- }
397
- if (address.kind === "sequence" && address.schema) {
398
- const postcondition = generatedPostcondition(step, address);
399
- if (postcondition.kind !== "sequence")
400
- throw new Error(
401
- `generated sequence ${address.name} has a non-sequence structural postcondition`
365
+ }
366
+ case "extension": {
367
+ return identityObserved(
368
+ executor,
369
+ postcondition,
370
+ bindingAddress,
371
+ "extension"
402
372
  );
403
- const row = (await executor.query(
404
- `SELECT sequence.start_value::text AS start_value, sequence.increment_by::text AS increment_by, sequence.min_value::text AS min_value, sequence.max_value::text AS max_value, sequence.cache_size::text AS cache_size, sequence.cycle AS cycle FROM pg_catalog.pg_sequences sequence WHERE sequence.schemaname = $1 AND sequence.sequencename = $2`,
405
- [address.schema, address.name]
406
- )).rows[0];
407
- if (!row) throw new Error(`generated sequence ${address.name} is absent`);
408
- const actual = {
409
- startValue: String(row.start_value),
410
- incrementBy: String(row.increment_by),
411
- minValue: String(row.min_value),
412
- maxValue: String(row.max_value),
413
- cycle: row.cycle === true
414
- };
415
- for (const key of [
416
- "startValue",
417
- "incrementBy",
418
- "minValue",
419
- "maxValue",
420
- "cycle"
421
- ])
422
- if (postcondition[key] !== void 0 && postcondition[key] !== actual[key])
373
+ }
374
+ case "absent": {
375
+ const live = await readPgCatalogueIdentity(executor, address);
376
+ if (live)
423
377
  throw new Error(
424
- `generated sequence ${address.name} postcondition differs`
378
+ `generated ${address.kind} absence postcondition differs: ${address.name} is still present`
425
379
  );
426
- return generatedPayload({ kind: "sequence", ...row });
427
- }
428
- if (address.kind === "extension") {
429
- const postcondition = generatedPostcondition(step, address);
430
- if (postcondition.kind !== "extension")
431
- throw new Error(
432
- `generated extension ${address.name} has a non-extension structural postcondition`
433
- );
434
- const row = (await executor.query(
435
- `SELECT extension.extversion AS version FROM pg_catalog.pg_extension extension WHERE extension.extname = $1`,
436
- [address.name]
437
- )).rows[0];
438
- if (!row)
439
- throw new Error(
440
- `generated extension ${address.name} version postcondition differs`
441
- );
442
- if (postcondition.version !== void 0 && String(row.version) !== postcondition.version)
443
- throw new Error(
444
- `generated extension ${address.name} version postcondition differs`
445
- );
446
- return generatedPayload({
447
- kind: "extension",
448
- version: String(row.version)
449
- });
380
+ return { observed: generatedPayload({ kind: "absent" }) };
381
+ }
382
+ default:
383
+ throw new Error("generated v3 postcondition has no declarable read-back");
450
384
  }
451
- throw new Error(`generated ${address.kind} has no declarable read-back`);
385
+ }
386
+ async function readGeneratedPostcondition(executor, step, address) {
387
+ executor = assertGeneratedPostconditionSession(executor);
388
+ const decodedPostcondition = generatedPostcondition(step, address);
389
+ if (decodedPostcondition.postconditionVersion === 3)
390
+ return (await readGeneratedV3Postcondition(
391
+ executor,
392
+ decodedPostcondition,
393
+ address
394
+ )).observed;
395
+ throw new Error(
396
+ "generated postcondition decoder returned no supported version"
397
+ );
452
398
  }
453
399
  function containedBy(root, candidate) {
454
400
  for (let parent = candidate.parent; parent; parent = parent.parent) {
@@ -531,7 +477,7 @@ async function executeGeneratorPlan(input) {
531
477
  try {
532
478
  const database = await databaseId(input.pool);
533
479
  const executionId = `dbsp.generator.execution.${randomUUID()}`;
534
- await input.recordAttempt?.(executionId);
480
+ await input.recordAttempt(executionId);
535
481
  const steps = managedSteps(manifest);
536
482
  for (const step of steps) {
537
483
  if (step.lifecycle?.kind === "adoption-refused")
@@ -599,6 +545,7 @@ async function executeGeneratorPlan(input) {
599
545
  manifest,
600
546
  recomputedPlanDigest: input.planDigest,
601
547
  approval,
548
+ executionId,
602
549
  step,
603
550
  home: home(address2),
604
551
  address: address2,
@@ -611,7 +558,11 @@ async function executeGeneratorPlan(input) {
611
558
  completedStepKeys.push(step.stepKey);
612
559
  continue;
613
560
  }
614
- return adopted.outcome === "adoption-refused" ? { outcome: "adoption-refused", detail: adopted.detail } : { outcome: "execution-failed", detail: adopted.detail };
561
+ if (adopted.outcome === "adoption-refused")
562
+ return { outcome: "adoption-refused", detail: adopted.detail };
563
+ if (adopted.outcome === "recovery-required") return adopted;
564
+ if (adopted.outcome === "transport-ambiguous") return adopted;
565
+ return { outcome: "execution-failed", detail: adopted.detail };
615
566
  }
616
567
  if (step.lifecycle?.kind === "readdress") {
617
568
  const result = await executePgPersistedTableReaddress({
@@ -620,6 +571,7 @@ async function executeGeneratorPlan(input) {
620
571
  manifest,
621
572
  recomputedPlanDigest: input.planDigest,
622
573
  approval,
574
+ executionId,
623
575
  step,
624
576
  database,
625
577
  targetSchema: input.schema,
@@ -705,6 +657,8 @@ async function executeGeneratorPlan(input) {
705
657
  claimId: result.claimId,
706
658
  detail: `claim ${result.claimId} remains open and requires recovery: ${result.reason}`
707
659
  };
660
+ if (result.kind === "outcome-transport-ambiguous")
661
+ return { outcome: "transport-ambiguous", detail: result.reason };
708
662
  if ("reason" in result) return partial(result.reason);
709
663
  completedStepKeys.push(step.stepKey);
710
664
  continue;
@@ -814,7 +768,31 @@ async function executeGeneratorPlan(input) {
814
768
  kind: "destructive-outcome",
815
769
  request: admitRequest,
816
770
  readBackAndResolve: async (session) => {
817
- const live = await readPgCatalogueIdentity(session, address);
771
+ const declarationReadBack = step.classification === "data-destructive" && step.expectedDeclaration !== void 0 ? await withGeneratedPostconditionSession(
772
+ {
773
+ connect: async () => ({
774
+ query: session.query.bind(session),
775
+ release: () => void 0
776
+ })
777
+ },
778
+ (proof) => {
779
+ const postcondition = generatedPostcondition(
780
+ step,
781
+ address
782
+ );
783
+ if (postcondition.postconditionVersion !== 3)
784
+ throw new Error(
785
+ "generated postcondition decoder returned no supported version"
786
+ );
787
+ return readGeneratedV3Postcondition(
788
+ proof,
789
+ postcondition,
790
+ address
791
+ );
792
+ }
793
+ ) : void 0;
794
+ const live = step.classification === "removal" ? await readPgCatalogueIdentity(session, address) : void 0;
795
+ const terminalCatalogueIdentity = step.classification === "removal" ? live?.catalogueIdentity : declarationReadBack?.catalogueIdentity;
818
796
  const survivors = step.classification === "removal" && live ? [address] : [];
819
797
  const childLives = await Promise.all(
820
798
  containedClaims.map(async (child) => ({
@@ -831,9 +809,10 @@ async function executeGeneratorPlan(input) {
831
809
  const survivorNames = survivors.map(
832
810
  (survivor) => `${survivor.kind} ${survivor.schema ?? "<database>"}.${survivor.name}`
833
811
  );
812
+ const survivorValue = { survivors: survivorNames };
834
813
  const survivorObservation = {
835
- value: { survivors: survivorNames },
836
- digest: createHash("sha256").update(JSON.stringify(survivorNames)).digest("hex")
814
+ value: survivorValue,
815
+ digest: canonicalJsonDigest(survivorValue)
837
816
  };
838
817
  return {
839
818
  rootClaimId: claim.claimId,
@@ -893,8 +872,10 @@ async function executeGeneratorPlan(input) {
893
872
  eventKind: step.classification === "removal" ? "absent" : "observed",
894
873
  // The append protocol reads this address's terminal on its
895
874
  // pinned transaction session after the post-DDL read-back.
896
- ...live?.catalogueIdentity ? { catalogueIdentity: live.catalogueIdentity } : {},
897
- ...step.classification === "removal" ? {} : { observed: observed(address) }
875
+ ...terminalCatalogueIdentity === void 0 ? {} : { catalogueIdentity: terminalCatalogueIdentity },
876
+ ...step.classification === "removal" ? {} : {
877
+ observed: declarationReadBack?.observed ?? observed(address)
878
+ }
898
879
  }
899
880
  }
900
881
  ];
@@ -931,10 +912,7 @@ async function executeGeneratorPlan(input) {
931
912
  detail: `claim ${executed.claimId} remains open and requires recovery: ${executed.reason}`
932
913
  };
933
914
  if (executed.kind === "outcome-transport-ambiguous")
934
- return {
935
- outcome: "execution-failed",
936
- detail: executed.reason
937
- };
915
+ return { outcome: "transport-ambiguous", detail: executed.reason };
938
916
  if (executed.kind === "outcome-protocol-pending")
939
917
  return partial(
940
918
  `destructive claim ${claim.claimId} remains pending after executing: ${executed.reason}`
@@ -958,7 +936,7 @@ async function executeGeneratorPlan(input) {
958
936
  }
959
937
 
960
938
  // src/commands/generator-plan.ts
961
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
939
+ import { randomUUID as randomUUID2 } from "crypto";
962
940
  import {
963
941
  assertDeclarableChangeKind,
964
942
  classifyGeneratedMutation,
@@ -974,17 +952,16 @@ import {
974
952
  } from "@dbsp/adapter-pgsql";
975
953
  import {
976
954
  acquireTransitionLease,
955
+ canonicalJson as canonicalJson2,
956
+ canonicalJsonDigest as canonicalJsonDigest2,
977
957
  transitionPlanDigest,
978
958
  validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest2
979
959
  } from "@dbsp/core";
980
- function digest(value) {
981
- return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
982
- }
983
960
  function adoptionDeclaration(table) {
984
961
  const value = JSON.parse(
985
- JSON.stringify({ kind: "table", name: table.name, shape: table })
962
+ canonicalJson2({ kind: "table", name: table.name, shape: table })
986
963
  );
987
- return { value, digest: digest(value) };
964
+ return { value, digest: canonicalJsonDigest2(value) };
988
965
  }
989
966
  function replacementStatements(table, schema) {
990
967
  const quote = (value) => `"${value.replaceAll('"', '""')}"`;
@@ -1481,7 +1458,10 @@ async function runGeneratorPlan(input) {
1481
1458
  const run = {
1482
1459
  runId: `dbsp-generator-${randomUUID2()}`,
1483
1460
  planDigest,
1484
- targetContextDigest: digest({ database: await databaseId2(pool), schema }),
1461
+ targetContextDigest: canonicalJsonDigest2({
1462
+ database: await databaseId2(pool),
1463
+ schema
1464
+ }),
1485
1465
  databaseId: await databaseId2(pool),
1486
1466
  coreVersion: "schema-differ-generator-v1",
1487
1467
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2067,6 +2047,72 @@ function generatorRunHasPriorStepEvents(loaded) {
2067
2047
  return !(typeof executionId === "string" && event.stepId === `dbsp.generator.attempt:${executionId}` && event.operationRef === "dbsp.generator.attempt");
2068
2048
  });
2069
2049
  }
2050
+ function ledgerHomeForGeneratorAddress(address) {
2051
+ if (address.scope === "database") return { scope: "database" };
2052
+ if (!address.schema)
2053
+ throw new Error(
2054
+ `schema-scoped generated address ${address.name} has no schema ledger`
2055
+ );
2056
+ return { scope: "schema", schema: address.schema };
2057
+ }
2058
+ function generatorPlanLedgerAddresses(steps) {
2059
+ const addresses = /* @__PURE__ */ new Map();
2060
+ for (const step of steps) {
2061
+ if (step.address !== void 0) {
2062
+ addresses.set(ledgerAddressKey2(step.address), step.address);
2063
+ continue;
2064
+ }
2065
+ const closure = step.closure;
2066
+ if (!closure)
2067
+ throw new Error(`generated step ${step.stepKey} has no ledger address`);
2068
+ const candidates = [
2069
+ closure.root,
2070
+ ...closure.members.map((member) => member.address)
2071
+ ];
2072
+ for (const address of candidates)
2073
+ addresses.set(ledgerAddressKey2(address), address);
2074
+ }
2075
+ return [...addresses.values()];
2076
+ }
2077
+ async function generatorRunHasLedgerEvidence(session, journal, steps) {
2078
+ const executionIds = executionIdsForRun(journal);
2079
+ const executionIdSet = new Set(executionIds);
2080
+ const addresses = generatorPlanLedgerAddresses(steps);
2081
+ const homes = /* @__PURE__ */ new Map();
2082
+ for (const address of addresses) {
2083
+ const home2 = ledgerHomeForGeneratorAddress(address);
2084
+ homes.set(`${home2.scope}:${home2.schema ?? ""}`, home2);
2085
+ }
2086
+ for (const home2 of homes.values()) {
2087
+ for (const executionId of executionIds) {
2088
+ const reservations = await readPgLedgerReservationsForExecution(
2089
+ session,
2090
+ home2,
2091
+ executionId
2092
+ );
2093
+ if (reservations.length > 0) return true;
2094
+ }
2095
+ }
2096
+ for (const address of addresses) {
2097
+ const chain = await readPgLedgerAddressChain2(
2098
+ session,
2099
+ ledgerHomeForGeneratorAddress(address),
2100
+ address
2101
+ );
2102
+ if (chain.events.some((event) => executionIdSet.has(event.executionId ?? "")))
2103
+ return true;
2104
+ }
2105
+ return false;
2106
+ }
2107
+ async function generatorRunRequiresRecovery(target, journal, steps) {
2108
+ if (generatorRunHasPriorStepEvents(journal)) return true;
2109
+ const lease = await acquireExclusiveTransitionLease(target);
2110
+ try {
2111
+ return await generatorRunHasLedgerEvidence(lease.session, journal, steps);
2112
+ } finally {
2113
+ await lease.release();
2114
+ }
2115
+ }
2070
2116
  var GENERATOR_ATTEMPT_OPERATION = {
2071
2117
  ref: "dbsp.generator.attempt",
2072
2118
  operationKind: {
@@ -2114,19 +2160,19 @@ async function withPoolCleanupReported(result, close) {
2114
2160
  function compareCodeUnits(left, right) {
2115
2161
  return left < right ? -1 : left > right ? 1 : 0;
2116
2162
  }
2117
- function canonicalJson(value) {
2163
+ function canonicalJson3(value) {
2118
2164
  if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
2119
2165
  if (typeof value === "number" && !Number.isFinite(value))
2120
2166
  throw new Error("policy values must be finite JSON values");
2121
2167
  return JSON.stringify(value);
2122
2168
  }
2123
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
2169
+ if (Array.isArray(value)) return `[${value.map(canonicalJson3).join(",")}]`;
2124
2170
  if (typeof value !== "object")
2125
2171
  throw new Error(
2126
2172
  "policy values must be JSON values (undefined is not allowed)"
2127
2173
  );
2128
2174
  const record2 = value;
2129
- return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record2[key])}`).join(",")}}`;
2175
+ return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson3(record2[key])}`).join(",")}}`;
2130
2176
  }
2131
2177
  function nonEmptyString(value, path) {
2132
2178
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value)
@@ -2186,7 +2232,7 @@ function validateResourceAddress(value, path) {
2186
2232
  `${path}.catalogueIdentity.value`
2187
2233
  );
2188
2234
  try {
2189
- canonicalJson(identityValue);
2235
+ canonicalJson3(identityValue);
2190
2236
  } catch (error) {
2191
2237
  throw new Error(
2192
2238
  `${path}.catalogueIdentity.value must be a canonicalizable JSON object: ${errorDetail(error)}`
@@ -2256,11 +2302,11 @@ function normaliseAcceptance(acceptance2) {
2256
2302
  const validated = validateAssumptionAcceptance(acceptance2);
2257
2303
  const withinScope = validated.withinScope ? [
2258
2304
  ...new Map(
2259
- validated.withinScope.map((item) => [canonicalJson(item), item])
2305
+ validated.withinScope.map((item) => [canonicalJson3(item), item])
2260
2306
  )
2261
- ].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(canonicalJson(item))) : void 0;
2307
+ ].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(canonicalJson3(item))) : void 0;
2262
2308
  return JSON.parse(
2263
- canonicalJson({
2309
+ canonicalJson3({
2264
2310
  class: validated.class,
2265
2311
  ...validated.fromTrustRoot ? { fromTrustRoot: validated.fromTrustRoot } : {},
2266
2312
  ...withinScope === void 0 ? {} : { withinScope }
@@ -2272,7 +2318,7 @@ function canonicalApplyPolicy(acceptances) {
2272
2318
  ...new Map(
2273
2319
  acceptances.map((item) => {
2274
2320
  const normalized = normaliseAcceptance(item);
2275
- return [canonicalJson(normalized), normalized];
2321
+ return [canonicalJson3(normalized), normalized];
2276
2322
  })
2277
2323
  )
2278
2324
  ].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => item);
@@ -2281,8 +2327,8 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
2281
2327
  const authorizedInstant = new Date(authorizedAt);
2282
2328
  if (Number.isNaN(authorizedInstant.getTime()))
2283
2329
  throw new Error("authorization timestamp must be a valid instant");
2284
- return createHash3("sha256").update(
2285
- canonicalJson({
2330
+ return createHash("sha256").update(
2331
+ canonicalJson3({
2286
2332
  runId,
2287
2333
  planDigest,
2288
2334
  policy: canonicalApplyPolicy(policy),
@@ -2295,7 +2341,7 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
2295
2341
  }
2296
2342
  function hasReusableAuthorization(authorizations, runId, planDigest, policy, grants) {
2297
2343
  return authorizations?.some(
2298
- (item) => canonicalJson(item.policy) === canonicalJson(policy) && canonicalJson(item.grants) === canonicalJson(grants) && item.digest === authorizationDigest(
2344
+ (item) => canonicalJson3(item.policy) === canonicalJson3(policy) && canonicalJson3(item.grants) === canonicalJson3(grants) && item.digest === authorizationDigest(
2299
2345
  runId,
2300
2346
  planDigest,
2301
2347
  policy,
@@ -2370,7 +2416,7 @@ var APPLY_OUTCOME_CONTRACT = [
2370
2416
  [
2371
2417
  "prior-step-events-refusal",
2372
2418
  19,
2373
- "attempted runs must be classified by recover"
2419
+ "attempted runs with prior generator step-attempt events must be classified by dbsp reconcile --db <database> <run-id>"
2374
2420
  ],
2375
2421
  [
2376
2422
  "compatibility-refusal",
@@ -2402,7 +2448,12 @@ var APPLY_OUTCOME_CONTRACT = [
2402
2448
  [
2403
2449
  "recovery-required",
2404
2450
  64,
2405
- "an admitted non-transactional claim remains open; run recover with the reported claim reference"
2451
+ "an admitted claim remains open; run dbsp reconcile --db <database> <run-id>"
2452
+ ],
2453
+ [
2454
+ "transport-ambiguous",
2455
+ 65,
2456
+ "an admitted operation has an ambiguous transport outcome; reconcile the run before retrying"
2406
2457
  ],
2407
2458
  ["database-read-only", 34, "target cannot accept managed writes"],
2408
2459
  [
@@ -2455,6 +2506,7 @@ function exitCodeForApplyOutcome(outcome) {
2455
2506
  return applyExitCodes.get(outcome);
2456
2507
  }
2457
2508
  function outcomeForApplyResult(result) {
2509
+ if (result.unresolvedOutcome) return result.unresolvedOutcome.kind;
2458
2510
  if (result.durableOutcome && isApplyExecutionOutcome(result.durableOutcome))
2459
2511
  return result.durableOutcome;
2460
2512
  if (result.assessment.lifecycle === "partially-applied")
@@ -2475,8 +2527,90 @@ function outcomeForApplyResult(result) {
2475
2527
  return "outcome-unknown";
2476
2528
  return "context-mismatch";
2477
2529
  }
2530
+ function generatorApplyCommandResult(execution, runId, refusal) {
2531
+ const fields = refusal === void 0 ? {} : { refusal };
2532
+ switch (execution.outcome) {
2533
+ case "completed":
2534
+ return {
2535
+ outcome: execution.outcome,
2536
+ runId,
2537
+ result: execution,
2538
+ ...fields
2539
+ };
2540
+ case "partially-applied":
2541
+ return {
2542
+ outcome: execution.outcome,
2543
+ runId,
2544
+ result: execution,
2545
+ ...fields
2546
+ };
2547
+ case "selection-incomplete":
2548
+ return {
2549
+ outcome: execution.outcome,
2550
+ runId,
2551
+ result: execution,
2552
+ ...fields
2553
+ };
2554
+ case "adoption-refused":
2555
+ return {
2556
+ outcome: execution.outcome,
2557
+ runId,
2558
+ result: execution,
2559
+ ...fields
2560
+ };
2561
+ case "readdress-unsupported":
2562
+ return {
2563
+ outcome: execution.outcome,
2564
+ runId,
2565
+ result: execution,
2566
+ ...fields
2567
+ };
2568
+ case "readdress-refused":
2569
+ return {
2570
+ outcome: execution.outcome,
2571
+ runId,
2572
+ result: execution,
2573
+ ...fields
2574
+ };
2575
+ case "destructive-authority-refused":
2576
+ return {
2577
+ outcome: execution.outcome,
2578
+ runId,
2579
+ result: execution,
2580
+ ...fields
2581
+ };
2582
+ case "prior-step-events-refusal":
2583
+ return {
2584
+ outcome: execution.outcome,
2585
+ runId,
2586
+ result: execution,
2587
+ ...fields
2588
+ };
2589
+ case "recovery-required":
2590
+ return {
2591
+ outcome: execution.outcome,
2592
+ runId,
2593
+ result: execution,
2594
+ ...fields
2595
+ };
2596
+ case "transport-ambiguous":
2597
+ return {
2598
+ outcome: execution.outcome,
2599
+ runId,
2600
+ result: execution,
2601
+ ...fields
2602
+ };
2603
+ case "execution-failed":
2604
+ return {
2605
+ outcome: execution.outcome,
2606
+ runId,
2607
+ result: execution,
2608
+ ...fields
2609
+ };
2610
+ }
2611
+ }
2478
2612
  function formatApplyHuman(result) {
2479
- const line = `${result.outcome}: ${result.runId}`;
2613
+ const line = `${escapeDiagnosticText3(result.outcome)}: ${escapeDiagnosticText3(result.runId)}`;
2480
2614
  if (result.refusal) {
2481
2615
  if ("address" in result.refusal)
2482
2616
  return formatPreAppendRefusalHuman(line, result.refusal);
@@ -2488,13 +2622,62 @@ function formatApplyHuman(result) {
2488
2622
  `resolving command: ${escapeDiagnosticText3(result.refusal.resolvingCommand)}`
2489
2623
  ].join("\n");
2490
2624
  }
2491
- if (result.outcome !== "plan-digest-mismatch" || !result.result) return line;
2492
- const detail = result.result.assessment.reasons[0]?.detail;
2493
- return detail ? `${line}
2494
- ${detail}` : line;
2625
+ if (!("result" in result)) return line;
2626
+ if ("assessment" in result.result) {
2627
+ if (result.result.unresolvedOutcome) {
2628
+ const unresolved = result.result.unresolvedOutcome;
2629
+ return unresolved.kind === "recovery-required" ? [
2630
+ line,
2631
+ `claim: ${escapeDiagnosticText3(unresolved.claimId)}`,
2632
+ `detail: ${escapeDiagnosticText3(unresolved.detail)}`,
2633
+ `resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
2634
+ ].join("\n") : [
2635
+ line,
2636
+ `detail: ${escapeDiagnosticText3(unresolved.detail)}`,
2637
+ `resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
2638
+ ].join("\n");
2639
+ }
2640
+ if (result.outcome !== "plan-digest-mismatch") return line;
2641
+ const detail = result.result.assessment.reasons[0]?.detail;
2642
+ return detail ? `${line}
2643
+ ${escapeDiagnosticText3(detail)}` : line;
2644
+ }
2645
+ const execution = result.result;
2646
+ switch (execution.outcome) {
2647
+ case "completed":
2648
+ return line;
2649
+ case "recovery-required":
2650
+ return [
2651
+ line,
2652
+ `claim: ${escapeDiagnosticText3(execution.claimId)}`,
2653
+ `detail: ${escapeDiagnosticText3(execution.detail)}`,
2654
+ `resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
2655
+ ].join("\n");
2656
+ case "transport-ambiguous":
2657
+ return [
2658
+ line,
2659
+ `detail: ${escapeDiagnosticText3(execution.detail)}`,
2660
+ `resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
2661
+ ].join("\n");
2662
+ case "partially-applied":
2663
+ case "selection-incomplete":
2664
+ case "adoption-refused":
2665
+ case "readdress-unsupported":
2666
+ case "readdress-refused":
2667
+ case "destructive-authority-refused":
2668
+ case "execution-failed":
2669
+ return `${line}
2670
+ detail: ${escapeDiagnosticText3(execution.detail)}`;
2671
+ case "prior-step-events-refusal":
2672
+ return [
2673
+ line,
2674
+ `detail: ${escapeDiagnosticText3(execution.detail)}`,
2675
+ `resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
2676
+ ].join("\n");
2677
+ }
2495
2678
  }
2496
2679
  function acceptanceMatches(assumption, acceptance2) {
2497
- return acceptance2.class === assumption.class && (!acceptance2.fromTrustRoot || canonicalJson(acceptance2.fromTrustRoot) === canonicalJson(assumption.asserter)) && (assumption.scope.length === 0 ? !acceptance2.withinScope || acceptance2.withinScope.length === 0 : !acceptance2.withinScope || acceptance2.withinScope.length === 0 || assumption.scope.every(
2680
+ return acceptance2.class === assumption.class && (!acceptance2.fromTrustRoot || canonicalJson3(acceptance2.fromTrustRoot) === canonicalJson3(assumption.asserter)) && (assumption.scope.length === 0 ? !acceptance2.withinScope || acceptance2.withinScope.length === 0 : !acceptance2.withinScope || acceptance2.withinScope.length === 0 || assumption.scope.every(
2498
2681
  (resource) => acceptance2.withinScope?.some(
2499
2682
  (selector) => selectorMatchesResource(selector, resource)
2500
2683
  )
@@ -2730,10 +2913,14 @@ async function runApplyInternal(runId, options, pool) {
2730
2913
  throw new RecordedPlanDigestMismatchError(
2731
2914
  `persisted generator manifest is invalid: ${lifecycleError}`
2732
2915
  );
2733
- if (generatorRunHasPriorStepEvents(current))
2916
+ if (await generatorRunRequiresRecovery(
2917
+ target,
2918
+ current,
2919
+ manifest.manifest.steps
2920
+ ))
2734
2921
  return {
2735
2922
  outcome: "prior-step-events-refusal",
2736
- detail: "run has prior generator step-attempt events; run dbsp recover instead"
2923
+ detail: "run has prior generator step-attempt events; run dbsp reconcile --db <database> <run-id>"
2737
2924
  };
2738
2925
  return executeGeneratorPlan({
2739
2926
  pool: owned,
@@ -2780,12 +2967,11 @@ async function runApplyInternal(runId, options, pool) {
2780
2967
  withheldAuthority
2781
2968
  }
2782
2969
  };
2783
- result = {
2784
- outcome: execution.outcome,
2970
+ result = generatorApplyCommandResult(
2971
+ execution,
2785
2972
  runId,
2786
- result: execution,
2787
- ...preAppendRefusal === void 0 ? {} : { refusal: preAppendRefusal }
2788
- };
2973
+ preAppendRefusal
2974
+ );
2789
2975
  }
2790
2976
  } else {
2791
2977
  const locked = await withPgTransitionRunLock(
@@ -2837,7 +3023,7 @@ async function runApplyInternal(runId, options, pool) {
2837
3023
  return;
2838
3024
  const actor = process.env.USER ?? process.env.LOGNAME ?? "unknown-local-actor";
2839
3025
  const authorizedAt = (/* @__PURE__ */ new Date()).toISOString();
2840
- const digest2 = authorizationDigest(
3026
+ const digest = authorizationDigest(
2841
3027
  run.runId,
2842
3028
  transitionPlanDigest2(plan),
2843
3029
  policy.accepts,
@@ -2849,7 +3035,7 @@ async function runApplyInternal(runId, options, pool) {
2849
3035
  runId: run.runId,
2850
3036
  policy: policy.accepts,
2851
3037
  grants,
2852
- digest: digest2,
3038
+ digest,
2853
3039
  actor,
2854
3040
  authorizedAt
2855
3041
  };
@@ -2965,9 +3151,7 @@ var applyCommand = new Command2("apply").description(
2965
3151
  console.log("confirmation-declined: persisted run was not executed");
2966
3152
  else if (result2.outcome === "not-executable") {
2967
3153
  } else if (result2.outcome !== "dry-run" && "result" in result2)
2968
- console.log(
2969
- "runId" in result2.result ? formatApplyHuman(result2.result) : `${result2.result.outcome}${"detail" in result2.result ? `: ${result2.result.detail}` : ""}`
2970
- );
3154
+ console.log(formatApplyHuman(result2.result));
2971
3155
  }
2972
3156
  process.exitCode = exitCode2;
2973
3157
  return;
@@ -3143,7 +3327,7 @@ import {
3143
3327
  DBSP_LEDGER_EVENT_TABLE,
3144
3328
  escapeDiagnosticText as escapeDiagnosticText4,
3145
3329
  readPgCatalogueIdentity as readPgCatalogueIdentity3,
3146
- readPgLedgerAddressChain as readPgLedgerAddressChain2,
3330
+ readPgLedgerAddressChain as readPgLedgerAddressChain3,
3147
3331
  readPgLedgerMarker
3148
3332
  } from "@dbsp/adapter-pgsql";
3149
3333
  import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain as projectLedgerChain2 } from "@dbsp/core";
@@ -3316,7 +3500,7 @@ async function runInspect(selector, options) {
3316
3500
  let projection;
3317
3501
  try {
3318
3502
  projection = projectLedgerChain2(
3319
- await readPgLedgerAddressChain2(lease.session, ledger, address)
3503
+ await readPgLedgerAddressChain3(lease.session, ledger, address)
3320
3504
  );
3321
3505
  } catch (error) {
3322
3506
  return {
@@ -3483,7 +3667,7 @@ var introspectCommand = new Command5("introspect").description("Generate schema.
3483
3667
  );
3484
3668
 
3485
3669
  // src/commands/preflight.ts
3486
- import { createHash as createHash4 } from "crypto";
3670
+ import { createHash as createHash2 } from "crypto";
3487
3671
  import { mkdtemp, open, rename, rm } from "fs/promises";
3488
3672
  import { dirname as dirname3, join } from "path";
3489
3673
  import {
@@ -3568,7 +3752,7 @@ function declarationsForScopes(model, database, scopes, naming) {
3568
3752
  });
3569
3753
  return {
3570
3754
  version: 1,
3571
- digest: createHash4("sha256").update(canonical).digest("hex"),
3755
+ digest: createHash2("sha256").update(canonical).digest("hex"),
3572
3756
  declarations: uniqueDeclarations
3573
3757
  };
3574
3758
  }
@@ -3655,14 +3839,14 @@ var preflightCommand = new Command6("preflight").description(
3655
3839
  });
3656
3840
 
3657
3841
  // src/commands/reconcile.ts
3658
- import { createHash as createHash5 } from "crypto";
3842
+ import { createHash as createHash3 } from "crypto";
3659
3843
  import {
3660
3844
  assertCreateUniqueIndexConcurrentlyRecoveryNotInvalid,
3661
3845
  assertPgDatabaseWritable,
3662
3846
  escapeDiagnosticText as escapeDiagnosticText6,
3663
3847
  isPgDatabaseReadOnlyError,
3664
- readPgLedgerAddressChain as readPgLedgerAddressChain3,
3665
- readPgLedgerReservationsForExecution,
3848
+ readPgLedgerAddressChain as readPgLedgerAddressChain4,
3849
+ readPgLedgerReservationsForExecution as readPgLedgerReservationsForExecution2,
3666
3850
  readPgLedgerScopeCurrency as readPgLedgerScopeCurrency2,
3667
3851
  readTransitionJournal as readTransitionJournal2,
3668
3852
  readVerifiedPgLedgerReservationsForPair,
@@ -3673,6 +3857,8 @@ import { recoverPgOutcomeClaim } from "@dbsp/adapter-pgsql/internal";
3673
3857
  import {
3674
3858
  acquireExclusiveTransitionLease as acquireExclusiveTransitionLease2,
3675
3859
  assumptionAccepted,
3860
+ canonicalJson as canonicalJson4,
3861
+ canonicalJsonDigest as canonicalJsonDigest3,
3676
3862
  outcomeClaimId as outcomeClaimId2,
3677
3863
  projectLedgerChain as projectLedgerChain3,
3678
3864
  resourceScopeCovers,
@@ -3705,6 +3891,9 @@ function classifyReconcileFailure(error, stage) {
3705
3891
  if (stage === "journal") return "malformed-journal";
3706
3892
  return "catalogue";
3707
3893
  }
3894
+ function isUnresolvedRecoveryOutcome(report) {
3895
+ return report.outcome === "pending" || report.outcome === "blocked" || report.outcome === "malformed-chain" || report.outcome === "protocol-refused" || report.outcome === "transport-ambiguous" || report.outcome === "no-open-claim" || report.outcome === "indeterminate-appended" || report.outcome === "indeterminate-pair";
3896
+ }
3708
3897
  function ledgerHome(address) {
3709
3898
  if (address.scope === "database") return { scope: "database" };
3710
3899
  if (!address.schema)
@@ -3715,18 +3904,21 @@ function ledgerHome(address) {
3715
3904
  }
3716
3905
  function recoveryPayload(identity) {
3717
3906
  const value = JSON.parse(
3718
- JSON.stringify({ catalogueIdentity: identity })
3907
+ canonicalJson4({
3908
+ ...identity === void 0 ? {} : { catalogueIdentity: identity }
3909
+ })
3719
3910
  );
3720
3911
  return {
3721
3912
  value,
3722
- digest: createHash5("sha256").update(JSON.stringify(value)).digest("hex")
3913
+ digest: canonicalJsonDigest3(value)
3723
3914
  };
3724
3915
  }
3725
3916
  function recoveryReport(address, result) {
3726
3917
  if (result.kind === "outcome-recovery-appended") {
3918
+ const indeterminate = result.classification.resolution.eventKind === "indeterminate";
3727
3919
  return {
3728
3920
  address,
3729
- outcome: result.append.kind === "already-appended-outcome-resolution" ? "already-appended" : "appended",
3921
+ outcome: indeterminate ? "indeterminate-appended" : result.append.kind === "already-appended-outcome-resolution" ? "already-appended" : "appended",
3730
3922
  reason: result.classification.resolution.reason
3731
3923
  };
3732
3924
  }
@@ -3796,9 +3988,7 @@ function readdressRecoveryReport(reservations, result) {
3796
3988
  };
3797
3989
  }
3798
3990
  function unresolvedRecoveryDetail(reports) {
3799
- const unresolved = reports.filter(
3800
- (report) => report.outcome === "pending" || report.outcome === "blocked" || report.outcome === "malformed-chain" || report.outcome === "protocol-refused" || report.outcome === "transport-ambiguous" || report.outcome === "no-open-claim" || report.outcome === "indeterminate-pair"
3801
- );
3991
+ const unresolved = reports.filter(isUnresolvedRecoveryOutcome);
3802
3992
  if (unresolved.length === 0) return void 0;
3803
3993
  return unresolved.map(
3804
3994
  (report) => `${escapeDiagnosticText6(report.address.name)}: ${escapeDiagnosticText6(report.reason ?? report.outcome)}`
@@ -3806,23 +3996,18 @@ function unresolvedRecoveryDetail(reports) {
3806
3996
  }
3807
3997
  function formatReconcileHuman(result) {
3808
3998
  const line = `${escapeDiagnosticText6(result.outcome)}: ${escapeDiagnosticText6(result.runId)}`;
3809
- return result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
3810
- }
3811
- function executionIdsForRun(journal) {
3812
- const executionIds = /* @__PURE__ */ new Set([journal.run.runId]);
3813
- for (const event of journal.events) {
3814
- const record2 = event.record;
3815
- if (event.event === "intent" && "executionId" in record2 && typeof record2.executionId === "string")
3816
- executionIds.add(record2.executionId);
3817
- if (event.event === "observed" && "intent" in record2 && record2.intent && typeof record2.intent.executionId === "string")
3818
- executionIds.add(record2.intent.executionId);
3819
- }
3820
- if ("generator" in journal.plan)
3821
- executionIds.add(`dbsp.generator.execution.${journal.run.runId}`);
3822
- return [...executionIds];
3999
+ const base = result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
4000
+ if (result.outcome === "reconcile-completed" || !result.recovery) return base;
4001
+ const unresolved = result.recovery.filter(isUnresolvedRecoveryOutcome);
4002
+ return unresolved.length === 0 ? base : [
4003
+ base,
4004
+ ...unresolved.map(
4005
+ (report) => `${escapeDiagnosticText6(report.address.name)}: ${escapeDiagnosticText6(report.outcome)}${report.reason ? `: ${escapeDiagnosticText6(report.reason)}` : ""}`
4006
+ )
4007
+ ].join("\n");
3823
4008
  }
3824
4009
  function statementBundleDigest(statements) {
3825
- return createHash5("sha256").update(
4010
+ return createHash3("sha256").update(
3826
4011
  JSON.stringify(statements.map(({ ordinal, sql }) => [ordinal, sql]))
3827
4012
  ).digest("hex");
3828
4013
  }
@@ -3963,7 +4148,7 @@ async function runReconcile(runId, options, pool) {
3963
4148
  const reservations = (await Promise.all(
3964
4149
  [...homes.values()].flatMap(
3965
4150
  (home2) => executionIds.map(
3966
- (executionId) => readPgLedgerReservationsForExecution(
4151
+ (executionId) => readPgLedgerReservationsForExecution2(
3967
4152
  lease.session,
3968
4153
  home2,
3969
4154
  executionId
@@ -4024,7 +4209,7 @@ async function runReconcile(runId, options, pool) {
4024
4209
  const rootCandidates = [];
4025
4210
  for (const row of rows) {
4026
4211
  stage = "catalogue";
4027
- const chain = await readPgLedgerAddressChain3(
4212
+ const chain = await readPgLedgerAddressChain4(
4028
4213
  lease.session,
4029
4214
  ledgerHome(row.address),
4030
4215
  row.address