Make local graph execution optional across composed packages

This commit is contained in:
Timothy J. Aveni
2026-09-28 21:05:42 -07:00
parent 1e72713ae2
commit e9c15fe40f
87 changed files with 3703 additions and 3400 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
import { readFile, writeFile } from "node:fs/promises";
import { generatePortableClientBindings } from "./portable-client.js";
async function main() {
const [plan, output, target = "browser", ...rest] = process.argv.slice(2);
const [plan, output, target = "target:org.quixos.execution.javascript@1", ...rest] = process.argv.slice(2);
if (!plan || !output || rest.length)
throw Error("usage: quixos-codegen-client EXECUTION_PLAN.json OUTPUT.ts [TARGET]");
await writeFile(output, generatePortableClientBindings(JSON.parse(await readFile(plan, "utf8")), { target }));
+1 -1
View File
@@ -17,7 +17,7 @@ export function generatePortableClientBindings(
unavailable: { key: string; reason: string }[] = [];
const seen = new Set<string>();
for (const operation of plan.definition.operations) {
if (operation.execution.kind === "remote" || !operation.execution.targets.includes(options.target)) {
if (operation.execution.kind === "remote" || !operation.availableTargets.includes(options.target)) {
unavailable.push({ key: operation.key, reason: "OPERATION_NOT_PORTABLE" });
continue;
}
+11 -6
View File
@@ -5,6 +5,7 @@ import type { StructuralRequest } from "./structural-plan.js";
import { parseQx, walkSyntax } from "./source.js";
import { addImplementation } from "./implementation-edit.js";
import { formatQuixosLock } from "../resource-lock/index.js";
import { javascriptTarget } from "../execution-world/native-backends.js";
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
const q = (value: string) => JSON.stringify(value);
const nixString = (value: string) => q(value).replaceAll("${", "\\${");
@@ -31,7 +32,10 @@ export async function executionScaffold(
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
if (command === "package") {
if (!spec.tools || !spec.revision) throw new Error("Execution package requires exact tool sources and a revision");
create("package.qx", `package ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`);
create(
"package.qx",
`symbol JavaScript id ${q(javascriptTarget)};\npackage ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`,
);
create(
"quixos.lock",
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
@@ -90,7 +94,7 @@ in camino.assembleCaminoPackage {
pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage {
inherit pkgs context dependencies; src = ./.;
entry = "src/server.ts"; entries.browser = "src/browser.ts";
targets = pkgs.lib.unique (pkgs.lib.concatMap (entry: entry.execution.targets) portable);
targets = pkgs.lib.unique (map (target: if target == "target:org.quixos.execution.javascript@1" then "browser" else if target == "server" then "server" else throw "Unsupported TypeScript backend target") (pkgs.lib.concatMap (entry: entry.execution.targets) portable));
}) ++ pkgs.lib.optional (remote != []) (camino.mkCaminoTypeScriptService {
inherit pkgs context dependencies; src = ./.; entry = "src/remote.ts";
}) ++ pkgs.lib.optional (modules != {}) (camino.mkCaminoReactPackage {
@@ -134,15 +138,16 @@ in camino.assembleCaminoPackage {
.filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end)
.map((token) => token.text);
const remote = tokens[1] === "remote";
const targets = remote
const declaredTargets = remote
? ["remote"]
: [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text);
const targets = declaredTargets.length ? declaredTargets : ["server"];
if (
!targets.length ||
new Set(targets).size !== targets.length ||
targets.some((target) => !(remote ? ["remote"] : ["browser", "server"]).includes(target))
targets.some((target) => !(remote ? ["remote"] : ["JavaScript", "server"]).includes(target))
)
throw new Error("Execution scaffold supports browser/server portable targets and remote service handlers");
throw new Error("This TypeScript scaffold supports the JavaScript target, server, and remote service handlers");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const syntax = parseQx(authored);
const packageNode = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
@@ -174,7 +179,7 @@ in camino.assembleCaminoPackage {
`import type { ${remote ? "Remote" : "Portable"}Implementation } from "../gen/${remote ? "remote" : "qx"}.js";\nexport const handler: ${remote ? "Remote" : "Portable"}Implementation[${q(spec.name)}] = async (_context) => { throw new Error(${q("Implement " + spec.name)}); };\n`,
);
for (const target of targets) {
const file = prefix + `src/${target}.ts`;
const file = prefix + `src/${target === "JavaScript" ? "browser" : target}.ts`;
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
File diff suppressed because one or more lines are too long
@@ -42,120 +42,121 @@ REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
ATOM=45
INTERFACE=46
INTERFACES=47
PACKAGE=48
VALUE=49
FIELD=50
RELATION=51
OPERATION=52
FUNCTION=53
CONSTRUCTOR=54
CONSTRUCTS=55
INPUT=56
CONFORM=57
AS=58
BIND=59
STATIC=60
TO=61
PRIVATE=62
SHARED=63
STATE=64
EDGE=65
PROJECTION=66
WITH=67
USING=68
ACCEPT=69
VIA=70
MATERIALIZE=71
IF=72
ABSENT=73
ON=74
POLICY=75
DEFAULT=76
SOURCE=77
REPOSITORY=78
COMMIT=79
REVISION=80
SEMANTIC_MAJOR=81
ON_DELETE=82
LIFECYCLE=83
JOURNAL=84
SUBSCRIPTION=85
RETAIN_OTHER=86
KEYED=87
PUBLIC_TRAVERSAL=88
ID=89
DOC=90
MODE=91
EMITS=92
RECEIVER=93
REQUIRES=94
ANY=95
GET=96
SET=97
WATCH=98
START=99
STOP=100
READ=101
WRITE=102
RESOLVE=103
CONNECT=104
DISCONNECT=105
CALL=106
WATCH_START=107
WATCH_STOP=108
SUBSCRIBE=109
UNSUBSCRIBE=110
REGISTER=111
CRDT=112
OPTIONAL_ONE=113
EXACTLY_ONE=114
MANY_UNIQUE=115
MANY=116
ORDERED=117
UNIT=118
WATCH_HANDLE=119
MESSAGE=120
ATOM_REF=121
INTERFACE_REF=122
OPTIONAL=123
LIST=124
RECORD=125
BOOL=126
BYTES=127
DOUBLE=128
INT32=129
INT64=130
STRING=131
UINT32=132
UINT64=133
TRUE=134
FALSE=135
NULL=136
ARROW=137
COLON=138
SEMI=139
COMMA=140
DOT=141
LBRACE=142
RBRACE=143
LBRACK=144
RBRACK=145
LPAREN=146
RPAREN=147
LT=148
GT=149
AMP=150
EQUAL=151
INTEGER=152
JSON_NUMBER=153
IDENTIFIER=154
STRING_LITERAL=155
LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
SYMBOL=45
ATOM=46
INTERFACE=47
INTERFACES=48
PACKAGE=49
VALUE=50
FIELD=51
RELATION=52
OPERATION=53
FUNCTION=54
CONSTRUCTOR=55
CONSTRUCTS=56
INPUT=57
CONFORM=58
AS=59
BIND=60
STATIC=61
TO=62
PRIVATE=63
SHARED=64
STATE=65
EDGE=66
PROJECTION=67
WITH=68
USING=69
ACCEPT=70
VIA=71
MATERIALIZE=72
IF=73
ABSENT=74
ON=75
POLICY=76
DEFAULT=77
SOURCE=78
REPOSITORY=79
COMMIT=80
REVISION=81
SEMANTIC_MAJOR=82
ON_DELETE=83
LIFECYCLE=84
JOURNAL=85
SUBSCRIPTION=86
RETAIN_OTHER=87
KEYED=88
PUBLIC_TRAVERSAL=89
ID=90
DOC=91
MODE=92
EMITS=93
RECEIVER=94
REQUIRES=95
ANY=96
GET=97
SET=98
WATCH=99
START=100
STOP=101
READ=102
WRITE=103
RESOLVE=104
CONNECT=105
DISCONNECT=106
CALL=107
WATCH_START=108
WATCH_STOP=109
SUBSCRIBE=110
UNSUBSCRIBE=111
REGISTER=112
CRDT=113
OPTIONAL_ONE=114
EXACTLY_ONE=115
MANY_UNIQUE=116
MANY=117
ORDERED=118
UNIT=119
WATCH_HANDLE=120
MESSAGE=121
ATOM_REF=122
INTERFACE_REF=123
OPTIONAL=124
LIST=125
RECORD=126
BOOL=127
BYTES=128
DOUBLE=129
INT32=130
INT64=131
STRING=132
UINT32=133
UINT64=134
TRUE=135
FALSE=136
NULL=137
ARROW=138
COLON=139
SEMI=140
COMMA=141
DOT=142
LBRACE=143
RBRACE=144
LBRACK=145
RBRACK=146
LPAREN=147
RPAREN=148
LT=149
GT=150
AMP=151
EQUAL=152
INTEGER=153
JSON_NUMBER=154
IDENTIFIER=155
STRING_LITERAL=156
LINE_COMMENT=157
BLOCK_COMMENT=158
WS=159
'edit'=1
'module'=2
'contract'=3
@@ -200,110 +201,111 @@ WS=158
'fragment'=42
'import'=43
'external'=44
'atom'=45
'interface'=46
'interfaces'=47
'package'=48
'value'=49
'field'=50
'relation'=51
'operation'=52
'function'=53
'constructor'=54
'constructs'=55
'input'=56
'conform'=57
'as'=58
'bind'=59
'static'=60
'to'=61
'private'=62
'shared'=63
'state'=64
'edge'=65
'projection'=66
'with'=67
'using'=68
'accept'=69
'via'=70
'materialize'=71
'if'=72
'absent'=73
'on'=74
'policy'=75
'default'=76
'source'=77
'repository'=78
'commit'=79
'revision'=80
'semantic-major'=81
'on-delete'=82
'lifecycle'=83
'journal'=84
'subscription'=85
'retain-other'=86
'keyed'=87
'public-traversal'=88
'id'=89
'doc'=90
'mode'=91
'emits'=92
'receiver'=93
'requires'=94
'any'=95
'get'=96
'set'=97
'watch'=98
'start'=99
'stop'=100
'read'=101
'write'=102
'resolve'=103
'connect'=104
'disconnect'=105
'call'=106
'watch-start'=107
'watch-stop'=108
'subscribe'=109
'unsubscribe'=110
'register'=111
'crdt'=112
'optional-one'=113
'exactly-one'=114
'many-unique'=115
'many'=116
'ordered'=117
'unit'=118
'watch-handle'=119
'message'=120
'atom-ref'=121
'interface-ref'=122
'optional'=123
'list'=124
'record'=125
'bool'=126
'bytes'=127
'double'=128
'int32'=129
'int64'=130
'string'=131
'uint32'=132
'uint64'=133
'true'=134
'false'=135
'null'=136
'->'=137
':'=138
';'=139
','=140
'.'=141
'{'=142
'}'=143
'['=144
']'=145
'('=146
')'=147
'<'=148
'>'=149
'&'=150
'='=151
'symbol'=45
'atom'=46
'interface'=47
'interfaces'=48
'package'=49
'value'=50
'field'=51
'relation'=52
'operation'=53
'function'=54
'constructor'=55
'constructs'=56
'input'=57
'conform'=58
'as'=59
'bind'=60
'static'=61
'to'=62
'private'=63
'shared'=64
'state'=65
'edge'=66
'projection'=67
'with'=68
'using'=69
'accept'=70
'via'=71
'materialize'=72
'if'=73
'absent'=74
'on'=75
'policy'=76
'default'=77
'source'=78
'repository'=79
'commit'=80
'revision'=81
'semantic-major'=82
'on-delete'=83
'lifecycle'=84
'journal'=85
'subscription'=86
'retain-other'=87
'keyed'=88
'public-traversal'=89
'id'=90
'doc'=91
'mode'=92
'emits'=93
'receiver'=94
'requires'=95
'any'=96
'get'=97
'set'=98
'watch'=99
'start'=100
'stop'=101
'read'=102
'write'=103
'resolve'=104
'connect'=105
'disconnect'=106
'call'=107
'watch-start'=108
'watch-stop'=109
'subscribe'=110
'unsubscribe'=111
'register'=112
'crdt'=113
'optional-one'=114
'exactly-one'=115
'many-unique'=116
'many'=117
'ordered'=118
'unit'=119
'watch-handle'=120
'message'=121
'atom-ref'=122
'interface-ref'=123
'optional'=124
'list'=125
'record'=126
'bool'=127
'bytes'=128
'double'=129
'int32'=130
'int64'=131
'string'=132
'uint32'=133
'uint64'=134
'true'=135
'false'=136
'null'=137
'->'=138
':'=139
';'=140
','=141
'.'=142
'{'=143
'}'=144
'['=145
']'=146
'('=147
')'=148
'<'=149
'>'=150
'&'=151
'='=152
File diff suppressed because one or more lines are too long
@@ -42,120 +42,121 @@ REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
ATOM=45
INTERFACE=46
INTERFACES=47
PACKAGE=48
VALUE=49
FIELD=50
RELATION=51
OPERATION=52
FUNCTION=53
CONSTRUCTOR=54
CONSTRUCTS=55
INPUT=56
CONFORM=57
AS=58
BIND=59
STATIC=60
TO=61
PRIVATE=62
SHARED=63
STATE=64
EDGE=65
PROJECTION=66
WITH=67
USING=68
ACCEPT=69
VIA=70
MATERIALIZE=71
IF=72
ABSENT=73
ON=74
POLICY=75
DEFAULT=76
SOURCE=77
REPOSITORY=78
COMMIT=79
REVISION=80
SEMANTIC_MAJOR=81
ON_DELETE=82
LIFECYCLE=83
JOURNAL=84
SUBSCRIPTION=85
RETAIN_OTHER=86
KEYED=87
PUBLIC_TRAVERSAL=88
ID=89
DOC=90
MODE=91
EMITS=92
RECEIVER=93
REQUIRES=94
ANY=95
GET=96
SET=97
WATCH=98
START=99
STOP=100
READ=101
WRITE=102
RESOLVE=103
CONNECT=104
DISCONNECT=105
CALL=106
WATCH_START=107
WATCH_STOP=108
SUBSCRIBE=109
UNSUBSCRIBE=110
REGISTER=111
CRDT=112
OPTIONAL_ONE=113
EXACTLY_ONE=114
MANY_UNIQUE=115
MANY=116
ORDERED=117
UNIT=118
WATCH_HANDLE=119
MESSAGE=120
ATOM_REF=121
INTERFACE_REF=122
OPTIONAL=123
LIST=124
RECORD=125
BOOL=126
BYTES=127
DOUBLE=128
INT32=129
INT64=130
STRING=131
UINT32=132
UINT64=133
TRUE=134
FALSE=135
NULL=136
ARROW=137
COLON=138
SEMI=139
COMMA=140
DOT=141
LBRACE=142
RBRACE=143
LBRACK=144
RBRACK=145
LPAREN=146
RPAREN=147
LT=148
GT=149
AMP=150
EQUAL=151
INTEGER=152
JSON_NUMBER=153
IDENTIFIER=154
STRING_LITERAL=155
LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
SYMBOL=45
ATOM=46
INTERFACE=47
INTERFACES=48
PACKAGE=49
VALUE=50
FIELD=51
RELATION=52
OPERATION=53
FUNCTION=54
CONSTRUCTOR=55
CONSTRUCTS=56
INPUT=57
CONFORM=58
AS=59
BIND=60
STATIC=61
TO=62
PRIVATE=63
SHARED=64
STATE=65
EDGE=66
PROJECTION=67
WITH=68
USING=69
ACCEPT=70
VIA=71
MATERIALIZE=72
IF=73
ABSENT=74
ON=75
POLICY=76
DEFAULT=77
SOURCE=78
REPOSITORY=79
COMMIT=80
REVISION=81
SEMANTIC_MAJOR=82
ON_DELETE=83
LIFECYCLE=84
JOURNAL=85
SUBSCRIPTION=86
RETAIN_OTHER=87
KEYED=88
PUBLIC_TRAVERSAL=89
ID=90
DOC=91
MODE=92
EMITS=93
RECEIVER=94
REQUIRES=95
ANY=96
GET=97
SET=98
WATCH=99
START=100
STOP=101
READ=102
WRITE=103
RESOLVE=104
CONNECT=105
DISCONNECT=106
CALL=107
WATCH_START=108
WATCH_STOP=109
SUBSCRIBE=110
UNSUBSCRIBE=111
REGISTER=112
CRDT=113
OPTIONAL_ONE=114
EXACTLY_ONE=115
MANY_UNIQUE=116
MANY=117
ORDERED=118
UNIT=119
WATCH_HANDLE=120
MESSAGE=121
ATOM_REF=122
INTERFACE_REF=123
OPTIONAL=124
LIST=125
RECORD=126
BOOL=127
BYTES=128
DOUBLE=129
INT32=130
INT64=131
STRING=132
UINT32=133
UINT64=134
TRUE=135
FALSE=136
NULL=137
ARROW=138
COLON=139
SEMI=140
COMMA=141
DOT=142
LBRACE=143
RBRACE=144
LBRACK=145
RBRACK=146
LPAREN=147
RPAREN=148
LT=149
GT=150
AMP=151
EQUAL=152
INTEGER=153
JSON_NUMBER=154
IDENTIFIER=155
STRING_LITERAL=156
LINE_COMMENT=157
BLOCK_COMMENT=158
WS=159
'edit'=1
'module'=2
'contract'=3
@@ -200,110 +201,111 @@ WS=158
'fragment'=42
'import'=43
'external'=44
'atom'=45
'interface'=46
'interfaces'=47
'package'=48
'value'=49
'field'=50
'relation'=51
'operation'=52
'function'=53
'constructor'=54
'constructs'=55
'input'=56
'conform'=57
'as'=58
'bind'=59
'static'=60
'to'=61
'private'=62
'shared'=63
'state'=64
'edge'=65
'projection'=66
'with'=67
'using'=68
'accept'=69
'via'=70
'materialize'=71
'if'=72
'absent'=73
'on'=74
'policy'=75
'default'=76
'source'=77
'repository'=78
'commit'=79
'revision'=80
'semantic-major'=81
'on-delete'=82
'lifecycle'=83
'journal'=84
'subscription'=85
'retain-other'=86
'keyed'=87
'public-traversal'=88
'id'=89
'doc'=90
'mode'=91
'emits'=92
'receiver'=93
'requires'=94
'any'=95
'get'=96
'set'=97
'watch'=98
'start'=99
'stop'=100
'read'=101
'write'=102
'resolve'=103
'connect'=104
'disconnect'=105
'call'=106
'watch-start'=107
'watch-stop'=108
'subscribe'=109
'unsubscribe'=110
'register'=111
'crdt'=112
'optional-one'=113
'exactly-one'=114
'many-unique'=115
'many'=116
'ordered'=117
'unit'=118
'watch-handle'=119
'message'=120
'atom-ref'=121
'interface-ref'=122
'optional'=123
'list'=124
'record'=125
'bool'=126
'bytes'=127
'double'=128
'int32'=129
'int64'=130
'string'=131
'uint32'=132
'uint64'=133
'true'=134
'false'=135
'null'=136
'->'=137
':'=138
';'=139
','=140
'.'=141
'{'=142
'}'=143
'['=144
']'=145
'('=146
')'=147
'<'=148
'>'=149
'&'=150
'='=151
'symbol'=45
'atom'=46
'interface'=47
'interfaces'=48
'package'=49
'value'=50
'field'=51
'relation'=52
'operation'=53
'function'=54
'constructor'=55
'constructs'=56
'input'=57
'conform'=58
'as'=59
'bind'=60
'static'=61
'to'=62
'private'=63
'shared'=64
'state'=65
'edge'=66
'projection'=67
'with'=68
'using'=69
'accept'=70
'via'=71
'materialize'=72
'if'=73
'absent'=74
'on'=75
'policy'=76
'default'=77
'source'=78
'repository'=79
'commit'=80
'revision'=81
'semantic-major'=82
'on-delete'=83
'lifecycle'=84
'journal'=85
'subscription'=86
'retain-other'=87
'keyed'=88
'public-traversal'=89
'id'=90
'doc'=91
'mode'=92
'emits'=93
'receiver'=94
'requires'=95
'any'=96
'get'=97
'set'=98
'watch'=99
'start'=100
'stop'=101
'read'=102
'write'=103
'resolve'=104
'connect'=105
'disconnect'=106
'call'=107
'watch-start'=108
'watch-stop'=109
'subscribe'=110
'unsubscribe'=111
'register'=112
'crdt'=113
'optional-one'=114
'exactly-one'=115
'many-unique'=116
'many'=117
'ordered'=118
'unit'=119
'watch-handle'=120
'message'=121
'atom-ref'=122
'interface-ref'=123
'optional'=124
'list'=125
'record'=126
'bool'=127
'bytes'=128
'double'=129
'int32'=130
'int64'=131
'string'=132
'uint32'=133
'uint64'=134
'true'=135
'false'=136
'null'=137
'->'=138
':'=139
';'=140
','=141
'.'=142
'{'=143
'}'=144
'['=145
']'=146
'('=147
')'=148
'<'=149
'>'=150
'&'=151
'='=152
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@ import { WorkspaceItemContext } from "./QuixosCapabilityParser.js";
import { ResourceImportDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalAtomDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
import { SymbolDeclContext } from "./QuixosCapabilityParser.js";
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
import { AtomDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
@@ -145,6 +146,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitExternalInterfaceDecl?: (ctx: ExternalInterfaceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.symbolDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSymbolDecl?: (ctx: SymbolDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.resourcePreamble`.
* @param ctx the parse tree
+60 -16
View File
@@ -205,6 +205,7 @@ interface LoweringState {
fileName: string;
diagnostics: CapabilitySourceDiagnostic[];
atoms: Map<string, AtomId>;
symbols: Map<string, string>;
interfaces: Map<string, InterfaceSymbol>;
packages: Map<string, PackageSymbol>;
attachments: Map<string, AttachmentSymbol>;
@@ -387,14 +388,33 @@ const lowerSelectedOperations = (
return { operations };
};
const lowerExecution = (context: ExecutionClauseContext | null): { execution?: ExecutionContract } => {
const lowerExecution = (
state: LoweringState,
context: ExecutionClauseContext | null,
implementation = false,
): { execution?: ExecutionContract } => {
if (!context) return {};
if (!implementation && context.identifierList())
loweringIssue(
state,
context,
"interface-execution-targets",
"Interfaces declare effects; execution targets belong to package implementations",
);
const targets = context
.identifierList()
?.identifier()
.map((node) => {
const name = identifier(node);
if (name === "server") return name;
return requireSymbol(state, state.symbols, name, node, "nominal execution target") ?? `unresolved:${name}`;
}) ?? ["server"];
return {
execution: context.REMOTE()
? { kind: "remote" }
: {
kind: context.SNAPSHOT() ? "snapshot" : "transaction",
targets: context.identifierList()!.identifier().map(identifier),
targets: implementation ? targets : ["server"],
},
};
};
@@ -508,17 +528,17 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
if (operation.GET()) {
operations.push({
...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
});
} else if (operation.EDIT()) {
operations.push({
...valueOperation("edit", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
});
} else if (operation.SET()) {
operations.push({
...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
});
} else {
operations.push(
@@ -611,7 +631,7 @@ const lowerRelationshipMember = (
);
}
if (operation.executionClause())
Object.assign(operations[operations.length - 1]!, lowerExecution(operation.executionClause()));
Object.assign(operations[operations.length - 1]!, lowerExecution(state, operation.executionClause()));
}
return {
kind: "relationship",
@@ -653,7 +673,7 @@ const lowerOperationMember = (state: LoweringState, context: OperationMemberCont
outputType,
mode: "call",
...(context.STATIC() ? { scope: "class" as const } : {}),
...lowerExecution(context.executionClause()),
...lowerExecution(state, context.executionClause()),
},
],
};
@@ -814,18 +834,24 @@ const lowerInterfaceTemplate = (
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
if (operation.GET())
return [
{ ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) },
{
...signature("get", id, valueType.unit, type),
...lowerExecution(state, operation.executionClause()),
},
];
if (operation.EDIT())
return [
{
...signature("edit", id, { kind: "change", value: type }, valueType.unit),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
},
];
if (operation.SET())
return [
{ ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) },
{
...signature("set", id, type, valueType.unit),
...lowerExecution(state, operation.executionClause()),
},
];
return [
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", type),
@@ -859,7 +885,7 @@ const lowerInterfaceTemplate = (
outputType,
),
...(operation.STATIC() ? { scope: "class" as const } : {}),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
},
],
};
@@ -907,7 +933,10 @@ const lowerInterfaceTemplate = (
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
if (operation.RESOLVE())
return [
{ ...signature("resolve", id, valueType.unit, resolved), ...lowerExecution(operation.executionClause()) },
{
...signature("resolve", id, valueType.unit, resolved),
...lowerExecution(state, operation.executionClause()),
},
];
if (operation.CONNECT() || operation.DISCONNECT())
return [
@@ -918,7 +947,7 @@ const lowerInterfaceTemplate = (
operation.CONNECT() ? keyedTarget : targetType,
valueType.unit,
),
...lowerExecution(operation.executionClause()),
...lowerExecution(state, operation.executionClause()),
},
];
return [
@@ -1196,7 +1225,7 @@ const lowerPackageOperation = (
inputType: lowerValueType(state, context.valueType(0)),
outputType: lowerValueType(state, context.valueType(1)),
mode: text(context.operationMode()) as InterfaceOperationMode,
...lowerExecution(context.executionClause()),
...lowerExecution(state, context.executionClause(), true),
eventType: event ? lowerValueType(state, event.valueType()) : undefined,
receiverRequirement: lowerReceiver(state, context.receiverRequirement()),
dependencyPorts: ports,
@@ -1213,7 +1242,7 @@ const lowerPackageFunction = (
const alias = identifier(context.identifier());
const entry: PackageExport = {
kind: "function",
...lowerExecution(context.executionClause()),
...lowerExecution(state, context.executionClause(), true),
id: capabilityId.packageExport(stringValue(context.stringLiteral())),
displayName: alias,
inputType: lowerValueType(state, context.valueType(0)),
@@ -1355,7 +1384,7 @@ const lowerGenericPackageExport = (
displayName: identifier(context.identifier()),
parameters,
kind: operation ? "operation" : "function",
...lowerExecution(context.executionClause()),
...lowerExecution(state, context.executionClause(), true),
inputType: state.types.expression(context.valueType(0)!),
outputType: state.types.expression(context.valueType(1)!),
dependencyPorts,
@@ -3092,6 +3121,7 @@ const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnos
atoms,
types: new GenericSourceTypes(atoms),
interfaces: new Map(),
symbols: new Map(),
packages: new Map(),
attachments: new Map(),
};
@@ -3240,6 +3270,20 @@ const compileCapabilityResourceSourceInternal = (
const externalAtoms = externalAtomsFrom(state, preamble.atoms);
const imports = registerImports(state, preamble.imports, environment);
const externalInterfaces = externalInterfacesFrom(state, preamble.interfaces);
for (const declaration of (interfaceContext ?? packageContext)!
.resourcePreamble()
.flatMap((entry) => entry.symbolDecl() ?? [])) {
const name = identifier(declaration.identifier());
const identity = stringValue(declaration.stringLiteral());
if (name === "server" || identity === "server" || !identity.trim())
loweringIssue(
state,
declaration,
"invalid-symbol",
"Nominal symbols require a nonempty identity distinct from the server executor",
);
declareSymbol(state, state.symbols, name, identity, declaration, "symbol");
}
state.types.declareAliases(preamble.aliases);
let resource: CapabilityResource;
+1 -8
View File
@@ -25,7 +25,6 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined,
path: string,
requireTargets = true,
) => {
if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") {
@@ -34,9 +33,6 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
}
if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction");
if (requireTargets)
for (const target of requested.targets ?? [])
if (!provided.targets?.includes(target)) fail(path, `Dependency does not provide execution target ${target}`);
};
for (const revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) {
@@ -84,10 +80,7 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
if (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? [])
// Package targets describe available code. Required public target
// guarantees are checked over the installed operation closure;
// an optional implementation target must not strengthen a dependency.
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath, false);
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath);
}
}
if (!entry.execution || entry.execution.kind === "remote") continue;
+1 -4
View File
@@ -32,10 +32,7 @@ export function publicOperationContract(
const execution = operation.execution ?? { kind: "remote" };
const member = iface.members.find((m) => m.operations.includes(operation))!;
const fieldPolicy =
member.kind === "value" &&
operation.scope !== "class" &&
execution.kind === "transaction" &&
execution.targets.includes("browser")
member.kind === "value" && operation.scope !== "class" && execution.kind === "transaction"
? operation.displayName === "edit"
? "crdt"
: member.nativeField && operation.displayName === "set"
+5 -1
View File
@@ -117,7 +117,11 @@ export class BrowserArtifactGateway {
const entries: BrowserArtifact[] = [];
for (const manifest of loaded.manifests)
for (const implementation of manifest.implementations) {
if (implementation.target !== "browser" || implementation.compatibility !== "quixos-js-module-v1") continue;
if (
implementation.target !== "target:org.quixos.execution.javascript@1" ||
implementation.compatibility !== "quixos-js-module-v1"
)
continue;
const prefix = `/__quixos/artifacts/${loaded.digest}/${implementation.contentDigest}/`;
for (const file of implementation.files) {
if (!/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(file.mediaType)) fail(file.path, "Invalid asset MIME type");
+1
View File
@@ -1,4 +1,5 @@
export * from "./plan.js";
export * from "./native-backends.js";
export * from "./artifacts.js";
export * from "./ports.js";
export * from "./files.js";
+3 -1
View File
@@ -81,7 +81,9 @@ export function compileInspectionTemplates(plan: ExecutionPlan) {
),
);
if (providers.some((op) => !op || op.unsupported || !["snapshot", "remote"].includes(op.effect))) continue;
const portable = providers.every((op) => op!.effect === "snapshot" && op!.targets.includes("browser"));
const portable = providers.every(
(op) => op!.effect === "snapshot" && op!.targets.includes("target:org.quixos.execution.javascript@1"),
);
if (lane === "local" && !portable) continue;
const key = "m" + index,
flag = "s" + index;
+4 -2
View File
@@ -250,7 +250,9 @@ function assembleExecutionInstallation(
})
: undefined;
const browserExports = exports.filter(
(entry) => entry.execution.kind !== "remote" && entry.execution.targets.includes("browser"),
(entry) =>
entry.execution.kind !== "remote" &&
entry.execution.targets.includes("target:org.quixos.execution.javascript@1"),
);
if (!browserExports.length) {
if (selection.browser) fail(revision, "Package has no browser exports to select");
@@ -259,7 +261,7 @@ function assembleExecutionInstallation(
}
const browser = selectArtifactImplementation(manifest, {
...selection.browser,
target: "browser",
target: "target:org.quixos.execution.javascript@1",
compatibility: "quixos-js-module-v1",
exports: browserExports.map((entry) => entry.id),
});
+21
View File
@@ -0,0 +1,21 @@
import type { Binding } from "../capability-model/types.js";
/** Nominal library identity. Backends implement primitives; target names carry no
* built-in semantics in the graph planner. The JS backend is installed by the
* current toolchain, alongside the authoritative server executor. */
export const javascriptTarget = "target:org.quixos.execution.javascript@1";
export interface NativeExecutionBackend {
readonly target: string;
readonly supports: (binding: Exclude<Binding, { kind: "interface" }>) => boolean;
}
export const standardNativeBackends: readonly NativeExecutionBackend[] = [
{ target: "server", supports: () => true },
{
target: javascriptTarget,
supports: (binding) =>
binding.kind === "state" ||
binding.kind === "edge" ||
binding.kind === "journal" ||
(binding.kind === "subscription" && binding.primitive === "status"),
},
];
+19 -14
View File
@@ -1,3 +1,4 @@
import { standardNativeBackends, type NativeExecutionBackend } from "./native-backends.js";
import type {
WorkspaceRevision,
ExecutionContract,
@@ -45,7 +46,7 @@ export interface PlannedOperation {
operationId: string;
scope: "object" | "class";
execution: ExecutionContract;
/** Installed implementation closure; the public execution contract above remains required. */
/** Targets available for this complete installed implementation/dependency closure. */
availableTargets: string[];
input: ValueType;
output: ValueType;
@@ -171,7 +172,10 @@ function checkValue(type: ValueType, path: string, allowChange = false): void {
}
/** Compile declarations into a closed dispatch/dependency graph before artifact selection. */
export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
export function compileExecutionPlan(
input: WorkspaceRevision,
backends: readonly NativeExecutionBackend[] = standardNativeBackends,
): ExecutionPlan {
const workspace = structuredClone(input);
const issues = validateWorkspaceRevision(workspace);
if (issues.length)
@@ -311,13 +315,6 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
path.join(" -> "),
"Execution effect is not allowed by the root contract",
);
for (const target of requested.targets)
if (!node.execution.targets.includes(target))
throw new ExecutionPlanError(
"MISSING_EXECUTION_TARGET",
path.join(" -> "),
`Dependency does not provide ${target}`,
);
for (const dependency of node.dependencies) {
if (dependency.kind === "operation")
walk(dependency.operation, [...path, dependency.port, dependency.operation]);
@@ -352,9 +349,10 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
const provider =
binding.kind === "package"
? packages.get(binding.packageRevisionId)!.exports.find((entry) => entry.id === binding.exportId)!.execution
: binding.kind === "lifecycle" || (binding.kind === "subscription" && binding.primitive !== "status")
? { kind: "transaction" as const, targets: ["server" as const] }
: { kind: node.execution.kind, targets: ["browser" as const, "server" as const] };
: {
kind: node.execution.kind,
targets: backends.filter((backend) => backend.supports(binding)).map((backend) => backend.target),
};
if (provider && provider.kind !== "remote") node.availableTargets = [...provider.targets].sort();
}
let targetsChanged = true;
@@ -382,6 +380,13 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
}
}
}
for (const node of operationMap.values())
if (node.execution.kind !== "remote" && !node.availableTargets.includes("server"))
throw new ExecutionPlanError(
"MISSING_SERVER_IMPLEMENTATION",
node.key,
"Every graph operation requires an authoritative implementation across its dependency closure",
);
const plannedQueries: PlannedQuery[] = queries.map((query) => ({
id: queryKey(query.packageRevisionId, query.checked.declaration.id),
query,
@@ -515,11 +520,11 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
source.admittedOperations.push(...query.operations);
for (const operation of query.operations) {
const node = operationMap.get(operation)!;
if (node.execution.kind !== "snapshot" || !node.execution.targets.includes("browser"))
if (node.execution.kind !== "snapshot" || !node.availableTargets.some((target) => target !== "server"))
throw new ExecutionPlanError(
"NONPORTABLE_QUERY_FIELD",
`${query.id}/${operation}`,
"Local query getter must provide browser snapshot execution",
"Local query getter needs an installed local snapshot implementation",
);
}
}
+3 -2
View File
@@ -15,6 +15,8 @@ export interface ArtifactReceipt {
generated: { target: string; path: string; digest: string; bytes: number }[];
}
const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
/** Nominal identities never become filesystem paths. */
export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`;
/** Deterministic expected receipt. A trusted build must also typecheck and bundle these exact sources.
* This is consistency evidence inside a trusted Nix build path, not remote build attestation. */
export function expectedArtifactReceipt(
@@ -56,8 +58,7 @@ export function expectedArtifactReceipt(
.sort();
const sources = new Map<string, Uint8Array>();
const generated = targets.map((target) => {
if (!/^[a-zA-Z0-9_-]+$/.test(target)) throw new Error("Unsupported generated target filename");
const path = `share/quixos/generated/${target}.ts`;
const path = generatedBindingPath(target);
const bytes = new TextEncoder().encode(
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }),
);
+2 -2
View File
@@ -250,10 +250,10 @@ export async function compileQuery(
const getter = member.operations.find(
(operation) => operation.displayName === (member.kind === "value" ? "get" : "resolve"),
);
if (getter?.execution?.kind !== "snapshot" || !getter.execution.targets.includes("browser"))
if (getter?.execution?.kind !== "snapshot")
fail(
"QUERY_LOCAL_DEPENDENCY",
`Local query requires a browser snapshot getter for ${contract.displayName}.${name}; split the remote field or move the query to the server`,
`Local query requires a graph snapshot getter for ${contract.displayName}.${name}; split the remote field or move the query to the server`,
node,
);
if (member.queryRead.execution === "rpc-permitted")