@substrat-run/adapter-sqlite 0.144.0 → 0.146.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.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
- import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, isSubstratError, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, principalBecomeCapabilityInput, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
- import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
4
+ import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, assertNoBindingShapeBreak, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, isSubstratError, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, principalBecomeCapabilityInput, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
+ import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, peerCallerAt, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerVerticalSpellings, samePeerVertical, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
6
6
  import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
7
7
  import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
8
8
  import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
@@ -318,9 +318,9 @@ const KERNEL_DDL = `
318
318
  -- #1575: attachment text and its FTS5 index, shared from @substrat-run/kernel. After
319
319
  -- _substrat_attachments, whose delete trigger it adds.
320
320
  ${ATTACHMENT_TEXT_DDL}
321
- -- #901: an entity's version is the ULID of the last event about it, so
322
- -- MAX(id) per (entity_type, entity_id) is the read. The id column sits last so
323
- -- SQLite walks to the end of the matched range instead of aggregating over it.
321
+ -- #901: an entity's version is the ULID of the last event about it (a state move
322
+ -- excepted, #2191), so the newest id per (entity_type, entity_id) is the read. The id
323
+ -- column sits last so SQLite walks the matched range backwards instead of aggregating.
324
324
  ${OUTBOX_ENTITY_INDEX}
325
325
  `;
326
326
  /** Row → contract shape. Never reads the secrets table — that is the point of the split. */
@@ -628,6 +628,8 @@ export class SqliteScopeHost {
628
628
  listPlans = new Map();
629
629
  /** #119: entity type → its archive/trash plan, from every registered module. */
630
630
  statePlans = new Map();
631
+ /** #2166: entity type → the updated stamps the host holds, from every registered module. */
632
+ updatedStampPlans = new Map();
631
633
  /** #2068: each registered module's erasure, in registration order — what `shredSubject` runs. */
632
634
  erasurePlans = [];
633
635
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
@@ -1654,6 +1656,7 @@ export class SqliteScopeHost {
1654
1656
  seen.add(m.version);
1655
1657
  }
1656
1658
  addStatePlans(this.statePlans, manifest.id, manifest.entityStates, manifest.permissions);
1659
+ addUpdatedStampPlans(this.updatedStampPlans, manifest.id, manifest.updatedStamps);
1657
1660
  for (const plan of listIndexPlans(manifest.id, manifest.lists, manifest.entityStates)) {
1658
1661
  const existing = this.listPlans.get(plan.entityType);
1659
1662
  if (existing) {
@@ -2905,7 +2908,10 @@ export class SqliteScopeHost {
2905
2908
  // `{ vertical, scope }` and the authorization they passed (K-34). What it emits was
2906
2909
  // emitted BECAUSE of the producer's event (#1237); the id resolves through
2907
2910
  // `_substrat_imports`, which the row above has just written.
2908
- await imp.handler(this.deliveryContext(rt, subject, undefined, { causedBy: e.id, invocationId: unit.invocationId }), event);
2911
+ const ctx = this.deliveryContext(rt, subject, undefined, { causedBy: e.id, invocationId: unit.invocationId });
2912
+ const stamps = watchUpdatedStamps(spineSql(rt.db), this.updatedStampPlans);
2913
+ await imp.handler(ctx, event);
2914
+ stamps?.assertHeld(ctx, `${imp.moduleId} consuming ${e.type}`);
2909
2915
  rt.db
2910
2916
  .prepare(`INSERT INTO _substrat_deliveries (event_id, consumer_module, delivered_at, invocation_id)
2911
2917
  VALUES (?, ?, ?, NULL)`)
@@ -2956,24 +2962,63 @@ export class SqliteScopeHost {
2956
2962
  * move, the one case it decides anything.
2957
2963
  */
2958
2964
  bindBreaks(actor, scope, incoming, servingRef) {
2959
- const vertical = scope.vertical && (scope.servingRef || servingRef)
2960
- ? this.directory
2961
- .prepare('SELECT serving_ref, serving_version_id FROM verticals WHERE slug = ?')
2962
- .get(scope.vertical)
2963
- : undefined;
2964
2965
  return bindExportBreaksOf({
2965
2966
  admin: this.admin,
2966
2967
  actor,
2967
2968
  scope,
2968
2969
  incoming: { id: incoming.id, verticalSlug: incoming.verticalSlug },
2969
2970
  ...(servingRef !== undefined ? { servingRef } : {}),
2970
- serving: vertical?.serving_ref && vertical.serving_version_id
2971
- ? { ref: vertical.serving_ref, versionId: vertical.serving_version_id }
2972
- : null,
2971
+ serving: this.servingPointerOf(scope, servingRef),
2973
2972
  readExports: async (versionId) => exportsOfManifestJson(this.manifestJsonOf(versionId) ?? null),
2974
2973
  readImports: async (slug, versionId) => this.versionImports(slug, versionId),
2975
2974
  });
2976
2975
  }
2976
+ /**
2977
+ * The serving pointer of `scope`'s vertical, read only when the scope is on a serving script
2978
+ * before or after a move (`servingRef`), the one case it decides what the scope runs.
2979
+ */
2980
+ servingPointerOf(scope, servingRef) {
2981
+ return scope.vertical && (scope.servingRef || servingRef) ? this.servingPointerOfVertical(scope.vertical) : null;
2982
+ }
2983
+ /** What `slug` serves in place, or null when nothing is. */
2984
+ servingPointerOfVertical(slug) {
2985
+ const v = this.directory
2986
+ .prepare('SELECT serving_ref, serving_version_id FROM verticals WHERE slug = ?')
2987
+ .get(slug);
2988
+ return v?.serving_ref && v.serving_version_id ? { ref: v.serving_ref, versionId: v.serving_version_id } : null;
2989
+ }
2990
+ /**
2991
+ * #2178: refuse running `incomingId` where any of `froms` ran, each distinct version judged once,
2992
+ * and where installs last reconciled one of `receipts` — a refusal from those names them.
2993
+ */
2994
+ assertNoStrandFrom(froms, incomingId, receipts = []) {
2995
+ const incoming = this.manifestJsonOf(incomingId);
2996
+ const judged = [...new Set(froms)].filter((f) => !!f && f !== incomingId);
2997
+ for (const from of judged)
2998
+ assertNoBindingShapeBreak(this.manifestJsonOf(from), incoming);
2999
+ // What each group of installs is judged from, and how (`strandJudgmentsOf`, one rule for both adapters).
3000
+ for (const r of receipts)
3001
+ for (const j of strandJudgmentsOf(r, judged, incomingId))
3002
+ assertNoBindingShapeBreak(this.manifestJsonOf(j.from), incoming, j.opts);
3003
+ }
3004
+ /**
3005
+ * #2178: refuse moving `scope` (a bind, or a route with `servingRef`) onto code that strands
3006
+ * people a link bound — any scope, fork or preview included, since bound people live in its own
3007
+ * spine. No acknowledgement passes it.
3008
+ */
3009
+ assertBindingShapesKept(scope, incomingId, servingRef) {
3010
+ const move = scopeMoveOf(scope, incomingId, servingRef, this.servingPointerOf(scope, servingRef));
3011
+ // A primary scope is also judged from the version it last reconciled: until then, a retirement
3012
+ // the running version declares has not reached it (`PROVISIONED_VERSIONS_SQL`).
3013
+ if (!move)
3014
+ return;
3015
+ const receipt = isPrimaryScope(scope) ? [installReconciled(scope, move.before)] : [];
3016
+ this.assertNoStrandFrom([move.before], move.after, receipt);
3017
+ }
3018
+ /** `PROVISIONED_VERSIONS_SQL` over the directory. */
3019
+ provisionedVersionsOf(vertical, serving, ownerTenant) {
3020
+ return installsReconciledOf(this.directory.prepare(PROVISIONED_VERSIONS_SQL).all(...provisionedVersionsParams(vertical, serving, ownerTenant)));
3021
+ }
2977
3022
  /** What one version of `slug` imports, from the registry; `not_found` for a version it does not know. */
2978
3023
  versionImports(slug, versionId) {
2979
3024
  const manifest = this.manifestJsonOf(versionId, slug);
@@ -3022,6 +3067,32 @@ export class SqliteScopeHost {
3022
3067
  await this.applyPendingMigrations(rt);
3023
3068
  return rt;
3024
3069
  }
3070
+ /**
3071
+ * The tenant a peer name is read in (#2252): the directory's own slug for that tenant, the one
3072
+ * prefix a push pinned to it registers under. Never parsed out of the name being matched.
3073
+ */
3074
+ peerTenant(tenantId) {
3075
+ const row = this.directory.prepare('SELECT slug FROM tenants WHERE tenant_id = ?').get(tenantId);
3076
+ return { id: tenantId, slug: row?.slug ?? null };
3077
+ }
3078
+ /**
3079
+ * Every scope of the tenant registered under a spelling of the peer name `vertical` in
3080
+ * `tenant` (`peerVerticalSpellings`), for the kernel's resolvers to choose among (#2252).
3081
+ */
3082
+ peerCandidates(tenant, vertical) {
3083
+ const rows = this.directory
3084
+ .prepare(`SELECT scope_id, tenant_id, vertical, status, kind, forked_from FROM scopes
3085
+ WHERE tenant_id = ? AND vertical IN (SELECT value FROM json_each(?))`)
3086
+ .all(tenant.id, JSON.stringify(peerVerticalSpellings(vertical, tenant.slug)));
3087
+ return rows.map((r) => ({
3088
+ id: r.scope_id,
3089
+ tenantId: r.tenant_id,
3090
+ vertical: r.vertical,
3091
+ status: r.status,
3092
+ kind: r.kind ?? '',
3093
+ forkedFrom: r.forked_from ?? null,
3094
+ }));
3095
+ }
3025
3096
  /** The directory's current hold, read again when background work is about to run. */
3026
3097
  scopeWorkRefusal(tenantId, scopeId, allowProvisioning = false) {
3027
3098
  const row = this.directory
@@ -3116,7 +3187,8 @@ export class SqliteScopeHost {
3116
3187
  * here, and `admitPeer` runs inside the scope's actor task on every invoke (`buildStub`).
3117
3188
  */
3118
3189
  async getVerticalScope(caller, tenantId, scopeId, options) {
3119
- const parsed = verticalCaller.parse(caller);
3190
+ // #2252: the caller as this vertical's `peers` names it, as on the hosted door.
3191
+ const parsed = peerCallerAt(this.peerDeclarations(), verticalCaller.parse(caller));
3120
3192
  const rt = await this.openActiveScope(tenantId, scopeId);
3121
3193
  return this.buildStub(tenantId, scopeId, rt, { kind: 'vertical-peer', caller: parsed }, options);
3122
3194
  }
@@ -3826,15 +3898,18 @@ export class SqliteScopeHost {
3826
3898
  // so the only loss is effort on a path that was already failing.
3827
3899
  const ref = guarded ? this.concurrencyRef(operation, guarded, parsed) : undefined;
3828
3900
  const seen = ref && invokeOptions?.ifMatch !== undefined ? this.versionAt(rt, ref) : undefined;
3901
+ // #2166: what the guards and the handler emit about a stamped entity, held below.
3902
+ const stamps = watchUpdatedStamps(spineSql(rt.db), this.updatedStampPlans);
3829
3903
  // #119: an operation on a TRASHED entity it did not declare it reaches is refused
3830
3904
  // here, before the guards and the handler — the declared key first, so a caller
3831
3905
  // without it meets the same `forbidden` an active entity gives (`entity-trash.ts`).
3832
- await refuseTrashedTarget({ sql: spineSql(rt.db), plans: this.statePlans, check: ctx.check }, operation, target, parsed, purging ? { now: this.clock(), gate: this.purgeGateFacts(rt, subject.id, tenantId, scopeId) } : undefined);
3906
+ await refuseTrashedTarget({ sql: spineSql(rt.db), plans: this.statePlans, check: ctx.check, now: ctx.now() }, operation, target, parsed, purging ? { gate: this.purgeGateFacts(rt, subject.id, tenantId, scopeId) } : undefined);
3833
3907
  // Manifest guards (K-17): pre-conditions, inside the operation's own
3834
3908
  // transaction, before the handler. A throw here blocks the operation
3835
3909
  // and rolls back exactly like a handler throw — fail closed.
3836
3910
  await this.runGuards(operation, ctx, parsed);
3837
3911
  result = await handler(ctx, parsed);
3912
+ stamps?.assertHeld(ctx, operation);
3838
3913
  // #119: the purge gate's AUTHORITATIVE read. Lifecycle, primacy and tenant live in the
3839
3914
  // directory, which this transaction does not cover and whose writers (a suspend, a
3840
3915
  // tenant status, a reclassification) do not take the scope's actor — so a hold could
@@ -4682,7 +4757,9 @@ export class SqliteScopeHost {
4682
4757
  });
4683
4758
  rt.db.exec('BEGIN IMMEDIATE');
4684
4759
  try {
4760
+ const stamps = watchUpdatedStamps(spineSql(rt.db), this.updatedStampPlans);
4685
4761
  await consumer.handler(ctx, event);
4762
+ stamps?.assertHeld(ctx, `${mod.id} consuming ${event.type}`);
4686
4763
  rt.db
4687
4764
  .prepare(`INSERT INTO _substrat_deliveries
4688
4765
  (event_id, consumer_module, delivered_at, invocation_id)
@@ -5226,6 +5303,8 @@ export class SqliteScopeHost {
5226
5303
  schemaVersion: r.schema_version,
5227
5304
  verticalVersionId: r.vertical_version_id,
5228
5305
  provisionedVersionId: r.provisioned_version_id ?? null,
5306
+ retirementsFinishedVersionId: r.retirements_finished_version_id ?? null,
5307
+ boundShapes: boundShapesOfColumn(r.bound_shapes),
5229
5308
  migrationFailure: mapMigrationFailure(r),
5230
5309
  forkedFrom: r.forked_from,
5231
5310
  forkedAt: r.forked_at,
@@ -6078,32 +6157,16 @@ export class SqliteScopeHost {
6078
6157
  return rows.map(mapHostname);
6079
6158
  },
6080
6159
  // #1706: the directory's answer to "vertical Y in tenant T", by the kernel's one rule.
6160
+ // Names match EXACTLY: this is the cross-vertical edges' read (the import cursor's
6161
+ // producer). A peer call's tenant-pinned spelling is `resolvePeerInstance`'s (#2252).
6081
6162
  resolveVerticalInstance: async (tenantId, vertical) => {
6082
- const rows = this.directory
6083
- .prepare(`SELECT scope_id, tenant_id, vertical, status, kind, forked_from FROM scopes
6084
- WHERE tenant_id = ? AND vertical = ?`)
6085
- .all(tenantId, vertical);
6086
- return resolveVerticalInstanceFrom(rows.map((r) => ({
6087
- id: r.scope_id,
6088
- tenantId: r.tenant_id,
6089
- vertical: r.vertical,
6090
- status: r.status,
6091
- kind: r.kind ?? '',
6092
- forkedFrom: r.forked_from ?? null,
6093
- })), tenantId, vertical);
6163
+ const tenant = { id: tenantId, slug: null };
6164
+ return resolveVerticalInstanceFrom(this.peerCandidates(tenant, vertical), tenant, vertical);
6094
6165
  },
6095
6166
  resolvePeerInstance: async (tenantId, callerScopeId, vertical) => {
6096
6167
  const binding = this.directory.prepare('SELECT target_scope_id, invalidated FROM peer_bindings WHERE tenant_id = ? AND caller_scope_id = ? AND vertical = ?').get(tenantId, callerScopeId, vertical);
6097
- const rows = this.directory.prepare(`SELECT scope_id, tenant_id, vertical, status, kind, forked_from FROM scopes
6098
- WHERE tenant_id = ? AND vertical = ?`).all(tenantId, vertical);
6099
- return resolvePeerInstanceFrom(rows.map((r) => ({
6100
- id: r.scope_id,
6101
- tenantId: r.tenant_id,
6102
- vertical: r.vertical,
6103
- status: r.status,
6104
- kind: r.kind ?? '',
6105
- forkedFrom: r.forked_from,
6106
- })), tenantId, vertical, binding?.target_scope_id ?? null, binding?.invalidated === 1);
6168
+ const tenant = this.peerTenant(tenantId);
6169
+ return resolvePeerInstanceFrom(this.peerCandidates(tenant, vertical), tenant, vertical, binding?.target_scope_id ?? null, binding?.invalidated === 1);
6107
6170
  },
6108
6171
  peerBinding: async (actor, tenantId, callerScopeId, vertical) => {
6109
6172
  const row = this.directory.prepare('SELECT target_scope_id, invalidated FROM peer_bindings WHERE tenant_id = ? AND caller_scope_id = ? AND vertical = ?').get(tenantId, callerScopeId, vertical);
@@ -6122,7 +6185,7 @@ export class SqliteScopeHost {
6122
6185
  }
6123
6186
  if (targetScopeId !== null) {
6124
6187
  const target = this.directory.prepare('SELECT tenant_id, vertical, status, kind, forked_from FROM scopes WHERE scope_id = ?').get(targetScopeId);
6125
- if (!target || target.tenant_id !== tenantId || target.vertical !== vertical || target.status !== 'active' || !isPrimaryScopeRow(target)) {
6188
+ if (!target || target.tenant_id !== tenantId || !samePeerVertical(target.vertical, vertical, this.peerTenant(tenantId).slug) || target.status !== 'active' || !isPrimaryScopeRow(target)) {
6126
6189
  throw substratError('not_found', 'target is not a live primary instance of this vertical in this tenant');
6127
6190
  }
6128
6191
  }
@@ -6484,6 +6547,27 @@ export class SqliteScopeHost {
6484
6547
  .get(verticalSlug, channel);
6485
6548
  const outgoing = current ? readVersion(current.version_id) : undefined;
6486
6549
  const ack = promotionAcknowledgement.parse(acknowledge ?? {});
6550
+ // The owning tenant's live scopes a PRIVATE prod promote re-points — below for an embedded
6551
+ // vertical, after the in-place serve for a dispatch-backed one (#321) — read before
6552
+ // anything moves so the #2178 gate can judge each one.
6553
+ const owning = channel === 'prod' ? readVertical(verticalSlug) : undefined;
6554
+ const owned = owning && owning.ownerTenant !== null && !owning.listed
6555
+ ? this.directory
6556
+ .prepare(`SELECT * FROM scopes
6557
+ WHERE vertical = ? AND tenant_id = ? AND status = 'active' AND forked_from IS NULL AND kind != 'preview'`)
6558
+ .all(verticalSlug, owning.ownerTenant)
6559
+ : [];
6560
+ // #2178, ahead of the acknowledgements below because none of them passes it. What the promote
6561
+ // moves something from: the channel's version, the version served in place (a prod promote
6562
+ // serves the incoming one next, and a failed serve leaves it behind the channel), and what each
6563
+ // re-pointed scope RUNS (`runningVersionOf`: on the serving script, the served version).
6564
+ const served = channel === 'prod' ? this.servingPointerOfVertical(verticalSlug) : null;
6565
+ // And what each scope it moves last reconciled (`PROVISIONED_VERSIONS_SQL`).
6566
+ this.assertNoStrandFrom([
6567
+ outgoing?.id,
6568
+ served?.versionId,
6569
+ ...owned.map((s) => runningVersionOf(mapScope(s), served)),
6570
+ ], versionId, served || owned.length > 0 ? this.provisionedVersionsOf(verticalSlug, served, owned.length > 0 ? owning.ownerTenant : null) : []);
6487
6571
  // §4's two checkpoints, fired where the blast radius is. A FIRST promotion
6488
6572
  // has nothing to diff against, so there is nothing to acknowledge — the
6489
6573
  // gate is about change, not about existence.
@@ -6532,26 +6616,17 @@ export class SqliteScopeHost {
6532
6616
  // adopt-then-rebind for those (serve → adopt legacy scopes → advance versions),
6533
6617
  // so we skip the rebind here. An EMBEDDED vertical (deploymentRef null) has no
6534
6618
  // per-version script and keeps the rebind here. See adapter-cloudflare parity.
6535
- if (channel === 'prod' && !incoming.deploymentRef) {
6536
- const owning = readVertical(verticalSlug);
6537
- if (owning && owning.ownerTenant !== null && !owning.listed) {
6538
- const bound = this.directory
6539
- .prepare(`SELECT scope_id, tenant_id, vertical_version_id FROM scopes
6540
- WHERE vertical = ? AND tenant_id = ? AND status = 'active' AND forked_from IS NULL AND kind != 'preview'`)
6541
- .all(verticalSlug, owning.ownerTenant);
6542
- for (const s of bound) {
6543
- if (s.vertical_version_id === versionId)
6544
- continue;
6545
- const prev = s.vertical_version_id ? readVersion(s.vertical_version_id) : undefined;
6546
- if (prev && prev.migrationDigest !== incoming.migrationDigest) {
6547
- await this.snapshotScope(actor, s.tenant_id, s.scope_id);
6548
- }
6549
- this.directory
6550
- .prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ?')
6551
- .run(versionId, verticalSlug, s.scope_id);
6552
- this.recordAdmin(actor, 'bindScopeVersion', { tenantId: s.tenant_id, scopeId: s.scope_id }, prev ? { versionId: prev.id, version: prev.version } : null, { versionId, vertical: verticalSlug, version: incoming.version, via: 'promoteVersion' });
6553
- }
6619
+ for (const s of incoming.deploymentRef ? [] : owned) {
6620
+ if (s.vertical_version_id === versionId)
6621
+ continue;
6622
+ const prev = s.vertical_version_id ? readVersion(s.vertical_version_id) : undefined;
6623
+ if (prev && prev.migrationDigest !== incoming.migrationDigest) {
6624
+ await this.snapshotScope(actor, s.tenant_id, s.scope_id);
6554
6625
  }
6626
+ this.directory
6627
+ .prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ?')
6628
+ .run(versionId, verticalSlug, s.scope_id);
6629
+ this.recordAdmin(actor, 'bindScopeVersion', { tenantId: s.tenant_id, scopeId: s.scope_id }, prev ? { versionId: prev.id, version: prev.version } : null, { versionId, vertical: verticalSlug, version: incoming.version, via: 'promoteVersion' });
6555
6630
  }
6556
6631
  // The acknowledgement is recorded, not just enforced: that is what turns
6557
6632
  // "someone reviewed the permission change" into evidence.
@@ -6607,6 +6682,9 @@ export class SqliteScopeHost {
6607
6682
  },
6608
6683
  bindingImpact: async (actor, tenantId, scopeId, versionId, opts) => {
6609
6684
  const { v, scope } = bindTarget(tenantId, scopeId, versionId);
6685
+ // #2178: a move no acknowledgement passes is refused here too, so a caller asking first
6686
+ // (an adopt, an Update) stops before anything is copied.
6687
+ this.assertBindingShapesKept(mapScope(scope), versionId, opts?.servingRef);
6610
6688
  const breaks = await this.bindBreaks(actor, mapScope(scope), v, opts?.servingRef);
6611
6689
  this.recordAccess(actor, 'bindingImpact', { tenantId, scopeId }, { versionId, ...opts }, breaks.length);
6612
6690
  return breaks;
@@ -6622,6 +6700,8 @@ export class SqliteScopeHost {
6622
6700
  throw substratError('precondition_failed', 'scope erasure changed; reload the scope and retry');
6623
6701
  }
6624
6702
  const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
6703
+ // #2178 first: no acknowledgement passes it, so none should be asked for ahead of it.
6704
+ this.assertBindingShapesKept(mapScope(scope), versionId);
6625
6705
  // #1756: an export an app in this tenant imports, dropped or re-versioned by what this
6626
6706
  // scope would run. Before the snapshot, so a refused bind leaves nothing behind.
6627
6707
  if (!ack.exportBreak) {
@@ -6753,17 +6833,25 @@ export class SqliteScopeHost {
6753
6833
  * Record that this scope's provision has now run against `versionId` (#1172).
6754
6834
  * Written only after a provision or reconcile succeeded — a receipt, never a hope.
6755
6835
  */
6756
- markScopeProvisioned: async (actor, tenantId, scopeId, versionId) => {
6836
+ markScopeProvisioned: async (actor, tenantId, scopeId, versionId, outcome) => {
6757
6837
  const scope = this.directory
6758
6838
  .prepare('SELECT tenant_id FROM scopes WHERE scope_id = ?')
6759
6839
  .get(scopeId);
6760
6840
  if (!scope || scope.tenant_id !== tenantId) {
6761
6841
  throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
6762
6842
  }
6843
+ // #2185: both in one write — the retirements count as finished only for the version
6844
+ // this receipt names, and a receipt written without that answer clears them.
6845
+ const finished = versionId !== null && outcome?.retirementsFinished === true ? versionId : null;
6846
+ // #2184: and the binding shapes it reported bound, paired with this receipt's version.
6763
6847
  this.directory
6764
- .prepare('UPDATE scopes SET provisioned_version_id = ? WHERE scope_id = ?')
6765
- .run(versionId, scopeId);
6766
- this.recordAdmin(actor, 'markScopeProvisioned', { tenantId, scopeId }, null, { versionId });
6848
+ .prepare('UPDATE scopes SET provisioned_version_id = ?, retirements_finished_version_id = ?, bound_shapes = ? WHERE scope_id = ?')
6849
+ .run(versionId, finished, boundShapesColumn(versionId, outcome?.boundShapes), scopeId);
6850
+ this.recordAdmin(actor, 'markScopeProvisioned', { tenantId, scopeId }, null, {
6851
+ versionId,
6852
+ ...(finished ? { retirementsFinished: true } : {}),
6853
+ ...(versionId !== null && outcome?.boundShapes ? { boundShapes: outcome.boundShapes } : {}),
6854
+ });
6767
6855
  },
6768
6856
  verticalServing: async (actor, verticalSlug) => {
6769
6857
  const r = this.directory
@@ -6788,6 +6876,8 @@ export class SqliteScopeHost {
6788
6876
  .get(verticalSlug);
6789
6877
  if (!r)
6790
6878
  throw substratError('not_found', `unknown vertical '${verticalSlug}'`);
6879
+ // #2178: every install on the serving script runs the new version from this moment.
6880
+ this.assertNoStrandFrom([r.serving_version_id], parsed.versionId, r.serving_ref ? this.provisionedVersionsOf(verticalSlug, r.serving_version_id ? { ref: r.serving_ref, versionId: r.serving_version_id } : null, null) : []);
6791
6881
  this.directory
6792
6882
  .prepare(`UPDATE verticals SET serving_ref = ?, serving_version_id = ?,
6793
6883
  serving_do_classes = ?, serving_migration_tag = ? WHERE slug = ?`)
@@ -6821,6 +6911,8 @@ export class SqliteScopeHost {
6821
6911
  // #1756: onto (or off) a serving script, the scope runs other code before its pointer
6822
6912
  // moves. Judged here, so no caller that routes first and binds second passes unasked.
6823
6913
  const bound = scope.vertical_version_id ? readVersion(scope.vertical_version_id) : undefined;
6914
+ if (bound)
6915
+ this.assertBindingShapesKept(mapScope(scope), bound.id, servingRef);
6824
6916
  if (!ack.exportBreak && bound) {
6825
6917
  const breaks = await this.bindBreaks(actor, mapScope(scope), bound, servingRef);
6826
6918
  if (breaks.length > 0)
@@ -9112,6 +9204,11 @@ export class SqliteScopeHost {
9112
9204
  // #1172: which version this scope's provision hook last ran against. Null on every
9113
9205
  // existing row, which reads as "unknown, reconcile once" rather than "up to date".
9114
9206
  this.ensureColumn(this.directory, 'scopes', 'provisioned_version_id', 'provisioned_version_id TEXT');
9207
+ // #2185: the version whose reconcile finished its retirements here. Null on every existing
9208
+ // row, which reads as "not finished": the sweep reconciles each such install once more.
9209
+ this.ensureColumn(this.directory, 'scopes', 'retirements_finished_version_id', 'retirements_finished_version_id TEXT');
9210
+ // #2184: null on every existing row, which reads as unknown (the sweep reconciles it once more).
9211
+ this.ensureColumn(this.directory, 'scopes', 'bound_shapes', 'bound_shapes TEXT');
9115
9212
  this.ensureColumn(this.directory, 'scopes', 'erasure_epoch', 'erasure_epoch INTEGER');
9116
9213
  this.ensureColumn(this.directory, 'scopes', 'reap_claimed_at', 'reap_claimed_at TEXT');
9117
9214
  // §4.8's grace-window timestamp on tenants (mirrors scopes' archived_at).
@@ -9421,6 +9518,8 @@ export class SqliteScopeHost {
9421
9518
  // envelope data module code could set or branch on.
9422
9519
  rt.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
9423
9520
  };
9521
+ // #2192: the call's invocation id, one reader for `ctx.log` and `ctx.requestId`.
9522
+ const requestIdNow = () => rt.invocationId ?? delivery?.invocationId ?? null;
9424
9523
  const ctxRef = {
9425
9524
  tenantId: rt.tenantId,
9426
9525
  scopeId: rt.scopeId,
@@ -9440,13 +9539,17 @@ export class SqliteScopeHost {
9440
9539
  tenantId: rt.tenantId,
9441
9540
  scopeId: rt.scopeId,
9442
9541
  operation: operation ?? null,
9443
- invocationId: () => rt.invocationId ?? delivery?.invocationId ?? null,
9542
+ invocationId: requestIdNow,
9444
9543
  // A consumer runs under the system override, so it logs as `system`.
9445
9544
  principalKind: overrideActor ? 'system' : subject.kind,
9446
9545
  // The string-safe redaction: `redactSecrets` parses its serialization back, and a
9447
9546
  // log's text is not JSON.
9448
9547
  redact: (text) => redactSecretText(text, minted),
9449
9548
  }, this.logSink),
9549
+ // #2192: the same id the log lines carry.
9550
+ get requestId() {
9551
+ return requestIdNow();
9552
+ },
9450
9553
  emit: (event) => writeEvent(event, 'module'),
9451
9554
  requestPlatform: (request) => {
9452
9555
  assertImpersonationWrites(impersonation, 'ctx.requestPlatform');