Compile and dispatch authoritative acceptance hooks with target-specific package roots

This commit is contained in:
Timothy J. Aveni
2026-09-20 22:05:59 -07:00
parent 1e69b0e662
commit 266d2d98d0
16 changed files with 3112 additions and 2701 deletions
+12 -3
View File
@@ -169,12 +169,16 @@ queryClause
packageOperationExport
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? acceptanceClause? SEMI
;
packageFunctionExport
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
executionClause? dependencyBlock? SEMI
executionClause? dependencyBlock? acceptanceClause? SEMI
;
acceptanceClause
: ACCEPT USING identifier
;
executionClause
@@ -332,7 +336,11 @@ operationProvider
: STATE identifier DOT statePrimitive
| EDGE identifier DOT identifier DOT edgePrimitive
| INTERFACE identifier typeArguments? DOT memberOperationRef
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock?
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
;
acceptanceBinding
: ACCEPT dependencyBindingBlock
;
statePrimitive
@@ -528,6 +536,7 @@ EDGE: 'edge';
PROJECTION: 'projection';
WITH: 'with';
USING: 'using';
ACCEPT: 'accept';
VIA: 'via';
MATERIALIZE: 'materialize';
IF: 'if';
File diff suppressed because one or more lines are too long
@@ -58,92 +58,93 @@ EDGE=57
PROJECTION=58
WITH=59
USING=60
VIA=61
MATERIALIZE=62
IF=63
ABSENT=64
ON=65
POLICY=66
DEFAULT=67
SOURCE=68
REPOSITORY=69
COMMIT=70
REVISION=71
SEMANTIC_MAJOR=72
ON_DELETE=73
RETAIN_OTHER=74
KEYED=75
PUBLIC_TRAVERSAL=76
ID=77
DOC=78
MODE=79
EMITS=80
RECEIVER=81
REQUIRES=82
ANY=83
GET=84
SET=85
WATCH=86
START=87
STOP=88
READ=89
WRITE=90
RESOLVE=91
CONNECT=92
DISCONNECT=93
CALL=94
WATCH_START=95
WATCH_STOP=96
SUBSCRIBE=97
UNSUBSCRIBE=98
REGISTER=99
CRDT=100
OPTIONAL_ONE=101
EXACTLY_ONE=102
MANY_UNIQUE=103
MANY=104
ORDERED=105
UNIT=106
WATCH_HANDLE=107
MESSAGE=108
ATOM_REF=109
INTERFACE_REF=110
OPTIONAL=111
LIST=112
RECORD=113
BOOL=114
BYTES=115
DOUBLE=116
INT32=117
INT64=118
STRING=119
UINT32=120
UINT64=121
TRUE=122
FALSE=123
NULL=124
ARROW=125
COLON=126
SEMI=127
COMMA=128
DOT=129
LBRACE=130
RBRACE=131
LBRACK=132
RBRACK=133
LPAREN=134
RPAREN=135
LT=136
GT=137
AMP=138
EQUAL=139
INTEGER=140
JSON_NUMBER=141
IDENTIFIER=142
STRING_LITERAL=143
LINE_COMMENT=144
BLOCK_COMMENT=145
WS=146
ACCEPT=61
VIA=62
MATERIALIZE=63
IF=64
ABSENT=65
ON=66
POLICY=67
DEFAULT=68
SOURCE=69
REPOSITORY=70
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
'edit'=1
'creation'=2
'initialize'=3
@@ -204,82 +205,83 @@ WS=146
'projection'=58
'with'=59
'using'=60
'via'=61
'materialize'=62
'if'=63
'absent'=64
'on'=65
'policy'=66
'default'=67
'source'=68
'repository'=69
'commit'=70
'revision'=71
'semantic-major'=72
'on-delete'=73
'retain-other'=74
'keyed'=75
'public-traversal'=76
'id'=77
'doc'=78
'mode'=79
'emits'=80
'receiver'=81
'requires'=82
'any'=83
'get'=84
'set'=85
'watch'=86
'start'=87
'stop'=88
'read'=89
'write'=90
'resolve'=91
'connect'=92
'disconnect'=93
'call'=94
'watch-start'=95
'watch-stop'=96
'subscribe'=97
'unsubscribe'=98
'register'=99
'crdt'=100
'optional-one'=101
'exactly-one'=102
'many-unique'=103
'many'=104
'ordered'=105
'unit'=106
'watch-handle'=107
'message'=108
'atom-ref'=109
'interface-ref'=110
'optional'=111
'list'=112
'record'=113
'bool'=114
'bytes'=115
'double'=116
'int32'=117
'int64'=118
'string'=119
'uint32'=120
'uint64'=121
'true'=122
'false'=123
'null'=124
'->'=125
':'=126
';'=127
','=128
'.'=129
'{'=130
'}'=131
'['=132
']'=133
'('=134
')'=135
'<'=136
'>'=137
'&'=138
'='=139
'accept'=61
'via'=62
'materialize'=63
'if'=64
'absent'=65
'on'=66
'policy'=67
'default'=68
'source'=69
'repository'=70
'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
'->'=126
':'=127
';'=128
','=129
'.'=130
'{'=131
'}'=132
'['=133
']'=134
'('=135
')'=136
'<'=137
'>'=138
'&'=139
'='=140
File diff suppressed because one or more lines are too long
@@ -58,92 +58,93 @@ EDGE=57
PROJECTION=58
WITH=59
USING=60
VIA=61
MATERIALIZE=62
IF=63
ABSENT=64
ON=65
POLICY=66
DEFAULT=67
SOURCE=68
REPOSITORY=69
COMMIT=70
REVISION=71
SEMANTIC_MAJOR=72
ON_DELETE=73
RETAIN_OTHER=74
KEYED=75
PUBLIC_TRAVERSAL=76
ID=77
DOC=78
MODE=79
EMITS=80
RECEIVER=81
REQUIRES=82
ANY=83
GET=84
SET=85
WATCH=86
START=87
STOP=88
READ=89
WRITE=90
RESOLVE=91
CONNECT=92
DISCONNECT=93
CALL=94
WATCH_START=95
WATCH_STOP=96
SUBSCRIBE=97
UNSUBSCRIBE=98
REGISTER=99
CRDT=100
OPTIONAL_ONE=101
EXACTLY_ONE=102
MANY_UNIQUE=103
MANY=104
ORDERED=105
UNIT=106
WATCH_HANDLE=107
MESSAGE=108
ATOM_REF=109
INTERFACE_REF=110
OPTIONAL=111
LIST=112
RECORD=113
BOOL=114
BYTES=115
DOUBLE=116
INT32=117
INT64=118
STRING=119
UINT32=120
UINT64=121
TRUE=122
FALSE=123
NULL=124
ARROW=125
COLON=126
SEMI=127
COMMA=128
DOT=129
LBRACE=130
RBRACE=131
LBRACK=132
RBRACK=133
LPAREN=134
RPAREN=135
LT=136
GT=137
AMP=138
EQUAL=139
INTEGER=140
JSON_NUMBER=141
IDENTIFIER=142
STRING_LITERAL=143
LINE_COMMENT=144
BLOCK_COMMENT=145
WS=146
ACCEPT=61
VIA=62
MATERIALIZE=63
IF=64
ABSENT=65
ON=66
POLICY=67
DEFAULT=68
SOURCE=69
REPOSITORY=70
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
'edit'=1
'creation'=2
'initialize'=3
@@ -204,82 +205,83 @@ WS=146
'projection'=58
'with'=59
'using'=60
'via'=61
'materialize'=62
'if'=63
'absent'=64
'on'=65
'policy'=66
'default'=67
'source'=68
'repository'=69
'commit'=70
'revision'=71
'semantic-major'=72
'on-delete'=73
'retain-other'=74
'keyed'=75
'public-traversal'=76
'id'=77
'doc'=78
'mode'=79
'emits'=80
'receiver'=81
'requires'=82
'any'=83
'get'=84
'set'=85
'watch'=86
'start'=87
'stop'=88
'read'=89
'write'=90
'resolve'=91
'connect'=92
'disconnect'=93
'call'=94
'watch-start'=95
'watch-stop'=96
'subscribe'=97
'unsubscribe'=98
'register'=99
'crdt'=100
'optional-one'=101
'exactly-one'=102
'many-unique'=103
'many'=104
'ordered'=105
'unit'=106
'watch-handle'=107
'message'=108
'atom-ref'=109
'interface-ref'=110
'optional'=111
'list'=112
'record'=113
'bool'=114
'bytes'=115
'double'=116
'int32'=117
'int64'=118
'string'=119
'uint32'=120
'uint64'=121
'true'=122
'false'=123
'null'=124
'->'=125
':'=126
';'=127
','=128
'.'=129
'{'=130
'}'=131
'['=132
']'=133
'('=134
')'=135
'<'=136
'>'=137
'&'=138
'='=139
'accept'=61
'via'=62
'materialize'=63
'if'=64
'absent'=65
'on'=66
'policy'=67
'default'=68
'source'=69
'repository'=70
'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
'->'=126
':'=127
';'=128
','=129
'.'=130
'{'=131
'}'=132
'['=133
']'=134
'('=135
')'=136
'<'=137
'>'=138
'&'=139
'='=140
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -34,6 +34,7 @@ import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { ExecutionClauseContext } from "./QuixosCapabilityParser.js";
import { SelectedOperationsContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
@@ -62,6 +63,7 @@ import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
import { OperationNameContext } from "./QuixosCapabilityParser.js";
import { OperationProviderContext } from "./QuixosCapabilityParser.js";
import { AcceptanceBindingContext } from "./QuixosCapabilityParser.js";
import { StatePrimitiveContext } from "./QuixosCapabilityParser.js";
import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
@@ -279,6 +281,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.executionClause`.
* @param ctx the parse tree
@@ -447,6 +455,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitOperationProvider?: (ctx: OperationProviderContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceBinding`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceBinding?: (ctx: AcceptanceBindingContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`.
* @param ctx the parse tree
+40
View File
@@ -1514,6 +1514,13 @@ const lowerPackage = (
const operation = exportContext.packageOperationExport();
const generic = operation ?? exportContext.packageFunctionExport();
if (generic?.typeParameters()) {
if (generic.acceptanceClause())
loweringIssue(
state,
generic,
"unsupported-generic-acceptance",
"Acceptance hooks currently require a closed export",
);
genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic)));
return [];
}
@@ -1535,6 +1542,22 @@ const lowerPackage = (
state.types.self = previousSelf;
}
});
// Resolve after all exports have been declared, so checks can follow bodies.
for (const source of context.packageExport()) {
const declaration = source.packageOperationExport() ?? source.packageFunctionExport();
const clause = declaration?.acceptanceClause();
if (!declaration || !clause || declaration.typeParameters()) continue;
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
const check = exports.find((entry) => entry.displayName === identifier(clause.identifier()));
if (!check)
loweringIssue(
state,
clause,
"unknown-acceptance-export",
"Acceptance hook must name a closed export in this package",
);
else if (entry) entry.acceptanceExportId = check.id;
}
for (const entry of context.packageExport()) {
const specialized = entry.packageQuerySpecialization();
if (!specialized) continue;
@@ -2047,6 +2070,22 @@ const lowerConformance = (
} else if (provider.typeArguments()) {
throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments");
}
const packageExport = packageSymbol?.definition?.exports.find((entry) => entry.id === exportSymbol?.exportId);
const check = packageSymbol?.definition?.exports.find((entry) => entry.id === packageExport?.acceptanceExportId);
const acceptanceBlock = provider.acceptanceBinding()?.dependencyBindingBlock();
if (acceptanceBlock && !check)
loweringIssue(
state,
acceptanceBlock,
"unexpected-acceptance-binding",
"Export does not declare an acceptance hook",
);
const acceptanceDependencies = check
? lowerBoundDependencies(state, acceptanceBlock ?? undefined, {
exportId: check.id,
ports: new Map(check.dependencyPorts.map((port) => [port.displayName, port.id])),
})
: undefined;
return packageSymbol && exportSymbol
? [
{
@@ -2056,6 +2095,7 @@ const lowerConformance = (
kind: "package" as const,
packageRevisionId: packageSymbol.revisionId,
exportId: exportSymbol.exportId,
...(acceptanceDependencies ? { acceptanceDependencies } : {}),
dependencies: lowerBoundDependencies(
state,
provider.dependencyBindingBlock() ?? undefined,
+3
View File
@@ -257,6 +257,8 @@ export interface DependencyPort {
}
interface PackageExportBase {
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract;
id: PackageExportId;
displayName: string;
@@ -375,6 +377,7 @@ export type Binding =
packageRevisionId: PackageRevisionId;
exportId: PackageExportId;
dependencies: BoundDependency[];
acceptanceDependencies?: BoundDependency[];
};
export interface OperationBinding {
+40
View File
@@ -961,6 +961,32 @@ const validatePackages = (
const exportPath = `${path}.exports[${exportIndex}]`;
validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes);
validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes);
if (entry.acceptanceExportId !== undefined) {
const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId);
if (
entry.execution?.kind !== "transaction" ||
!entry.execution.targets.includes("server") ||
!check ||
check.execution?.kind !== "snapshot" ||
check.execution.targets.length !== 1 ||
check.execution.targets[0] !== "server" ||
check.acceptanceExportId !== undefined ||
entry.kind !== check.kind ||
entry.kind === "constructor" ||
!valueTypesEqual(entry.inputType, check.inputType) ||
!valueTypesEqual(check.outputType, valueType.unit) ||
(entry.kind === "operation" &&
(check.kind !== "operation" ||
check.mode !== "call" ||
!isDeepStrictEqual(entry.receiverRequirement, check.receiverRequirement)))
)
issue(
issues,
"invalid-operation",
`${exportPath}.acceptanceExportId`,
"Acceptance requires a server snapshot export with matching kind, receiver and input, unit output, and no further acceptance hook",
);
}
if (entry.kind === "operation") {
validateOperationEventShape(issues, entry, exportPath);
if (entry.eventType) {
@@ -1862,6 +1888,20 @@ const validateConformances = (
requirementGraph,
graphSourceKey: key,
});
const check = packageEntry.exports.get(packageExport.acceptanceExportId!);
if (check) {
validateBoundDependencies(issues, {
dependencies: binding.acceptanceDependencies ?? [],
dependencyPorts: check.dependencyPorts,
atomId: conformance.atomId,
conformance,
path: `${bindingPath}.binding.acceptance`,
indexes,
requirementGraph,
graphSourceKey: key,
});
} else if (binding.acceptanceDependencies !== undefined)
issue(issues, "invalid-package-binding", bindingPath, "Acceptance ports require a declared acceptance export");
}
for (const member of interfaceEntry.revision.members) {
+67 -51
View File
@@ -3,6 +3,8 @@ import type {
InitializationField,
PersistentAttachment,
DependencyBinding,
BoundDependency,
PackageExport,
} from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js";
@@ -107,57 +109,70 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
else {
const pkg = workspace.packageImports.find((p) => p.revisionId === binding.packageRevisionId)!;
const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
const ports = binding.dependencies.map((dependency) => {
const requirement = entry.dependencyPorts.find((p) => p.id === dependency.portId)!.requirement;
const bound = dependency.binding;
const common = { id: dependency.portId, ...via(bound) };
if (bound.kind === "state" && requirement.kind === "state")
return {
...common,
kind: "state",
owner: slotOwner(bound.slotId),
slot: bound.slotId,
primitives: requirement.primitives,
};
if (bound.kind === "edge" && requirement.kind === "edge")
return {
...common,
kind: "edge",
edge: bound.edgeTypeId,
projection: bound.projectionId,
primitives: requirement.primitives,
};
if (bound.kind === "interface" && requirement.kind === "interface") {
const iface = workspace.interfaceImports.find((i) => i.revisionId === bound.interfaceRevisionId)!;
return {
...common,
kind: "interface",
interface: bound.interfaceRevisionId,
operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)),
};
}
if (bound.kind === "creation" && requirement.kind === "creation")
return {
...common,
kind: "creation",
atom: bound.atomId,
schema: portableValueSchema(requirement.inputType),
recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }),
owners: bound.owners.map((seed) => {
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
if (!owner)
throw new ExecutionPlanError(
"MISSING_STORAGE_OWNER",
dependency.portId,
"Creation has no initialization owner",
);
return { owner: owner.id, inputPath: seed.inputPath };
}),
};
unsupported = `UNSUPPORTED_EXECUTION_PORT:${bound.kind}`;
return { ...common, kind: "unavailable" };
});
target = { kind: "package", package: binding.packageRevisionId, export: binding.exportId, ports };
const compilePorts = (entry: PackageExport, dependencies: BoundDependency[]) =>
dependencies.map((dependency) => {
const requirement = entry.dependencyPorts.find((p) => p.id === dependency.portId)!.requirement;
const bound = dependency.binding;
const common = { id: dependency.portId, ...via(bound) };
if (bound.kind === "state" && requirement.kind === "state")
return {
...common,
kind: "state",
owner: slotOwner(bound.slotId),
slot: bound.slotId,
primitives: requirement.primitives,
};
if (bound.kind === "edge" && requirement.kind === "edge")
return {
...common,
kind: "edge",
edge: bound.edgeTypeId,
projection: bound.projectionId,
primitives: requirement.primitives,
};
if (bound.kind === "interface" && requirement.kind === "interface") {
const iface = workspace.interfaceImports.find((i) => i.revisionId === bound.interfaceRevisionId)!;
return {
...common,
kind: "interface",
interface: bound.interfaceRevisionId,
operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)),
};
}
if (bound.kind === "creation" && requirement.kind === "creation")
return {
...common,
kind: "creation",
atom: bound.atomId,
schema: portableValueSchema(requirement.inputType),
recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }),
owners: bound.owners.map((seed) => {
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
if (!owner)
throw new ExecutionPlanError(
"MISSING_STORAGE_OWNER",
dependency.portId,
"Creation has no initialization owner",
);
return { owner: owner.id, inputPath: seed.inputPath };
}),
};
unsupported = `UNSUPPORTED_EXECUTION_PORT:${bound.kind}`;
return { ...common, kind: "unavailable" };
});
const ports = compilePorts(entry, binding.dependencies);
const check = entry.acceptanceExportId
? pkg.exports.find((candidate) => candidate.id === entry.acceptanceExportId)!
: undefined;
target = {
kind: "package",
package: binding.packageRevisionId,
export: binding.exportId,
ports,
...(check
? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } }
: {}),
};
}
const input = schema(operation.input),
output = schema(operation.output);
@@ -179,6 +194,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
revision: pkg.revisionId,
exports: pkg.exports.map((entry) => ({
id: entry.id,
...(entry.acceptanceExportId ? { acceptance: entry.acceptanceExportId } : {}),
effect: entry.execution?.kind ?? "remote",
targets: entry.execution && entry.execution.kind !== "remote" ? entry.execution.targets : [],
receiver:
+41
View File
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
const contract = `interface Limit id "limit" revision "limit@1" {
operation change id "change" : int32 -> unit {
call id "change:call" execution transaction targets [browser, server];
}
}`;
export const runtime = `external atom Item id "item";
package Runtime id "runtime" revision "runtime@1" {
operation change id "runtime:change" : int32 -> unit mode call execution transaction targets [browser, server]
receiver atom Item requires { state level id "write:value" : int32 [write]; } accept using check;
operation check id "runtime:check" : int32 -> unit mode call execution snapshot targets [server]
receiver atom Item requires {
state level id "read:value" : int32 [read];
state ceiling id "read:ceiling" : int32 [read];
};
}`;
export const workspace = `workspace LimitTest id "workspace:limit" revision "workspace:limit@1" commit "${"a".repeat(40)}" {
import interface Limit;
import package Runtime;
atom Item id "item";
conform Item as Limit id "item:limit" {
private state Value id "value" on Item : int32 policy register default 0;
private state Ceiling id "ceiling" on Item : int32 policy register default 100;
bind change.call to package Runtime.change with { level to state Value; }
accept with { level to state Value; ceiling to state Ceiling; };
}
}`;
export function compile(packageSource = runtime, workspaceSource = workspace) {
const source = { repository: "https://example.test/limit.git", commit: "a".repeat(40) };
const iface = compileCapabilityResourceSource(contract, { source });
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
const pkg = compileCapabilityResourceSource(packageSource, { source });
if (!pkg.ok) return pkg;
assert.equal(pkg.resource.kind, "package");
if (pkg.resource.kind !== "package") throw Error("Expected package");
return compileCapabilitySource(workspaceSource, "workspace.qx", {
interfaces: new Map([["Limit", iface.resource.revision]]),
packages: new Map([["Runtime", pkg.resource.revision]]),
});
}
+70
View File
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/parser.js";
import { formatQx } from "../src/capability-language/source.js";
import { compileExecutionPlan, compileKernelProgram, checkExecutionWorld } from "../src/execution-world/index.js";
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
import { buildArtifactFixture } from "./artifact-fixture.js";
import { compile, runtime, workspace } from "./acceptance-fixture.js";
test("acceptance syntax retains separate final-state ports through formatting, planning, adapters and artifacts", async () => {
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const formatted = compile(formatQx(runtime), 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: { ports: { id: string }[]; acceptance: { export: string; ports: { id: string }[] } } }[];
packages: { exports: { id: string; acceptance?: string }[] }[];
};
assert.deepEqual(
kernel.operations[0]!.binding.ports.map((p) => p.id),
["write:value"],
);
assert.equal(kernel.operations[0]!.binding.acceptance.export, "runtime:check");
assert.deepEqual(kernel.operations[0]!.binding.acceptance.ports.map((p) => p.id).sort(), [
"read:ceiling",
"read:value",
]);
assert.equal(kernel.packages[0]!.exports.find((e) => e.id === "runtime:change")!.acceptance, "runtime:check");
assert.ok(
!generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { target: "browser" }).includes(
'"runtime:check"',
),
);
assert.ok(
generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { target: "server" }).includes(
'"runtime:check"',
),
);
const artifacts = buildArtifactFixture(plan, "runtime@1");
await checkExecutionWorld(plan, [artifacts.manifest], artifacts.read);
});
test("acceptance declarations reject wrong effects/signatures and missing or mutable ports", () => {
for (const [pkg, ws] of [
[runtime.replace("snapshot targets [server]", "transaction targets [server]"), workspace],
[runtime.replace("snapshot targets [server]", "snapshot targets [browser, server]"), workspace],
[
runtime.replace('check id "runtime:check" : int32 -> unit', 'check id "runtime:check" : string -> unit'),
workspace,
],
[
runtime.replace('check id "runtime:check" : int32 -> unit', 'check id "runtime:check" : int32 -> int32'),
workspace,
],
[runtime.replace('id "read:ceiling" : int32 [read]', 'id "read:ceiling" : int32 [write]'), workspace],
[runtime, workspace.replace("ceiling to state Ceiling;", "")],
[runtime.replace(" accept using check", ""), workspace],
])
assert.equal(compile(pkg, ws).ok, false, `${pkg}\n${ws}`);
});
test("generic acceptance syntax rejects explicitly until specialization is supported", () => {
const result = compileCapabilityResourceSource(
runtime.replace("operation change id", "operation change<value T> id"),
{
source: { repository: "https://example.test/limit.git", commit: "a".repeat(40) },
},
);
assert.equal(result.ok, false);
assert.ok(result.diagnostics.some((d) => d.code === "unsupported-generic-acceptance"));
});
+3 -1
View File
@@ -21,7 +21,9 @@ export const buildArtifactFixture = (plan: ExecutionPlan, revision = plan.defini
path,
ports: "quixos-transaction-ports-v1" as const,
...(compatibility === "quixos-js-module-v1" ? { registryExport: "handlers" } : {}),
exports: expected.exports.map((e) => e.id),
exports: expected.exports
.filter((e) => target === "server" || (e.execution.kind !== "remote" && e.execution.targets.includes(target)))
.map((e) => e.id),
files: [
{
path,
+7
View File
@@ -89,6 +89,13 @@ void negative;
);
const { asyncObjectPlan } = await import("./async-fixture.js");
const { compile: compileAcceptance } = await import("./acceptance-fixture.js");
const acceptance = compileAcceptance();
if (!acceptance.ok) throw Error(JSON.stringify(acceptance.diagnostics));
const acceptancePlan = compileExecutionPlan(acceptance.workspace);
await mkdir("dist/portable-reader/acceptance", { recursive: true });
await writeFile("dist/portable-reader/acceptance/plan.json", JSON.stringify(acceptancePlan));
await writeFile("dist/portable-reader/acceptance/kernel.json", JSON.stringify(compileKernelProgram(acceptancePlan)));
const { recurringTaskPlan } = await import("./recurring-fixture.js");
const recurringPlan = await recurringTaskPlan();
await mkdir("dist/portable-reader/recurring", { recursive: true });