@sixb/core 0.1.5 → 0.1.6

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 (90) hide show
  1. package/dist/actions/index.js +8 -8
  2. package/dist/actions/types/definitions.d.ts +8 -2
  3. package/dist/actions/types/definitions.d.ts.map +1 -1
  4. package/dist/agents/index.js +13 -13
  5. package/dist/auth/index.d.ts +1 -1
  6. package/dist/auth/index.d.ts.map +1 -1
  7. package/dist/auth/index.js +1 -1
  8. package/dist/auth/runtime.d.ts.map +1 -1
  9. package/dist/auth/strategy.d.ts +1 -1
  10. package/dist/auth/strategy.d.ts.map +1 -1
  11. package/dist/auth/types.d.ts +13 -0
  12. package/dist/auth/types.d.ts.map +1 -1
  13. package/dist/bootstrap/index.js +17 -17
  14. package/dist/{chunk-78c6xvyr.js → chunk-0hc5j867.js} +2 -2
  15. package/dist/{chunk-tkdscva6.js → chunk-1e8m6yn3.js} +6 -1
  16. package/dist/{chunk-tkdscva6.js.map → chunk-1e8m6yn3.js.map} +4 -4
  17. package/dist/{chunk-xdf9yswn.js → chunk-1g8negke.js} +1 -1
  18. package/dist/{chunk-am6eczah.js → chunk-5gynx4xn.js} +8 -8
  19. package/dist/{chunk-z3ga9q5m.js → chunk-5sxb1168.js} +4 -4
  20. package/dist/chunk-72q2y8hn.js.map +3 -3
  21. package/dist/{chunk-r0rry8dn.js → chunk-8gv5z0zm.js} +1 -1
  22. package/dist/{chunk-1q02e95s.js → chunk-a44d5zgd.js} +1 -1
  23. package/dist/{chunk-h6h91mfy.js → chunk-dy311asx.js} +5 -5
  24. package/dist/{chunk-ea73pzk7.js → chunk-e42bc6m3.js} +1 -1
  25. package/dist/{chunk-a09jx26t.js → chunk-fe5ev6mr.js} +1 -1
  26. package/dist/{chunk-nzw7sm3b.js → chunk-hceck7dq.js} +1 -1
  27. package/dist/{chunk-6d5xdj1q.js → chunk-md8k0r54.js} +1 -1
  28. package/dist/{chunk-gadsm794.js → chunk-n4spznws.js} +2 -2
  29. package/dist/{chunk-tbv03sx2.js → chunk-nkbfrt0c.js} +1 -1
  30. package/dist/{chunk-84h6qhmp.js → chunk-nva1xkjz.js} +10 -10
  31. package/dist/{chunk-v0egge8b.js → chunk-t624af60.js} +24 -18
  32. package/dist/{chunk-v0egge8b.js.map → chunk-t624af60.js.map} +3 -3
  33. package/dist/{chunk-zp2v2rv6.js → chunk-tg5mee00.js} +1 -1
  34. package/dist/{chunk-4h947jd8.js → chunk-znagycq7.js} +1 -1
  35. package/dist/execution/agent.js +13 -13
  36. package/dist/execution/primitive.js +13 -13
  37. package/dist/execution/request.js +13 -13
  38. package/dist/index.js +18 -18
  39. package/dist/materializer/index.js +6 -6
  40. package/dist/objects/index.js +9 -9
  41. package/dist/objects/query/index.js +6 -6
  42. package/dist/pipelines/index.js +7 -7
  43. package/dist/projections/internal.js +5 -5
  44. package/dist/storage/agents/provider.js +3 -3
  45. package/dist/storage/index.d.ts +1 -1
  46. package/dist/storage/index.d.ts.map +1 -1
  47. package/dist/storage/index.js +5 -5
  48. package/dist/storage/ontology/in-memory/materializations.d.ts +4 -3
  49. package/dist/storage/ontology/in-memory/materializations.d.ts.map +1 -1
  50. package/dist/storage/ontology/index.d.ts +1 -1
  51. package/dist/storage/ontology/index.d.ts.map +1 -1
  52. package/dist/storage/ontology/materializations.d.ts +7 -3
  53. package/dist/storage/ontology/materializations.d.ts.map +1 -1
  54. package/dist/storage/ontology/provider.d.ts +2 -2
  55. package/dist/storage/ontology/provider.d.ts.map +1 -1
  56. package/dist/storage/workflow-runs/provider.js +3 -3
  57. package/dist/syncs/index.js +7 -7
  58. package/dist/testing/index.js +159 -21
  59. package/dist/testing/index.js.map +3 -3
  60. package/dist/testing/materializer-storage-contract.d.ts.map +1 -1
  61. package/dist/webhooks/internal.js +5 -5
  62. package/dist/workflows/index.js +14 -14
  63. package/package.json +1 -1
  64. package/src/actions/types/definitions.ts +24 -2
  65. package/src/auth/index.ts +1 -0
  66. package/src/auth/runtime.ts +8 -0
  67. package/src/auth/strategy.ts +1 -0
  68. package/src/auth/types.ts +14 -0
  69. package/src/storage/index.ts +1 -0
  70. package/src/storage/ontology/in-memory/materializations.ts +34 -22
  71. package/src/storage/ontology/index.ts +1 -0
  72. package/src/storage/ontology/materializations.ts +8 -3
  73. package/src/storage/ontology/provider.ts +2 -2
  74. package/src/testing/materializer-storage-contract.ts +170 -2
  75. /package/dist/{chunk-78c6xvyr.js.map → chunk-0hc5j867.js.map} +0 -0
  76. /package/dist/{chunk-xdf9yswn.js.map → chunk-1g8negke.js.map} +0 -0
  77. /package/dist/{chunk-am6eczah.js.map → chunk-5gynx4xn.js.map} +0 -0
  78. /package/dist/{chunk-z3ga9q5m.js.map → chunk-5sxb1168.js.map} +0 -0
  79. /package/dist/{chunk-r0rry8dn.js.map → chunk-8gv5z0zm.js.map} +0 -0
  80. /package/dist/{chunk-1q02e95s.js.map → chunk-a44d5zgd.js.map} +0 -0
  81. /package/dist/{chunk-h6h91mfy.js.map → chunk-dy311asx.js.map} +0 -0
  82. /package/dist/{chunk-ea73pzk7.js.map → chunk-e42bc6m3.js.map} +0 -0
  83. /package/dist/{chunk-a09jx26t.js.map → chunk-fe5ev6mr.js.map} +0 -0
  84. /package/dist/{chunk-nzw7sm3b.js.map → chunk-hceck7dq.js.map} +0 -0
  85. /package/dist/{chunk-6d5xdj1q.js.map → chunk-md8k0r54.js.map} +0 -0
  86. /package/dist/{chunk-gadsm794.js.map → chunk-n4spznws.js.map} +0 -0
  87. /package/dist/{chunk-tbv03sx2.js.map → chunk-nkbfrt0c.js.map} +0 -0
  88. /package/dist/{chunk-84h6qhmp.js.map → chunk-nva1xkjz.js.map} +0 -0
  89. /package/dist/{chunk-zp2v2rv6.js.map → chunk-tg5mee00.js.map} +0 -0
  90. /package/dist/{chunk-4h947jd8.js.map → chunk-znagycq7.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"materializer-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/materializer-storage-contract.ts"],"names":[],"mappings":"AACA,OAAO,EASL,KAAK,OAAO,EACb,MAAM,IAAI,CAAA;AAeX,MAAM,WAAW,mCAAmC,CAAC,QAAQ,SAAS,OAAO;IAC3E,QAAQ,CAAC,aAAa,EAAE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D;AAgFD,wBAAgB,mCAAmC,CAAC,QAAQ,SAAS,OAAO,EAC1E,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,mCAAmC,CAAC,QAAQ,CAAC,GACtD,IAAI,CAmcN"}
1
+ {"version":3,"file":"materializer-storage-contract.d.ts","sourceRoot":"","sources":["../../src/testing/materializer-storage-contract.ts"],"names":[],"mappings":"AACA,OAAO,EASL,KAAK,OAAO,EACb,MAAM,IAAI,CAAA;AAgBX,MAAM,WAAW,mCAAmC,CAAC,QAAQ,SAAS,OAAO;IAC3E,QAAQ,CAAC,aAAa,EAAE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D;AA6FD,wBAAgB,mCAAmC,CAAC,QAAQ,SAAS,OAAO,EAC1E,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,mCAAmC,CAAC,QAAQ,CAAC,GACtD,IAAI,CA6lBN"}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  canRetryWebhookRun,
7
7
  isStorageSerializationFailure
8
- } from "../chunk-v0egge8b.js";
8
+ } from "../chunk-t624af60.js";
9
9
  import"../chunk-72q2y8hn.js";
10
10
  import"../chunk-xc7hq65b.js";
11
11
  import"../chunk-cy0n5dms.js";
@@ -14,11 +14,11 @@ import"../chunk-62c7t4jc.js";
14
14
  import {
15
15
  WebhookRunError
16
16
  } from "../chunk-ezake7g1.js";
17
- import"../chunk-a09jx26t.js";
17
+ import"../chunk-fe5ev6mr.js";
18
18
  import"../chunk-htq0qh7z.js";
19
19
  import"../chunk-xkthy56g.js";
20
- import"../chunk-xdf9yswn.js";
21
- import"../chunk-nzw7sm3b.js";
20
+ import"../chunk-1g8negke.js";
21
+ import"../chunk-hceck7dq.js";
22
22
  import"../chunk-t9xghkva.js";
23
23
  import"../chunk-drjqm1aa.js";
24
24
  import"../chunk-5gd7tpv6.js";
@@ -32,7 +32,7 @@ import"../chunk-3j7573mj.js";
32
32
  import"../chunk-4gz41why.js";
33
33
  import"../chunk-zcnhxkvd.js";
34
34
  import"../chunk-t0jjkep3.js";
35
- import"../chunk-tkdscva6.js";
35
+ import"../chunk-1e8m6yn3.js";
36
36
  import"../chunk-rhrbz7d4.js";
37
37
  import"../chunk-xwnf87e1.js";
38
38
  import"../chunk-f83ka50g.js";
@@ -7,7 +7,7 @@ import {
7
7
  defineWorkflowStep,
8
8
  interventionField,
9
9
  unwrapWorkflowNodeFailure
10
- } from "../chunk-gadsm794.js";
10
+ } from "../chunk-n4spznws.js";
11
11
  import"../chunk-5sz8ta9r.js";
12
12
  import"../chunk-979ww6b3.js";
13
13
  import"../chunk-74drx9gd.js";
@@ -40,36 +40,36 @@ import {
40
40
  validateWorkflowStepInput,
41
41
  validateWorkflowStepOutput,
42
42
  validateWorkflowsAtStartup
43
- } from "../chunk-84h6qhmp.js";
44
- import"../chunk-zp2v2rv6.js";
45
- import"../chunk-r0rry8dn.js";
46
- import"../chunk-z3ga9q5m.js";
43
+ } from "../chunk-nva1xkjz.js";
44
+ import"../chunk-tg5mee00.js";
45
+ import"../chunk-8gv5z0zm.js";
46
+ import"../chunk-5sxb1168.js";
47
47
  import"../chunk-4rh2m6zg.js";
48
48
  import"../chunk-xhy826cz.js";
49
- import"../chunk-6d5xdj1q.js";
49
+ import"../chunk-md8k0r54.js";
50
50
  import"../chunk-rte5sm3z.js";
51
51
  import"../chunk-qewtks41.js";
52
52
  import"../chunk-8b55ygje.js";
53
53
  import"../chunk-51fjndv0.js";
54
54
  import"../chunk-8m5ejdpn.js";
55
55
  import"../chunk-nf4r78r4.js";
56
- import"../chunk-78c6xvyr.js";
57
- import"../chunk-1q02e95s.js";
58
- import"../chunk-tbv03sx2.js";
56
+ import"../chunk-0hc5j867.js";
57
+ import"../chunk-a44d5zgd.js";
58
+ import"../chunk-nkbfrt0c.js";
59
59
  import"../chunk-96swp2wd.js";
60
60
  import"../chunk-f6erf2vv.js";
61
- import"../chunk-v0egge8b.js";
61
+ import"../chunk-t624af60.js";
62
62
  import"../chunk-72q2y8hn.js";
63
63
  import"../chunk-xc7hq65b.js";
64
64
  import"../chunk-cy0n5dms.js";
65
65
  import"../chunk-dysw7nsm.js";
66
66
  import"../chunk-62c7t4jc.js";
67
67
  import"../chunk-ezake7g1.js";
68
- import"../chunk-a09jx26t.js";
68
+ import"../chunk-fe5ev6mr.js";
69
69
  import"../chunk-htq0qh7z.js";
70
70
  import"../chunk-xkthy56g.js";
71
- import"../chunk-xdf9yswn.js";
72
- import"../chunk-nzw7sm3b.js";
71
+ import"../chunk-1g8negke.js";
72
+ import"../chunk-hceck7dq.js";
73
73
  import"../chunk-t9xghkva.js";
74
74
  import"../chunk-drjqm1aa.js";
75
75
  import"../chunk-5gd7tpv6.js";
@@ -87,7 +87,7 @@ import"../chunk-yt8wphsx.js";
87
87
  import"../chunk-rdeagz6b.js";
88
88
  import"../chunk-dm72y8wt.js";
89
89
  import"../chunk-t0jjkep3.js";
90
- import"../chunk-tkdscva6.js";
90
+ import"../chunk-1e8m6yn3.js";
91
91
  import"../chunk-rhrbz7d4.js";
92
92
  import"../chunk-xwnf87e1.js";
93
93
  import"../chunk-f83ka50g.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "The ontology-first runtime for Sixb — objects, links, telemetry, actions, and provider contracts",
5
5
  "keywords": [
6
6
  "sixb",
@@ -1,3 +1,4 @@
1
+ import type { ReadonlyJsonValue } from "../../json"
1
2
  import type { ObjectTypeWithPropertyTokens } from "../../ontology/tokens"
2
3
  import type {
3
4
  ActionEditsHandler,
@@ -13,6 +14,18 @@ import type {
13
14
  } from "./handlers"
14
15
  import type { ActionParamsConfig, InferActionParams } from "./params"
15
16
 
17
+ // Validate fields structurally so JSON-shaped interfaces do not need an index signature.
18
+ // Builders infer the original result first, then check it without widening via NoInfer.
19
+ type WritebackJsonValue<T> = T extends ReadonlyJsonValue
20
+ ? T
21
+ : T extends (...args: never[]) => unknown
22
+ ? never
23
+ : T extends object
24
+ ? { [K in keyof T]: WritebackJsonValue<T[K]> }
25
+ : never
26
+
27
+ type WritebackResult<T> = T extends ReturnType<() => void> ? T : WritebackJsonValue<T>
28
+
16
29
  export interface BaseActionDefinition<
17
30
  TId extends string = string,
18
31
  TParams extends ActionParamsConfig = ActionParamsConfig,
@@ -136,7 +149,11 @@ export interface GlobalActionPhaseBuilder<TId extends string, TParams extends Ac
136
149
  validator: GlobalActionValidator<InferActionParams<TParams>>
137
150
  ): GlobalActionPhaseBuilder<TId, TParams>
138
151
  writeback<const TResult>(
139
- handler: GlobalActionWritebackHandler<InferActionParams<TParams>, TResult>
152
+ handler: GlobalActionWritebackHandler<InferActionParams<TParams>, TResult> &
153
+ GlobalActionWritebackHandler<
154
+ InferActionParams<TParams>,
155
+ WritebackResult<NoInfer<Awaited<TResult>>>
156
+ >
140
157
  ): GlobalActionAfterWritebackBuilder<TId, TParams, TResult>
141
158
  edits<const TResult extends ActionNoResultHandlerResult>(
142
159
  handler: GlobalActionEditsHandler<InferActionParams<TParams>, undefined, TResult>
@@ -152,7 +169,12 @@ export interface ObjectActionPhaseBuilder<
152
169
  validator: ActionValidator<TObjectType, InferActionParams<TParams>>
153
170
  ): ObjectActionPhaseBuilder<TId, TObjectType, TParams>
154
171
  writeback<const TResult>(
155
- handler: ActionWritebackHandler<TObjectType, InferActionParams<TParams>, TResult>
172
+ handler: ActionWritebackHandler<TObjectType, InferActionParams<TParams>, TResult> &
173
+ ActionWritebackHandler<
174
+ TObjectType,
175
+ InferActionParams<TParams>,
176
+ WritebackResult<NoInfer<Awaited<TResult>>>
177
+ >
156
178
  ): ObjectActionAfterWritebackBuilder<TId, TObjectType, TParams, TResult>
157
179
  edits<const TResult extends ActionNoResultHandlerResult>(
158
180
  handler: ActionEditsHandler<TObjectType, InferActionParams<TParams>, undefined, TResult>
package/src/auth/index.ts CHANGED
@@ -127,6 +127,7 @@ export type {
127
127
  ReactivateMemberInput,
128
128
  ReactivateMemberResult,
129
129
  ResolvedAuthConfig,
130
+ RevealedInvitationLink,
130
131
  RevokeAccessTokenResult,
131
132
  RevokeInvitationInput,
132
133
  RevokeInvitationResult,
@@ -1242,6 +1242,7 @@ export class AuthRuntime {
1242
1242
  audience: deliveryAudience,
1243
1243
  returnTo: options.delivery?.returnTo ?? sanitizeReturnTo(input.returnTo),
1244
1244
  requestOrigin: options.delivery?.requestOrigin ?? new URL(request.url).origin,
1245
+ revealLink: input.revealLink,
1245
1246
  now,
1246
1247
  })
1247
1248
  } catch (error) {
@@ -1249,6 +1250,13 @@ export class AuthRuntime {
1249
1250
  throw error
1250
1251
  }
1251
1252
 
1253
+ // The runtime owns the disclosure boundary even for third-party strategies. A strategy that
1254
+ // accidentally returns a link must not expose it unless the caller opted in explicitly.
1255
+ delivery = {
1256
+ status: delivery.status,
1257
+ ...(input.revealLink && delivery.link ? { link: delivery.link } : {}),
1258
+ }
1259
+
1252
1260
  if (delivery.status === "not_supported") {
1253
1261
  return { invitation, delivery }
1254
1262
  }
@@ -38,6 +38,7 @@ export type {
38
38
  OidcCallbackResult,
39
39
  OidcStartSignInInput,
40
40
  OidcStartSignInResult,
41
+ RevealedInvitationLink,
41
42
  } from "./types"
42
43
  export {
43
44
  isInvitationDeliveryAuthStrategy,
package/src/auth/types.ts CHANGED
@@ -73,6 +73,8 @@ export interface InvitationDeliveryInput {
73
73
  readonly audience: AuthSessionAudience
74
74
  readonly returnTo: string
75
75
  readonly requestOrigin: string
76
+ /** Return the delivered link once so the inviter can copy it through an approved channel. */
77
+ readonly revealLink?: boolean
76
78
  readonly now?: Date
77
79
  }
78
80
 
@@ -80,8 +82,18 @@ export type AuthEmailDeliveryStatus = "sent" | "skipped" | "rate_limited"
80
82
 
81
83
  export type InviteDeliveryStatus = AuthEmailDeliveryStatus | "not_supported"
82
84
 
85
+ export interface RevealedInvitationLink {
86
+ readonly url: string
87
+ readonly expiresAt?: Date
88
+ }
89
+
83
90
  export interface InviteDeliveryResult {
84
91
  readonly status: InviteDeliveryStatus
92
+ /**
93
+ * Present only when the caller explicitly requests `revealLink`. Treat this as a credential:
94
+ * never persist it or include it in list responses, logs, or analytics.
95
+ */
96
+ readonly link?: RevealedInvitationLink
85
97
  }
86
98
 
87
99
  export interface InvitationDeliveryAuthStrategy extends AuthStrategy {
@@ -214,6 +226,8 @@ export interface InviteUserInput {
214
226
  readonly groupIds?: readonly string[]
215
227
  readonly expiresAt?: Date
216
228
  readonly returnTo?: string
229
+ /** Reveal the delivered link once in the result. Defaults to false. */
230
+ readonly revealLink?: boolean
217
231
  }
218
232
 
219
233
  export interface InviteUserResult {
@@ -422,6 +422,7 @@ export type {
422
422
  MaterializationClassificationWorkRecord,
423
423
  MaterializationEventWorkRecord,
424
424
  MaterializationIncidentObjectWorkRecord,
425
+ MaterializationLinkScopeRevision,
425
426
  MaterializationLinkScopeState,
426
427
  MaterializationLinkState,
427
428
  MaterializationObjectExistence,
@@ -38,6 +38,7 @@ import type {
38
38
  FinalizeMaterializationInput,
39
39
  MaterializationCardinalityOccupantWorkRecord,
40
40
  MaterializationEventWorkRecord,
41
+ MaterializationLinkScopeRevision,
41
42
  MaterializationLinkScopeState,
42
43
  MaterializationLinkState,
43
44
  MaterializationObjectExistence,
@@ -134,7 +135,7 @@ export interface SessionState {
134
135
  readonly workStreams: Record<StreamMaterializationWorkInput["order"], WorkStreamState>
135
136
  workSealed: boolean
136
137
  incidentLinksByObject: Map<string, readonly OntologyLinkRef[]> | null
137
- linkScopeStates: Map<string, MaterializationLinkScopeState> | null
138
+ linkSlotStates: Map<string, MaterializationLinkScopeState> | null
138
139
  readonly objectExistence: Map<string, MaterializationObjectExistenceWorkRecord>
139
140
  replacement: ReplacementSessionState | null
140
141
  }
@@ -209,13 +210,13 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
209
210
  await this.assertObject(expected, input.commit.projectId)
210
211
  for (const expected of input.expected.links)
211
212
  await this.assertLink(expected, input.commit.projectId)
212
- const expectedScopeStates = new Map<string, MaterializationLinkScopeState>()
213
+ const expectedScopeRevisions = new Map<string, MaterializationLinkScopeRevision>()
213
214
  for (const expected of input.expected.linkScopes) {
214
215
  const key = linkScopeSortKey(expected.source, expected.linkId)
215
216
  const current =
216
- expectedScopeStates.get(key) ??
217
- this.computeLinkScopeState(input.commit.projectId, expected.source, expected.linkId)
218
- expectedScopeStates.set(key, current)
217
+ expectedScopeRevisions.get(key) ??
218
+ this.computeEffectiveLinkScope(input.commit.projectId, expected.source, expected.linkId)
219
+ expectedScopeRevisions.set(key, current)
219
220
  if (current.fingerprint !== expected.fingerprint) {
220
221
  throw new MaterializationConflictError(
221
222
  "effective-state",
@@ -247,7 +248,7 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
247
248
  },
248
249
  workSealed: false,
249
250
  incidentLinksByObject: null,
250
- linkScopeStates: expectedScopeStates,
251
+ linkSlotStates: new Map(),
251
252
  objectExistence: new Map<string, MaterializationObjectExistenceWorkRecord>(),
252
253
  replacement: null,
253
254
  }
@@ -275,7 +276,7 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
275
276
  session.appliedPlanItems.length = 0
276
277
  session.outboxEnvelopes.clear()
277
278
  session.incidentLinksByObject = null
278
- session.linkScopeStates = null
279
+ session.linkSlotStates = null
279
280
  session.objectExistence.clear()
280
281
  session.replacement = null
281
282
  this.liveSessions.delete(session)
@@ -329,7 +330,7 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
329
330
  linkScopeSortKey(value.source, value.linkId)
330
331
  )) {
331
332
  this.requireSession(input.session)
332
- const value = this.linkScopeState(session, scope.source, scope.linkId)
333
+ const value = this.linkSlotState(session, scope.source, scope.linkId)
333
334
  // Scope rows are currently only requested for cardinality-one links, so the complete member
334
335
  // set is bounded by the ontology invariant. Replacement scope enumeration is flattened in
335
336
  // its dedicated stream and never uses this shape.
@@ -1136,25 +1137,44 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
1136
1137
  }
1137
1138
  }
1138
1139
 
1139
- private linkScopeState(
1140
+ private linkSlotState(
1140
1141
  session: SessionState,
1141
1142
  source: OntologyObjectRef,
1142
1143
  linkId: string
1143
1144
  ): MaterializationLinkScopeState {
1144
- session.linkScopeStates ??= new Map()
1145
+ session.linkSlotStates ??= new Map()
1145
1146
  const key = linkScopeSortKey(source, linkId)
1146
- const existing = session.linkScopeStates.get(key)
1147
+ const existing = session.linkSlotStates.get(key)
1147
1148
  if (existing) return structuredClone(existing)
1148
- const computed = this.computeLinkScopeState(session.header.commit.projectId, source, linkId)
1149
- session.linkScopeStates.set(key, computed)
1149
+ const computed = this.computeLinkSlotState(session.header.commit.projectId, source, linkId)
1150
+ session.linkSlotStates.set(key, computed)
1150
1151
  return structuredClone(computed)
1151
1152
  }
1152
1153
 
1153
- private computeLinkScopeState(
1154
+ private computeLinkSlotState(
1154
1155
  projectId: string,
1155
1156
  source: OntologyObjectRef,
1156
1157
  linkId: string
1157
1158
  ): MaterializationLinkScopeState {
1159
+ const value = this.computeEffectiveLinkScope(projectId, source, linkId)
1160
+ return {
1161
+ ...value,
1162
+ sourceAssertion: this.findActiveLinkScopeSource(projectId, source, linkId),
1163
+ override: structuredClone(
1164
+ publicLinkSlotOverride(
1165
+ this.state.linkSlotOverrides.get(
1166
+ projectEntityKey(projectId, linkScopeKey(source, linkId))
1167
+ )
1168
+ )
1169
+ ),
1170
+ }
1171
+ }
1172
+
1173
+ private computeEffectiveLinkScope(
1174
+ projectId: string,
1175
+ source: OntologyObjectRef,
1176
+ linkId: string
1177
+ ): MaterializationLinkScopeRevision & Pick<MaterializationLinkScopeState, "effective"> {
1158
1178
  const rows: ObjectLinkRow[] = []
1159
1179
  getInMemoryObjectMaterializerAdapter(this.objects).visitExactScopeLinks(
1160
1180
  projectId,
@@ -1177,14 +1197,6 @@ export class InMemoryOntologyMaterializationStorage implements OntologyMateriali
1177
1197
  const effective = rows.length === 1 ? linkSnapshot(rows[0]!) : null
1178
1198
  return {
1179
1199
  ...finishScopeAccumulator(accumulator),
1180
- sourceAssertion: this.findActiveLinkScopeSource(projectId, source, linkId),
1181
- override: structuredClone(
1182
- publicLinkSlotOverride(
1183
- this.state.linkSlotOverrides.get(
1184
- projectEntityKey(projectId, linkScopeKey(source, linkId))
1185
- )
1186
- )
1187
- ),
1188
1200
  effective,
1189
1201
  }
1190
1202
  }
@@ -45,6 +45,7 @@ export type {
45
45
  MaterializationClassificationWorkRecord,
46
46
  MaterializationEventWorkRecord,
47
47
  MaterializationIncidentObjectWorkRecord,
48
+ MaterializationLinkScopeRevision,
48
49
  MaterializationLinkScopeState,
49
50
  MaterializationLinkState,
50
51
  MaterializationObjectExistence,
@@ -77,17 +77,22 @@ export interface MaterializationLinkState {
77
77
  readonly effective: EffectiveLinkSnapshot | null
78
78
  }
79
79
 
80
- export interface MaterializationLinkScopeState {
80
+ /** Revision of the complete effective `(source, linkId)` member set, for either cardinality. */
81
+ export interface MaterializationLinkScopeRevision {
81
82
  readonly source: OntologyObjectRef
82
83
  readonly linkId: string
84
+ readonly effectiveCount: number
85
+ readonly fingerprint: string
86
+ }
87
+
88
+ /** Cardinality-one slot state used while resolving managed and projected authority. */
89
+ export interface MaterializationLinkScopeState extends MaterializationLinkScopeRevision {
83
90
  /** Complete active source value for the link slot. */
84
91
  readonly sourceAssertion: StoredSourceLinkAssertion | null
85
92
  /** Managed slot authority; absence delegates to source authority. */
86
93
  readonly override: StoredLinkSlotOverride | null
87
94
  /** The single effective edge, if its endpoints are effective. */
88
95
  readonly effective: EffectiveLinkSnapshot | null
89
- readonly effectiveCount: number
90
- readonly fingerprint: string
91
96
  }
92
97
 
93
98
  export interface MaterializationStatePage {
@@ -14,7 +14,7 @@ import type { OntologyCommitOriginSelector, OntologyCommitWrite } from "./commit
14
14
  import type {
15
15
  FinalizeMaterializationInput,
16
16
  MaterializationEventWorkRecord,
17
- MaterializationLinkScopeState,
17
+ MaterializationLinkScopeRevision,
18
18
  MaterializationPlanChunk,
19
19
  MaterializationPlanHeader,
20
20
  MaterializationPlanWorkItem,
@@ -99,7 +99,7 @@ export function appendScopeSnapshot(
99
99
 
100
100
  export function finishScopeAccumulator(
101
101
  accumulator: LinkScopeAccumulator
102
- ): Pick<MaterializationLinkScopeState, "source" | "linkId" | "effectiveCount" | "fingerprint"> {
102
+ ): MaterializationLinkScopeRevision {
103
103
  accumulator.hash.update("]")
104
104
  return {
105
105
  source: accumulator.source,
@@ -15,6 +15,7 @@ import { restoreTrustedPrimitiveExecutionScope } from "../execution/durable"
15
15
  import { createTestingScope } from "../execution/scopes"
16
16
  import type { OntologyMaterializer } from "../materializer"
17
17
  import {
18
+ createLinkScopeFingerprint,
18
19
  createOntologyMaterializer,
19
20
  type OntologyEditOperation,
20
21
  ProjectionRegistry,
@@ -42,7 +43,7 @@ const Device = defineObjectType({
42
43
  prop("name", "string", { required: true }),
43
44
  prop("temperature", "double", { mode: "telemetry" }),
44
45
  ],
45
- links: [link.self("parent", { cardinality: "one" })],
46
+ links: [link.self("parent", { cardinality: "one" }), link.self("peers", { cardinality: "many" })],
46
47
  })
47
48
 
48
49
  const devices = defineDataset("storage_contract_devices", {
@@ -55,6 +56,9 @@ const devices = defineDataset("storage_contract_devices", {
55
56
  const readings = defineDataset("storage_contract_readings", {
56
57
  schema: [col("device_id", "string"), col("at", "timestamp"), col("value", "float64")],
57
58
  })
59
+ const devicePeers = defineDataset("storage_contract_device_peers", {
60
+ schema: [col("source_id", "string"), col("target_id", "string")],
61
+ })
58
62
  const ConflictDevice = defineObjectType({
59
63
  id: "StorageContractConflictDevice",
60
64
  name: "Conflict device",
@@ -88,6 +92,10 @@ const temperatureProjection = defineProjection(
88
92
  )
89
93
  .fromDataset(readings)
90
94
  .points({ objectId: "device_id", at: "at", value: "value" })
95
+ const devicePeersProjection = defineProjection("storage_contract_device_peers", Device.l.peers)
96
+ .fromDataset(devicePeers)
97
+ .sourceField("source_id")
98
+ .targetField("target_id")
91
99
  const conflictDeviceProjection = defineProjection(
92
100
  "storage_contract_conflict_devices",
93
101
  ConflictDevice
@@ -98,11 +106,17 @@ const conflictDeviceProjection = defineProjection(
98
106
 
99
107
  const ontology = new OntologyRegistry({ sources: [Device, ConflictDevice] })
100
108
  const projections = new ProjectionRegistry({
101
- projections: [deviceProjection, temperatureProjection, conflictDeviceProjection],
109
+ projections: [
110
+ deviceProjection,
111
+ temperatureProjection,
112
+ devicePeersProjection,
113
+ conflictDeviceProjection,
114
+ ],
102
115
  ontology,
103
116
  datasetsById: new Map<string, DatasetDefinition>([
104
117
  [devices.id, devices],
105
118
  [readings.id, readings],
119
+ [devicePeers.id, devicePeers],
106
120
  [conflictDevices.id, conflictDevices],
107
121
  ]),
108
122
  })
@@ -561,6 +575,160 @@ export function runMaterializerStorageContractSuite<TStorage extends Storage>(
561
575
  await provider.cleanup?.(createdStorage)
562
576
  }
563
577
  })
578
+
579
+ test(`${name} fences projected cardinality-many link scopes by fingerprint`, async () => {
580
+ const createdStorage = await provider.createStorage()
581
+ const storage = requireContractStorage(createdStorage)
582
+ let materializationOrdinal = 0
583
+ const materializer = createOntologyMaterializer({
584
+ projectId: "materializer-storage-contract",
585
+ ontology,
586
+ projections,
587
+ storage,
588
+ dependencies: {
589
+ batching: { sourceStageRows: 1, statePageRows: 1, planChunkRows: 1 },
590
+ clock: () => new Date("2026-02-01T12:00:00.000Z"),
591
+ materializationId: () => `many-scope-contract-candidate-${++materializationOrdinal}`,
592
+ },
593
+ })
594
+ const runtimeMaterializer = materializer.withScope(runtimeScope())
595
+ const source = { objectTypeId: Device.id, primaryId: "source" }
596
+ const peer = (primaryId: string) => ({ objectTypeId: Device.id, primaryId })
597
+ const linkRef = (targetId: string) => ({
598
+ source,
599
+ linkId: "peers",
600
+ target: peer(targetId),
601
+ })
602
+
603
+ try {
604
+ await runtimeMaterializer.edits.commit({
605
+ mode: "atomic",
606
+ source: { kind: "runtime", requestId: "many-scope-endpoints" },
607
+ operations: ["source", "peer-a", "peer-b", "peer-c"].map((primaryId) => ({
608
+ id: `create-${primaryId}`,
609
+ kind: "object.create" as const,
610
+ ref: peer(primaryId),
611
+ properties: { name: primaryId },
612
+ })),
613
+ expectedObjects: [],
614
+ expectedLinks: [],
615
+ expectedLinkScopes: [],
616
+ })
617
+
618
+ const datasetVersion = {
619
+ datasetId: devicePeers.id,
620
+ versionId: "many-links-v1",
621
+ createdAt: "2026-02-01T00:00:00.000Z",
622
+ }
623
+ const execution = await claim(storage, {
624
+ runId: "many-links-v1",
625
+ projectionId: devicePeersProjection.id,
626
+ protocol: "replacement",
627
+ datasetVersion,
628
+ })
629
+ const projectedLinks = [linkRef("peer-a"), linkRef("peer-b")]
630
+ await (
631
+ await projectionMaterializer(
632
+ materializer,
633
+ storage,
634
+ devicePeersProjection.id,
635
+ "many-links-v1"
636
+ )
637
+ ).projections.replace({
638
+ source: { projectionId: devicePeersProjection.id },
639
+ datasetVersion,
640
+ execution,
641
+ entries: entries(
642
+ projectedLinks.map((ref) => ({
643
+ root: { kind: "link", ref },
644
+ assertions: [{ kind: "link", ref }],
645
+ }))
646
+ ),
647
+ })
648
+
649
+ const observedLinks = await storage.objects.listLinks({
650
+ projectId: "materializer-storage-contract",
651
+ objectTypeId: Device.id,
652
+ objectId: source.primaryId,
653
+ linkId: "peers",
654
+ })
655
+ const fingerprint = createLinkScopeFingerprint(
656
+ observedLinks.map((row) => {
657
+ if (row.lastCommitId === undefined) {
658
+ throw new Error("Projected contract link is missing materializer provenance.")
659
+ }
660
+ return {
661
+ ref: linkRef(row.targetId),
662
+ createdAt: row.createdAt.toISOString(),
663
+ updatedAt: row.updatedAt.toISOString(),
664
+ lastCommitId: row.lastCommitId,
665
+ }
666
+ })
667
+ )
668
+ const commitObservation = async (runId: string, name: string) => {
669
+ await queueTestActionRun(storage, {
670
+ id: runId,
671
+ projectId: "materializer-storage-contract",
672
+ actionId: "observePeers",
673
+ subject: { kind: "object", objectTypeId: Device.id, primaryId: source.primaryId },
674
+ params: {},
675
+ idempotencyKey: runId,
676
+ })
677
+ await storage.actionRuns.start({ id: runId, projectId: "materializer-storage-contract" })
678
+ const actionMaterializer = await primitiveMaterializer(materializer, storage, {
679
+ kind: "action",
680
+ id: "observePeers",
681
+ runId,
682
+ })
683
+ return actionMaterializer.edits.commit({
684
+ mode: "atomic",
685
+ source: { kind: "action", actionId: "observePeers", runId },
686
+ operations: [
687
+ {
688
+ id: `record-${runId}`,
689
+ kind: "object.patch",
690
+ ref: source,
691
+ set: { name },
692
+ unset: [],
693
+ reset: [],
694
+ },
695
+ ],
696
+ expectedObjects: [],
697
+ expectedLinks: [],
698
+ expectedLinkScopes: [{ source, linkId: "peers", fingerprint }],
699
+ })
700
+ }
701
+
702
+ // Regression guard for G-021: make begin() hydrate the expected scope through the
703
+ // cardinality-one slot reader again and this otherwise unrelated Action commit fails before
704
+ // its already-stable fingerprint can be compared.
705
+ await expect(commitObservation("many-scope-action", "observed peers")).resolves.toMatchObject(
706
+ { kind: "edit", created: true }
707
+ )
708
+
709
+ await runtimeMaterializer.edits.commit({
710
+ mode: "atomic",
711
+ source: { kind: "runtime", requestId: "many-scope-change" },
712
+ operations: [{ id: "add-peer", kind: "link.upsert", ref: linkRef("peer-c") }],
713
+ expectedObjects: [],
714
+ expectedLinks: [],
715
+ expectedLinkScopes: [],
716
+ })
717
+
718
+ await expect(commitObservation("stale-many-scope-action", "must not commit")).rejects.toThrow(
719
+ "Expected link scope changed"
720
+ )
721
+ expect(
722
+ await storage.objects.getByPrimaryId({
723
+ projectId: "materializer-storage-contract",
724
+ objectTypeId: Device.id,
725
+ primaryId: source.primaryId,
726
+ })
727
+ ).toMatchObject({ properties: { name: "observed peers" } })
728
+ } finally {
729
+ await provider.cleanup?.(createdStorage)
730
+ }
731
+ })
564
732
  }
565
733
 
566
734
  function runtimeScope() {