@sixb/core 0.1.14 → 0.1.15

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 (206) hide show
  1. package/dist/actions/index.js +21 -21
  2. package/dist/agents/index.js +33 -33
  3. package/dist/agents/index.js.map +1 -1
  4. package/dist/agents/streams/index.js +2 -2
  5. package/dist/agents/streams/internal.js +2 -2
  6. package/dist/auth/index.js +3 -3
  7. package/dist/authorization/index.js +2 -2
  8. package/dist/bootstrap/index.js +36 -36
  9. package/dist/{chunk-dv4ct8mp.js → chunk-1jvq82rp.js} +3 -3
  10. package/dist/{chunk-16jenmzw.js → chunk-2tz5vnvz.js} +28 -28
  11. package/dist/{chunk-16jenmzw.js.map → chunk-2tz5vnvz.js.map} +1 -1
  12. package/dist/{chunk-728fp090.js → chunk-3c90jvfj.js} +2 -2
  13. package/dist/{chunk-vn3yh259.js → chunk-4gmng3vf.js} +1 -1
  14. package/dist/{chunk-hxgj4kzd.js → chunk-4jph1t3v.js} +7 -7
  15. package/dist/{chunk-znw0zvy4.js → chunk-4wn7bxxa.js} +25 -7
  16. package/dist/chunk-4wn7bxxa.js.map +12 -0
  17. package/dist/{chunk-3qpcrcj8.js → chunk-6aawea28.js} +2 -4
  18. package/dist/chunk-6aawea28.js.map +10 -0
  19. package/dist/{chunk-rzkr70gr.js → chunk-6fsr7y93.js} +5 -1
  20. package/dist/{chunk-rzkr70gr.js.map → chunk-6fsr7y93.js.map} +3 -3
  21. package/dist/{chunk-y487ywrv.js → chunk-6q7c68yx.js} +4 -4
  22. package/dist/{chunk-ypj2wqtj.js → chunk-70jmg2zx.js} +2 -2
  23. package/dist/{chunk-wn7mznxc.js → chunk-7jc21943.js} +3 -3
  24. package/dist/{chunk-rzcjp5m2.js → chunk-8fjmqjyx.js} +1 -1
  25. package/dist/{chunk-m8zkvep1.js → chunk-8w27xf9e.js} +1 -1
  26. package/dist/{chunk-z9h698s8.js → chunk-a4g5yjm0.js} +1 -1
  27. package/dist/{chunk-4ypbe95r.js → chunk-adbve9zt.js} +1 -1
  28. package/dist/{chunk-hj0n3c2p.js → chunk-dqkd3kdj.js} +40 -22
  29. package/dist/{chunk-hj0n3c2p.js.map → chunk-dqkd3kdj.js.map} +3 -3
  30. package/dist/{chunk-wnxabtn4.js → chunk-ex8s8wsk.js} +6 -6
  31. package/dist/{chunk-4tnrzqqh.js → chunk-fj1dvp37.js} +1 -1
  32. package/dist/{chunk-3fpgxpsq.js → chunk-fjw417jf.js} +127 -69
  33. package/dist/{chunk-3fpgxpsq.js.map → chunk-fjw417jf.js.map} +4 -4
  34. package/dist/{chunk-ht4a7mjg.js → chunk-g4rbm4a6.js} +42 -8
  35. package/dist/{chunk-ht4a7mjg.js.map → chunk-g4rbm4a6.js.map} +3 -3
  36. package/dist/{chunk-qma80ych.js → chunk-hn2r7z41.js} +3 -2
  37. package/dist/chunk-hn2r7z41.js.map +11 -0
  38. package/dist/{chunk-bdhfn0ww.js → chunk-kkqwzy6h.js} +1 -1
  39. package/dist/{chunk-ww185kt6.js → chunk-mmac1mp6.js} +2 -2
  40. package/dist/{chunk-jfdggg1p.js → chunk-n5t4mq6z.js} +5 -5
  41. package/dist/{chunk-3a338wrp.js → chunk-p0nbkys4.js} +1 -1
  42. package/dist/{chunk-kxea54yg.js → chunk-py2dw8cw.js} +3 -3
  43. package/dist/{chunk-e2c5yxfe.js → chunk-q3grgzp1.js} +2 -2
  44. package/dist/{chunk-6rzb1g6h.js → chunk-qne45xqq.js} +1 -1
  45. package/dist/{chunk-cbn96bve.js → chunk-r5sc1d2b.js} +2 -2
  46. package/dist/{chunk-y6vnvayj.js → chunk-vjcd8d27.js} +1 -1
  47. package/dist/{chunk-ma1mhh7w.js → chunk-wd34wnsy.js} +6 -6
  48. package/dist/{chunk-pvgpckm1.js → chunk-wem5vqjw.js} +7 -7
  49. package/dist/{chunk-gct3m1da.js → chunk-xr7y35yj.js} +3 -3
  50. package/dist/{chunk-q2xghyft.js → chunk-y0zmae99.js} +5 -5
  51. package/dist/{chunk-xgptywp0.js → chunk-y2aq6ag9.js} +1 -1
  52. package/dist/{chunk-yvf2jqj8.js → chunk-yfppyaty.js} +1 -1
  53. package/dist/{chunk-00pfcqh4.js → chunk-z708gemf.js} +129 -70
  54. package/dist/{chunk-00pfcqh4.js.map → chunk-z708gemf.js.map} +5 -5
  55. package/dist/deploy/define.d.ts +4 -0
  56. package/dist/deploy/define.d.ts.map +1 -0
  57. package/dist/deploy/index.d.ts +3 -0
  58. package/dist/deploy/index.d.ts.map +1 -0
  59. package/dist/deploy/index.js +10 -0
  60. package/dist/deploy/index.js.map +10 -0
  61. package/dist/deploy/internal.d.ts +3 -0
  62. package/dist/deploy/internal.d.ts.map +1 -0
  63. package/dist/deploy/internal.js +244 -0
  64. package/dist/deploy/internal.js.map +11 -0
  65. package/dist/deploy/services.d.ts +8 -0
  66. package/dist/deploy/services.d.ts.map +1 -0
  67. package/dist/deploy/types.d.ts +309 -0
  68. package/dist/deploy/types.d.ts.map +1 -0
  69. package/dist/deploy/validation.d.ts +7 -0
  70. package/dist/deploy/validation.d.ts.map +1 -0
  71. package/dist/error-reporting/internal.js +3 -3
  72. package/dist/errors/catalog.d.ts +4 -0
  73. package/dist/errors/catalog.d.ts.map +1 -1
  74. package/dist/errors/internal.js +1 -1
  75. package/dist/events/index.js +3 -3
  76. package/dist/execution/agent.js +32 -32
  77. package/dist/execution/primitive.js +28 -28
  78. package/dist/execution/request.js +30 -30
  79. package/dist/execution/request.js.map +1 -1
  80. package/dist/index.js +48 -48
  81. package/dist/index.js.map +1 -1
  82. package/dist/maintenance/ontology-maintenance.d.ts +2 -0
  83. package/dist/maintenance/ontology-maintenance.d.ts.map +1 -1
  84. package/dist/maintenance/types.d.ts +8 -2
  85. package/dist/maintenance/types.d.ts.map +1 -1
  86. package/dist/materialization/dataset-watermark.d.ts +6 -1
  87. package/dist/materialization/dataset-watermark.d.ts.map +1 -1
  88. package/dist/materialization/index.js +1 -1
  89. package/dist/materialization/model.d.ts +6 -0
  90. package/dist/materialization/model.d.ts.map +1 -1
  91. package/dist/materializer/index.js +17 -17
  92. package/dist/materializer/index.js.map +1 -1
  93. package/dist/materializer/projections/finish-run.d.ts.map +1 -1
  94. package/dist/materializer/projections/replace.d.ts.map +1 -1
  95. package/dist/materializer/projections/source-materialization.d.ts +6 -2
  96. package/dist/materializer/projections/source-materialization.d.ts.map +1 -1
  97. package/dist/models/execution/index.js +13 -13
  98. package/dist/objects/index.js +20 -20
  99. package/dist/objects/query/index.js +14 -14
  100. package/dist/pipelines/index.js +18 -18
  101. package/dist/projections/internal.js +15 -15
  102. package/dist/projections/types.d.ts +1 -1
  103. package/dist/projections/types.d.ts.map +1 -1
  104. package/dist/queues/in-memory.d.ts +1 -1
  105. package/dist/queues/in-memory.d.ts.map +1 -1
  106. package/dist/runtime/internal.js +18 -18
  107. package/dist/shares/internal.js +13 -13
  108. package/dist/shares/internal.js.map +1 -1
  109. package/dist/storage/action-runs/failure.js +2 -2
  110. package/dist/storage/ai-limits/enforcement.js +2 -2
  111. package/dist/storage/connector-connections/provider.js +1 -1
  112. package/dist/storage/file-upload-sessions/provider.js +4 -4
  113. package/dist/storage/index.d.ts +2 -2
  114. package/dist/storage/index.d.ts.map +1 -1
  115. package/dist/storage/index.js +28 -28
  116. package/dist/storage/index.js.map +1 -1
  117. package/dist/storage/internal.js +1 -1
  118. package/dist/storage/ontology/in-memory/sources.d.ts +6 -3
  119. package/dist/storage/ontology/in-memory/sources.d.ts.map +1 -1
  120. package/dist/storage/ontology/index.d.ts +1 -1
  121. package/dist/storage/ontology/index.d.ts.map +1 -1
  122. package/dist/storage/ontology/provider.d.ts +16 -3
  123. package/dist/storage/ontology/provider.d.ts.map +1 -1
  124. package/dist/storage/ontology/provider.js +8 -4
  125. package/dist/storage/ontology/provider.js.map +1 -1
  126. package/dist/storage/ontology/sources.d.ts +36 -8
  127. package/dist/storage/ontology/sources.d.ts.map +1 -1
  128. package/dist/storage/projection-runs/in-memory.d.ts +3 -1
  129. package/dist/storage/projection-runs/in-memory.d.ts.map +1 -1
  130. package/dist/storage/projection-runs/index.d.ts +1 -1
  131. package/dist/storage/projection-runs/index.d.ts.map +1 -1
  132. package/dist/storage/projection-runs/lifecycle.d.ts +12 -0
  133. package/dist/storage/projection-runs/lifecycle.d.ts.map +1 -1
  134. package/dist/storage/projection-runs/provider.js +10 -4
  135. package/dist/storage/projection-runs/provider.js.map +1 -1
  136. package/dist/storage/projection-runs/types.d.ts +32 -1
  137. package/dist/storage/projection-runs/types.d.ts.map +1 -1
  138. package/dist/storage/workflow-runs/provider.js +4 -4
  139. package/dist/syncs/index.js +19 -19
  140. package/dist/testing/index.js +296 -54
  141. package/dist/testing/index.js.map +4 -4
  142. package/dist/testing/ontology-storage-contract.d.ts.map +1 -1
  143. package/dist/testing/projection-run-storage-contract.d.ts.map +1 -1
  144. package/dist/webhooks/internal.js +14 -14
  145. package/dist/webhooks/internal.js.map +1 -1
  146. package/dist/workers/index.js +1 -1
  147. package/dist/workflows/index.js +33 -33
  148. package/package.json +13 -1
  149. package/src/deploy/define.ts +6 -0
  150. package/src/deploy/index.ts +41 -0
  151. package/src/deploy/internal.ts +7 -0
  152. package/src/deploy/services.ts +51 -0
  153. package/src/deploy/types.ts +338 -0
  154. package/src/deploy/validation.ts +261 -0
  155. package/src/errors/catalog.ts +4 -0
  156. package/src/maintenance/ontology-maintenance.ts +32 -2
  157. package/src/maintenance/types.ts +8 -2
  158. package/src/materialization/dataset-watermark.ts +6 -4
  159. package/src/materialization/model.ts +7 -0
  160. package/src/materializer/README.md +26 -6
  161. package/src/materializer/projections/finish-run.ts +129 -26
  162. package/src/materializer/projections/replace.ts +67 -49
  163. package/src/materializer/projections/source-materialization.ts +11 -6
  164. package/src/projections/types.ts +1 -0
  165. package/src/storage/index.ts +6 -1
  166. package/src/storage/ontology/in-memory/sources.ts +119 -55
  167. package/src/storage/ontology/index.ts +4 -1
  168. package/src/storage/ontology/provider.ts +64 -7
  169. package/src/storage/ontology/sources.ts +42 -8
  170. package/src/storage/projection-runs/in-memory.ts +41 -2
  171. package/src/storage/projection-runs/index.ts +2 -0
  172. package/src/storage/projection-runs/lifecycle.ts +61 -4
  173. package/src/storage/projection-runs/types.ts +41 -1
  174. package/src/testing/ontology-storage-contract.ts +141 -15
  175. package/src/testing/projection-run-storage-contract.ts +174 -1
  176. package/dist/chunk-3qpcrcj8.js.map +0 -10
  177. package/dist/chunk-qma80ych.js.map +0 -11
  178. package/dist/chunk-znw0zvy4.js.map +0 -12
  179. /package/dist/{chunk-dv4ct8mp.js.map → chunk-1jvq82rp.js.map} +0 -0
  180. /package/dist/{chunk-728fp090.js.map → chunk-3c90jvfj.js.map} +0 -0
  181. /package/dist/{chunk-vn3yh259.js.map → chunk-4gmng3vf.js.map} +0 -0
  182. /package/dist/{chunk-hxgj4kzd.js.map → chunk-4jph1t3v.js.map} +0 -0
  183. /package/dist/{chunk-y487ywrv.js.map → chunk-6q7c68yx.js.map} +0 -0
  184. /package/dist/{chunk-ypj2wqtj.js.map → chunk-70jmg2zx.js.map} +0 -0
  185. /package/dist/{chunk-wn7mznxc.js.map → chunk-7jc21943.js.map} +0 -0
  186. /package/dist/{chunk-rzcjp5m2.js.map → chunk-8fjmqjyx.js.map} +0 -0
  187. /package/dist/{chunk-m8zkvep1.js.map → chunk-8w27xf9e.js.map} +0 -0
  188. /package/dist/{chunk-z9h698s8.js.map → chunk-a4g5yjm0.js.map} +0 -0
  189. /package/dist/{chunk-4ypbe95r.js.map → chunk-adbve9zt.js.map} +0 -0
  190. /package/dist/{chunk-wnxabtn4.js.map → chunk-ex8s8wsk.js.map} +0 -0
  191. /package/dist/{chunk-4tnrzqqh.js.map → chunk-fj1dvp37.js.map} +0 -0
  192. /package/dist/{chunk-bdhfn0ww.js.map → chunk-kkqwzy6h.js.map} +0 -0
  193. /package/dist/{chunk-ww185kt6.js.map → chunk-mmac1mp6.js.map} +0 -0
  194. /package/dist/{chunk-jfdggg1p.js.map → chunk-n5t4mq6z.js.map} +0 -0
  195. /package/dist/{chunk-3a338wrp.js.map → chunk-p0nbkys4.js.map} +0 -0
  196. /package/dist/{chunk-kxea54yg.js.map → chunk-py2dw8cw.js.map} +0 -0
  197. /package/dist/{chunk-e2c5yxfe.js.map → chunk-q3grgzp1.js.map} +0 -0
  198. /package/dist/{chunk-6rzb1g6h.js.map → chunk-qne45xqq.js.map} +0 -0
  199. /package/dist/{chunk-cbn96bve.js.map → chunk-r5sc1d2b.js.map} +0 -0
  200. /package/dist/{chunk-y6vnvayj.js.map → chunk-vjcd8d27.js.map} +0 -0
  201. /package/dist/{chunk-ma1mhh7w.js.map → chunk-wd34wnsy.js.map} +0 -0
  202. /package/dist/{chunk-pvgpckm1.js.map → chunk-wem5vqjw.js.map} +0 -0
  203. /package/dist/{chunk-gct3m1da.js.map → chunk-xr7y35yj.js.map} +0 -0
  204. /package/dist/{chunk-q2xghyft.js.map → chunk-y0zmae99.js.map} +0 -0
  205. /package/dist/{chunk-xgptywp0.js.map → chunk-y2aq6ag9.js.map} +0 -0
  206. /package/dist/{chunk-yvf2jqj8.js.map → chunk-yfppyaty.js.map} +0 -0
@@ -442,6 +442,15 @@ export function failProjectionRunEnqueue(
442
442
  }
443
443
  }
444
444
 
445
+ /** Validates the failure of an attempt that leaves its run running, before it is stored. */
446
+ export function parseProjectionAttemptFailure(
447
+ record: Pick<ProjectionRunRecord, "id" | "projectId" | "status">,
448
+ error: SixbFailure<ProjectionRunFailureCode>
449
+ ): SixbFailure<ProjectionRunFailureCode> {
450
+ assertProjectionRunRunning(record)
451
+ return parseSixbFailure(error, PROJECTION_RUN_FAILURE_CODES)
452
+ }
453
+
445
454
  export function canRequeueProjectionRunAfterEnqueueFailure(
446
455
  record: ProjectionRunRecord,
447
456
  input: QueueProjectionRunInput
@@ -457,6 +466,34 @@ export function canRequeueProjectionRunAfterEnqueueFailure(
457
466
  )
458
467
  }
459
468
 
469
+ /**
470
+ * Statuses of a run that still stands to replace its projection's source. A later run in one of
471
+ * them makes an earlier run stale; a failed or cancelled one replaces nothing.
472
+ */
473
+ export const SUPERSEDING_PROJECTION_RUN_STATUSES = [
474
+ "queued",
475
+ "running",
476
+ "succeeded",
477
+ ] as const satisfies readonly ProjectionRunStatus[]
478
+
479
+ /**
480
+ * Whether `newer` makes `run` stale. Dataset version timestamps are canonical UTC ISO strings, so
481
+ * they order as text; SQL adapters compare them under a binary collation for the same reason.
482
+ */
483
+ export function projectionRunSupersedes(
484
+ newer: ProjectionRunRecord,
485
+ run: ProjectionRunRecord
486
+ ): boolean {
487
+ return (
488
+ run.identity.protocol === "replacement" &&
489
+ newer.identity.protocol === "replacement" &&
490
+ newer.projectId === run.projectId &&
491
+ newer.identity.projectionId === run.identity.projectionId &&
492
+ newer.identity.datasetVersion.createdAt > run.identity.datasetVersion.createdAt &&
493
+ (SUPERSEDING_PROJECTION_RUN_STATUSES as readonly ProjectionRunStatus[]).includes(newer.status)
494
+ )
495
+ }
496
+
460
497
  // ── Persistence mapping ─────────────────────────────────────
461
498
 
462
499
  export function restoreProjectionRun(row: PersistedProjectionRunRecord): StoredProjectionRunRecord {
@@ -616,8 +653,7 @@ function assertPersistedLifecycle(row: PersistedProjectionRunRecord): void {
616
653
  row.attempt < 1 ||
617
654
  row.startedAt === undefined ||
618
655
  row.finishedAt !== undefined ||
619
- row.executionToken === undefined ||
620
- hasError
656
+ row.executionToken === undefined
621
657
  ) {
622
658
  throw incompleteProjectionRun(row.id)
623
659
  }
@@ -641,7 +677,12 @@ function assertPersistedLifecycle(row: PersistedProjectionRunRecord): void {
641
677
  return
642
678
  }
643
679
  }
644
- if (row.attempt < 1 || row.startedAt === undefined || (row.status === "succeeded" && hasError)) {
680
+ if (
681
+ row.attempt < 1 ||
682
+ row.startedAt === undefined ||
683
+ ((row.status === "succeeded" || row.status === "superseded") && hasError) ||
684
+ (row.status === "superseded" && row.protocol !== "replacement")
685
+ ) {
645
686
  throw incompleteProjectionRun(row.id)
646
687
  }
647
688
  }
@@ -652,6 +693,17 @@ function assertProjectionRunDate(value: Date, fieldName: string): void {
652
693
  }
653
694
  }
654
695
 
696
+ function isProjectionRunTerminalStatus(
697
+ status: unknown
698
+ ): status is ProjectionRunFinishPlan["status"] {
699
+ return (
700
+ status === "succeeded" ||
701
+ status === "failed" ||
702
+ status === "cancelled" ||
703
+ status === "superseded"
704
+ )
705
+ }
706
+
655
707
  function isProjectionKind(value: unknown): value is ProjectionKind {
656
708
  return value === "object" || value === "link" || value === "telemetry"
657
709
  }
@@ -722,7 +774,7 @@ function assertFinishDecision(
722
774
  record: ProjectionRunRecord,
723
775
  input: ProjectionRunTerminalDecision
724
776
  ): void {
725
- if (input.status !== "succeeded" && input.status !== "failed" && input.status !== "cancelled") {
777
+ if (!isProjectionRunTerminalStatus(input.status)) {
726
778
  throw new ProjectionRunError(
727
779
  `[Sixb] Projection run '${record.id}' finish status must be terminal.`
728
780
  )
@@ -735,6 +787,11 @@ function assertFinishDecision(
735
787
  `[Sixb] Projection run '${record.id}' finish protocol does not match its identity.`
736
788
  )
737
789
  }
790
+ if (input.status === "superseded" && input.protocol !== "replacement") {
791
+ throw new ProjectionRunError(
792
+ `[Sixb] Telemetry projection run '${record.id}' cannot be superseded.`
793
+ )
794
+ }
738
795
  const inputExhausted = "inputExhausted" in input ? input.inputExhausted : undefined
739
796
  if (input.status === "succeeded" && input.protocol === "telemetry") {
740
797
  if (inputExhausted === true) return
@@ -16,7 +16,18 @@ import type {
16
16
  export type { ProjectionRunFailureCode } from "../../projections/types"
17
17
  export { PROJECTION_RUN_FAILURE_CODES } from "../../projections/types"
18
18
 
19
- export type ProjectionRunStatus = "queued" | "running" | "succeeded" | "failed" | "cancelled"
19
+ /**
20
+ * `superseded` ends a replacement run whose output would be stale on arrival: a run of the same
21
+ * projection pinned to a later dataset version exists and has not failed. Telemetry runs append
22
+ * readings rather than replace a source, so none is ever superseded.
23
+ */
24
+ export type ProjectionRunStatus =
25
+ | "queued"
26
+ | "running"
27
+ | "succeeded"
28
+ | "failed"
29
+ | "cancelled"
30
+ | "superseded"
20
31
 
21
32
  export interface ProjectionRunProgress {
22
33
  /** Physical immutable-version rows consumed, before normalization or semantic materialization. */
@@ -76,6 +87,11 @@ interface ProjectionRunRecordBase {
76
87
  readonly queuedAt: Date
77
88
  readonly startedAt?: Date
78
89
  readonly finishedAt?: Date
90
+ /**
91
+ * The run's most recent failure, read with its status: on a `running` run, why its last attempt
92
+ * failed while the queue retries it; on a `failed` or `cancelled` run, why it ended. Cleared when
93
+ * the run succeeds or is superseded, so a present error always describes something current.
94
+ */
79
95
  readonly error?: SixbFailure<ProjectionRunFailureCode>
80
96
  }
81
97
 
@@ -202,6 +218,17 @@ export interface RecordProjectionMissingTargetInput
202
218
  readonly missingTarget: ProjectionMissingTarget
203
219
  }
204
220
 
221
+ export interface RecordProjectionAttemptFailureInput
222
+ extends LockProjectionRunForMaterializationInput {
223
+ readonly error: SixbFailure<ProjectionRunFailureCode>
224
+ }
225
+
226
+ export interface FindSupersedingProjectionRunInput {
227
+ readonly projectId: string
228
+ /** The run that may be superseded. */
229
+ readonly id: string
230
+ }
231
+
205
232
  export interface ListProjectionRunsInput {
206
233
  readonly projectId: string
207
234
  readonly projectionId?: string
@@ -258,10 +285,23 @@ export interface ProjectionRunStorage {
258
285
  recordMissingTarget(
259
286
  input: RecordProjectionMissingTargetInput
260
287
  ): Promise<TelemetryProjectionRunRecord>
288
+ /**
289
+ * Records the failure of the current attempt as the `error` of a run that stays `running` to be
290
+ * retried. Fenced like every other write: a delivery that lost the run cannot overwrite the
291
+ * failure of the one that holds it now. A reclaim keeps it until the run ends.
292
+ */
293
+ recordAttemptFailure(input: RecordProjectionAttemptFailureInput): Promise<ProjectionRunRecord>
261
294
  /** Telemetry success also records the worker's EOF observation atomically. */
262
295
  finish(input: FinishProjectionRunInput): Promise<ProjectionRunRecord>
296
+ /**
297
+ * The newest replacement run of the same projection that makes this one stale: pinned to a
298
+ * later dataset version, and queued, running, or succeeded. A failed or cancelled run replaces
299
+ * nothing, so it supersedes nothing either. Null for a telemetry run.
300
+ */
301
+ findSupersedingRun(input: FindSupersedingProjectionRunInput): Promise<ProjectionRunRecord | null>
263
302
  getById(params: { projectId: string; id: string }): Promise<ProjectionRunRecord | null>
264
303
  list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult>
304
+ /** Superseded runs are skipped: the run that superseded them is the one that matters. */
265
305
  listLatestByProjectionIds(
266
306
  input: ListLatestProjectionRunsInput
267
307
  ): Promise<ListLatestProjectionRunsResult>
@@ -410,38 +410,76 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
410
410
  abandonedAt: "2026-01-01T00:03:00.000Z",
411
411
  })
412
412
  ).rejects.toMatchObject({ kind: "execution-lost" })
413
+ const adopted = await storage.ontology.sources.adopt({
414
+ projectId: "contract-project",
415
+ source,
416
+ execution: reclaimed.execution,
417
+ adoptedAt: "2026-01-01T00:01:30.000Z",
418
+ })
419
+ expect(adopted).toMatchObject({
420
+ record: {
421
+ materializationId: "staging-candidate",
422
+ status: "ready",
423
+ executionToken: reclaimed.execution.executionToken,
424
+ updatedAt: "2026-01-01T00:01:30.000Z",
425
+ },
426
+ resumeStagingOrdinal: 1,
427
+ })
428
+ // Adopting twice from the same execution is a no-op, not a second owner change.
429
+ expect(
430
+ await storage.ontology.sources.adopt({
431
+ projectId: "contract-project",
432
+ source,
433
+ execution: reclaimed.execution,
434
+ adoptedAt: "2026-01-01T00:01:40.000Z",
435
+ })
436
+ ).toMatchObject({ record: { executionToken: reclaimed.execution.executionToken } })
437
+
413
438
  const abandoned = await storage.ontology.sources.abandon({
414
- kind: "reclaim",
439
+ kind: "run",
415
440
  projectId: "contract-project",
416
441
  source,
417
442
  execution: reclaimed.execution,
418
443
  abandonedAt: "2026-01-01T00:02:00.000Z",
419
444
  })
420
445
  expect(abandoned).toMatchObject({ status: "abandoned", executionToken: null })
446
+ expect(
447
+ await storage.ontology.sources.abandon({
448
+ kind: "run",
449
+ projectId: "contract-project",
450
+ source,
451
+ execution: reclaimed.execution,
452
+ abandonedAt: "2026-01-01T00:02:00.000Z",
453
+ })
454
+ ).toBeNull()
455
+ expect(
456
+ await storage.ontology.sources.adopt({
457
+ projectId: "contract-project",
458
+ source,
459
+ execution: reclaimed.execution,
460
+ adoptedAt: "2026-01-01T00:02:10.000Z",
461
+ })
462
+ ).toBeNull()
421
463
  expect(
422
464
  await storage.ontology.sources.summarizeTerminal({ projectId: "contract-project" })
423
465
  ).toEqual({ count: 1, oldestTerminalAt: "2026-01-01T00:02:00.000Z" })
424
466
 
467
+ // Superseded retention does not hold an abandoned candidate back.
425
468
  expect(
426
469
  await storage.ontology.sources.cleanupTerminal({
427
470
  projectId: "contract-project",
428
471
  terminalBefore: "2026-02-01T00:00:00.000Z",
429
- limit: 1,
472
+ limit: 10,
430
473
  })
474
+ ).toEqual({ rowsDeleted: 0, materializationsDeleted: 0 })
475
+ expect(
476
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
431
477
  ).toEqual({ rowsDeleted: 1, materializationsDeleted: 0 })
432
478
  expect(
433
- await storage.ontology.sources.cleanupTerminal({
434
- projectId: "contract-project",
435
- terminalBefore: "2026-02-01T00:00:00.000Z",
436
- limit: 1,
437
- })
479
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
438
480
  ).toEqual({ rowsDeleted: 1, materializationsDeleted: 0 })
439
481
  expect(
440
- await storage.ontology.sources.cleanupTerminal({
441
- projectId: "contract-project",
442
- terminalBefore: "2026-02-01T00:00:00.000Z",
443
- limit: 1,
444
- })
482
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
445
483
  ).toEqual({ rowsDeleted: 0, materializationsDeleted: 1 })
446
484
  expect(
447
485
  await storage.ontology.sources.summarizeTerminal({ projectId: "contract-project" })
@@ -449,6 +487,95 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
449
487
  })
450
488
  })
451
489
 
490
+ test("adopts a staging candidate at its last staged root", async () => {
491
+ await withStorage(async (storage) => {
492
+ const identity = replacementIdentity("01")
493
+ const claimed = await startTestProjectionRun(storage, {
494
+ id: "resumed-run",
495
+ projectId: "contract-project",
496
+ identity,
497
+ target: { objectTypeId: "ContractDevice" },
498
+ })
499
+ const source = { projectionId: identity.projectionId }
500
+ await storage.ontology.sources.beginMaterialization({
501
+ projectId: "contract-project",
502
+ source,
503
+ materializationId: "resumed-candidate",
504
+ execution: claimed.execution,
505
+ projectionKind: "object",
506
+ protocol: "replacement",
507
+ datasetVersion: identity.datasetVersion,
508
+ projectionRevision: identity.projectionRevision,
509
+ ownershipHash: identity.ownershipHash,
510
+ ontologyRevision: identity.ontologyRevision,
511
+ createdAt: "2026-01-01T00:00:00.000Z",
512
+ })
513
+ const row = (primaryId: string, stagingOrdinal: number) => ({
514
+ root: { kind: "object" as const, ref: { objectTypeId: "ContractDevice", primaryId } },
515
+ assertion: {
516
+ kind: "object" as const,
517
+ ref: { objectTypeId: "ContractDevice", primaryId },
518
+ properties: { name: primaryId },
519
+ },
520
+ stagingOrdinal,
521
+ })
522
+ await storage.ontology.sources.stageRows({
523
+ projectId: "contract-project",
524
+ source,
525
+ materializationId: "resumed-candidate",
526
+ execution: claimed.execution,
527
+ rows: [row("zero", 0), row("one", 1)],
528
+ })
529
+
530
+ const reclaimed = await storage.projectionRuns.startOrReclaim({
531
+ id: claimed.run.id,
532
+ projectId: claimed.run.projectId,
533
+ identity,
534
+ target: { objectTypeId: "ContractDevice" },
535
+ })
536
+ const adopted = await storage.ontology.sources.adopt({
537
+ projectId: "contract-project",
538
+ source,
539
+ execution: reclaimed.execution,
540
+ adoptedAt: "2026-01-01T00:01:00.000Z",
541
+ })
542
+ expect(adopted).toMatchObject({
543
+ record: { status: "staging", executionToken: reclaimed.execution.executionToken },
544
+ resumeStagingOrdinal: 1,
545
+ })
546
+ // The previous execution lost the candidate with the run.
547
+ await expect(
548
+ storage.ontology.sources.stageRows({
549
+ projectId: "contract-project",
550
+ source,
551
+ materializationId: "resumed-candidate",
552
+ execution: claimed.execution,
553
+ rows: [row("two", 2)],
554
+ })
555
+ ).rejects.toMatchObject({ kind: "execution-lost" })
556
+ expect(
557
+ await storage.ontology.sources.stageRows({
558
+ projectId: "contract-project",
559
+ source,
560
+ materializationId: "resumed-candidate",
561
+ execution: reclaimed.execution,
562
+ rows: [row("one", 1), row("two", 2)],
563
+ })
564
+ ).toEqual({ inserted: 1, unchanged: 1 })
565
+ expect(
566
+ await storage.ontology.sources.markReady({
567
+ projectId: "contract-project",
568
+ source,
569
+ materializationId: "resumed-candidate",
570
+ execution: reclaimed.execution,
571
+ rootCount: 3,
572
+ assertionCount: 3,
573
+ readyAt: "2026-01-01T00:02:00.000Z",
574
+ })
575
+ ).toMatchObject({ status: "ready", rootCount: 3 })
576
+ })
577
+ })
578
+
452
579
  test("atomically activates explicit empty candidates and rejects competing stale heads", async () => {
453
580
  await withStorage(async (storage) => {
454
581
  const first = await createReadyEmptyCandidate(storage, "run-one", "candidate-one", "01")
@@ -532,16 +659,15 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
532
659
  )
533
660
  ).rejects.toThrow("execution token is stale")
534
661
  await storage.ontology.sources.abandon({
535
- kind: "reclaim",
662
+ kind: "run",
536
663
  projectId: "contract-project",
537
664
  source: stale.source,
538
665
  execution: reclaimed.execution,
539
666
  abandonedAt: "2026-01-06T00:01:00.000Z",
540
667
  })
541
668
 
542
- const cleanup = await storage.ontology.sources.cleanupTerminal({
669
+ const cleanup = await storage.ontology.sources.purgeAbandoned({
543
670
  projectId: "contract-project",
544
- terminalBefore: "2026-02-01T00:00:00.000Z",
545
671
  limit: 10,
546
672
  })
547
673
  expect(cleanup.materializationsDeleted).toBeGreaterThanOrEqual(1)
@@ -41,6 +41,13 @@ const failure = {
41
41
  details: { projectionId: "contract.devices", runId: "failed-run" },
42
42
  } as const satisfies SixbFailure<ProjectionRunFailureCode>
43
43
 
44
+ const attemptFailure = {
45
+ code: "storage.unavailable",
46
+ message: "Storage is temporarily unavailable.",
47
+ retryable: true,
48
+ at: "2026-06-01T11:00:00.000Z",
49
+ } as const satisfies SixbFailure<ProjectionRunFailureCode>
50
+
44
51
  const replacementIdentity = {
45
52
  projectionId: "contract.devices",
46
53
  projectionKind: "object",
@@ -149,11 +156,149 @@ export function runProjectionRunStorageContractSuite<TStorage extends Projection
149
156
  error: { code: "queue.enqueue_failed", message: "queue unavailable" },
150
157
  })
151
158
 
152
- await expect(context.projectionRuns.queue(admission)).resolves.toMatchObject({
159
+ const requeued = await context.projectionRuns.queue(admission)
160
+ expect(requeued).toMatchObject({
153
161
  executionId: admission.executionId,
154
162
  status: "queued",
155
163
  attempt: 0,
156
164
  })
165
+ expect(requeued.error).toBeUndefined()
166
+ })
167
+ })
168
+
169
+ test("keeps the failure of a retried attempt until the run ends", async () => {
170
+ await withStorage(async (context) => {
171
+ const storage = context.projectionRuns
172
+ const input = replacementInput("retried-run")
173
+ await admitProjectionRun(context, input)
174
+ const first = await storage.startOrReclaim(input)
175
+
176
+ const failed = await storage.recordAttemptFailure({
177
+ ...executionInput(first),
178
+ error: attemptFailure,
179
+ })
180
+ expect(failed).toMatchObject({ status: "running", error: attemptFailure })
181
+
182
+ const second = await storage.startOrReclaim(input)
183
+ expect(second.run).toMatchObject({ attempt: 2, error: attemptFailure })
184
+ await expect(
185
+ storage.recordAttemptFailure({ ...executionInput(first), error: failure })
186
+ ).rejects.toThrow("execution token is stale")
187
+ await expect(
188
+ storage.recordAttemptFailure({
189
+ ...executionInput(second),
190
+ error: { ...failure, code: "dataset.not_found" } as never,
191
+ })
192
+ ).rejects.toThrow("code is not allowed by this failure contract")
193
+
194
+ const succeeded = await storage.finish({
195
+ ...executionInput(second),
196
+ protocol: "replacement",
197
+ status: "succeeded",
198
+ })
199
+ expect(succeeded).toMatchObject({ status: "succeeded" })
200
+ expect(succeeded.error).toBeUndefined()
201
+ await expect(
202
+ storage.recordAttemptFailure({ ...executionInput(second), error: failure })
203
+ ).rejects.toThrow("already terminal")
204
+ })
205
+ })
206
+
207
+ test("finds the newest later run that still stands to replace a source", async () => {
208
+ await withStorage(async (context) => {
209
+ const storage = context.projectionRuns
210
+ const current = replacementInput("current-run")
211
+ const earlier = versionedReplacementInput("earlier-run", "version-0", "2025-12-31")
212
+ const later = versionedReplacementInput("later-run", "version-2", "2026-01-02")
213
+ const latest = versionedReplacementInput("latest-run", "version-3", "2026-01-03")
214
+ const otherProjection = {
215
+ ...versionedReplacementInput("other-projection-run", "version-4", "2026-01-04"),
216
+ identity: {
217
+ ...versionedReplacementInput("other", "version-4", "2026-01-04").identity,
218
+ projectionId: "contract.rooms",
219
+ },
220
+ }
221
+ for (const input of [current, earlier, later, latest, otherProjection]) {
222
+ await admitProjectionRun(context, input)
223
+ }
224
+ const superseding = () => storage.findSupersedingRun({ projectId, id: current.id })
225
+
226
+ await expect(superseding()).resolves.toMatchObject({ id: latest.id, status: "queued" })
227
+ await expect(storage.findSupersedingRun({ projectId, id: latest.id })).resolves.toBeNull()
228
+
229
+ // A run that failed or was cancelled replaces nothing, so it supersedes nothing either.
230
+ await finishClaimed(context, latest, "failed")
231
+ await expect(superseding()).resolves.toMatchObject({ id: later.id })
232
+ await finishClaimed(context, later, "cancelled")
233
+ await expect(superseding()).resolves.toBeNull()
234
+
235
+ const telemetry = {
236
+ id: "telemetry-run",
237
+ projectId,
238
+ identity: telemetryIdentity,
239
+ target: objectTarget,
240
+ fixedBatchSize: 2,
241
+ } as const
242
+ await admitProjectionRun(context, telemetry)
243
+ await expect(
244
+ storage.findSupersedingRun({ projectId, id: telemetry.id })
245
+ ).resolves.toBeNull()
246
+ await expect(storage.findSupersedingRun({ projectId, id: "missing-run" })).rejects.toThrow(
247
+ "not found"
248
+ )
249
+ })
250
+ })
251
+
252
+ test("ends a superseded run without a failure and never reports it as the latest", async () => {
253
+ await withStorage(async (context) => {
254
+ const storage = context.projectionRuns
255
+ const stale = replacementInput("stale-run")
256
+ const newer = versionedReplacementInput("newer-run", "version-2", "2026-01-02")
257
+ await admitProjectionRun(context, stale)
258
+ await admitProjectionRun(context, newer)
259
+ // Started after the newer run was queued, so it is the most recent by activity.
260
+ // Removal proof: drop the superseded filter from listLatestByProjectionIds.
261
+ const claim = await storage.startOrReclaim({
262
+ ...stale,
263
+ startedAt: new Date("2026-01-01T00:00:05.000Z"),
264
+ })
265
+
266
+ const superseded = await storage.finish({
267
+ ...executionInput(claim),
268
+ protocol: "replacement",
269
+ status: "superseded",
270
+ })
271
+
272
+ expect(superseded).toMatchObject({ status: "superseded", attempt: 1 })
273
+ expect(superseded.error).toBeUndefined()
274
+ expect(superseded.finishedAt).toBeInstanceOf(Date)
275
+ await expect(
276
+ storage.listLatestByProjectionIds({
277
+ projectId,
278
+ projectionIds: [replacementIdentity.projectionId],
279
+ })
280
+ ).resolves.toMatchObject({ runs: [{ id: newer.id, status: "queued" }] })
281
+ await expect(storage.list({ projectId, statuses: ["superseded"] })).resolves.toMatchObject({
282
+ runs: [{ id: stale.id }],
283
+ total: 1,
284
+ })
285
+
286
+ const telemetry = {
287
+ id: "telemetry-run",
288
+ projectId,
289
+ identity: telemetryIdentity,
290
+ target: objectTarget,
291
+ fixedBatchSize: 2,
292
+ } as const
293
+ await admitProjectionRun(context, telemetry)
294
+ const telemetryClaim = await storage.startOrReclaim(telemetry)
295
+ await expect(
296
+ storage.finish({
297
+ ...executionInput(telemetryClaim),
298
+ protocol: "telemetry",
299
+ status: "superseded",
300
+ } as never)
301
+ ).rejects.toThrow("cannot be superseded")
157
302
  })
158
303
  })
159
304
 
@@ -582,6 +727,34 @@ function replacementInput(id: string) {
582
727
  return { id, projectId, identity: replacementIdentity, target: objectTarget } as const
583
728
  }
584
729
 
730
+ /** A replacement run of the same projection, pinned to another version created on `day`. */
731
+ function versionedReplacementInput(id: string, versionId: string, day: string) {
732
+ return {
733
+ ...replacementInput(id),
734
+ identity: {
735
+ ...replacementIdentity,
736
+ datasetVersion: {
737
+ ...replacementIdentity.datasetVersion,
738
+ versionId,
739
+ createdAt: `${day}T00:00:00.000Z`,
740
+ },
741
+ },
742
+ } as const
743
+ }
744
+
745
+ async function finishClaimed<TStorage extends ProjectionRunStorage>(
746
+ context: ProjectionRunStorageContractContext<TStorage>,
747
+ input: StartOrReclaimProjectionRunInput,
748
+ status: "failed" | "cancelled"
749
+ ): Promise<void> {
750
+ const claim = await context.projectionRuns.startOrReclaim(input)
751
+ await context.projectionRuns.finish({
752
+ ...executionInput(claim),
753
+ protocol: "replacement",
754
+ status,
755
+ })
756
+ }
757
+
585
758
  function executionInput(claim: ProjectionRunClaim) {
586
759
  return {
587
760
  id: claim.run.id,
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/materialization/dataset-watermark.ts"],
4
- "sourcesContent": [
5
- "import { MaterializationConflictError } from \"./errors\"\nimport type { PinnedDatasetVersion } from \"./model\"\n\nexport type PinnedDatasetWatermarkConflict =\n | \"dataset-mismatch\"\n | \"version-metadata-mismatch\"\n | \"older-version\"\n | \"ambiguous-version\"\n\nexport function comparePinnedDatasetWatermarks(\n active: PinnedDatasetVersion,\n next: PinnedDatasetVersion\n): PinnedDatasetWatermarkConflict | null {\n if (active.datasetId !== next.datasetId) return \"dataset-mismatch\"\n if (active.versionId === next.versionId && active.createdAt !== next.createdAt) {\n return \"version-metadata-mismatch\"\n }\n\n const activeAt = Date.parse(active.createdAt)\n const nextAt = Date.parse(next.createdAt)\n if (nextAt < activeAt) return \"older-version\"\n if (nextAt === activeAt && next.versionId !== active.versionId) return \"ambiguous-version\"\n return null\n}\n\nexport function assertPinnedDatasetWatermark(\n active: PinnedDatasetVersion,\n next: PinnedDatasetVersion,\n subject: string\n): void {\n const conflict = comparePinnedDatasetWatermarks(active, next)\n if (!conflict) return\n\n const message = pinnedDatasetWatermarkConflictMessage(conflict, subject)\n throw new MaterializationConflictError(\"projection-fence\", message)\n}\n\nfunction pinnedDatasetWatermarkConflictMessage(\n conflict: PinnedDatasetWatermarkConflict,\n subject: string\n): string {\n switch (conflict) {\n case \"dataset-mismatch\":\n return `${subject} dataset does not match the active source dataset.`\n case \"version-metadata-mismatch\":\n return `${subject} reused an immutable dataset version id with different metadata.`\n case \"older-version\":\n return `${subject} dataset version is older than the active watermark.`\n case \"ambiguous-version\":\n return `${subject} dataset watermark is ambiguous.`\n }\n}\n"
6
- ],
7
- "mappings": ";;;;;;AASO,SAAS,+BAA8B,CAC5C,QACA,MACuC;AAAA,EACvC,IAAI,OAAO,cAAc,KAAK;AAAA,IAAW,OAAO;AAAA,EAChD,IAAI,OAAO,cAAc,KAAK,aAAa,OAAO,cAAc,KAAK,WAAW;AAAA,IAC9E,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,KAAK,MAAM,OAAO,SAAS;AAAA,EAC5C,MAAM,SAAS,KAAK,MAAM,KAAK,SAAS;AAAA,EACxC,IAAI,SAAS;AAAA,IAAU,OAAO;AAAA,EAC9B,IAAI,WAAW,YAAY,KAAK,cAAc,OAAO;AAAA,IAAW,OAAO;AAAA,EACvE,OAAO;AAAA;AAGF,SAAS,6BAA4B,CAC1C,QACA,MACA,SACM;AAAA,EACN,MAAM,WAAW,gCAA+B,QAAQ,IAAI;AAAA,EAC5D,IAAI,CAAC;AAAA,IAAU;AAAA,EAEf,MAAM,UAAU,sCAAsC,UAAU,OAAO;AAAA,EACvE,MAAM,IAAI,8BAA6B,oBAAoB,OAAO;AAAA;AAGpE,SAAS,qCAAqC,CAC5C,UACA,SACQ;AAAA,EACR,QAAQ;AAAA,SACD;AAAA,MACH,OAAO,GAAG;AAAA,SACP;AAAA,MACH,OAAO,GAAG;AAAA,SACP;AAAA,MACH,OAAO,GAAG;AAAA,SACP;AAAA,MACH,OAAO,GAAG;AAAA;AAAA;",
8
- "debugId": "76FA48CB1D1BE09F64756E2164756E21",
9
- "names": []
10
- }
@@ -1,11 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/projections/types.ts", "../src/storage/projection-runs/types.ts"],
4
- "sourcesContent": [
5
- "import type { SixbErrorCode } from \"../errors/types\"\n\n/** Error codes a projection run can persist and expose through its public contract. */\nexport const PROJECTION_RUN_FAILURE_CODES = [\n \"internal.unexpected\",\n \"runtime.cancelled\",\n \"queue.enqueue_failed\",\n \"projection.execution_failed\",\n] as const satisfies readonly [SixbErrorCode, ...SixbErrorCode[]]\n\nexport type ProjectionRunFailureCode = (typeof PROJECTION_RUN_FAILURE_CODES)[number]\n\n/**\n * Lowered, serializable definition interfaces for projection DSL.\n *\n * These are the runtime-inspectable data structures that projection builders\n * lower into. They use plain string ids (no tokens, no methods) so they can\n * be stored, serialized, and introspected by tooling.\n */\n\n/**\n * Describes how a foreign key column in a dataset resolves to a link target.\n *\n * The source can be either a projected source property or a raw dataset field.\n * Its value equals the target object's primary property value, creating an\n * implicit link between source and target.\n *\n * `targetObjectTypeId` declares the concrete target type for this projection.\n * It must be the same type or a subtype (via `extends`) of the link's declared\n * target at the ontology level. Validation of the subtype relationship happens\n * at Sixb startup (Increment 7), not at DSL build time.\n */\nexport interface ForeignKeyDescriptor {\n readonly linkId: string\n /** Object property holding the target id. Mutually exclusive with sourceField. */\n readonly sourcePropertyId?: string\n /** Dataset column holding the target id. Mutually exclusive with sourcePropertyId. */\n readonly sourceField?: string\n readonly targetObjectTypeId: string\n}\n\n/** Resolves competing source-projection and managed-edit values for projected properties. */\nexport type SourceEditConflictResolution =\n | { readonly strategy: \"editsWin\" }\n | { readonly strategy: \"mostRecent\"; readonly sourceTimestamp: string }\n\n/**\n * Lowered, serializable definition for projecting a dataset into object instances.\n *\n * Each row contributes one complete object root to an authoritative replacement snapshot, with\n * properties mapped from dataset columns to object type properties. Duplicate roots are rejected.\n * FK links are resolved from source property or dataset field values to target object primary\n * values.\n */\nexport interface ObjectProjectionDefinition {\n readonly _tag: \"ObjectProjectionDefinition\"\n readonly id: string\n readonly objectTypeId: string\n readonly datasetId: string\n /** Maps object type property id -> dataset column name. */\n readonly properties: Readonly<Record<string, string>>\n /** Maps link id -> FK descriptor. Empty `{}` when no FK links are projected. */\n readonly links: Readonly<Record<string, ForeignKeyDescriptor>>\n /** Defaults to `editsWin` when omitted by a legacy lowered definition. */\n readonly conflictResolution?: SourceEditConflictResolution\n}\n\n/**\n * Lowered, serializable definition for projecting a join dataset into link instances.\n *\n * Each row in the join dataset contributes one complete link root to an authoritative replacement\n * snapshot, identified by its source, link id, and target. Duplicate roots are rejected.\n */\nexport interface LinkProjectionDefinition {\n readonly _tag: \"LinkProjectionDefinition\"\n readonly id: string\n readonly linkId: string\n readonly sourceObjectTypeId: string\n readonly targetObjectTypeId: string\n readonly datasetId: string\n /** Dataset column holding the source object's primary property value. */\n readonly sourceField: string\n /** Dataset column holding the target object's primary property value. */\n readonly targetField: string\n}\n\n/** Dataset columns that produce one telemetry property value. */\nexport interface TelemetryProjectionPropertyMapping {\n /** Dataset column holding the telemetry value. */\n readonly valueField: string\n /** Dataset column holding the telemetry unit, when the property requires one. */\n readonly unitField?: string\n}\n\n/**\n * Lowered, serializable definition for projecting dataset rows into telemetry history.\n *\n * Each row shares one target object id and observation timestamp, then contributes up to one\n * telemetry point for every mapped property. A property-token projection is lowered to the same\n * shape with one entry, so execution and ownership have one canonical contract.\n */\nexport interface TelemetryProjectionDefinition {\n readonly _tag: \"TelemetryProjectionDefinition\"\n readonly id: string\n readonly objectTypeId: string\n readonly datasetId: string\n /** Dataset column holding the target object's primary id. */\n readonly objectIdField: string\n /** Dataset column holding the telemetry observation timestamp. */\n readonly atField: string\n /** Maps telemetry property id -> value and optional unit dataset columns. */\n readonly properties: Readonly<Record<string, TelemetryProjectionPropertyMapping>>\n}\n\n/** Union of all projection definition types. */\nexport type ProjectionDefinition =\n | ObjectProjectionDefinition\n | LinkProjectionDefinition\n | TelemetryProjectionDefinition\n\nexport interface ProjectionOwnership {\n readonly objects: readonly {\n readonly objectTypeId: string\n readonly existence: boolean\n readonly propertyIds: readonly string[]\n }[]\n readonly links: readonly {\n readonly sourceObjectTypeId: string\n readonly linkId: string\n }[]\n readonly telemetry: readonly {\n readonly objectTypeId: string\n readonly propertyId: string\n }[]\n}\n\n/** Canonical correlation between a projection kind and its materialization protocol. */\nexport type ProjectionProtocolIdentity =\n | { readonly projectionKind: \"object\"; readonly protocol: \"replacement\" }\n | { readonly projectionKind: \"link\"; readonly protocol: \"replacement\" }\n | { readonly projectionKind: \"telemetry\"; readonly protocol: \"telemetry\" }\n\nexport type ProjectionKind = ProjectionProtocolIdentity[\"projectionKind\"]\n\nexport interface ObjectProjectionTarget {\n readonly objectTypeId: string\n}\n\nexport interface LinkProjectionTarget {\n readonly sourceObjectTypeId: string\n readonly targetObjectTypeId: string\n}\n\nexport interface ProjectionTargetByKind {\n readonly object: ObjectProjectionTarget\n readonly link: LinkProjectionTarget\n readonly telemetry: ObjectProjectionTarget\n}\n\n/** Object types affected by one projection, correlated with its projection kind. */\nexport type ProjectionTarget<TKind extends ProjectionKind = ProjectionKind> =\n ProjectionTargetByKind[TKind]\n\n/**\n * Frozen semantic identity used to dispatch one registered projection.\n *\n * Dataset version metadata is attached only when a committed dataset event is routed. Keeping the\n * registry-owned fields together prevents the orchestrator from recomputing ontology semantics.\n */\nexport type ProjectionDispatchDescriptor = ProjectionDispatchDescriptorBase &\n ProjectionProtocolIdentity\n\ninterface ProjectionDispatchDescriptorBase {\n readonly projectionId: string\n readonly datasetId: string\n readonly ontologyRevision: string\n readonly projectionRevision: string\n readonly ownershipHash: string\n}\n\nexport interface ResolvedProjection<TDefinition extends ProjectionDefinition> {\n readonly projectionId: string\n readonly datasetId: string\n readonly projectionRevision: string\n readonly ownershipHash: string\n readonly ownership: ProjectionOwnership\n readonly definition: TDefinition\n}\n",
6
- "import type { SixbFailure } from \"../../errors/types\"\nimport type {\n PinnedDatasetVersion,\n ProjectionExecution,\n ProjectionMaterializationIdentity,\n ProjectionRunTerminalDecision,\n} from \"../../materialization/model\"\nimport type {\n LinkProjectionTarget,\n ObjectProjectionTarget,\n ProjectionKind,\n ProjectionRunFailureCode,\n ProjectionTarget,\n} from \"../../projections/types\"\n\nexport type { ProjectionRunFailureCode } from \"../../projections/types\"\nexport { PROJECTION_RUN_FAILURE_CODES } from \"../../projections/types\"\n\nexport type ProjectionRunStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"cancelled\"\n\nexport interface ProjectionRunProgress {\n /** Physical immutable-version rows consumed, before normalization or semantic materialization. */\n readonly sourceRowsRead: number\n /** Physical rows intentionally skipped because a required mapped value was blank. */\n readonly sourceRowsSkipped: number\n /** Maximum complete change records consumed by one incremental attempt; not a recovery cursor. */\n readonly sourceChangesRead?: number\n}\n\nconst progressKeyFlags: Record<keyof ProjectionRunProgress, true> = {\n sourceRowsRead: true,\n sourceRowsSkipped: true,\n sourceChangesRead: true,\n}\n\nexport const PROJECTION_RUN_PROGRESS_KEYS = Object.keys(\n progressKeyFlags\n) as readonly (keyof ProjectionRunProgress)[]\n\nexport function zeroProjectionRunProgress(): ProjectionRunProgress {\n return { sourceRowsRead: 0, sourceRowsSkipped: 0 }\n}\n\nexport interface ProjectionTelemetryCheckpoint {\n readonly fixedBatchSize: number\n readonly nextBatchOrdinal: number\n readonly nextRowOffset: number\n readonly inputExhausted: boolean\n}\n\n/**\n * The object a telemetry batch is waiting for, and when this run first found it missing.\n *\n * A projection's dataset can legitimately be materialized before the objects it references, so\n * the run waits instead of failing. The wait needs a start, and the run has no other durable one:\n * `startedAt` is when the run began, which on a batched projection is long before the batch that\n * cannot commit, and `attempt` counts every reclaim including the ones an unreachable queue caused.\n *\n * Cleared by any committed batch, so a run that is making progress never carries a stale wait.\n */\nexport interface ProjectionMissingTarget {\n readonly objectTypeId: string\n readonly objectId: string\n /** The batch that could not commit because of it. */\n readonly batchOrdinal: number\n readonly firstSeenAt: Date\n}\n\ninterface ProjectionRunRecordBase {\n readonly id: string\n readonly projectId: string\n readonly executionId: string\n readonly status: ProjectionRunStatus\n readonly attempt: number\n readonly progress: ProjectionRunProgress\n readonly queuedAt: Date\n readonly startedAt?: Date\n readonly finishedAt?: Date\n readonly error?: SixbFailure<ProjectionRunFailureCode>\n}\n\ntype ProjectionIdentity<TKind extends ProjectionKind> = Extract<\n ProjectionMaterializationIdentity,\n { readonly projectionKind: TKind }\n>\n\nexport type ObjectProjectionRunRecord = ProjectionRunRecordBase & {\n readonly identity: ProjectionIdentity<\"object\">\n readonly target: ObjectProjectionTarget\n readonly telemetryCheckpoint?: never\n readonly missingTarget?: never\n}\n\nexport type LinkProjectionRunRecord = ProjectionRunRecordBase & {\n readonly identity: ProjectionIdentity<\"link\">\n readonly target: LinkProjectionTarget\n readonly telemetryCheckpoint?: never\n readonly missingTarget?: never\n}\n\nexport type TelemetryProjectionRunRecord = ProjectionRunRecordBase & {\n readonly identity: ProjectionIdentity<\"telemetry\">\n readonly target: ObjectProjectionTarget\n readonly telemetryCheckpoint: ProjectionTelemetryCheckpoint\n /**\n * Only telemetry carries this: `MaterializationObjectNotFoundError` is raised by telemetry\n * validation alone, and only a batched protocol has a checkpoint to anchor the wait to.\n */\n readonly missingTarget?: ProjectionMissingTarget\n}\n\n/** Durable projection lifecycle state. The execution token is deliberately returned separately. */\nexport type ProjectionRunRecord =\n | ObjectProjectionRunRecord\n | LinkProjectionRunRecord\n | TelemetryProjectionRunRecord\n\nexport function projectionRunObjectTypesVisible(\n run: ProjectionRunRecord,\n canView: (objectTypeId: string) => boolean\n): boolean {\n const target: ProjectionTarget = run.target\n if (\"sourceObjectTypeId\" in target) {\n return canView(target.sourceObjectTypeId) && canView(target.targetObjectTypeId)\n }\n return canView(target.objectTypeId)\n}\n\ninterface ProjectionRunAdmissionBase {\n readonly id: string\n readonly projectId: string\n readonly executionId: string\n readonly queuedAt?: Date\n}\n\nexport type QueueProjectionRunInput =\n | (ProjectionRunAdmissionBase & {\n readonly identity: ProjectionIdentity<\"object\">\n readonly target: ObjectProjectionRunRecord[\"target\"]\n readonly fixedBatchSize?: never\n })\n | (ProjectionRunAdmissionBase & {\n readonly identity: ProjectionIdentity<\"link\">\n readonly target: LinkProjectionRunRecord[\"target\"]\n readonly fixedBatchSize?: never\n })\n | (ProjectionRunAdmissionBase & {\n readonly identity: ProjectionIdentity<\"telemetry\">\n readonly target: TelemetryProjectionRunRecord[\"target\"]\n readonly fixedBatchSize: number\n })\n\nexport type StartOrReclaimProjectionRunInput = QueueProjectionRunInput extends infer TInput\n ? TInput extends QueueProjectionRunInput\n ? Omit<TInput, \"executionId\" | \"queuedAt\"> & { readonly startedAt?: Date }\n : never\n : never\n\nexport interface FailProjectionRunEnqueueInput {\n readonly id: string\n readonly projectId: string\n readonly finishedAt?: Date\n readonly error: SixbFailure<ProjectionRunFailureCode>\n}\n\nexport interface ProjectionRunClaim {\n readonly run: ProjectionRunRecord\n readonly execution: ProjectionExecution\n}\n\nexport interface LockProjectionRunForMaterializationInput {\n readonly id: string\n readonly projectId: string\n readonly executionToken: string\n readonly identity: ProjectionMaterializationIdentity\n}\n\nexport interface UpdateProjectionRunInput extends LockProjectionRunForMaterializationInput {\n readonly progress: Partial<ProjectionRunProgress>\n}\n\nexport type FinishProjectionRunInput = ProjectionRunTerminalDecision &\n LockProjectionRunForMaterializationInput & {\n readonly finishedAt?: Date\n readonly progress?: Partial<ProjectionRunProgress>\n }\n\nexport interface AdvanceProjectionTelemetryCheckpointInput\n extends LockProjectionRunForMaterializationInput {\n /** Must equal the run's current nextBatchOrdinal. */\n readonly batchOrdinal: number\n /** Physical dataset rows consumed by this batch, including skipped rows. */\n readonly batchRowCount: number\n /** Physical rows intentionally skipped inside this batch. */\n readonly batchRowsSkipped: number\n /** True when this batch consumed the final row of the immutable dataset version. */\n readonly inputExhausted: boolean\n}\n\nexport interface RecordProjectionMissingTargetInput\n extends LockProjectionRunForMaterializationInput {\n readonly missingTarget: ProjectionMissingTarget\n}\n\nexport interface ListProjectionRunsInput {\n readonly projectId: string\n readonly projectionId?: string\n readonly projectionKind?: ProjectionKind\n readonly datasetId?: string\n readonly datasetVersionId?: string\n /**\n * Viewable object type ids. A run is included only if every target type is present. Omit for an\n * unrestricted internal execution; an empty set matches no runs.\n */\n readonly objectTypeIds?: readonly string[]\n readonly statuses?: readonly ProjectionRunStatus[]\n readonly startedAfter?: Date\n readonly startedBefore?: Date\n readonly limit?: number\n readonly offset?: number\n readonly order?: \"asc\" | \"desc\"\n}\n\nexport interface ListProjectionRunsResult {\n readonly runs: readonly ProjectionRunRecord[]\n readonly hasMore: boolean\n readonly total: number\n}\n\nexport interface ListLatestProjectionRunsInput {\n readonly projectId: string\n readonly projectionIds: readonly string[]\n}\n\nexport interface ListLatestProjectionRunsResult {\n readonly runs: readonly ProjectionRunRecord[]\n}\n\nexport interface ProjectionRunStorage {\n queue(input: QueueProjectionRunInput): Promise<ProjectionRunRecord>\n startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim>\n failEnqueue(input: FailProjectionRunEnqueueInput): Promise<ProjectionRunRecord>\n /** Transactionally fences the current execution before ontology materialization writes. */\n lockForMaterialization(\n input: LockProjectionRunForMaterializationInput\n ): Promise<ProjectionRunRecord>\n /** Updates physical source progress only. */\n update(input: UpdateProjectionRunInput): Promise<ProjectionRunRecord>\n /**\n * Advances telemetry progress atomically with the corresponding ontology commit.\n *\n * Clears any recorded missing target: a batch that commits is a batch that was not waiting.\n */\n advanceTelemetryCheckpoint(\n input: AdvanceProjectionTelemetryCheckpointInput\n ): Promise<TelemetryProjectionRunRecord>\n /** Records the object a telemetry batch is waiting for, so the wait has a durable start. */\n recordMissingTarget(\n input: RecordProjectionMissingTargetInput\n ): Promise<TelemetryProjectionRunRecord>\n /** Telemetry success also records the worker's EOF observation atomically. */\n finish(input: FinishProjectionRunInput): Promise<ProjectionRunRecord>\n getById(params: { projectId: string; id: string }): Promise<ProjectionRunRecord | null>\n list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult>\n listLatestByProjectionIds(\n input: ListLatestProjectionRunsInput\n ): Promise<ListLatestProjectionRunsResult>\n}\n\nexport type {\n LinkProjectionTarget,\n ObjectProjectionTarget,\n PinnedDatasetVersion,\n ProjectionExecution,\n ProjectionKind,\n ProjectionMaterializationIdentity,\n ProjectionTarget,\n}\n"
7
- ],
8
- "mappings": ";;AAGO,IAAM,gCAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;ACqBA,IAAM,mBAA8D;AAAA,EAClE,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,mBAAmB;AACrB;AAEO,IAAM,gCAA+B,OAAO,KACjD,gBACF;AAEO,SAAS,0BAAyB,GAA0B;AAAA,EACjE,OAAO,EAAE,gBAAgB,GAAG,mBAAmB,EAAE;AAAA;AA6E5C,SAAS,gCAA+B,CAC7C,KACA,SACS;AAAA,EACT,MAAM,SAA2B,IAAI;AAAA,EACrC,IAAI,wBAAwB,QAAQ;AAAA,IAClC,OAAO,QAAQ,OAAO,kBAAkB,KAAK,QAAQ,OAAO,kBAAkB;AAAA,EAChF;AAAA,EACA,OAAO,QAAQ,OAAO,YAAY;AAAA;",
9
- "debugId": "20098923B6EB738064756E2164756E21",
10
- "names": []
11
- }