@cosmicdrift/kumiko-framework 0.330.2 → 0.332.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/db/blind-index-cleanup.d.ts +1 -0
  2. package/dist/db/blind-index-cleanup.js +12 -0
  3. package/dist/db/index.d.ts +1 -1
  4. package/dist/db/index.js +1 -1
  5. package/dist/db/queries/event-consumer.d.ts +2 -1
  6. package/dist/db/queries/event-consumer.js +12 -2
  7. package/dist/engine/boot-validator/action-wiring.js +3 -0
  8. package/dist/engine/boot-validator/config-deps.d.ts +1 -0
  9. package/dist/engine/boot-validator/config-deps.js +24 -0
  10. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  11. package/dist/engine/boot-validator/entity-handler.js +20 -0
  12. package/dist/engine/boot-validator/index.js +6 -2
  13. package/dist/engine/boot-validator/projection-list-screens.js +11 -6
  14. package/dist/engine/boot-validator/query-output-columns.js +23 -2
  15. package/dist/engine/boot-validator/query-refs.d.ts +2 -1
  16. package/dist/engine/boot-validator/query-refs.js +24 -1
  17. package/dist/engine/boot-validator/screens.d.ts +3 -1
  18. package/dist/engine/boot-validator/screens.js +180 -78
  19. package/dist/engine/boot-validator/selector-owner-panels.d.ts +2 -0
  20. package/dist/engine/boot-validator/selector-owner-panels.js +25 -0
  21. package/dist/engine/boot-validator/url-prefill-fields.js +18 -1
  22. package/dist/engine/build-app-schema.js +3 -0
  23. package/dist/engine/build-config-feature-schema.js +40 -6
  24. package/dist/engine/changeset-changes.js +6 -0
  25. package/dist/engine/config-helpers.d.ts +2 -0
  26. package/dist/engine/config-helpers.js +9 -0
  27. package/dist/engine/entity-handlers.js +1 -1
  28. package/dist/engine/feature-ui-extensions.d.ts +4 -2
  29. package/dist/engine/feature-ui-extensions.js +40 -3
  30. package/dist/engine/index.d.ts +1 -1
  31. package/dist/engine/project-app-schema-for-roles.js +30 -2
  32. package/dist/engine/registry-facade.js +3 -0
  33. package/dist/engine/registry-ingest.js +2 -0
  34. package/dist/engine/registry-state.d.ts +2 -1
  35. package/dist/engine/registry-state.js +1 -0
  36. package/dist/engine/schema-builder.d.ts +3 -1
  37. package/dist/engine/schema-builder.js +5 -6
  38. package/dist/engine/types/index.d.ts +2 -2
  39. package/dist/errors/reasons.js +7 -6
  40. package/dist/i18n/required-surface-keys.js +27 -10
  41. package/dist/pipeline/dispatch-write.js +26 -2
  42. package/dist/pipeline/event-consumer-state.js +6 -0
  43. package/dist/pipeline/event-dispatcher-admin.js +36 -16
  44. package/dist/pipeline/event-dispatcher-delivery.d.ts +2 -0
  45. package/dist/pipeline/event-dispatcher-delivery.js +22 -2
  46. package/dist/pipeline/event-dispatcher.js +1 -0
  47. package/dist/ui-types/index.d.ts +1 -1
  48. package/package.json +4 -4
  49. package/src/changes.json +54 -0
@@ -71,7 +71,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
71
71
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
72
72
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
73
73
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
74
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RecordHeaderSpec, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
74
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
75
75
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
76
76
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
77
77
  export { runValidation } from "./validation.js";
@@ -55,6 +55,7 @@
55
55
  // - WorkspaceSchema.definition.nav — the raw explicit nav-QN list,
56
56
  // verbatim in JSON, distinct from the computed/filtered navMembers
57
57
  // - entityList.createScreen (same-feature target, dropped when not kept)
58
+ // - entityList.expandableRow (a relatedList: same projection as a relatedList section)
58
59
  // - listScreenId (projectionDetail/entityEdit/actionForm/secretMint/custom —
59
60
  // the breadcrumb "back to list" target). It's serialized, so a visible
60
61
  // screen would otherwise disclose a denied screen's short id. Resolved
@@ -400,6 +401,22 @@ function projectMetrics(metrics, indices, keptScreenQns) {
400
401
  });
401
402
  return changed ? next : metrics;
402
403
  }
404
+ function projectHeader(header, indices, keptScreenQns) {
405
+ const subtitle = header?.subtitle;
406
+ if (header === undefined || typeof subtitle === "string" || subtitle === undefined) {
407
+ return header;
408
+ }
409
+ let changed = false;
410
+ const nextParts = subtitle.map((part) => {
411
+ if (typeof part === "string" || part.navigate === undefined)
412
+ return part;
413
+ if (keepMetricNavigate(part.navigate, indices, keptScreenQns))
414
+ return part;
415
+ changed = true;
416
+ return part.field;
417
+ });
418
+ return changed ? { ...header, subtitle: nextParts } : header;
419
+ }
403
420
  function projectDashboardPanels(panels, feature, keptScreenQns) {
404
421
  const kept = panels.filter((p) => p.kind !== "screen" || isScreenTargetKept(feature.featureName, p.screen, keptScreenQns));
405
422
  return kept.length === panels.length ? panels : kept;
@@ -433,21 +450,31 @@ function projectListScreen(original, feature, indices, keptScreenQns) {
433
450
  const { rowActions, toolbarActions, ...rest } = screen;
434
451
  const nextRowActions = projectRowActionArray(rowActions, feature, indices, keptScreenQns);
435
452
  const nextToolbarActions = projectToolbarActionArray(toolbarActions, feature, indices, keptScreenQns);
436
- if (nextRowActions === rowActions && nextToolbarActions === toolbarActions)
453
+ const expandableRow = screen.type === "entityList" ? screen.expandableRow : undefined;
454
+ const nextExpandableRow = expandableRow === undefined
455
+ ? undefined
456
+ : projectRelatedListSection(expandableRow, feature, indices, keptScreenQns);
457
+ if (nextRowActions === rowActions &&
458
+ nextToolbarActions === toolbarActions &&
459
+ nextExpandableRow === expandableRow) {
437
460
  return screen;
461
+ }
438
462
  return {
439
463
  ...rest,
440
464
  ...(nextRowActions !== undefined && { rowActions: nextRowActions }),
441
465
  ...(nextToolbarActions !== undefined && { toolbarActions: nextToolbarActions }),
466
+ ...(nextExpandableRow !== undefined && { expandableRow: nextExpandableRow }),
442
467
  };
443
468
  }
444
469
  function projectProjectionDetailScreen(screen, feature, indices, keptScreenQns) {
445
- const { metrics, actions, layout, listScreenId, ...rest } = screen;
470
+ const { metrics, header, actions, layout, listScreenId, ...rest } = screen;
446
471
  const nextMetrics = projectMetrics(metrics, indices, keptScreenQns);
472
+ const nextHeader = projectHeader(header, indices, keptScreenQns);
447
473
  const nextActions = projectRowActionArray(actions, feature, indices, keptScreenQns);
448
474
  const nextLayout = projectEditLayout(layout, feature, indices, keptScreenQns);
449
475
  const nextListScreenId = projectListScreenId(listScreenId, indices, keptScreenQns);
450
476
  const unchanged = nextMetrics === metrics &&
477
+ nextHeader === header &&
451
478
  nextActions === actions &&
452
479
  nextLayout === layout &&
453
480
  nextListScreenId === listScreenId;
@@ -456,6 +483,7 @@ function projectProjectionDetailScreen(screen, feature, indices, keptScreenQns)
456
483
  return {
457
484
  ...rest,
458
485
  layout: nextLayout,
486
+ ...(nextHeader !== undefined && { header: nextHeader }),
459
487
  ...(nextMetrics !== undefined && { metrics: nextMetrics }),
460
488
  ...(nextActions !== undefined && { actions: nextActions }),
461
489
  ...(nextListScreenId !== undefined && { listScreenId: nextListScreenId }),
@@ -149,6 +149,9 @@ export function buildRegistryFacade(state) {
149
149
  getAllExtensionSelectors() {
150
150
  return state.extensionSelectorMap;
151
151
  },
152
+ getExtensionSelectorPanels(extensionName) {
153
+ return state.extensionSelectorPanelsMap.get(extensionName) ?? [];
154
+ },
152
155
  getAllNotifications() {
153
156
  return state.notificationMap;
154
157
  },
@@ -189,6 +189,8 @@ export function populateExtensionsAndSeeds(state, feature) {
189
189
  `(feature "${feature.name}") — one owning feature declares the selector.`);
190
190
  }
191
191
  state.extensionSelectorMap.set(sel.extensionName, sel.qualifiedKey);
192
+ if (sel.panels !== undefined)
193
+ state.extensionSelectorPanelsMap.set(sel.extensionName, sel.panels);
192
194
  }
193
195
  state.allReferenceData.push(...(feature.referenceData ?? []));
194
196
  state.allConfigSeeds.push(...(feature.configSeeds ?? []));
@@ -1,5 +1,5 @@
1
1
  import { type QnType, qualifyEntityName } from "./qualified-name.js";
2
- import type { AuthClaimsHookDef, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventDef, EventUpcastFn, FeatureDefinition, FeatureMetricDef, HookPhase, JobDefinition, MultiStreamProjectionDefinition, NavDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, ScreenDefinition, SearchPayloadContributorFn, SecretKeyDefinition, StoreTableDef, StreamHandlerDef, TranslationValue, TreeActionDef, WorkspaceDefinition, WriteHandlerDef } from "./types/index.js";
2
+ import type { AuthClaimsHookDef, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventDef, EventUpcastFn, ExtensionSelectorPanel, FeatureDefinition, FeatureMetricDef, HookPhase, JobDefinition, MultiStreamProjectionDefinition, NavDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, ScreenDefinition, SearchPayloadContributorFn, SecretKeyDefinition, StoreTableDef, StreamHandlerDef, TranslationValue, TreeActionDef, WorkspaceDefinition, WriteHandlerDef } from "./types/index.js";
3
3
  export type IncomingRelation = {
4
4
  sourceEntity: string;
5
5
  relationName: string;
@@ -46,6 +46,7 @@ export type RegistryState = {
46
46
  extensionMap: Map<string, RegistrarExtensionDef>;
47
47
  extensionUsages: RegistrarExtensionRegistration[];
48
48
  extensionSelectorMap: Map<string, string>;
49
+ extensionSelectorPanelsMap: Map<string, readonly ExtensionSelectorPanel[]>;
49
50
  allReferenceData: ReferenceDataDef[];
50
51
  allConfigSeeds: ConfigSeedDef[];
51
52
  mergedTranslations: Record<string, Record<string, TranslationValue>>;
@@ -116,6 +116,7 @@ export function createInitialState() {
116
116
  extensionMap: new Map(),
117
117
  extensionUsages: [],
118
118
  extensionSelectorMap: new Map(),
119
+ extensionSelectorPanelsMap: new Map(),
119
120
  allReferenceData: [],
120
121
  allConfigSeeds: [],
121
122
  mergedTranslations: {},
@@ -4,5 +4,7 @@ export declare function isRepresentableAtScale(n: number, scale: number): boolea
4
4
  export declare function fieldToZod(field: FieldDefinition, currencies: readonly string[], opts?: {
5
5
  readonly applyDefaults?: boolean;
6
6
  }): z.ZodTypeAny;
7
- export declare function buildInsertSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
7
+ export declare function buildInsertSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[], options?: {
8
+ readonly allowCallerId?: boolean;
9
+ }): z.ZodObject<Record<string, z.ZodTypeAny>>;
8
10
  export declare function buildUpdateSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
@@ -366,7 +366,7 @@ function applyTotalsMatchRefinements(entity, schema) {
366
366
  function notWritableField() {
367
367
  return z.never().optional();
368
368
  }
369
- export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = []) {
369
+ export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = [], options = {}) {
370
370
  const shape = {};
371
371
  for (const [name, field] of Object.entries(entity.fields)) {
372
372
  if (excludedFields.includes(name)) {
@@ -378,11 +378,10 @@ export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES],
378
378
  const isRequired = "required" in field && field.required === true;
379
379
  shape[name] = isRequired || hasDefault ? zodField : zodField.optional();
380
380
  }
381
- // Caller-chosen aggregate id for idempotent creates (deterministic-id
382
- // patterns). Accepting it here only lets it survive parsing — the create
383
- // handler still honors it only for a system-identity caller (jobs/hooks
384
- // via createSystemUser), never for an ordinary HTTP-authenticated user.
385
- shape["id"] = z.uuid().optional();
381
+ // Opt-in only for the generic create verb, which honors the id for system identities;
382
+ // other schema consumers must keep stripping an unknown `id`.
383
+ if (options.allowCallerId)
384
+ shape["id"] = z.uuid().optional();
386
385
  return applyTotalsMatchRefinements(entity, z.object(shape));
387
386
  }
388
387
  export function buildUpdateSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = []) {
@@ -1,5 +1,5 @@
1
1
  export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
2
- export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
2
+ export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
3
3
  export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
4
4
  export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
5
5
  export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
17
17
  export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
18
18
  export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
19
19
  export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
20
- export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
20
+ export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
21
21
  export { metricField } from "@cosmicdrift/kumiko-types/screen";
22
22
  export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
23
23
  export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
@@ -66,16 +66,17 @@ export const FrameworkReasons = {
66
66
  // sister catalog to FrameworkReasons instead of extending it — see the
67
67
  // file-header note above for why these carry the `agent.` prefix.
68
68
  export const AgentReasons = {
69
- // AccessDeniedError: the agent called a tool name outside its allowed
70
- // catalog for this run.
69
+ // Error turn in the agent loop when the model requests a tool outside the
70
+ // run's catalog; UnprocessableError in rule validation (handler missing from
71
+ // the caller's role-filtered manifest, or `ai-agent-edit` not mounted).
71
72
  toolNotAllowed: "agent.tool_not_allowed",
72
- // UnprocessableError: the agent loop hit its maximum round count before
73
- // producing a final answer.
73
+ // Not thrown: the run ends with a `clarify` turn (`payload.reason`) once
74
+ // MAX_AGENT_ROUNDS is reached.
74
75
  iterationLimit: "agent.iteration_limit",
75
76
  // AccessDeniedError: the tool exists, but the caller's roles don't cover
76
77
  // the handler behind it.
77
78
  permissionDenied: "agent.permission_denied",
78
- // AccessDeniedError: an `agent.risk: "high"` handler was invoked without
79
- // the required explicit confirmation.
79
+ // UnprocessableError: a permission rule with `always` was requested for an
80
+ // `agent: { risk: "high" }` handler; `details.handlerQn` names it.
80
81
  highRiskNoAlways: "agent.high_risk_no_always",
81
82
  };
@@ -107,6 +107,30 @@ function pushToolbarActionKeys(out, action) {
107
107
  // toolbar button AND the Drawer title, see ToolbarDrawerHost) is
108
108
  // already pushed above.
109
109
  }
110
+ function pushRelatedListSectionKeys(out, section) {
111
+ pushKey(out, section.title);
112
+ pushKey(out, section.description);
113
+ pushKey(out, section.itemNoun);
114
+ for (const col of section.columns) {
115
+ const normalized = normalizeListColumn(col);
116
+ if (normalized.label !== undefined)
117
+ pushKey(out, normalized.label);
118
+ }
119
+ for (const action of section.rowActions ?? [])
120
+ pushRowActionKeys(out, action);
121
+ }
122
+ function pushExpandableRowKeys(out, section) {
123
+ pushRelatedListSectionKeys(out, section);
124
+ for (const action of section.actions ?? [])
125
+ pushRowActionKeys(out, action);
126
+ for (const action of section.toolbarActions ?? [])
127
+ pushToolbarActionKeys(out, action);
128
+ pushKey(out, section.emptyState?.title);
129
+ pushKey(out, section.emptyState?.description);
130
+ if (section.emptyState?.action !== undefined) {
131
+ pushRowActionKeys(out, section.emptyState.action);
132
+ }
133
+ }
110
134
  export function requiredKeysFromScreen(featureName, screen, options = {}) {
111
135
  const { treatDotFormAsKey = false } = options;
112
136
  const out = new Set();
@@ -129,6 +153,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
129
153
  pushRowActionKeys(out, action);
130
154
  for (const action of list.toolbarActions ?? [])
131
155
  pushToolbarActionKeys(out, action);
156
+ if (list.expandableRow !== undefined)
157
+ pushExpandableRowKeys(out, list.expandableRow);
132
158
  break;
133
159
  }
134
160
  case "projectionList": {
@@ -301,16 +327,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
301
327
  continue;
302
328
  }
303
329
  if (section.kind === "relatedList") {
304
- pushKey(out, section.title);
305
- pushKey(out, section.description);
306
- pushKey(out, section.itemNoun);
307
- for (const col of section.columns) {
308
- const normalized = normalizeListColumn(col);
309
- if (normalized.label !== undefined)
310
- pushKey(out, normalized.label);
311
- }
312
- for (const action of section.rowActions ?? [])
313
- pushRowActionKeys(out, action);
330
+ pushRelatedListSectionKeys(out, section);
314
331
  continue;
315
332
  }
316
333
  if (isWriteFormEditSection(section)) {
@@ -1,6 +1,7 @@
1
1
  import { selectRowForUpdateById } from "../db/queries/entity-read.js";
2
2
  import { asEntityTableMeta, selectMany } from "../db/query.js";
3
3
  import { buildEntityTable, toSnakeCase } from "../db/table-builder.js";
4
+ import { createTenantDb, hasTenantColumn } from "../db/tenant-db.js";
4
5
  import { tenantDbRunner } from "../db/tenant-db-runner.js";
5
6
  import { hasAccess } from "../engine/access.js";
6
7
  import { ConfigScopes } from "../engine/constants.js";
@@ -126,6 +127,24 @@ function isForeignTenantParentRow(parentRow, user) {
126
127
  const rowTenantId = parentRow["tenantId"];
127
128
  return typeof rowTenantId === "string" && rowTenantId !== user.tenantId;
128
129
  }
130
+ // A custom create handler may return a row without a string tenantId; the row's
131
+ // tenant is then unknown from the payload, so re-read it through the caller's tenant
132
+ // filter. Fail-closed: anything that cannot be verified counts as not visible.
133
+ // Tenant reads also return SYSTEM reference rows, so the row's own tenantId must match
134
+ // (same rule as the string path) unless the entity has no tenant column.
135
+ async function isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user, tx) {
136
+ if (parentEntityName === undefined)
137
+ return true;
138
+ const table = getTable(ctx, parentEntityName);
139
+ const source = resolveDbSource(ctx, tx);
140
+ if (!table || !source)
141
+ return true;
142
+ const callerDb = createTenantDb(source, user.tenantId, "tenant");
143
+ const visibleRow = await callerDb.fetchOne(table, { id: parentId });
144
+ if (visibleRow === undefined)
145
+ return true;
146
+ return hasTenantColumn(table) && visibleRow["tenantId"] !== user.tenantId;
147
+ }
129
148
  // Nested-write orchestration (v1: depth=1, create-only, hasMany-only).
130
149
  //
131
150
  // When a parent `:create` handler's payload carries values under keys
@@ -199,13 +218,18 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
199
218
  }));
200
219
  }
201
220
  // A custom create handler may return an existing foreign row; verify it before attaching children.
202
- if (!registry.isHandlerSystemScoped(type) && isForeignTenantParentRow(parentRow, user)) {
221
+ const parentEntityName = registry.getHandlerEntity(type);
222
+ const isCallerScoped = !registry.isHandlerSystemScoped(type);
223
+ const isParentForeign = isCallerScoped &&
224
+ (typeof parentRow["tenantId"] === "string"
225
+ ? isForeignTenantParentRow(parentRow, user)
226
+ : await isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user, tx));
227
+ if (isParentForeign) {
203
228
  return writeFailure(new AccessDeniedError({
204
229
  message: `nested-write: parent row belongs to another tenant — refusing to attach children to "${type}"`,
205
230
  details: { reason: FrameworkReasons.fieldAccessDenied, handler: type },
206
231
  }));
207
232
  }
208
- const parentEntityName = registry.getHandlerEntity(type);
209
233
  if (parentEntityName) {
210
234
  const parentEntity = registry.getEntity(parentEntityName);
211
235
  if (parentEntity) {
@@ -29,6 +29,10 @@ export const eventConsumerStateTable = pgTable("kumiko_event_consumers", {
29
29
  // skipPoisonEvent() — an operator vouching the consumer is healthy.
30
30
  rearmCount: integer("rearm_count").notNull().default(0),
31
31
  pendingGaps: jsonb("pending_gaps").$type().default([]).notNull(),
32
+ // Id of the event whose handler failed last (halt-on-poison). skipPoisonEvent
33
+ // skips exactly this id; guessing the smallest visible id would drop a healthy
34
+ // writer that committed late into a pending gap. Null once a delivery succeeds.
35
+ lastFailedEventId: bigint("last_failed_event_id", { mode: "bigint" }),
32
36
  lastError: text("last_error"),
33
37
  updatedAt: instant("updated_at", { precision: 3 }).notNull().default(sql `now()`),
34
38
  }, (t) => ({
@@ -78,6 +82,8 @@ export async function createEventConsumerStateTable(db) {
78
82
  /* ifNotExists */ true);
79
83
  await alterTableAddColumn(db, "kumiko_event_consumers", "pending_gaps", "jsonb", " DEFAULT '[]'::jsonb", " NOT NULL",
80
84
  /* ifNotExists */ true);
85
+ await alterTableAddColumn(db, "kumiko_event_consumers", "last_failed_event_id", "bigint", "", "",
86
+ /* ifNotExists */ true);
81
87
  // Runs on every boot, including mid-rolling-deploy: a new pod can delete
82
88
  // an old pod's still-in-use per-instance row out from under it. That old
83
89
  // pod's next acquireConsumer then finds no row, delivers no more SSE/
@@ -1,8 +1,8 @@
1
- import { advanceConsumerPastEventReturning, removePendingGapReturning, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
1
+ import { advanceConsumerPastEventReturning, removePendingGapReturning, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
2
2
  import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
3
3
  import { coerceRow, extractTableInfo, selectMany } from "../db/query.js";
4
4
  import { getEventsHighWaterMark } from "../event-store/index.js";
5
- import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
5
+ import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
6
6
  import { rangeContainsId, splitRangeExcludingIds, toIdRanges } from "./pending-gap-ranges.js";
7
7
  // --- Ops recovery surface ---
8
8
  //
@@ -76,29 +76,38 @@ export async function enableConsumer(db, name, instanceId = SHARED_INSTANCE_SENT
76
76
  }
77
77
  return applyConsumerStatusTransition(db, name, instanceId, "idle");
78
78
  }
79
- // skipPoisonEvent advances past whatever is currently blocking the consumer.
80
- // If the smallest visible id inside pending_gaps exists, THAT'S the poison —
81
- // halt-on-poison walks gap ids before any id past the cursor — so it's split
82
- // out of its range directly; the cursor stays put (it's already above this
83
- // id). Otherwise the poison is the usual next event after the cursor. Single
84
- // TX so concurrent dispatcher passes can't double-advance. Neither exists →
85
- // idempotent no-op.
79
+ // skipPoisonEvent skips the event whose handler failed last
80
+ // (last_failed_event_id, set by the delivery's halt-on-poison branch): inside
81
+ // pending_gaps it is split out of its range, otherwise the cursor moves onto
82
+ // it. Without a recorded id (consumer parked before that column existed) it
83
+ // falls back to a guess: the smallest visible pending-gap id, else the next
84
+ // event after the cursor. Single TX so concurrent dispatcher passes can't
85
+ // double-advance. Nothing to skip → idempotent no-op.
86
86
  export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
87
87
  const before = await requireConsumerRow(db, name, instanceId);
88
88
  return db.begin(async (tx) => {
89
89
  const pendingGaps = before.pendingGaps;
90
- const smallestVisiblePending = await selectSmallestVisibleIdInRanges(tx, toIdRanges(pendingGaps));
91
- if (smallestVisiblePending !== null) {
92
- const newPendingGaps = pendingGaps.flatMap((gap) => rangeContainsId(gap, smallestVisiblePending)
93
- ? splitRangeExcludingIds(gap, [smallestVisiblePending])
90
+ const failedEventId = before.lastFailedEventId;
91
+ const failedIsPending = failedEventId !== null && pendingGaps.some((gap) => rangeContainsId(gap, failedEventId));
92
+ const failedIsAboveCursor = failedEventId !== null && failedEventId > before.lastProcessedEventId;
93
+ const pendingPoisonId = failedIsPending
94
+ ? failedEventId
95
+ : failedIsAboveCursor
96
+ ? null
97
+ : await selectSmallestVisibleIdInRanges(tx, toIdRanges(pendingGaps));
98
+ if (pendingPoisonId !== null) {
99
+ const newPendingGaps = pendingGaps.flatMap((gap) => rangeContainsId(gap, pendingPoisonId)
100
+ ? splitRangeExcludingIds(gap, [pendingPoisonId])
94
101
  : [gap]);
95
102
  const raw = await removePendingGapReturning(tx, name, instanceId, newPendingGaps);
96
103
  const updated = raw && coerceRow(raw, extractTableInfo(eventConsumerStateTable));
97
104
  if (!updated)
98
105
  throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`);
99
- return { ...normalizeConsumerState(updated), skippedEventId: smallestVisiblePending };
106
+ return { ...normalizeConsumerState(updated), skippedEventId: pendingPoisonId };
100
107
  }
101
- const poisonId = await selectNextEventIdAfter(tx, before.lastProcessedEventId);
108
+ const poisonId = failedIsAboveCursor
109
+ ? failedEventId
110
+ : await selectNextEventIdAfter(tx, before.lastProcessedEventId);
102
111
  if (poisonId === null) {
103
112
  const [unchanged] = await selectMany(tx, eventConsumerStateTable, {
104
113
  name,
@@ -108,7 +117,18 @@ export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SEN
108
117
  throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
109
118
  return { ...normalizeConsumerState(unchanged), skippedEventId: null };
110
119
  }
111
- const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId);
120
+ // Delivery stops at the poison event, so ids between the cursor and it were
121
+ // invisible during that pass (in-flight commits). Jumping the cursor over them
122
+ // would lose them; keep them as a pending gap like the dispatcher's own gaps.
123
+ const skippedWindowStart = before.lastProcessedEventId + 1n;
124
+ const gapToAdd = poisonId > skippedWindowStart
125
+ ? {
126
+ from: skippedWindowStart.toString(),
127
+ to: (poisonId - 1n).toString(),
128
+ xmax: await selectSnapshotXmax(tx),
129
+ }
130
+ : null;
131
+ const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId, gapToAdd);
112
132
  const updated = raw && coerceRow(raw, extractTableInfo(eventConsumerStateTable));
113
133
  if (!updated)
114
134
  throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`);
@@ -13,6 +13,7 @@ export type ConsumerStateRowShape = {
13
13
  readonly attempts: number;
14
14
  readonly rearmCount: number;
15
15
  readonly pendingGaps: readonly PendingGapEntry[];
16
+ readonly lastFailedEventId: bigint | null;
16
17
  readonly lastError: string | null;
17
18
  readonly updatedAt: Temporal.Instant;
18
19
  };
@@ -58,6 +59,7 @@ export type DeliveryOutcome = {
58
59
  readonly deadLettered: boolean;
59
60
  readonly processed: number;
60
61
  readonly failed: number;
62
+ readonly failedEventId: bigint | null;
61
63
  readonly resolvedPendingIds: readonly bigint[];
62
64
  };
63
65
  export declare function deliverEvents(consumer: EventConsumer, events: ReadonlyArray<StoredEventRow>, context: AppContext, maxAttempts: number, state: ConsumerStateRow): Promise<DeliveryOutcome>;
@@ -171,6 +171,7 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
171
171
  const effectiveMaxAttempts = consumer.errorPolicy?.maxAttempts ?? maxAttempts;
172
172
  let processed = 0;
173
173
  let failed = 0;
174
+ let failedEventId = null;
174
175
  const resolvedPendingIds = [];
175
176
  // A pending row sits below startCursor: it resolves its gap but never moves
176
177
  // the cursor backward. ORDER BY id walks all pending rows first.
@@ -188,7 +189,16 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
188
189
  for (const row of events)
189
190
  resolve(row.id);
190
191
  processed = events.length;
191
- return { cursor, attempts, lastError, deadLettered, processed, failed, resolvedPendingIds };
192
+ return {
193
+ cursor,
194
+ attempts,
195
+ lastError,
196
+ deadLettered,
197
+ processed,
198
+ failed,
199
+ failedEventId,
200
+ resolvedPendingIds,
201
+ };
192
202
  }
193
203
  // fall through to the per-event loop below; the batch failure itself
194
204
  // does NOT bump attempts — only a per-event failure does.
@@ -209,12 +219,22 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
209
219
  }
210
220
  attempts += 1;
211
221
  lastError = errMessage;
222
+ failedEventId = row.id;
212
223
  if (attempts >= effectiveMaxAttempts)
213
224
  deadLettered = true;
214
225
  break;
215
226
  }
216
227
  }
217
- return { cursor, attempts, lastError, deadLettered, processed, failed, resolvedPendingIds };
228
+ return {
229
+ cursor,
230
+ attempts,
231
+ lastError,
232
+ deadLettered,
233
+ processed,
234
+ failed,
235
+ failedEventId,
236
+ resolvedPendingIds,
237
+ };
218
238
  }
219
239
  // Shared requestContext scope for one apply — event-derived fields
220
240
  // (correlationId, causationId, writeOrigin) come from `causationSource`;
@@ -331,6 +331,7 @@ export function createEventDispatcher(options) {
331
331
  deadLettered: false,
332
332
  processed: 0,
333
333
  failed: 0,
334
+ failedEventId: acquired.state.lastFailedEventId,
334
335
  resolvedPendingIds: [],
335
336
  };
336
337
  processed = outcome.processed;
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
11
11
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
12
- export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
12
+ export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
13
13
  export { metricField } from "../engine/types/screen.js";
14
14
  export type { TargetRef } from "../engine/types/target-ref.js";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.330.2",
3
+ "version": "0.332.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -206,8 +206,8 @@
206
206
  "./package.json": "./package.json"
207
207
  },
208
208
  "dependencies": {
209
- "@cosmicdrift/kumiko-http": "0.330.2",
210
- "@cosmicdrift/kumiko-types": "0.330.2",
209
+ "@cosmicdrift/kumiko-http": "0.332.0",
210
+ "@cosmicdrift/kumiko-types": "0.332.0",
211
211
  "bullmq": "^5.76.7",
212
212
  "bun-types": "^1.3.13",
213
213
  "hono": "^4.13.1",
@@ -223,7 +223,7 @@
223
223
  "zod": "^4.4.3"
224
224
  },
225
225
  "devDependencies": {
226
- "@cosmicdrift/kumiko-dispatcher-live": "0.330.2",
226
+ "@cosmicdrift/kumiko-dispatcher-live": "0.332.0",
227
227
  "bun-types": "^1.3.13",
228
228
  "pino-pretty": "^13.1.3"
229
229
  },