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
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
+7
View File
@@ -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");
+1
View File
@@ -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;
+14 -1
View File
@@ -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);
+1
View File
@@ -358,6 +358,7 @@ export interface RelationshipMaterialization {
}
export type Binding =
| { kind: "lifecycle"; primitive: "delete" }
| {
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
+21 -1
View File
@@ -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);
+5 -8
View File
@@ -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 = {
+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 });
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) {