Expose authoritative lifecycle deletion through capability bindings

This commit is contained in:
Timothy J. Aveni
2026-09-20 22:48:09 -07:00
parent d80464d285
commit 115976cacd
15 changed files with 1837 additions and 1698 deletions
+3 -1
View File
@@ -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
+7
View File
@@ -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");
+1
View File
@@ -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;
+14 -1
View File
@@ -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);
+1
View File
@@ -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;
+21 -1
View File
@@ -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);
+5 -8
View File
@@ -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 = {
+1 -1
View File
@@ -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) {
+66
View File
@@ -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),
);
}
});