Expose authoritative lifecycle deletion through capability bindings
This commit is contained in:
@@ -333,7 +333,8 @@ operationName
|
|||||||
;
|
;
|
||||||
|
|
||||||
operationProvider
|
operationProvider
|
||||||
: STATE identifier DOT statePrimitive
|
: LIFECYCLE DOT identifier
|
||||||
|
| STATE identifier DOT statePrimitive
|
||||||
| EDGE identifier DOT identifier DOT edgePrimitive
|
| EDGE identifier DOT identifier DOT edgePrimitive
|
||||||
| INTERFACE identifier typeArguments? DOT memberOperationRef
|
| INTERFACE identifier typeArguments? DOT memberOperationRef
|
||||||
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
|
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
|
||||||
@@ -550,6 +551,7 @@ COMMIT: 'commit';
|
|||||||
REVISION: 'revision';
|
REVISION: 'revision';
|
||||||
SEMANTIC_MAJOR: 'semantic-major';
|
SEMANTIC_MAJOR: 'semantic-major';
|
||||||
ON_DELETE: 'on-delete';
|
ON_DELETE: 'on-delete';
|
||||||
|
LIFECYCLE: 'lifecycle';
|
||||||
RETAIN_OTHER: 'retain-other';
|
RETAIN_OTHER: 'retain-other';
|
||||||
KEYED: 'keyed';
|
KEYED: 'keyed';
|
||||||
PUBLIC_TRAVERSAL: 'public-traversal';
|
PUBLIC_TRAVERSAL: 'public-traversal';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -72,79 +72,80 @@ COMMIT=71
|
|||||||
REVISION=72
|
REVISION=72
|
||||||
SEMANTIC_MAJOR=73
|
SEMANTIC_MAJOR=73
|
||||||
ON_DELETE=74
|
ON_DELETE=74
|
||||||
RETAIN_OTHER=75
|
LIFECYCLE=75
|
||||||
KEYED=76
|
RETAIN_OTHER=76
|
||||||
PUBLIC_TRAVERSAL=77
|
KEYED=77
|
||||||
ID=78
|
PUBLIC_TRAVERSAL=78
|
||||||
DOC=79
|
ID=79
|
||||||
MODE=80
|
DOC=80
|
||||||
EMITS=81
|
MODE=81
|
||||||
RECEIVER=82
|
EMITS=82
|
||||||
REQUIRES=83
|
RECEIVER=83
|
||||||
ANY=84
|
REQUIRES=84
|
||||||
GET=85
|
ANY=85
|
||||||
SET=86
|
GET=86
|
||||||
WATCH=87
|
SET=87
|
||||||
START=88
|
WATCH=88
|
||||||
STOP=89
|
START=89
|
||||||
READ=90
|
STOP=90
|
||||||
WRITE=91
|
READ=91
|
||||||
RESOLVE=92
|
WRITE=92
|
||||||
CONNECT=93
|
RESOLVE=93
|
||||||
DISCONNECT=94
|
CONNECT=94
|
||||||
CALL=95
|
DISCONNECT=95
|
||||||
WATCH_START=96
|
CALL=96
|
||||||
WATCH_STOP=97
|
WATCH_START=97
|
||||||
SUBSCRIBE=98
|
WATCH_STOP=98
|
||||||
UNSUBSCRIBE=99
|
SUBSCRIBE=99
|
||||||
REGISTER=100
|
UNSUBSCRIBE=100
|
||||||
CRDT=101
|
REGISTER=101
|
||||||
OPTIONAL_ONE=102
|
CRDT=102
|
||||||
EXACTLY_ONE=103
|
OPTIONAL_ONE=103
|
||||||
MANY_UNIQUE=104
|
EXACTLY_ONE=104
|
||||||
MANY=105
|
MANY_UNIQUE=105
|
||||||
ORDERED=106
|
MANY=106
|
||||||
UNIT=107
|
ORDERED=107
|
||||||
WATCH_HANDLE=108
|
UNIT=108
|
||||||
MESSAGE=109
|
WATCH_HANDLE=109
|
||||||
ATOM_REF=110
|
MESSAGE=110
|
||||||
INTERFACE_REF=111
|
ATOM_REF=111
|
||||||
OPTIONAL=112
|
INTERFACE_REF=112
|
||||||
LIST=113
|
OPTIONAL=113
|
||||||
RECORD=114
|
LIST=114
|
||||||
BOOL=115
|
RECORD=115
|
||||||
BYTES=116
|
BOOL=116
|
||||||
DOUBLE=117
|
BYTES=117
|
||||||
INT32=118
|
DOUBLE=118
|
||||||
INT64=119
|
INT32=119
|
||||||
STRING=120
|
INT64=120
|
||||||
UINT32=121
|
STRING=121
|
||||||
UINT64=122
|
UINT32=122
|
||||||
TRUE=123
|
UINT64=123
|
||||||
FALSE=124
|
TRUE=124
|
||||||
NULL=125
|
FALSE=125
|
||||||
ARROW=126
|
NULL=126
|
||||||
COLON=127
|
ARROW=127
|
||||||
SEMI=128
|
COLON=128
|
||||||
COMMA=129
|
SEMI=129
|
||||||
DOT=130
|
COMMA=130
|
||||||
LBRACE=131
|
DOT=131
|
||||||
RBRACE=132
|
LBRACE=132
|
||||||
LBRACK=133
|
RBRACE=133
|
||||||
RBRACK=134
|
LBRACK=134
|
||||||
LPAREN=135
|
RBRACK=135
|
||||||
RPAREN=136
|
LPAREN=136
|
||||||
LT=137
|
RPAREN=137
|
||||||
GT=138
|
LT=138
|
||||||
AMP=139
|
GT=139
|
||||||
EQUAL=140
|
AMP=140
|
||||||
INTEGER=141
|
EQUAL=141
|
||||||
JSON_NUMBER=142
|
INTEGER=142
|
||||||
IDENTIFIER=143
|
JSON_NUMBER=143
|
||||||
STRING_LITERAL=144
|
IDENTIFIER=144
|
||||||
LINE_COMMENT=145
|
STRING_LITERAL=145
|
||||||
BLOCK_COMMENT=146
|
LINE_COMMENT=146
|
||||||
WS=147
|
BLOCK_COMMENT=147
|
||||||
|
WS=148
|
||||||
'edit'=1
|
'edit'=1
|
||||||
'creation'=2
|
'creation'=2
|
||||||
'initialize'=3
|
'initialize'=3
|
||||||
@@ -219,69 +220,70 @@ WS=147
|
|||||||
'revision'=72
|
'revision'=72
|
||||||
'semantic-major'=73
|
'semantic-major'=73
|
||||||
'on-delete'=74
|
'on-delete'=74
|
||||||
'retain-other'=75
|
'lifecycle'=75
|
||||||
'keyed'=76
|
'retain-other'=76
|
||||||
'public-traversal'=77
|
'keyed'=77
|
||||||
'id'=78
|
'public-traversal'=78
|
||||||
'doc'=79
|
'id'=79
|
||||||
'mode'=80
|
'doc'=80
|
||||||
'emits'=81
|
'mode'=81
|
||||||
'receiver'=82
|
'emits'=82
|
||||||
'requires'=83
|
'receiver'=83
|
||||||
'any'=84
|
'requires'=84
|
||||||
'get'=85
|
'any'=85
|
||||||
'set'=86
|
'get'=86
|
||||||
'watch'=87
|
'set'=87
|
||||||
'start'=88
|
'watch'=88
|
||||||
'stop'=89
|
'start'=89
|
||||||
'read'=90
|
'stop'=90
|
||||||
'write'=91
|
'read'=91
|
||||||
'resolve'=92
|
'write'=92
|
||||||
'connect'=93
|
'resolve'=93
|
||||||
'disconnect'=94
|
'connect'=94
|
||||||
'call'=95
|
'disconnect'=95
|
||||||
'watch-start'=96
|
'call'=96
|
||||||
'watch-stop'=97
|
'watch-start'=97
|
||||||
'subscribe'=98
|
'watch-stop'=98
|
||||||
'unsubscribe'=99
|
'subscribe'=99
|
||||||
'register'=100
|
'unsubscribe'=100
|
||||||
'crdt'=101
|
'register'=101
|
||||||
'optional-one'=102
|
'crdt'=102
|
||||||
'exactly-one'=103
|
'optional-one'=103
|
||||||
'many-unique'=104
|
'exactly-one'=104
|
||||||
'many'=105
|
'many-unique'=105
|
||||||
'ordered'=106
|
'many'=106
|
||||||
'unit'=107
|
'ordered'=107
|
||||||
'watch-handle'=108
|
'unit'=108
|
||||||
'message'=109
|
'watch-handle'=109
|
||||||
'atom-ref'=110
|
'message'=110
|
||||||
'interface-ref'=111
|
'atom-ref'=111
|
||||||
'optional'=112
|
'interface-ref'=112
|
||||||
'list'=113
|
'optional'=113
|
||||||
'record'=114
|
'list'=114
|
||||||
'bool'=115
|
'record'=115
|
||||||
'bytes'=116
|
'bool'=116
|
||||||
'double'=117
|
'bytes'=117
|
||||||
'int32'=118
|
'double'=118
|
||||||
'int64'=119
|
'int32'=119
|
||||||
'string'=120
|
'int64'=120
|
||||||
'uint32'=121
|
'string'=121
|
||||||
'uint64'=122
|
'uint32'=122
|
||||||
'true'=123
|
'uint64'=123
|
||||||
'false'=124
|
'true'=124
|
||||||
'null'=125
|
'false'=125
|
||||||
'->'=126
|
'null'=126
|
||||||
':'=127
|
'->'=127
|
||||||
';'=128
|
':'=128
|
||||||
','=129
|
';'=129
|
||||||
'.'=130
|
','=130
|
||||||
'{'=131
|
'.'=131
|
||||||
'}'=132
|
'{'=132
|
||||||
'['=133
|
'}'=133
|
||||||
']'=134
|
'['=134
|
||||||
'('=135
|
']'=135
|
||||||
')'=136
|
'('=136
|
||||||
'<'=137
|
')'=137
|
||||||
'>'=138
|
'<'=138
|
||||||
'&'=139
|
'>'=139
|
||||||
'='=140
|
'&'=140
|
||||||
|
'='=141
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -72,79 +72,80 @@ COMMIT=71
|
|||||||
REVISION=72
|
REVISION=72
|
||||||
SEMANTIC_MAJOR=73
|
SEMANTIC_MAJOR=73
|
||||||
ON_DELETE=74
|
ON_DELETE=74
|
||||||
RETAIN_OTHER=75
|
LIFECYCLE=75
|
||||||
KEYED=76
|
RETAIN_OTHER=76
|
||||||
PUBLIC_TRAVERSAL=77
|
KEYED=77
|
||||||
ID=78
|
PUBLIC_TRAVERSAL=78
|
||||||
DOC=79
|
ID=79
|
||||||
MODE=80
|
DOC=80
|
||||||
EMITS=81
|
MODE=81
|
||||||
RECEIVER=82
|
EMITS=82
|
||||||
REQUIRES=83
|
RECEIVER=83
|
||||||
ANY=84
|
REQUIRES=84
|
||||||
GET=85
|
ANY=85
|
||||||
SET=86
|
GET=86
|
||||||
WATCH=87
|
SET=87
|
||||||
START=88
|
WATCH=88
|
||||||
STOP=89
|
START=89
|
||||||
READ=90
|
STOP=90
|
||||||
WRITE=91
|
READ=91
|
||||||
RESOLVE=92
|
WRITE=92
|
||||||
CONNECT=93
|
RESOLVE=93
|
||||||
DISCONNECT=94
|
CONNECT=94
|
||||||
CALL=95
|
DISCONNECT=95
|
||||||
WATCH_START=96
|
CALL=96
|
||||||
WATCH_STOP=97
|
WATCH_START=97
|
||||||
SUBSCRIBE=98
|
WATCH_STOP=98
|
||||||
UNSUBSCRIBE=99
|
SUBSCRIBE=99
|
||||||
REGISTER=100
|
UNSUBSCRIBE=100
|
||||||
CRDT=101
|
REGISTER=101
|
||||||
OPTIONAL_ONE=102
|
CRDT=102
|
||||||
EXACTLY_ONE=103
|
OPTIONAL_ONE=103
|
||||||
MANY_UNIQUE=104
|
EXACTLY_ONE=104
|
||||||
MANY=105
|
MANY_UNIQUE=105
|
||||||
ORDERED=106
|
MANY=106
|
||||||
UNIT=107
|
ORDERED=107
|
||||||
WATCH_HANDLE=108
|
UNIT=108
|
||||||
MESSAGE=109
|
WATCH_HANDLE=109
|
||||||
ATOM_REF=110
|
MESSAGE=110
|
||||||
INTERFACE_REF=111
|
ATOM_REF=111
|
||||||
OPTIONAL=112
|
INTERFACE_REF=112
|
||||||
LIST=113
|
OPTIONAL=113
|
||||||
RECORD=114
|
LIST=114
|
||||||
BOOL=115
|
RECORD=115
|
||||||
BYTES=116
|
BOOL=116
|
||||||
DOUBLE=117
|
BYTES=117
|
||||||
INT32=118
|
DOUBLE=118
|
||||||
INT64=119
|
INT32=119
|
||||||
STRING=120
|
INT64=120
|
||||||
UINT32=121
|
STRING=121
|
||||||
UINT64=122
|
UINT32=122
|
||||||
TRUE=123
|
UINT64=123
|
||||||
FALSE=124
|
TRUE=124
|
||||||
NULL=125
|
FALSE=125
|
||||||
ARROW=126
|
NULL=126
|
||||||
COLON=127
|
ARROW=127
|
||||||
SEMI=128
|
COLON=128
|
||||||
COMMA=129
|
SEMI=129
|
||||||
DOT=130
|
COMMA=130
|
||||||
LBRACE=131
|
DOT=131
|
||||||
RBRACE=132
|
LBRACE=132
|
||||||
LBRACK=133
|
RBRACE=133
|
||||||
RBRACK=134
|
LBRACK=134
|
||||||
LPAREN=135
|
RBRACK=135
|
||||||
RPAREN=136
|
LPAREN=136
|
||||||
LT=137
|
RPAREN=137
|
||||||
GT=138
|
LT=138
|
||||||
AMP=139
|
GT=139
|
||||||
EQUAL=140
|
AMP=140
|
||||||
INTEGER=141
|
EQUAL=141
|
||||||
JSON_NUMBER=142
|
INTEGER=142
|
||||||
IDENTIFIER=143
|
JSON_NUMBER=143
|
||||||
STRING_LITERAL=144
|
IDENTIFIER=144
|
||||||
LINE_COMMENT=145
|
STRING_LITERAL=145
|
||||||
BLOCK_COMMENT=146
|
LINE_COMMENT=146
|
||||||
WS=147
|
BLOCK_COMMENT=147
|
||||||
|
WS=148
|
||||||
'edit'=1
|
'edit'=1
|
||||||
'creation'=2
|
'creation'=2
|
||||||
'initialize'=3
|
'initialize'=3
|
||||||
@@ -219,69 +220,70 @@ WS=147
|
|||||||
'revision'=72
|
'revision'=72
|
||||||
'semantic-major'=73
|
'semantic-major'=73
|
||||||
'on-delete'=74
|
'on-delete'=74
|
||||||
'retain-other'=75
|
'lifecycle'=75
|
||||||
'keyed'=76
|
'retain-other'=76
|
||||||
'public-traversal'=77
|
'keyed'=77
|
||||||
'id'=78
|
'public-traversal'=78
|
||||||
'doc'=79
|
'id'=79
|
||||||
'mode'=80
|
'doc'=80
|
||||||
'emits'=81
|
'mode'=81
|
||||||
'receiver'=82
|
'emits'=82
|
||||||
'requires'=83
|
'receiver'=83
|
||||||
'any'=84
|
'requires'=84
|
||||||
'get'=85
|
'any'=85
|
||||||
'set'=86
|
'get'=86
|
||||||
'watch'=87
|
'set'=87
|
||||||
'start'=88
|
'watch'=88
|
||||||
'stop'=89
|
'start'=89
|
||||||
'read'=90
|
'stop'=90
|
||||||
'write'=91
|
'read'=91
|
||||||
'resolve'=92
|
'write'=92
|
||||||
'connect'=93
|
'resolve'=93
|
||||||
'disconnect'=94
|
'connect'=94
|
||||||
'call'=95
|
'disconnect'=95
|
||||||
'watch-start'=96
|
'call'=96
|
||||||
'watch-stop'=97
|
'watch-start'=97
|
||||||
'subscribe'=98
|
'watch-stop'=98
|
||||||
'unsubscribe'=99
|
'subscribe'=99
|
||||||
'register'=100
|
'unsubscribe'=100
|
||||||
'crdt'=101
|
'register'=101
|
||||||
'optional-one'=102
|
'crdt'=102
|
||||||
'exactly-one'=103
|
'optional-one'=103
|
||||||
'many-unique'=104
|
'exactly-one'=104
|
||||||
'many'=105
|
'many-unique'=105
|
||||||
'ordered'=106
|
'many'=106
|
||||||
'unit'=107
|
'ordered'=107
|
||||||
'watch-handle'=108
|
'unit'=108
|
||||||
'message'=109
|
'watch-handle'=109
|
||||||
'atom-ref'=110
|
'message'=110
|
||||||
'interface-ref'=111
|
'atom-ref'=111
|
||||||
'optional'=112
|
'interface-ref'=112
|
||||||
'list'=113
|
'optional'=113
|
||||||
'record'=114
|
'list'=114
|
||||||
'bool'=115
|
'record'=115
|
||||||
'bytes'=116
|
'bool'=116
|
||||||
'double'=117
|
'bytes'=117
|
||||||
'int32'=118
|
'double'=118
|
||||||
'int64'=119
|
'int32'=119
|
||||||
'string'=120
|
'int64'=120
|
||||||
'uint32'=121
|
'string'=121
|
||||||
'uint64'=122
|
'uint32'=122
|
||||||
'true'=123
|
'uint64'=123
|
||||||
'false'=124
|
'true'=124
|
||||||
'null'=125
|
'false'=125
|
||||||
'->'=126
|
'null'=126
|
||||||
':'=127
|
'->'=127
|
||||||
';'=128
|
':'=128
|
||||||
','=129
|
';'=129
|
||||||
'.'=130
|
','=130
|
||||||
'{'=131
|
'.'=131
|
||||||
'}'=132
|
'{'=132
|
||||||
'['=133
|
'}'=133
|
||||||
']'=134
|
'['=134
|
||||||
'('=135
|
']'=135
|
||||||
')'=136
|
'('=136
|
||||||
'<'=137
|
')'=137
|
||||||
'>'=138
|
'<'=138
|
||||||
'&'=139
|
'>'=139
|
||||||
'='=140
|
'&'=140
|
||||||
|
'='=141
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1965,6 +1965,13 @@ const lowerConformance = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const provider = bindingContext.operationProvider();
|
const provider = bindingContext.operationProvider();
|
||||||
|
if (provider.LIFECYCLE()) {
|
||||||
|
if (identifier(provider.identifier(0)) !== "delete") {
|
||||||
|
loweringIssue(state, provider, "invalid-lifecycle-binding", "Only lifecycle.delete is supported");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ operationId, binding: { kind: "lifecycle", primitive: "delete" } }];
|
||||||
|
}
|
||||||
if (provider.INTERFACE()) {
|
if (provider.INTERFACE()) {
|
||||||
const targetName = identifier(provider.identifier(0));
|
const targetName = identifier(provider.identifier(0));
|
||||||
let target = requireSymbol(state, state.interfaces, targetName, provider, "interface");
|
let target = requireSymbol(state, state.interfaces, targetName, provider, "interface");
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
|
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
|
||||||
|
if (value.kind === "lifecycle") return;
|
||||||
if (value.kind !== "package") {
|
if (value.kind !== "package") {
|
||||||
dependency(parent, value, atomId);
|
dependency(parent, value, atomId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
|||||||
if (!revision) continue;
|
if (!revision) continue;
|
||||||
for (const entry of conformance.operationBindings) {
|
for (const entry of conformance.operationBindings) {
|
||||||
const operation = operations(entry.operationId, revision);
|
const operation = operations(entry.operationId, revision);
|
||||||
if (!operation?.execution || operation.execution.kind === "remote") continue;
|
if (!operation) continue;
|
||||||
const path = `${conformance.atomId}/${revision.revisionId}/${entry.operationId}`;
|
const path = `${conformance.atomId}/${revision.revisionId}/${entry.operationId}`;
|
||||||
let binding;
|
let binding;
|
||||||
try {
|
try {
|
||||||
@@ -142,6 +142,19 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
|||||||
const target = interfaces.get(entry.binding.interfaceRevisionId);
|
const target = interfaces.get(entry.binding.interfaceRevisionId);
|
||||||
if (target) supports(operation.execution, operations(entry.binding.operationId, target)?.execution, path);
|
if (target) supports(operation.execution, operations(entry.binding.operationId, target)?.execution, path);
|
||||||
}
|
}
|
||||||
|
if (binding.kind === "lifecycle") {
|
||||||
|
if (
|
||||||
|
operation.execution?.kind !== "transaction" ||
|
||||||
|
operation.execution.targets.length !== 1 ||
|
||||||
|
operation.execution.targets[0] !== "server"
|
||||||
|
)
|
||||||
|
fail(
|
||||||
|
path,
|
||||||
|
"Deletion requires an explicitly server-only transaction; partial replicas cannot enumerate incoming references",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!operation.execution || operation.execution.kind === "remote") continue;
|
||||||
if (binding.kind === "package") {
|
if (binding.kind === "package") {
|
||||||
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
|
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
|
||||||
supports(operation.execution, provider?.execution, path);
|
supports(operation.execution, provider?.execution, path);
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ export interface RelationshipMaterialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Binding =
|
export type Binding =
|
||||||
|
| { kind: "lifecycle"; primitive: "delete" }
|
||||||
| {
|
| {
|
||||||
kind: "interface";
|
kind: "interface";
|
||||||
interfaceRevisionId: InterfaceRevisionId;
|
interfaceRevisionId: InterfaceRevisionId;
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export type CapabilityValidationIssueCode =
|
|||||||
| "invalid-attachment"
|
| "invalid-attachment"
|
||||||
| "invalid-state-binding"
|
| "invalid-state-binding"
|
||||||
| "invalid-edge-binding"
|
| "invalid-edge-binding"
|
||||||
|
| "invalid-lifecycle-binding"
|
||||||
| "invalid-package-binding"
|
| "invalid-package-binding"
|
||||||
| "invalid-forwarding-binding"
|
| "invalid-forwarding-binding"
|
||||||
| "invalid-dependency-binding"
|
| "invalid-dependency-binding"
|
||||||
@@ -1696,6 +1697,24 @@ const validateConformances = (
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (binding.kind === "lifecycle") {
|
||||||
|
if (
|
||||||
|
binding.primitive !== "delete" ||
|
||||||
|
operationEntry.member.kind !== "operation" ||
|
||||||
|
operation.mode !== "call" ||
|
||||||
|
operation.inputType.kind !== "builtin" ||
|
||||||
|
operation.inputType.name !== "unit" ||
|
||||||
|
operation.outputType.kind !== "builtin" ||
|
||||||
|
operation.outputType.name !== "unit"
|
||||||
|
)
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-lifecycle-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Deletion implements an object call from unit to unit",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (binding.kind === "state") {
|
if (binding.kind === "state") {
|
||||||
const attachment = findAttachment(indexes, "state", binding.slotId);
|
const attachment = findAttachment(indexes, "state", binding.slotId);
|
||||||
if (!attachment || attachment.attachment.kind !== "state") {
|
if (!attachment || attachment.attachment.kind !== "state") {
|
||||||
@@ -2324,6 +2343,7 @@ export const resolveOperationPlan = (
|
|||||||
attachment: owned as OwnedAttachment & { attachment: EdgeDefinition },
|
attachment: owned as OwnedAttachment & { attachment: EdgeDefinition },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (binding.kind === "lifecycle") return undefined; // Old runtime has no lifecycle provider.
|
||||||
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
||||||
const packageExport = packageRevision?.exports.find(
|
const packageExport = packageRevision?.exports.find(
|
||||||
(entry): entry is PackageOperationExport | PackageFunctionExport =>
|
(entry): entry is PackageOperationExport | PackageFunctionExport =>
|
||||||
@@ -2469,7 +2489,7 @@ export const computeCapabilityClosure = (
|
|||||||
attachments.add(binding.edgeTypeId);
|
attachments.add(binding.edgeTypeId);
|
||||||
} else if (binding.kind === "interface") {
|
} else if (binding.kind === "interface") {
|
||||||
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
|
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
|
||||||
} else {
|
} else if (binding.kind === "package") {
|
||||||
packages.add(binding.packageRevisionId);
|
packages.add(binding.packageRevisionId);
|
||||||
includeDependencies(root.atomId, binding.dependencies);
|
includeDependencies(root.atomId, binding.dependencies);
|
||||||
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
||||||
|
|||||||
@@ -44,16 +44,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const endpoints = storage.endpoints.map((endpoint) => {
|
const endpoints = storage.endpoints.map((endpoint) => {
|
||||||
if (
|
if (endpoint.ordered || endpoint.keyType || endpoint.retainOther)
|
||||||
endpoint.ordered ||
|
|
||||||
endpoint.keyType ||
|
|
||||||
endpoint.retainOther ||
|
|
||||||
(endpoint.onDelete && endpoint.onDelete !== "restrict")
|
|
||||||
)
|
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"UNSUPPORTED_GRAPH_FEATURE",
|
"UNSUPPORTED_GRAPH_FEATURE",
|
||||||
storage.id,
|
storage.id,
|
||||||
"Ordered/keyed/deletion policies require LF-18",
|
"Ordered/keyed relations and automatic retention are not implemented",
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
projection: endpoint.projectionId,
|
projection: endpoint.projectionId,
|
||||||
@@ -65,6 +60,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
: endpoint.constraint.interfaceRevisionId,
|
: endpoint.constraint.interfaceRevisionId,
|
||||||
},
|
},
|
||||||
cardinality: endpoint.cardinality,
|
cardinality: endpoint.cardinality,
|
||||||
|
...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
edges.push({ id: storage.id, owner: owner.id, a: endpoints[0], b: endpoints[1] });
|
edges.push({ id: storage.id, owner: owner.id, a: endpoints[0], b: endpoints[1] });
|
||||||
@@ -97,7 +93,8 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
const binding = operation.binding;
|
const binding = operation.binding;
|
||||||
let target: unknown;
|
let target: unknown;
|
||||||
let unsupported: string | null = null;
|
let unsupported: string | null = null;
|
||||||
if (binding.kind === "state")
|
if (binding.kind === "lifecycle") target = { kind: "lifecycle", primitive: binding.primitive };
|
||||||
|
else if (binding.kind === "state")
|
||||||
target = { kind: "state", owner: slotOwner(binding.slotId), slot: binding.slotId, primitive: binding.primitive };
|
target = { kind: "state", owner: slotOwner(binding.slotId), slot: binding.slotId, primitive: binding.primitive };
|
||||||
else if (binding.kind === "edge")
|
else if (binding.kind === "edge")
|
||||||
target = {
|
target = {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
|||||||
if (binding.kind === "state") planned.dependencies.push({ kind: "state", slot: binding.slotId });
|
if (binding.kind === "state") planned.dependencies.push({ kind: "state", slot: binding.slotId });
|
||||||
else if (binding.kind === "edge")
|
else if (binding.kind === "edge")
|
||||||
planned.dependencies.push({ kind: "edge", edge: binding.edgeTypeId, projection: binding.projectionId });
|
planned.dependencies.push({ kind: "edge", edge: binding.edgeTypeId, projection: binding.projectionId });
|
||||||
else {
|
else if (binding.kind === "package") {
|
||||||
const entry = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
|
const entry = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
|
||||||
if (!entry) throw new ExecutionPlanError("MISSING_EXPORT", key, "Unresolved package export");
|
if (!entry) throw new ExecutionPlanError("MISSING_EXPORT", key, "Unresolved package export");
|
||||||
for (const dependency of binding.dependencies) {
|
for (const dependency of binding.dependencies) {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||||
|
import { formatQx } from "../src/capability-language/source.js";
|
||||||
|
import { compileExecutionPlan, compileKernelProgram } from "../src/execution-world/index.js";
|
||||||
|
const contract = `interface Item id "item" revision "item@1" {
|
||||||
|
operation remove id "remove" : unit -> unit {
|
||||||
|
call id "remove:call" execution transaction targets [server];
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const workspace = `workspace Lifecycle id "workspace:lifecycle" revision "workspace:lifecycle@1" commit "${"a".repeat(40)}" {
|
||||||
|
import interface Item;
|
||||||
|
atom Node id "node";
|
||||||
|
conform Node as Item id "node:item" {
|
||||||
|
private edge Links id "links" {
|
||||||
|
atom Node projection children id "children" many on-delete "cascade-other";
|
||||||
|
atom Node projection parent id "parent" optional-one on-delete "detach";
|
||||||
|
}
|
||||||
|
bind remove.call to lifecycle.delete;
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
function compile(ifaceSource = contract, workspaceSource = workspace) {
|
||||||
|
const iface = compileCapabilityResourceSource(ifaceSource, {
|
||||||
|
source: { repository: "https://example.test/lifecycle.git", commit: "a".repeat(40) },
|
||||||
|
});
|
||||||
|
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||||
|
return compileCapabilitySource(workspaceSource, "workspace.qx", {
|
||||||
|
interfaces: new Map([["Item", iface.resource.revision]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test("lifecycle binding and endpoint policies survive formatting and kernel compilation", () => {
|
||||||
|
const result = compile();
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const formatted = compile(formatQx(contract), formatQx(workspace));
|
||||||
|
assert.ok(formatted.ok, JSON.stringify(formatted.diagnostics));
|
||||||
|
const plan = compileExecutionPlan(result.workspace);
|
||||||
|
assert.equal(compileExecutionPlan(formatted.workspace).digest, plan.digest);
|
||||||
|
const kernel = compileKernelProgram(plan) as {
|
||||||
|
operations: { binding: unknown }[];
|
||||||
|
model: { edges: { a: { onDelete: string }; b: { onDelete: string } }[] };
|
||||||
|
};
|
||||||
|
assert.deepEqual(kernel.operations[0]!.binding, { kind: "lifecycle", primitive: "delete" });
|
||||||
|
assert.equal(kernel.model.edges[0]!.a.onDelete, "cascade-other");
|
||||||
|
assert.equal(kernel.model.edges[0]!.b.onDelete, "detach");
|
||||||
|
});
|
||||||
|
test("deletion rejects browser, snapshot, remote, class, payload and unknown primitive promises", () => {
|
||||||
|
for (const [iface, ws] of [
|
||||||
|
[contract.replace("targets [server]", "targets [browser, server]"), workspace],
|
||||||
|
[contract.replace("transaction targets", "snapshot targets"), workspace],
|
||||||
|
[contract.replace("execution transaction targets [server]", "execution remote"), workspace],
|
||||||
|
[contract.replace("execution transaction targets [server]", ""), workspace],
|
||||||
|
[contract.replace("operation remove", "static operation remove"), workspace],
|
||||||
|
[contract.replace("unit -> unit", "string -> unit"), workspace],
|
||||||
|
[contract.replace("unit -> unit", "unit -> string"), workspace],
|
||||||
|
[contract, workspace.replace("lifecycle.delete", "lifecycle.resurrect")],
|
||||||
|
]) {
|
||||||
|
const result = compile(iface, ws);
|
||||||
|
assert.equal(result.ok, false, iface + ws);
|
||||||
|
assert.ok(
|
||||||
|
result.diagnostics.some((d) =>
|
||||||
|
["invalid-execution-contract", "invalid-lifecycle-binding", "invalid-package-binding"].includes(d.code),
|
||||||
|
),
|
||||||
|
JSON.stringify(result.diagnostics),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user