@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
@@ -2,16 +2,20 @@ import { MaterializationConflictError } from "./errors"
2
2
  import type { PinnedDatasetVersion } from "./model"
3
3
 
4
4
  export type PinnedDatasetWatermarkConflict =
5
- | "dataset-mismatch"
6
5
  | "version-metadata-mismatch"
7
6
  | "older-version"
8
7
  | "ambiguous-version"
9
8
 
9
+ /**
10
+ * Orders versions of one dataset. A projection rebound to a different dataset starts a new
11
+ * watermark: versions of two datasets have no order, and whether a run may read that dataset is
12
+ * decided by the registered definition, which rejects runs pinned to any other dataset.
13
+ */
10
14
  export function comparePinnedDatasetWatermarks(
11
15
  active: PinnedDatasetVersion,
12
16
  next: PinnedDatasetVersion
13
17
  ): PinnedDatasetWatermarkConflict | null {
14
- if (active.datasetId !== next.datasetId) return "dataset-mismatch"
18
+ if (active.datasetId !== next.datasetId) return null
15
19
  if (active.versionId === next.versionId && active.createdAt !== next.createdAt) {
16
20
  return "version-metadata-mismatch"
17
21
  }
@@ -40,8 +44,6 @@ function pinnedDatasetWatermarkConflictMessage(
40
44
  subject: string
41
45
  ): string {
42
46
  switch (conflict) {
43
- case "dataset-mismatch":
44
- return `${subject} dataset does not match the active source dataset.`
45
47
  case "version-metadata-mismatch":
46
48
  return `${subject} reused an immutable dataset version id with different metadata.`
47
49
  case "older-version":
@@ -217,6 +217,13 @@ export type ProjectionRunTerminalDecision =
217
217
  readonly error?: SixbFailure<ProjectionRunFailureCode>
218
218
  readonly inputExhausted?: never
219
219
  }
220
+ | {
221
+ readonly protocol: "replacement"
222
+ /** A run of the same projection pinned to a later dataset version makes this one stale. */
223
+ readonly status: "superseded"
224
+ readonly error?: never
225
+ readonly inputExhausted?: never
226
+ }
220
227
 
221
228
  /**
222
229
  * Queue-agnostic terminal decision for one fenced projection execution.
@@ -70,13 +70,33 @@ dataset entries
70
70
  -> atomically activate candidate and finalize commit
71
71
  ```
72
72
 
73
- Source ingress is sealed before commit time is assigned. A retry reuses the ready candidate and
74
- the same semantic commit identity. Activation and ontology commit finalization are atomic; the
75
- projection run stores no separate commit pointer.
73
+ Source ingress is sealed before commit time is assigned. Activation and ontology commit
74
+ finalization are atomic; the projection run stores no separate commit pointer.
76
75
 
77
- Projection runs finish only through `projections.finishRun(...)`. Its serializable transaction reads
78
- the authoritative ontology commit before replacement success, rejects failure after commit, and
79
- then applies the fenced run transition.
76
+ A candidate belongs to its run, not to the delivery that staged it:
77
+
78
+ ```text
79
+ delivery fails transiently -> candidate stays with the run
80
+ next delivery -> adopts it: ready is published as is, staging resumes at its last root
81
+ run ends without success -> finishRun abandons it in the same transaction
82
+ maintenance -> deletes abandoned candidates whole, without retention
83
+ ```
84
+
85
+ An adopted candidate staged against another delta base is abandoned and replaced, since its roots
86
+ describe a different change. Staging resumes by reading the entries again from the start, which is
87
+ sound because they are deterministic for the pinned dataset version and definition.
88
+
89
+ Projection runs finish only through `projections.finishRun(...)`, including a failure found before
90
+ materialization began. Its serializable transaction reads the authoritative ontology commit before
91
+ replacement success, rejects failure after commit, releases the run's candidate, and then applies
92
+ the fenced run transition. Success must match the deployed definition; failure only has to hold
93
+ the run's execution token, so a run pinned to a definition a deploy has since replaced can still
94
+ end.
95
+
96
+ A replacement run ends `superseded`, without materializing, when a run of the same projection
97
+ pinned to a later dataset version is queued, running, or succeeded. The worker checks this on every
98
+ delivery after its claim; `finishRun` checks it again under its transaction, since a newer run that
99
+ failed in between replaces nothing, and releases the candidate like any other end without success.
80
100
 
81
101
  ## Managed edits and Actions
82
102
 
@@ -12,7 +12,10 @@ import type { ProjectionDefinition, ResolvedProjection } from "../../projections
12
12
  import type { OntologyCommitRecord } from "../../storage/ontology"
13
13
  import type { MaterializerContext, MaterializerStorage } from "../context"
14
14
  import { withSerializationRetry } from "../execution/commit-lifecycle"
15
- import { lockProjectionRunForMaterialization } from "../execution/run-correlation"
15
+ import {
16
+ type LockedProjectionExecution,
17
+ lockProjectionRunForMaterialization,
18
+ } from "../execution/run-correlation"
16
19
  import {
17
20
  assertMaterializerRunExecution,
18
21
  assertTrustedPrimitiveMutationExecution,
@@ -31,12 +34,21 @@ import { createProjectionRunMaterializationIdentity } from "../shared/projection
31
34
  interface PreparedProjectionRunFinish {
32
35
  readonly projectId: string
33
36
  readonly input: ProjectionRunFinishInput
34
- readonly identity: ProjectionMaterializationIdentity
35
- readonly resolved: ResolvedProjection<ProjectionDefinition>
37
+ /**
38
+ * The deployed definition a success must match. A failure needs none: it only has to hold the
39
+ * run's execution token, and it must stay possible after a deploy changed or removed the
40
+ * projection, or the run and its candidate could never end.
41
+ */
42
+ readonly deployed: DeployedProjectionIdentity | null
36
43
  readonly finishedAt: Date
37
44
  readonly execution: MaterializerExecution
38
45
  }
39
46
 
47
+ interface DeployedProjectionIdentity {
48
+ readonly identity: ProjectionMaterializationIdentity
49
+ readonly resolved: ResolvedProjection<ProjectionDefinition>
50
+ }
51
+
40
52
  export async function finishProjectionRun(
41
53
  context: MaterializerContext,
42
54
  raw: MaterializerCommand<ProjectionRunFinishInput>
@@ -57,28 +69,35 @@ function prepareProjectionRunFinish(
57
69
  const source = normalizeProjectionSourceRef(raw.input.source)
58
70
  const datasetVersion = normalizePinnedDatasetVersion(raw.input.datasetVersion)
59
71
  const projectionExecution = normalizeProjectionExecution(raw.input.execution)
72
+ const input = { ...raw.input, source, datasetVersion, execution: projectionExecution }
73
+ return {
74
+ projectId: context.projectId,
75
+ input,
76
+ deployed: input.status === "succeeded" ? resolveDeployedProjection(context, input) : null,
77
+ finishedAt: new Date(raw.input.finishedAt ?? context.clock()),
78
+ execution: prepareMaterializerExecution(context.projectId, raw.scope),
79
+ }
80
+ }
81
+
82
+ function resolveDeployedProjection(
83
+ context: Pick<MaterializerContext, "projectionRegistry">,
84
+ input: ProjectionRunFinishInput
85
+ ): DeployedProjectionIdentity {
60
86
  const resolved =
61
- raw.input.protocol === "replacement"
62
- ? context.projectionRegistry.resolveSource(source.projectionId)
63
- : context.projectionRegistry.resolveTelemetry(source.projectionId)
64
- if (resolved.datasetId !== datasetVersion.datasetId) {
87
+ input.protocol === "replacement"
88
+ ? context.projectionRegistry.resolveSource(input.source.projectionId)
89
+ : context.projectionRegistry.resolveTelemetry(input.source.projectionId)
90
+ if (resolved.datasetId !== input.datasetVersion.datasetId) {
65
91
  throw new MaterializationValidationError(
66
92
  `Projection '${resolved.projectionId}' requires dataset '${resolved.datasetId}'.`
67
93
  )
68
94
  }
69
95
  const identity = createProjectionRunMaterializationIdentity({
70
96
  resolved,
71
- datasetVersion,
97
+ datasetVersion: input.datasetVersion,
72
98
  ontologyRevision: context.projectionRegistry.ontologyRevision,
73
99
  })
74
- return {
75
- projectId: context.projectId,
76
- input: { ...raw.input, source, datasetVersion, execution: projectionExecution },
77
- identity,
78
- resolved,
79
- finishedAt: new Date(raw.input.finishedAt ?? context.clock()),
80
- execution: prepareMaterializerExecution(context.projectId, raw.scope),
81
- }
100
+ return { identity, resolved }
82
101
  }
83
102
 
84
103
  async function finishProjectionRunTransaction(
@@ -91,36 +110,115 @@ async function finishProjectionRunTransaction(
91
110
  id: command.input.source.projectionId,
92
111
  runId: command.input.execution.projectionRunId,
93
112
  })
94
- const { projectionRuns, run } = await lockProjectionRunForMaterialization(storage, {
95
- projectId: command.projectId,
96
- projectionRunId: command.input.execution.projectionRunId,
97
- executionToken: command.input.execution.executionToken,
98
- identity: command.identity,
99
- resolved: command.resolved,
100
- })
113
+ const { projectionRuns, run } = command.deployed
114
+ ? await lockProjectionRunForMaterialization(storage, {
115
+ projectId: command.projectId,
116
+ projectionRunId: command.input.execution.projectionRunId,
117
+ executionToken: command.input.execution.executionToken,
118
+ identity: command.deployed.identity,
119
+ resolved: command.deployed.resolved,
120
+ })
121
+ : await lockProjectionRunAsStored(storage, command)
101
122
  assertMaterializerRunExecution(command.execution, run.executionId, `Projection run '${run.id}'`)
102
123
 
103
124
  if (command.input.protocol === "replacement") {
104
125
  await assertReplacementTerminalDecision(storage, command)
126
+ if (command.input.status === "superseded") {
127
+ await assertRunSuperseded(projectionRuns, command)
128
+ }
129
+ if (command.input.status !== "succeeded") {
130
+ // The candidate belongs to the run: once the run ends, nothing may adopt or activate it.
131
+ await storage.ontology.sources.abandon({
132
+ kind: "run",
133
+ projectId: command.projectId,
134
+ source: command.input.source,
135
+ execution: command.input.execution,
136
+ abandonedAt: command.finishedAt.toISOString(),
137
+ })
138
+ }
105
139
  }
106
140
 
107
141
  await projectionRuns.finish({
108
142
  id: command.input.execution.projectionRunId,
109
143
  projectId: command.projectId,
110
144
  executionToken: command.input.execution.executionToken,
111
- identity: command.identity,
145
+ identity: run.identity,
112
146
  ...terminalDecision(command.input),
113
147
  finishedAt: command.finishedAt,
114
148
  })
115
149
  }
116
150
 
151
+ /**
152
+ * Checked again here, under the finish transaction, rather than trusted from the caller's earlier
153
+ * read: the newer run may have failed since, and a run superseded by a failure would leave the
154
+ * projection with neither version.
155
+ */
156
+ async function assertRunSuperseded(
157
+ projectionRuns: LockedProjectionExecution["projectionRuns"],
158
+ command: PreparedProjectionRunFinish
159
+ ): Promise<void> {
160
+ const newer = await projectionRuns.findSupersedingRun({
161
+ projectId: command.projectId,
162
+ id: command.input.execution.projectionRunId,
163
+ })
164
+ if (newer) return
165
+ throw new MaterializationConflictError(
166
+ "run-correlation",
167
+ `Projection run '${command.input.execution.projectionRunId}' cannot be superseded: no run of a later dataset version stands to replace it.`
168
+ )
169
+ }
170
+
171
+ /** Locks a run by the identity it was pinned with, for a decision that needs no definition. */
172
+ async function lockProjectionRunAsStored(
173
+ storage: MaterializerStorage,
174
+ command: PreparedProjectionRunFinish
175
+ ): Promise<LockedProjectionExecution> {
176
+ const projectionRuns = storage.projectionRuns
177
+ if (!projectionRuns) {
178
+ throw new MaterializationValidationError(
179
+ "Storage transaction does not provide projection run capabilities."
180
+ )
181
+ }
182
+ const { input } = command
183
+ const stored = await projectionRuns.getById({
184
+ projectId: command.projectId,
185
+ id: input.execution.projectionRunId,
186
+ })
187
+ if (
188
+ !stored ||
189
+ stored.identity.projectionId !== input.source.projectionId ||
190
+ stored.identity.protocol !== input.protocol ||
191
+ !datasetVersionsEqual(stored.identity.datasetVersion, input.datasetVersion)
192
+ ) {
193
+ throw new MaterializationConflictError(
194
+ "run-correlation",
195
+ `Projection run '${input.execution.projectionRunId}' does not match its finish decision.`
196
+ )
197
+ }
198
+ const run = await projectionRuns.lockForMaterialization({
199
+ id: stored.id,
200
+ projectId: command.projectId,
201
+ executionToken: input.execution.executionToken,
202
+ identity: stored.identity,
203
+ })
204
+ return { projectionRuns, run }
205
+ }
206
+
117
207
  function assertValidTerminalDecision(input: ProjectionRunFinishInput): void {
118
208
  if (input.protocol !== "replacement" && input.protocol !== "telemetry") {
119
209
  throw new MaterializationValidationError("Projection finish protocol is invalid.")
120
210
  }
121
- if (input.status !== "succeeded" && input.status !== "failed" && input.status !== "cancelled") {
211
+ if (
212
+ input.status !== "succeeded" &&
213
+ input.status !== "failed" &&
214
+ input.status !== "cancelled" &&
215
+ input.status !== "superseded"
216
+ ) {
122
217
  throw new MaterializationValidationError("Projection finish status must be terminal.")
123
218
  }
219
+ if (input.status === "superseded" && input.protocol !== "replacement") {
220
+ throw new MaterializationValidationError("Only a replacement projection run can be superseded.")
221
+ }
124
222
  const inputExhausted = "inputExhausted" in input ? input.inputExhausted : undefined
125
223
  if (input.status === "succeeded" && input.protocol === "telemetry") {
126
224
  if (inputExhausted === true) return
@@ -136,6 +234,9 @@ function assertValidTerminalDecision(input: ProjectionRunFinishInput): void {
136
234
  }
137
235
 
138
236
  function terminalDecision(input: ProjectionRunFinishInput): ProjectionRunTerminalDecision {
237
+ if (input.status === "superseded") {
238
+ return { protocol: "replacement", status: "superseded" }
239
+ }
139
240
  if (input.status !== "succeeded") {
140
241
  return {
141
242
  protocol: input.protocol,
@@ -182,8 +283,10 @@ function assertReplacementCommitCorrelation(
182
283
  commit: OntologyCommitRecord,
183
284
  command: PreparedProjectionRunFinish
184
285
  ): void {
185
- const { input, identity } = command
286
+ const { input } = command
287
+ const identity = command.deployed?.identity
186
288
  if (
289
+ !identity ||
187
290
  commit.origin.kind !== "projection" ||
188
291
  commit.executionId !== command.execution.executionId ||
189
292
  commit.intent.kind !== "projection" ||
@@ -1,6 +1,5 @@
1
1
  import { assertPinnedDatasetWatermark } from "../../materialization/dataset-watermark"
2
2
  import {
3
- MaterializationCancellationError,
4
3
  MaterializationConflictError,
5
4
  MaterializationValidationError,
6
5
  } from "../../materialization/errors"
@@ -18,6 +17,7 @@ import type {
18
17
  } from "../../materialization/model"
19
18
  import type { ProjectionRegistry } from "../../projections/registry"
20
19
  import type {
20
+ AdoptedSourceMaterialization,
21
21
  OntologyCommitRecord,
22
22
  OntologyCommitWrite,
23
23
  OntologyMaterializationStorage,
@@ -76,6 +76,8 @@ interface PreparedProjectionReplacement {
76
76
  interface ProjectionCandidate {
77
77
  readonly materializationId: string
78
78
  readonly createdAt: string
79
+ /** Set when a redelivery adopted the candidate a previous execution of this run staged. */
80
+ readonly adopted?: AdoptedSourceMaterialization
79
81
  readonly expectedSource: {
80
82
  readonly source: ProjectionSourceRef
81
83
  readonly activeMaterializationId: string | null
@@ -96,14 +98,11 @@ export async function replaceProjection(
96
98
  const replay = await admitProjectionExecution(context, command)
97
99
  if (replay) return replay
98
100
 
101
+ // A failed attempt leaves its candidate to the run: a redelivery adopts it, and the run's
102
+ // terminal transition (`finishRun`) abandons it.
99
103
  const candidate = await prepareProjectionCandidate(context, command)
100
- try {
101
- const ready = await stageProjectionCandidate(context, command, candidate)
102
- return await commitProjectionCandidate(context, command, ready)
103
- } catch (error) {
104
- await abandonFailedCandidate(context, command, candidate, error)
105
- throw error
106
- }
104
+ const ready = await stageProjectionCandidate(context, command, candidate)
105
+ return commitProjectionCandidate(context, command, ready)
107
106
  }
108
107
 
109
108
  function prepareProjectionReplacement(
@@ -230,12 +229,11 @@ async function prepareProjectionCandidate(
230
229
  context: MaterializerContext,
231
230
  command: PreparedProjectionReplacement
232
231
  ): Promise<ProjectionCandidate> {
233
- await context.storage.ontology.sources.abandon({
234
- kind: "reclaim",
232
+ const adopted = await context.storage.ontology.sources.adopt({
235
233
  projectId: context.projectId,
236
234
  source: command.source,
237
235
  execution: command.execution,
238
- abandonedAt: context.clock().toISOString(),
236
+ adoptedAt: context.clock().toISOString(),
239
237
  })
240
238
  const active = await context.storage.ontology.sources.getActive({
241
239
  projectId: context.projectId,
@@ -243,14 +241,35 @@ async function prepareProjectionCandidate(
243
241
  })
244
242
  validateProjectionWatermark(active, command.datasetVersion)
245
243
  assertDeltaBase(active, command)
244
+ const expectedSource = {
245
+ source: command.source,
246
+ activeMaterializationId: active?.materializationId ?? null,
247
+ lastCommitId: active?.lastCommitId ?? null,
248
+ }
249
+ if (adopted && sameSourceBase(adopted.record.base, command.base)) {
250
+ return {
251
+ materializationId: adopted.record.materializationId,
252
+ createdAt: adopted.record.createdAt,
253
+ adopted,
254
+ expectedSource,
255
+ }
256
+ }
257
+ if (adopted) {
258
+ // Staged against another delta base (the active source moved, or this attempt reads the
259
+ // whole version): its roots answer a different question, so it cannot be completed.
260
+ await context.storage.ontology.sources.abandon({
261
+ kind: "candidate",
262
+ projectId: context.projectId,
263
+ source: command.source,
264
+ materializationId: adopted.record.materializationId,
265
+ execution: command.execution,
266
+ abandonedAt: context.clock().toISOString(),
267
+ })
268
+ }
246
269
  return {
247
270
  materializationId: context.materializationId(),
248
271
  createdAt: context.clock().toISOString(),
249
- expectedSource: {
250
- source: command.source,
251
- activeMaterializationId: active?.materializationId ?? null,
252
- lastCommitId: active?.lastCommitId ?? null,
253
- },
272
+ expectedSource,
254
273
  }
255
274
  }
256
275
 
@@ -259,6 +278,26 @@ async function stageProjectionCandidate(
259
278
  command: PreparedProjectionReplacement,
260
279
  candidate: ProjectionCandidate
261
280
  ): Promise<ReadyProjectionReplacement> {
281
+ const staged = await stageOrReuseCandidate(context, command, candidate)
282
+ return {
283
+ ...candidate,
284
+ staged,
285
+ // Commit time starts only after the source candidate is sealed ready.
286
+ identity: timestampCommitIdentity(command.identity, context.clock()),
287
+ }
288
+ }
289
+
290
+ async function stageOrReuseCandidate(
291
+ context: MaterializerContext,
292
+ command: PreparedProjectionReplacement,
293
+ candidate: ProjectionCandidate
294
+ ): Promise<StagedProjectionMaterialization> {
295
+ const adopted = candidate.adopted?.record
296
+ if (adopted?.status === "ready" && adopted.rootCount !== null) {
297
+ // The entries are left unread: the pinned version and definition that produced this candidate
298
+ // are the ones this run is bound to.
299
+ return { rootCount: adopted.rootCount, assertionCount: adopted.assertionCount ?? 0 }
300
+ }
262
301
  const input = {
263
302
  source: command.source,
264
303
  materializationId: candidate.materializationId,
@@ -270,15 +309,10 @@ async function stageProjectionCandidate(
270
309
  createdAt: candidate.createdAt,
271
310
  entries: command.entries,
272
311
  ...(command.base ? { base: command.base } : {}),
312
+ resumeStagingOrdinal: candidate.adopted?.resumeStagingOrdinal ?? 0,
273
313
  validateEntry: createProjectionEntryValidator(context.ontology, command.resolved),
274
314
  }
275
- const staged = await stageCandidateEntries(context, input, command.signal)
276
- return {
277
- ...candidate,
278
- staged,
279
- // Commit time starts only after the source candidate is sealed ready.
280
- identity: timestampCommitIdentity(command.identity, context.clock()),
281
- }
315
+ return stageCandidateEntries(context, input, command.signal)
282
316
  }
283
317
 
284
318
  async function stageCandidateEntries(
@@ -462,32 +496,6 @@ function projectionActivation(
462
496
  }
463
497
  }
464
498
 
465
- async function abandonFailedCandidate(
466
- context: MaterializerContext,
467
- command: PreparedProjectionReplacement,
468
- candidate: ProjectionCandidate,
469
- error: unknown
470
- ): Promise<void> {
471
- if (!shouldAbandonCandidate(error)) return
472
- await context.storage.ontology.sources.abandon({
473
- kind: "candidate",
474
- projectId: context.projectId,
475
- source: command.source,
476
- materializationId: candidate.materializationId,
477
- execution: command.execution,
478
- abandonedAt: context.clock().toISOString(),
479
- })
480
- }
481
-
482
- function shouldAbandonCandidate(error: unknown): boolean {
483
- if (error instanceof MaterializationValidationError) return true
484
- if (error instanceof MaterializationCancellationError) return true
485
- return (
486
- error instanceof MaterializationConflictError &&
487
- (error.kind === "run-correlation" || error.kind === "idempotency")
488
- )
489
- }
490
-
491
499
  function validateProjectionWatermark(
492
500
  active: Awaited<ReturnType<OntologyStorage["sources"]["getActive"]>>,
493
501
  next: PinnedDatasetVersion
@@ -496,6 +504,16 @@ function validateProjectionWatermark(
496
504
  assertPinnedDatasetWatermark(active.datasetVersion, next, "Projection replacement")
497
505
  }
498
506
 
507
+ function sameSourceBase(
508
+ left: ProjectionSourceBase | undefined,
509
+ right: ProjectionSourceBase | undefined
510
+ ): boolean {
511
+ return (
512
+ left?.materializationId === right?.materializationId &&
513
+ left?.lastCommitId === right?.lastCommitId
514
+ )
515
+ }
516
+
499
517
  function assertDeltaBase(
500
518
  active: OntologySourceRecord | null,
501
519
  command: PreparedProjectionReplacement
@@ -23,8 +23,10 @@ export interface StagedProjectionMaterialization {
23
23
  /**
24
24
  * Persist and seal one explicit source-materialization candidate.
25
25
  *
26
- * A candidate is deliberately left staging/ready when execution is interrupted. The next claim
27
- * reclaims it with a fresh execution token; age is never used to infer abandonment.
26
+ * A candidate is deliberately left staging/ready when execution is interrupted. The next delivery
27
+ * of the same run adopts it and resumes at `resumeStagingOrdinal`: the entries are read again from
28
+ * the start, since they are deterministic for the pinned version, but only the roots from that
29
+ * ordinal on are written. Age is never used to infer abandonment; the run's end is.
28
30
  */
29
31
  export async function stageProjectionMaterialization(
30
32
  context: Pick<
@@ -42,6 +44,8 @@ export async function stageProjectionMaterialization(
42
44
  readonly createdAt: string
43
45
  readonly entries: AsyncIterable<ProjectionSourceEntry | ProjectionSourceDeletion>
44
46
  readonly base?: ProjectionSourceBase
47
+ /** First root ordinal to write; earlier ones are already staged. */
48
+ readonly resumeStagingOrdinal: number
45
49
  readonly validateEntry: ProjectionEntryValidator
46
50
  readonly signal?: AbortSignal
47
51
  }
@@ -77,16 +81,17 @@ export async function stageProjectionMaterialization(
77
81
  context.projectionRegistry.resolveSource(input.source.projectionId),
78
82
  root
79
83
  )
80
- yield { root, stagingOrdinal: rootCount++ }
84
+ const stagingOrdinal = rootCount++
85
+ if (stagingOrdinal >= input.resumeStagingOrdinal) yield { root, stagingOrdinal }
81
86
  continue
82
87
  }
83
88
  const entry = input.validateEntry(normalizeProjectionSourceEntry(rawEntry))
84
- const stagingOrdinal = rootCount
89
+ const stagingOrdinal = rootCount++
90
+ assertionCount += entry.assertions.length
91
+ if (stagingOrdinal < input.resumeStagingOrdinal) continue
85
92
  for (const assertion of entry.assertions) {
86
- assertionCount += 1
87
93
  yield { root: entry.root, assertion, stagingOrdinal }
88
94
  }
89
- rootCount += 1
90
95
  }
91
96
  throwIfAborted(input.signal)
92
97
  }
@@ -6,6 +6,7 @@ export const PROJECTION_RUN_FAILURE_CODES = [
6
6
  "runtime.cancelled",
7
7
  "queue.enqueue_failed",
8
8
  "projection.execution_failed",
9
+ "storage.unavailable",
9
10
  ] as const satisfies readonly [SixbErrorCode, ...SixbErrorCode[]]
10
11
 
11
12
  export type ProjectionRunFailureCode = (typeof PROJECTION_RUN_FAILURE_CODES)[number]
@@ -449,8 +449,11 @@ export {
449
449
  snapshotObjectReadExecutionLimits,
450
450
  } from "./objects"
451
451
  export type {
452
+ AbandonRunSourceMaterializationInput,
452
453
  AbandonSourceMaterializationCandidateInput,
453
454
  AbandonSourceMaterializationInput,
455
+ AdoptedSourceMaterialization,
456
+ AdoptSourceMaterializationInput,
454
457
  ApplyMaterializationChunkInput,
455
458
  ApplyMaterializationResult,
456
459
  AssertSourceMaterializationExecution,
@@ -530,9 +533,9 @@ export type {
530
533
  OntologyStorage,
531
534
  OntologyVectorStorage,
532
535
  ProjectionOntologyCommitIntent,
536
+ PurgeAbandonedSourceMaterializationsInput,
533
537
  PurgePublishedOntologyOutboxInput,
534
538
  ReadMaterializationObjectExistenceInput,
535
- ReclaimSourceMaterializationInput,
536
539
  RescheduleOntologyOutboxLeaseInput,
537
540
  SourceActivationWrite,
538
541
  SourceReplacementLinkState,
@@ -595,6 +598,7 @@ export {
595
598
  export type {
596
599
  AdvanceProjectionTelemetryCheckpointInput,
597
600
  FailProjectionRunEnqueueInput,
601
+ FindSupersedingProjectionRunInput,
598
602
  FinishProjectionRunInput,
599
603
  LinkProjectionRunRecord,
600
604
  LinkProjectionTarget,
@@ -616,6 +620,7 @@ export type {
616
620
  ProjectionTarget,
617
621
  ProjectionTelemetryCheckpoint,
618
622
  QueueProjectionRunInput,
623
+ RecordProjectionAttemptFailureInput,
619
624
  RecordProjectionMissingTargetInput,
620
625
  StartOrReclaimProjectionRunInput,
621
626
  TelemetryProjectionRunRecord,