Expose authoritative lifecycle deletion through capability bindings
This commit is contained in:
@@ -333,7 +333,8 @@ operationName
|
||||
;
|
||||
|
||||
operationProvider
|
||||
: STATE identifier DOT statePrimitive
|
||||
: LIFECYCLE DOT identifier
|
||||
| STATE identifier DOT statePrimitive
|
||||
| EDGE identifier DOT identifier DOT edgePrimitive
|
||||
| INTERFACE identifier typeArguments? DOT memberOperationRef
|
||||
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
|
||||
@@ -550,6 +551,7 @@ COMMIT: 'commit';
|
||||
REVISION: 'revision';
|
||||
SEMANTIC_MAJOR: 'semantic-major';
|
||||
ON_DELETE: 'on-delete';
|
||||
LIFECYCLE: 'lifecycle';
|
||||
RETAIN_OTHER: 'retain-other';
|
||||
KEYED: 'keyed';
|
||||
PUBLIC_TRAVERSAL: 'public-traversal';
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -72,79 +72,80 @@ COMMIT=71
|
||||
REVISION=72
|
||||
SEMANTIC_MAJOR=73
|
||||
ON_DELETE=74
|
||||
RETAIN_OTHER=75
|
||||
KEYED=76
|
||||
PUBLIC_TRAVERSAL=77
|
||||
ID=78
|
||||
DOC=79
|
||||
MODE=80
|
||||
EMITS=81
|
||||
RECEIVER=82
|
||||
REQUIRES=83
|
||||
ANY=84
|
||||
GET=85
|
||||
SET=86
|
||||
WATCH=87
|
||||
START=88
|
||||
STOP=89
|
||||
READ=90
|
||||
WRITE=91
|
||||
RESOLVE=92
|
||||
CONNECT=93
|
||||
DISCONNECT=94
|
||||
CALL=95
|
||||
WATCH_START=96
|
||||
WATCH_STOP=97
|
||||
SUBSCRIBE=98
|
||||
UNSUBSCRIBE=99
|
||||
REGISTER=100
|
||||
CRDT=101
|
||||
OPTIONAL_ONE=102
|
||||
EXACTLY_ONE=103
|
||||
MANY_UNIQUE=104
|
||||
MANY=105
|
||||
ORDERED=106
|
||||
UNIT=107
|
||||
WATCH_HANDLE=108
|
||||
MESSAGE=109
|
||||
ATOM_REF=110
|
||||
INTERFACE_REF=111
|
||||
OPTIONAL=112
|
||||
LIST=113
|
||||
RECORD=114
|
||||
BOOL=115
|
||||
BYTES=116
|
||||
DOUBLE=117
|
||||
INT32=118
|
||||
INT64=119
|
||||
STRING=120
|
||||
UINT32=121
|
||||
UINT64=122
|
||||
TRUE=123
|
||||
FALSE=124
|
||||
NULL=125
|
||||
ARROW=126
|
||||
COLON=127
|
||||
SEMI=128
|
||||
COMMA=129
|
||||
DOT=130
|
||||
LBRACE=131
|
||||
RBRACE=132
|
||||
LBRACK=133
|
||||
RBRACK=134
|
||||
LPAREN=135
|
||||
RPAREN=136
|
||||
LT=137
|
||||
GT=138
|
||||
AMP=139
|
||||
EQUAL=140
|
||||
INTEGER=141
|
||||
JSON_NUMBER=142
|
||||
IDENTIFIER=143
|
||||
STRING_LITERAL=144
|
||||
LINE_COMMENT=145
|
||||
BLOCK_COMMENT=146
|
||||
WS=147
|
||||
LIFECYCLE=75
|
||||
RETAIN_OTHER=76
|
||||
KEYED=77
|
||||
PUBLIC_TRAVERSAL=78
|
||||
ID=79
|
||||
DOC=80
|
||||
MODE=81
|
||||
EMITS=82
|
||||
RECEIVER=83
|
||||
REQUIRES=84
|
||||
ANY=85
|
||||
GET=86
|
||||
SET=87
|
||||
WATCH=88
|
||||
START=89
|
||||
STOP=90
|
||||
READ=91
|
||||
WRITE=92
|
||||
RESOLVE=93
|
||||
CONNECT=94
|
||||
DISCONNECT=95
|
||||
CALL=96
|
||||
WATCH_START=97
|
||||
WATCH_STOP=98
|
||||
SUBSCRIBE=99
|
||||
UNSUBSCRIBE=100
|
||||
REGISTER=101
|
||||
CRDT=102
|
||||
OPTIONAL_ONE=103
|
||||
EXACTLY_ONE=104
|
||||
MANY_UNIQUE=105
|
||||
MANY=106
|
||||
ORDERED=107
|
||||
UNIT=108
|
||||
WATCH_HANDLE=109
|
||||
MESSAGE=110
|
||||
ATOM_REF=111
|
||||
INTERFACE_REF=112
|
||||
OPTIONAL=113
|
||||
LIST=114
|
||||
RECORD=115
|
||||
BOOL=116
|
||||
BYTES=117
|
||||
DOUBLE=118
|
||||
INT32=119
|
||||
INT64=120
|
||||
STRING=121
|
||||
UINT32=122
|
||||
UINT64=123
|
||||
TRUE=124
|
||||
FALSE=125
|
||||
NULL=126
|
||||
ARROW=127
|
||||
COLON=128
|
||||
SEMI=129
|
||||
COMMA=130
|
||||
DOT=131
|
||||
LBRACE=132
|
||||
RBRACE=133
|
||||
LBRACK=134
|
||||
RBRACK=135
|
||||
LPAREN=136
|
||||
RPAREN=137
|
||||
LT=138
|
||||
GT=139
|
||||
AMP=140
|
||||
EQUAL=141
|
||||
INTEGER=142
|
||||
JSON_NUMBER=143
|
||||
IDENTIFIER=144
|
||||
STRING_LITERAL=145
|
||||
LINE_COMMENT=146
|
||||
BLOCK_COMMENT=147
|
||||
WS=148
|
||||
'edit'=1
|
||||
'creation'=2
|
||||
'initialize'=3
|
||||
@@ -219,69 +220,70 @@ WS=147
|
||||
'revision'=72
|
||||
'semantic-major'=73
|
||||
'on-delete'=74
|
||||
'retain-other'=75
|
||||
'keyed'=76
|
||||
'public-traversal'=77
|
||||
'id'=78
|
||||
'doc'=79
|
||||
'mode'=80
|
||||
'emits'=81
|
||||
'receiver'=82
|
||||
'requires'=83
|
||||
'any'=84
|
||||
'get'=85
|
||||
'set'=86
|
||||
'watch'=87
|
||||
'start'=88
|
||||
'stop'=89
|
||||
'read'=90
|
||||
'write'=91
|
||||
'resolve'=92
|
||||
'connect'=93
|
||||
'disconnect'=94
|
||||
'call'=95
|
||||
'watch-start'=96
|
||||
'watch-stop'=97
|
||||
'subscribe'=98
|
||||
'unsubscribe'=99
|
||||
'register'=100
|
||||
'crdt'=101
|
||||
'optional-one'=102
|
||||
'exactly-one'=103
|
||||
'many-unique'=104
|
||||
'many'=105
|
||||
'ordered'=106
|
||||
'unit'=107
|
||||
'watch-handle'=108
|
||||
'message'=109
|
||||
'atom-ref'=110
|
||||
'interface-ref'=111
|
||||
'optional'=112
|
||||
'list'=113
|
||||
'record'=114
|
||||
'bool'=115
|
||||
'bytes'=116
|
||||
'double'=117
|
||||
'int32'=118
|
||||
'int64'=119
|
||||
'string'=120
|
||||
'uint32'=121
|
||||
'uint64'=122
|
||||
'true'=123
|
||||
'false'=124
|
||||
'null'=125
|
||||
'->'=126
|
||||
':'=127
|
||||
';'=128
|
||||
','=129
|
||||
'.'=130
|
||||
'{'=131
|
||||
'}'=132
|
||||
'['=133
|
||||
']'=134
|
||||
'('=135
|
||||
')'=136
|
||||
'<'=137
|
||||
'>'=138
|
||||
'&'=139
|
||||
'='=140
|
||||
'lifecycle'=75
|
||||
'retain-other'=76
|
||||
'keyed'=77
|
||||
'public-traversal'=78
|
||||
'id'=79
|
||||
'doc'=80
|
||||
'mode'=81
|
||||
'emits'=82
|
||||
'receiver'=83
|
||||
'requires'=84
|
||||
'any'=85
|
||||
'get'=86
|
||||
'set'=87
|
||||
'watch'=88
|
||||
'start'=89
|
||||
'stop'=90
|
||||
'read'=91
|
||||
'write'=92
|
||||
'resolve'=93
|
||||
'connect'=94
|
||||
'disconnect'=95
|
||||
'call'=96
|
||||
'watch-start'=97
|
||||
'watch-stop'=98
|
||||
'subscribe'=99
|
||||
'unsubscribe'=100
|
||||
'register'=101
|
||||
'crdt'=102
|
||||
'optional-one'=103
|
||||
'exactly-one'=104
|
||||
'many-unique'=105
|
||||
'many'=106
|
||||
'ordered'=107
|
||||
'unit'=108
|
||||
'watch-handle'=109
|
||||
'message'=110
|
||||
'atom-ref'=111
|
||||
'interface-ref'=112
|
||||
'optional'=113
|
||||
'list'=114
|
||||
'record'=115
|
||||
'bool'=116
|
||||
'bytes'=117
|
||||
'double'=118
|
||||
'int32'=119
|
||||
'int64'=120
|
||||
'string'=121
|
||||
'uint32'=122
|
||||
'uint64'=123
|
||||
'true'=124
|
||||
'false'=125
|
||||
'null'=126
|
||||
'->'=127
|
||||
':'=128
|
||||
';'=129
|
||||
','=130
|
||||
'.'=131
|
||||
'{'=132
|
||||
'}'=133
|
||||
'['=134
|
||||
']'=135
|
||||
'('=136
|
||||
')'=137
|
||||
'<'=138
|
||||
'>'=139
|
||||
'&'=140
|
||||
'='=141
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -72,79 +72,80 @@ COMMIT=71
|
||||
REVISION=72
|
||||
SEMANTIC_MAJOR=73
|
||||
ON_DELETE=74
|
||||
RETAIN_OTHER=75
|
||||
KEYED=76
|
||||
PUBLIC_TRAVERSAL=77
|
||||
ID=78
|
||||
DOC=79
|
||||
MODE=80
|
||||
EMITS=81
|
||||
RECEIVER=82
|
||||
REQUIRES=83
|
||||
ANY=84
|
||||
GET=85
|
||||
SET=86
|
||||
WATCH=87
|
||||
START=88
|
||||
STOP=89
|
||||
READ=90
|
||||
WRITE=91
|
||||
RESOLVE=92
|
||||
CONNECT=93
|
||||
DISCONNECT=94
|
||||
CALL=95
|
||||
WATCH_START=96
|
||||
WATCH_STOP=97
|
||||
SUBSCRIBE=98
|
||||
UNSUBSCRIBE=99
|
||||
REGISTER=100
|
||||
CRDT=101
|
||||
OPTIONAL_ONE=102
|
||||
EXACTLY_ONE=103
|
||||
MANY_UNIQUE=104
|
||||
MANY=105
|
||||
ORDERED=106
|
||||
UNIT=107
|
||||
WATCH_HANDLE=108
|
||||
MESSAGE=109
|
||||
ATOM_REF=110
|
||||
INTERFACE_REF=111
|
||||
OPTIONAL=112
|
||||
LIST=113
|
||||
RECORD=114
|
||||
BOOL=115
|
||||
BYTES=116
|
||||
DOUBLE=117
|
||||
INT32=118
|
||||
INT64=119
|
||||
STRING=120
|
||||
UINT32=121
|
||||
UINT64=122
|
||||
TRUE=123
|
||||
FALSE=124
|
||||
NULL=125
|
||||
ARROW=126
|
||||
COLON=127
|
||||
SEMI=128
|
||||
COMMA=129
|
||||
DOT=130
|
||||
LBRACE=131
|
||||
RBRACE=132
|
||||
LBRACK=133
|
||||
RBRACK=134
|
||||
LPAREN=135
|
||||
RPAREN=136
|
||||
LT=137
|
||||
GT=138
|
||||
AMP=139
|
||||
EQUAL=140
|
||||
INTEGER=141
|
||||
JSON_NUMBER=142
|
||||
IDENTIFIER=143
|
||||
STRING_LITERAL=144
|
||||
LINE_COMMENT=145
|
||||
BLOCK_COMMENT=146
|
||||
WS=147
|
||||
LIFECYCLE=75
|
||||
RETAIN_OTHER=76
|
||||
KEYED=77
|
||||
PUBLIC_TRAVERSAL=78
|
||||
ID=79
|
||||
DOC=80
|
||||
MODE=81
|
||||
EMITS=82
|
||||
RECEIVER=83
|
||||
REQUIRES=84
|
||||
ANY=85
|
||||
GET=86
|
||||
SET=87
|
||||
WATCH=88
|
||||
START=89
|
||||
STOP=90
|
||||
READ=91
|
||||
WRITE=92
|
||||
RESOLVE=93
|
||||
CONNECT=94
|
||||
DISCONNECT=95
|
||||
CALL=96
|
||||
WATCH_START=97
|
||||
WATCH_STOP=98
|
||||
SUBSCRIBE=99
|
||||
UNSUBSCRIBE=100
|
||||
REGISTER=101
|
||||
CRDT=102
|
||||
OPTIONAL_ONE=103
|
||||
EXACTLY_ONE=104
|
||||
MANY_UNIQUE=105
|
||||
MANY=106
|
||||
ORDERED=107
|
||||
UNIT=108
|
||||
WATCH_HANDLE=109
|
||||
MESSAGE=110
|
||||
ATOM_REF=111
|
||||
INTERFACE_REF=112
|
||||
OPTIONAL=113
|
||||
LIST=114
|
||||
RECORD=115
|
||||
BOOL=116
|
||||
BYTES=117
|
||||
DOUBLE=118
|
||||
INT32=119
|
||||
INT64=120
|
||||
STRING=121
|
||||
UINT32=122
|
||||
UINT64=123
|
||||
TRUE=124
|
||||
FALSE=125
|
||||
NULL=126
|
||||
ARROW=127
|
||||
COLON=128
|
||||
SEMI=129
|
||||
COMMA=130
|
||||
DOT=131
|
||||
LBRACE=132
|
||||
RBRACE=133
|
||||
LBRACK=134
|
||||
RBRACK=135
|
||||
LPAREN=136
|
||||
RPAREN=137
|
||||
LT=138
|
||||
GT=139
|
||||
AMP=140
|
||||
EQUAL=141
|
||||
INTEGER=142
|
||||
JSON_NUMBER=143
|
||||
IDENTIFIER=144
|
||||
STRING_LITERAL=145
|
||||
LINE_COMMENT=146
|
||||
BLOCK_COMMENT=147
|
||||
WS=148
|
||||
'edit'=1
|
||||
'creation'=2
|
||||
'initialize'=3
|
||||
@@ -219,69 +220,70 @@ WS=147
|
||||
'revision'=72
|
||||
'semantic-major'=73
|
||||
'on-delete'=74
|
||||
'retain-other'=75
|
||||
'keyed'=76
|
||||
'public-traversal'=77
|
||||
'id'=78
|
||||
'doc'=79
|
||||
'mode'=80
|
||||
'emits'=81
|
||||
'receiver'=82
|
||||
'requires'=83
|
||||
'any'=84
|
||||
'get'=85
|
||||
'set'=86
|
||||
'watch'=87
|
||||
'start'=88
|
||||
'stop'=89
|
||||
'read'=90
|
||||
'write'=91
|
||||
'resolve'=92
|
||||
'connect'=93
|
||||
'disconnect'=94
|
||||
'call'=95
|
||||
'watch-start'=96
|
||||
'watch-stop'=97
|
||||
'subscribe'=98
|
||||
'unsubscribe'=99
|
||||
'register'=100
|
||||
'crdt'=101
|
||||
'optional-one'=102
|
||||
'exactly-one'=103
|
||||
'many-unique'=104
|
||||
'many'=105
|
||||
'ordered'=106
|
||||
'unit'=107
|
||||
'watch-handle'=108
|
||||
'message'=109
|
||||
'atom-ref'=110
|
||||
'interface-ref'=111
|
||||
'optional'=112
|
||||
'list'=113
|
||||
'record'=114
|
||||
'bool'=115
|
||||
'bytes'=116
|
||||
'double'=117
|
||||
'int32'=118
|
||||
'int64'=119
|
||||
'string'=120
|
||||
'uint32'=121
|
||||
'uint64'=122
|
||||
'true'=123
|
||||
'false'=124
|
||||
'null'=125
|
||||
'->'=126
|
||||
':'=127
|
||||
';'=128
|
||||
','=129
|
||||
'.'=130
|
||||
'{'=131
|
||||
'}'=132
|
||||
'['=133
|
||||
']'=134
|
||||
'('=135
|
||||
')'=136
|
||||
'<'=137
|
||||
'>'=138
|
||||
'&'=139
|
||||
'='=140
|
||||
'lifecycle'=75
|
||||
'retain-other'=76
|
||||
'keyed'=77
|
||||
'public-traversal'=78
|
||||
'id'=79
|
||||
'doc'=80
|
||||
'mode'=81
|
||||
'emits'=82
|
||||
'receiver'=83
|
||||
'requires'=84
|
||||
'any'=85
|
||||
'get'=86
|
||||
'set'=87
|
||||
'watch'=88
|
||||
'start'=89
|
||||
'stop'=90
|
||||
'read'=91
|
||||
'write'=92
|
||||
'resolve'=93
|
||||
'connect'=94
|
||||
'disconnect'=95
|
||||
'call'=96
|
||||
'watch-start'=97
|
||||
'watch-stop'=98
|
||||
'subscribe'=99
|
||||
'unsubscribe'=100
|
||||
'register'=101
|
||||
'crdt'=102
|
||||
'optional-one'=103
|
||||
'exactly-one'=104
|
||||
'many-unique'=105
|
||||
'many'=106
|
||||
'ordered'=107
|
||||
'unit'=108
|
||||
'watch-handle'=109
|
||||
'message'=110
|
||||
'atom-ref'=111
|
||||
'interface-ref'=112
|
||||
'optional'=113
|
||||
'list'=114
|
||||
'record'=115
|
||||
'bool'=116
|
||||
'bytes'=117
|
||||
'double'=118
|
||||
'int32'=119
|
||||
'int64'=120
|
||||
'string'=121
|
||||
'uint32'=122
|
||||
'uint64'=123
|
||||
'true'=124
|
||||
'false'=125
|
||||
'null'=126
|
||||
'->'=127
|
||||
':'=128
|
||||
';'=129
|
||||
','=130
|
||||
'.'=131
|
||||
'{'=132
|
||||
'}'=133
|
||||
'['=134
|
||||
']'=135
|
||||
'('=136
|
||||
')'=137
|
||||
'<'=138
|
||||
'>'=139
|
||||
'&'=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 [];
|
||||
}
|
||||
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()) {
|
||||
const targetName = identifier(provider.identifier(0));
|
||||
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) => {
|
||||
if (value.kind === "lifecycle") return;
|
||||
if (value.kind !== "package") {
|
||||
dependency(parent, value, atomId);
|
||||
return;
|
||||
|
||||
@@ -125,7 +125,7 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
||||
if (!revision) continue;
|
||||
for (const entry of conformance.operationBindings) {
|
||||
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}`;
|
||||
let binding;
|
||||
try {
|
||||
@@ -142,6 +142,19 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
||||
const target = interfaces.get(entry.binding.interfaceRevisionId);
|
||||
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") {
|
||||
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
|
||||
supports(operation.execution, provider?.execution, path);
|
||||
|
||||
@@ -358,6 +358,7 @@ export interface RelationshipMaterialization {
|
||||
}
|
||||
|
||||
export type Binding =
|
||||
| { kind: "lifecycle"; primitive: "delete" }
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
|
||||
@@ -70,6 +70,7 @@ export type CapabilityValidationIssueCode =
|
||||
| "invalid-attachment"
|
||||
| "invalid-state-binding"
|
||||
| "invalid-edge-binding"
|
||||
| "invalid-lifecycle-binding"
|
||||
| "invalid-package-binding"
|
||||
| "invalid-forwarding-binding"
|
||||
| "invalid-dependency-binding"
|
||||
@@ -1696,6 +1697,24 @@ const validateConformances = (
|
||||
);
|
||||
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") {
|
||||
const attachment = findAttachment(indexes, "state", binding.slotId);
|
||||
if (!attachment || attachment.attachment.kind !== "state") {
|
||||
@@ -2324,6 +2343,7 @@ export const resolveOperationPlan = (
|
||||
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 packageExport = packageRevision?.exports.find(
|
||||
(entry): entry is PackageOperationExport | PackageFunctionExport =>
|
||||
@@ -2469,7 +2489,7 @@ export const computeCapabilityClosure = (
|
||||
attachments.add(binding.edgeTypeId);
|
||||
} else if (binding.kind === "interface") {
|
||||
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
|
||||
} else {
|
||||
} else if (binding.kind === "package") {
|
||||
packages.add(binding.packageRevisionId);
|
||||
includeDependencies(root.atomId, binding.dependencies);
|
||||
const packageRevision = plan.packages.get(binding.packageRevisionId);
|
||||
|
||||
@@ -44,16 +44,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
});
|
||||
} else {
|
||||
const endpoints = storage.endpoints.map((endpoint) => {
|
||||
if (
|
||||
endpoint.ordered ||
|
||||
endpoint.keyType ||
|
||||
endpoint.retainOther ||
|
||||
(endpoint.onDelete && endpoint.onDelete !== "restrict")
|
||||
)
|
||||
if (endpoint.ordered || endpoint.keyType || endpoint.retainOther)
|
||||
throw new ExecutionPlanError(
|
||||
"UNSUPPORTED_GRAPH_FEATURE",
|
||||
storage.id,
|
||||
"Ordered/keyed/deletion policies require LF-18",
|
||||
"Ordered/keyed relations and automatic retention are not implemented",
|
||||
);
|
||||
return {
|
||||
projection: endpoint.projectionId,
|
||||
@@ -65,6 +60,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
: endpoint.constraint.interfaceRevisionId,
|
||||
},
|
||||
cardinality: endpoint.cardinality,
|
||||
...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}),
|
||||
};
|
||||
});
|
||||
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;
|
||||
let target: unknown;
|
||||
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 };
|
||||
else if (binding.kind === "edge")
|
||||
target = {
|
||||
|
||||
@@ -214,7 +214,7 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
||||
if (binding.kind === "state") planned.dependencies.push({ kind: "state", slot: binding.slotId });
|
||||
else if (binding.kind === "edge")
|
||||
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);
|
||||
if (!entry) throw new ExecutionPlanError("MISSING_EXPORT", key, "Unresolved package export");
|
||||
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