@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.
- package/dist/actions/index.js +8 -8
- package/dist/actions/types/definitions.d.ts +8 -2
- package/dist/actions/types/definitions.d.ts.map +1 -1
- package/dist/agents/index.js +13 -13
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/runtime.d.ts.map +1 -1
- package/dist/auth/strategy.d.ts +1 -1
- package/dist/auth/strategy.d.ts.map +1 -1
- package/dist/auth/types.d.ts +13 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/bootstrap/index.js +17 -17
- package/dist/{chunk-78c6xvyr.js → chunk-0hc5j867.js} +2 -2
- package/dist/{chunk-tkdscva6.js → chunk-1e8m6yn3.js} +6 -1
- package/dist/{chunk-tkdscva6.js.map → chunk-1e8m6yn3.js.map} +4 -4
- package/dist/{chunk-xdf9yswn.js → chunk-1g8negke.js} +1 -1
- package/dist/{chunk-am6eczah.js → chunk-5gynx4xn.js} +8 -8
- package/dist/{chunk-z3ga9q5m.js → chunk-5sxb1168.js} +4 -4
- package/dist/chunk-72q2y8hn.js.map +3 -3
- package/dist/{chunk-r0rry8dn.js → chunk-8gv5z0zm.js} +1 -1
- package/dist/{chunk-1q02e95s.js → chunk-a44d5zgd.js} +1 -1
- package/dist/{chunk-h6h91mfy.js → chunk-dy311asx.js} +5 -5
- package/dist/{chunk-ea73pzk7.js → chunk-e42bc6m3.js} +1 -1
- package/dist/{chunk-a09jx26t.js → chunk-fe5ev6mr.js} +1 -1
- package/dist/{chunk-nzw7sm3b.js → chunk-hceck7dq.js} +1 -1
- package/dist/{chunk-6d5xdj1q.js → chunk-md8k0r54.js} +1 -1
- package/dist/{chunk-gadsm794.js → chunk-n4spznws.js} +2 -2
- package/dist/{chunk-tbv03sx2.js → chunk-nkbfrt0c.js} +1 -1
- package/dist/{chunk-84h6qhmp.js → chunk-nva1xkjz.js} +10 -10
- package/dist/{chunk-v0egge8b.js → chunk-t624af60.js} +24 -18
- package/dist/{chunk-v0egge8b.js.map → chunk-t624af60.js.map} +3 -3
- package/dist/{chunk-zp2v2rv6.js → chunk-tg5mee00.js} +1 -1
- package/dist/{chunk-4h947jd8.js → chunk-znagycq7.js} +1 -1
- package/dist/execution/agent.js +13 -13
- package/dist/execution/primitive.js +13 -13
- package/dist/execution/request.js +13 -13
- package/dist/index.js +18 -18
- package/dist/materializer/index.js +6 -6
- package/dist/objects/index.js +9 -9
- package/dist/objects/query/index.js +6 -6
- package/dist/pipelines/index.js +7 -7
- package/dist/projections/internal.js +5 -5
- package/dist/storage/agents/provider.js +3 -3
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +5 -5
- package/dist/storage/ontology/in-memory/materializations.d.ts +4 -3
- package/dist/storage/ontology/in-memory/materializations.d.ts.map +1 -1
- package/dist/storage/ontology/index.d.ts +1 -1
- package/dist/storage/ontology/index.d.ts.map +1 -1
- package/dist/storage/ontology/materializations.d.ts +7 -3
- package/dist/storage/ontology/materializations.d.ts.map +1 -1
- package/dist/storage/ontology/provider.d.ts +2 -2
- package/dist/storage/ontology/provider.d.ts.map +1 -1
- package/dist/storage/workflow-runs/provider.js +3 -3
- package/dist/syncs/index.js +7 -7
- package/dist/testing/index.js +159 -21
- package/dist/testing/index.js.map +3 -3
- package/dist/testing/materializer-storage-contract.d.ts.map +1 -1
- package/dist/webhooks/internal.js +5 -5
- package/dist/workflows/index.js +14 -14
- package/package.json +1 -1
- package/src/actions/types/definitions.ts +24 -2
- package/src/auth/index.ts +1 -0
- package/src/auth/runtime.ts +8 -0
- package/src/auth/strategy.ts +1 -0
- package/src/auth/types.ts +14 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/ontology/in-memory/materializations.ts +34 -22
- package/src/storage/ontology/index.ts +1 -0
- package/src/storage/ontology/materializations.ts +8 -3
- package/src/storage/ontology/provider.ts +2 -2
- package/src/testing/materializer-storage-contract.ts +170 -2
- /package/dist/{chunk-78c6xvyr.js.map → chunk-0hc5j867.js.map} +0 -0
- /package/dist/{chunk-xdf9yswn.js.map → chunk-1g8negke.js.map} +0 -0
- /package/dist/{chunk-am6eczah.js.map → chunk-5gynx4xn.js.map} +0 -0
- /package/dist/{chunk-z3ga9q5m.js.map → chunk-5sxb1168.js.map} +0 -0
- /package/dist/{chunk-r0rry8dn.js.map → chunk-8gv5z0zm.js.map} +0 -0
- /package/dist/{chunk-1q02e95s.js.map → chunk-a44d5zgd.js.map} +0 -0
- /package/dist/{chunk-h6h91mfy.js.map → chunk-dy311asx.js.map} +0 -0
- /package/dist/{chunk-ea73pzk7.js.map → chunk-e42bc6m3.js.map} +0 -0
- /package/dist/{chunk-a09jx26t.js.map → chunk-fe5ev6mr.js.map} +0 -0
- /package/dist/{chunk-nzw7sm3b.js.map → chunk-hceck7dq.js.map} +0 -0
- /package/dist/{chunk-6d5xdj1q.js.map → chunk-md8k0r54.js.map} +0 -0
- /package/dist/{chunk-gadsm794.js.map → chunk-n4spznws.js.map} +0 -0
- /package/dist/{chunk-tbv03sx2.js.map → chunk-nkbfrt0c.js.map} +0 -0
- /package/dist/{chunk-84h6qhmp.js.map → chunk-nva1xkjz.js.map} +0 -0
- /package/dist/{chunk-zp2v2rv6.js.map → chunk-tg5mee00.js.map} +0 -0
- /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;
|
|
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-
|
|
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-
|
|
17
|
+
import"../chunk-fe5ev6mr.js";
|
|
18
18
|
import"../chunk-htq0qh7z.js";
|
|
19
19
|
import"../chunk-xkthy56g.js";
|
|
20
|
-
import"../chunk-
|
|
21
|
-
import"../chunk-
|
|
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-
|
|
35
|
+
import"../chunk-1e8m6yn3.js";
|
|
36
36
|
import"../chunk-rhrbz7d4.js";
|
|
37
37
|
import"../chunk-xwnf87e1.js";
|
|
38
38
|
import"../chunk-f83ka50g.js";
|
package/dist/workflows/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
defineWorkflowStep,
|
|
8
8
|
interventionField,
|
|
9
9
|
unwrapWorkflowNodeFailure
|
|
10
|
-
} from "../chunk-
|
|
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-
|
|
44
|
-
import"../chunk-
|
|
45
|
-
import"../chunk-
|
|
46
|
-
import"../chunk-
|
|
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-
|
|
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-
|
|
57
|
-
import"../chunk-
|
|
58
|
-
import"../chunk-
|
|
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-
|
|
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-
|
|
68
|
+
import"../chunk-fe5ev6mr.js";
|
|
69
69
|
import"../chunk-htq0qh7z.js";
|
|
70
70
|
import"../chunk-xkthy56g.js";
|
|
71
|
-
import"../chunk-
|
|
72
|
-
import"../chunk-
|
|
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-
|
|
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,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
package/src/auth/runtime.ts
CHANGED
|
@@ -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
|
}
|
package/src/auth/strategy.ts
CHANGED
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 {
|
package/src/storage/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
this.
|
|
218
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1140
|
+
private linkSlotState(
|
|
1140
1141
|
session: SessionState,
|
|
1141
1142
|
source: OntologyObjectRef,
|
|
1142
1143
|
linkId: string
|
|
1143
1144
|
): MaterializationLinkScopeState {
|
|
1144
|
-
session.
|
|
1145
|
+
session.linkSlotStates ??= new Map()
|
|
1145
1146
|
const key = linkScopeSortKey(source, linkId)
|
|
1146
|
-
const existing = session.
|
|
1147
|
+
const existing = session.linkSlotStates.get(key)
|
|
1147
1148
|
if (existing) return structuredClone(existing)
|
|
1148
|
-
const computed = this.
|
|
1149
|
-
session.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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: [
|
|
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() {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|