Publish quixos-protocol from 37a50c17063a41d23037b39785f7054992d7c708

This commit is contained in:
Quixos Subtree Publisher
2026-10-06 04:38:24 +00:00
270 changed files with 26348 additions and 4542 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "c87835bff85794bc232dbaaddc1eb3b593c2aab8", "sourceCommit": "37a50c17063a41d23037b39785f7054992d7c708",
"sourcePath": "quixos-protocol", "sourcePath": "quixos-protocol",
"exportName": "quixos-protocol", "exportName": "quixos-protocol",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
+20 -2
View File
@@ -29,6 +29,7 @@
pkgs.jujutsu pkgs.jujutsu
pkgs.gnutar pkgs.gnutar
pkgs.util-linux pkgs.util-linux
pkgs.nix
]; ];
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild
@@ -39,6 +40,8 @@
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}" export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin
yarn build yarn build
esbuild dist/src/execution-world/index.js --bundle --platform=node --target=node24 --format=esm --outfile=execution-world.mjs
esbuild dist/src/react-build/package-queries.js --bundle --platform=node --target=node24 --format=esm --outfile=react-query-generator.mjs
esbuild dist/src/bindings/client.js --bundle --platform=node --target=node24 --format=esm --outfile=client-codegen.mjs esbuild dist/src/bindings/client.js --bundle --platform=node --target=node24 --format=esm --outfile=client-codegen.mjs
diff --recursive --unified "$TMPDIR/generated-before/proto" src/gen diff --recursive --unified "$TMPDIR/generated-before/proto" src/gen
diff --recursive --unified "$TMPDIR/generated-before/capability" src/capability-language/generated diff --recursive --unified "$TMPDIR/generated-before/capability" src/capability-language/generated
@@ -59,6 +62,7 @@
--bundle --platform=node --target=node24 --format=esm \ --bundle --platform=node --target=node24 --format=esm \
--outfile=quixos-lock-check.mjs --outfile=quixos-lock-check.mjs
esbuild dist/src/bindings/cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-ts.mjs esbuild dist/src/bindings/cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-ts.mjs
esbuild dist/src/bindings/portable-client-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-codegen-client.mjs
esbuild dist/src/bindings/react-platform.js --bundle --platform=node --target=node24 --format=esm --outfile=react-platform.mjs esbuild dist/src/bindings/react-platform.js --bundle --platform=node --target=node24 --format=esm --outfile=react-platform.mjs
esbuild dist/src/capability-language/tool-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-qx.mjs esbuild dist/src/capability-language/tool-cli.js --bundle --platform=node --target=node24 --format=esm --outfile=quixos-qx.mjs
runHook postBuild runHook postBuild
@@ -67,20 +71,31 @@
checkPhase = '' checkPhase = ''
runHook preCheck runHook preCheck
yarn test yarn test
node dist/test/emit-portable-reader.js
node dist/test/emit-managed.js
runHook postCheck runHook postCheck
''; '';
installPhase = '' installPhase = ''
runHook preInstall runHook preInstall
mkdir -p "$out" mkdir -p "$out"
install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix" install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix"
install -Dm644 nix/checked-artifacts.nix "$out/share/checked-artifacts.nix"
install -Dm644 nix/package-context.nix "$out/share/package-context.nix"
install -Dm644 nix/lock-source.nix "$out/share/lock-source.nix"
install -Dm644 nix/package-context.mjs "$out/share/package-context.mjs"
install -Dm644 nix/check-package.mjs "$out/share/check-package.mjs"
install -Dm644 nix/check-execution.mjs "$out/share/check-execution.mjs"
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \ substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
--replace-fail '@nixpkgs@' '${pkgs.path}' --replace-fail '@nixpkgs@' '${pkgs.path}' \
--replace-fail '@nixpkgsNarHash@' '${nixpkgs.narHash}'
cp --reflink=auto --recursive grammar "$out/grammar" cp --reflink=auto --recursive grammar "$out/grammar"
cp --reflink=auto --recursive proto "$out/proto" cp --reflink=auto --recursive proto "$out/proto"
cp --reflink=auto --recursive dist "$out/dist" cp --reflink=auto --recursive dist "$out/dist"
cp package.json "$out/package.json" cp package.json "$out/package.json"
mkdir -p "$out/share/quixos-protocol/qualification"
cp -r dist/portable-reader dist/managed "$out/share/quixos-protocol/qualification/"
mkdir -p "$out/bin" mkdir -p "$out/bin"
for tool in quixos-codegen-ts quixos-qx; do for tool in quixos-codegen-ts quixos-codegen-client quixos-qx; do
printf '#!/bin/sh\nexec ${pkgs.nodejs_24}/bin/node "%s/libexec/quixos-protocol/%s.mjs" "$@"\n' "$out" "$tool" > "$out/bin/$tool" printf '#!/bin/sh\nexec ${pkgs.nodejs_24}/bin/node "%s/libexec/quixos-protocol/%s.mjs" "$@"\n' "$out" "$tool" > "$out/bin/$tool"
chmod +x "$out/bin/$tool" chmod +x "$out/bin/$tool"
done done
@@ -112,9 +127,12 @@
EOF EOF
chmod +x "$out/bin/quixos-lock-check" chmod +x "$out/bin/quixos-lock-check"
mkdir -p "$out/libexec/quixos-protocol" mkdir -p "$out/libexec/quixos-protocol"
install -m644 execution-world.mjs "$out/libexec/quixos-protocol/execution-world.mjs"
install -m644 react-query-generator.mjs "$out/libexec/quixos-protocol/react-query-generator.mjs"
install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs" install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs"
install -m644 react-platform.mjs "$out/libexec/quixos-protocol/react-platform.mjs" install -m644 react-platform.mjs "$out/libexec/quixos-protocol/react-platform.mjs"
install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/" install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
install -m644 quixos-codegen-client.mjs "$out/libexec/quixos-protocol/"
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs" install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs" install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
install -m644 quixos-workspace-compile.mjs "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs" install -m644 quixos-workspace-compile.mjs "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs"
+165 -24
View File
@@ -27,6 +27,7 @@ workspaceItem
| sharedAttachmentDecl | sharedAttachmentDecl
| conformanceDecl | conformanceDecl
| constructorBindingDecl | constructorBindingDecl
| presentationDecl
| typeAliasDecl | typeAliasDecl
; ;
@@ -43,10 +44,15 @@ externalInterfaceDecl
: EXTERNAL INTERFACE identifier REVISION stringLiteral SEMI : EXTERNAL INTERFACE identifier REVISION stringLiteral SEMI
; ;
symbolDecl
: SYMBOL identifier ID stringLiteral SEMI
;
resourcePreamble resourcePreamble
: resourceImportDecl : resourceImportDecl
| externalAtomDecl | externalAtomDecl
| externalInterfaceDecl | externalInterfaceDecl
| symbolDecl
| typeAliasDecl | typeAliasDecl
; ;
@@ -92,21 +98,27 @@ interfaceMember
: valueMember : valueMember
| relationshipMember | relationshipMember
| operationMember | operationMember
| moduleMember
;
moduleMember
: MODULE identifier ID stringLiteral CONTRACT stringLiteral (SUBJECT targetConstraint)? SEMI
; ;
operationMember operationMember
: STATIC? OPERATION identifier ID stringLiteral COLON valueType ARROW valueType : STATIC? OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
LBRACE CALL ID stringLiteral SEMI RBRACE LBRACE CALL ID stringLiteral executionClause? SEMI RBRACE
; ;
valueMember valueMember
: queryReadContract? VALUE identifier ID stringLiteral COLON valueType : queryReadContract? (VALUE | FIELD) identifier ID stringLiteral COLON valueType
LBRACE valueMemberOperation* RBRACE LBRACE valueMemberOperation* RBRACE
; ;
valueMemberOperation valueMemberOperation
: GET ID stringLiteral SEMI : GET ID stringLiteral executionClause? SEMI
| SET ID stringLiteral SEMI | SET ID stringLiteral executionClause? SEMI
| EDIT ID stringLiteral executionClause? SEMI
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI | WATCH START ID stringLiteral STOP ID stringLiteral SEMI
; ;
@@ -116,13 +128,13 @@ relationshipMember
; ;
queryReadContract queryReadContract
: QUERYABLE RPC? : QUERYABLE (RPC | PORTABLE)?
; ;
relationshipOperation relationshipOperation
: RESOLVE ID stringLiteral SEMI : RESOLVE ID stringLiteral executionClause? SEMI
| CONNECT ID stringLiteral SEMI | CONNECT ID stringLiteral executionClause? SEMI
| DISCONNECT ID stringLiteral SEMI | DISCONNECT ID stringLiteral executionClause? SEMI
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI | WATCH START ID stringLiteral STOP ID stringLiteral SEMI
; ;
@@ -144,11 +156,22 @@ packageExport
| packageConstructorExport | packageConstructorExport
| packageQuery | packageQuery
| packageQuerySpecialization | packageQuerySpecialization
| moduleExport
;
moduleExport
: MODULE identifier ID stringLiteral IMPLEMENTS interfaceType DOT identifier SEMI
;
presentationDecl
: PRESENTATION identifier ID stringLiteral FOR interfaceType USING identifier DOT identifier SEMI
; ;
packageQuery packageQuery
: QUERY identifier typeParameters? ID stringLiteral ROOT interfaceType : QUERY FRAGMENT? identifier typeParameters? ID stringLiteral ROOT WORKSPACE? interfaceType
DOCUMENT stringLiteral OPERATION stringLiteral LBRACE queryClause* RBRACE DOCUMENT stringLiteral
(OPERATION stringLiteral LBRACE queryClause* RBRACE
| FRAGMENT stringLiteral (OPERATION stringLiteral)? (LBRACE queryClause* RBRACE | SEMI))
; ;
packageQuerySpecialization packageQuerySpecialization
@@ -156,7 +179,10 @@ packageQuerySpecialization
; ;
queryClause queryClause
: FRAGMENTS stringLiteral SEMI : REPLICA SEMI
| LOCAL FROM identifier SEMI
| FRAGMENTS stringLiteral SEMI
| FRAGMENTS identifier DOT identifier (WITH LBRACE fragmentVariable* RBRACE)? SEMI
| VIEW OBJECT? identifier AS interfaceType SEMI | VIEW OBJECT? identifier AS interfaceType SEMI
| MAX identifier INTEGER SEMI | MAX identifier INTEGER SEMI
| ALLOW interfaceType DOT identifier identifier stringLiteral SEMI | ALLOW interfaceType DOT identifier identifier stringLiteral SEMI
@@ -164,14 +190,33 @@ queryClause
| POLL INTEGER stringLiteral SEMI | POLL INTEGER stringLiteral SEMI
; ;
fragmentVariable : identifier TO identifier SEMI;
packageOperationExport packageOperationExport
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? preparationClause? acceptanceClause? SEMI
; ;
packageFunctionExport packageFunctionExport
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
dependencyBlock? SEMI executionClause? dependencyBlock? preparationClause? acceptanceClause? SEMI
;
acceptanceClause
: ACCEPT USING identifier
;
preparationClause
: PREPARES CREATION identifier USING stringLiteral
;
executionClause
: EXECUTION REMOTE
| EXECUTION (SNAPSHOT | TRANSACTION) (TARGETS LBRACK identifierList RBRACK)?
;
selectedOperations
: USING LBRACK memberOperationRef (COMMA memberOperationRef)* RBRACK
; ;
packageConstructorExport packageConstructorExport
@@ -207,9 +252,11 @@ dependencyBlock
; ;
dependencyPort dependencyPort
: STATE identifier ID stringLiteral COLON valueType primitiveList SEMI : REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI | STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? SEMI | EDGE identifier ID stringLiteral COLON cardinality targetConstraint (KEYED stringLiteral)? primitiveList SEMI
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? (FROM INPUT)? SEMI
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI | CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI | QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
; ;
@@ -221,6 +268,7 @@ primitiveList
primitive primitive
: READ : READ
| WRITE | WRITE
| EDIT
| RESOLVE | RESOLVE
| CONNECT | CONNECT
| DISCONNECT | DISCONNECT
@@ -243,8 +291,10 @@ stateDecl
; ;
storagePolicy storagePolicy
: OPTIMISTIC_REGISTER : REGISTER
| CRDT LPAREN valueType RPAREN | CRDT LPAREN valueType RPAREN
| JOURNAL jsonObject
| SUBSCRIPTION LPAREN valueType RPAREN
; ;
edgeDecl edgeDecl
@@ -257,15 +307,38 @@ edgeEndpoint
; ;
conformanceDecl conformanceDecl
: CONFORM identifier AS identifier typeArguments? (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)? : CONFORM typeParameters? identifier conformanceSourceArguments? AS identifier typeArguments? (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)?
LBRACE conformanceItem* RBRACE LBRACE conformanceItem* RBRACE
; ;
conformanceSourceArguments
: typeArguments
;
conformanceItem conformanceItem
: PRIVATE attachmentDecl : PRIVATE attachmentDecl
| operationBindingDecl | operationBindingDecl
| stateFieldBindingDecl | stateFieldBindingDecl
| initializationDecl
| relationshipMaterializationDecl | relationshipMaterializationDecl
| moduleBindingDecl
;
moduleBindingDecl
: BIND identifier TO identifier DOT identifier SEMI
;
initializationDecl
: INITIALIZE LBRACE initializationField* RBRACE SEMI
;
initializationField
: identifier TO STATE identifier SEMI
| identifier TO EDGE identifier DOT identifier SEMI
;
creationOwnerMapping
: interfaceType FROM INPUT (DOT identifier)* SEMI
; ;
stateFieldBindingDecl stateFieldBindingDecl
@@ -289,6 +362,7 @@ operationName
| CALL | CALL
| GET | GET
| SET | SET
| EDIT
| RESOLVE | RESOLVE
| CONNECT | CONNECT
| DISCONNECT | DISCONNECT
@@ -299,14 +373,23 @@ operationName
; ;
operationProvider operationProvider
: STATE identifier DOT statePrimitive : LIFECYCLE DOT identifier
| JOURNAL identifier DOT identifier
| SUBSCRIPTION identifier DOT identifier
| STATE identifier DOT statePrimitive
| EDGE identifier DOT identifier DOT edgePrimitive | EDGE identifier DOT identifier DOT edgePrimitive
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? | INTERFACE identifier typeArguments? DOT memberOperationRef
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
;
acceptanceBinding
: ACCEPT dependencyBindingBlock
; ;
statePrimitive statePrimitive
: READ : READ
| WRITE | WRITE
| EDIT
| WATCH_START | WATCH_START
| WATCH_STOP | WATCH_STOP
; ;
@@ -324,11 +407,13 @@ dependencyBindingBlock
; ;
dependencyBinding dependencyBinding
: identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI : identifier TO WORKSPACE REFLECTION LBRACK identifierList RBRACK SEMI
| identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI | identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI | identifier TO INTERFACE identifier typeArguments? ((VIA EDGE identifier DOT identifier) | (FROM INPUT))? SEMI
| identifier TO CREATION identifier WITH LBRACE creationOwnerMapping* RBRACE SEMI
| identifier TO CONSTRUCTOR identifier SEMI | identifier TO CONSTRUCTOR identifier SEMI
| identifier TO QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI | identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
; ;
constructorBindingDecl constructorBindingDecl
@@ -346,6 +431,7 @@ valueType
| OPTIONAL LT valueType GT | OPTIONAL LT valueType GT
| LIST LT valueType GT | LIST LT valueType GT
| RECORD LBRACE recordField* RBRACE | RECORD LBRACE recordField* RBRACE
| VARIANT ID stringLiteral LBRACE variantCase+ RBRACE
| identifier typeArguments? | identifier typeArguments?
; ;
@@ -353,6 +439,10 @@ recordField
: identifier COLON valueType SEMI : identifier COLON valueType SEMI
; ;
variantCase
: identifier ID stringLiteral COLON valueType SEMI
;
scalarType scalarType
: BOOL : BOOL
| BYTES | BYTES
@@ -396,12 +486,30 @@ jsonArray
identifier identifier
: IDENTIFIER : IDENTIFIER
| START
| STOP
| EDIT
| CREATION
| INITIALIZE
| EXECUTION
| SNAPSHOT
| TRANSACTION
| REMOTE
| TARGETS
| PORTABLE
| REPLICA
| LOCAL
| FROM
| REGISTER
| INTERFACES
| REFLECTION
| SOURCE | SOURCE
| QUERY | QUERY
| SPECIALIZE | SPECIALIZE
| ROOT | ROOT
| DOCUMENT | DOCUMENT
| FRAGMENTS | FRAGMENTS
| FRAGMENT
| VIEW | VIEW
| MAX | MAX
| ALLOW | ALLOW
@@ -411,17 +519,44 @@ identifier
| RESULT_BYTES | RESULT_BYTES
| RPC_CALLS | RPC_CALLS
| DEADLINE_MS | DEADLINE_MS
| PRESENTATION
| SUBJECT
| MESSAGE
| JOURNAL
| SUBSCRIPTION
| CONNECT
| FOR
; ;
stringLiteral stringLiteral
: STRING_LITERAL : STRING_LITERAL
; ;
EDIT: 'edit';
MODULE: 'module';
CONTRACT: 'contract';
PRESENTATION: 'presentation';
SUBJECT: 'subject';
FOR: 'for';
CREATION: 'creation';
VARIANT: 'variant';
PREPARES: 'prepares';
INITIALIZE: 'initialize';
WORKSPACE: 'workspace'; WORKSPACE: 'workspace';
EXECUTION: 'execution';
SNAPSHOT: 'snapshot';
TRANSACTION: 'transaction';
REMOTE: 'remote';
TARGETS: 'targets';
PORTABLE: 'portable';
REPLICA: 'replica';
LOCAL: 'local';
FROM: 'from';
RESULT_BYTES: 'result-bytes'; RESULT_BYTES: 'result-bytes';
RPC_CALLS: 'rpc-calls'; RPC_CALLS: 'rpc-calls';
DEADLINE_MS: 'deadline-ms'; DEADLINE_MS: 'deadline-ms';
QUERY: 'query'; QUERY: 'query';
REFLECTION: 'reflection';
SPECIALIZE: 'specialize'; SPECIALIZE: 'specialize';
ROOT: 'root'; ROOT: 'root';
DOCUMENT: 'document'; DOCUMENT: 'document';
@@ -441,11 +576,13 @@ REF: 'ref';
FRAGMENT: 'fragment'; FRAGMENT: 'fragment';
IMPORT: 'import'; IMPORT: 'import';
EXTERNAL: 'external'; EXTERNAL: 'external';
SYMBOL: 'symbol';
ATOM: 'atom'; ATOM: 'atom';
INTERFACE: 'interface'; INTERFACE: 'interface';
INTERFACES: 'interfaces'; INTERFACES: 'interfaces';
PACKAGE: 'package'; PACKAGE: 'package';
VALUE: 'value'; VALUE: 'value';
FIELD: 'field';
RELATION: 'relation'; RELATION: 'relation';
OPERATION: 'operation'; OPERATION: 'operation';
FUNCTION: 'function'; FUNCTION: 'function';
@@ -464,6 +601,7 @@ EDGE: 'edge';
PROJECTION: 'projection'; PROJECTION: 'projection';
WITH: 'with'; WITH: 'with';
USING: 'using'; USING: 'using';
ACCEPT: 'accept';
VIA: 'via'; VIA: 'via';
MATERIALIZE: 'materialize'; MATERIALIZE: 'materialize';
IF: 'if'; IF: 'if';
@@ -477,6 +615,9 @@ COMMIT: 'commit';
REVISION: 'revision'; REVISION: 'revision';
SEMANTIC_MAJOR: 'semantic-major'; SEMANTIC_MAJOR: 'semantic-major';
ON_DELETE: 'on-delete'; ON_DELETE: 'on-delete';
LIFECYCLE: 'lifecycle';
JOURNAL: 'journal';
SUBSCRIPTION: 'subscription';
RETAIN_OTHER: 'retain-other'; RETAIN_OTHER: 'retain-other';
KEYED: 'keyed'; KEYED: 'keyed';
PUBLIC_TRAVERSAL: 'public-traversal'; PUBLIC_TRAVERSAL: 'public-traversal';
@@ -502,7 +643,7 @@ WATCH_START: 'watch-start';
WATCH_STOP: 'watch-stop'; WATCH_STOP: 'watch-stop';
SUBSCRIBE: 'subscribe'; SUBSCRIBE: 'subscribe';
UNSUBSCRIBE: 'unsubscribe'; UNSUBSCRIBE: 'unsubscribe';
OPTIMISTIC_REGISTER: 'optimistic-register'; REGISTER: 'register';
CRDT: 'crdt'; CRDT: 'crdt';
OPTIONAL_ONE: 'optional-one'; OPTIONAL_ONE: 'optional-one';
EXACTLY_ONE: 'exactly-one'; EXACTLY_ONE: 'exactly-one';
+2
View File
@@ -20,6 +20,7 @@ importEntry
resourceKind resourceKind
: INTERFACE : INTERFACE
| PACKAGE | PACKAGE
| BUILD
; ;
sourceBlock sourceBlock
@@ -58,6 +59,7 @@ QUIXOS: 'quixos';
SOURCE: 'source'; SOURCE: 'source';
INTERFACE: 'interface'; INTERFACE: 'interface';
PACKAGE: 'package'; PACKAGE: 'package';
BUILD: 'build';
REPOSITORY: 'repository'; REPOSITORY: 'repository';
COMMIT: 'commit'; COMMIT: 'commit';
POLICY: 'policy'; POLICY: 'policy';
+47
View File
@@ -0,0 +1,47 @@
// Validate compiler-owned plans and immutable artifact receipts before publishing check outputs.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
const [compilerPath, planPath, checksPath, output] = process.argv.slice(2);
const compiler = await import(pathToFileURL(compilerPath));
const plan = JSON.parse(await readFile(planPath, "utf8"));
const checks = JSON.parse(await readFile(checksPath, "utf8"));
compiler.compileWorkspaceEntry(plan, JSON.parse(await readFile(join(output, "entry.json"), "utf8")));
const roots = Object.create(null);
for (const check of checks) {
if (Object.hasOwn(roots, check.packageRevisionId)) throw Error("Duplicate checked package revision");
roots[check.packageRevisionId] = check.artifactPath;
}
const generator = createHash("sha256")
.update(await readFile(compilerPath))
.digest("hex");
// These compiler-dependent checks belong to the Nix build, not runtime recovery.
const candidate = JSON.parse(await readFile(join(output, "candidate.json"), "utf8"));
const { executionContracts: _legacyContracts, ...workspace } = candidate;
if (compiler.hexDigest(compiler.compileExecutionPlan(workspace)) !== compiler.hexDigest(plan))
throw Error("CANDIDATE_PLAN_MISMATCH: build plan differs from source compilation");
const checked = await compiler.loadCheckedArtifactWorld(plan, roots, generator);
const installation = await compiler.loadExecutionInstallation(plan, roots);
const kernel = compiler.compileKernelProgram(plan),
queries = compiler.compileQueryProgram(plan);
const graph = compiler.graphExecutionRepresentation(plan, installation, kernel, queries);
for (const [file, value] of [
["graph-execution.json", graph],
["execution-world.json", checked.world],
["installation.json", installation],
["kernel.json", kernel],
["queries.json", queries],
[
"execution-artifacts.json",
{
codec: "camino-checked-execution-v2",
generator,
roots,
manifests: checked.manifests,
execution: compiler.hexDigest(graph),
programs: { kernel: compiler.hexDigest(kernel), queries: compiler.hexDigest(queries) },
},
],
])
await writeFile(join(output, file), JSON.stringify(value) + "\n");
+41
View File
@@ -0,0 +1,41 @@
// An independent package check witnesses its contract and bytes, without
// inventing a workspace or granting installation/activation authority.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
const [compilerPath, schemaPath, checksPath, output] = process.argv.slice(2);
const compiler = await import(pathToFileURL(compilerPath));
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
const checks = JSON.parse(await readFile(checksPath, "utf8"));
const generator = createHash("sha256")
.update(await readFile(compilerPath))
.digest("hex");
const evidence = [];
for (const check of checks) {
const pkg = schema.packages.find((pkg) => pkg.revisionId === check.packageRevisionId);
if (!pkg) throw Error("Unknown checked package");
const contract = compiler.packageBuildContract(schema, pkg.revisionId);
const sourceDigest = compiler.hexDigest({ revision: pkg.revisionId, source: pkg.source });
const manifest = compiler.parsePackageArtifacts(
JSON.parse(await readFile(join(check.artifactPath, "share/quixos/package-artifacts.json"), "utf8")),
);
compiler.checkPackageCoverage(
{ ...contract.definition.package, bindingDigest: contract.digest, sourceDigest },
manifest,
);
const receipt = JSON.parse(await readFile(join(check.artifactPath, "share/quixos/build-receipt.json"), "utf8"));
const expected = compiler.expectedPackageReceipt(contract, manifest, sourceDigest, generator);
if (compiler.hexDigest(receipt) !== compiler.hexDigest(expected.receipt))
throw Error("Invalid package build receipt");
const read = await compiler.artifactFileReader({ [pkg.revisionId]: check.artifactPath });
await compiler.checkArtifactFiles(manifest, read);
for (const [file, bytes] of expected.sources)
if (!Buffer.from(bytes).equals(await read(pkg.revisionId, file, bytes.length)))
throw Error("Modified generated binding " + file);
evidence.push({ ...check, contract: contract.digest, sourceDigest, receipt });
}
await writeFile(
join(output, "package-checks.json"),
JSON.stringify({ codec: "camino-package-checks-v1", generator, packages: evidence }),
);
+49
View File
@@ -0,0 +1,49 @@
# Candidate artifact evaluation. This is separate from the old server activation path;
# the new coordinator consumes its manifest/receipt, not the legacy server ABI.
{
source,
plan,
generator,
core,
sharedRuntime,
typescript,
packageRevisionId,
system ? builtins.currentSystem,
}:
let
packageSource = builtins.path {
path = /. + source;
name = "quixos-package-source";
filter =
path: _:
!(builtins.elem (baseNameOf path) [
".git"
".jj"
]);
};
package = builtins.getFlake (
"path:" + builtins.unsafeDiscardStringContext (toString packageSource)
);
pkgs = import package.inputs.nixpkgs { inherit system; };
buildContext = import ./package-context.nix {
inherit pkgs packageRevisionId;
protocol = builtins.storePath generator;
plan = builtins.path {
path = /. + plan;
name = "candidate-execution-plan.json";
};
};
checked =
package.quixosPackages.${system}.checkedArtifacts
or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world.");
in
checked (
buildContext
// {
inherit packageRevisionId;
generator = builtins.storePath generator;
core = builtins.storePath core;
sharedRuntime = builtins.storePath sharedRuntime;
typescript = builtins.storePath typescript;
}
)
+299 -34
View File
@@ -9,7 +9,11 @@
system ? builtins.currentSystem, system ? builtins.currentSystem,
}: }:
let let
pkgs = import (/. + "@nixpkgs@") { inherit system; }; pkgs = import (/. + "@nixpkgs@") {
inherit system;
config = { };
overlays = [ ];
};
protocol = builtins.storePath generator; protocol = builtins.storePath generator;
sourceKey = source: "${source.kind}:${source.repository}@${source.commit}"; sourceKey = source: "${source.kind}:${source.repository}@${source.commit}";
fetch = fetch =
@@ -20,35 +24,100 @@ let
ref = "refs/tags/quixos-reachability/${source.commit}"; ref = "refs/tags/quixos-reachability/${source.commit}";
shallow = true; shallow = true;
}; };
load = # Memoize by exact source identity. A diamond-shaped dependency graph must
ancestors: source: # not recursively evaluate every path through the same imported subtree.
if builtins.elem (sourceKey source) ancestors then loadSource =
throw "Source dependency cycle at ${sourceKey source}" source:
else if builtins.length ancestors >= 100 then
throw "Source dependency depth exceeds 100"
else
let let
directory = fetch source; directory = fetch source;
lockSource = import ./lock-source.nix { source = directory; };
lockFile = pkgs.runCommand "qx-source-lock.json" { } '' lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out" ${protocol}/bin/quixos-lock-check ${lockSource}/quixos.lock > "$out"
''; '';
lock = builtins.fromJSON (builtins.readFile lockFile); lock = builtins.fromJSON (builtins.readFile lockFile);
children = map (
entry: load (ancestors ++ [ (sourceKey source) ]) (entry.source // { inherit (entry) kind; })
) lock.resources;
in in
source // { inherit directory children; }; source // { inherit directory lock; };
root = load [ ] { inherit kind repository commit; }; sourceEntry = source: {
flatten = node: [ node ] ++ pkgs.lib.concatMap flatten node.children; key = sourceKey source;
nodes = builtins.attrValues ( value = loadSource source;
builtins.listToAttrs ( };
map (node: { sourceGraph = builtins.genericClosure {
name = sourceKey node; startSet = [ (sourceEntry { inherit kind repository commit; }) ];
value = node; operator =
}) (flatten root) entry:
) map (
resource: sourceEntry (resource.source // { inherit (resource) kind; })
) entry.value.lock.resources;
};
nodeTable = builtins.listToAttrs (
map (entry: {
name = entry.key;
value = entry.value // {
children = map (
resource:
nodeTable.${sourceKey (resource.source // { inherit (resource) kind; })}
// {
inherit (resource) binding;
}
) entry.value.lock.resources;
};
}) sourceGraph
); );
snapshots = pkgs.writeText "qx-nix-source-graph.json" ( root = nodeTable.${sourceKey { inherit kind repository commit; }};
configuration =
if builtins.pathExists (root.directory + "/quixos.check.json") then
builtins.fromJSON (builtins.readFile (root.directory + "/quixos.check.json"))
else
{ };
execution = (configuration.backend or "typescript") == "execution";
runtime = builtins.getFlake (
"git+${root.lock.quixos.repository}?rev=${root.lock.quixos.commit}&ref=refs/tags/quixos-reachability/${root.lock.quixos.commit}"
);
runtimePackages = runtime.packages.${system};
resourceClosure =
includeBuild: node:
map (entry: entry.value) (
builtins.genericClosure {
startSet = pkgs.lib.optional (includeBuild || node.kind != "build") {
key = sourceKey node;
value = node;
};
operator =
entry:
map (child: {
key = sourceKey child;
value = child;
}) (builtins.filter (child: includeBuild || child.kind != "build") entry.value.children);
}
);
capabilityFlatten = resourceClosure false;
# Frontends are an explicit build choice. The capability assembler itself
# neither discovers React contracts nor loads executable package code.
queryFrontends =
node:
pkgs.lib.unique (
pkgs.lib.concatMap (
node:
let
config = node.directory + "/quixos.check.json";
in
if builtins.pathExists config then
(builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ]
else
[ ]
) (capabilityFlatten node)
);
queryGeneratorFlags =
node:
if queryFrontends node == [ ] then
""
else if queryFrontends node == [ "react" ] then
"--query-generator ${protocol}/libexec/quixos-protocol/react-query-generator.mjs"
else
throw "Unsupported query frontend selection: ${builtins.toJSON (queryFrontends node)}";
snapshots =
node:
pkgs.writeText "qx-nix-source-graph.json" (
builtins.toJSON { builtins.toJSON {
resources = map (node: { resources = map (node: {
inherit (node) inherit (node)
@@ -57,44 +126,194 @@ let
commit commit
directory directory
; ;
}) (builtins.filter (node: node.kind != "workspace") nodes); }) (builtins.filter (entry: entry.kind != "workspace") (resourceClosure true node));
} }
); );
compile = # Each resource is checked once per exact source/closure/frontend/toolchain.
# Composition imports these compiler outputs; authoring receipts are never
# build inputs. Missing entries in ordinary CLI use take the full check path.
compiledNodes = builtins.mapAttrs (_: node: compileNode node) nodeTable;
compile = node: compiledNodes.${sourceKey node};
checkedResources =
node: node:
pkgs.runCommand "qx-${node.kind}-contract" { } '' pkgs.writeText "qx-checked-resources.json" (
builtins.toJSON (
map
(dependency: {
inherit (dependency) kind repository commit;
artifact = compile dependency;
})
(
builtins.filter (
dependency:
dependency.kind != "workspace"
&& dependency.kind != "build"
&& sourceKey dependency != sourceKey node
) (resourceClosure true node)
)
)
);
compileNode =
node:
pkgs.runCommand "qx-${node.kind}-contract" { QUIXOS_TRACE_PROFILE = "1"; } ''
mkdir -p "$out" mkdir -p "$out"
${ ${
if node.kind == "workspace" then if node.kind == "workspace" then
'' ''
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \ ${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
${queryGeneratorFlags root} \
--checked-resources ${checkedResources node} \
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \ --source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} \ --checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} \
--graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" > "$out/candidate.json" --graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" \
${
pkgs.lib.optionalString (execution && kind == "workspace")
''--execution-plan-out "$out/execution-plan.json" --build-contexts-out "$out/build-contexts.json"''
} > "$out/candidate.json"
'' ''
else else
'' ''
${protocol}/bin/quixos-resource-compile --root ${node.directory} \ ${protocol}/bin/quixos-resource-compile --root ${node.directory} \
${queryGeneratorFlags root} \
--checked-resources ${checkedResources node} \
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \ --kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
--commit ${pkgs.lib.escapeShellArg node.commit} \ --commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} --snapshot-only true \ --checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} --snapshot-only true \
--graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json" --graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json"
'' ''
} }
''; '';
contract = compile root; contract = compile root;
# Parse shared workspace outputs once. Repeating fromJSON in every package
# makes composition quadratic in package count and in the serialized world.
workspacePackages = builtins.listToAttrs (
map
(pkg: {
name = sourceKey (pkg.source // { kind = "package"; });
value = pkg;
})
(builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json"))
.definition.bindingSchema.packages
);
workspaceContexts = builtins.fromJSON (builtins.readFile "${contract}/build-contexts.json");
buildDependencies =
node:
builtins.listToAttrs (
map (dependency: {
name = dependency.binding;
value = {
src = dependency.directory;
source = { inherit (dependency) repository commit; };
flake = builtins.getFlake "git+${dependency.repository}?rev=${dependency.commit}&ref=refs/tags/quixos-reachability/${dependency.commit}";
};
}) (builtins.filter (dependency: dependency.kind == "build") node.children)
);
reactContracts =
node: moduleInterfaces:
let
interfaces = builtins.attrValues (
builtins.listToAttrs (
map (dependency: {
name = sourceKey dependency;
value = dependency;
}) (builtins.filter (dependency: dependency.kind == "interface") (capabilityFlatten node))
)
);
in
pkgs.lib.concatMap (
dependency:
if !builtins.pathExists (dependency.directory + "/flake.nix") then
[ ]
else
let
source = builtins.getFlake "git+${dependency.repository}?rev=${dependency.commit}&ref=refs/tags/quixos-reachability/${dependency.commit}";
build = source.quixosInterfaces.${system}.reactTypes or null;
in
if build == null then
[ ]
else if
!(builtins.any (
entry:
let
resource = builtins.fromJSON (builtins.readFile "${compile dependency}/candidate.json");
in
entry.revisionId == resource.revision.revisionId
|| (entry.definitionId or null) == resource.revision.revisionId
) moduleInterfaces)
then
[ ]
else
[
(build {
interface = "${compile dependency}/candidate.json";
generator = protocol;
typescript = runtimePackages.camino-replica-core.dependencies;
reactSdk = runtimePackages.camino-react;
dependencies = buildDependencies dependency;
})
]
) interfaces;
checkPackage = checkPackage =
node: node:
let let
compiled = compile node; compiled = compile node;
candidate = builtins.fromJSON (builtins.readFile "${compiled}/candidate.json"); candidate =
if execution && kind == "workspace" then
{
revision =
workspacePackages.${sourceKey node}
or (throw "Source package missing from workspace compilation: ${node.repository}");
}
else
builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
buildContext = import ./package-context.nix {
inherit pkgs protocol;
contexts = if kind == "workspace" then "${contract}/build-contexts.json" else null;
contextData =
if execution && kind == "workspace" then
workspaceContexts.${candidate.revision.revisionId}
else
null;
plan = "${compiled}/bindings.json";
packageRevisionId = candidate.revision.revisionId;
};
buildContract = builtins.fromJSON (builtins.readFile buildContext.buildContract);
componentEntries = if execution then buildContract.definition.package.components or [ ] else [ ];
package = builtins.getFlake ( package = builtins.getFlake (
"git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}" "git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}"
); );
checked = checked =
if execution then
package.quixosPackages.${system}.checkedArtifacts
or (throw "Package ${node.repository} lacks checkedArtifacts; execution workspaces cannot import legacy servers.")
else
package.quixosPackages.${system}.checkedServer package.quixosPackages.${system}.checkedServer
or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold."); or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
artifact = checked { artifact = checked (
if execution then
buildContext
// {
generator = protocol;
core = runtimePackages.camino-replica-core;
sharedRuntime = runtimePackages.camino-shared-runtime;
typescript = runtimePackages.camino-replica-core.dependencies;
packageRevisionId = candidate.revision.revisionId;
dependencies = buildDependencies node;
buildEnvironment = {
inherit pkgs system;
nixpkgsSource = toString pkgs.path;
narHash = "@nixpkgsNarHash@";
};
}
// pkgs.lib.optionalAttrs (componentEntries != [ ]) {
react = {
sdk = runtimePackages.camino-react;
runtime = runtimePackages.camino-react-runtime;
contracts = reactContracts node buildContract.definition.moduleInterfaces;
};
}
else
{
schema = schema =
if kind == "workspace" then if kind == "workspace" then
pkgs.writeText "package-specialization-schema.json" ( pkgs.writeText "package-specialization-schema.json" (
@@ -106,7 +325,8 @@ let
"${compiled}/bindings.json"; "${compiled}/bindings.json";
generator = protocol; generator = protocol;
packageRevisionId = candidate.revision.revisionId; packageRevisionId = candidate.revision.revisionId;
}; }
);
in in
{ {
packageRevisionId = candidate.revision.revisionId; packageRevisionId = candidate.revision.revisionId;
@@ -116,7 +336,20 @@ let
if contractOnly then if contractOnly then
[ ] [ ]
else else
map checkPackage (builtins.filter (node: node.kind == "package") nodes); map checkPackage (
builtins.filter
(node: node.kind == "package" && (kind == "workspace" || sourceKey node == sourceKey root))
(
builtins.attrValues (
builtins.listToAttrs (
map (node: {
name = sourceKey node;
value = node;
}) (capabilityFlatten root)
)
)
)
);
manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks); manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks);
bindingOptions = pkgs.writeText "qx-typescript-options.json" ( bindingOptions = pkgs.writeText "qx-typescript-options.json" (
builtins.toJSON ( builtins.toJSON (
@@ -126,13 +359,45 @@ let
in in
pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } '' pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } ''
mkdir -p "$out" mkdir -p "$out"
cp ${
builtins.toFile "qx-candidate-source.json" (builtins.toJSON { inherit repository commit kind; })
} "$out/source.json"
cp ${contract}/candidate.json "$out/candidate.json" cp ${contract}/candidate.json "$out/candidate.json"
cp ${contract}/graph.json "$out/graph.json" cp ${contract}/graph.json "$out/graph.json"
cp ${manifest} "$out/checks.json" cp ${manifest} "$out/checks.json"
${pkgs.lib.optionalString (execution && kind == "workspace") ''
${
if builtins.pathExists "${root.directory}/quixos.entry.json" then
''
cp ${root.directory}/quixos.entry.json "$out/entry.json"
''
else
''
echo null > "$out/entry.json"
''
}
cp ${contract}/execution-plan.json "$out/execution-plan.json"
${pkgs.lib.optionalString (!contractOnly) ''
${pkgs.nodejs_24}/bin/node ${./check-execution.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs \
"$out/execution-plan.json" "$out/checks.json" "$out"
mkdir -p "$out/bin"
cat > "$out/bin/run-isolated" <<EOF
#!${pkgs.runtimeShell}
exec ${runtimePackages.camino-isolated-workspace}/bin/camino-isolated-workspace "$out" "\$@"
EOF
chmod +x "$out/bin/run-isolated"
''}
''}
${pkgs.lib.optionalString ( ${pkgs.lib.optionalString (
kind != "workspace" kind != "workspace"
) ''cp ${contract}/bindings.json "$out/bindings.json"''} ) ''cp ${contract}/bindings.json "$out/bindings.json"''}
${pkgs.lib.optionalString (kind == "package") '' ${pkgs.lib.optionalString (execution && kind == "package" && !contractOnly) ''
${pkgs.nodejs_24}/bin/node ${./check-package.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${contract}/bindings.json \
"$out/checks.json" "$out"
''}
${pkgs.lib.optionalString (kind == "package" && !execution) ''
${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \ ${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \ ${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
"$out/bindings.ts" ${bindingOptions} "$out/bindings.ts" ${bindingOptions}
+16
View File
@@ -0,0 +1,16 @@
# Lock parsing only reads this one file when no import token can occur. A
# conservative substring probe also sends comments/URLs containing "import"
# through the complete-source path. Fragment names may have any extension.
{ source }:
let
regular = ((builtins.readDir source)."quixos.lock" or null) == "regular";
text = if regular then builtins.readFile (source + "/quixos.lock") else "import";
in
if builtins.replaceStrings [ "import" ] [ "" ] text != text then
source
else
builtins.path {
path = source;
name = "qx-lock-source";
filter = filename: _: filename == toString source || filename == "${source}/quixos.lock";
}
+42
View File
@@ -0,0 +1,42 @@
{ pkgs, protocol }:
let
base = ''quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "1111111111111111111111111111111111111111"; } }'';
fixture =
extra: lock:
pkgs.runCommand "qx-lock-source-fixture" { } ''
mkdir -p "$out/nested"
printf '%s' ${pkgs.lib.escapeShellArg lock} > "$out/quixos.lock"
printf '%s' ${pkgs.lib.escapeShellArg extra} > "$out/implementation.ts"
printf '%s' 'quixos-lock fragment version 1 {}' > "$out/nested/dependencies.txt"
'';
first = fixture "first implementation" base;
edited = fixture "edited implementation" base;
changed = fixture "first implementation" (
builtins.replaceStrings
[ "1111111111111111111111111111111111111111" ]
[ "2222222222222222222222222222222222222222" ]
base
);
fragments = fixture "first implementation" (
builtins.replaceStrings [ "quixos source" ] [ ''import "nested/dependencies.txt"; quixos source'' ]
base
);
conservative = fixture "first implementation" (base + "\n// import in a comment\n");
slice = source: import ./lock-source.nix { inherit source; };
parse =
source:
pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${slice source}/quixos.lock > "$out"
'';
in
assert slice first == slice edited;
assert (parse first).drvPath == (parse edited).drvPath;
assert slice first != slice changed;
pkgs.runCommand "qx-lock-source-check" { } ''
${protocol}/bin/quixos-lock-check ${slice first}/quixos.lock > first.json
${protocol}/bin/quixos-lock-check ${slice edited}/quixos.lock > edited.json
cmp first.json edited.json
${protocol}/bin/quixos-lock-check ${slice conservative}/quixos.lock > comment.json
cmp first.json comment.json
${protocol}/bin/quixos-lock-check ${slice fragments}/quixos.lock > "$out"
''
+15
View File
@@ -0,0 +1,15 @@
import fs from "node:fs/promises";
import { pathToFileURL } from "node:url";
const compiler = await import(pathToFileURL(process.argv[2]));
const input = JSON.parse(await fs.readFile(process.argv[3], "utf8"));
const revision = process.argv[4];
const schema = input.definition?.bindingSchema ?? input;
if (input.definition && compiler.hexDigest(input.definition) !== input.digest) throw Error("Invalid execution plan");
const pkg = schema.packages.find((entry) => entry.revisionId === revision);
if (!pkg) throw Error("Unknown build package " + revision);
process.stdout.write(
JSON.stringify({
contract: compiler.packageBuildContract(schema, revision),
sourceDigest: compiler.hexDigest({ revision, source: pkg.source }),
}),
);
+33
View File
@@ -0,0 +1,33 @@
# Workspace composition may reevaluate this projection after any graph edit.
# Only the canonical, source-free value becomes a package compilation input.
{
pkgs,
protocol,
plan ? null,
contexts ? null,
contextData ? null,
packageRevisionId,
}:
let
projected = pkgs.runCommand "camino-package-context.json" { } ''
${pkgs.nodejs_24}/bin/node ${./package-context.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${plan} \
${pkgs.lib.escapeShellArg packageRevisionId} > "$out"
'';
context =
if contextData != null then
contextData
else if contexts == null then
builtins.fromJSON (builtins.readFile projected)
else
(builtins.fromJSON (builtins.readFile contexts)).${packageRevisionId};
in
{
# This is typed semantic data (nominal IDs, types and generated source), never
# Nix artifact paths. Remove the projection derivation's string context, not
# any code/toolchain dependency. Those are separate explicit helper arguments.
buildContract = builtins.toFile "camino-package-contract.json" (
builtins.unsafeDiscardStringContext (builtins.toJSON context.contract)
);
sourceDigest = builtins.unsafeDiscardStringContext context.sourceDigest;
}
+3 -1
View File
@@ -6,6 +6,7 @@
"type": "module", "type": "module",
"bin": { "bin": {
"quixos-capability-compile": "dist/src/capability-language/cli.js", "quixos-capability-compile": "dist/src/capability-language/cli.js",
"quixos-codegen-client": "dist/src/bindings/portable-client-cli.js",
"quixos-codegen-ts": "dist/src/bindings/cli.js", "quixos-codegen-ts": "dist/src/bindings/cli.js",
"quixos-descriptor-check": "dist/src/descriptor-check.js", "quixos-descriptor-check": "dist/src/descriptor-check.js",
"quixos-lock-check": "dist/src/resource-lock/cli.js", "quixos-lock-check": "dist/src/resource-lock/cli.js",
@@ -18,7 +19,8 @@
"./capability-model": "./dist/src/capability-model/index.js", "./capability-model": "./dist/src/capability-model/index.js",
"./capability-language": "./dist/src/capability-language/index.js", "./capability-language": "./dist/src/capability-language/index.js",
"./resource-lock": "./dist/src/resource-lock/index.js", "./resource-lock": "./dist/src/resource-lock/index.js",
"./gen/*": "./dist/src/gen/*" "./gen/*": "./dist/src/gen/*",
"./execution-world": "./dist/src/execution-world/index.js"
}, },
"scripts": { "scripts": {
"build": "yarn generate && tsc -p tsconfig.build.json", "build": "yarn generate && tsc -p tsconfig.build.json",
+6
View File
@@ -241,7 +241,10 @@ message CaminoOp {
message QueryRequest { message QueryRequest {
string query_id = 1; string query_id = 1;
oneof root {
string object_id = 2; string object_id = 2;
quixos.WorkspaceQueryScope workspace_scope = 5;
}
map<string, Value> variables = 3; map<string, Value> variables = 3;
// Typed callers require this exact checked definition. Administrative/raw // Typed callers require this exact checked definition. Administrative/raw
// callers may omit it to explicitly select the currently installed definition. // callers may omit it to explicitly select the currently installed definition.
@@ -351,7 +354,10 @@ message QueryFieldFailure {
} }
message QueryChangesRequest { message QueryChangesRequest {
string query_id = 1; string query_id = 1;
oneof root {
string object_id = 2; string object_id = 2;
quixos.WorkspaceQueryScope workspace_scope = 4;
}
string data_version = 3; string data_version = 3;
} }
message QueryChangesResponse { message QueryChangesResponse {
+29 -1
View File
@@ -125,6 +125,7 @@ message QueryRelationPath {
message QueryFieldOperand { message QueryFieldOperand {
string interface_revision_id = 1; string interface_revision_id = 1;
string member_id = 2; string member_id = 2;
repeated string value_path = 3;
} }
message QueryExpression { message QueryExpression {
repeated QueryPathStep path = 1; repeated QueryPathStep path = 1;
@@ -165,6 +166,7 @@ enum QueryComparisonOperator {
QUERY_COMPARISON_OPERATOR_LTE = 5; QUERY_COMPARISON_OPERATOR_LTE = 5;
QUERY_COMPARISON_OPERATOR_GT = 6; QUERY_COMPARISON_OPERATOR_GT = 6;
QUERY_COMPARISON_OPERATOR_GTE = 7; QUERY_COMPARISON_OPERATOR_GTE = 7;
QUERY_COMPARISON_OPERATOR_CONTAINS_INSENSITIVE = 8;
} }
message QueryComparison { message QueryComparison {
QueryOperand operand = 1; QueryOperand operand = 1;
@@ -194,6 +196,10 @@ message QueryReductionPredicate {
QueryPredicate where = 3; QueryPredicate where = 3;
QueryPredicate having = 4; QueryPredicate having = 4;
} }
message QueryInterfacePredicate {
string interface_revision_id = 1;
QueryPredicate predicate = 2;
}
message QueryPredicate { message QueryPredicate {
oneof predicate { oneof predicate {
QueryPredicateList and = 1; QueryPredicateList and = 1;
@@ -202,6 +208,7 @@ message QueryPredicate {
QueryComparison compare = 4; QueryComparison compare = 4;
QueryRelationPredicate relation = 5; QueryRelationPredicate relation = 5;
QueryReductionPredicate reduce = 6; QueryReductionPredicate reduce = 6;
QueryInterfacePredicate as = 7;
} }
} }
message QueryRow { message QueryRow {
@@ -286,6 +293,15 @@ message QueryReadBinding {
Cardinality cardinality = 13; Cardinality cardinality = 13;
string key_type = 14; string key_type = 14;
} }
// Checked schema survives even when no atom currently implements an interface.
message QueryFieldContract {
string interface_revision_id = 1;
string member_id = 2;
string field_name = 3;
string value_type_json = 4;
Cardinality cardinality = 5;
string key_type = 6;
}
message QueryBudgets { message QueryBudgets {
uint32 rows = 1; uint32 rows = 1;
uint32 depth = 2; uint32 depth = 2;
@@ -295,11 +311,20 @@ message QueryBudgets {
uint32 concurrency = 6; uint32 concurrency = 6;
uint32 deadline_ms = 7; uint32 deadline_ms = 7;
} }
message QueryPlacement {
oneof kind {
bool replica = 1;
string local_source_query_id = 2;
}
}
message InstalledQuery { message InstalledQuery {
string id = 1; string id = 1;
string definition_digest = 2; string definition_digest = 2;
string binding_digest = 3; string binding_digest = 3;
string root_interface_revision_id = 4; oneof root {
string object_interface = 4;
string workspace_interface = 14;
}
repeated QuerySelection selection = 5; repeated QuerySelection selection = 5;
repeated QueryReadBinding bindings = 6; repeated QueryReadBinding bindings = 6;
QueryBudgets budgets = 7; QueryBudgets budgets = 7;
@@ -309,4 +334,7 @@ message InstalledQuery {
bool watch = 11; bool watch = 11;
uint32 polling_interval_ms = 12; uint32 polling_interval_ms = 12;
bool rpc_predicate_or_order = 13; bool rpc_predicate_or_order = 13;
repeated string root_atom_ids = 15;
repeated QueryFieldContract fields = 16;
QueryPlacement placement = 17;
} }
+80
View File
@@ -9,6 +9,8 @@ import "quixos/refs.proto";
import "quixos/runtime.proto"; import "quixos/runtime.proto";
service OrchestratorRuntime { service OrchestratorRuntime {
rpc ReadDefinitions(DefinitionRequest) returns (DefinitionPage);
rpc WatchDefinitions(DefinitionRequest) returns (stream DefinitionPage);
rpc ExecuteQuery(camino.QueryRequest) returns (camino.QueryResponse); rpc ExecuteQuery(camino.QueryRequest) returns (camino.QueryResponse);
rpc WatchQuery(camino.QueryRequest) returns (stream QueryEvent); rpc WatchQuery(camino.QueryRequest) returns (stream QueryEvent);
rpc TryConform(TryConformRequest) returns (TryConformResponse); rpc TryConform(TryConformRequest) returns (TryConformResponse);
@@ -26,6 +28,83 @@ service OrchestratorRuntime {
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse); rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
} }
// Activated definitions, not runtime metrics or object data. Nested type JSON
// uses the compiler's canonical ValueType/InterfaceRevision public contracts.
message DefinitionSelector {
string kind = 1; // active | atoms | interfaces | conformances | queries | indexes
repeated string ids = 2;
string atom_id = 3;
string interface_revision_id = 4;
string package_revision_id = 5;
}
message DefinitionIdentity {
string workspace_id = 1;
string workspace_revision_id = 2;
string source_root_commit = 3;
string workspace_epoch = 4;
string definition_digest = 5;
}
message DefinitionRequest {
DefinitionSelector selector = 1;
uint32 first = 2; // 1..100; 0 uses 50
string after = 3;
string snapshot = 4; // issued definition_digest; current serving snapshot only
string workspace_id = 5; // required for injected package authority
string workspace_epoch = 6; // required for invocation-bound package calls
}
message ReflectedAtom {
string id = 1;
string display_name = 2;
string constructor_input_type_json = 3;
}
message ReflectedInterface {
string id = 1;
string interface_id = 2;
string display_name = 3;
string contract_json = 4; // canonical interface contract, excluding source location
}
message ReflectedConformance {
string id = 1;
string atom_id = 2;
string interface_revision_id = 3;
string definition_id = 4;
repeated string operation_ids = 5;
repeated ClassCapability class_capabilities = 6;
bool instance = 7;
}
message ReflectedQuery {
string id = 1;
string name = 2;
string package_revision_id = 3;
string definition_digest = 4;
string binding_digest = 5;
string root_kind = 6;
string interface_revision_id = 7;
string variables_type_json = 8;
string output_type_json = 9;
string effects_json = 10;
bool watch = 11;
WorkspaceQueryScope workspace_scope = 12;
}
message ReflectedIndex {
string id = 1;
string contract_json = 2;
}
message DefinitionRecord {
oneof definition {
ReflectedAtom atom = 1;
ReflectedInterface interface = 2;
ReflectedConformance conformance = 3;
ReflectedQuery query = 4;
ReflectedIndex index = 5;
}
}
message DefinitionPage {
DefinitionIdentity identity = 1;
repeated DefinitionRecord records = 2;
string next_cursor = 3;
}
message QueryEvent { message QueryEvent {
string run_id = 1; string run_id = 1;
uint64 sequence = 2; uint64 sequence = 2;
@@ -145,6 +224,7 @@ message QueryDescription {
repeated string source_files = 6; repeated string source_files = 6;
string effects_json = 7; string effects_json = 7;
camino.InstalledQuery plan = 8; camino.InstalledQuery plan = 8;
WorkspaceQueryScope workspace_scope = 9;
} }
message ClassCapability { message ClassCapability {
string conformance_id = 1; string conformance_id = 1;
+22
View File
@@ -44,12 +44,34 @@ message InjectedDependency {
string interface_revision_id = 4; string interface_revision_id = 4;
string constructor_atom_id = 5; string constructor_atom_id = 5;
string query_id = 7; string query_id = 7;
WorkspaceQueryDependency workspace_query = 8;
ReflectionDependency reflection = 9;
} }
// Defaults to the invocation receiver. A checked dependency traversal can // Defaults to the invocation receiver. A checked dependency traversal can
// select a related object explicitly before the package runtime starts. // select a related object explicitly before the package runtime starts.
string object_id = 6; string object_id = 6;
} }
message ReflectionDependency {
string workspace_id = 1;
string workspace_epoch = 2;
repeated string selectors = 3;
}
// Scope evidence is not authority by itself. The caller must also hold the
// matching workspace-query grant (or the authenticated workspace user grant).
message WorkspaceQueryScope {
string workspace_id = 1;
string workspace_revision_id = 2;
string workspace_epoch = 3;
string interface_revision_id = 4;
string binding_digest = 5;
}
message WorkspaceQueryDependency {
string query_id = 1;
WorkspaceQueryScope scope = 2;
}
message EdgeDependency { message EdgeDependency {
string edge_type_id = 1; string edge_type_id = 1;
string projection_id = 2; string projection_id = 2;
+22 -2
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises"; import { readFile, writeFile } from "node:fs/promises";
import { generateTypeScriptBindings } from "./index.js"; import {
generateTypeScriptBindings,
generatePortableTypeScriptBindings,
generateRemoteTypeScriptBindings,
} from "./index.js";
import path from "node:path"; import path from "node:path";
import { reactPlatformTypes } from "./react-platform.js"; import { reactPlatformTypes } from "./react-platform.js";
import { generateReactBindings } from "./react.js"; import { generateReactBindings } from "./react.js";
@@ -11,7 +15,23 @@ const main = async () => {
throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]"); throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
const config = options ? JSON.parse(await readFile(options, "utf8")) : {}; const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
const contracts = JSON.parse(await readFile(schema, "utf8")); const contracts = JSON.parse(await readFile(schema, "utf8"));
const generated = generateTypeScriptBindings(contracts, revision, config); if ([config.portable, config.remote, config.react].filter(Boolean).length > 1)
throw new Error("Portable, remote and React bindings are separate artifacts");
if (config.remote !== undefined && config.remote !== true) throw new Error("Remote binding mode must be true");
if (config.portable && typeof config.portable.target !== "string")
throw new Error("Portable bindings require an explicit target");
const generated = config.remote
? generateRemoteTypeScriptBindings(
contracts,
revision,
config.runtimeModule ? { runtimeModule: config.runtimeModule } : {},
)
: config.portable
? generatePortableTypeScriptBindings(contracts, revision, {
...config.portable,
...(config.runtimeModule ? { runtimeModule: config.runtimeModule } : {}),
})
: generateTypeScriptBindings(contracts, revision, config);
await writeFile(output, generated); await writeFile(output, generated);
if (config.react) { if (config.react) {
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes); await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
+4
View File
@@ -11,6 +11,8 @@ export const generateClientContracts = (
throw new Error("Host contracts require closed interface applications, not generic definitions"); throw new Error("Host contracts require closed interface applications, not generic definitions");
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
return value.name === "unit" ? "undefined" : "string"; return value.name === "unit" ? "undefined" : "string";
case "scalar": case "scalar":
@@ -28,6 +30,8 @@ export const generateClientContracts = (
return qualified return qualified
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>` ? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
: `{readonly $quixosRef: string}`; : `{readonly $quixosRef: string}`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
+14 -1
View File
@@ -49,6 +49,8 @@ export const genericImplementationType = (
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => { const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit"); if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
switch (type.kind) { switch (type.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "parameter": { case "parameter": {
const bound = scope.arguments.get(type.parameterId); const bound = scope.arguments.get(type.parameterId);
if (bound) return bound; if (bound) return bound;
@@ -60,6 +62,8 @@ export const genericImplementationType = (
return `QxObjectRef<${target(type.expectation, scope)}>`; return `QxObjectRef<${target(type.expectation, scope)}>`;
case "record": case "record":
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)])); return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${value(type.value, scope, depth + 1)} | null)`; return `(${value(type.value, scope, depth + 1)} | null)`;
case "list": case "list":
@@ -79,6 +83,8 @@ export const genericImplementationType = (
const requirement = entry.requirement, const requirement = entry.requirement,
scope = rootScope(); scope = rootScope();
switch (requirement.kind) { switch (requirement.kind) {
case "reflection":
return `QxReflectionPort<${requirement.selectors.map((value) => JSON.stringify(value)).join(" | ")}>`;
case "state": case "state":
return object([ return object([
...requirement.primitives.map((primitive): [string, string] => { ...requirement.primitives.map((primitive): [string, string] => {
@@ -113,6 +119,8 @@ export const genericImplementationType = (
} }
case "query": case "query":
return queryPort(requirement); return queryPort(requirement);
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor": case "constructor":
return object([ return object([
[ [
@@ -129,7 +137,12 @@ export const genericImplementationType = (
}; };
const operations = (contract.template?.members ?? contract.members).flatMap((member) => const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
member.operations member.operations
.filter((op) => op.mode === "call" && op.scope !== "class") .filter(
(op) =>
op.mode === "call" &&
op.scope !== "class" &&
(!requirement.operations || requirement.operations.includes(op.id)),
)
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })), .map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
); );
const methods: [string, string][] = [ const methods: [string, string][] = [
+47 -7
View File
@@ -9,6 +9,14 @@ import type { CompiledCapabilityResourceRepository } from "../capability-languag
import { genericImplementationType } from "./generics.js"; import { genericImplementationType } from "./generics.js";
import { capabilityId } from "../capability-model/types.js"; import { capabilityId } from "../capability-model/types.js";
/** Source-free package compilation inputs. Nominal references do not import
* implementations or concrete workspace conformances. */
export type BindingContractSchema = {
format: "quixos-bindings";
version: 1;
interfaces: Omit<InterfaceRevision, "source">[];
packages: Pick<PackageRevision, "revisionId" | "exports" | "genericExports">[];
};
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */ /** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = { export type BindingSchema = {
format: "quixos-bindings"; format: "quixos-bindings";
@@ -17,24 +25,29 @@ export type BindingSchema = {
interfaceTemplates?: InterfaceRevision[]; interfaceTemplates?: InterfaceRevision[];
packages: PackageRevision[]; packages: PackageRevision[];
}; };
export const bindingSchema = (compiled: Pick<CompiledCapabilityResourceRepository, "resources">): BindingSchema => ({ export const bindingSchema = (
compiled: Pick<CompiledCapabilityResourceRepository, "resources"> &
Partial<Pick<CompiledCapabilityResourceRepository, "capabilityResources">>,
): BindingSchema => ({
format: "quixos-bindings", format: "quixos-bindings",
version: 1, version: 1,
interfaceTemplates: compiled.resources.flatMap((node) => interfaceTemplates: (compiled.capabilityResources ?? compiled.resources).flatMap((node) =>
node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [], node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [],
), ),
interfaces: [ interfaces: [
...new Map( ...new Map(
compiled.resources (compiled.capabilityResources ?? compiled.resources)
.flatMap((node) => [ .flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []), ...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.specializations ?? []), ...(node.resource.kind === "build" ? [] : (node.resource.specializations ?? [])),
]) ])
.filter((entry) => !entry.template) .filter((entry) => !entry.template)
.map((entry) => [entry.revisionId, entry]), .map((entry) => [entry.revisionId, entry]),
).values(), ).values(),
], ],
packages: compiled.resources.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])), packages: (compiled.capabilityResources ?? compiled.resources).flatMap((node) =>
node.resource.kind === "package" ? [node.resource.revision] : [],
),
}); });
export const specializeBindingSchema = ( export const specializeBindingSchema = (
@@ -107,6 +120,8 @@ export const generateTypeScriptBindings = (
const messages = new Map<string, string>(); const messages = new Map<string, string>();
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
return value.name === "unit" ? "null" : "QxWatchHandle"; return value.name === "unit" ? "null" : "QxWatchHandle";
case "scalar": case "scalar":
@@ -122,6 +137,8 @@ export const generateTypeScriptBindings = (
}[value.name]; }[value.name];
case "object-ref": case "object-ref":
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`; return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
@@ -144,6 +161,11 @@ export const generateTypeScriptBindings = (
const port = (entry: DependencyPort): { type: string; spec: unknown } => { const port = (entry: DependencyPort): { type: string; spec: unknown } => {
const requirement = entry.requirement; const requirement = entry.requirement;
switch (requirement.kind) { switch (requirement.kind) {
case "reflection":
return {
type: `QxReflectionPort<${requirement.selectors.map((value) => q(value)).join(" | ")}>`,
spec: { kind: "reflection", id: entry.id, selectors: requirement.selectors },
};
case "query": { case "query": {
const query = schema.packages const query = schema.packages
.find((candidate) => candidate.revisionId === requirement.packageRevisionId) .find((candidate) => candidate.revisionId === requirement.packageRevisionId)
@@ -208,7 +230,12 @@ export const generateTypeScriptBindings = (
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today. // Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
const operations = contract.members.flatMap((member) => const operations = contract.members.flatMap((member) =>
member.operations member.operations
.filter((operation) => operation.mode === "call" && operation.scope !== "class") .filter(
(operation) =>
operation.mode === "call" &&
operation.scope !== "class" &&
(!requirement.operations || requirement.operations.includes(operation.id)),
)
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })), .map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
); );
const methods: [string, string][] = [ const methods: [string, string][] = [
@@ -239,6 +266,8 @@ export const generateTypeScriptBindings = (
}, },
}; };
} }
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor": { case "constructor": {
const input = requirement.inputType; const input = requirement.inputType;
if (!input) if (!input)
@@ -321,6 +350,7 @@ export const generateTypeScriptBindings = (
// A used port still fails above if its own required codec is missing. // A used port still fails above if its own required codec is missing.
const hasCodec = (value: ValueType): boolean => { const hasCodec = (value: ValueType): boolean => {
if (value.kind === "message") return !!options.messages?.[value.descriptorId]; if (value.kind === "message") return !!options.messages?.[value.descriptorId];
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every(hasCodec); if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value); if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
return true; return true;
@@ -378,6 +408,7 @@ export const generateTypeScriptBindings = (
"RelationshipCollection", "RelationshipCollection",
"RelationshipEntry", "RelationshipEntry",
"QxQuerySnapshot", "QxQuerySnapshot",
"QxReflectionPort",
"QxQueryDescriptor", "QxQueryDescriptor",
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures)); ].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
return ( return (
@@ -388,7 +419,7 @@ export const generateTypeScriptBindings = (
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` + `declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` + `export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
`export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` + `export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` +
`export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, rootInterfaceRevisionId: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` + `export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, root: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` + `export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
`export const interfaces = {${contracts `export const interfaces = {${contracts
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1) .filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
@@ -406,3 +437,12 @@ export const generateTypeScriptBindings = (
`\n },\n});\n` `\n },\n});\n`
); );
}; };
export {
generatePortableTypeScriptBindings,
generateRemoteTypeScriptBindings,
portableValueSchema,
PortableBindingError,
type PortableBindingOptions,
} from "./portable.js";
export * from "./portable-client.js";
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import { generatePortableClientBindings } from "./portable-client.js";
async function main() {
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 }));
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
+128
View File
@@ -0,0 +1,128 @@
import { compileKernelProgram } from "../execution-world/kernel.js";
import { hexDigest, type ExecutionPlan } from "../execution-world/plan.js";
import { resultInterfaces } from "../execution-world/projections.js";
import { portableValueSchema, tsType, PortableBindingError } from "./portable.js";
import type { QuerySelection } from "../query/types.js";
const conditional = (selections: readonly QuerySelection[]): boolean =>
selections.some((selection) => selection.conditions.length > 0 || conditional(selection.selection));
/** Closed-world app descriptors. Handler bindings and application acquisition share the same public identities. */
export function generatePortableClientBindings(
plan: ExecutionPlan,
options: { target: string; runtimeModule?: string },
): string {
if (hexDigest(plan.definition) !== plan.digest) throw new PortableBindingError("client", "Invalid plan digest");
const executionPlan = (compileKernelProgram(plan) as { plan: string }).plan;
const workspace = plan.definition.workspace,
atoms = new Map<string, string[]>(),
unavailable: { key: string; reason: string }[] = [];
const seen = new Set<string>();
for (const operation of plan.definition.operations) {
if (operation.execution.kind === "remote" || !operation.availableTargets.includes(options.target)) {
unavailable.push({ key: operation.key, reason: "OPERATION_NOT_PORTABLE" });
continue;
}
let input: string, output: string;
try {
input = tsType(operation.input);
output = tsType(operation.output);
} catch (error) {
if (!(error instanceof PortableBindingError)) throw error;
unavailable.push({ key: operation.key, reason: "UNSUPPORTED_OPERATION_CODEC" });
continue;
}
const atom = workspace.atoms.find((a) => a.id === operation.atomId)!;
const iface = workspace.interfaceImports.find((i) => i.revisionId === operation.interfaceRevisionId)!;
const member = iface.members.find((m) => m.operations.some((o) => o.id === operation.operationId))!;
const facet = member.operations.find((o) => o.id === operation.operationId)!;
const name = `${iface.displayName}.${member.displayName}.${facet.displayName}`;
const alias = JSON.stringify([atom.displayName, name]);
if (seen.has(alias)) throw new PortableBindingError(alias, "Ambiguous application descriptor alias");
seen.add(alias);
const roots = resultInterfaces(plan, operation.key);
const descriptor = {
key: operation.key,
plan: executionPlan,
effect: operation.execution.kind,
input: portableValueSchema(operation.input),
output: portableValueSchema(operation.output),
resultInterfaces: roots,
};
const list = atoms.get(atom.displayName) ?? [];
list.push(
`${JSON.stringify(name)}: defineOperationDescriptor<${input}, ${output}, ${roots.map((r) => JSON.stringify(r)).join(" | ") || "never"}, ${JSON.stringify(operation.execution.kind)}>(${JSON.stringify(descriptor)})`,
);
atoms.set(atom.displayName, list);
}
const queries = new Map<string, string[]>();
const views = new Map<string, string[]>();
const serverProjections = new Map<string, string[]>();
const results = new Map<string, string[]>();
for (const query of plan.definition.queries) {
const declaration = query.query.checked.declaration;
const pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!;
const alias = JSON.stringify([pkg.displayName, declaration.displayName]);
if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias");
seen.add(`query:${alias}`);
const addResult = () => {
if (conditional(query.query.checked.selection)) {
unavailable.push({ key: query.id, reason: "CONDITIONAL_RESULT_DECODER_UNSUPPORTED" });
return;
}
const decoders = results.get(pkg.displayName) ?? [];
decoders.push(
`${JSON.stringify(declaration.displayName)}: (data: Json): ${tsType(query.query.checked.output)} => decodeArgument(${JSON.stringify(portableValueSchema(query.query.checked.output))}, { codec: "camino-value-v1", usage: "argument", value: data }, { workspace: workspaceId(${JSON.stringify(workspace.workspaceId)}), limits: { bytes: ${declaration.budgets.resultBytes + 128}, nodes: 100000, depth: 64 } }) as ${tsType(query.query.checked.output)}`,
);
results.set(pkg.displayName, decoders);
};
if (!declaration.placement) {
const operations = query.operations.map(
(key) => plan.definition.operations.find((operation) => operation.key === key)!,
);
if (
operations.some(
(operation) => operation.execution.kind !== "snapshot" || !operation.execution.targets.includes("server"),
)
) {
unavailable.push({ key: query.id, reason: "QUERY_NOT_SNAPSHOT_PROJECTION" });
continue;
}
const list = serverProjections.get(pkg.displayName) ?? [];
list.push(
JSON.stringify(declaration.displayName) +
": " +
JSON.stringify({
kind: "server-projection",
id: query.id,
plan: executionPlan,
root: declaration.root.interface,
rootKind: declaration.root.kind,
}) +
" as const satisfies ServerProjection<" +
JSON.stringify(declaration.root.interface) +
", " +
JSON.stringify(declaration.root.kind) +
">",
);
serverProjections.set(pkg.displayName, list);
addResult();
continue;
}
if (declaration.placement.kind === "local") {
const list = views.get(pkg.displayName) ?? [];
list.push(
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: executionPlan, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
);
views.set(pkg.displayName, list);
addResult();
continue;
}
const list = queries.get(pkg.displayName) ?? [];
list.push(
`${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(executionPlan)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`,
);
queries.set(pkg.displayName, list);
}
const groups = (entries: Map<string, string[]>) =>
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type ServerProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(executionPlan)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const serverProjections = {${groups(serverProjections)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
}
+365
View File
@@ -0,0 +1,365 @@
import { portableReflectionType } from "../execution-world/reflection.js";
import type { BindingContractSchema } from "./index.js";
import type {
ValueType,
ObjectExpectation,
PackageExport,
DependencyPort,
ExecutionContract,
} from "../capability-model/types.js";
export class PortableBindingError extends Error {
constructor(
readonly path: string,
message: string,
) {
super(`${path}: ${message}`);
this.name = "PortableBindingError";
}
}
const q = JSON.stringify;
const unit: ValueType = { kind: "builtin", name: "unit" };
const string: ValueType = { kind: "scalar", name: "string" };
const bool: ValueType = { kind: "scalar", name: "bool" };
const isUnit = (t: ValueType) => t.kind === "builtin" && t.name === "unit";
const expectation = (e: ObjectExpectation) => ({
kind: e.kind,
id: e.kind === "atom" ? e.atomId : e.interfaceRevisionId,
});
/** The protocol IR and replica wire schema intentionally share structure, not reference identity. */
export function portableValueSchema(type: ValueType): unknown {
switch (type.kind) {
case "variant":
return {
kind: "variant",
id: type.id,
cases: Object.fromEntries(
Object.entries(type.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: portableValueSchema(entry.value) },
]),
),
};
case "change":
return {
kind: "record",
fields: {
codec: { kind: "scalar", name: "string" },
profile: { kind: "scalar", name: "string" },
generation: { kind: "scalar", name: "string" },
changes: { kind: "list", value: { kind: "scalar", name: "bytes" } },
},
};
case "scalar":
return type;
case "builtin":
if (type.name === "unit") return type;
break;
case "object-ref":
return { kind: type.kind, expectation: expectation(type.expectation) };
case "optional":
case "list":
return { kind: type.kind, value: portableValueSchema(type.value) };
case "record":
return {
kind: type.kind,
fields: Object.fromEntries(
Object.entries(type.fields).map(([key, value]) => [key, portableValueSchema(value)]),
),
};
}
throw new PortableBindingError(
type.kind,
"Unsupported portable value; use a supported record/reference contract (native edits and generic adapters have separate tasks)",
);
}
export function tsType(type: ValueType): string {
portableValueSchema(type);
switch (type.kind) {
case "variant":
return `(${Object.entries(type.cases)
.map(([label, entry]) => `{ readonly kind: ${q(label)}; readonly value: ${tsType(entry.value)} }`)
.join(" | ")})`;
case "change":
return `PreparedChange<${tsType(type.value)}>`;
case "scalar":
return type.name === "bool"
? "boolean"
: type.name === "string"
? "string"
: type.name === "bytes"
? "Uint8Array"
: ["int64", "uint64"].includes(type.name)
? "bigint"
: "number";
case "builtin":
return "null";
case "object-ref": {
const e = expectation(type.expectation);
return `ManagedReference<${q(e.kind)}, ${q(e.id)}>`;
}
case "optional":
return `(${tsType(type.value)} | null)`;
case "list":
return `ReadonlyArray<${tsType(type.value)}>`;
case "record":
return `{ ${Object.entries(type.fields)
.map(([key, value]) => `readonly ${q(key)}: ${tsType(value)}`)
.join("; ")} }`;
default:
throw new PortableBindingError(type.kind, "Unsupported type");
}
}
interface Method {
name: string;
portId: string;
portName: string;
action: { kind: string; primitive?: string; operationId?: string; selector?: string };
input: ValueType;
output: ValueType;
keyed: boolean;
explicitInput?: boolean;
}
function methods(
schema: BindingContractSchema,
entry: PackageExport,
port: DependencyPort,
target: string,
remote = false,
): Method[] {
const req = port.requirement;
const path = `${entry.displayName}.ports.${port.displayName}`;
const method = (
name: string,
action: Method["action"],
input: ValueType,
output: ValueType,
keyed = false,
): Method => ({ name, portId: port.id, portName: port.displayName, action, input, output, keyed });
const writable = () => {
if (remote)
throw new PortableBindingError(path, "Remote writes and creation require a named transaction capability");
if (entry.execution?.kind !== "transaction")
throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports");
};
switch (req.kind) {
case "reflection":
if (remote)
throw new PortableBindingError(
path,
"Use an ordinary portable snapshot capability to read installation metadata",
);
return req.selectors.map((selector) =>
method(selector, { kind: "reflection", selector }, unit, portableReflectionType(selector)),
);
case "creation":
writable();
return [
method(
"create",
{ kind: "creation" },
req.inputType,
{ kind: "object-ref", expectation: { kind: "atom", atomId: req.atomId } },
true,
),
];
case "state":
return req.primitives.map((primitive) => {
if (primitive === "read") return method("get", { kind: "state", primitive }, unit, req.valueType);
if (primitive === "write") {
writable();
return method("set", { kind: "state", primitive }, req.valueType, unit);
}
if (primitive === "edit") {
writable();
return method("edit", { kind: "state", primitive }, { kind: "change", value: req.valueType }, unit);
}
throw new PortableBindingError(path, `State ${primitive} is not supported by this portable adapter yet`);
});
case "edge": {
const ref: ValueType = { kind: "object-ref", expectation: req.target };
const mapKey: ValueType | undefined = req.keyType
? { kind: "scalar", name: req.keyType === "boolean" ? "bool" : req.keyType }
: undefined;
const item: ValueType = { kind: "record", fields: { entry: string, target: ref, ...(mapKey ? { mapKey } : {}) } };
const connectInput: ValueType = mapKey ? { kind: "record", fields: { mapKey, target: ref } } : ref;
return req.primitives.map((primitive) => {
if (primitive === "resolve")
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
writable();
if (primitive === "connect") return method("connect", { kind: "edge", primitive }, connectInput, string, true);
if (primitive === "disconnect") return method("disconnect", { kind: "edge", primitive }, string, bool);
throw new PortableBindingError(path, `Edge ${primitive} is not supported by this portable adapter yet`);
});
}
case "interface": {
const iface = schema.interfaces.find((i) => i.revisionId === req.interfaceRevisionId);
if (!iface) throw new PortableBindingError(path, `Missing interface ${req.interfaceRevisionId}`);
const operations = iface.members.flatMap((member) => member.operations.map((op) => ({ member, op })));
if (req.operations?.some((id) => !operations.some(({ op }) => op.id === id)))
throw new PortableBindingError(path, "Unknown selected interface operation");
return operations
.filter(({ op }) => !req.operations || req.operations.includes(op.id))
.map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if (
(execution.kind === "remote" && !remote) ||
op.mode !== "call" ||
(op.scope === "class" && (remote || req.receiver === "input"))
)
throw new PortableBindingError(
`${path}.${member.displayName}.${op.displayName}`,
`Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`,
);
if (execution.kind === "transaction" && !remote) writable();
if (req.receiver === "input" && remote)
throw new PortableBindingError(path, "Reference-selected dependencies require portable execution");
return {
...method(
`${member.displayName}.${op.displayName}`,
{ kind: "interface", operationId: op.id },
req.receiver === "input"
? {
kind: "record",
fields: {
receiver: {
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId },
},
input: op.inputType,
},
}
: op.inputType,
op.outputType,
execution.kind === "transaction",
),
...(op.scope === "class" ? { explicitInput: true } : {}),
};
});
}
default:
throw new PortableBindingError(path, `Unsupported ${req.kind} port in portable bindings`);
}
}
export interface PortableBindingOptions {
target: string;
runtimeModule?: string;
}
/** Generate one target's handlers. The same source signatures are used in browser/server builds. */
export function generatePortableTypeScriptBindings(
schema: BindingContractSchema,
revision: string,
options: PortableBindingOptions,
): string {
return generateBindings(schema, revision, options, false);
}
export function generateRemoteTypeScriptBindings(
schema: BindingContractSchema,
revision: string,
options: { runtimeModule?: string } = {},
): string {
return generateBindings(schema, revision, { ...options, target: "server" }, true);
}
function generateBindings(
schema: BindingContractSchema,
revision: string,
options: PortableBindingOptions,
remote: boolean,
): string {
const mode = remote ? "Remote" : "Portable";
const pkg = schema.packages.find((p) => p.revisionId === revision);
if (!pkg) throw new PortableBindingError(revision, "Missing package");
const entries = pkg.exports.filter((e) =>
remote
? !e.execution || e.execution.kind === "remote"
: e.execution && e.execution.kind !== "remote" && e.execution.targets.includes(options.target),
);
const names = new Set<string>();
const contexts: string[] = [],
implementations: string[] = [],
adapters: string[] = [],
descriptors: unknown[] = [];
for (const entry of entries) {
const effect = entry.execution?.kind ?? "remote";
// The checked binding schema already contains concrete generic applications.
// Each has its own identity, value codecs and specialized dependency ports.
if (entry.kind === "constructor")
throw new PortableBindingError(entry.id, "Legacy constructors are not supported; declare an ordinary factory");
if (names.has(entry.displayName)) throw new PortableBindingError(entry.id, "Duplicate handler name");
names.add(entry.displayName);
const portNames = new Set<string>();
const all: Method[] = [],
facets: string[] = [];
for (const port of entry.dependencyPorts) {
if (portNames.has(port.displayName))
throw new PortableBindingError(entry.id, `Duplicate port name ${port.displayName}`);
portNames.add(port.displayName);
const selected = methods(schema, entry, port, options.target, remote);
if (new Set(selected.map((m) => m.name)).size !== selected.length)
throw new PortableBindingError(port.id, "Duplicate method name");
all.push(...selected);
facets.push(
`readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) && !m.explicitInput ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`,
);
}
const receiver =
entry.kind === "function"
? null
: entry.receiverRequirement.kind === "exact-atom"
? { kind: "atom", id: entry.receiverRequirement.atomId }
: "object";
const receiverType =
receiver === null
? "null"
: receiver === "object"
? "ExactReference"
: `ManagedReference<"atom", ${q(receiver.id)}>`;
contexts.push(
`readonly ${q(entry.displayName)}: { readonly effect: ${q(effect)}; ${remote ? "readonly signal: AbortSignal;" : ""} readonly input: ${tsType(entry.inputType)}; readonly receiver: ${receiverType}; readonly ports: { ${facets.join("; ")} } }`,
);
const output = tsType(entry.outputType);
implementations.push(
`readonly ${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => ${output} | Promise<${output}>`,
);
adapters.push(
`${q(entry.id)}: context => installed[${q(entry.displayName)}](context as Contexts[${q(entry.displayName)}])`,
);
descriptors.push({
id: entry.id,
name: entry.displayName,
effect,
receiver,
input: portableValueSchema(entry.inputType),
output: portableValueSchema(entry.outputType),
methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })),
});
}
const groups = new Map<string, typeof entries>();
for (const template of pkg.genericExports ?? []) {
if (template.execution?.kind !== "remote" && template.execution?.targets.includes(options.target))
groups.set(template.displayName, []);
}
for (const entry of entries) {
if (!entry.application) continue;
const template = pkg.genericExports?.find((item) => item.id === entry.application!.exportId);
if (!template) throw new PortableBindingError(entry.id, "Missing generic export template");
groups.set(template.displayName, [...(groups.get(template.displayName) ?? []), entry]);
}
const generic = [...groups].map(([name, applications]) => {
if (!applications.length) return `${q(name)}: (_handler: unknown) => ({})`;
contexts.push(
`readonly ${q(name)}: ${applications.map((entry) => `Contexts[${q(entry.displayName)}]`).join(" | ")}`,
);
const type = applications
.map(
(entry) =>
`((context: Contexts[${q(entry.displayName)}]) => ${tsType(entry.outputType)} | Promise<${tsType(entry.outputType)}>)`,
)
.join(" & ");
const handlers = applications
.map((entry) => `${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => handler(context)`)
.join(",");
return `${q(name)}: (handler: ${type}) => ({${handlers}})`;
});
return `// Generated ${remote ? "remote" : "portable"} bindings. Regenerate from the checked capability schema; do not edit.\nimport { define${mode}Registry, type ${mode}Export, type PreparedChange, type ManagedReference, type ExactReference, type ${remote ? "ServerMutationOptions" : "MutationKey"} } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type ${mode}Implementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly ${mode}Export[];\nexport const generic = { ${generic.join(",\n")} };\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`;
}
+83
View File
@@ -0,0 +1,83 @@
import { createHash } from "node:crypto";
import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js";
import { defaultQueryBudgets, type QueryDeclaration } from "../query/types.js";
/** React contract tooling generates ordinary queries before normal checking.
* Neither the query language nor its execution planner knows presentation IDs. */
export function presentationQueries(pkg: PackageRevision, interfaces: readonly InterfaceRevision[]) {
const documents = new Map<string, string>();
const generated: QueryDeclaration[] = [];
const contracts = new Map(interfaces.map((contract) => [contract.revisionId, contract]));
const navigation = new Set<string>();
for (const module of pkg.modules ?? []) {
const contract = contracts.get(module.interfaceRevisionId);
const member = contract?.members.find((member) => member.id === module.memberId);
if (member?.kind !== "module" || member.contract !== "org.quixos.react.component/2") continue;
const argument = contract?.application?.arguments[1];
if (argument?.kind !== "object" || argument.target.kind !== "interface")
throw Error("Stateful React presentations require a closed navigation interface");
navigation.add(argument.target.interfaceRevisionId);
}
const valueSelection = (type: ValueType): string => {
if (type.kind === "optional" || type.kind === "list") return valueSelection(type.value);
return type.kind === "record"
? `{${Object.entries(type.fields)
.map(([name, type]) => `${name} ${valueSelection(type)}`)
.join(" ")}}`
: "";
};
for (const id of [...navigation].sort()) {
const contract = contracts.get(id as InterfaceRevision["revisionId"]);
if (!contract || contract.template) throw Error(`Missing closed presentation navigation ${id}`);
const fields = contract.members.flatMap((member) => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
if (member.kind === "value") return [`${member.displayName} ${valueSelection(member.valueType)}`];
if (member.target.kind !== "interface")
throw Error(`Navigation relationship ${member.displayName} must declare an interface view`);
return [
member.cardinality === "many" || member.cardinality === "many-unique"
? `${member.displayName}(all:512) {entries {node {_camino {ref}}}}`
: `${member.displayName} {_camino {ref}}`,
];
});
const stem = `PresentationNavigation${createHash("sha256").update(id).digest("hex").slice(0, 12)}`;
const inputs = `presentation:navigation:${id}:inputs`;
for (const local of [false, true]) {
const name = `${stem}${local ? "View" : "Inputs"}`;
const document = `generated/${name}.graphql`;
documents.set(
document,
`query ${name}($presentationMount: QxRef_PresentationMount!) {
root { ${local ? "availableObjects" : "objects"}(all:512, where:{_camino:{as:{PresentationState:{presentationMount:{eq:$presentationMount}}}}}) {
entries { node { ${fields.join("\n")}
... on PresentationState {
_presentationOccurrence: occurrence
_presentationSource: source {_camino {ref}}
_presentationParent: parent {_camino {ref}}
_presentationMount: presentationMount {_camino {ref}}
}
} }
} }
}`,
);
generated.push({
id: local ? `presentation:navigation:${id}:view` : inputs,
displayName: name,
root: { kind: "workspace-interface", interface: contract.revisionId },
document,
operation: name,
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets, rows: 5000, candidates: 5000, resultBytes: 8388608 },
watch: local,
placement: local ? { kind: "local", sourceQueryId: inputs } : { kind: "replica" },
});
}
}
for (const query of generated)
if (pkg.queries?.some((existing) => existing.id === query.id || existing.displayName === query.displayName))
throw Error(`Presentation query conflicts with an authored query: ${query.id}`);
pkg.queries = [...(pkg.queries ?? []), ...generated];
return documents;
}
+16 -7
View File
@@ -10,11 +10,16 @@ declare module "@quixos/web-studio-react-runtime" {
export type ObjectRef<AtomId extends string> = string & { export type ObjectRef<AtomId extends string> = string & {
readonly $quixosAtom: AtomId; readonly $quixosAtom: AtomId;
}; };
export type FieldCapability = {objectId: string; interfaceRevisionId: string; getOperationId: string; watchOperationId?: string; setOperationId?: string; inputKind?: "fields" | "value"}; export type ReadableField<T> = {readonly value?: T; readonly $field: "query" | "independent"};
export type ReadableField<T> = {readonly value?: T; readonly capability: FieldCapability}; export type QueryField<T> = ReadableField<T> & {readonly $field: "query"};
export type IndependentField<T> = ReadableField<T> & {readonly $field: "independent"};
/** set(T) uses the resolved capability: native CRDT fields send incremental edits; /** set(T) uses the resolved capability: native CRDT fields send incremental edits;
* custom/manual setters receive semantic values. Storage provenance grants no authority. */ * custom/manual setters receive semantic values. Storage provenance grants no authority. */
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}}; export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true};
export type WritableQueryField<T> = WritableField<T> & QueryField<T>;
export type WritableIndependentField<T> = WritableField<T> & IndependentField<T>;
export function detachField<T>(field: WritableQueryField<T>): WritableIndependentField<T>;
export function detachField<T>(field: QueryField<T>): IndependentField<T>;
export type LiveFieldProp<T> = ReadableField<T>; export type LiveFieldProp<T> = ReadableField<T>;
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields}; export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View}; export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
@@ -25,11 +30,14 @@ declare module "@quixos/web-studio-react-runtime" {
export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}}; export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}};
export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract}; export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract};
export type QueryLiveProjection = {path: readonly string[]; selectionId: string; interfaceRevisionId: string; getOperationId: string; setOperationId?: string; watchOperationId?: string; inputKind: "fields" | "value"; valueType: QueryValueType; conditions: {include: boolean; defaultValue?: boolean; value: {kind: "variable"; name: string} | {kind: "literal"; value: unknown}}[]}; export type QueryLiveProjection = {path: readonly string[]; selectionId: string; interfaceRevisionId: string; getOperationId: string; setOperationId?: string; watchOperationId?: string; inputKind: "fields" | "value"; valueType: QueryValueType; conditions: {include: boolean; defaultValue?: boolean; value: {kind: "variable"; name: string} | {kind: "literal"; value: unknown}}[]};
export interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly liveFields?: readonly QueryLiveProjection[]; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]} export interface QueryDescriptor<Variables, Result, Kind extends "object" | "workspace-interface" = "object" | "workspace-interface"> {readonly id: string; readonly definitionDigest: string; readonly root: {readonly kind: Kind; readonly interface: string}; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly liveFields?: readonly QueryLiveProjection[]; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
export type QueryPartial<T> = T extends {readonly $quixosRef: string} | {readonly capability: {readonly getOperationId: string}} ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T; export type QueryPartial<T> = T extends {readonly $quixosRef: string} | ReadableField<unknown> ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string}; export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string};
export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error}); export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error});
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result>, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void}; export type WorkspaceQueryScope = {readonly $workspaceQueryScope: true};
export function getWorkspaceQueryScope<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">): Promise<WorkspaceQueryScope>;
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "object">, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">, options: {scope: WorkspaceQueryScope; variables: Variables}): QueryState<Result> & {refresh(): void};
export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}} export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}}
export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement; export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement;
export type ReactComponentHostProps<Action> = { export type ReactComponentHostProps<Action> = {
@@ -58,7 +66,8 @@ declare module "@quixos/web-studio-react-runtime" {
options?: {clientMutationId?: string; signal?: AbortSignal}, options?: {clientMutationId?: string; signal?: AbortSignal},
) => Promise<Result>; ) => Promise<Result>;
export const h: typeof React.createElement; export const h: typeof React.createElement;
export type LiveFieldState<T> = {refreshing: boolean; stale: boolean; pendingWrites: number; writeError?: Error; refresh(): Promise<void>; discard(): void} & ({status: "loading"; value?: undefined; error?: undefined} | {status: "ready"; value: T; error?: Error} | {status: "error"; value?: undefined; error: Error}); export type FieldReadState = "connecting" | "live" | "snapshot" | "inactive" | "not-selected" | "error" | "obsolete";
export type LiveFieldState<T> = {readState: FieldReadState; refreshing: boolean; stale: boolean; pendingWrites: number; writeError?: Error; refresh(): Promise<void>; discard(): void} & ({status: "loading"; value?: undefined; error?: undefined} | {status: "ready"; value: T; error?: Error} | {status: "error"; value?: undefined; error: Error});
export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>}; export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>};
export function useLiveField<T>(field: ReadableField<T>, options: {id: string; write: (value: T) => Promise<void>}): WritableLiveFieldState<T>; export function useLiveField<T>(field: ReadableField<T>, options: {id: string; write: (value: T) => Promise<void>}): WritableLiveFieldState<T>;
export function useLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T>; export function useLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T>;
+9 -22
View File
@@ -1,6 +1,5 @@
import type { BindingSchema } from "./index.js"; import type { BindingSchema } from "./index.js";
import type { ValueType } from "../capability-model/types.js"; import type { ValueType } from "../capability-model/types.js";
import { queryPresentation } from "../query/presentation.js";
/** Browser projection of the SAME checked RPC types, not a second props schema. */ /** Browser projection of the SAME checked RPC types, not a second props schema. */
export function generateReactBindings( export function generateReactBindings(
@@ -16,6 +15,8 @@ export function generateReactBindings(
const declarations: string[] = []; const declarations: string[] = [];
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`); if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
return "null"; return "null";
@@ -30,6 +31,8 @@ export function generateReactBindings(
uint32: "number", uint32: "number",
double: "number", double: "number",
}[value.name]; }[value.name];
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
@@ -84,6 +87,7 @@ export function generateReactBindings(
const browserValue = (value: ValueType, seen: Set<string>): boolean => { const browserValue = (value: ValueType, seen: Set<string>): boolean => {
if (value.kind === "message") return false; if (value.kind === "message") return false;
if (value.kind === "builtin") return value.name === "unit"; if (value.kind === "builtin") return value.name === "unit";
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen)); if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen); if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
if (value.kind === "object-ref" && value.expectation.kind === "interface") { if (value.kind === "object-ref" && value.expectation.kind === "interface") {
@@ -129,43 +133,26 @@ export function generateReactBindings(
return type(value); return type(value);
}; };
const queries = pkg.checkedQueries ?? []; const queries = pkg.checkedQueries ?? [];
const presentedType = (
value: ValueType,
fields: ReturnType<typeof queryPresentation>,
path: string[] = [],
): string => {
const field = fields.find((field) => JSON.stringify(field.path) === JSON.stringify(path));
if (field)
return `${field.conditions.length ? "(" : ""}${field.setOperationId ? "WritableField" : "ReadableField"}<${type(field.valueType)}>${field.conditions.length ? " | null)" : ""}`;
if (value.kind === "optional") return `(${presentedType(value.value, fields, path)} | null)`;
if (value.kind === "list") return `Array<${presentedType(value.value, fields, [...path, "*"])}>`;
if (value.kind === "record")
return `{${Object.entries(value.fields)
.map(([key, child]) => `${q(key)}: ${presentedType(child, fields, [...path, key])}`)
.join(";")}}`;
return queryType(value);
};
const queryCode = const queryCode =
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` + `export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${presentedType(query.output, queryPresentation(query, schema.interfaces))}`).join(";")}};\n` + `export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.output)}`).join(";")}};\n` +
`export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}]>`).join(";")}} = ${JSON.stringify( `export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root.kind)}>`).join(";")}} = ${JSON.stringify(
Object.fromEntries( Object.fromEntries(
queries.map((query) => [ queries.map((query) => [
query.declaration.displayName, query.declaration.displayName,
{ {
id: `${pkg.revisionId}:${query.declaration.id}`, id: `${pkg.revisionId}:${query.declaration.id}`,
definitionDigest: query.definitionDigest, definitionDigest: query.definitionDigest,
rootInterfaceRevisionId: query.declaration.root, root: query.declaration.root,
variables: query.variables, variables: query.variables,
output: query.output, output: query.output,
liveFields: queryPresentation(query, schema.interfaces),
watch: query.declaration.watch, watch: query.declaration.watch,
}, },
]), ]),
), ),
)};\n`; )};\n`;
return ( return (
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` + `// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryField, WritableQueryField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
queryCode + queryCode +
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` + `const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` + `export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
+226 -24
View File
@@ -1,9 +1,13 @@
import { readFile, realpath } from "node:fs/promises"; import { readFile, realpath } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { profile } from "./profile.js";
import { loadQxSources, resolveQxSources } from "./source-loader.js"; import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileRepositoryQuery } from "../query/compile.js"; import { compileQuery, compileRepositoryQuery } from "../query/compile.js";
import type { QueryDeclaration } from "../query/types.js";
import { compileQueryComposition, type QueryComposition } from "../query/composition.js";
import { checkQueryTemplate } from "../query/templates.js"; import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js"; import { readRepositorySource } from "./source-loader.js";
import { parseControllerCatalog } from "../capability-model/controllers.js";
import { linkQueries } from "../query/link.js"; import { linkQueries } from "../query/link.js";
import { import {
capabilityId, capabilityId,
@@ -40,13 +44,40 @@ export type CapabilityRepositoryResolver = (
kind: LockedResource["kind"], kind: LockedResource["kind"],
) => Promise<CapabilityRepositorySnapshot>; ) => Promise<CapabilityRepositorySnapshot>;
/** Already checked outputs of this compiler, composed by the Nix build DAG.
* Source capture/check reports never grant this authority. */
export type CheckedResourceResolver = (
kind: LockedResource["kind"],
source: GitSource,
lock: QuixosRepositoryLock,
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>,
) => Promise<CapabilityResource | undefined>;
/** An explicitly selected build frontend supplies ordinary query sources. The
* assembler never chooses a frontend by inspecting an interface/contract ID.
* Generated sources go through the same query and imported-fragment checks. */
export type PackageQueryGenerator = ((input: {
readonly package: Readonly<PackageRevision>;
readonly interfaces: readonly InterfaceRevision[];
readonly readSource: (name: string) => Promise<string>;
}) => Promise<{
readonly queries: readonly QueryDeclaration[];
readonly documents: ReadonlyMap<string, string>;
readonly compositions?: readonly QueryComposition[];
readonly artifacts?: Readonly<Record<string, unknown>>;
}>) & {
/** Optional explicit frontend stage after workspace binding resolution.
* Its output still passes normal workspace/query validation. */
completeWorkspace?: (workspace: WorkspaceRevision) => Promise<void>;
};
export type ResolvedCapabilityResource = { export type ResolvedCapabilityResource = {
key: string; key: string;
kind: LockedResource["kind"]; kind: LockedResource["kind"];
source: GitSource; source: GitSource;
directory: string; directory: string;
lock: QuixosRepositoryLock; lock: QuixosRepositoryLock;
resource: CapabilityResource; resource: CapabilityResource | { kind: "build" };
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>; dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
}; };
@@ -55,13 +86,15 @@ export type CompiledWorkspaceRepository = {
plan: CompiledWorkspaceRevision; plan: CompiledWorkspaceRevision;
lock: QuixosRepositoryLock; lock: QuixosRepositoryLock;
resources: ResolvedCapabilityResource[]; resources: ResolvedCapabilityResource[];
capabilityResources: ResolvedCapabilityResource[];
directResources: ReadonlyMap<string, ResolvedCapabilityResource>; directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
}; };
export type CompiledCapabilityResourceRepository = { export type CompiledCapabilityResourceRepository = {
resource: CapabilityResource; resource: CapabilityResource | { kind: "build" };
lock: QuixosRepositoryLock; lock: QuixosRepositoryLock;
resources: ResolvedCapabilityResource[]; resources: ResolvedCapabilityResource[];
capabilityResources: ResolvedCapabilityResource[];
directResources: ReadonlyMap<string, ResolvedCapabilityResource>; directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
}; };
@@ -78,7 +111,10 @@ const assertImportsMatchLock = (
resources: readonly LockedResource[], resources: readonly LockedResource[],
) => { ) => {
const authored = [...imports].map(importsKey).sort(); const authored = [...imports].map(importsKey).sort();
const locked = [...resources].map(importsKey).sort(); const locked = resources
.filter((entry) => entry.kind !== "build")
.map(importsKey)
.sort();
if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) { if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) {
throw new Error( throw new Error(
`${label} imports do not match quixos.lock:\n` + `${label} imports do not match quixos.lock:\n` +
@@ -136,13 +172,19 @@ const diagnosticsMessage = (
}) })
.join("\n")}`; .join("\n")}`;
const revisionFor = (node: ResolvedCapabilityResource) => node.resource.revision; const revisionFor = (node: ResolvedCapabilityResource) => {
if (node.resource.kind === "build") throw Error("Build sources have no capability revision");
return node.resource.revision;
};
const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): ResolvedCapabilityResource[] => { const resourceClosure = (
roots: readonly ResolvedCapabilityResource[],
capabilityOnly = false,
): ResolvedCapabilityResource[] => {
const result: ResolvedCapabilityResource[] = []; const result: ResolvedCapabilityResource[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
const visit = (node: ResolvedCapabilityResource) => { const visit = (node: ResolvedCapabilityResource) => {
if (seen.has(node.key)) return; if (seen.has(node.key) || (capabilityOnly && node.kind === "build")) return;
seen.add(node.key); seen.add(node.key);
for (const dependency of node.dependencies.values()) visit(dependency); for (const dependency of node.dependencies.values()) visit(dependency);
result.push(node); result.push(node);
@@ -154,7 +196,12 @@ const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): Resolved
const environmentFor = ( const environmentFor = (
direct: readonly [LockedResource, ResolvedCapabilityResource][], direct: readonly [LockedResource, ResolvedCapabilityResource][],
closure: readonly ResolvedCapabilityResource[], closure: readonly ResolvedCapabilityResource[],
): CapabilityImportEnvironment => ({ ): CapabilityImportEnvironment => {
closure = resourceClosure(
direct.map(([, node]) => node),
true,
);
return {
interfaces: new Map( interfaces: new Map(
direct.flatMap(([locked, node]) => direct.flatMap(([locked, node]) =>
node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [], node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [],
@@ -168,16 +215,28 @@ const environmentFor = (
interfaceClosure: exactRevisions( interfaceClosure: exactRevisions(
closure.flatMap((node) => [ closure.flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []), ...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.specializations ?? []), ...(node.resource.kind === "build" ? [] : (node.resource.specializations ?? [])),
]), ]),
), ),
packageClosure: exactRevisions( packageClosure: exactRevisions(
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])), closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
), ),
externalAtoms: exactAtoms(closure.flatMap((node) => node.resource.externalAtoms)), externalAtoms: exactAtoms(
}); closure.flatMap((node) => (node.resource.kind === "build" ? [] : node.resource.externalAtoms)),
),
};
};
const createResourceGraphResolver = (quixosCommit: string, resolveResource: CapabilityRepositoryResolver) => { const createResourceGraphResolver = (
quixosCommit: string,
resolveResource: CapabilityRepositoryResolver,
generateQueries?: PackageQueryGenerator,
checkedResource?: CheckedResourceResolver,
) => {
const graphStarted = performance.now();
let checkedResources = 0,
reusedResources = 0,
checkedQueries = 0;
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>(); const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
const active: string[] = []; const active: string[] = [];
@@ -217,7 +276,44 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
for (const dependency of lockResult.lock.resources) { for (const dependency of lockResult.lock.resources) {
directPairs.push([dependency, await visit(dependency)]); directPairs.push([dependency, await visit(dependency)]);
} }
if (locked.kind === "build") {
return {
key,
kind: locked.kind,
source: locked.source,
directory: snapshot.directory,
lock: lockResult.lock,
resource: { kind: "build" as const },
dependencies: new Map(
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
),
};
}
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node)); const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
const dependencies = new Map(
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
);
const checked = await checkedResource?.(locked.kind, locked.source, lockResult.lock, dependencies);
if (checked) {
if (
checked.kind !== locked.kind ||
checked.revision.source.repository !== locked.source.repository ||
checked.revision.source.commit !== locked.source.commit
)
throw new Error("Checked resource identity mismatch");
assertImportsMatchLock("Checked resource", checked.imports, lockResult.lock.resources);
reusedResources++;
return {
key,
kind: locked.kind,
source: locked.source,
directory: snapshot.directory,
lock: lockResult.lock,
resource: structuredClone(checked),
dependencies,
};
}
const checkStarted = performance.now();
const environment = environmentFor(directPairs, dependencyClosure); const environment = environmentFor(directPairs, dependencyClosure);
const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx"; const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx";
const manifestPath = path.join(snapshot.directory, manifestName); const manifestPath = path.join(snapshot.directory, manifestName);
@@ -239,15 +335,86 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
...(environment.interfaceClosure ?? []), ...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []), ...(compiled.resource.specializations ?? []),
]; ];
const generated = await generateQueries?.({
package: structuredClone(compiled.resource.revision),
interfaces: structuredClone(queryInterfaces),
readSource: (name) => readRepositorySource(snapshot.directory, name),
});
if (generated) {
if (generated.artifacts) compiled.resource.revision.buildArtifacts = structuredClone(generated.artifacts);
const queries = [...(compiled.resource.revision.queries ?? [])];
for (const query of generated.queries) {
if (queries.some((entry) => entry.id === query.id || entry.displayName === query.displayName))
throw new Error(`Generated query conflicts with an authored query: ${query.id}`);
if (!generated.documents.has(query.document))
throw new Error(`Generated query has no source: ${query.document}`);
queries.push(structuredClone(query));
}
compiled.resource.revision.queries = queries;
}
for (const template of compiled.resource.revision.queryTemplates ?? []) for (const template of compiled.resource.revision.queryTemplates ?? [])
await checkQueryTemplate(template, queryInterfaces, (name) => await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name), readRepositorySource(snapshot.directory, name),
); );
if (compiled.resource.revision.queries?.length) if (compiled.resource.revision.queries?.length) {
compiled.resource.revision.checkedQueries = await Promise.all( const pkg = compiled.resource.revision;
compiled.resource.revision.queries.map((query) => const checked = new Map<string, import("../query/types.js").CheckedQuery>();
compileRepositoryQuery(snapshot.directory, query, queryInterfaces), const compile = async (
), id: string,
parents: readonly string[] = [],
): Promise<import("../query/types.js").CheckedQuery> => {
if (parents.includes(id)) throw new Error(`Query fragment cycle: ${[...parents, id].join(" -> ")}`);
const previous = checked.get(id);
if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const resolveFragment = async (packageId: string, fragmentId: string) => {
if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]);
const imported = environment.packageClosure
?.find((other) => other.revisionId === packageId)
?.checkedQueries?.find((query) => query.declaration.id === fragmentId);
if (!imported) throw new Error(`Unchecked imported fragment ${packageId}/${fragmentId}`);
return imported;
};
const result = generated?.documents.has(query.document)
? await compileQuery(
query,
queryInterfaces,
async (name) =>
generated.documents.get(name) ?? (await readRepositorySource(snapshot.directory, name)),
resolveFragment,
)
: await compileRepositoryQuery(snapshot.directory, query, queryInterfaces, resolveFragment);
checked.set(id, result);
return result;
};
pkg.checkedQueries = [];
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
}
if (generated?.compositions?.length) {
const pkg = compiled.resource.revision;
const queries = new Map((pkg.checkedQueries ?? []).map((query) => [query.declaration.id, query]));
for (const dependency of environment.packageClosure ?? [])
for (const query of dependency.checkedQueries ?? [])
queries.set(JSON.stringify([dependency.revisionId, query.declaration.id]), query);
const ids = new Set<string>();
pkg.queryCompositions = generated.compositions.map((composition) => {
if (ids.has(composition.id)) throw new Error(`Duplicate query composition: ${composition.id}`);
ids.add(composition.id);
return compileQueryComposition(composition, queries);
});
}
let controllerText: string | undefined;
try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (controllerText !== undefined)
compiled.resource.revision.controllers = parseControllerCatalog(
controllerText,
queryInterfaces,
compiled.resource.revision.checkedQueries,
); );
let catalogText: string | undefined; let catalogText: string | undefined;
try { try {
@@ -271,6 +438,15 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
compiled.resource.revision.migrationCatalog = catalog; compiled.resource.revision.migrationCatalog = catalog;
} }
} }
checkedResources++;
const queries =
compiled.resource.kind === "package" ? (compiled.resource.revision.checkedQueries?.length ?? 0) : 0;
checkedQueries += queries;
profile("resource-check", checkStarted, {
kind: locked.kind,
resource: compiled.resource.revision.displayName,
queries,
});
return { return {
key, key,
kind: locked.kind, kind: locked.kind,
@@ -292,7 +468,11 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
return { return {
visit, visit,
nodes: async () => await Promise.all(resolved.values()), nodes: async () => {
const nodes = await Promise.all(resolved.values());
profile("resource-graph", graphStarted, { checkedResources, reusedResources, checkedQueries });
return nodes;
},
}; };
}; };
@@ -301,12 +481,19 @@ export const compileCapabilityResourceRepository = async (options: {
kind: LockedResource["kind"]; kind: LockedResource["kind"];
source: GitSource; source: GitSource;
resolveResource: CapabilityRepositoryResolver; resolveResource: CapabilityRepositoryResolver;
generateQueries?: PackageQueryGenerator;
checkedResource?: CheckedResourceResolver;
}): Promise<CompiledCapabilityResourceRepository> => { }): Promise<CompiledCapabilityResourceRepository> => {
const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock")); const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!lockResult.ok) { if (!lockResult.ok) {
throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics)); throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics));
} }
const resolver = createResourceGraphResolver(lockResult.lock.quixos.commit, options.resolveResource); const resolver = createResourceGraphResolver(
lockResult.lock.quixos.commit,
options.resolveResource,
options.generateQueries,
options.checkedResource,
);
const root = await resolver.visit( const root = await resolver.visit(
{ {
kind: options.kind, kind: options.kind,
@@ -319,6 +506,7 @@ export const compileCapabilityResourceRepository = async (options: {
resource: root.resource, resource: root.resource,
lock: root.lock, lock: root.lock,
resources: await resolver.nodes(), resources: await resolver.nodes(),
capabilityResources: resourceClosure([root], true),
directResources: root.dependencies, directResources: root.dependencies,
}; };
}; };
@@ -331,13 +519,20 @@ export const compileWorkspaceRepository = async (options: {
sourceRootCommit?: string; sourceRootCommit?: string;
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */ /** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
readSource?: (name: string) => Promise<string>; readSource?: (name: string) => Promise<string>;
generateQueries?: PackageQueryGenerator;
checkedResource?: CheckedResourceResolver;
}): Promise<CompiledWorkspaceRepository> => { }): Promise<CompiledWorkspaceRepository> => {
const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock")); const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!rootLockResult.ok) { if (!rootLockResult.ok) {
throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics)); throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics));
} }
const rootLock = rootLockResult.lock; const rootLock = rootLockResult.lock;
const resolver = createResourceGraphResolver(rootLock.quixos.commit, options.resolveResource); const resolver = createResourceGraphResolver(
rootLock.quixos.commit,
options.resolveResource,
options.generateQueries,
options.checkedResource,
);
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = []; const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
for (const locked of rootLock.resources) { for (const locked of rootLock.resources) {
@@ -363,11 +558,13 @@ export const compileWorkspaceRepository = async (options: {
); );
} }
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources); assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
const availableInterfaceIds = new Set( const availableInterfaceIds = new Set((environment.interfaceClosure ?? []).map((iface) => iface.revisionId));
nodes.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision.revisionId] : [])),
);
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id)); const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
for (const node of nodes) { for (const node of resourceClosure(
directPairs.map(([, node]) => node),
true,
)) {
if (node.resource.kind === "build") continue;
for (const requirement of node.resource.externalInterfaces) { for (const requirement of node.resource.externalInterfaces) {
if (!availableInterfaceIds.has(requirement.revisionId)) { if (!availableInterfaceIds.has(requirement.revisionId)) {
throw new Error( throw new Error(
@@ -399,6 +596,7 @@ export const compileWorkspaceRepository = async (options: {
: {}), : {}),
...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}), ...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}),
}; };
await options.generateQueries?.completeWorkspace?.(workspace);
const checked = compileWorkspaceRevision(workspace); const checked = compileWorkspaceRevision(workspace);
if (!checked.ok) { if (!checked.ok) {
throw new Error( throw new Error(
@@ -417,6 +615,10 @@ export const compileWorkspaceRepository = async (options: {
plan: checked.plan, plan: checked.plan,
lock: rootLock, lock: rootLock,
resources: nodes, resources: nodes,
capabilityResources: resourceClosure(
directPairs.map(([, node]) => node),
true,
),
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])), directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
}; };
}; };
+32 -4
View File
@@ -7,7 +7,12 @@ import type { convergeAuthoring } from "./authoring-converge.js";
import { execFile as callback } from "node:child_process"; import { execFile as callback } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js"; import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js";
import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../capability-model/index.js"; import {
planEvolution,
reviewSemanticMajors,
type WorkspaceRevision,
type EvolutionReview,
} from "../capability-model/index.js";
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json"; export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
export async function checkAuthoring( export async function checkAuthoring(
@@ -34,6 +39,7 @@ export async function checkAuthoring(
phase: string; phase: string;
output: string; output: string;
compilation?: "passed"; compilation?: "passed";
runtime?: "execution";
activationReadiness?: "preserve" | "migration-required" | "blocked"; activationReadiness?: "preserve" | "migration-required" | "blocked";
migrationRequired?: string[]; migrationRequired?: string[];
} = { } = {
@@ -58,7 +64,7 @@ export async function checkAuthoring(
// Repository-scoped agents can check separate immutable candidates in parallel. // Repository-scoped agents can check separate immutable candidates in parallel.
const capture = promisify(callback)("quixos-qx", ["converge", context.workbench, directory], { const capture = promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
maxBuffer: 4 * 1024 * 1024, maxBuffer: 4 * 1024 * 1024,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_TRACE_CAPTURE: "1" }, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_TRACE_CAPTURE: "1", QUIXOS_TRACE_PROFILE: "1" },
}); });
console.error(`Capture details: ${path.join(output, "capture.log")}`); console.error(`Capture details: ${path.join(output, "capture.log")}`);
capture.child.stderr?.on("data", (chunk) => appendFileSync(path.join(output, "capture.log"), chunk)); capture.child.stderr?.on("data", (chunk) => appendFileSync(path.join(output, "capture.log"), chunk));
@@ -88,6 +94,9 @@ export async function checkAuthoring(
resource.kind, resource.kind,
path.join(output, "nix.log"), path.join(output, "nix.log"),
options.contractOnly, options.contractOnly,
(phase, ms) => {
timings[phase] = ms;
},
); );
} finally { } finally {
timings.immutableCheckMs = Math.round(performance.now() - buildStarted); timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
@@ -98,7 +107,26 @@ export async function checkAuthoring(
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8"); const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidateText); await fs.writeFile(path.join(output, "candidate.json"), candidateText);
report.compilation = "passed"; report.compilation = "passed";
if (resource.kind === "workspace" && !options.contractOnly) { try {
await fs.access(path.join(report.artifactPath, "execution-plan.json"));
report.runtime = "execution";
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (resource.kind === "workspace" && !options.contractOnly && report.runtime === "execution") {
report.phase = "semantic-review";
if (options.reviews && !options.baseline)
throw new Error("Execution semantic reviews require an explicit --baseline candidate");
const baseline = options.baseline
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
: null;
const receipts = options.reviews
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
: [];
const review = reviewSemanticMajors(baseline, JSON.parse(candidateText), receipts);
await fs.writeFile(path.join(output, "semantic-review.json"), JSON.stringify(review, null, 2));
report.blockers.push(...review.blockers);
} else if (resource.kind === "workspace" && !options.contractOnly) {
report.phase = "evolution"; report.phase = "evolution";
await progress(); await progress();
let baseline = options.baseline; let baseline = options.baseline;
@@ -147,7 +175,7 @@ export async function checkAuthoring(
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 }); await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
await fs.rename(temporary, filename); await fs.rename(temporary, filename);
}; };
if (report.artifactPath) { if (report.artifactPath && report.compilation === "passed" && !report.blockers.length) {
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8")); const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
for (const checked of graph.resources) { for (const checked of graph.resources) {
const managed = context.resources.find( const managed = context.resources.find(
+2 -2
View File
@@ -3,7 +3,7 @@ import path from "node:path";
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js"; import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
export type AuthoringResource = { export type AuthoringResource = {
kind: "workspace" | "interface" | "package"; kind: "workspace" | "interface" | "package" | "build";
directory: string; directory: string;
resourceId?: string; resourceId?: string;
source?: GitSource; source?: GitSource;
@@ -31,7 +31,7 @@ export async function authoringContext(start: string) {
if (typeof entry.directory === "string" && path.isAbsolute(entry.directory)) if (typeof entry.directory === "string" && path.isAbsolute(entry.directory))
entry.directory = path.relative(workbench, entry.directory); entry.directory = path.relative(workbench, entry.directory);
if ( if (
!["interface", "package"].includes(entry.kind) || !["interface", "package", "build"].includes(entry.kind) ||
!entry.source || !entry.source ||
!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory) !/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory)
) { ) {
+97 -28
View File
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { authoringContext } from "./authoring-context.js"; import { authoringContext } from "./authoring-context.js";
import { snapshotCommit } from "./checked-build.js"; import { snapshotCommit } from "./checked-build.js";
import { profile } from "./profile.js";
import { import {
loadQuixosLock, loadQuixosLock,
parseQuixosLockDocument, parseQuixosLockDocument,
@@ -14,6 +15,42 @@ import {
} from "../resource-lock/index.js"; } from "../resource-lock/index.js";
const execFile = promisify(callback); const execFile = promisify(callback);
// Bound independent repository operations. One visit owns each repository;
// all visits finish before the final concurrent-edit validation pass.
const concurrency = 4;
async function mapConcurrent<T, R>(values: readonly T[], action: (value: T) => Promise<R>): Promise<R[]> {
const results = new Array<R>(values.length);
let next = 0;
await Promise.all(
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
while (next < values.length) {
const index = next++;
results[index] = await action(values[index]);
}
}),
);
return results;
}
const limiter = () => {
let running = 0;
const waiting: Array<() => void> = [];
return async <T>(action: () => Promise<T>): Promise<T> => {
await new Promise<void>((resolve) => {
if (running < concurrency) {
running++;
resolve();
} else waiting.push(resolve);
});
try {
return await action();
} finally {
const next = waiting.shift();
if (next) next();
else running--;
}
};
};
const command = async (cwd: string, executable: string, args: string[]) => { const command = async (cwd: string, executable: string, args: string[]) => {
const start = performance.now(); const start = performance.now();
// Do not log arguments: transports may contain credentials. Source identities // Do not log arguments: transports may contain credentials. Source identities
@@ -44,13 +81,14 @@ export type AuthoringBlocker = {
* worklist grants typechecking, semantic review or activation approval. * worklist grants typechecking, semantic review or activation approval.
* Caller serializes coordinators; package authors may still be editing. */ * Caller serializes coordinators; package authors may still be editing. */
export async function convergeAuthoring(start: string, target = "root") { export async function convergeAuthoring(start: string, target = "root") {
const started = performance.now();
const context = await authoringContext(start); const context = await authoringContext(start);
const graphFile = path.join(context.workbench, ".quixos/resource-graph.json"); const graphFile = path.join(context.workbench, ".quixos/resource-graph.json");
const graphBefore = await readFile(graphFile, "utf8"); const graphBefore = await readFile(graphFile, "utf8");
const blockers: AuthoringBlocker[] = []; const blockers: AuthoringBlocker[] = [];
const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>(); const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>();
const selected = new Map<string, string>(); const selected = new Map<string, string>();
for (const entry of context.resources) { const inventory = await mapConcurrent(context.resources, async (entry) => {
const root = path.join(context.workbench, entry.directory); const root = path.join(context.workbench, entry.directory);
let repository = entry.source?.repository; let repository = entry.source?.repository;
try { try {
@@ -64,6 +102,9 @@ export async function convergeAuthoring(start: string, target = "root") {
blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) }); blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) });
if (!repository) throw error; // The root has no separate registered source. if (!repository) throw error; // The root has no separate registered source.
} }
return { ...entry, repository };
});
for (const { repository, ...entry } of inventory) {
const key = identity(entry.kind, repository); const key = identity(entry.kind, repository);
if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`); if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`);
selected.set(key, entry.directory); selected.set(key, entry.directory);
@@ -73,7 +114,7 @@ export async function convergeAuthoring(start: string, target = "root") {
dependencies: [], dependencies: [],
}); });
} }
for (const node of nodes.values()) { await mapConcurrent([...nodes.values()], async (node) => {
try { try {
const lock = await loadQuixosLock(path.join(context.workbench, node.directory, "quixos.lock")); const lock = await loadQuixosLock(path.join(context.workbench, node.directory, "quixos.lock"));
if (!lock.ok) throw new Error(lock.diagnostics.map((d) => `${d.fileName}: ${d.message}`).join("\n")); if (!lock.ok) throw new Error(lock.diagnostics.map((d) => `${d.fileName}: ${d.message}`).join("\n"));
@@ -88,31 +129,43 @@ export async function convergeAuthoring(start: string, target = "root") {
} catch (error) { } catch (error) {
blockers.push({ directory: node.directory, phase: "resolution", message: String(error) }); blockers.push({ directory: node.directory, phase: "resolution", message: String(error) });
} }
} });
const complete = new Map<string, GitSource>(), // Detect cycles before starting concurrent futures: two roots awaiting each
active = new Set<string>(); // other must fail clearly rather than deadlocking on their cached promises.
const visited = new Set<string>(); const scanned = new Set<string>();
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`); const detectCycles = (directory: string, parents: string[] = []) => {
const visit = async (directory: string): Promise<boolean> => { if (parents.includes(directory)) {
visited.add(directory);
if (complete.has(directory)) return true;
if (blockers.some((entry) => entry.directory === directory)) return false;
if (active.has(directory)) {
blockers.push({ blockers.push({
directory, directory,
phase: "dependency", phase: "dependency",
message: `Source dependency cycle: ${[...active, directory].join(" -> ")}`, message: `Source dependency cycle: ${[...parents, directory].join(" -> ")}`,
}); });
return false; return;
} }
active.add(directory); if (scanned.has(directory)) return;
for (const dependency of nodes.get(directory)!.dependencies) detectCycles(dependency, [...parents, directory]);
scanned.add(directory);
};
for (const directory of nodes.keys()) detectCycles(directory);
const complete = new Map<string, GitSource>();
const pending = new Map<string, Promise<boolean>>();
const capture = limiter();
const visited = new Set<string>();
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
const visit = (directory: string): Promise<boolean> => {
visited.add(directory);
const existing = pending.get(directory);
if (existing) return existing;
const result = (async () => {
if (blockers.some((entry) => entry.directory === directory)) return false;
const node = nodes.get(directory)!; const node = nodes.get(directory)!;
for (const dependency of node.dependencies) const ready = await Promise.all(node.dependencies.map(visit));
if (!(await visit(dependency))) { const unavailable = node.dependencies.find((_, index) => !ready[index]);
blockers.push({ directory, phase: "dependency", message: `Waiting for ${dependency}` }); if (unavailable) {
active.delete(directory); blockers.push({ directory, phase: "dependency", message: `Waiting for ${unavailable}` });
return false; return false;
} }
return await capture(async () => {
const root = path.join(context.workbench, directory); const root = path.join(context.workbench, directory);
let phase: AuthoringBlocker["phase"] = "source"; let phase: AuthoringBlocker["phase"] = "source";
try { try {
@@ -128,7 +181,9 @@ export async function convergeAuthoring(start: string, target = "root") {
const target = selected.get(identity(dependency.kind, dependency.source.repository)); const target = selected.get(identity(dependency.kind, dependency.source.repository));
const source = target ? complete.get(target) : undefined; const source = target ? complete.get(target) : undefined;
if (target && !source) if (target && !source)
throw new Error(`Dependencies changed during convergence (${dependency.binding}); join writers and retry`); throw new Error(
`Dependencies changed during convergence (${dependency.binding}); join writers and retry`,
);
if (source && source.commit !== dependency.source.commit) { if (source && source.commit !== dependency.source.commit) {
dependency.source = source; dependency.source = source;
changed = true; changed = true;
@@ -155,30 +210,41 @@ export async function convergeAuthoring(start: string, target = "root") {
phase = "publication"; phase = "publication";
const ref = retentionTagForCommit(commit); const ref = retentionTagForCommit(commit);
const remote = await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref]); const remote = await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref]);
if (remote && remote.split(/\s+/)[0] !== commit) throw new Error(`Conflicting immutable retention ref ${ref}`); if (remote && remote.split(/\s+/)[0] !== commit)
if (!remote) await command(root, "git", ["push", node.source.repository, `${commit}:${ref}`]); throw new Error(`Conflicting immutable retention ref ${ref}`);
if ((await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !== commit) if (!remote) {
await command(root, "git", ["push", node.source.repository, `${commit}:${ref}`]);
if (
(await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !==
commit
)
throw new Error("Published source retention was not observed"); throw new Error("Published source retention was not observed");
}
complete.set(directory, { ...node.source, commit }); complete.set(directory, { ...node.source, commit });
} catch (error) { } catch (error) {
blockers.push({ directory, phase, message: String(error).slice(0, 4000) }); blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
} }
active.delete(directory);
return complete.has(directory); return complete.has(directory);
});
})();
pending.set(directory, result);
return result;
}; };
// Include newly created, not-yet-imported resources, then the root. // Include newly created, not-yet-imported resources, then the root.
if (target === "root") for (const directory of [...nodes.keys()].filter((d) => d !== "root")) await visit(directory); if (target === "root") await Promise.all([...nodes.keys()].filter((d) => d !== "root").map(visit));
await visit(target); await visit(target);
for (let index = blockers.length - 1; index >= 0; index--) for (let index = blockers.length - 1; index >= 0; index--)
if (!visited.has(blockers[index].directory)) blockers.splice(index, 1); if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
for (const [directory, source] of complete) { const recheckStarted = performance.now();
await mapConcurrent([...complete], async ([directory, source]) => {
try { try {
if ((await snapshotCommit(path.join(context.workbench, directory))) !== source.commit) if ((await snapshotCommit(path.join(context.workbench, directory))) !== source.commit)
throw new Error("Source advanced while converging; join writers and retry"); throw new Error("Source advanced while converging; join writers and retry");
} catch (error) { } catch (error) {
blockers.push({ directory, phase: "concurrent-edit", message: String(error) }); blockers.push({ directory, phase: "concurrent-edit", message: String(error) });
} }
} });
profile("capture-recheck", recheckStarted, { repositories: complete.size });
// Persist successful selections even if another repository is still broken. // Persist successful selections even if another repository is still broken.
// Recovery must not depend on all parents succeeding in the same invocation. // Recovery must not depend on all parents succeeding in the same invocation.
const graph = JSON.parse(graphBefore); const graph = JSON.parse(graphBefore);
@@ -238,11 +304,14 @@ export async function convergeAuthoring(start: string, target = "root") {
await rm(temporary, { force: true }); await rm(temporary, { force: true });
} }
} }
profile("capture", started, { repositories: nodes.size, snapshots: complete.size * 2, target });
return { return {
workbench: context.workbench, workbench: context.workbench,
converged: blockers.length === 0, converged: blockers.length === 0,
candidate: blockers.length ? null : (complete.get(target) ?? null), candidate: blockers.length ? null : (complete.get(target) ?? null),
retained: [...complete].map(([directory, source]) => ({ directory, source })), retained: [...complete]
.sort(([left], [right]) => left.localeCompare(right))
.map(([directory, source]) => ({ directory, source })),
worklist: blockers, worklist: blockers,
verificationEvidence: false, verificationEvidence: false,
activated: false, activated: false,
+3 -1
View File
@@ -69,7 +69,9 @@ export async function inspectAuthoringRepository(root: string, historyLimit = 10
declarations: syntax declarations: syntax
? [...walkSyntax(syntax.root)] ? [...walkSyntax(syntax.root)]
.filter((node) => .filter((node) =>
/^(?:interface|package|atom|state|edge|method|function|event|conformance)\w*Decl$/.test(node.kind), /^(?:(?:interface|package|atom|state|edge|method|function|event|conformance|presentation)\w*Decl|module(?:Member|Export))$/.test(
node.kind,
),
) )
.map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) })) .map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) }))
: [], : [],
+2 -1
View File
@@ -209,7 +209,7 @@ export const checkWorkspaceCandidate = async (options: {
: []; : [];
const evolution = planEvolution(baseline, compiled.workspace, { reviews }); const evolution = planEvolution(baseline, compiled.workspace, { reviews });
blockers.push(...evolution.blockers); blockers.push(...evolution.blockers);
for (const resource of compiled.resources.filter((entry) => entry.kind === "package")) { for (const resource of compiled.capabilityResources.filter((entry) => entry.kind === "package")) {
// Per-package recursive schema, identical to host activation, not unrelated // Per-package recursive schema, identical to host activation, not unrelated
// workspace declarations that would unnecessarily invalidate build caches. // workspace declarations that would unnecessarily invalidate build caches.
const candidate = await compileCapabilityResourceRepository({ const candidate = await compileCapabilityResourceRepository({
@@ -218,6 +218,7 @@ export const checkWorkspaceCandidate = async (options: {
source: resource.source, source: resource.source,
resolveResource, resolveResource,
}); });
if (candidate.resource.kind !== "package") throw Error("Expected checked package contract");
const schema = path.join(temporary, "bindings.json"); const schema = path.join(temporary, "bindings.json");
await fs.writeFile( await fs.writeFile(
schema, schema,
+70 -43
View File
@@ -13,13 +13,14 @@ export const checkerIdentity = () =>
export async function snapshotCommit(root: string): Promise<string> { export async function snapshotCommit(root: string): Promise<string> {
const run = async (...args: string[]) => const run = async (...args: string[]) =>
(await execFile("jj", args, { cwd: root, env: environment() })).stdout.trim(); (await execFile("jj", args, { cwd: root, env: environment() })).stdout.trim();
await run("status"); // A single normal log snapshots the working copy and observes both values
// jj resolve --list exits 1 on a clean revision. Query structured revision // from that exact revision. Separate status/log processes add no evidence.
// metadata instead of depending on diagnostic wording or swallowing errors. const [conflict, commit] = (await run("log", "--no-graph", "-r", "@", "-T", 'conflict ++ "\\n" ++ commit_id')).split(
if ((await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "conflict")) !== "false") "\n",
throw new Error("Resolve source conflicts before verification"); );
const commit = await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"); if (conflict !== "false") throw new Error("Resolve source conflicts before verification");
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Verification requires an exact jj commit"); if (!commit || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))
throw new Error("Verification requires an exact jj commit");
await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], { await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], {
cwd: root, cwd: root,
env: environment(), env: environment(),
@@ -107,9 +108,10 @@ export async function buildCheckedPackage(source: string, schema: string, packag
* nested fetches. Keep build noise in a named log, with a bounded failure tail. */ * nested fetches. Keep build noise in a named log, with a bounded failure tail. */
export async function buildImmutableCandidate( export async function buildImmutableCandidate(
source: { repository: string; commit: string }, source: { repository: string; commit: string },
kind: "workspace" | "interface" | "package", kind: "workspace" | "interface" | "package" | "build",
logFile: string, logFile: string,
contractOnly = false, contractOnly = false,
observe?: (phase: "nixEvaluationMs" | "nixBuildMs", ms: number) => void,
): Promise<string> { ): Promise<string> {
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit)) if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit))
throw new Error("An immutable candidate requires an exact commit"); throw new Error("An immutable candidate requires an exact commit");
@@ -126,16 +128,52 @@ export async function buildImmutableCandidate(
log.once("open", () => resolve()); log.once("open", () => resolve());
log.once("error", reject); log.once("error", reject);
}); });
return await new Promise((resolve, reject) => { const run = async (executable: string, args: string[]) =>
await new Promise<string>((resolve, reject) => {
let output = "", let output = "",
tail = "", tail = "",
failure: Error | undefined; failure: Error | undefined;
const child = spawn( const child = spawn(executable, args, { env: environment(), stdio: ["ignore", "pipe", "pipe"] });
"nix", const failedLog = (error: Error) => {
[ failure = error;
"build", child.kill();
"--impure", };
"--file", log.on("error", failedLog);
child.stdout.on("data", (chunk) => {
output += chunk;
});
child.stderr.on("data", (chunk) => {
log.write(chunk);
tail = (tail + String(chunk)).slice(-6000);
});
child.on("error", (error) => {
failure = error;
});
child.on("close", (code) => {
log.off("error", failedLog);
if (failure) reject(failure);
else if (code !== 0) reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
else resolve(output.trim());
});
});
const timed = async (phase: "nixEvaluationMs" | "nixBuildMs", action: () => Promise<string>) => {
const started = performance.now();
try {
return await action();
} finally {
observe?.(phase, Math.round(performance.now() - started));
}
};
let gcDirectory: string | undefined;
try {
// Evaluation includes import-from-derivation builds (locks and contracts).
// Build the resulting derivation directly, without evaluating the DAG twice.
// Keep the derivation alive across the two processes, including concurrent
// GC. This temporary root is removed on success and on verification failure.
gcDirectory = await fs.mkdtemp(path.join(path.dirname(logFile), ".gc-root-"));
const gcRoot = path.join(gcDirectory, "candidate");
const derivation = await timed("nixEvaluationMs", async () => {
await run("nix-instantiate", [
builder, builder,
"--argstr", "--argstr",
"repository", "repository",
@@ -152,33 +190,22 @@ export async function buildImmutableCandidate(
"--arg", "--arg",
"contractOnly", "contractOnly",
contractOnly ? "true" : "false", contractOnly ? "true" : "false",
"--no-link", "--add-root",
"--print-out-paths", gcRoot,
"-L", "--indirect",
], ]);
{ env: environment(), stdio: ["ignore", "pipe", "pipe"] }, return await fs.readlink(gcRoot);
});
if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+\.drv$/.test(derivation))
throw new Error("Nix returned an invalid candidate derivation");
const artifact = await timed("nixBuildMs", () =>
run("nix", ["build", derivation + "^*", "--no-link", "--print-out-paths", "-L"]),
); );
log.on("error", (error) => { if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact))
failure = error; throw new Error("Nix returned an invalid candidate artifact");
child.kill(); return artifact;
}); } finally {
child.stdout.on("data", (chunk) => { await new Promise<void>((resolve) => log.end(resolve));
output += chunk; if (gcDirectory) await fs.rm(gcDirectory, { recursive: true, force: true });
}); }
child.stderr.on("data", (chunk) => {
log.write(chunk);
tail = (tail + String(chunk)).slice(-6000);
});
child.on("error", (error) => {
failure = error;
});
child.on("close", (code) =>
log.end(() => {
if (failure) reject(failure);
else if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(output.trim()))
reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
else resolve(output.trim());
}),
);
});
} }
@@ -0,0 +1,40 @@
import { readFile } from "node:fs/promises";
import type { CheckedResourceResolver } from "./assembly.js";
import type { CapabilityResource } from "./parser.js";
/** Internal compiler inputs produced by checked-candidate.nix, not authoring
* receipts. The Nix composition owns their derivations and exact toolchain.
* Ordinary compiler callers omit this map and run all checks directly. */
export async function loadCheckedResources(filename?: string): Promise<CheckedResourceResolver | undefined> {
if (!filename) return undefined;
const entries = JSON.parse(await readFile(filename, "utf8")) as Array<{
kind: string;
repository: string;
commit: string;
artifact: string;
}>;
const resources = new Map<string, string>();
for (const entry of entries) {
const key = `${entry.kind}\0${entry.repository}\0${entry.commit}`;
if (resources.has(key)) throw new Error("Repeated checked resource input");
if (!/^\/nix\/store\/[^/]+$/.test(entry.artifact))
throw new Error("Checked resources must be immutable Nix outputs");
resources.set(key, entry.artifact);
}
return async (kind, source, lock, dependencies) => {
const artifact = resources.get(`${kind}\0${source.repository}\0${source.commit}`);
if (!artifact) return undefined;
const graph = JSON.parse(await readFile(`${artifact}/graph.json`, "utf8"));
const expected = [...dependencies].map(([binding, node]) => ({ binding, resourceKey: node.key }));
if (
graph.quixos.commit !== lock.quixos.commit ||
graph.quixos.repository !== lock.quixos.repository ||
graph.root.kind !== kind ||
graph.root.source.repository !== source.repository ||
graph.root.source.commit !== source.commit ||
JSON.stringify(graph.root.dependencies) !== JSON.stringify(expected)
)
throw new Error("Checked resource input does not match its locked dependency graph");
return JSON.parse(await readFile(`${artifact}/candidate.json`, "utf8")) as CapabilityResource;
};
}
+52
View File
@@ -0,0 +1,52 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
const execFile = promisify(callback);
export const nixLiteral = (value: string): string => JSON.stringify(value).replace(/\$\{/g, "\\${");
/** Recognize the deliberately small scaffold edit surface, never evaluate Nix.
* Authors may replace it with arbitrary Nix; then scaffolding declines the edit. */
export async function componentEntries(source: string): Promise<Record<string, { entry: string; export: string }>> {
const { stdout } = await execFile("nix-instantiate", ["--parse", "--expr", source], { maxBuffer: 1024 * 1024 });
const tokens = stdout.match(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]/g) ?? [];
if (stdout.replace(/"(?:\\.|[^"\\])*"|[A-Za-z_][A-Za-z0-9_-]*|[{}=;]|\s+/g, ""))
throw Error("components.nix is outside the supported literal-entry shape");
let cursor = 0;
const expect = (token: string) => {
if (tokens[cursor++] !== token) throw Error("components.nix is outside the supported literal-entry shape");
};
const string = () => {
const token = tokens[cursor++];
if (!token?.startsWith('"')) throw Error("Component entries must use literal strings");
const json = token.replace(/(\\*)\$\{/g, (_match, escapes: string) => {
if (escapes.length % 2 === 0) throw Error("Component entries must use literal strings");
return escapes.slice(1) + "${";
});
return JSON.parse(json) as string;
};
const entries: Record<string, { entry: string; export: string }> = Object.create(null);
expect("{");
while (tokens[cursor] !== "}") {
const id = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
if (!id) throw Error("Missing component identity");
if (Object.hasOwn(entries, id)) throw Error("Duplicate component entry");
expect("=");
expect("{");
const properties: Record<string, string> = Object.create(null);
while (tokens[cursor] !== "}") {
const key = tokens[cursor]?.startsWith('"') ? string() : tokens[cursor++];
if (!key || !["entry", "export"].includes(key) || Object.hasOwn(properties, key))
throw Error("Unsupported component entry property");
expect("=");
properties[key] = string();
expect(";");
}
expect("}");
expect(";");
if (!properties.entry || !properties.export) throw Error("Component entry needs entry and export");
entries[id] = { entry: properties.entry, export: properties.export };
}
expect("}");
if (cursor !== tokens.length || !/\n}\s*$/.test(source))
throw Error("components.nix is outside the supported scaffold shape");
return entries;
}
@@ -0,0 +1,100 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parseQx, walkSyntax } from "./source.js";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { componentEntries, nixLiteral } from "./component-nix.js";
/** Add one ordinary component entry without regenerating existing application code. */
export async function scaffoldComponent(
root: string,
prefix: string,
spec: ScaffoldRecipe,
): Promise<StructuralRequest> {
if (!spec.declaration || !spec.name || !/^[A-Z][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
throw Error("Component scaffold requires a PascalCase name, ID and QX declaration");
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
const syntax = parseQx(source);
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "moduleExport");
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
if (
syntax.diagnostics.length ||
syntax.root.children.length !== 1 ||
declarations.length !== 1 ||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1
)
throw Error("Expected exactly one component declaration");
const declaration = declarations[0]!;
const identifier = declaration.children.find((node) => node.kind === "identifier");
const id = declaration.children.find((node) => node.kind === "stringLiteral");
if (
!identifier ||
source.slice(identifier.start, identifier.end) !== spec.name ||
!id ||
JSON.parse(source.slice(id.start, id.end)) !== spec.id
)
throw Error("Component declaration name/ID must match its registration");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const existing = parseQx(authored);
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
if (existing.diagnostics.length || !parent) throw Error("Fix package.qx before adding a component");
for (const item of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
const name = item.children.find((node) => node.kind === "identifier");
const literal = item.children.find((node) => node.kind === "stringLiteral");
if (
(name && authored.slice(name.start, name.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw Error("Component requires a unique name and ID");
}
const filename = "components.nix";
const before = await fs.readFile(path.join(root, prefix, filename), "utf8");
const entries = await componentEntries(before);
if (!entries || typeof entries !== "object" || Array.isArray(entries) || Object.hasOwn(entries, spec.id))
throw Error("Invalid or duplicate component entry");
const insertion = ` ${nixLiteral(spec.id)} = { entry = ${nixLiteral(`src/components/${spec.name}.tsx`)}; export = ${nixLiteral(spec.name)}; };\n`;
const after = before.replace(/\n}(\s*)$/, (_match, trailing) => "\n" + insertion + "}" + trailing);
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
const configFile = prefix + "quixos.check.json";
const configBefore = await fs.readFile(path.join(root, configFile), "utf8");
const config = JSON.parse(configBefore);
config.queryFrontends = [...new Set([...(config.queryFrontends ?? []), "react"])];
const preparationFile = prefix + "quixos.react.json";
let preparationBefore: string | undefined;
try {
preparationBefore = await fs.readFile(path.join(root, preparationFile), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const preparation = preparationBefore ? JSON.parse(preparationBefore) : { modules: {} };
if (Object.hasOwn(preparation.modules, spec.name)) throw Error("Duplicate presentation preparation");
preparation.modules[spec.name] = { inputs: [] };
const preparationText = JSON.stringify(preparation, null, 2) + "\n";
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [
preparationBefore === undefined
? { file: preparationFile, create: preparationText }
: { file: preparationFile, expected: preparationBefore, replace: preparationText },
{ file: configFile, expected: configBefore, replace: JSON.stringify(config, null, 2) + "\n" },
{
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
source: spec.declaration,
},
],
},
{ file: prefix + filename, expected: before, replace: after },
{
file: prefix + `src/components/${spec.name}.tsx`,
create: `import type { Modules } from "../gen/client.js";\n\n// Declare initial query inputs in quixos.react.json; use interfaces for actions.\nexport const ${spec.name}: Modules[${JSON.stringify(spec.name)}] = ({subject}) => <p>{subject.object}</p>;\n`,
},
],
};
}
@@ -0,0 +1,43 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
export async function scaffoldController(
root: string,
prefix: string,
spec: ScaffoldRecipe,
): Promise<StructuralRequest> {
const entry = { id: spec.id, ...spec.controller };
const fields = ["id", "subject", "snapshot", "remote", "input"];
if (
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
fields.some((key) => typeof (entry as Record<string, unknown>)[key] !== "string") ||
!["unit", "value"].includes(entry.input ?? "")
)
throw Error("Controller requires id, subject, snapshot, remote and input (unit/value)");
const file = prefix + "quixos.controllers.json";
let before: string | undefined;
try {
before = await fs.readFile(path.join(root, file), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const catalog = before ? JSON.parse(before) : { schemaVersion: 1, controllers: [] };
if (
catalog.schemaVersion !== 1 ||
!Array.isArray(catalog.controllers) ||
catalog.controllers.length >= 1000 ||
catalog.controllers.some((c: { id: string }) => c.id === entry.id)
)
throw Error("Invalid catalog or duplicate controller ID");
catalog.controllers.push(entry);
const content = JSON.stringify(catalog, null, 2) + "\n";
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [before === undefined ? { file, create: content } : { file, expected: before, replace: content }],
};
}
@@ -0,0 +1,208 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
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("${", "\\${");
export async function isExecutionScaffold(root: string, prefix: string): Promise<boolean> {
try {
return JSON.parse(await fs.readFile(path.join(root, prefix, "quixos.check.json"), "utf8")).backend === "execution";
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
throw error;
}
}
/** Insert ordinary authored code; declarations remain the only export inventory. */
export async function executionScaffold(
root: string,
command: string,
spec: ScaffoldRecipe,
prefix: string,
nixSource: (source: NonNullable<ScaffoldRecipe["tools"]>["helpers"]) => string,
): Promise<StructuralRequest> {
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
throw new Error("Execution scaffold requires a simple name and an ID");
if (spec.initialFiles && command !== "package") throw new Error("Initial files are only for new packages");
const files: StructuralRequest["files"] = [];
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",
`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: [] }),
);
create("quixos.check.json", json({ backend: "execution" }));
create("components.nix", "{\n}\n");
create(
"package.json",
json({
name: `@quixos/${spec.name.toLowerCase()}`,
private: true,
type: "module",
packageManager: "yarn@4.18.0",
scripts: { typecheck: "qx-workspace check" },
}),
);
create(
".gitignore",
"node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n.yarn/cache/\n.yarn/install-state.gz\n.yarn/unplugged/\n",
);
create(
".yarnrc.yml",
'enableScripts: true\n\ngenerateDefaultNix: false\n\nindividualNixPackaging: true\n\nnodeLinker: node-modules\n\nsupportedArchitectures:\n os:\n - current\n - linux\n cpu:\n - current\n - x64\n - arm64\n libc:\n - current\n - glibc\n\nplugins:\n - checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b\n path: .yarn/plugins/yarn-plugin-nixify.cjs\n spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"\n',
);
for (const lane of ["browser", "server", "remote"])
create(
`src/${lane}.ts`,
`import { createRegistry } from "./gen/${lane === "remote" ? "remote" : "qx"}.js";\nexport const registry = createRegistry({});\n`,
);
create(
"flake.nix",
`{
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; };
outputs = { nixpkgs, flake-utils, helpers, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; };
camino = import (helpers + "/quixos-package-helpers.nix");
in {
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
quixosPackages.checkedArtifacts = context:
let
pkgs = camino.packageSet { inherit nixpkgs system context; };
contract = builtins.fromJSON (builtins.readFile context.buildContract);
package = contract.definition.package;
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
modules = import ./components.nix;
dependencies = if builtins.pathExists ./yarn-project.nix
then camino.mkCaminoYarnDependencies { inherit pkgs; src = ./.; } else null;
in camino.assembleCaminoPackage {
inherit pkgs context;
artifacts =
pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage {
inherit pkgs context dependencies; src = ./.;
entry = "src/server.ts"; entries.browser = "src/browser.ts";
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 {
inherit pkgs context dependencies modules; src = ./.;
});
};
});
}
`,
);
create(
"README.md",
`# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`,
);
} else if (command === "function") {
if (!spec.declaration)
throw new Error("Execution functions require a declaration with explicit execution effect and targets");
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${spec.declaration} }`;
const parsed = parseQx(wrapped);
const pkg = parsed.root.children.find((node) => node.kind === "packageResourceDecl");
const exports = [...walkSyntax(parsed.root)].filter((node) =>
["packageFunctionExport", "packageOperationExport"].includes(node.kind),
);
const declaration = exports[0];
if (
parsed.diagnostics.length ||
exports.length !== 1 ||
!declaration ||
parsed.root.children.length !== 1 ||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1 ||
pkg?.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
)
throw new Error("Expected one function or operation export with an execution contract");
const text = (node: { start: number; end: number }) => wrapped.slice(node.start, node.end);
const name = declaration.children.find((node) => node.kind === "identifier");
const id = declaration.children.find((node) => node.kind === "stringLiteral");
const execution = declaration.children.find((node) => node.kind === "executionClause");
if (!name || text(name) !== spec.name || !id || JSON.parse(text(id)) !== spec.id || !execution)
throw new Error("Declaration name/ID must match; an explicit execution contract is required");
const tokens = parsed.tokens
.filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end)
.map((token) => token.text);
const remote = tokens[1] === "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"] : ["JavaScript", "server"]).includes(target))
)
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");
if (syntax.diagnostics.length || !packageNode) throw new Error("Fix package.qx before adding a handler");
for (const node of [...walkSyntax(packageNode)].filter((node) =>
["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind),
)) {
const identifier = node.children.find((child) => child.kind === "identifier");
const literal = node.children.find((child) => child.kind === "stringLiteral");
if (
(identifier && authored.slice(identifier.start, identifier.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw new Error("New export requires a unique name and ID");
}
const packageID = packageNode.children.find((node) => node.kind === "stringLiteral")!;
files.push({
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(packageID.start, packageID.end)) },
source: spec.declaration,
},
],
});
create(
`src/impl/${spec.name}.ts`,
`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 === "JavaScript" ? "browser" : target}.ts`;
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
expected: before,
replace: addImplementation(before, "createRegistry", spec.name, `./impl/${spec.name}.js`),
});
}
} else
throw new Error("Execution packages use ordinary factory transactions; migration scaffolding is not supported");
if (spec.initialFiles) {
for (const [file, content] of Object.entries(spec.initialFiles)) {
if (
typeof content !== "string" ||
!/^(?:package\.qx|(?:package|components|flake|yarn-project)\.nix|package\.json|yarn\.lock|\.yarnrc\.yml|\.gitignore|\.yarn\/plugins\/[A-Za-z0-9_.-]+\.cjs|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test(
file,
) ||
file.split("/").some((part) => part === ".." || part === "." || part === "gen")
)
throw new Error("Not an initial authored execution file: " + file);
const previous = files.findIndex((entry) => entry.file === prefix + file);
if (previous >= 0) files.splice(previous, 1);
create(file, content);
}
}
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
}
File diff suppressed because one or more lines are too long
@@ -1,259 +1,311 @@
WORKSPACE=1 EDIT=1
RESULT_BYTES=2 MODULE=2
RPC_CALLS=3 CONTRACT=3
DEADLINE_MS=4 PRESENTATION=4
QUERY=5 SUBJECT=5
SPECIALIZE=6 FOR=6
ROOT=7 CREATION=7
DOCUMENT=8 VARIANT=8
FRAGMENTS=9 PREPARES=9
VIEW=10 INITIALIZE=10
MAX=11 WORKSPACE=11
ALLOW=12 EXECUTION=12
POLL=13 SNAPSHOT=13
QUERYABLE=14 TRANSACTION=14
RPC=15 REMOTE=15
QUERY_REASON=16 TARGETS=16
TYPE=17 PORTABLE=17
OBJECT=18 REPLICA=18
STORABLE=19 LOCAL=19
IMPLEMENTS=20 FROM=20
REF=21 RESULT_BYTES=21
FRAGMENT=22 RPC_CALLS=22
IMPORT=23 DEADLINE_MS=23
EXTERNAL=24 QUERY=24
ATOM=25 REFLECTION=25
INTERFACE=26 SPECIALIZE=26
INTERFACES=27 ROOT=27
PACKAGE=28 DOCUMENT=28
VALUE=29 FRAGMENTS=29
RELATION=30 VIEW=30
OPERATION=31 MAX=31
FUNCTION=32 ALLOW=32
CONSTRUCTOR=33 POLL=33
CONSTRUCTS=34 QUERYABLE=34
INPUT=35 RPC=35
CONFORM=36 QUERY_REASON=36
AS=37 TYPE=37
BIND=38 OBJECT=38
STATIC=39 STORABLE=39
TO=40 IMPLEMENTS=40
PRIVATE=41 REF=41
SHARED=42 FRAGMENT=42
STATE=43 IMPORT=43
EDGE=44 EXTERNAL=44
PROJECTION=45 SYMBOL=45
WITH=46 ATOM=46
USING=47 INTERFACE=47
VIA=48 INTERFACES=48
MATERIALIZE=49 PACKAGE=49
IF=50 VALUE=50
ABSENT=51 FIELD=51
ON=52 RELATION=52
POLICY=53 OPERATION=53
DEFAULT=54 FUNCTION=54
SOURCE=55 CONSTRUCTOR=55
REPOSITORY=56 CONSTRUCTS=56
COMMIT=57 INPUT=57
REVISION=58 CONFORM=58
SEMANTIC_MAJOR=59 AS=59
ON_DELETE=60 BIND=60
RETAIN_OTHER=61 STATIC=61
KEYED=62 TO=62
PUBLIC_TRAVERSAL=63 PRIVATE=63
ID=64 SHARED=64
DOC=65 STATE=65
MODE=66 EDGE=66
EMITS=67 PROJECTION=67
RECEIVER=68 WITH=68
REQUIRES=69 USING=69
ANY=70 ACCEPT=70
GET=71 VIA=71
SET=72 MATERIALIZE=72
WATCH=73 IF=73
START=74 ABSENT=74
STOP=75 ON=75
READ=76 POLICY=76
WRITE=77 DEFAULT=77
RESOLVE=78 SOURCE=78
CONNECT=79 REPOSITORY=79
DISCONNECT=80 COMMIT=80
CALL=81 REVISION=81
WATCH_START=82 SEMANTIC_MAJOR=82
WATCH_STOP=83 ON_DELETE=83
SUBSCRIBE=84 LIFECYCLE=84
UNSUBSCRIBE=85 JOURNAL=85
OPTIMISTIC_REGISTER=86 SUBSCRIPTION=86
CRDT=87 RETAIN_OTHER=87
OPTIONAL_ONE=88 KEYED=88
EXACTLY_ONE=89 PUBLIC_TRAVERSAL=89
MANY_UNIQUE=90 ID=90
MANY=91 DOC=91
ORDERED=92 MODE=92
UNIT=93 EMITS=93
WATCH_HANDLE=94 RECEIVER=94
MESSAGE=95 REQUIRES=95
ATOM_REF=96 ANY=96
INTERFACE_REF=97 GET=97
OPTIONAL=98 SET=98
LIST=99 WATCH=99
RECORD=100 START=100
BOOL=101 STOP=101
BYTES=102 READ=102
DOUBLE=103 WRITE=103
INT32=104 RESOLVE=104
INT64=105 CONNECT=105
STRING=106 DISCONNECT=106
UINT32=107 CALL=107
UINT64=108 WATCH_START=108
TRUE=109 WATCH_STOP=109
FALSE=110 SUBSCRIBE=110
NULL=111 UNSUBSCRIBE=111
ARROW=112 REGISTER=112
COLON=113 CRDT=113
SEMI=114 OPTIONAL_ONE=114
COMMA=115 EXACTLY_ONE=115
DOT=116 MANY_UNIQUE=116
LBRACE=117 MANY=117
RBRACE=118 ORDERED=118
LBRACK=119 UNIT=119
RBRACK=120 WATCH_HANDLE=120
LPAREN=121 MESSAGE=121
RPAREN=122 ATOM_REF=122
LT=123 INTERFACE_REF=123
GT=124 OPTIONAL=124
AMP=125 LIST=125
EQUAL=126 RECORD=126
INTEGER=127 BOOL=127
JSON_NUMBER=128 BYTES=128
IDENTIFIER=129 DOUBLE=129
STRING_LITERAL=130 INT32=130
LINE_COMMENT=131 INT64=131
BLOCK_COMMENT=132 STRING=132
WS=133 UINT32=133
'workspace'=1 UINT64=134
'result-bytes'=2 TRUE=135
'rpc-calls'=3 FALSE=136
'deadline-ms'=4 NULL=137
'query'=5 ARROW=138
'specialize'=6 COLON=139
'root'=7 SEMI=140
'document'=8 COMMA=141
'fragments'=9 DOT=142
'view'=10 LBRACE=143
'max'=11 RBRACE=144
'allow'=12 LBRACK=145
'poll'=13 RBRACK=146
'queryable'=14 LPAREN=147
'rpc'=15 RPAREN=148
'query-reason'=16 LT=149
'type'=17 GT=150
'object'=18 AMP=151
'storable'=19 EQUAL=152
'implements'=20 INTEGER=153
'ref'=21 JSON_NUMBER=154
'fragment'=22 IDENTIFIER=155
'import'=23 STRING_LITERAL=156
'external'=24 LINE_COMMENT=157
'atom'=25 BLOCK_COMMENT=158
'interface'=26 WS=159
'interfaces'=27 'edit'=1
'package'=28 'module'=2
'value'=29 'contract'=3
'relation'=30 'presentation'=4
'operation'=31 'subject'=5
'function'=32 'for'=6
'constructor'=33 'creation'=7
'constructs'=34 'variant'=8
'input'=35 'prepares'=9
'conform'=36 'initialize'=10
'as'=37 'workspace'=11
'bind'=38 'execution'=12
'static'=39 'snapshot'=13
'to'=40 'transaction'=14
'private'=41 'remote'=15
'shared'=42 'targets'=16
'state'=43 'portable'=17
'edge'=44 'replica'=18
'projection'=45 'local'=19
'with'=46 'from'=20
'using'=47 'result-bytes'=21
'via'=48 'rpc-calls'=22
'materialize'=49 'deadline-ms'=23
'if'=50 'query'=24
'absent'=51 'reflection'=25
'on'=52 'specialize'=26
'policy'=53 'root'=27
'default'=54 'document'=28
'source'=55 'fragments'=29
'repository'=56 'view'=30
'commit'=57 'max'=31
'revision'=58 'allow'=32
'semantic-major'=59 'poll'=33
'on-delete'=60 'queryable'=34
'retain-other'=61 'rpc'=35
'keyed'=62 'query-reason'=36
'public-traversal'=63 'type'=37
'id'=64 'object'=38
'doc'=65 'storable'=39
'mode'=66 'implements'=40
'emits'=67 'ref'=41
'receiver'=68 'fragment'=42
'requires'=69 'import'=43
'any'=70 'external'=44
'get'=71 'symbol'=45
'set'=72 'atom'=46
'watch'=73 'interface'=47
'start'=74 'interfaces'=48
'stop'=75 'package'=49
'read'=76 'value'=50
'write'=77 'field'=51
'resolve'=78 'relation'=52
'connect'=79 'operation'=53
'disconnect'=80 'function'=54
'call'=81 'constructor'=55
'watch-start'=82 'constructs'=56
'watch-stop'=83 'input'=57
'subscribe'=84 'conform'=58
'unsubscribe'=85 'as'=59
'optimistic-register'=86 'bind'=60
'crdt'=87 'static'=61
'optional-one'=88 'to'=62
'exactly-one'=89 'private'=63
'many-unique'=90 'shared'=64
'many'=91 'state'=65
'ordered'=92 'edge'=66
'unit'=93 'projection'=67
'watch-handle'=94 'with'=68
'message'=95 'using'=69
'atom-ref'=96 'accept'=70
'interface-ref'=97 'via'=71
'optional'=98 'materialize'=72
'list'=99 'if'=73
'record'=100 'absent'=74
'bool'=101 'on'=75
'bytes'=102 'policy'=76
'double'=103 'default'=77
'int32'=104 'source'=78
'int64'=105 'repository'=79
'string'=106 'commit'=80
'uint32'=107 'revision'=81
'uint64'=108 'semantic-major'=82
'true'=109 'on-delete'=83
'false'=110 'lifecycle'=84
'null'=111 'journal'=85
'->'=112 'subscription'=86
':'=113 'retain-other'=87
';'=114 'keyed'=88
','=115 'public-traversal'=89
'.'=116 'id'=90
'{'=117 'doc'=91
'}'=118 'mode'=92
'['=119 'emits'=93
']'=120 'receiver'=94
'('=121 'requires'=95
')'=122 'any'=96
'<'=123 'get'=97
'>'=124 'set'=98
'&'=125 'watch'=99
'='=126 '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
@@ -1,259 +1,311 @@
WORKSPACE=1 EDIT=1
RESULT_BYTES=2 MODULE=2
RPC_CALLS=3 CONTRACT=3
DEADLINE_MS=4 PRESENTATION=4
QUERY=5 SUBJECT=5
SPECIALIZE=6 FOR=6
ROOT=7 CREATION=7
DOCUMENT=8 VARIANT=8
FRAGMENTS=9 PREPARES=9
VIEW=10 INITIALIZE=10
MAX=11 WORKSPACE=11
ALLOW=12 EXECUTION=12
POLL=13 SNAPSHOT=13
QUERYABLE=14 TRANSACTION=14
RPC=15 REMOTE=15
QUERY_REASON=16 TARGETS=16
TYPE=17 PORTABLE=17
OBJECT=18 REPLICA=18
STORABLE=19 LOCAL=19
IMPLEMENTS=20 FROM=20
REF=21 RESULT_BYTES=21
FRAGMENT=22 RPC_CALLS=22
IMPORT=23 DEADLINE_MS=23
EXTERNAL=24 QUERY=24
ATOM=25 REFLECTION=25
INTERFACE=26 SPECIALIZE=26
INTERFACES=27 ROOT=27
PACKAGE=28 DOCUMENT=28
VALUE=29 FRAGMENTS=29
RELATION=30 VIEW=30
OPERATION=31 MAX=31
FUNCTION=32 ALLOW=32
CONSTRUCTOR=33 POLL=33
CONSTRUCTS=34 QUERYABLE=34
INPUT=35 RPC=35
CONFORM=36 QUERY_REASON=36
AS=37 TYPE=37
BIND=38 OBJECT=38
STATIC=39 STORABLE=39
TO=40 IMPLEMENTS=40
PRIVATE=41 REF=41
SHARED=42 FRAGMENT=42
STATE=43 IMPORT=43
EDGE=44 EXTERNAL=44
PROJECTION=45 SYMBOL=45
WITH=46 ATOM=46
USING=47 INTERFACE=47
VIA=48 INTERFACES=48
MATERIALIZE=49 PACKAGE=49
IF=50 VALUE=50
ABSENT=51 FIELD=51
ON=52 RELATION=52
POLICY=53 OPERATION=53
DEFAULT=54 FUNCTION=54
SOURCE=55 CONSTRUCTOR=55
REPOSITORY=56 CONSTRUCTS=56
COMMIT=57 INPUT=57
REVISION=58 CONFORM=58
SEMANTIC_MAJOR=59 AS=59
ON_DELETE=60 BIND=60
RETAIN_OTHER=61 STATIC=61
KEYED=62 TO=62
PUBLIC_TRAVERSAL=63 PRIVATE=63
ID=64 SHARED=64
DOC=65 STATE=65
MODE=66 EDGE=66
EMITS=67 PROJECTION=67
RECEIVER=68 WITH=68
REQUIRES=69 USING=69
ANY=70 ACCEPT=70
GET=71 VIA=71
SET=72 MATERIALIZE=72
WATCH=73 IF=73
START=74 ABSENT=74
STOP=75 ON=75
READ=76 POLICY=76
WRITE=77 DEFAULT=77
RESOLVE=78 SOURCE=78
CONNECT=79 REPOSITORY=79
DISCONNECT=80 COMMIT=80
CALL=81 REVISION=81
WATCH_START=82 SEMANTIC_MAJOR=82
WATCH_STOP=83 ON_DELETE=83
SUBSCRIBE=84 LIFECYCLE=84
UNSUBSCRIBE=85 JOURNAL=85
OPTIMISTIC_REGISTER=86 SUBSCRIPTION=86
CRDT=87 RETAIN_OTHER=87
OPTIONAL_ONE=88 KEYED=88
EXACTLY_ONE=89 PUBLIC_TRAVERSAL=89
MANY_UNIQUE=90 ID=90
MANY=91 DOC=91
ORDERED=92 MODE=92
UNIT=93 EMITS=93
WATCH_HANDLE=94 RECEIVER=94
MESSAGE=95 REQUIRES=95
ATOM_REF=96 ANY=96
INTERFACE_REF=97 GET=97
OPTIONAL=98 SET=98
LIST=99 WATCH=99
RECORD=100 START=100
BOOL=101 STOP=101
BYTES=102 READ=102
DOUBLE=103 WRITE=103
INT32=104 RESOLVE=104
INT64=105 CONNECT=105
STRING=106 DISCONNECT=106
UINT32=107 CALL=107
UINT64=108 WATCH_START=108
TRUE=109 WATCH_STOP=109
FALSE=110 SUBSCRIBE=110
NULL=111 UNSUBSCRIBE=111
ARROW=112 REGISTER=112
COLON=113 CRDT=113
SEMI=114 OPTIONAL_ONE=114
COMMA=115 EXACTLY_ONE=115
DOT=116 MANY_UNIQUE=116
LBRACE=117 MANY=117
RBRACE=118 ORDERED=118
LBRACK=119 UNIT=119
RBRACK=120 WATCH_HANDLE=120
LPAREN=121 MESSAGE=121
RPAREN=122 ATOM_REF=122
LT=123 INTERFACE_REF=123
GT=124 OPTIONAL=124
AMP=125 LIST=125
EQUAL=126 RECORD=126
INTEGER=127 BOOL=127
JSON_NUMBER=128 BYTES=128
IDENTIFIER=129 DOUBLE=129
STRING_LITERAL=130 INT32=130
LINE_COMMENT=131 INT64=131
BLOCK_COMMENT=132 STRING=132
WS=133 UINT32=133
'workspace'=1 UINT64=134
'result-bytes'=2 TRUE=135
'rpc-calls'=3 FALSE=136
'deadline-ms'=4 NULL=137
'query'=5 ARROW=138
'specialize'=6 COLON=139
'root'=7 SEMI=140
'document'=8 COMMA=141
'fragments'=9 DOT=142
'view'=10 LBRACE=143
'max'=11 RBRACE=144
'allow'=12 LBRACK=145
'poll'=13 RBRACK=146
'queryable'=14 LPAREN=147
'rpc'=15 RPAREN=148
'query-reason'=16 LT=149
'type'=17 GT=150
'object'=18 AMP=151
'storable'=19 EQUAL=152
'implements'=20 INTEGER=153
'ref'=21 JSON_NUMBER=154
'fragment'=22 IDENTIFIER=155
'import'=23 STRING_LITERAL=156
'external'=24 LINE_COMMENT=157
'atom'=25 BLOCK_COMMENT=158
'interface'=26 WS=159
'interfaces'=27 'edit'=1
'package'=28 'module'=2
'value'=29 'contract'=3
'relation'=30 'presentation'=4
'operation'=31 'subject'=5
'function'=32 'for'=6
'constructor'=33 'creation'=7
'constructs'=34 'variant'=8
'input'=35 'prepares'=9
'conform'=36 'initialize'=10
'as'=37 'workspace'=11
'bind'=38 'execution'=12
'static'=39 'snapshot'=13
'to'=40 'transaction'=14
'private'=41 'remote'=15
'shared'=42 'targets'=16
'state'=43 'portable'=17
'edge'=44 'replica'=18
'projection'=45 'local'=19
'with'=46 'from'=20
'using'=47 'result-bytes'=21
'via'=48 'rpc-calls'=22
'materialize'=49 'deadline-ms'=23
'if'=50 'query'=24
'absent'=51 'reflection'=25
'on'=52 'specialize'=26
'policy'=53 'root'=27
'default'=54 'document'=28
'source'=55 'fragments'=29
'repository'=56 'view'=30
'commit'=57 'max'=31
'revision'=58 'allow'=32
'semantic-major'=59 'poll'=33
'on-delete'=60 'queryable'=34
'retain-other'=61 'rpc'=35
'keyed'=62 'query-reason'=36
'public-traversal'=63 'type'=37
'id'=64 'object'=38
'doc'=65 'storable'=39
'mode'=66 'implements'=40
'emits'=67 'ref'=41
'receiver'=68 'fragment'=42
'requires'=69 'import'=43
'any'=70 'external'=44
'get'=71 'symbol'=45
'set'=72 'atom'=46
'watch'=73 'interface'=47
'start'=74 'interfaces'=48
'stop'=75 'package'=49
'read'=76 'value'=50
'write'=77 'field'=51
'resolve'=78 'relation'=52
'connect'=79 'operation'=53
'disconnect'=80 'function'=54
'call'=81 'constructor'=55
'watch-start'=82 'constructs'=56
'watch-stop'=83 'input'=57
'subscribe'=84 'conform'=58
'unsubscribe'=85 'as'=59
'optimistic-register'=86 'bind'=60
'crdt'=87 'static'=61
'optional-one'=88 'to'=62
'exactly-one'=89 'private'=63
'many-unique'=90 'shared'=64
'many'=91 'state'=65
'ordered'=92 'edge'=66
'unit'=93 'projection'=67
'watch-handle'=94 'with'=68
'message'=95 'using'=69
'atom-ref'=96 'accept'=70
'interface-ref'=97 'via'=71
'optional'=98 'materialize'=72
'list'=99 'if'=73
'record'=100 'absent'=74
'bool'=101 'on'=75
'bytes'=102 'policy'=76
'double'=103 'default'=77
'int32'=104 'source'=78
'int64'=105 'repository'=79
'string'=106 'commit'=80
'uint32'=107 'revision'=81
'uint64'=108 'semantic-major'=82
'true'=109 'on-delete'=83
'false'=110 'lifecycle'=84
'null'=111 'journal'=85
'->'=112 'subscription'=86
':'=113 'retain-other'=87
';'=114 'keyed'=88
','=115 'public-traversal'=89
'.'=116 'id'=90
'{'=117 'doc'=91
'}'=118 'mode'=92
'['=119 'emits'=93
']'=120 'receiver'=94
'('=121 'requires'=95
')'=122 'any'=96
'<'=123 'get'=97
'>'=124 'set'=98
'&'=125 'watch'=99
'='=126 '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 { ResourceImportDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalAtomDeclContext } from "./QuixosCapabilityParser.js"; import { ExternalAtomDeclContext } from "./QuixosCapabilityParser.js";
import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js"; import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
import { SymbolDeclContext } from "./QuixosCapabilityParser.js";
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js"; import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
import { AtomDeclContext } from "./QuixosCapabilityParser.js"; import { AtomDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js"; import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
@@ -20,6 +21,7 @@ import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentContext } from "./QuixosCapabilityParser.js"; import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js"; import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js"; import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
import { ModuleMemberContext } from "./QuixosCapabilityParser.js";
import { OperationMemberContext } from "./QuixosCapabilityParser.js"; import { OperationMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberContext } from "./QuixosCapabilityParser.js"; import { ValueMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js"; import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js";
@@ -29,11 +31,18 @@ import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
import { TargetConstraintContext } from "./QuixosCapabilityParser.js"; import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js"; import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js"; import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { ModuleExportContext } from "./QuixosCapabilityParser.js";
import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js"; import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js"; import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js"; import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js"; import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js"; import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { PreparationClauseContext } from "./QuixosCapabilityParser.js";
import { ExecutionClauseContext } from "./QuixosCapabilityParser.js";
import { SelectedOperationsContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js"; import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
import { EventClauseContext } from "./QuixosCapabilityParser.js"; import { EventClauseContext } from "./QuixosCapabilityParser.js";
import { OperationModeContext } from "./QuixosCapabilityParser.js"; import { OperationModeContext } from "./QuixosCapabilityParser.js";
@@ -50,13 +59,19 @@ import { StoragePolicyContext } from "./QuixosCapabilityParser.js";
import { EdgeDeclContext } from "./QuixosCapabilityParser.js"; import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js"; import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js"; import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
import { ConformanceSourceArgumentsContext } from "./QuixosCapabilityParser.js";
import { ConformanceItemContext } from "./QuixosCapabilityParser.js"; import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
import { ModuleBindingDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationFieldContext } from "./QuixosCapabilityParser.js";
import { CreationOwnerMappingContext } from "./QuixosCapabilityParser.js";
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js"; import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js"; import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js"; import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js"; import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
import { OperationNameContext } from "./QuixosCapabilityParser.js"; import { OperationNameContext } from "./QuixosCapabilityParser.js";
import { OperationProviderContext } from "./QuixosCapabilityParser.js"; import { OperationProviderContext } from "./QuixosCapabilityParser.js";
import { AcceptanceBindingContext } from "./QuixosCapabilityParser.js";
import { StatePrimitiveContext } from "./QuixosCapabilityParser.js"; import { StatePrimitiveContext } from "./QuixosCapabilityParser.js";
import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js"; import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js"; import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
@@ -64,6 +79,7 @@ import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js"; import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
import { ValueTypeContext } from "./QuixosCapabilityParser.js"; import { ValueTypeContext } from "./QuixosCapabilityParser.js";
import { RecordFieldContext } from "./QuixosCapabilityParser.js"; import { RecordFieldContext } from "./QuixosCapabilityParser.js";
import { VariantCaseContext } from "./QuixosCapabilityParser.js";
import { ScalarTypeContext } from "./QuixosCapabilityParser.js"; import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
import { CardinalityContext } from "./QuixosCapabilityParser.js"; import { CardinalityContext } from "./QuixosCapabilityParser.js";
import { JsonLiteralContext } from "./QuixosCapabilityParser.js"; import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
@@ -130,6 +146,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitExternalInterfaceDecl?: (ctx: ExternalInterfaceDeclContext) => 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`. * Visit a parse tree produced by `QuixosCapabilityParser.resourcePreamble`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -190,6 +212,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitInterfaceMember?: (ctx: InterfaceMemberContext) => Result; visitInterfaceMember?: (ctx: InterfaceMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleMember`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleMember?: (ctx: ModuleMemberContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.operationMember`. * Visit a parse tree produced by `QuixosCapabilityParser.operationMember`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -244,6 +272,18 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitPackageExport?: (ctx: PackageExportContext) => Result; visitPackageExport?: (ctx: PackageExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleExport`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleExport?: (ctx: ModuleExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.presentationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPresentationDecl?: (ctx: PresentationDeclContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`. * Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -262,6 +302,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitQueryClause?: (ctx: QueryClauseContext) => Result; visitQueryClause?: (ctx: QueryClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentVariable`.
* @param ctx the parse tree
* @return the visitor result
*/
visitFragmentVariable?: (ctx: FragmentVariableContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`. * Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -274,6 +320,30 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => 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.preparationClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPreparationClause?: (ctx: PreparationClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.executionClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExecutionClause?: (ctx: ExecutionClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.selectedOperations`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSelectedOperations?: (ctx: SelectedOperationsContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.packageConstructorExport`. * Visit a parse tree produced by `QuixosCapabilityParser.packageConstructorExport`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -370,12 +440,42 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitConformanceDecl?: (ctx: ConformanceDeclContext) => Result; visitConformanceDecl?: (ctx: ConformanceDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceSourceArguments`.
* @param ctx the parse tree
* @return the visitor result
*/
visitConformanceSourceArguments?: (ctx: ConformanceSourceArgumentsContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceItem`. * Visit a parse tree produced by `QuixosCapabilityParser.conformanceItem`.
* @param ctx the parse tree * @param ctx the parse tree
* @return the visitor result * @return the visitor result
*/ */
visitConformanceItem?: (ctx: ConformanceItemContext) => Result; visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleBindingDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleBindingDecl?: (ctx: ModuleBindingDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationDecl?: (ctx: InitializationDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationField`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationField?: (ctx: InitializationFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.creationOwnerMapping`.
* @param ctx the parse tree
* @return the visitor result
*/
visitCreationOwnerMapping?: (ctx: CreationOwnerMappingContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`. * Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -412,6 +512,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitOperationProvider?: (ctx: OperationProviderContext) => 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`. * Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -454,6 +560,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitRecordField?: (ctx: RecordFieldContext) => Result; visitRecordField?: (ctx: RecordFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.variantCase`.
* @param ctx the parse tree
* @return the visitor result
*/
visitVariantCase?: (ctx: VariantCaseContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`. * Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
* @param ctx the parse tree * @param ctx the parse tree
+28 -1
View File
@@ -34,6 +34,7 @@ export class GenericSourceTypes {
readonly applications = new Map<string, InterfaceRevision>(); readonly applications = new Map<string, InterfaceRevision>();
readonly aliases = new Map<string, ValueAliasDefinition>(); readonly aliases = new Map<string, ValueAliasDefinition>();
parameters = new Map<string, TypeParameter>(); parameters = new Map<string, TypeParameter>();
bindings = new Map<string, ClosedTypeArgument>();
self?: AtomId; self?: AtomId;
private active: string[] = []; private active: string[] = [];
private applicationCount = 0; private applicationCount = 0;
@@ -42,7 +43,7 @@ export class GenericSourceTypes {
environment(): GenericTypeEnvironment { environment(): GenericTypeEnvironment {
return { return {
arguments: new Map(), arguments: this.bindings,
aliases: this.aliases, aliases: this.aliases,
self: this.self, self: this.self,
applyInterface: (id, args) => this.apply(id, args).revisionId, applyInterface: (id, args) => this.apply(id, args).revisionId,
@@ -108,6 +109,8 @@ export class GenericSourceTypes {
return parameter?.kind === "value" && Boolean(parameter.storable); return parameter?.kind === "value" && Boolean(parameter.storable);
} }
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1); if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
if (type.kind === "variant") return Object.values(type.cases).every((entry) => storable(entry.value, depth + 1));
if (type.kind === "alias") { if (type.kind === "alias") {
const alias = aliases.get(type.definitionId); const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false; if (!alias || alias.parameters.length !== type.arguments.length) return false;
@@ -299,6 +302,24 @@ export class GenericSourceTypes {
} }
expression(context: ValueTypeContext): ValueTypeExpression { expression(context: ValueTypeContext): ValueTypeExpression {
if (context.VARIANT()) {
const entries = context.variantCase().map(
(entry) =>
[
entry.identifier().getText(),
{
id: literal(entry.stringLiteral()),
value: this.expression(entry.valueType()),
},
] as const,
);
if (
new Set(entries.map(([label]) => label)).size !== entries.length ||
new Set(entries.map(([, entry]) => entry.id)).size !== entries.length
)
throw new GenericTypeError("duplicate-variant-case", "variant", "Variant labels and case IDs must be unique");
return { kind: "variant", id: literal(context.stringLiteral()!), cases: Object.fromEntries(entries) };
}
if (context.scalarType()) if (context.scalarType())
return { return {
kind: "scalar", kind: "scalar",
@@ -318,6 +339,12 @@ export class GenericSourceTypes {
return { kind: "record", fields: Object.fromEntries(fields) }; return { kind: "record", fields: Object.fromEntries(fields) };
} }
const name = context.identifier()!.getText(); const name = context.identifier()!.getText();
if (name === "change" && context.typeArguments()) {
const args = this.arguments(context.typeArguments());
if (args.length !== 1 || args[0]!.kind !== "value")
throw new GenericTypeError("invalid-change-type", name, "change<T> requires one materialized value type");
return { kind: "change", value: args[0]!.type };
}
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) }; if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
if (context.INTERFACE_REF()) if (context.INTERFACE_REF())
return { return {
+1 -1
View File
@@ -38,7 +38,7 @@ export const createGitCapabilityResolver = async (options: {
}>; }>;
}; };
for (const entry of document.resources ?? []) { for (const entry of document.resources ?? []) {
if (entry.kind !== "interface" && entry.kind !== "package") { if (entry.kind !== "interface" && entry.kind !== "package" && entry.kind !== "build") {
throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`); throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`);
} }
const key = sourceKey(entry.kind, entry.repository, entry.commit); const key = sourceKey(entry.kind, entry.repository, entry.commit);
@@ -7,7 +7,7 @@ const node = (value: unknown): value is Node =>
* Ambiguous/custom wiring is left alone with an actionable error. */ * Ambiguous/custom wiring is left alone with an actionable error. */
export function addImplementation( export function addImplementation(
text: string, text: string,
factory: "createRuntime" | "serveMigration", factory: "createRuntime" | "serveMigration" | "createRegistry",
key: string, key: string,
importPath: string, importPath: string,
migrationOnly = false, migrationOnly = false,
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -19,7 +19,7 @@ import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../
import { withFileLock } from "./file-lock.js"; import { withFileLock } from "./file-lock.js";
const execFile = promisify(callback); const execFile = promisify(callback);
type Source = { repository: string; commit: string }; type Source = { repository: string; commit: string };
export type UpgradeNode = { kind: "workspace" | "package" | "interface"; directory: string; source: Source }; export type UpgradeNode = { kind: "workspace" | "package" | "interface" | "build"; directory: string; source: Source };
export type UpgradeSpec = { export type UpgradeSpec = {
nodes: UpgradeNode[]; nodes: UpgradeNode[];
quixos?: Source; quixos?: Source;
@@ -105,7 +105,7 @@ export const discoverUpgradeSpec = async (workbench: string): Promise<UpgradeSpe
commit: await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]), commit: await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]),
}, },
}, },
...graph.resources.map((entry: { kind: "interface" | "package"; directory: string; source: Source }) => ({ ...graph.resources.map((entry: { kind: "interface" | "package" | "build"; directory: string; source: Source }) => ({
kind: entry.kind, kind: entry.kind,
source: entry.source, source: entry.source,
directory: path.relative(workbench, path.resolve(workbench, entry.directory)), directory: path.relative(workbench, path.resolve(workbench, entry.directory)),
@@ -139,7 +139,7 @@ export const planPinUpgrades = async (workbenchPath: string, spec: UpgradeSpec):
const keys = new Map<string, string>(); const keys = new Map<string, string>();
for (const node of spec.nodes) { for (const node of spec.nodes) {
validSource(node.source); validSource(node.source);
if (!["workspace", "package", "interface"].includes(node.kind) || keys.has(sourceKey(node))) if (!["workspace", "package", "interface", "build"].includes(node.kind) || keys.has(sourceKey(node)))
throw new Error("Duplicate/invalid upgrade resource identity"); throw new Error("Duplicate/invalid upgrade resource identity");
keys.set(sourceKey(node), node.directory); keys.set(sourceKey(node), node.directory);
} }
@@ -423,14 +423,14 @@ export const applyPinUpgrades = async (
const directory = await location(plan.workbench, node.directory); const directory = await location(plan.workbench, node.directory);
const standalone = await compileCapabilityResourceRepository({ const standalone = await compileCapabilityResourceRepository({
rootDirectory: directory, rootDirectory: directory,
kind: node.kind as "package" | "interface", kind: node.kind as "package" | "interface" | "build",
source, source,
resolveResource, resolveResource,
}); });
for (const dependency of standalone.resources) resources.set(dependency.key, dependency); for (const dependency of standalone.resources) resources.set(dependency.key, dependency);
resources.set(key, { resources.set(key, {
key, key,
kind: node.kind as "package" | "interface", kind: node.kind as "package" | "interface" | "build",
source, source,
directory, directory,
lock: standalone.lock, lock: standalone.lock,
@@ -450,11 +450,15 @@ export const applyPinUpgrades = async (
kind: node.kind, kind: node.kind,
source: node.source, source: node.source,
directory: node.directory, directory: node.directory,
...(node.resource.kind === "build"
? {}
: {
resourceId: resourceId:
node.resource.kind === "interface" node.resource.kind === "interface"
? node.resource.revision.interfaceId ? node.resource.revision.interfaceId
: node.resource.revision.packageId, : node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId, revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({ dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding, binding,
resourceKey: dependency.key, resourceKey: dependency.key,
+5
View File
@@ -0,0 +1,5 @@
/** Observations belong in logs, never in content-addressed compiler outputs. */
export const profile = (phase: string, started: number, details: Record<string, unknown> = {}) => {
if (process.env.QUIXOS_TRACE_PROFILE === "1")
console.error(`[qx-profile] ${JSON.stringify({ phase, ms: Math.round(performance.now() - started), ...details })}`);
};
@@ -0,0 +1,18 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import type { PackageQueryGenerator } from "./assembly.js";
/** The build invoker selects this executable explicitly. Never resolve a
* generator by a capability name or execute a module discovered in user QX. */
export async function loadQueryGenerator(filename?: string): Promise<PackageQueryGenerator | undefined> {
if (!filename) return undefined;
const module = await import(pathToFileURL(path.resolve(filename)).href);
if (typeof module.generatePackageQueries !== "function")
throw new Error("Query generator must export generatePackageQueries");
const generator: PackageQueryGenerator = module.generatePackageQueries;
if (module.completeWorkspace !== undefined) {
if (typeof module.completeWorkspace !== "function") throw new Error("completeWorkspace must be a function");
generator.completeWorkspace = module.completeWorkspace;
}
return generator;
}
+79
View File
@@ -0,0 +1,79 @@
import fs from "node:fs/promises";
import path from "node:path";
import { parse } from "graphql";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { parseQx, walkSyntax } from "./source.js";
/** Insert the authored query and its document together; placement never changes implicitly. */
export async function scaffoldQuery(root: string, prefix: string, spec: ScaffoldRecipe): Promise<StructuralRequest> {
if (
!spec.name ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
!spec.id ||
!spec.declaration ||
typeof spec.queryDocument !== "string"
)
throw new Error("Query scaffold requires name, id, declaration and queryDocument");
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
const syntax = parseQx(source);
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
const queries = [...walkSyntax(syntax.root)].filter((node) => node.kind === "packageQuery");
const query = queries[0];
if (
syntax.diagnostics.length ||
syntax.root.children.length !== 1 ||
!pkg ||
queries.length !== 1 ||
!query ||
pkg.children.filter((node) => node.kind === "packageExport").length !== 1 ||
pkg.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
)
throw new Error("Expected one QX query declaration");
const identifier = query.children.find((node) => node.kind === "identifier")!;
const literals = query.children
.filter((node) => node.kind === "stringLiteral")
.map((node) => JSON.parse(source.slice(node.start, node.end)) as string);
const [id, document, operation] = literals;
if (source.slice(identifier.start, identifier.end) !== spec.name || id !== spec.id)
throw new Error("Query declaration name/ID must match its registration");
if (!document || !/^queries\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+\.graphql$/.test(document))
throw new Error("Query document must be a contained queries/*.graphql path");
const graphql = parse(spec.queryDocument);
const operations = graphql.definitions.filter((definition) => definition.kind === "OperationDefinition");
if (operations.length !== 1 || operations[0]!.operation !== "query" || operations[0]!.name?.value !== operation)
throw new Error("GraphQL document must contain the declared named query operation");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const existing = parseQx(authored);
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
if (existing.diagnostics.length || !parent) throw new Error("Fix package.qx before adding a query");
for (const node of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
const name = node.children.find((child) => child.kind === "identifier");
const literal = node.children.find((child) => child.kind === "stringLiteral");
if (
(name && authored.slice(name.start, name.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw new Error("New query requires a unique name and ID");
}
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [
{
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
source: spec.declaration,
},
],
},
{ file: prefix + document, create: spec.queryDocument },
],
};
}
+21 -2
View File
@@ -5,10 +5,13 @@ import process from "node:process";
import { bindingSchema } from "../bindings/index.js"; import { bindingSchema } from "../bindings/index.js";
import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js"; import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js"; import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
import { loadCheckedResources } from "./checked-resources.js";
const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package|build
--repository URL --commit GIT_REV --checkout-root DIRECTORY --repository URL --commit GIT_REV --checkout-root DIRECTORY
[--snapshot-map PATH] [--snapshot-only true] [--graph-out PATH] [--schema-out PATH] [--snapshot-map PATH] [--snapshot-only true] [--graph-out PATH] [--schema-out PATH]
[--query-generator BUILD_MODULE_PATH] [--checked-resources NIX_MAP]
Resolves a standalone resource's recursive dependency-lock graph, validates Resolves a standalone resource's recursive dependency-lock graph, validates
its interface.qx or package.qx manifest, and emits the checked resource as JSON.`; its interface.qx or package.qx manifest, and emits the checked resource as JSON.`;
@@ -30,6 +33,8 @@ const parseArgs = (args: string[]) => {
"--snapshot-only", "--snapshot-only",
"--graph-out", "--graph-out",
"--schema-out", "--schema-out",
"--query-generator",
"--checked-resources",
].includes(key) || ].includes(key) ||
values.has(key) values.has(key)
) )
@@ -41,7 +46,13 @@ const parseArgs = (args: string[]) => {
const repository = values.get("--repository"); const repository = values.get("--repository");
const commit = values.get("--commit")?.toLowerCase(); const commit = values.get("--commit")?.toLowerCase();
const checkoutRoot = values.get("--checkout-root"); const checkoutRoot = values.get("--checkout-root");
if (!rootDirectory || !repository || !commit || !checkoutRoot || (kind !== "interface" && kind !== "package")) { if (
!rootDirectory ||
!repository ||
!commit ||
!checkoutRoot ||
(kind !== "interface" && kind !== "package" && kind !== "build")
) {
throw new Error(usage); throw new Error(usage);
} }
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) { if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) {
@@ -59,6 +70,8 @@ const parseArgs = (args: string[]) => {
snapshotOnly: values.get("--snapshot-only") === "true", snapshotOnly: values.get("--snapshot-only") === "true",
graphOut: values.get("--graph-out"), graphOut: values.get("--graph-out"),
schemaOut: values.get("--schema-out"), schemaOut: values.get("--schema-out"),
queryGenerator: values.get("--query-generator"),
checkedResources: values.get("--checked-resources"),
} as const; } as const;
}; };
@@ -67,9 +80,13 @@ const graphEntry = (node: ResolvedCapabilityResource) => ({
kind: node.kind, kind: node.kind,
source: node.source, source: node.source,
directory: node.directory, directory: node.directory,
...(node.resource.kind === "build"
? {}
: {
resourceId: resourceId:
node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId, node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId, revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({ dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding, binding,
resourceKey: dependency.key, resourceKey: dependency.key,
@@ -96,6 +113,8 @@ const main = async () => {
commit: options.commit, commit: options.commit,
}, },
resolveResource, resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
checkedResource: await loadCheckedResources(options.checkedResources),
}); });
if (options.graphOut) { if (options.graphOut) {
await writeFile( await writeFile(
+33 -389
View File
@@ -1,43 +1,26 @@
import fs from "node:fs/promises"; import type { GitSource } from "../resource-lock/index.js";
import path from "node:path";
import { contentDigest } from "../capability-model/evolution.js";
import {
validateMigrationCatalog,
type MigrationCatalog,
type MigrationDeclaration,
} from "../capability-model/migrations.js";
import { formatQuixosLock, type GitSource } from "../resource-lock/index.js";
import { parseQx, walkSyntax } from "./source.js";
import type { StructuralRequest } from "./structural-plan.js"; import type { StructuralRequest } from "./structural-plan.js";
import { addImplementation } from "./implementation-edit.js"; import { executionScaffold, isExecutionScaffold } from "./execution-scaffold.js";
import { reactPlatformTypes } from "../bindings/react-platform.js"; import { scaffoldController } from "./controller-scaffold.js";
import { scaffoldComponent } from "./component-scaffold.js";
import { scaffoldQuery } from "./query-scaffold.js";
type Source = { repository: string; commit: string }; type Source = { repository: string; commit: string };
type PackageEditModel = {
generatedBy: "qx-scaffold-v1";
name: string;
id: string;
revision: string;
exports: { name: string; id: string; file: string; migration?: boolean }[];
};
export type ScaffoldRecipe = { export type ScaffoldRecipe = {
template?: "typescript" | "typescript-react"; template?: "typescript-execution" | "typescript-component";
source: Source; source: Source;
directory?: string; directory?: string;
name?: string; name?: string;
id?: string; id?: string;
revision?: string; revision?: string;
declaration?: string; declaration?: string;
/** Initial authored files for a composed recipe; only used for a new package. */ queryDocument?: string;
initialFiles?: Record<string, string>; initialFiles?: Record<string, string>;
tools?: { quixos: Source; protocol: Source; helpers: Source; sdk: Source }; controller?: { registry?: string; subject: string; snapshot: string; remote: string; input: "unit" | "value" };
nixifyPluginUrl?: string; tools?: { quixos: Source; protocol: Source; helpers: Source; sdk?: Source };
migration?: Omit<MigrationDeclaration, "implementation"> & { contracts: Record<string, unknown> };
}; };
const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
const source = (value: Source): GitSource => { const source = (value: Source): GitSource => {
if (!value || typeof value.repository !== "string" || typeof value.commit !== "string") if (!value || typeof value.repository !== "string" || typeof value.commit !== "string")
throw new Error("Scaffold requires an exact source identity; use qx-workspace from a registered repository"); throw Error("Scaffold requires an exact source identity; use a registered repository");
const url = new URL(value.repository); const url = new URL(value.repository);
if ( if (
url.protocol !== "https:" || url.protocol !== "https:" ||
@@ -47,375 +30,36 @@ const source = (value: Source): GitSource => {
url.hash || url.hash ||
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit) !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit)
) )
throw new Error("Scaffolds require credential-free HTTPS sources and full exact commits"); throw Error("Scaffolds require credential-free HTTPS sources and full exact commits");
return { resolver: "git", ...value }; return { resolver: "git", ...value };
}; };
const nixSource = (value: Source) => const nixSource = (value: Source) =>
`git+${source(value).repository}?ref=refs/tags/quixos-reachability/${value.commit}&rev=${value.commit}`; `git+${source(value).repository}?ref=refs/tags/quixos-reachability/${value.commit}&rev=${value.commit}`;
const nixString = (value: string) => JSON.stringify(value).replaceAll("${", "\\${"); /** One-shot edits; structural planning retains interrupted work and authored files. */
const safeName = (name: string | undefined): string => { export async function scaffoldRecipe(
if (!name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("Scaffold requires a simple authored name");
return name;
};
const ownedJson = async <T>(root: string, file: string): Promise<T> => {
const target = path.join(root, file);
if (!(await fs.realpath(target)).startsWith(`${await fs.realpath(root)}/`))
throw new Error("Scaffold input escapes repository");
const value = JSON.parse(await fs.readFile(target, "utf8"));
if (value.generatedBy !== "qx-scaffold-v1") throw new Error(`Not scaffold-owned: ${file}`);
return value;
};
/** Recipes describe structural edits; planStructure owns validation/journaling.
* Implementation files are created once; later edits preserve authored wiring. */
export const scaffoldRecipe = async (
root: string, root: string,
command: "package" | "function" | "migration" | "refresh", command: "package" | "function" | "component" | "controller" | "query" | "migration" | "refresh",
spec: ScaffoldRecipe, spec: ScaffoldRecipe,
): Promise<StructuralRequest> => { ): Promise<StructuralRequest> {
if (command === "refresh") if (command === "refresh")
throw new Error( throw Error(
"Scaffold refresh has been removed. Edit declarations and typed server wiring directly, then run qx-workspace check. Use scaffold function to add a declaration and handler together.", "Scaffold refresh is removed. Edit authored code and run check; add entries with scaffold function/component/query.",
);
if (command === "migration")
throw Error(
"Old-workspace migration scaffolding is retired. Current disposable workspaces do not require migration code.",
); );
source(spec.source); source(spec.source);
if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory)) if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory))
throw new Error("Scaffold directory must be contained"); throw Error("Scaffold directory must be contained");
const prefix = spec.directory ? `${spec.directory}/` : ""; if (spec.template && !["typescript-execution", "typescript-component"].includes(spec.template))
const files: StructuralRequest["files"] = []; throw Error("Use typescript-execution or typescript-component; old server/source-string presets are retired");
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content }); const prefix = spec.directory ? spec.directory + "/" : "";
const generated = (file: string, content: string) => files.push({ file: prefix + file, generated: content }); if (command !== "package" && !(await isExecutionScaffold(root, prefix)))
let packageModel: PackageEditModel; throw Error("Scaffolding requires a checked execution package");
let catalog: MigrationCatalog & { generatedBy: "qx-scaffold-v1" }; if (spec.tools) Object.values(spec.tools).forEach(source);
if (command === "package") { if (command === "controller") return scaffoldController(root, prefix, spec);
const name = safeName(spec.name); if (command === "component") return scaffoldComponent(root, prefix, spec);
if (spec.template && !["typescript", "typescript-react"].includes(spec.template)) if (command === "query") return scaffoldQuery(root, prefix, spec);
throw new Error("Unknown package template"); return executionScaffold(root, command, spec, prefix, nixSource);
const react = spec.template === "typescript-react"; }
if (!spec.id || !spec.revision || !spec.tools)
throw new Error("Package scaffold requires id, revision, and exact quixos/protocol/helpers/sdk tool sources");
Object.values(spec.tools).forEach(source);
packageModel = { generatedBy: "qx-scaffold-v1", name, id: spec.id, revision: spec.revision, exports: [] };
catalog = { generatedBy: "qx-scaffold-v1", schemaVersion: 1, contracts: {}, migrations: [] };
if (react)
packageModel.exports.push({ name: "sourceGet", id: `export:${name}:source`, file: "src/impl/sourceGet.ts" });
create(
"package.qx",
`package ${name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n${react ? ` function sourceGet id ${JSON.stringify(`export:${name}:source`)} : unit -> string;\n` : ""}}\n`,
);
create("quixos.lock", formatQuixosLock({ formatVersion: 1, quixos: source(spec.tools.quixos), resources: [] }));
create(
"package.json",
json({
name: `@quixos/${name.toLowerCase()}`,
version: "0.1.0",
private: true,
type: "module",
packageManager: "yarn@4.18.0",
scripts: { build: "tsc -p tsconfig.json", typecheck: "tsc --noEmit" },
dependencies: {
"@quixos/camino-package-runtime": `${spec.tools.sdk.repository}#commit=${spec.tools.sdk.commit}`,
},
devDependencies: {
"@types/node": "^24",
typescript: "^7.0.2",
...(react ? { react: "^18.3.1", "@types/react": "^18.3.12", esbuild: "^0.25.12" } : {}),
},
}),
);
create(
"tsconfig.json",
json({
compilerOptions: {
target: "ES2023",
module: "NodeNext",
moduleResolution: "NodeNext",
strict: true,
types: ["node", ...(react ? ["react"] : [])],
outDir: "dist",
rootDir: "src",
skipLibCheck: true,
...(react ? { jsx: "react-jsx", esModuleInterop: true } : {}),
},
include: ["src/**/*.ts", "src/**/*.tsx"],
}),
);
if (react) {
create(
"src/component.tsx",
`// Add a checked props export, select it in quixos.check.json options.react,\n// then use its generated ReactResults type. Scaffold bundle does this for you.\nexport default function Component(_props: {camino: Record<string, never>; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1></section>;\n}\n`,
);
create(
"src/impl/sourceGet.ts",
`import componentSource from "../component.js?browser-source";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => componentSource;\n`,
);
create(
"src/browser-assets.d.ts",
`declare module "*?browser-source" { const source: string; export default source; }\ndeclare module "*.css" {}\n`,
);
create("src/gen/web-studio-react-runtime.d.ts", reactPlatformTypes);
}
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
create(
".yarnrc.yml",
`nodeLinker: node-modules\nenableScripts: true\nnpmMinimalAgeGate: 0\napprovedGitRepositories:\n - ${JSON.stringify(spec.tools.sdk.repository)}\nsupportedArchitectures:\n os: [current, linux]\n cpu: [current, x64, arm64]\n libc: [current, glibc]\n`,
);
const nixifyPluginUrl =
spec.nixifyPluginUrl ??
"https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js";
const plugin = new URL(nixifyPluginUrl);
if (
plugin.protocol !== "https:" ||
plugin.username ||
plugin.password ||
plugin.search ||
plugin.hash ||
!/\/commit\/[a-f0-9]{40,64}\//.test(plugin.pathname)
)
throw new Error("Nixify plugin must have an exact credential-free HTTPS commit URL");
generated("quixos.toolchain.json", json({ generatedBy: "qx-scaffold-v1", nixifyPluginUrl }));
create(
"quixos.check.json",
json({
backend: "typescript",
bindingOutput: "src/gen/qx.ts",
...(react
? {
options: { react: { propsExports: [] } },
}
: {}),
}),
);
create(
"flake.nix",
`{
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; };
outputs = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
bundle = { entry = ${JSON.stringify(react ? "src/server.ts" : "dist/server.js")}; ${react ? "browserSources = true;" : ""} };
migrationEntrypoint = "dist/migrate.js";
installServer = { libexecName = ${JSON.stringify(name.toLowerCase())}; descriptorPath = "descriptor.quixos-package.txtpb"; };
};
}\n`,
);
} else {
catalog = await ownedJson<typeof catalog>(root, prefix + "quixos.migrations.json");
// Derive the edit model from authored declarations; it is never persisted.
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const syntax = parseQx(authored);
if (syntax.diagnostics.length)
throw new Error("Cannot scaffold into an invalid package.qx; fix the reported syntax first");
const declaration = [...walkSyntax(syntax.root)].find((node) => node.kind === "packageResourceDecl");
if (!declaration) throw new Error("Expected a package declaration");
const text = (node: typeof declaration) => authored.slice(node.start, node.end);
const literals = declaration.children.filter((node) => node.kind === "stringLiteral");
packageModel = {
generatedBy: "qx-scaffold-v1",
name: text(declaration.children.find((node) => node.kind === "identifier")!),
id: JSON.parse(text(literals[0])),
revision: JSON.parse(text(literals[1])),
exports: [],
};
for (const node of walkSyntax(declaration)) {
if (!["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind))
continue;
const name = safeName(text(node.children.find((child) => child.kind === "identifier")!));
const id = JSON.parse(text(node.children.find((child) => child.kind === "stringLiteral")!));
if (packageModel.exports.some((entry) => entry.id === id || entry.name === name))
throw new Error("Duplicate package export name or ID");
const migration = catalog.migrations.find((entry) => entry.implementation.exportId === id);
packageModel.exports.push({
name,
id,
file: migration?.implementation.file ?? `src/impl/${name}.ts`,
...(migration ? { migration: true } : {}),
});
}
{
const name = safeName(spec.name);
if (!spec.id || packageModel.exports.some((entry) => entry.id === spec.id || entry.name === name))
throw new Error("New export requires a unique name and ID");
const declaration = spec.declaration ?? `function ${name} id ${JSON.stringify(spec.id)} : unit -> unit;`;
const parsed = parseQx(`package Scaffold id "scaffold" revision "scaffold@1" { ${declaration} }`);
const exports = [...walkSyntax(parsed.root)].filter((node) =>
["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind),
);
if (parsed.diagnostics.length || exports.length !== 1)
throw new Error("Expected one valid package export declaration");
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${declaration} }`;
const node = exports[0];
const derived = [...walkSyntax(node)].some((entry) => entry.kind === "eventClause");
if (command === "migration" && (node.kind !== "packageFunctionExport" || spec.declaration))
throw new Error(
"Migration exports use the scaffold's unit function declaration and dedicated migration entrypoint",
);
if (
wrapped.slice(
node.children.find((child) => child.kind === "identifier")!.start,
node.children.find((child) => child.kind === "identifier")!.end,
) !== name ||
JSON.parse(
wrapped.slice(
node.children.find((child) => child.kind === "stringLiteral")!.start,
node.children.find((child) => child.kind === "stringLiteral")!.end,
),
) !== spec.id
)
throw new Error("Declaration name/ID must match its registration");
files.push({
file: prefix + "package.qx",
edits: [
{ operation: "append", parent: { kind: "packageResourceDecl", id: packageModel.id }, source: declaration },
],
});
const file = `src/${command === "migration" ? "migrations" : "impl"}/${name}.ts`;
const implementation =
command === "migration"
? `import type {MigrationContext} from "@quixos/camino-package-runtime";\nexport const handler = async (_context: MigrationContext): Promise<void> => { throw new Error(${JSON.stringify(`Implement migration ${name}`)}); };\n`
: `import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation[${JSON.stringify(name)}] = ${derived ? '{kind: "derived", get: ' : ""}async (_context) => { throw new Error(${JSON.stringify(`Implement ${name}`)}); }${derived ? "}" : ""};\n`;
create(file, implementation);
packageModel.exports.push({ name, id: spec.id, file, ...(command === "migration" ? { migration: true } : {}) });
if (command === "migration") {
if (!spec.migration)
throw new Error("Migration scaffold requires retained contracts and an explicit transition");
const { contracts, ...transition } = spec.migration;
for (const [digest, contract] of Object.entries(contracts)) {
if (
contentDigest(contract) !== digest ||
(catalog.contracts[digest] && contentDigest(catalog.contracts[digest]) !== digest)
)
throw new Error("Retained migration contract mismatch");
catalog.contracts[digest] = contract;
}
catalog.migrations.push({
...transition,
implementation: { exportId: spec.id, file, digest: contentDigest(implementation) },
});
}
}
}
validateMigrationCatalog(catalog, new Set(packageModel.exports.map((entry) => entry.id)));
generated("quixos.migrations.json", json(catalog));
if (command !== "package") {
const entry = packageModel.exports.at(-1)!;
const file = prefix + "src/server.ts";
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
expected: before,
replace: addImplementation(
before,
"createRuntime",
entry.name,
`./${entry.file.slice(4, -3)}.js`,
!!entry.migration,
),
});
if (entry.migration) {
const file = prefix + "src/migrate.ts";
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
expected: before,
replace: addImplementation(before, "serveMigration", entry.id, `./${entry.file.slice(4, -3)}.js`),
});
}
} else {
create(
"src/server.ts",
`import {servePackageRuntime} from "@quixos/camino-package-runtime";\nimport {createRuntime} from "./gen/qx.js";\n` +
packageModel.exports
.filter((entry) => !entry.migration)
.map(
(entry, index) =>
`import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`,
)
.join("") +
`servePackageRuntime(createRuntime({\n` +
packageModel.exports
.map(
(entry) =>
` ${JSON.stringify(entry.name)}: ${entry.migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl${packageModel.exports.filter((value) => !value.migration).indexOf(entry)}`},`,
)
.join("\n") +
`\n}));\n`,
);
const migrations = packageModel.exports.filter((entry) => entry.migration);
create(
"src/migrate.ts",
`import {serveMigration} from "@quixos/camino-package-runtime";\n` +
migrations
.map(
(entry, index) =>
`import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`,
)
.join("") +
`await serveMigration({${migrations.map((entry, index) => `${JSON.stringify(entry.id)}: impl${index}`).join(", ")}});\n`,
);
}
generated(
"descriptor.quixos-package.txtpb",
`# Generated by qx-scaffold-v1\npackage_id: ${JSON.stringify(packageModel.id)}\npackage_revision_id: ${JSON.stringify(packageModel.revision)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` +
packageModel.exports
.map(
(entry) =>
`exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.name)} }\n`,
)
.join(""),
);
if (spec.initialFiles) {
if (command !== "package") throw new Error("initialFiles is only valid when creating a package");
const authored = spec.initialFiles["package.qx"];
if (authored !== undefined) {
const parsed = parseQx(authored);
const declaration = [...walkSyntax(parsed.root)].find((n) => n.kind === "packageResourceDecl");
const literals = declaration?.children.filter((n) => n.kind === "stringLiteral") ?? [];
const identifier = declaration?.children.find((n) => n.kind === "identifier");
if (
parsed.diagnostics.length ||
literals.length !== 2 ||
!identifier ||
authored.slice(identifier.start, identifier.end) !== spec.name ||
JSON.parse(authored.slice(literals[0].start, literals[0].end)) !== spec.id ||
JSON.parse(authored.slice(literals[1].start, literals[1].end)) !== spec.revision
)
throw new Error("Initial package declaration must match the provisioned name, ID and revision");
}
for (const [file, content] of Object.entries(spec.initialFiles)) {
if (
typeof content !== "string" ||
["quixos.lock", "flake.nix", "quixos.toolchain.json", "package.json"].includes(file)
)
throw new Error(`Not an initial authored file: ${file}`);
if (file === "quixos.check.json") {
const check = JSON.parse(content);
if (
check.backend !== "typescript" ||
check.bindingOutput !== "src/gen/qx.ts" ||
Object.keys(check).some((key) => !["backend", "bindingOutput", "options"].includes(key))
)
throw new Error(
"Initial check configuration may customize binding options, not the scaffold verification backend or output",
);
}
const existing = files.findIndex((entry) => entry.file === prefix + file);
if (existing >= 0) files.splice(existing, 1);
create(file, content);
}
// A composed React recipe supplies its own modules and complete server.
if (reactRecipe(spec)) {
for (const file of ["src/component.tsx", "src/impl/sourceGet.ts", "descriptor.quixos-package.txtpb"])
if (!(file in spec.initialFiles)) {
const index = files.findIndex((entry) => entry.file === prefix + file);
if (index >= 0) files.splice(index, 1);
}
}
}
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
};
const reactRecipe = (spec: ScaffoldRecipe) =>
spec.template === "typescript-react" && spec.initialFiles?.["package.qx"] && spec.initialFiles?.["src/server.ts"];
+18
View File
@@ -86,3 +86,21 @@ export const resolveQxSources = async (read: (name: string) => Promise<string>,
}; };
export const loadQxSources = (root: string) => resolveQxSources((name) => readQxSource(root, name)); export const loadQxSources = (root: string) => resolveQxSources((name) => readQxSource(root, name));
/** The same validated closure used by compilation, for installation and pinning. */
export const qxSourceManifest = async (root: string) => {
const resolved = await loadQxSources(root);
const syntax = parseQx(resolved.source);
const imports = [...walkSyntax(syntax.root)]
.filter((node) => node.kind === "resourceImportDecl")
.map((node) => {
const name = node.children.find((child) => child.kind === "identifier")!;
const kind = syntax.tokens.some(
(token) => token.start >= node.start && token.end <= node.end && token.kind === "INTERFACE",
)
? ("interface" as const)
: ("package" as const);
return { kind, binding: resolved.source.slice(name.start, name.end) };
});
return { sourceFiles: resolved.sourceFiles, imports };
};
+6 -3
View File
@@ -15,7 +15,7 @@ export type StructuralEdit =
| { operation: "quixos-pin"; source: { repository: string; commit: string } } | { operation: "quixos-pin"; source: { repository: string; commit: string } }
| { | {
operation: "dependency"; operation: "dependency";
kind: "interface" | "package"; kind: "interface" | "package" | "build";
name: string; name: string;
source: { repository: string; commit: string } | null; source: { repository: string; commit: string } | null;
}; };
@@ -29,6 +29,8 @@ const selectable = new Set([
"packageResourceDecl", "packageResourceDecl",
"atomDecl", "atomDecl",
"valueMember", "valueMember",
"moduleMember",
"moduleExport",
"relationshipMember", "relationshipMember",
"operationMember", "operationMember",
"packageOperationExport", "packageOperationExport",
@@ -155,7 +157,7 @@ export const editStructure = (source: string, edit: StructuralEdit): string => {
if (edit.operation === "dependency") { if (edit.operation === "dependency") {
const parsed = parseQuixosLockDocument(source); const parsed = parseQuixosLockDocument(source);
if (!parsed.ok) throw new Error("Cannot scaffold an invalid lockfile"); if (!parsed.ok) throw new Error("Cannot scaffold an invalid lockfile");
if (!["interface", "package"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name)) if (!["interface", "package", "build"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
throw new Error("Invalid dependency selector"); throw new Error("Invalid dependency selector");
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source)))); const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
const tree = parser.document(); const tree = parser.document();
@@ -208,7 +210,8 @@ export const editStructure = (source: string, edit: StructuralEdit): string => {
return source; return source;
} }
const identifiers = node.children.filter((entry) => entry.kind === "identifier"); const identifiers = node.children.filter((entry) => entry.kind === "identifier");
const position = identifiers[identifiers.length - 1].end; const targetArguments = node.children.find((entry) => entry.kind === "typeArguments");
const position = targetArguments?.end ?? identifiers[identifiers.length - 1].end;
result = applySourceEdits(source, [{ start: position, end: position, text: ` id ${JSON.stringify(edit.id)}` }]); result = applySourceEdits(source, [{ start: position, end: position, text: ` id ${JSON.stringify(edit.id)}` }]);
} else if (edit.operation === "append") { } else if (edit.operation === "append") {
const closing = syntax.tokens.find((token) => token.kind === "RBRACE" && token.end === node.end); const closing = syntax.tokens.find((token) => token.kind === "RBRACE" && token.end === node.end);
+11 -6
View File
@@ -13,7 +13,7 @@ import { parseQuixosLockDocument } from "../resource-lock/index.js";
import { withFileLock } from "./file-lock.js"; import { withFileLock } from "./file-lock.js";
export type StructuralRequest = { export type StructuralRequest = {
kind: "workspace" | "interface" | "package"; kind: "workspace" | "interface" | "package" | "build";
source?: { repository: string; commit: string }; source?: { repository: string; commit: string };
resourceRoot?: string; resourceRoot?: string;
validation?: "syntax" | "resource-graph"; validation?: "syntax" | "resource-graph";
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] }; type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
const safeFile = (file: string) => { const safeFile = (file: string) => {
if ( if (
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test( !/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc\.yml|\.yarn\/plugins\/[A-Za-z0-9_-][A-Za-z0-9_.-]*\.cjs|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
file, file,
) || ) ||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part)) file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
@@ -141,7 +141,7 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
for (const change of changes) { for (const change of changes) {
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length) if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length)
throw new Error(`Invalid QX syntax in ${change.file}`); throw new Error(`Invalid QX syntax in ${change.file}`);
if (change.file.endsWith(".lock") && !parseQuixosLockDocument(change.after, change.file).ok) if (path.basename(change.file) === "quixos.lock" && !parseQuixosLockDocument(change.after, change.file).ok)
throw new Error(`Invalid lock syntax in ${change.file}`); throw new Error(`Invalid lock syntax in ${change.file}`);
} }
} else { } else {
@@ -159,16 +159,21 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
const resourceRoot = path.join(snapshot.directory, request.resourceRoot ?? ""); const resourceRoot = path.join(snapshot.directory, request.resourceRoot ?? "");
if (request.kind === "workspace") if (request.kind === "workspace")
await compileWorkspaceRepository({ rootDirectory: resourceRoot, resolveResource }); await compileWorkspaceRepository({ rootDirectory: resourceRoot, resolveResource });
else if (["package", "interface"].includes(request.kind) && request.source) { else if (["package", "interface", "build"].includes(request.kind) && request.source) {
const compiled = await compileCapabilityResourceRepository({ const compiled = await compileCapabilityResourceRepository({
rootDirectory: resourceRoot, rootDirectory: resourceRoot,
kind: request.kind as "package" | "interface", kind: request.kind as "package" | "interface" | "build",
source: { resolver: "git", ...request.source }, source: { resolver: "git", ...request.source },
resolveResource, resolveResource,
}); });
if (compiled.resource.kind === "package") { if (compiled.resource.kind === "package") {
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8")); const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
const artifacts = [ // Execution bindings are immutable build outputs owned by checkedArtifacts,
// not source edits produced by a structural preview.
const artifacts =
configuration.backend === "execution"
? []
: [
{ {
file: configuration.bindingOutput as string, file: configuration.bindingOutput as string,
after: generateTypeScriptBindings( after: generateTypeScriptBindings(
+75 -51
View File
@@ -2,7 +2,7 @@
import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises"; import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises";
import { parseQx, formatQx, lintQx } from "./source.js"; import { parseQx, formatQx, lintQx } from "./source.js";
import { scaffoldAtom } from "./scaffold.js"; import { scaffoldAtom } from "./scaffold.js";
import { loadQxSources } from "./source-loader.js"; import { loadQxSources, qxSourceManifest } from "./source-loader.js";
import { createGitCapabilityResolver } from "./git-resolver.js"; import { createGitCapabilityResolver } from "./git-resolver.js";
import { planEvolution } from "../capability-model/index.js"; import { planEvolution } from "../capability-model/index.js";
import { snapshotRepository } from "./candidate-check.js"; import { snapshotRepository } from "./candidate-check.js";
@@ -54,6 +54,11 @@ const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
const main = async () => { const main = async () => {
const [command, ...args] = process.argv.slice(2); const [command, ...args] = process.argv.slice(2);
if (command === "source-manifest") {
if (args.length !== 1) throw new Error("source-manifest WORKSPACE_ROOT");
process.stdout.write(JSON.stringify(await qxSourceManifest(args[0])) + "\n");
return;
}
if (command === "scaffold-validate-qx") { if (command === "scaffold-validate-qx") {
if (args.length !== 1) throw new Error("scaffold-validate-qx SOURCES_JSON"); if (args.length !== 1) throw new Error("scaffold-validate-qx SOURCES_JSON");
const sources = JSON.parse(args[0]); const sources = JSON.parse(args[0]);
@@ -192,16 +197,16 @@ const main = async () => {
} }
if (command === "check-committed" && !args.includes("--help")) { if (command === "check-committed" && !args.includes("--help")) {
const [kind, repository, commit, log, ...extra] = args; const [kind, repository, commit, log, ...extra] = args;
if (!["workspace", "interface", "package"].includes(kind) || !log || extra.length) if (!["workspace", "interface", "package", "build"].includes(kind) || !log || extra.length)
throw new Error("usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE"); throw new Error("usage: quixos-qx check-committed workspace|interface|package|build REPOSITORY COMMIT LOG_FILE");
process.stdout.write( process.stdout.write(
`${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package", log)}\n`, `${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package" | "build", log)}\n`,
); );
return; return;
} }
if (!command || command === "--help" || args.includes("--help")) { if (!command || command === "--help" || args.includes("--help")) {
process.stdout.write( process.stdout.write(
"quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-function, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" + "quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-build, scaffold-function, scaffold-query, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" + "inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n", "resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n",
); );
@@ -226,7 +231,7 @@ const main = async () => {
if (flags.length && !flags[0].startsWith("--")) { if (flags.length && !flags[0].startsWith("--")) {
repository = flags.shift(); repository = flags.shift();
commit = flags.shift(); commit = flags.shift();
} else if (root && ["interface", "package"].includes(kind) && name) { } else if (root && ["interface", "package", "build"].includes(kind) && name) {
const context = await authoringContext(root); const context = await authoringContext(root);
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null); const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
const matches = context.resources.filter( const matches = context.resources.filter(
@@ -256,15 +261,17 @@ const main = async () => {
} }
if ( if (
!root || !root ||
!["interface", "package"].includes(kind) || !["interface", "package", "build"].includes(kind) ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") ||
!repository || !repository ||
!commit || !commit ||
flags.some((flag) => flag !== "--write") flags.some((flag) => flag !== "--write")
) )
throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]"); throw new Error(
const resourceKind = kind as "package" | "interface"; "usage: quixos-qx scaffold-dependency ROOT interface|package|build NAME REPOSITORY COMMIT [--write]",
let entrypoint: "workspace" | "package" | "interface" | undefined; );
const resourceKind = kind as "package" | "interface" | "build";
let entrypoint: "workspace" | "package" | "interface" | "build" | undefined;
for (const candidate of ["workspace", "package", "interface"] as const) { for (const candidate of ["workspace", "package", "interface"] as const) {
try { try {
await readFile(path.join(root, `${candidate}.qx`)); await readFile(path.join(root, `${candidate}.qx`));
@@ -274,7 +281,14 @@ const main = async () => {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
} }
} }
if (!entrypoint) throw new Error("No QX repository entrypoint"); if (!entrypoint) {
const context = await authoringContext(root);
const selected = context.resources.find(
(entry) => path.join(context.workbench, entry.directory) === path.resolve(root),
);
if (selected?.kind !== "build") throw new Error("No QX repository entrypoint");
entrypoint = "build";
}
const lock = await loadQuixosLock(path.join(root, "quixos.lock")); const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) throw new Error("Invalid resource lock"); if (!lock.ok) throw new Error("Invalid resource lock");
let target = "quixos.lock"; let target = "quixos.lock";
@@ -288,7 +302,9 @@ const main = async () => {
source: await authorSource(root), source: await authorSource(root),
validation: "syntax", validation: "syntax",
files: [ files: [
{ file: `${entrypoint}.qx`, edits: [{ operation: "import", kind: resourceKind, name }] }, ...(entrypoint !== "build" && resourceKind !== "build"
? [{ file: `${entrypoint}.qx`, edits: [{ operation: "import" as const, kind: resourceKind, name }] }]
: []),
{ {
file: target, file: target,
edits: [{ operation: "dependency", kind: resourceKind, name, source: { repository, commit } }], edits: [{ operation: "dependency", kind: resourceKind, name, source: { repository, commit } }],
@@ -301,26 +317,45 @@ const main = async () => {
); );
return; return;
} }
if (command === "scaffold-interface") { if (command === "scaffold-interface" || command === "scaffold-build") {
const isBuild = command === "scaffold-build";
const [root, specFile, ...flags] = args; const [root, specFile, ...flags] = args;
if (!root || !specFile || flags.some((flag) => flag !== "--write")) if (!root || !specFile || flags.some((flag) => flag !== "--write"))
throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]"); throw new Error("usage: quixos-qx scaffold-interface|scaffold-build ROOT SPEC_JSON [--write]");
const spec = (await readSpec(specFile)) as ScaffoldRecipe; const spec = (await readSpec(specFile)) as ScaffoldRecipe;
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id || !spec.revision || !spec.tools?.quixos) if (
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source"); !spec.name ||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
(!isBuild && (!spec.id || !spec.revision)) ||
!spec.tools?.quixos
)
throw new Error(
isBuild
? "Build scaffold requires name and Quixos toolchain source"
: "Interface scaffold requires name, id, revision and Quixos toolchain source",
);
const request: StructuralRequest = { const request: StructuralRequest = {
kind: "interface", kind: isBuild ? "build" : "interface",
// Like package scaffolding, declaration creation is provisional. Imports // Like package scaffolding, declaration creation is provisional. Imports
// can be attached next; the normal check verifies the complete graph. // can be attached next; the normal check verifies the complete graph.
validation: "syntax", validation: "syntax",
source: spec.source, source: spec.source,
files: [ files: [
...(!isBuild
? [
{ {
file: "interface.qx", file: "interface.qx",
create: create:
spec.declaration ?? spec.declaration ??
`interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`, `interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`,
}, },
]
: [
{
file: "README.md",
create: `# ${spec.name}\n\nManaged build dependency. Consumers select sources or Nix outputs through context.dependencies.\n`,
},
]),
{ {
file: "quixos.lock", file: "quixos.lock",
create: formatQuixosLock({ create: formatQuixosLock({
@@ -385,9 +420,20 @@ const main = async () => {
} }
if (command === "scaffold-refresh") if (command === "scaffold-refresh")
throw new Error( throw new Error(
"Refresh is no longer required: edit declarations and typed implementation wiring, then run qx-workspace check. Dependency installation uses scaffold install.", "Refresh is no longer required: edit declarations and typed implementation wiring, then run qx-workspace check. Dependencies are provided by checkedArtifacts.",
); );
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-install"].includes(command)) { if (command === "scaffold-install")
throw Error("Execution dependencies are supplied by checkedArtifacts; run workspace-root check instead");
if (
[
"scaffold-package",
"scaffold-function",
"scaffold-query",
"scaffold-component",
"scaffold-controller",
"scaffold-migration",
].includes(command)
) {
const [root, specFile, ...flags] = args; const [root, specFile, ...flags] = args;
let spec: ScaffoldRecipe; let spec: ScaffoldRecipe;
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) { if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
@@ -420,46 +466,24 @@ const main = async () => {
flags.some((flag) => !["--write", "--install"].includes(flag)) || flags.some((flag) => !["--write", "--install"].includes(flag)) ||
(flags.includes("--install") && !flags.includes("--write")) (flags.includes("--install") && !flags.includes("--write"))
) )
throw new Error( throw new Error("usage: quixos-qx scaffold-package|function|query|component|controller ROOT SPEC_JSON [--write]");
"usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]",
);
const plan = const plan =
command === "scaffold-install" command === "scaffold-install"
? undefined ? undefined
: await planStructure( : await planStructure(
root, root,
await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration", spec), await scaffoldRecipe(
root,
command.slice(9) as "package" | "function" | "query" | "component" | "controller" | "migration",
spec,
),
process.env.QUIXOS_SNAPSHOT_MAP, process.env.QUIXOS_SNAPSHOT_MAP,
); );
if (flags.includes("--install"))
throw Error(
"Execution dependencies are supplied by checkedArtifacts; run workspace-root check instead of scaffold install",
);
const applied = plan && flags.includes("--write") ? await applyStructure(plan) : undefined; const applied = plan && flags.includes("--write") ? await applyStructure(plan) : undefined;
if (flags.includes("--install")) {
const cwd = path.resolve(root, spec.directory ?? "");
const toolchain = JSON.parse(await readFile(path.join(cwd, "quixos.toolchain.json"), "utf8"));
if (toolchain.generatedBy !== "qx-scaffold-v1" || typeof toolchain.nixifyPluginUrl !== "string")
throw new Error("Missing scaffold toolchain");
for (const [executable, args] of [
["corepack", ["yarn", "plugin", "import", toolchain.nixifyPluginUrl]],
["corepack", ["yarn", "config", "set", "generateDefaultNix", "false"]],
["corepack", ["yarn", "config", "set", "individualNixPackaging", "true"]],
["corepack", ["yarn", "install"]],
] as const) {
const result = spawnSync(executable, [...args], { cwd, stdio: ["inherit", 2, 2] });
if (result.error || result.status !== 0)
throw new Error(
`Scaffold files retained; ${executable} ${args.join(" ")} failed: ${result.error?.message ?? result.status}`,
);
}
try {
await readFile(path.join(cwd, "yarn-project.nix"));
} catch {
throw new Error(
"Nixify did not generate yarn-project.nix. It skips repositories under the OS temporary directory; use an ordinary workspace checkout and retry installation.",
);
}
await snapshotCommit(cwd);
const locked = spawnSync("nix", ["flake", "lock"], { cwd, stdio: ["inherit", 2, 2] });
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
}
process.stdout.write( process.stdout.write(
`${JSON.stringify({ ...(plan ? planSummary(plan) : { installed: true }), applied }, null, 2)}\n`, `${JSON.stringify({ ...(plan ? planSummary(plan) : { installed: true }), applied }, null, 2)}\n`,
); );
+31 -3
View File
@@ -4,6 +4,10 @@ import { readFile, writeFile } from "node:fs/promises";
import process from "node:process"; import process from "node:process";
import { compileWorkspaceRepository } from "./assembly.js"; import { compileWorkspaceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js"; import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
import { loadCheckedResources } from "./checked-resources.js";
import { profile } from "./profile.js";
import { compileExecutionPlan, packageBuildContexts } from "../execution-world/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js"; import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import { import {
planEvolution, planEvolution,
@@ -15,7 +19,8 @@ import {
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID] [--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH] [--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
[--reviews REVIEW_JSON] [--schemas-out PATH] [--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH] [--build-contexts-out PATH]
[--query-generator BUILD_MODULE_PATH] [--checked-resources NIX_MAP]
Resolves a workspace's recursive resource-lock graph, clones every exact Resolves a workspace's recursive resource-lock graph, clones every exact
resource revision, validates standalone interface/package manifests, and emits resource revision, validates standalone interface/package manifests, and emits
@@ -37,6 +42,8 @@ const parseArgs = (args: string[]) => {
checkoutRoot, checkoutRoot,
graphOut: values.get("--graph-out"), graphOut: values.get("--graph-out"),
schemasOut: values.get("--schemas-out"), schemasOut: values.get("--schemas-out"),
executionPlanOut: values.get("--execution-plan-out"),
buildContextsOut: values.get("--build-contexts-out"),
snapshotMap: values.get("--snapshot-map"), snapshotMap: values.get("--snapshot-map"),
workspaceId: values.get("--workspace-id"), workspaceId: values.get("--workspace-id"),
workspaceRevisionId: values.get("--workspace-revision-id"), workspaceRevisionId: values.get("--workspace-revision-id"),
@@ -44,6 +51,8 @@ const parseArgs = (args: string[]) => {
baseline: values.get("--baseline"), baseline: values.get("--baseline"),
evolutionOut: values.get("--evolution-out"), evolutionOut: values.get("--evolution-out"),
reviews: values.get("--reviews"), reviews: values.get("--reviews"),
queryGenerator: values.get("--query-generator"),
checkedResources: values.get("--checked-resources"),
}; };
}; };
@@ -57,13 +66,17 @@ const main = async () => {
checkoutRoot: options.checkoutRoot, checkoutRoot: options.checkoutRoot,
snapshotMap: options.snapshotMap, snapshotMap: options.snapshotMap,
}); });
const assemblyStarted = performance.now();
const assembled = await compileWorkspaceRepository({ const assembled = await compileWorkspaceRepository({
rootDirectory: options.rootDirectory, rootDirectory: options.rootDirectory,
workspaceId: options.workspaceId, workspaceId: options.workspaceId,
workspaceRevisionId: options.workspaceRevisionId, workspaceRevisionId: options.workspaceRevisionId,
sourceRootCommit: options.sourceRootCommit, sourceRootCommit: options.sourceRootCommit,
resolveResource, resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
checkedResource: await loadCheckedResources(options.checkedResources),
}); });
profile("workspace-assembly", assemblyStarted, { resources: assembled.resources.length });
if (options.graphOut) { if (options.graphOut) {
await writeFile( await writeFile(
@@ -81,11 +94,15 @@ const main = async () => {
kind: node.kind, kind: node.kind,
source: node.source, source: node.source,
directory: node.directory, directory: node.directory,
...(node.resource.kind === "build"
? {}
: {
resourceId: resourceId:
node.resource.kind === "interface" node.resource.kind === "interface"
? node.resource.revision.interfaceId ? node.resource.revision.interfaceId
: node.resource.revision.packageId, : node.resource.revision.packageId,
revisionId: node.resource.revision.revisionId, revisionId: node.resource.revision.revisionId,
}),
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({ dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
binding, binding,
resourceKey: dependency.key, resourceKey: dependency.key,
@@ -98,16 +115,26 @@ const main = async () => {
); );
} }
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) }; const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
if (options.executionPlanOut || options.buildContextsOut) {
const executionStarted = performance.now();
const plan = compileExecutionPlan(assembled.workspace);
if (options.executionPlanOut) await writeFile(options.executionPlanOut, `${JSON.stringify(plan, null, 2)}\n`);
if (options.buildContextsOut)
await writeFile(options.buildContextsOut, JSON.stringify(packageBuildContexts(plan.definition.bindingSchema)));
profile("execution-plan-and-contexts", executionStarted);
}
if (options.schemasOut) { if (options.schemasOut) {
const schemasStarted = performance.now();
const schemas: Record<string, unknown> = {}; const schemas: Record<string, unknown> = {};
for (const node of assembled.resources.filter((entry) => entry.kind === "package")) { for (const node of assembled.capabilityResources.filter((entry) => entry.kind === "package")) {
const closure = new Map<string, typeof node>(); const closure = new Map<string, typeof node>();
const visit = (entry: typeof node) => { const visit = (entry: typeof node) => {
if (closure.has(entry.key)) return; if (closure.has(entry.key) || entry.kind === "build") return;
closure.set(entry.key, entry); closure.set(entry.key, entry);
entry.dependencies.forEach(visit); entry.dependencies.forEach(visit);
}; };
visit(node); visit(node);
if (node.resource.kind !== "package") continue;
schemas[node.resource.revision.revisionId] = specializeBindingSchema( schemas[node.resource.revision.revisionId] = specializeBindingSchema(
bindingSchema({ resources: [...closure.values()] }), bindingSchema({ resources: [...closure.values()] }),
candidate, candidate,
@@ -115,6 +142,7 @@ const main = async () => {
); );
} }
await writeFile(options.schemasOut, JSON.stringify(schemas)); await writeFile(options.schemasOut, JSON.stringify(schemas));
profile("package-schemas", schemasStarted);
} }
if (options.evolutionOut) { if (options.evolutionOut) {
const baseline = options.baseline const baseline = options.baseline
+208
View File
@@ -0,0 +1,208 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type AtomId,
type Conformance,
type InterfaceRevision,
type InterfaceRevisionId,
} from "./types.js";
import {
GenericTypeError,
TypeSubstitution,
bindTypeParameters,
canonicalTypeArgument,
type ClosedTypeArgument,
type GenericTypeEnvironment,
type InterfaceApplicationExpression,
type TypeParameter,
} from "./generics.js";
/** Compiler input only. Runtime plans contain ordinary closed conformances. */
export interface ConformanceRule {
id: string;
parameters: TypeParameter[];
source: InterfaceApplicationExpression;
target: InterfaceApplicationExpression;
}
export interface RuleApplication {
rule: ConformanceRule;
atomId: AtomId;
sourceId: InterfaceRevisionId;
targetId: InterfaceRevisionId;
bindings: Map<string, ClosedTypeArgument>;
id: Conformance["id"];
}
const parameterId = (argument: ConformanceRule["source"]["arguments"][number]) => {
const value = argument.kind === "value" ? argument.type : argument.target;
return value.kind === "parameter" ? value.parameterId : undefined;
};
const hasParameter = (node: unknown): boolean =>
!!node &&
typeof node === "object" &&
(("kind" in node && node.kind === "parameter") || Object.values(node).some(hasParameter));
export function validateConformanceRules(
rules: readonly ConformanceRule[],
definitions: ReadonlyMap<string, InterfaceRevision>,
) {
const ids = new Set<string>();
const graph = new Map<string, ConformanceRule[]>();
for (const rule of rules) {
if (ids.has(rule.id)) throw new GenericTypeError("duplicate-rule-id", rule.id, `Duplicate rule ID ${rule.id}`);
ids.add(rule.id);
for (const application of [
rule.source,
rule.target,
...rule.parameters.flatMap((p) => (p.kind === "object" ? p.implements : [])),
]) {
const definition = definitions.get(application.definitionId);
if (!definition)
throw new GenericTypeError("unknown-interface", rule.id, `Unknown interface ${application.definitionId}`);
const parameters = definition.template?.parameters ?? [];
if (parameters.length !== application.arguments.length)
throw new GenericTypeError("type-arity", rule.id, `Wrong arity for ${definition.displayName}`);
parameters.forEach((p, i) => {
if (p.kind !== application.arguments[i].kind)
throw new GenericTypeError(
"parameter-kind",
rule.id,
`Wrong argument kind for ${definition.displayName}.${p.name}`,
);
});
}
const captured = new Set<string>();
for (const argument of rule.source.arguments) {
const id = parameterId(argument);
if (id) captured.add(id);
else if (hasParameter(argument))
throw new GenericTypeError(
"unsupported-rule-pattern",
rule.id,
"Capture whole source arguments, not nested type patterns",
);
}
for (const p of rule.parameters)
if (!captured.has(p.id))
throw new GenericTypeError(
"uncaptured-rule-parameter",
rule.id,
`Parameter ${p.name} must occur in the source application`,
);
graph.set(rule.source.definitionId, [...(graph.get(rule.source.definitionId) ?? []), rule]);
}
const visited = new Set<string>();
const active = new Set<string>();
const visit = (id: string, trail: string[]) => {
if (active.has(id))
throw new GenericTypeError(
"recursive-conformance-rule",
trail.at(-1) ?? id,
`Recursive conformance rules: ${trail.join(" -> ")}`,
);
if (visited.has(id)) return;
active.add(id);
for (const rule of graph.get(id) ?? []) visit(rule.target.definitionId, [...trail, rule.id]);
active.delete(id);
visited.add(id);
};
for (const id of graph.keys()) visit(id, []);
}
/** Monotone closure: bounds only enable applications, never invent evidence. */
export function expandConformanceRules(
rules: readonly ConformanceRule[],
initial: readonly Pick<Conformance, "atomId" | "interfaceRevisionId">[],
definitions: ReadonlyMap<string, InterfaceRevision>,
environment: (atom: AtomId, bindings: Map<string, ClosedTypeArgument>) => GenericTypeEnvironment,
): RuleApplication[] {
validateConformanceRules(rules, definitions);
const key = (atom: string, iface: string) => JSON.stringify([atom, iface]);
const facts = new Map(initial.map((f) => [key(f.atomId, f.interfaceRevisionId), f]));
const applications = new Map<string, RuleApplication>();
const implies = (actual: InterfaceRevisionId, required: InterfaceRevisionId, seen = new Set<string>()): boolean => {
if (actual === required) return true;
if (seen.has(actual)) return false;
seen.add(actual);
return (definitions.get(actual)?.requiredInterfaces ?? []).some((id) => implies(id, required, seen));
};
const bySource = new Map<string, ConformanceRule[]>();
for (const rule of [...rules].sort((a, b) => a.id.localeCompare(b.id)))
bySource.set(rule.source.definitionId, [...(bySource.get(rule.source.definitionId) ?? []), rule]);
let changed = true;
while (changed) {
changed = false;
for (const fact of [...facts.values()].sort((a, b) =>
key(a.atomId, a.interfaceRevisionId).localeCompare(key(b.atomId, b.interfaceRevisionId)),
)) {
const definition = definitions.get(fact.interfaceRevisionId);
if (!definition) continue;
for (const rule of bySource.get(definition.application?.definitionId ?? definition.revisionId) ?? []) {
const arguments_ = definition.application?.arguments ?? [];
const bindings = new Map<string, ClosedTypeArgument>();
const env = environment(fact.atomId, bindings);
const substitution = new TypeSubstitution(env);
if (
!rule.source.arguments.every((pattern, i) => {
const actual = arguments_[i];
if (!actual || actual.kind !== pattern.kind) return false;
const id = parameterId(pattern);
if (!id) return canonicalTypeArgument(substitution.argument(pattern)) === canonicalTypeArgument(actual);
const previous = bindings.get(id);
if (previous && canonicalTypeArgument(previous) !== canonicalTypeArgument(actual)) return false;
bindings.set(id, actual);
return true;
})
)
continue;
try {
bindTypeParameters(
rule.parameters,
rule.parameters.map((p) => bindings.get(p.id)!),
{
...env,
implementsInterface: (target, required) =>
target.kind === "atom"
? facts.has(key(target.atomId, required))
: implies(target.interfaceRevisionId, required),
},
rule.id,
);
} catch (error) {
if (error instanceof GenericTypeError && ["unsatisfied-bound", "non-storable-argument"].includes(error.code))
continue;
throw error;
}
const applicationKey = JSON.stringify([rule.id, fact.atomId, fact.interfaceRevisionId]);
if (applications.has(applicationKey)) continue;
if (applications.size >= 10000)
throw new GenericTypeError(
"rule-expansion-limit",
rule.id,
"Conformance rule expansion exceeds 10000 applications",
);
const targetId = substitution.application(rule.target);
const id = capabilityId.conformance(
`conformance-rule:sha256:${createHash("sha256")
.update(
JSON.stringify(["quixos-conformance-rule-v1", rule.id, fact.atomId, fact.interfaceRevisionId, targetId]),
)
.digest("hex")}`,
);
applications.set(applicationKey, {
rule,
atomId: fact.atomId,
sourceId: fact.interfaceRevisionId,
targetId,
bindings,
id,
});
const targetKey = key(fact.atomId, targetId);
if (!facts.has(targetKey)) {
facts.set(targetKey, { atomId: fact.atomId, interfaceRevisionId: targetId });
changed = true;
}
}
}
}
return [...applications.values()];
}
+128
View File
@@ -0,0 +1,128 @@
import type { InterfaceRevision, PackageController, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { canonicalJson } from "./evolution.js";
export function validateControllers(
controllers: readonly PackageController[],
interfaces: readonly InterfaceRevision[],
prefix: string,
queries: readonly import("../query/types.js").CheckedQuery[] = [],
): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [],
ids = new Set<string>();
const fail = (path: string, message: string) => issues.push({ code: "invalid-controller" as const, path, message });
for (const [index, controller] of controllers.entries()) {
const path = `${prefix}[${index}]`;
if (!controller.id.trim() || ids.has(controller.id))
fail(`${path}.id`, "Controller identities must be nonempty and unique within the package");
ids.add(controller.id);
if (controller.registry !== undefined) {
const query = queries.find((query) => query.declaration.id === controller.registry);
const selected = (entries: import("../query/types.js").QuerySelection[] | undefined, name: string) =>
entries?.find((entry) => entry.name === name && entry.key === name && !entry.conditions.length);
const root = selected(query?.selection, "root"),
objects = selected(root?.selection, "objects"),
entries = selected(objects?.selection, "entries"),
node = selected(entries?.selection, "node"),
qx = selected(node?.selection, "_camino"),
ref = selected(qx?.selection, "ref");
const all = objects?.arguments.all;
if (
!query ||
query.declaration.root.kind !== "workspace-interface" ||
query.declaration.root.interface !== controller.subject ||
query.declaration.placement?.kind === "local" ||
query.effects.some((effect) => effect.execution === "rpc-permitted") ||
query.variables.kind !== "record" ||
Object.keys(query.variables.fields).length ||
!ref ||
all?.kind !== "literal" ||
!/^\d+$/.test(String(all.value)) ||
!Number.isSafeInteger(Number(all.value)) ||
Number(all.value) < 1 ||
Number(all.value) > 64
)
fail(
`${path}.registry`,
"Controller registry requires a native/portable workspace query over its subject, no variables, and root.objects(all: 1..64).entries.node._camino.ref without aliases or conditional selections",
);
}
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
if (!iface) {
fail(`${path}.subject`, "Controller subject must be an imported closed interface");
continue;
}
const operations = iface.members.flatMap((m) => m.operations);
const snapshot = operations.find((o) => o.id === controller.snapshot),
remote = operations.find((o) => o.id === controller.remote);
const unit = (value: unknown) => canonicalJson(value) === canonicalJson({ kind: "builtin", name: "unit" });
if (
!snapshot ||
snapshot.scope === "class" ||
snapshot.mode !== "call" ||
snapshot.execution?.kind !== "snapshot" ||
!snapshot.execution.targets.includes("server") ||
!unit(snapshot.inputType)
)
fail(`${path}.snapshot`, "Controller snapshot must be a server snapshot object call with unit input");
if (
!remote ||
remote.scope === "class" ||
remote.mode !== "call" ||
(remote.execution?.kind ?? "remote") !== "remote"
)
fail(`${path}.remote`, "Controller target must be a remote object call");
if (controller.input !== "unit" && controller.input !== "value")
fail(`${path}.input`, "Controller input must be unit or value");
else if (
remote &&
(controller.input === "unit"
? !unit(remote.inputType)
: snapshot && canonicalJson(snapshot.outputType) !== canonicalJson(remote.inputType))
)
fail(`${path}.input`, "Controller input mapping does not match the remote operation input");
}
return issues;
}
export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
return workspace.packageImports.flatMap((pkg, index) =>
validateControllers(
pkg.controllers ?? [],
workspace.interfaceImports,
`packageImports[${index}].controllers`,
pkg.checkedQueries,
),
);
}
/** The file is declarative metadata, not an executable registration callback. */
export function parseControllerCatalog(
text: string,
interfaces: readonly InterfaceRevision[],
queries: readonly import("../query/types.js").CheckedQuery[] = [],
): PackageController[] {
if (Buffer.byteLength(text) > 1048576) throw new Error("Controller catalog exceeds 1 MiB");
const value: unknown = JSON.parse(text);
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value);
if (
!record(value) ||
value.schemaVersion !== 1 ||
Object.keys(value).some((k) => !["schemaVersion", "controllers"].includes(k)) ||
!Array.isArray(value.controllers) ||
value.controllers.length > 1000
)
throw new Error("Invalid controller catalog: expected schemaVersion 1 and at most 1000 controllers");
const fields = ["id", "subject", "snapshot", "remote", "input"];
for (const entry of value.controllers)
if (
!record(entry) ||
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
fields.some((key) => typeof entry[key] !== "string")
)
throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings");
const controllers = value.controllers as PackageController[];
const issues = validateControllers(controllers, interfaces, "controllers", queries);
if (issues.length) throw new Error(issues.map((i) => `${i.path}: ${i.message}`).join("\n"));
return controllers;
}
+86 -16
View File
@@ -149,6 +149,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
} }
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`); if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`); if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
if (binding.kind === "creation") {
for (const owner of binding.owners)
parent.dependencies.add(conformanceKey(binding.atomId, owner.interfaceRevisionId));
}
if (binding.kind === "constructor") { if (binding.kind === "constructor") {
parent.dependencies.add(`constructor:${binding.atomId}`); parent.dependencies.add(`constructor:${binding.atomId}`);
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId); const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
@@ -167,8 +171,22 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId)); parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
reviews?.add(conformanceIdentity(conformance)); reviews?.add(conformanceIdentity(conformance));
for (const operation of conformance.operationBindings) { for (const operation of conformance.operationBindings) {
if (operation.binding.kind !== "package") continue; let provider: Binding | undefined = operation.binding;
const revisionId = operation.binding.packageRevisionId; const visited = new Set<string>();
while (provider?.kind === "interface") {
const forward: Extract<Binding, { kind: "interface" }> = provider;
const key = `${provider.interfaceRevisionId}\0${provider.operationId}`;
if (visited.has(key)) throw new Error("Forwarding cycle in evolution contract");
visited.add(key);
const target = workspace.conformances.find(
(c) => c.atomId === conformance.atomId && c.interfaceRevisionId === forward.interfaceRevisionId,
);
if (!target) throw new Error("Missing forwarding conformance in evolution contract");
reviews?.add(conformanceIdentity(target));
provider = target.operationBindings.find((entry) => entry.operationId === forward.operationId)?.binding;
}
if (provider?.kind !== "package") continue;
const revisionId = provider.packageRevisionId;
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId); const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
if (pkg) reviews?.add(pkg.packageId); if (pkg) reviews?.add(pkg.packageId);
} }
@@ -177,6 +195,11 @@ 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 === "journal" || value.kind === "subscription") {
dependency(parent, { kind: "state", slotId: value.slotId }, atomId);
return;
}
if (value.kind !== "package") { if (value.kind !== "package") {
dependency(parent, value, atomId); dependency(parent, value, atomId);
return; return;
@@ -196,9 +219,17 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
atomId: conformance.atomId, atomId: conformance.atomId,
interfaceRevisionId: conformance.interfaceRevisionId, interfaceRevisionId: conformance.interfaceRevisionId,
operations: sorted(conformance.operationBindings, (entry) => entry.operationId), operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
...(conformance.moduleBindings?.length
? { modules: sorted(conformance.moduleBindings, (entry) => entry.memberId) }
: {}),
...(conformance.initialization
? { initialization: sorted(conformance.initialization, (entry) => entry.input) }
: {}),
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId), materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
}); });
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`); parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
for (const module of conformance.moduleBindings ?? [])
parent.dependencies.add(`package:${module.packageRevisionId}`);
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId); const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
for (const required of contract?.requiredInterfaces ?? []) for (const required of contract?.requiredInterfaces ?? [])
parent.dependencies.add(conformanceKey(conformance.atomId, required)); parent.dependencies.add(conformanceKey(conformance.atomId, required));
@@ -345,6 +376,36 @@ export const planEvolution = (
...(next ? { candidate: next } : {}), ...(next ? { candidate: next } : {}),
}); });
} }
const semanticReview = assessSemanticReviews(baseline, candidate, options.reviews ?? [], checkerVersion, [
...nextRuntimes.values(),
]);
const reviews = semanticReview.reviews;
blockers.push(...semanticReview.blockers);
return {
schemaVersion: 1,
baselineDigest: baseline ? contentDigest(baseline) : null,
candidateDigest,
checkerVersion,
runtimeActions,
storageChanges,
migrationRequired: storageChanges.filter((entry) => entry.requiresMigration).map((entry) => entry.id),
reviews,
packageChecks: runtimeActions
.filter((entry) => entry.candidate && entry.action !== "keep")
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })),
blockers,
};
};
/** Review dependency contracts, without making any storage or process-placement decision. */
function assessSemanticReviews(
baseline: WorkspaceRevision | null,
candidate: WorkspaceRevision,
receipts: readonly EvolutionReview[],
checkerVersion: string,
consumers: readonly RuntimeContract[],
): Pick<EvolutionReport, "reviews" | "blockers"> {
const blockers: string[] = [];
const providers = (workspace: WorkspaceRevision) => [ const providers = (workspace: WorkspaceRevision) => [
...workspace.packageImports.map((entry) => ({ ...workspace.packageImports.map((entry) => ({
id: entry.packageId as string, id: entry.packageId as string,
@@ -371,7 +432,7 @@ export const planEvolution = (
} }
if (!old[0] || old[0].major === provider.major) continue; if (!old[0] || old[0].major === provider.major) continue;
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`); if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
for (const consumer of nextRuntimes.values()) { for (const consumer of consumers) {
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue; if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
const requirement = { const requirement = {
consumerId: consumer.groupId, consumerId: consumer.groupId,
@@ -386,7 +447,7 @@ export const planEvolution = (
consumer: consumer.digest, consumer: consumer.digest,
checkerVersion, checkerVersion,
}); });
const accepted = (options.reviews ?? []).some( const accepted = receipts.some(
(entry) => (entry) =>
entry.requirementDigest === requirementDigest && entry.requirementDigest === requirementDigest &&
["changed", "accepted-unchanged"].includes(entry.decision) && ["changed", "accepted-unchanged"].includes(entry.decision) &&
@@ -397,18 +458,27 @@ export const planEvolution = (
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`); if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
} }
} }
return { reviews, blockers };
}
export function reviewSemanticMajors(
baseline: WorkspaceRevision | null,
candidate: WorkspaceRevision,
receipts: readonly EvolutionReview[] = [],
) {
for (const workspace of baseline ? [baseline, candidate] : [candidate]) {
const issues = validateWorkspaceRevision(workspace);
if (issues.length)
throw new Error("Invalid semantic-review workspace: " + issues.map((issue) => issue.message).join("; "));
}
if (baseline && baseline.workspaceId !== candidate.workspaceId)
throw new Error("Cannot review a different workspace");
const checkerVersion = "quixos-semantic-review-v1";
const report = assessSemanticReviews(baseline, candidate, receipts, checkerVersion, runtimeContracts(candidate));
return { return {
schemaVersion: 1,
baselineDigest: baseline ? contentDigest(baseline) : null,
candidateDigest,
checkerVersion, checkerVersion,
runtimeActions, baselineDigest: baseline ? contentDigest(baseline) : null,
storageChanges, candidateDigest: contentDigest(candidate),
migrationRequired: storageChanges.filter((entry) => entry.requiresMigration).map((entry) => entry.id), ...report,
reviews,
packageChecks: runtimeActions
.filter((entry) => entry.candidate && entry.action !== "keep")
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })),
blockers,
}; };
}; }
+178
View File
@@ -0,0 +1,178 @@
import type { ExecutionContract, InterfaceOperation, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { resolveForwardedBinding } from "./forwarding.js";
/** Validate declared effects and targets; this is not a JavaScript purity proof. */
export function validateExecutionContracts(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-execution-contract", path, message });
const interfaces = new Map(workspace.interfaceImports.map((i) => [i.revisionId, i]));
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId, p]));
const operations = (id: InterfaceOperation["id"], revision: WorkspaceRevision["interfaceImports"][number]) =>
(revision.template?.members ?? revision.members).flatMap((m) => m.operations).find((o) => o.id === id);
const check = (contract: ExecutionContract | undefined, path: string) => {
if (!contract || contract.kind === "remote") return;
if (
!["snapshot", "transaction"].includes(contract.kind) ||
!Array.isArray(contract.targets) ||
!contract.targets.length ||
contract.targets.some((t) => typeof t !== "string" || !t.trim()) ||
new Set(contract.targets).size !== contract.targets.length
)
fail(path, "Portable execution requires a nonempty set of distinct target names");
};
const supports = (
requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined,
path: string,
) => {
if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") {
fail(path, "Portable execution depends on a remote-only operation");
return;
}
if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction");
};
for (const revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) {
for (const operation of member.operations) {
check(operation.execution, `${revision.revisionId}/${operation.id}`);
if (operation.execution && operation.execution.kind !== "remote" && operation.mode !== "call")
fail(
`${revision.revisionId}/${operation.id}`,
"Watch lifecycle operations are remote; portable reads use snapshots",
);
}
if (member.kind !== "operation" && member.kind !== "module" && member.queryRead?.execution === "portable") {
const getter = member.operations.find((o) => o.displayName === (member.kind === "value" ? "get" : "resolve"));
if (getter?.execution?.kind !== "snapshot")
fail(`${revision.revisionId}/${member.id}`, "Queryable portable members require a snapshot getter");
}
}
for (const revision of workspace.packageImports) {
for (const entry of [...revision.exports, ...(revision.genericExports ?? [])]) {
const path = `${revision.revisionId}/${entry.id}`;
check(entry.execution, path);
for (const port of entry.dependencyPorts) {
const requirement = port.requirement;
const portPath = `${path}/${port.id}`;
if (requirement.kind === "interface") {
const contract = interfaces.get(
"interfaceRevisionId" in requirement
? requirement.interfaceRevisionId
: requirement.application.definitionId,
);
const selected = requirement.operations;
if (requirement.receiver === "input") {
if (!entry.execution || entry.execution.kind === "remote" || !selected?.length)
fail(portPath, "Reference-selected calls require portable execution and an explicit selection");
for (const id of selected ?? []) {
const operation = contract && operations(id, contract);
if (!operation?.execution || operation.execution.kind === "remote" || operation.scope === "class")
fail(portPath, "Reference-selected dependencies require portable object operations");
}
}
if (selected && (!selected.length || new Set(selected).size !== selected.length))
fail(portPath, "Selected interface operations must be nonempty and distinct");
for (const id of selected ?? [])
if (contract && !operations(id, contract)) fail(portPath, `Unknown selected operation ${id}`);
if (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? [])
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath);
}
}
if (!entry.execution || entry.execution.kind === "remote") continue;
if (requirement.kind === "state" || requirement.kind === "edge") {
const read = requirement.kind === "state" ? "read" : "resolve";
for (const primitive of requirement.primitives) {
if (primitive.startsWith("watch"))
fail(portPath, "Portable operations cannot acquire watch lifecycle primitives");
else if (entry.execution.kind === "snapshot" && primitive !== read)
fail(portPath, "Snapshot operations cannot acquire write primitives");
}
} else if (requirement.kind === "query") {
const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId);
if (query?.placement?.kind !== "local")
fail(portPath, "Portable operations require a local query dependency");
} else if (requirement.kind === "creation") {
if (entry.execution.kind !== "transaction") fail(portPath, "Object creation requires transaction execution");
} else if (requirement.kind === "reflection") {
if (requirement.selectors.some((selector) => selector !== "presentations"))
fail(portPath, "Only checked presentation metadata is portable");
} else if (requirement.kind !== "interface")
fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`);
}
}
for (const query of revision.queries ?? [])
if (query.placement?.kind === "local") {
const sourceQueryId = query.placement.sourceQueryId;
const source = revision.queries?.find((q) => q.id === sourceQueryId);
if (source?.placement?.kind !== "replica")
fail(`${revision.revisionId}/${query.id}`, "Local queries must name a replica query in the same package");
}
}
for (const conformance of workspace.conformances) {
const revision = interfaces.get(conformance.interfaceRevisionId);
if (!revision) continue;
for (const entry of conformance.operationBindings) {
const operation = operations(entry.operationId, revision);
if (!operation) continue;
const path = `${conformance.atomId}/${revision.revisionId}/${entry.operationId}`;
let binding;
try {
binding = resolveForwardedBinding(
workspace,
conformance.atomId,
revision.revisionId,
entry.operationId,
).binding;
} catch {
continue;
} // Ordinary binding validation reports unresolved references and cycles.
if (entry.binding.kind === "interface") {
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 === "journal") {
const expected = binding.primitive === "publish" ? "transaction" : "snapshot";
if (operation.execution.kind !== expected) fail(path, "Native journal operation requires the matching effect");
continue;
}
if (binding.kind === "subscription") {
const expected = binding.primitive === "status" ? "snapshot" : "transaction";
if (
operation.execution.kind !== expected ||
(binding.primitive !== "status" && operation.execution.targets.some((target) => target !== "server"))
)
fail(path, "Subscription controls require authoritative execution; status is a portable snapshot");
continue;
}
if (binding.kind === "package") {
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
supports(operation.execution, provider?.execution, path);
} else {
const read = binding.kind === "state" ? "read" : "resolve";
if (binding.primitive.startsWith("watch"))
fail(path, "Portable operations cannot bind watch lifecycle primitives");
else if (operation.execution.kind === "snapshot" && binding.primitive !== read)
fail(path, "Snapshot operations cannot bind write primitives");
}
}
}
return issues;
}
+36
View File
@@ -0,0 +1,36 @@
import type {
AtomId,
Binding,
Conformance,
InterfaceRevisionId,
OperationBinding,
OperationId,
WorkspaceRevision,
} from "./types.js";
export type ConcreteBinding = Exclude<Binding, { kind: "interface" }>;
export type ResolvedBinding = { conformance: Conformance; entry: OperationBinding; binding: ConcreteBinding };
/** Follow public operations on the same receiver, never private attachment names. */
export function resolveForwardedBinding(
workspace: Pick<WorkspaceRevision, "conformances">,
atomId: AtomId,
interfaceRevisionId: InterfaceRevisionId,
operationId: OperationId,
): ResolvedBinding {
const visited = new Set<string>();
for (;;) {
const key = `${interfaceRevisionId}\0${operationId}`;
if (visited.has(key)) throw new Error(`Forwarding cycle at ${interfaceRevisionId}/${operationId}`);
visited.add(key);
const conformance = workspace.conformances.find(
(c) => c.atomId === atomId && c.interfaceRevisionId === interfaceRevisionId,
);
const entry = conformance?.operationBindings.find((b) => b.operationId === operationId);
if (!conformance || !entry)
throw new Error(`Missing forwarding target ${atomId} as ${interfaceRevisionId}/${operationId}`);
if (entry.binding.kind !== "interface") return { conformance, entry, binding: entry.binding };
interfaceRevisionId = entry.binding.interfaceRevisionId;
operationId = entry.binding.operationId;
}
}
+21 -6
View File
@@ -22,7 +22,7 @@ import {
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & { export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
requirement: requirement:
| Extract<DependencyPort["requirement"], { kind: "query" }> | Extract<DependencyPort["requirement"], { kind: "query" | "reflection" }>
| { | {
kind: "state"; kind: "state";
valueType: ValueTypeExpression; valueType: ValueTypeExpression;
@@ -32,13 +32,21 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
kind: "edge"; kind: "edge";
target: ObjectTypeExpression; target: ObjectTypeExpression;
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"]; cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
keyType?: "string" | "boolean" | "int64";
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"]; primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
} }
| { kind: "interface"; application: InterfaceApplicationExpression } | {
kind: "interface";
application: InterfaceApplicationExpression;
operations?: import("./types.js").OperationId[];
receiver?: "input";
}
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression }; | { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
}; };
export interface GenericPackageExport { export interface GenericPackageExport {
execution?: import("./types.js").ExecutionContract;
id: PackageExportId; id: PackageExportId;
displayName: string; displayName: string;
parameters: TypeParameter[]; parameters: TypeParameter[];
@@ -68,9 +76,8 @@ export const specializePackageExport = (
const digest = createHash("sha256") const digest = createHash("sha256")
.update( .update(
JSON.stringify([ JSON.stringify([
"quixos-package-specialization-v1", "quixos-package-specialization-v2",
pkg.revisionId, pkg.revisionId,
[pkg.source.repository, pkg.source.commit],
definition.id, definition.id,
arguments_.map(canonicalTypeArgument), arguments_.map(canonicalTypeArgument),
environment.self ?? null, environment.self ?? null,
@@ -80,6 +87,7 @@ export const specializePackageExport = (
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => { const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
const requirement = port.requirement; const requirement = port.requirement;
switch (requirement.kind) { switch (requirement.kind) {
case "reflection":
case "query": case "query":
return { ...port, requirement }; return { ...port, requirement };
case "state": case "state":
@@ -89,8 +97,14 @@ export const specializePackageExport = (
case "interface": case "interface":
return { return {
...port, ...port,
requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) }, requirement: {
kind: "interface",
interfaceRevisionId: substitution.application(requirement.application),
...(requirement.operations ? { operations: requirement.operations } : {}),
...(requirement.receiver ? { receiver: requirement.receiver } : {}),
},
}; };
case "creation":
case "constructor": { case "constructor": {
const target = substitution.object(requirement.target); const target = substitution.object(requirement.target);
if (target.kind !== "atom") if (target.kind !== "atom")
@@ -102,7 +116,7 @@ export const specializePackageExport = (
return { return {
...port, ...port,
requirement: { requirement: {
kind: "constructor", kind: requirement.kind,
atomId: target.atomId, atomId: target.atomId,
inputType: substitution.value(requirement.inputType), inputType: substitution.value(requirement.inputType),
}, },
@@ -111,6 +125,7 @@ export const specializePackageExport = (
} }
}); });
const common = { const common = {
...(definition.execution ? { execution: structuredClone(definition.execution) } : {}),
id: capabilityId.packageExport(`export-application:sha256:${digest}`), id: capabilityId.packageExport(`export-application:sha256:${digest}`),
displayName: `${definition.displayName}$${digest}`, displayName: `${definition.displayName}$${digest}`,
inputType: substitution.value(definition.inputType), inputType: substitution.value(definition.inputType),
+37 -7
View File
@@ -16,7 +16,8 @@ export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }> | Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string } | { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> } | { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "list" | "optional"; value: ValueTypeExpression } | { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueTypeExpression }> }
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression } | { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] }; | { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
@@ -86,10 +87,18 @@ export const instantiateInterface = (
const members: InterfaceMember[] = template.members.map((member) => { const members: InterfaceMember[] = template.members.map((member) => {
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) }; const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
switch (member.kind) { switch (member.kind) {
case "module":
return {
...common,
kind: "module",
contract: member.contract,
...(member.subject ? { subject: substitution.object(member.subject, member.displayName) } : {}),
};
case "value": case "value":
return { return {
...common, ...common,
kind: "value", kind: "value",
...(member.nativeField ? { nativeField: true as const } : {}),
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}), ...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
valueType: substitution.value(member.valueType, member.displayName), valueType: substitution.value(member.valueType, member.displayName),
}; };
@@ -184,10 +193,12 @@ export const isStorableType = (
case "list": case "list":
case "optional": case "optional":
return visit(value.value, depth + 1); return visit(value.value, depth + 1);
// Records currently have RPC codecs, not ordinary-state persistence codecs. case "change":
// A generic bound must not promise storage that the installed model rejects.
case "record":
return false; return false;
case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1));
case "variant":
return Object.values(value.cases).every((entry) => visit(entry.value, depth + 1));
} }
}; };
return visit(type, 0); return visit(type, 0);
@@ -214,8 +225,17 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
return [value.kind, value.name]; return [value.kind, value.name];
case "message": case "message":
return ["message", value.descriptorId]; return ["message", value.descriptorId];
case "variant":
return [
"variant",
value.id,
Object.keys(value.cases)
.sort()
.map((label) => [label, value.cases[label].id, type(value.cases[label].value, depth + 1)]),
];
case "object-ref": case "object-ref":
return ["object-ref", target(value.expectation)]; return ["object-ref", target(value.expectation)];
case "change":
case "list": case "list":
case "optional": case "optional":
return [value.kind, type(value.value, depth + 1)]; return [value.kind, type(value.value, depth + 1)];
@@ -250,10 +270,8 @@ export interface AppliedInterfaceIdentity {
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => { export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
const encoding = JSON.stringify([ const encoding = JSON.stringify([
"quixos-applied-interface-v1", "quixos-applied-interface-v2",
application.definitionId, application.definitionId,
application.source.repository,
application.source.commit.toLowerCase(),
application.arguments.map(canonicalTypeArgument), application.arguments.map(canonicalTypeArgument),
application.self ?? null, application.self ?? null,
]); ]);
@@ -317,12 +335,24 @@ export class TypeSubstitution {
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType { value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth); this.budget.enter(path, depth);
switch (expression.kind) { switch (expression.kind) {
case "variant":
return {
kind: "variant",
id: expression.id,
cases: Object.fromEntries(
Object.entries(expression.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: this.value(entry.value, `${path}.${label}`, depth + 1) },
]),
),
};
case "builtin": case "builtin":
return { kind: "builtin", name: expression.name }; return { kind: "builtin", name: expression.name };
case "scalar": case "scalar":
return { kind: "scalar", name: expression.name }; return { kind: "scalar", name: expression.name };
case "message": case "message":
return { kind: "message", descriptorId: expression.descriptorId }; return { kind: "message", descriptorId: expression.descriptorId };
case "change":
case "list": case "list":
case "optional": case "optional":
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) }; return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
+1
View File
@@ -4,3 +4,4 @@ export * from "./evolution.js";
export * from "./migrations.js"; export * from "./migrations.js";
export * from "./generics.js"; export * from "./generics.js";
export * from "./generic-packages.js"; export * from "./generic-packages.js";
export * from "./modules.js";
+139
View File
@@ -0,0 +1,139 @@
import type {
BoundDependency,
Conformance,
DependencyPort,
InitializationField,
ValueType,
WorkspaceRevision,
} from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { cardinalityValueType, valueTypesEqual } from "./validation.js";
/** Creation grants seed inputs on new objects, never general private-state authority. */
export function validateInitialization(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-initialization", path, message });
const ownerKey = (owner: Conformance) => `${owner.atomId}/${owner.interfaceRevisionId}`;
const shapes = new Map<string, ValueType>();
const fieldType = (owner: Conformance, field: InitializationField): ValueType | undefined => {
const binding = field.binding;
const attachment = owner.privateAttachments.find(
(a) => a.kind === binding.kind && a.id === (binding.kind === "state" ? binding.slotId : binding.edgeTypeId),
);
if (!attachment) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`);
return;
}
if (attachment.kind === "state") {
if (["journal", "subscription"].includes(attachment.storagePolicy.kind)) {
fail(ownerKey(owner), "Native journals start empty and cannot be seeded as state");
return;
}
return attachment.valueType;
}
if (binding.kind !== "edge") return;
const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId);
const endpoint = attachment.endpoints[index];
const target = attachment.endpoints[1 - index];
if (!endpoint || !target || endpoint.constraint.kind !== "atom" || endpoint.constraint.atomId !== owner.atomId) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`);
return;
}
if (target.keyType) {
fail(
ownerKey(owner),
"Initialize a keyed relationship through its keyed projection; inverse seeds cannot supply a map key",
);
return;
}
if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") {
const fields: Record<string, ValueType> = {
key: { kind: "scalar", name: "string" },
target: cardinalityValueType(target.constraint, "exactly-one"),
};
if (endpoint.keyType)
fields.mapKey = { kind: "scalar", name: endpoint.keyType === "boolean" ? "bool" : endpoint.keyType };
return { kind: "list", value: { kind: "record", fields } };
}
return cardinalityValueType(target.constraint, endpoint.cardinality);
};
for (const owner of workspace.conformances) {
if (!owner.initialization) continue;
const fields: Record<string, ValueType> = Object.create(null);
const destinations = new Set<string>();
for (const field of owner.initialization) {
const path = `${ownerKey(owner)}/${field.input}`;
if (!field.input || Object.hasOwn(fields, field.input))
fail(path, "Initialization input names must be nonempty and distinct");
const destination = JSON.stringify(field.binding);
if (destinations.has(destination)) fail(path, "An attachment may have only one initialization input");
destinations.add(destination);
const type = fieldType(owner, field);
if (type) fields[field.input] = type;
}
shapes.set(ownerKey(owner), { kind: "record", fields });
}
const checkBindings = (ports: DependencyPort[], dependencies: BoundDependency[], path: string) => {
for (const dependency of dependencies) {
if (dependency.binding.kind !== "creation") continue;
const binding = dependency.binding;
const requirement = ports.find((p) => p.id === dependency.portId)?.requirement;
if (requirement?.kind !== "creation") continue; // General binding validation reports the kind mismatch.
const portPath = `${path}/${dependency.portId}`;
if (binding.atomId !== requirement.atomId) fail(portPath, `Creation port requires atom ${requirement.atomId}`);
const initialized = new Set<string>();
const supplied = new Set<string>();
for (const mapping of binding.owners) {
const key = `${binding.atomId}/${mapping.interfaceRevisionId}`;
if (supplied.has(key)) fail(portPath, `Duplicate initialization owner ${key}`);
supplied.add(key);
const owner = workspace.conformances.find((o) => ownerKey(o) === key);
const shape = shapes.get(key);
if (!owner || !shape) {
fail(portPath, `Owner ${key} has no declared initialization mapping`);
continue;
}
let seed: ValueType | undefined = requirement.inputType;
for (const component of mapping.inputPath) seed = seed?.kind === "record" ? seed.fields[component] : undefined;
if (!seed || !valueTypesEqual(seed, shape))
fail(
portPath,
`Seed input${mapping.inputPath.map((p) => `.${p}`).join("")} does not match initialization inputs of ${key}`,
);
for (const field of owner.initialization!)
if (field.binding.kind === "state") initialized.add(field.binding.slotId);
}
const states = [
...workspace.sharedAttachments,
...workspace.conformances.flatMap((o) => o.privateAttachments),
].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId);
for (const state of states)
if (
state.kind === "state" &&
!["journal", "subscription"].includes(state.storagePolicy.kind) &&
state.defaultValue === undefined &&
!initialized.has(state.id)
)
fail(portPath, `Required state ${state.id} has no default or declared seed mapping`);
}
};
const exports = new Map(
workspace.packageImports.flatMap((p) => p.exports.map((e) => [`${p.revisionId}/${e.id}`, e] as const)),
);
for (const owner of workspace.conformances)
for (const operation of owner.operationBindings) {
if (operation.binding.kind !== "package") continue;
const entry = exports.get(`${operation.binding.packageRevisionId}/${operation.binding.exportId}`);
if (entry)
checkBindings(
entry.dependencyPorts,
operation.binding.dependencies,
`${ownerKey(owner)}/${operation.operationId}`,
);
}
for (const constructor of workspace.constructors) {
const entry = exports.get(`${constructor.packageRevisionId}/${constructor.exportId}`);
if (entry) checkBindings(entry.dependencyPorts, constructor.dependencies, `constructor/${constructor.atomId}`);
}
return issues;
}
+131
View File
@@ -0,0 +1,131 @@
import {
valueType,
type JournalPrimitive,
type InterfaceOperation,
type InterfaceRevision,
type ObjectExpectation,
type OperationId,
type ValueType,
} from "./types.js";
import { valueTypesEqual } from "./validation.js";
export const subscriptionStatusType: ValueType = {
kind: "record",
fields: {
status: valueType.string,
generation: valueType.uint64,
cursor: valueType.uint64,
failure: {
kind: "optional",
value: {
kind: "record",
fields: { publication: valueType.string, code: valueType.string, message: valueType.string },
},
},
},
};
/** Native subscription configuration is selected by the checked attach contract.
* No library/interface ID or handler method name is reserved by Camino. */
export function subscriptionContract(
payload: ValueType,
parameters: ValueType,
attach: InterfaceOperation,
interfaces: readonly InterfaceRevision[],
): { source: ObjectExpectation; handler: ObjectExpectation; operation: OperationId } {
const fail = (message: string): never => {
throw new Error(message);
};
if (
attach.mode !== "call" ||
attach.scope === "class" ||
!valueTypesEqual(attach.outputType, valueType.unit) ||
attach.inputType.kind !== "record"
)
return fail("Subscription attachment must be an object call returning unit");
const fields = attach.inputType.fields;
if (
Object.keys(fields).sort().join(",") !== "handler,parameters,source,start" ||
fields.source?.kind !== "object-ref" ||
fields.handler?.kind !== "object-ref" ||
fields.handler.expectation.kind !== "interface" ||
!valueTypesEqual(fields.parameters!, parameters)
)
return fail("Attachment needs typed source/handler references, matching parameters, and a start policy");
const start = fields.start;
if (
start?.kind !== "variant" ||
Object.keys(start.cases).sort().join(",") !== "afterCursor,future,retained" ||
!valueTypesEqual(start.cases.future!.value, valueType.unit) ||
!valueTypesEqual(start.cases.retained!.value, valueType.unit) ||
!valueTypesEqual(start.cases.afterCursor!.value, valueType.string)
)
return fail("Subscription start requires future:unit, retained:unit and afterCursor:string variant cases");
const handler = fields.handler.expectation;
const expected: ValueType = { kind: "record", fields: { event: payload, parameters } };
const candidates =
interfaces
.find((iface) => iface.revisionId === handler.interfaceRevisionId)
?.members.flatMap((member) => member.operations)
.filter(
(operation) =>
operation.mode === "call" &&
operation.scope !== "class" &&
operation.execution?.kind === "transaction" &&
operation.execution.targets.includes("server") &&
valueTypesEqual(operation.inputType, expected) &&
valueTypesEqual(operation.outputType, valueType.unit),
) ?? [];
if (candidates.length !== 1)
return fail("Handler interface must select one unambiguous server transaction from {event, parameters} to unit");
return { source: fields.source.expectation, handler, operation: candidates[0]!.id };
}
/** Journals persist checked values, including managed references. They cannot
* persist process handles, opaque messages or edit commands. */
export function journalPayloadType(type: ValueType): boolean {
switch (type.kind) {
case "scalar":
case "object-ref":
return true;
case "builtin":
return type.name === "unit";
case "list":
case "optional":
return journalPayloadType(type.value);
case "record":
return Object.values(type.fields).every(journalPayloadType);
case "variant":
return Object.values(type.cases).every((entry) => journalPayloadType(entry.value));
default:
return false;
}
}
export function journalPrimitiveSignature(
payload: ValueType,
primitive: JournalPrimitive,
): Pick<InterfaceOperation, "mode" | "inputType" | "outputType"> {
const optional = (value: ValueType): ValueType => ({ kind: "optional", value });
if (primitive === "publish")
return {
mode: "call",
inputType: payload,
outputType: { kind: "record", fields: { publication: valueType.string } },
};
if (primitive === "head") return { mode: "call", inputType: valueType.unit, outputType: optional(valueType.uint64) };
return {
mode: "call",
inputType: { kind: "record", fields: { after: optional(valueType.string), limit: valueType.uint32 } },
outputType: {
kind: "record",
fields: {
entries: {
kind: "list",
value: { kind: "record", fields: { publication: valueType.string, position: valueType.uint64, payload } },
},
next: optional(valueType.string),
asOfHead: optional(valueType.uint64),
},
},
};
}
+82
View File
@@ -0,0 +1,82 @@
import type { InterfaceRevision, ModuleInterfaceMember, PackageModule, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
/** Public nominal contract lookup; does not resolve an executable artifact or grant access. */
export function moduleContract(
interfaces: readonly InterfaceRevision[],
module: PackageModule,
): ModuleInterfaceMember | undefined {
const iface = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
if (!iface || iface.template) return undefined;
const member = iface.members.find((entry) => entry.id === module.memberId);
return member?.kind === "module" ? member : undefined;
}
export function validateModules(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-module-contract", path, message });
for (const iface of workspace.interfaceImports) {
for (const member of iface.template?.members ?? iface.members) {
if (member.kind !== "module") continue;
const path = `${iface.revisionId}.${member.id}`;
if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+\/[1-9][0-9]*$/.test(member.contract))
fail(path, "Module contracts require a namespaced, versioned identifier");
if (member.operations.length) fail(path, "A module member cannot declare callable operations");
}
}
for (const pkg of workspace.packageImports) {
const ids = new Set<string>(pkg.exports.map((entry) => entry.id));
const names = new Set([
...pkg.exports.map((entry) => entry.displayName),
...(pkg.genericExports ?? []).map((entry) => entry.displayName),
...(pkg.queries ?? []).map((entry) => entry.displayName),
...(pkg.queryTemplates ?? []).map((entry) => entry.declaration.displayName),
]);
for (const module of pkg.modules ?? []) {
const path = `${pkg.revisionId}.${module.id}`;
if (!module.id.trim() || !module.displayName.trim() || ids.has(module.id) || names.has(module.displayName))
fail(path, "Duplicate or empty module identity or package member name");
ids.add(module.id);
names.add(module.displayName);
if (!moduleContract(workspace.interfaceImports, module))
fail(path, "Module export must implement a closed imported module member");
}
}
for (const conformance of workspace.conformances) {
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
if (!iface) continue;
const bound = new Set<string>();
for (const binding of conformance.moduleBindings ?? []) {
const path = `${conformance.id ?? conformance.atomId}.${binding.memberId}`;
const member = iface.members.find((entry) => entry.id === binding.memberId);
const pkg = workspace.packageImports.find((entry) => entry.revisionId === binding.packageRevisionId);
const module = pkg?.modules?.find((entry) => entry.id === binding.moduleId);
if (bound.has(binding.memberId)) fail(path, "Module member is bound more than once");
bound.add(binding.memberId);
if (
member?.kind !== "module" ||
!module ||
module.interfaceRevisionId !== iface.revisionId ||
module.memberId !== member.id
) {
fail(path, "Module binding must implement this exact nominal interface member");
continue;
}
const subject = member.subject;
if (
subject &&
!(subject.kind === "atom"
? subject.atomId === conformance.atomId
: workspace.conformances.some(
(entry) =>
entry.atomId === conformance.atomId && entry.interfaceRevisionId === subject.interfaceRevisionId,
))
)
fail(path, "Implementing atom does not satisfy the module subject contract");
}
for (const member of iface.members)
if (member.kind === "module" && !bound.has(member.id))
fail(`${conformance.id ?? conformance.atomId}.${member.id}`, "Missing module member binding");
}
return issues;
}
+49
View File
@@ -0,0 +1,49 @@
import type { WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { moduleContract } from "./modules.js";
/** Presentation metadata has no graph authority and cannot name arbitrary module URLs. */
export function validatePresentations(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-presentation", path, message });
const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry]));
const packages = new Map(workspace.packageImports.map((entry) => [entry.revisionId, entry]));
const text = (value: string, path: string) => {
if (!value.trim()) fail(path, "Presentation identities and names must be nonempty");
};
const subject = (id: string, path: string) => {
const contract = interfaces.get(id as WorkspaceRevision["interfaceImports"][number]["revisionId"]);
if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`);
};
for (const [index, pkg] of workspace.packageImports.entries()) {
for (const [alias, id] of Object.entries(pkg.clientInterfaces ?? {})) {
text(alias, `packageImports[${index}].clientInterfaces`);
subject(id, `packageImports[${index}].clientInterfaces.${alias}`);
}
}
const ids = new Set<string>(),
names = new Set<string>();
for (const [index, presentation] of (workspace.presentations ?? []).entries()) {
const path = `presentations[${index}]`;
text(presentation.id, `${path}.id`);
text(presentation.displayName, `${path}.displayName`);
if (ids.has(presentation.id) || names.has(presentation.displayName))
fail(path, "Duplicate presentation identity or name");
ids.add(presentation.id);
names.add(presentation.displayName);
subject(presentation.subject, `${path}.subject`);
const module = packages
.get(presentation.packageRevisionId)
?.modules?.find((entry) => entry.id === presentation.componentId);
if (module) {
const contract = moduleContract(workspace.interfaceImports, module);
if (!contract || !["org.quixos.react.subject-only/1", "org.quixos.react.component/2"].includes(contract.contract))
fail(`${path}.componentId`, "Standalone presentation requires a supported React presentation contract");
if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject)
fail(`${path}.subject`, "Component requires its exact declared subject interface");
continue;
}
fail(`${path}.componentId`, "Presentation names no installed package component");
}
return issues;
}
+123 -6
View File
@@ -65,9 +65,11 @@ export type ValueType =
| { kind: "scalar"; name: ScalarValueTypeName } | { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string } | { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> } | { kind: "record"; fields: Record<string, ValueType> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueType }> }
| { kind: "object-ref"; expectation: ObjectExpectation } | { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType } | { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType }; | { kind: "list"; value: ValueType }
| { kind: "change"; value: ValueType };
export const valueType = { export const valueType = {
unit: { kind: "builtin", name: "unit" } as const, unit: { kind: "builtin", name: "unit" } as const,
@@ -104,7 +106,11 @@ export interface AtomDefinition {
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe"; export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
/** Omission has remote semantics; availability never strengthens an imported contract. */
export type ExecutionContract = { kind: "remote" } | { kind: "snapshot" | "transaction"; targets: string[] };
export interface InterfaceOperation<Type = ValueType> { export interface InterfaceOperation<Type = ValueType> {
execution?: ExecutionContract;
id: OperationId; id: OperationId;
displayName: string; displayName: string;
inputType: Type; inputType: Type;
@@ -116,7 +122,7 @@ export interface InterfaceOperation<Type = ValueType> {
eventType?: Type; eventType?: Type;
} }
export type QueryReadContract = { execution: "native" | "rpc-permitted" }; export type QueryReadContract = { execution: "native" | "rpc-permitted" | "portable" };
interface InterfaceMemberBase<Type = ValueType> { interface InterfaceMemberBase<Type = ValueType> {
id: MemberId; id: MemberId;
@@ -126,6 +132,8 @@ interface InterfaceMemberBase<Type = ValueType> {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> { export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value"; kind: "value";
/** Every operation resolves directly to the same native state slot. */
nativeField?: true;
queryRead?: QueryReadContract; queryRead?: QueryReadContract;
valueType: Type; valueType: Type;
} }
@@ -158,10 +166,21 @@ export interface OperationInterfaceMember<Type = ValueType> extends InterfaceMem
outputType: Type; outputType: Type;
} }
/** A typed code artifact. Module bindings are neither calls nor storage fields. */
export interface ModuleInterfaceMember<
Type = ValueType,
Target = EdgeEndpointConstraint,
> extends InterfaceMemberBase<Type> {
kind: "module";
contract: string;
subject?: Target;
}
export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> = export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> =
| ValueInterfaceMember<Type> | ValueInterfaceMember<Type>
| RelationshipInterfaceMember<Type, Target> | RelationshipInterfaceMember<Type, Target>
| OperationInterfaceMember<Type>; | OperationInterfaceMember<Type>
| ModuleInterfaceMember<Type, Target>;
export interface InterfaceRevision { export interface InterfaceRevision {
interfaceId: InterfaceId; interfaceId: InterfaceId;
@@ -177,7 +196,19 @@ export interface InterfaceRevision {
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[]; argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
} }
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType }; export interface JournalStorageLimits {
maxEntries: number;
maxBytes: number;
maxPublicationBytes: number;
maxProtectedSubscriptions: number;
}
export type StoragePolicy =
| { kind: "register" }
| { kind: "crdt-document"; updateType: ValueType }
| { kind: "journal"; limits: JournalStorageLimits }
| { kind: "subscription"; payload: ValueType };
export type JournalPrimitive = "publish" | "readPage" | "head";
export type SubscriptionPrimitive = "attach" | "status" | "pause" | "resume" | "retire";
export interface StateSlotDefinition { export interface StateSlotDefinition {
kind: "state"; kind: "state";
@@ -210,7 +241,7 @@ export interface EdgeDefinition {
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition; export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
export type StatePrimitive = "read" | "write" | "watch-start" | "watch-stop"; export type StatePrimitive = "read" | "write" | "edit" | "watch-start" | "watch-stop";
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop"; export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
@@ -223,6 +254,7 @@ export type PackageReceiverRequirement =
}; };
export type DependencyPortRequirement = export type DependencyPortRequirement =
| { kind: "reflection"; selectors: string[] }
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string } | { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string }
| { | {
kind: "state"; kind: "state";
@@ -233,12 +265,17 @@ export type DependencyPortRequirement =
kind: "edge"; kind: "edge";
target: EdgeEndpointConstraint; target: EdgeEndpointConstraint;
cardinality: EdgeCardinality; cardinality: EdgeCardinality;
keyType?: "string" | "boolean" | "int64";
primitives: EdgePrimitive[]; primitives: EdgePrimitive[];
} }
| { | {
kind: "interface"; kind: "interface";
interfaceRevisionId: InterfaceRevisionId; interfaceRevisionId: InterfaceRevisionId;
/** Exact public operation identities, not implementation export names. */
operations?: OperationId[];
receiver?: "input";
} }
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType }; | { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
export interface DependencyPort { export interface DependencyPort {
@@ -248,6 +285,11 @@ export interface DependencyPort {
} }
interface PackageExportBase { interface PackageExportBase {
/** Returns this declared birth, with unchanged operation input as its initializer. */
preparation?: { port: DependencyPortId; key: string };
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract;
id: PackageExportId; id: PackageExportId;
displayName: string; displayName: string;
inputType: ValueType; inputType: ValueType;
@@ -275,9 +317,40 @@ export interface PackageConstructorExport extends PackageExportBase {
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport; export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
export interface PackageModule {
id: string;
displayName: string;
interfaceRevisionId: InterfaceRevisionId;
memberId: MemberId;
}
export interface ModuleBinding {
memberId: MemberId;
packageRevisionId: PackageRevisionId;
moduleId: string;
}
export interface WorkspacePresentation {
id: string;
displayName: string;
subject: InterfaceRevisionId;
packageRevisionId: PackageRevisionId;
componentId: string;
}
export interface PackageRevision { export interface PackageRevision {
/** Explicit build consumers own these artifacts. Generic checking and query
* execution attach no semantics to their keys or contents. */
buildArtifacts?: Record<string, unknown>;
modules?: PackageModule[];
/** Closed interface imports exposed to this package's checked component bindings. */
clientInterfaces?: Record<string, InterfaceRevisionId>;
/** Explicit package imports supplying module selections, never the whole workspace. */
clientPackages?: Record<string, PackageRevisionId>;
controllers?: PackageController[];
queries?: import("../query/types.js").QueryDeclaration[]; queries?: import("../query/types.js").QueryDeclaration[];
checkedQueries?: import("../query/types.js").CheckedQuery[]; checkedQueries?: import("../query/types.js").CheckedQuery[];
queryCompositions?: import("../query/composition.js").CheckedQueryComposition[];
queryTemplates?: import("../query/types.js").QueryTemplate[]; queryTemplates?: import("../query/types.js").QueryTemplate[];
genericExports?: import("./generic-packages.js").GenericPackageExport[]; genericExports?: import("./generic-packages.js").GenericPackageExport[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[]; argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
@@ -291,8 +364,26 @@ export interface PackageRevision {
semanticMajor?: number; semanticMajor?: number;
} }
/** Checked adapter registration. Implementations remain ordinary object capabilities. */
export interface PackageController {
/** Optional opt-in discovery: a bounded checked workspace query in this package. */
registry?: string;
id: string;
subject: InterfaceRevisionId;
snapshot: OperationId;
remote: OperationId;
input: "unit" | "value";
}
export type DependencyBinding = export type DependencyBinding =
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string; via?: EdgeTraversal } | { kind: "reflection"; selectors: string[]; via?: never }
| {
kind: "query";
packageRevisionId: PackageRevisionId;
queryId: string;
scope: "object" | "workspace";
via?: EdgeTraversal;
}
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal } | { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
| { | {
kind: "edge"; kind: "edge";
@@ -304,6 +395,13 @@ export type DependencyBinding =
kind: "interface"; kind: "interface";
interfaceRevisionId: InterfaceRevisionId; interfaceRevisionId: InterfaceRevisionId;
via?: EdgeTraversal; via?: EdgeTraversal;
receiver?: "input";
}
| {
kind: "creation";
atomId: AtomId;
owners: { interfaceRevisionId: InterfaceRevisionId; inputPath: string[] }[];
via?: never;
} }
| { kind: "constructor"; atomId: AtomId }; | { kind: "constructor"; atomId: AtomId };
@@ -331,6 +429,14 @@ export interface RelationshipMaterialization {
} }
export type Binding = export type Binding =
| { kind: "journal"; slotId: SlotId; primitive: JournalPrimitive }
| { kind: "subscription"; slotId: SlotId; primitive: SubscriptionPrimitive }
| { kind: "lifecycle"; primitive: "delete" }
| {
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
operationId: OperationId;
}
| { | {
kind: "state"; kind: "state";
slotId: SlotId; slotId: SlotId;
@@ -347,6 +453,7 @@ export type Binding =
packageRevisionId: PackageRevisionId; packageRevisionId: PackageRevisionId;
exportId: PackageExportId; exportId: PackageExportId;
dependencies: BoundDependency[]; dependencies: BoundDependency[];
acceptanceDependencies?: BoundDependency[];
}; };
export interface OperationBinding { export interface OperationBinding {
@@ -356,7 +463,16 @@ export interface OperationBinding {
queryReason?: string; queryReason?: string;
} }
export interface InitializationField {
input: string;
binding: { kind: "state"; slotId: SlotId } | { kind: "edge"; edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId };
}
export interface Conformance { export interface Conformance {
/** Source provenance of a compiler-expanded rule, never runtime dispatch input. */
ruleOrigin?: { ruleId: string; sourceInterfaceRevisionId: InterfaceRevisionId; line: number; column: number };
moduleBindings?: ModuleBinding[];
initialization?: InitializationField[];
/** Absent only for legacy assemblies awaiting explicit ownership enrollment. */ /** Absent only for legacy assemblies awaiting explicit ownership enrollment. */
id?: ConformanceId; id?: ConformanceId;
semanticMajor?: number; semanticMajor?: number;
@@ -375,6 +491,7 @@ export interface AtomConstructorBinding {
} }
export interface WorkspaceRevision { export interface WorkspaceRevision {
presentations?: WorkspacePresentation[];
linkedQueries?: import("../query/link.js").LinkedQuery[]; linkedQueries?: import("../query/link.js").LinkedQuery[];
id: WorkspaceRevisionId; id: WorkspaceRevisionId;
workspaceId: WorkspaceId; workspaceId: WorkspaceId;
+609 -51
View File
@@ -1,3 +1,16 @@
import { isStorableType } from "./generics.js";
import {
journalPayloadType,
journalPrimitiveSignature,
subscriptionContract,
subscriptionStatusType,
} from "./journals.js";
import { validateInitialization } from "./initialization.js";
import { validatePresentations } from "./presentations.js";
import { validateModules } from "./modules.js";
import { validateWorkspaceControllers } from "./controllers.js";
import { validateExecutionContracts } from "./execution.js";
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
import type { import type {
AtomConstructorBinding, AtomConstructorBinding,
AtomId, AtomId,
@@ -40,6 +53,11 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode = export type CapabilityValidationIssueCode =
| "invalid-controller"
| "invalid-presentation"
| "invalid-module-contract"
| "invalid-initialization"
| "invalid-execution-contract"
| "invalid-query-contract" | "invalid-query-contract"
| "query-provider-reason-required" | "query-provider-reason-required"
| "invalid-semantic-major" | "invalid-semantic-major"
@@ -65,7 +83,9 @@ 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-dependency-binding" | "invalid-dependency-binding"
| "private-attachment-access" | "private-attachment-access"
| "incompatible-package-receiver" | "incompatible-package-receiver"
@@ -134,6 +154,11 @@ const requireText = (issues: CapabilityValidationIssue[], value: string, path: s
const typeLabel = (type: ValueType): string => { const typeLabel = (type: ValueType): string => {
switch (type.kind) { switch (type.kind) {
case "variant":
return `variant:${type.id}{${Object.entries(type.cases)
.sort(([a], [b]) => a.localeCompare(b))
.map(([label, entry]) => `${label}:${entry.id}:${typeLabel(entry.value)}`)
.join(";")}}`;
case "builtin": case "builtin":
case "scalar": case "scalar":
return type.name; return type.name;
@@ -148,6 +173,8 @@ const typeLabel = (type: ValueType): string => {
return type.expectation.kind === "atom" return type.expectation.kind === "atom"
? `object:atom:${type.expectation.atomId}` ? `object:atom:${type.expectation.atomId}`
: `object:interface:${type.expectation.interfaceRevisionId}`; : `object:interface:${type.expectation.interfaceRevisionId}`;
case "change":
return `change<${typeLabel(type.value)}>`;
case "optional": case "optional":
return `optional<${typeLabel(type.value)}>`; return `optional<${typeLabel(type.value)}>`;
case "list": case "list":
@@ -160,6 +187,19 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
return false; return false;
} }
switch (left.kind) { switch (left.kind) {
case "variant": {
const other = right as typeof left;
return (
left.id === other.id &&
Object.keys(left.cases).length === Object.keys(other.cases).length &&
Object.entries(left.cases).every(
([label, entry]) =>
Object.hasOwn(other.cases, label) &&
entry.id === other.cases[label].id &&
valueTypesEqual(entry.value, other.cases[label].value),
)
);
}
case "builtin": case "builtin":
case "scalar": case "scalar":
return left.name === (right as typeof left).name; return left.name === (right as typeof left).name;
@@ -183,6 +223,7 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
? left.expectation.atomId === (other as typeof left.expectation).atomId ? left.expectation.atomId === (other as typeof left.expectation).atomId
: left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId; : left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId;
} }
case "change":
case "optional": case "optional":
case "list": case "list":
return valueTypesEqual(left.value, (right as typeof left).value); return valueTypesEqual(left.value, (right as typeof left).value);
@@ -215,6 +256,20 @@ const validateValueType = (
indexes: Pick<ValidationIndexes, "atoms" | "interfaces">, indexes: Pick<ValidationIndexes, "atoms" | "interfaces">,
) => { ) => {
switch (type.kind) { switch (type.kind) {
case "variant": {
requireText(issues, type.id, `${path}.id`, "Variant identity");
const entries = Object.entries(type.cases),
ids = new Set<string>();
if (!entries.length || entries.length > 256) issue(issues, "invalid-value-type", path, "Use 1–256 variant cases");
for (const [label, entry] of entries) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) issue(issues, "invalid-value-type", path, "Invalid variant label");
requireText(issues, entry.id, `${path}.${label}.id`, "Case identity");
if (ids.has(entry.id)) issue(issues, "invalid-value-type", path, "Duplicate case identity");
ids.add(entry.id);
validateValueType(issues, entry.value, `${path}.${label}`, indexes);
}
return;
}
case "record": case "record":
for (const [name, field] of Object.entries(type.fields)) { for (const [name, field] of Object.entries(type.fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
@@ -233,6 +288,16 @@ const validateValueType = (
case "message": case "message":
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity"); requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
return; return;
case "change":
if (!isStorableType(type.value))
issue(
issues,
"invalid-value-type",
path,
"change<T> requires a storable materialized type, not references or nested changes",
);
validateValueType(issues, type.value, path + ".value", indexes);
return;
case "optional": case "optional":
case "list": case "list":
validateValueType(issues, type.value, `${path}.value`, indexes); validateValueType(issues, type.value, `${path}.value`, indexes);
@@ -335,6 +400,8 @@ export const statePrimitiveSignature = (slot: StateSlotDefinition, primitive: St
inputType: valueType.unit, inputType: valueType.unit,
outputType: slot.valueType, outputType: slot.valueType,
}; };
case "edit":
return { mode: "call", inputType: { kind: "change", value: slot.valueType }, outputType: valueType.unit };
case "write": case "write":
return { return {
mode: "call", mode: "call",
@@ -410,7 +477,19 @@ export const edgePrimitiveSignature = (
case "disconnect": case "disconnect":
return { return {
mode: "call", mode: "call",
inputType: targetType, inputType:
primitive === "connect" && projection.endpoint.keyType
? {
kind: "record",
fields: {
mapKey: {
kind: "scalar",
name: projection.endpoint.keyType === "boolean" ? "bool" : projection.endpoint.keyType,
},
target: targetType,
},
}
: targetType,
outputType: valueType.unit, outputType: valueType.unit,
}; };
case "watch-start": case "watch-start":
@@ -438,15 +517,32 @@ const atomSatisfiesConstraint = (
? constraint.atomId === atomId ? constraint.atomId === atomId
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId)); : conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId));
const interfaceImplies = (
indexes: ValidationIndexes,
actual: InterfaceRevisionId,
required: InterfaceRevisionId,
visited = new Set<string>(),
): boolean => {
if (actual === required) return true;
if (visited.has(actual)) return false;
visited.add(actual);
return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) =>
interfaceImplies(indexes, entry, required, visited),
);
};
const constraintSatisfiesConstraint = ( const constraintSatisfiesConstraint = (
actual: EdgeEndpointConstraint, actual: EdgeEndpointConstraint,
required: EdgeEndpointConstraint, required: EdgeEndpointConstraint,
conformances: ReadonlyMap<string, Conformance>, indexes: ValidationIndexes,
) => ) =>
constraintsEqual(actual, required) || constraintsEqual(actual, required) ||
(actual.kind === "atom" && (actual.kind === "atom" &&
required.kind === "interface" && required.kind === "interface" &&
atomSatisfiesConstraint(actual.atomId, required, conformances)); atomSatisfiesConstraint(actual.atomId, required, indexes.conformances)) ||
(actual.kind === "interface" &&
required.kind === "interface" &&
interfaceImplies(indexes, actual.interfaceRevisionId, required.interfaceRevisionId));
const canAccessAttachment = ( const canAccessAttachment = (
owner: AttachmentOwner, owner: AttachmentOwner,
@@ -516,21 +612,49 @@ const collectIdentityIndexes = (
memberPath, memberPath,
"Keyed relationships require many cardinality and string, boolean or int64 keys", "Keyed relationships require many cardinality and string, boolean or int64 keys",
); );
if (member.kind !== "operation" && member.queryRead) { if (member.kind !== "operation" && member.kind !== "module" && member.queryRead) {
const contract = member.queryRead.execution; const contract = member.queryRead.execution;
const getter = member.kind === "value" ? "get" : "resolve"; const getter = member.kind === "value" ? "get" : "resolve";
if (contract !== "native" && contract !== "rpc-permitted") if (contract !== "native" && contract !== "rpc-permitted" && contract !== "portable")
issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract"); issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract");
if (member.operations.filter((operation) => operation.displayName === getter).length !== 1) if (member.operations.filter((operation) => operation.displayName === getter).length !== 1)
issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`); issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`);
if (member.kind === "relationship" && contract === "rpc-permitted") if (member.kind === "relationship" && contract === "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported"); issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
if (member.kind === "value") { if (member.kind === "value") {
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType; const queryValue = (type: ValueType): boolean =>
if (type.kind !== "scalar") type.kind === "scalar" ||
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar"); (type.kind === "variant" && isStorableType(type)) ||
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
(type.kind === "record" &&
Object.keys(type.fields).length > 0 &&
Object.values(type.fields).every(queryValue));
if (!queryValue(member.valueType))
issue(
issues,
"invalid-query-contract",
memberPath,
"Queryable values require scalar leaves in optional, list, or nonempty record shapes",
);
} }
} }
if (member.kind === "value" && member.nativeField) {
const names = member.operations.map((o) => o.displayName);
if (
!isStorableType(member.valueType) ||
!names.includes("get") ||
names.some((n) => !["get", "set", "edit"].includes(n)) ||
(names.includes("set") && names.includes("edit"))
)
issue(
issues,
"invalid-state-binding",
memberPath,
"Fields require a storable value, a native getter and at most one setter or editor",
);
if (member.queryRead && member.queryRead.execution !== "native")
issue(issues, "invalid-query-contract", memberPath, "Fields cannot have package-backed query reads");
}
requireText(issues, member.id, `${memberPath}.id`, "Member ID"); requireText(issues, member.id, `${memberPath}.id`, "Member ID");
requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name"); requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name");
if (memberIds.has(member.id)) { if (memberIds.has(member.id)) {
@@ -669,7 +793,14 @@ const collectIdentityIndexes = (
issues, issues,
"duplicate-conformance", "duplicate-conformance",
path, path,
`Duplicate conformance for ${conformance.atomId} as ${conformance.interfaceRevisionId}`, `Duplicate conformance for ${conformance.atomId} as ${conformance.interfaceRevisionId}: ` +
[conformances.get(key)!, conformance]
.map((candidate) =>
candidate.ruleOrigin
? `rule ${candidate.ruleOrigin.ruleId} from ${candidate.ruleOrigin.sourceInterfaceRevisionId} at line ${candidate.ruleOrigin.line}`
: `conformance ${candidate.id ?? "(unnamed)"}`,
)
.join(" conflicts with "),
); );
} else { } else {
conformances.set(key, conformance); conformances.set(key, conformance);
@@ -750,24 +881,12 @@ const validateInterfaces = (
issues: CapabilityValidationIssue[], issues: CapabilityValidationIssue[],
indexes: ValidationIndexes, indexes: ValidationIndexes,
) => { ) => {
const implies = (
actual: InterfaceRevisionId,
required: InterfaceRevisionId,
visited = new Set<string>(),
): boolean => {
if (actual === required) return true;
if (visited.has(actual)) return false;
visited.add(actual);
return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) =>
implies(entry, required, visited),
);
};
for (const [packageIndex, pkg] of workspace.packageImports.entries()) { for (const [packageIndex, pkg] of workspace.packageImports.entries()) {
for (const obligation of pkg.argumentRequirements ?? []) { for (const obligation of pkg.argumentRequirements ?? []) {
const satisfied = const satisfied =
obligation.target.kind === "atom" obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required); : interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied) if (!satisfied)
issue( issue(
issues, issues,
@@ -782,7 +901,7 @@ const validateInterfaces = (
for (const required of revision.requiredInterfaces ?? []) { for (const required of revision.requiredInterfaces ?? []) {
if (!indexes.interfaces.has(required)) if (!indexes.interfaces.has(required))
issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`); issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`);
else if (implies(required, revision.revisionId)) else if (interfaceImplies(indexes, required, revision.revisionId))
issue( issue(
issues, issues,
"cyclic-conformance-requirement", "cyclic-conformance-requirement",
@@ -794,7 +913,7 @@ const validateInterfaces = (
const satisfied = const satisfied =
obligation.target.kind === "atom" obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required); : interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied) if (!satisfied)
issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`); issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`);
} }
@@ -804,6 +923,8 @@ const validateInterfaces = (
validateValueType(issues, member.valueType, `${memberPath}.valueType`, indexes); validateValueType(issues, member.valueType, `${memberPath}.valueType`, indexes);
} else if (member.kind === "relationship") { } else if (member.kind === "relationship") {
validateConstraint(issues, member.target, `${memberPath}.target`, indexes); validateConstraint(issues, member.target, `${memberPath}.target`, indexes);
} else if (member.kind === "module") {
if (member.subject) validateConstraint(issues, member.subject, `${memberPath}.subject`, indexes);
} else { } else {
validateValueType(issues, member.inputType, `${memberPath}.inputType`, indexes); validateValueType(issues, member.inputType, `${memberPath}.inputType`, indexes);
validateValueType(issues, member.outputType, `${memberPath}.outputType`, indexes); validateValueType(issues, member.outputType, `${memberPath}.outputType`, indexes);
@@ -831,22 +952,62 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes); validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
const containsRpcType = (type: ValueType): boolean => const containsRpcType = (type: ValueType): boolean =>
type.kind === "object-ref" || type.kind === "object-ref" ||
type.kind === "record" || type.kind === "change" ||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
(type.kind === "variant" && Object.values(type.cases).some((entry) => containsRpcType(entry.value))) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value)); ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if ( if (
containsRpcType(attachment.valueType) || (!["journal", "subscription"].includes(attachment.storagePolicy.kind) &&
containsRpcType(attachment.valueType)) ||
(attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType)) (attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType))
) { ) {
issue( issue(
issues, issues,
"invalid-attachment", "invalid-attachment",
`${path}.valueType`, `${path}.valueType`,
"Managed object references belong in graph relationships, not ordinary state; record types are RPC-only", "Managed object references belong in graph relationships; captured changes are transport-only",
); );
} }
if (attachment.storagePolicy.kind === "crdt-document") { if (attachment.storagePolicy.kind === "crdt-document") {
validateValueType(issues, attachment.storagePolicy.updateType, `${path}.storagePolicy.updateType`, indexes); validateValueType(issues, attachment.storagePolicy.updateType, `${path}.storagePolicy.updateType`, indexes);
} }
if (attachment.storagePolicy.kind === "journal") {
if (!journalPayloadType(attachment.valueType) || attachment.defaultValue !== undefined)
issue(issues, "invalid-attachment", path, "Journals require finite typed payloads and start empty");
const limits = attachment.storagePolicy.limits;
const ceilings = {
maxEntries: 10000,
maxBytes: 1048576,
maxPublicationBytes: 262144,
maxProtectedSubscriptions: 256,
};
if (
!limits ||
Object.keys(limits).length !== 4 ||
Object.entries(ceilings).some(
([key, ceiling]) =>
!Number.isSafeInteger(limits[key as keyof typeof limits]) ||
limits[key as keyof typeof limits] < 1 ||
limits[key as keyof typeof limits] > ceiling,
) ||
limits.maxPublicationBytes > limits.maxBytes
)
issue(issues, "invalid-attachment", path, "Invalid bounded journal limits");
}
if (attachment.storagePolicy.kind === "subscription") {
validateValueType(issues, attachment.storagePolicy.payload, `${path}.storagePolicy.payload`, indexes);
if (
!journalPayloadType(attachment.valueType) ||
!journalPayloadType(attachment.storagePolicy.payload) ||
attachment.defaultValue !== undefined
)
issue(
issues,
"invalid-attachment",
path,
"Subscriptions require finite typed payloads/parameters and start unattached",
);
}
if (owner.kind === "conformance") { if (owner.kind === "conformance") {
if (attachment.attachedTo !== owner.atomId) { if (attachment.attachedTo !== owner.atomId) {
issue( issue(
@@ -938,6 +1099,52 @@ const validatePackages = (
const exportPath = `${path}.exports[${exportIndex}]`; const exportPath = `${path}.exports[${exportIndex}]`;
validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes); validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes);
validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes); validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes);
if (entry.preparation) {
const port = entry.dependencyPorts.find((port) => port.id === entry.preparation!.port);
if (
entry.execution?.kind !== "transaction" ||
entry.kind === "constructor" ||
(entry.kind === "operation" && entry.mode !== "call") ||
!entry.preparation.key ||
port?.requirement.kind !== "creation" ||
!valueTypesEqual(port.requirement.inputType, entry.inputType) ||
entry.outputType.kind !== "object-ref" ||
(entry.outputType.expectation.kind === "atom" &&
entry.outputType.expectation.atomId !== port.requirement.atomId)
)
issue(
issues,
"invalid-dependency-binding",
exportPath,
"Prepared creation requires a transaction returning its declared creation, with matching captured input types and a fixed key",
);
}
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") { if (entry.kind === "operation") {
validateOperationEventShape(issues, entry, exportPath); validateOperationEventShape(issues, entry, exportPath);
if (entry.eventType) { if (entry.eventType) {
@@ -996,6 +1203,19 @@ const validatePackages = (
for (const [portIndex, port] of entry.dependencyPorts.entries()) { for (const [portIndex, port] of entry.dependencyPorts.entries()) {
const portPath = `${exportPath}.dependencyPorts[${portIndex}]`; const portPath = `${exportPath}.dependencyPorts[${portIndex}]`;
switch (port.requirement.kind) { switch (port.requirement.kind) {
case "reflection":
if (
!port.requirement.selectors.length ||
new Set(port.requirement.selectors).size !== port.requirement.selectors.length ||
port.requirement.selectors.some(
(kind) =>
!["active", "atoms", "interfaces", "conformances", "queries", "indexes", "presentations"].includes(
kind,
),
)
)
issue(issues, "invalid-dependency-binding", portPath, "Reflection requires distinct supported selectors");
break;
case "query": case "query":
if ( if (
!indexes.packages !indexes.packages
@@ -1029,6 +1249,7 @@ const validatePackages = (
); );
} }
break; break;
case "creation":
case "constructor": case "constructor":
if (port.requirement.inputType) if (port.requirement.inputType)
validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes); validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes);
@@ -1160,6 +1381,20 @@ const validateBoundDependencies = (
continue; continue;
} }
switch (requirement.kind) { switch (requirement.kind) {
case "reflection": {
const selected = binding as Extract<DependencyBinding, { kind: "reflection" }>;
if (
selected.via ||
JSON.stringify([...selected.selectors].sort()) !== JSON.stringify([...requirement.selectors].sort())
)
issue(
issues,
"invalid-dependency-binding",
path,
"Reflection requires an explicit workspace binding with exactly the declared selectors",
);
break;
}
case "query": { case "query": {
const selected = binding as Extract<DependencyBinding, { kind: "query" }>; const selected = binding as Extract<DependencyBinding, { kind: "query" }>;
const query = params.indexes.packages const query = params.indexes.packages
@@ -1173,6 +1408,20 @@ const validateBoundDependencies = (
issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export"); issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export");
break; break;
} }
if (query.root.kind === "workspace-interface") {
if (selected.scope !== "workspace" || selected.via)
issue(
issues,
"invalid-dependency-binding",
path,
"Workspace-interface queries require an explicit workspace scope without an object traversal",
);
break;
}
if (selected.scope !== "object") {
issue(issues, "invalid-dependency-binding", path, "Object-root queries require an object dependency binding");
break;
}
const traversal = selected.via const traversal = selected.via
? validateTraversal( ? validateTraversal(
issues, issues,
@@ -1191,8 +1440,13 @@ const validateBoundDependencies = (
: [] : []
: [params.atomId]; : [params.atomId];
for (const atom of targets) for (const atom of targets)
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root))) if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root.interface)))
issue(issues, "unsatisfied-interface", path, `Query root ${atom} does not conform to ${query.root}`); issue(
issues,
"unsatisfied-interface",
path,
`Query root ${atom} does not conform to ${query.root.interface}`,
);
break; break;
} }
case "state": { case "state": {
@@ -1208,6 +1462,13 @@ const validateBoundDependencies = (
break; break;
} }
validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`); validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`);
if (["journal", "subscription"].includes(attachment.attachment.storagePolicy.kind))
issue(
issues,
"invalid-dependency-binding",
path,
"Journal storage requires checked journal interface calls, not ordinary state ports",
);
const traversal = stateBinding.via const traversal = stateBinding.via
? validateTraversal( ? validateTraversal(
issues, issues,
@@ -1237,6 +1498,12 @@ const validateBoundDependencies = (
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`, : `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
); );
} }
if (
requirement.primitives.includes("edit") &&
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
)
issue(issues, "invalid-dependency-binding", path, "Edit ports require a matching CRDT slot");
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) { if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
issue( issue(
issues, issues,
@@ -1270,6 +1537,13 @@ const validateBoundDependencies = (
); );
break; break;
} }
if (projection.target.keyType && requirement.primitives.includes("connect"))
issue(
issues,
"invalid-dependency-binding",
path,
"Connect through the keyed projection so the caller supplies a map key",
);
const traversal = edgeBinding.via const traversal = edgeBinding.via
? validateTraversal( ? validateTraversal(
issues, issues,
@@ -1301,24 +1575,30 @@ const validateBoundDependencies = (
); );
} }
if ( if (
!constraintSatisfiesConstraint( !constraintSatisfiesConstraint(projection.target.constraint, requirement.target, params.indexes) ||
projection.target.constraint, requirement.cardinality !== projection.endpoint.cardinality ||
requirement.target, requirement.keyType !== projection.endpoint.keyType
params.indexes.conformances,
) ||
requirement.cardinality !== projection.endpoint.cardinality
) { ) {
issue( issue(
issues, issues,
"invalid-dependency-binding", "invalid-dependency-binding",
`${path}.binding`, `${path}.binding`,
`Edge port target or cardinality does not match projection ${edgeBinding.projectionId}`, `Edge port target, cardinality or key type does not match projection ${edgeBinding.projectionId}`,
); );
} }
break; break;
} }
case "interface": { case "interface": {
const interfaceBinding = binding as Extract<DependencyBinding, { kind: "interface" }>; const interfaceBinding = binding as Extract<DependencyBinding, { kind: "interface" }>;
if (interfaceBinding.receiver !== requirement.receiver || (interfaceBinding.receiver && interfaceBinding.via)) {
issue(
issues,
"invalid-dependency-binding",
path,
"Reference-selected interface ports require an explicit matching from input binding",
);
break;
}
if (interfaceBinding.interfaceRevisionId !== requirement.interfaceRevisionId) { if (interfaceBinding.interfaceRevisionId !== requirement.interfaceRevisionId) {
issue( issue(
issues, issues,
@@ -1328,6 +1608,24 @@ const validateBoundDependencies = (
); );
break; break;
} }
const selected = requirement.operations?.map(
(id) => params.indexes.interfaces.get(requirement.interfaceRevisionId)?.operations.get(id)?.operation,
);
if (
selected?.some((op) => op?.scope === "class") &&
(interfaceBinding.via || interfaceBinding.receiver || selected.some((op) => op?.scope !== "class"))
) {
issue(
issues,
"invalid-dependency-binding",
path,
"Static interface dependencies require a receiverless selection of class operations on the bound atom",
);
break;
}
// The reference supplies its own checked witness at invocation time.
// This is not a requirement that the current receiver implements the interface.
if (interfaceBinding.receiver === "input") break;
const traversal = interfaceBinding.via const traversal = interfaceBinding.via
? validateTraversal( ? validateTraversal(
issues, issues,
@@ -1502,16 +1800,83 @@ const validateConformances = (
continue; continue;
} }
const operation = operationEntry.operation; const operation = operationEntry.operation;
const binding = entry.binding; let binding: ConcreteBinding;
if (entry.binding.kind === "interface") {
const target = indexes.interfaces
.get(entry.binding.interfaceRevisionId)
?.operations.get(entry.binding.operationId)?.operation;
if (!target || !signaturesMatch(operation, target) || operation.scope !== target.scope) {
issue(
issues,
"invalid-forwarding-binding",
bindingPath,
"Forwarding requires matching signatures, modes and receiver scopes",
);
continue;
}
try {
binding = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
entry.operationId,
).binding;
} catch (error) {
issue(issues, "invalid-forwarding-binding", bindingPath, String(error));
continue;
}
} else binding = entry.binding;
const member = operationEntry.member; const member = operationEntry.member;
if (member.kind === "value" && member.nativeField) {
const primitive = { get: "read", set: "write", edit: "edit" }[operation.displayName];
const getter = member.operations.find((o) => o.displayName === "get");
let read: Binding | undefined;
try {
if (getter)
read = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
getter.id,
).binding;
} catch {}
if (
binding.kind !== "state" ||
binding.primitive !== primitive ||
read?.kind !== "state" ||
read.primitive !== "read" ||
read.slotId !== binding.slotId
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Field operations must resolve to the same native state slot",
);
}
if (
member.kind === "value" &&
operation.displayName === "edit" &&
(binding.kind !== "state" || binding.primitive !== "edit")
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"CRDT edit operations must resolve to a native state edit capability",
);
if ( if (
member.kind !== "operation" && member.kind !== "operation" &&
member.kind !== "module" &&
member.queryRead && member.queryRead &&
operation.displayName === (member.kind === "value" ? "get" : "resolve") operation.displayName === (member.kind === "value" ? "get" : "resolve")
) { ) {
const native = const native =
member.kind === "value" member.kind === "value"
? binding.kind === "state" && binding.primitive === "read" ? (binding.kind === "state" && binding.primitive === "read") ||
(binding.kind === "journal" && binding.primitive === "head") ||
(binding.kind === "subscription" && binding.primitive === "status")
: binding.kind === "edge" && binding.primitive === "resolve"; : binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package")) if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue( issue(
@@ -1520,7 +1885,7 @@ const validateConformances = (
bindingPath, bindingPath,
"This queryable contract requires a native read binding; a justification cannot weaken it", "This queryable contract requires a native read binding; a justification cannot weaken it",
); );
else if (!native && !entry.queryReason?.trim()) else if (!native && member.queryRead.execution === "rpc-permitted" && !entry.queryReason?.trim())
issue( issue(
issues, issues,
"query-provider-reason-required", "query-provider-reason-required",
@@ -1537,6 +1902,9 @@ const validateConformances = (
); );
continue; continue;
} }
// The target conformance validates the concrete provider and its private
// authority. Forwarding exposes that operation, not its attachment ports.
if (entry.binding.kind === "interface") continue;
if (operation.scope === "class" && binding.kind !== "package") { if (operation.scope === "class" && binding.kind !== "package") {
issue( issue(
issues, issues,
@@ -1546,6 +1914,85 @@ 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 === "journal") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "journal"
) {
issue(issues, "invalid-state-binding", bindingPath, "Expected native journal storage");
continue;
}
validateAttachmentAccess(issues, attachment, conformance, bindingPath);
if (
attachment.attachment.attachedTo !== conformance.atomId ||
!["publish", "readPage", "head"].includes(binding.primitive) ||
!signaturesMatch(operation, journalPrimitiveSignature(attachment.attachment.valueType, binding.primitive))
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Journal primitive does not match this object operation's signature",
);
continue;
}
if (binding.kind === "subscription") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "subscription"
) {
issue(issues, "invalid-state-binding", bindingPath, "Expected native subscription storage");
continue;
}
validateAttachmentAccess(issues, attachment, conformance, bindingPath);
const slot = attachment.attachment;
if (slot.storagePolicy.kind !== "subscription") continue;
if (slot.attachedTo !== conformance.atomId)
issue(issues, "invalid-state-binding", bindingPath, "Subscription belongs to another atom");
if (binding.primitive === "attach") {
try {
subscriptionContract(
slot.storagePolicy.payload,
slot.valueType,
operation,
[...indexes.interfaces.values()].map((value) => value.revision),
);
} catch (error) {
issue(issues, "invalid-state-binding", bindingPath, String(error));
}
} else if (
!["status", "pause", "resume", "retire"].includes(binding.primitive) ||
!signaturesMatch(operation, {
mode: "call",
inputType: valueType.unit,
outputType: binding.primitive === "status" ? subscriptionStatusType : valueType.unit,
})
)
issue(issues, "invalid-state-binding", bindingPath, "Subscription primitive signature mismatch");
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") {
@@ -1558,6 +2005,8 @@ const validateConformances = (
continue; continue;
} }
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.slotId`); validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.slotId`);
if (["journal", "subscription"].includes(attachment.attachment.storagePolicy.kind))
issue(issues, "invalid-state-binding", bindingPath, "Use native journal primitives, not state access");
if (attachment.attachment.attachedTo !== conformance.atomId) { if (attachment.attachment.attachedTo !== conformance.atomId) {
issue( issue(
issues, issues,
@@ -1574,6 +2023,17 @@ const validateConformances = (
"State primitives may only implement value members", "State primitives may only implement value members",
); );
} }
if (
binding.primitive === "edit" &&
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Edit requires a CRDT slot with a matching materialized profile type",
);
const expected = statePrimitiveSignature(attachment.attachment, binding.primitive); const expected = statePrimitiveSignature(attachment.attachment, binding.primitive);
if (!signaturesMatch(operation, expected)) { if (!signaturesMatch(operation, expected)) {
issue( issue(
@@ -1598,6 +2058,13 @@ const validateConformances = (
continue; continue;
} }
const projection = edgeProjection(attachment.attachment, binding.projectionId); const projection = edgeProjection(attachment.attachment, binding.projectionId);
if (projection?.target.keyType && binding.primitive === "connect")
issue(
issues,
"invalid-edge-binding",
bindingPath,
"Connect through the keyed projection so the caller supplies a map key",
);
// An owned endpoint may explicitly export read-only traversal, including // An owned endpoint may explicitly export read-only traversal, including
// a native inverse relationship view. This never exports mutation rights. // a native inverse relationship view. This never exports mutation rights.
if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) { if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) {
@@ -1640,11 +2107,7 @@ const validateConformances = (
"Edge primitives may only implement relationship members", "Edge primitives may only implement relationship members",
); );
} else if ( } else if (
!constraintSatisfiesConstraint( !constraintSatisfiesConstraint(projection.target.constraint, operationEntry.member.target, indexes) ||
projection.target.constraint,
operationEntry.member.target,
indexes.conformances,
) ||
operationEntry.member.cardinality !== projection.endpoint.cardinality || operationEntry.member.cardinality !== projection.endpoint.cardinality ||
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType) (operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
) { ) {
@@ -1652,7 +2115,7 @@ const validateConformances = (
issues, issues,
"invalid-edge-binding", "invalid-edge-binding",
`${bindingPath}.binding`, `${bindingPath}.binding`,
"Edge projection target or cardinality does not match relationship member", `Edge projection target or cardinality does not match ${conformance.atomId}/${conformance.interfaceRevisionId}/${operationEntry.member.displayName}`,
); );
} }
if (!signaturesMatch(operation, expected)) { if (!signaturesMatch(operation, expected)) {
@@ -1719,14 +2182,26 @@ const validateConformances = (
}); });
if ( if (
operation.scope === "class" && operation.scope === "class" &&
(binding.dependencies.some((entry) => entry.binding.kind !== "constructor") || packageExport.dependencyPorts.some(({ requirement }) => {
packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor")) if (["constructor", "creation", "reflection"].includes(requirement.kind)) return false;
if (requirement.kind !== "interface") return true;
if (requirement.receiver === "input") return false;
const contract = indexes.interfaces.get(requirement.interfaceRevisionId);
return (
!requirement.operations?.length ||
!requirement.operations.every((id) =>
contract?.revision.members
.flatMap((m) => m.operations)
.some((op) => op.id === id && op.scope === "class"),
)
);
})
) { ) {
issue( issue(
issues, issues,
"invalid-package-binding", "invalid-package-binding",
bindingPath, bindingPath,
"Class functions may inject constructors, not instance-dependent ports", "Class functions require receiverless dependencies such as creation ports",
); );
} }
validateBoundDependencies(issues, { validateBoundDependencies(issues, {
@@ -1739,8 +2214,65 @@ const validateConformances = (
requirementGraph, requirementGraph,
graphSourceKey: key, 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) {
const start = member.operations.find((op) => op.mode === "watch-start");
const stop = member.operations.find((op) => op.mode === "watch-stop");
if (!start || !stop || ![start, stop].some((op) => bindings.get(op.id)?.kind === "interface")) continue;
try {
const a = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id);
const b = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, stop.id);
const target = indexes.interfaces
.get(a.conformance.interfaceRevisionId)
?.revision.members.find((m) => m.operations.some((op) => op.id === a.entry.operationId));
if (a.conformance !== b.conformance || !target?.operations.some((op) => op.id === b.entry.operationId))
issue(
issues,
"invalid-forwarding-binding",
path,
"Forwarded watch start/stop must resolve to the same target member",
);
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
if (get && (a.binding.kind === "state" || a.binding.kind === "edge")) {
const read = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
get.id,
).binding;
const matches =
a.binding.kind === "state"
? read.kind === "state" && read.slotId === a.binding.slotId
: read.kind === "edge" &&
read.edgeTypeId === a.binding.edgeTypeId &&
read.projectionId === a.binding.projectionId;
if (!matches)
issue(
issues,
"invalid-forwarding-binding",
path,
"Native forwarded watch must observe its getter's storage",
);
}
} catch (error) {
issue(issues, "invalid-forwarding-binding", path, String(error));
}
}
const materializedMembers = new Set<string>(); const materializedMembers = new Set<string>();
for (const [materializationIndex, materialization] of (conformance.relationshipMaterializations ?? []).entries()) { for (const [materializationIndex, materialization] of (conformance.relationshipMaterializations ?? []).entries()) {
const materializationPath = `${path}.relationshipMaterializations[${materializationIndex}]`; const materializationPath = `${path}.relationshipMaterializations[${materializationIndex}]`;
@@ -1966,6 +2498,13 @@ export const validateWorkspaceRevision = (workspace: WorkspaceRevision): Capabil
validatePackages(workspace, issues, indexes); validatePackages(workspace, issues, indexes);
validateConformances(workspace, issues, indexes); validateConformances(workspace, issues, indexes);
validateConstructors(workspace, issues, indexes); validateConstructors(workspace, issues, indexes);
issues.push(
...validateExecutionContracts(workspace),
...validateInitialization(workspace),
...validatePresentations(workspace),
...validateModules(workspace),
...validateWorkspaceControllers(workspace),
);
return issues; return issues;
}; };
@@ -2044,7 +2583,10 @@ export const resolveOperationBinding = (
atomId: AtomId, atomId: AtomId,
interfaceRevisionId: InterfaceRevisionId, interfaceRevisionId: InterfaceRevisionId,
operationId: OperationId, operationId: OperationId,
): Binding | undefined => resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.get(operationId); ): ConcreteBinding | undefined => {
if (!resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.has(operationId)) return undefined;
return resolveForwardedBinding(plan.source, atomId, interfaceRevisionId, operationId).binding;
};
export type ResolvedOperationPlan = export type ResolvedOperationPlan =
| { | {
@@ -2102,6 +2644,7 @@ export const resolveOperationPlan = (
attachment: owned as OwnedAttachment & { attachment: EdgeDefinition }, attachment: owned as OwnedAttachment & { attachment: EdgeDefinition },
}; };
} }
if (binding.kind === "lifecycle" || binding.kind === "journal" || binding.kind === "subscription") return undefined; // These native providers require the current kernel.
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 =>
@@ -2205,6 +2748,11 @@ export const computeCapabilityClosure = (
} }
break; break;
} }
case "creation": {
for (const owner of dependency.binding.owners)
queued.push({ atomId: dependency.binding.atomId, interfaceRevisionId: owner.interfaceRevisionId });
break;
}
case "constructor": { case "constructor": {
constructors.add(dependency.binding.atomId); constructors.add(dependency.binding.atomId);
const constructor = plan.constructors.get(dependency.binding.atomId); const constructor = plan.constructors.get(dependency.binding.atomId);
@@ -2235,12 +2783,22 @@ export const computeCapabilityClosure = (
for (const interfaceRevisionId of plan.interfaces.get(root.interfaceRevisionId)?.requiredInterfaces ?? []) { for (const interfaceRevisionId of plan.interfaces.get(root.interfaceRevisionId)?.requiredInterfaces ?? []) {
queued.push({ atomId: root.atomId, interfaceRevisionId }); queued.push({ atomId: root.atomId, interfaceRevisionId });
} }
for (const module of conformance.source.moduleBindings ?? []) {
packages.add(module.packageRevisionId);
const member = plan.interfaces
.get(root.interfaceRevisionId)
?.members.find((member) => member.id === module.memberId);
if (member?.kind === "module" && member.subject?.kind === "interface")
queued.push({ atomId: root.atomId, interfaceRevisionId: member.subject.interfaceRevisionId });
}
for (const binding of conformance.operationBindings.values()) { for (const binding of conformance.operationBindings.values()) {
if (binding.kind === "state") { if (binding.kind === "state" || binding.kind === "journal" || binding.kind === "subscription") {
attachments.add(binding.slotId); attachments.add(binding.slotId);
} else if (binding.kind === "edge") { } else if (binding.kind === "edge") {
attachments.add(binding.edgeTypeId); attachments.add(binding.edgeTypeId);
} else { } else if (binding.kind === "interface") {
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
} 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);
+141
View File
@@ -0,0 +1,141 @@
# Closed execution plans and artifacts
`compileExecutionPlan` validates a workspace, links its checked queries, resolves
public forwarding, and emits one versioned plan with an SHA-256 digest. It retains
exact interface/package revisions and generic applications, owner initialization,
storage definitions/profiles, selected operation identities, query placement, and
source provenance. `quixos-workspace-compile --execution-plan-out PATH` writes it.
The compiler emits operation dependencies as a graph. A traversed interface port
retains its edge/projection and exact selected operations; native reads identify
the canonical storage inputs. Cycles do not become infinitely expanded paths.
Execution and admission must apply their runtime depth/work budgets when walking
this graph. The plan includes the full linked GraphQL selections, not just root
traversals or a list of fields.
Local and replica queries must have the same root contract. A local query cannot
introduce a relationship path absent from its replica source; authors expand the
source selection explicitly. Portable getters may require extra canonical fields
and exactly-one dependency traversals, recorded in the dependency graph. Admission
resolves those inputs on the server for selected objects. This never changes the
source query's candidate membership, and does not claim a limited source is a
complete interface extent. An incomplete runtime projection remains a coverage
state, not permission to invent a missing value.
A portable transaction cannot depend on a local presentation query, including
through a selector. Pass explicit references and preconditions as mutation input.
Otherwise an admitted top-N list could select different mutation targets locally
and at acceptance. Remote transactions can implement explicit server workflows.
This restriction does not ban local queries from snapshot selectors. Such query
ports are view-relative: both hosts must use the frame's admitted candidate
universe. An executor must report unavailable coverage when that view is absent,
not substitute a full-server extent. Query-port acquisition remains unavailable
until the query executor implements this rule; emitting its checked dependency
graph is not runtime support.
## Artifacts and world identity
`PackageArtifacts` describes one package output containing a process service and
optional target modules. Implementation IDs are stable manifest keys, separate
from export capability IDs and JavaScript registry names. Each implementation
covers its ABI, export map, entrypoint, and declared asset paths/digests/sizes/MIME.
Its digest excludes its own digest field. Paths must be normalized and relative.
`checkExecutionWorld` verifies plan identity, supported codecs, exact source and
binding schema, service fallback, promised targets, declared exports, and all
supplied file bytes. The reader must confine paths/symlinks to the checked output
and honor its byte limit before allocation. `artifactFileReader` supplies that
adapter for trusted immutable output roots. `loadArtifactWorld` checks candidate
outputs without importing package code. `BrowserArtifactGateway` registers only
declared browser assets and verifies bytes before serving; Web Studio places it
behind workspace authentication. LF-05 still checks actual module
registries/imports and verifies build receipts; a manifest
claim alone does not establish the JavaScript exports or an isolated process.
The returned `camino-world-v1` descriptor includes every declared implementation
and its digest, so executor choice does not change semantics or silently load new
code. Each binding key is the JSON pair `[packageRevisionId, implementationId]`;
there can be both a service and module for the server target. World hashing uses
the core codec's canonical ordering and domain-separated SHA-256 framing. The
monorepo `camino-execution-world` check compares the compiler and core implementations.
No machine-local Nix path enters the world descriptor.
The current binding schema is the closed workspace schema. This is conservative:
an unrelated contract change can require checking/rebuilding several packages.
LF-05 may narrow it to exact checked per-package closures, but must use the same
schema for generation, receipts, and world validation. It must not substitute an
older package schema just because the source revision did not change.
## Transaction-port ABI
`quixos-transaction-ports-v1` uses coordinator-issued frame/invocation handles and
monotonic invocation-local sequence numbers. A package receives its export ID,
input, and optional receiver. Port calls name a declared port and its allowed
primitive or exact interface operation; they never select arbitrary private slots,
packages, or coordinator transactions. Creation uses an explicit stable call key. Interface calls into transaction
operations also require a stable key, checked after resolving the selected
operation. The coordinator extends the birth path; it never derives that path
from an asynchronous completion order or the transport sequence number.
The coordinator resolves each port through the installed operation binding,
checks effects/selected methods, validates typed arguments/references, and joins
nested calls to the outer logical transaction. A nested failure poisons the outer domain
transaction even if application code catches it; frame cleanup cannot turn it into
a partial success. Process completion cannot independently commit. The coordinator
owns savepoints, cancellation, frame expiry, and rejection. These enforcement
mechanisms are LF-06/LF-08, not guarantees provided by TypeScript frame types.
The service control vocabulary includes readiness, cancellation, and shutdown;
transport framing and generated adapters are LF-05/LF-08. Portable modules and
services implement the same declared operation contracts. ABI compatibility is
not a statement about package trust, co-location permission, or multi-user isolation.
Portable package bindings are generated with `quixos-codegen-ts` options
`{"portable":{"target":"browser"}}` (or `server`). They expose `Contexts`,
`PortableImplementation`, checked codec declarations, and `createRegistry`.
Handlers import the shared replica core, never Replicache. Stable mutation keys
are mandatory for creation and selected nested transaction calls. Snapshot
contexts expose only their declared read methods; runtime frame checks remain
mandatory even when the TypeScript source passed compilation.
The maintained reader source in `tools/local-first/reader/portable.ts` typechecks
against both generated targets without casts. The root
`camino-portable-bindings` qualification compares Node, Window, Worker and
serialized-port behavior. Its simulated channel tests adapters, not installed
operation acquisition or process isolation; those require the coordinator.
Closed generic package applications use the same portable adapters. The checked
workspace specializes their input/output types and dependency ports before code
generation. Each application has its own export ID and generated handler name
(`name$<specialization digest>`); handlers are concrete, so a string application
and a list-of-int64 application cannot accidentally share an untyped codec.
Common implementation helpers can still be ordinary generic TypeScript functions.
This does not implement universal runtime type application or legacy constructors.
The `camino-portable-bindings` check compiles positive/negative typed consumers and
executes both specializations in Node, Chromium Window, and Worker hosts.
Use `selectArtifactImplementation(checkedManifest, selection)` to choose an
implementation by target, ABI, required export IDs, and optionally its exact ID.
Without an ID, exactly one candidate must match. Missing exports, wrong targets,
and ambiguous choices fail; manifest order never determines execution. This
selects already checked bytes and does not grant permission to run untrusted
package code in process. Multiple selected implementations remain part of the
same checked world's complete artifact inventory.
```ts
const implementation = selectArtifactImplementation(manifest, {
target: "server",
compatibility: "quixos-service-v1",
id: "isolated-service",
exports: requiredExports,
});
```
Consumers use the selected entrypoint, registry export and export coverage from
that manifest. The reader's Nix builder accepts per-package
`implementationSelection = { "package:reader-runtime@1" = {
browser = "browser-module"; server = "server-module"; service = "service";
}; };` overrides. Its fixture requires one implementation per lane covering that
lane's needed exports; the selector itself permits a smaller requested subset.
The installation records the chosen service and module entries. Process lifecycle
and trusted-module placement still belong to the existing runner.
+411
View File
@@ -0,0 +1,411 @@
import { createHash } from "node:crypto";
import { canonicalJson } from "../capability-model/evolution.js";
import { type ExecutionPlan, type PlanPackage, ExecutionPlanError, hexDigest, executionVersions } from "./plan.js";
export interface ArtifactFile {
path: string;
digest: string;
bytes: number;
mediaType: string;
}
export interface ArtifactImplementation {
id: string;
target: string;
compatibility: "quixos-service-v1" | "quixos-js-module-v1";
path: string;
ports: "quixos-transaction-ports-v1";
registryExport?: string;
exports: string[];
/** Every executable/imported local asset, including the entrypoint, must be declared. */
files: ArtifactFile[];
contentDigest: string;
}
export interface ArtifactGroup {
schemaVersion: 1;
packageRevisionId: string;
bindingDigest: string;
versions: ExecutionPlan["definition"]["versions"];
implementations: ArtifactImplementation[];
components?: ComponentArtifact[];
}
/** Installation evidence binds a reusable group to the selected exact source.
* Compilation receipts never claim that a cache hit was built at a new pin. */
export interface PackageArtifacts extends ArtifactGroup {
sourceDigest: string;
}
export const componentSharedModules = [
"react",
"react-dom",
"react-dom/client",
"react/jsx-runtime",
"@quixos/camino-react",
"@quixos/camino-react/ui",
] as const;
export interface ComponentArtifact {
id: string;
subject: string;
compatibility: "org.quixos.react.esm/1";
path: string;
exportName: string;
sharedModules: Record<(typeof componentSharedModules)[number], string>;
files: ArtifactFile[];
contentDigest: string;
}
export interface ImplementationSelection {
target: string;
compatibility: ArtifactImplementation["compatibility"];
/** Optional only when exactly one compatible implementation covers these exports. */
id?: string;
exports: readonly string[];
}
/** Select from a checked manifest; selection does not establish package trust or isolation.
* Never resolve ambiguity by manifest order or silently substitute a requested implementation.
*/
export function selectArtifactImplementation(
manifest: PackageArtifacts,
selection: ImplementationSelection,
): ArtifactImplementation {
const candidates = manifest.implementations.filter(
(entry) =>
(selection.id === undefined || entry.id === selection.id) &&
entry.target === selection.target &&
entry.compatibility === selection.compatibility &&
selection.exports.every((id) => entry.exports.includes(id)),
);
if (candidates.length !== 1)
throw new ExecutionPlanError(
candidates.length ? "AMBIGUOUS_IMPLEMENTATION" : "IMPLEMENTATION_UNAVAILABLE",
manifest.packageRevisionId,
`${selection.id ?? "Select an explicit implementation ID"}: expected one ${selection.target}/${selection.compatibility} implementation covering the requested exports; found ${candidates.length}`,
);
return candidates[0]!;
}
export interface CheckedWorld {
codec: "camino-world-v1";
plan: string;
artifacts: { binding: string; target: string; digest: string }[];
versions: Record<string, string>;
}
const fail = (path: string, message: string): never => {
throw new ExecutionPlanError("INVALID_ARTIFACT", path, message);
};
const object = (value: unknown, path: string): Record<string, unknown> => {
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
fail(path, "Expected a plain JSON record");
return value as Record<string, unknown>;
};
const keys = (record: Record<string, unknown>, required: string[], optional: string[], path: string) => {
if (
required.some((key) => !Object.hasOwn(record, key)) ||
Object.keys(record).some((key) => ![...required, ...optional].includes(key))
)
fail(path, "Unexpected or missing fields");
};
const text = (value: unknown, path: string): string => {
if (
typeof value !== "string" ||
!value.length ||
value.length > 4096 ||
value.includes("\0") ||
Buffer.from(value, "utf8").toString("utf8") !== value
)
fail(path, "Expected bounded nonempty text");
return value as string;
};
const digest = (value: unknown, path: string): string => {
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) fail(path, "Expected SHA-256 hex");
return value as string;
};
const array = (value: unknown, path: string): unknown[] => {
if (!Array.isArray(value) || value.length > 10000) fail(path, "Expected a bounded array");
return value as unknown[];
};
const unique = (values: string[], path: string) => {
if (new Set(values).size !== values.length) fail(path, "Duplicate identities");
};
export function artifactPath(value: unknown): string {
const path = text(value, "artifact path");
if (
path.startsWith("/") ||
path.includes("\\") ||
path.split("/").some((part) => !part || part === "." || part === "..") ||
/[\u0000-\u001f\u007f?#%]/.test(path)
)
fail(path, "Expected a normalized relative artifact path");
return path;
}
/** No self-reference: the digest covers ABI/export metadata and the digests of every declared file. */
export function implementationDigest(implementation: Omit<ArtifactImplementation, "contentDigest">): string {
return hexDigest({
...implementation,
exports: [...implementation.exports].sort(),
files: [...implementation.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
});
}
export function componentArtifactDigest(component: Omit<ComponentArtifact, "contentDigest">): string {
return hexDigest({
...component,
files: [...component.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
});
}
function parseArtifactFiles(value: unknown, path: string): ArtifactFile[] {
const files = array(value, path).map((value): ArtifactFile => {
const file = object(value, path);
keys(file, ["path", "digest", "bytes", "mediaType"], [], path);
if (!Number.isSafeInteger(file.bytes) || (file.bytes as number) < 0 || (file.bytes as number) > 256 * 1024 * 1024)
fail(path, "Artifact file exceeds size bound");
return {
path: artifactPath(file.path),
digest: digest(file.digest, path),
bytes: file.bytes as number,
mediaType: text(file.mediaType, path),
};
});
unique(
files.map((file) => file.path),
path,
);
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
}
function parseComponent(value: unknown, index: number): ComponentArtifact {
const path = `components[${index}]`,
entry = object(value, path);
keys(
entry,
["id", "subject", "compatibility", "path", "exportName", "sharedModules", "files", "contentDigest"],
[],
path,
);
if (entry.compatibility !== "org.quixos.react.esm/1") fail(path, "Unsupported component ABI");
const files = parseArtifactFiles(entry.files, `${path}.files`);
const shared = object(entry.sharedModules, `${path}.sharedModules`);
keys(shared, [...componentSharedModules], [], `${path}.sharedModules`);
const sharedModules = Object.fromEntries(
componentSharedModules.map((name) => [name, artifactPath(shared[name])]),
) as ComponentArtifact["sharedModules"];
const entryPath = artifactPath(entry.path),
exportName = text(entry.exportName, path);
if (!/^[$A-Z_a-z][$\w]*$/.test(exportName)) fail(path, "Expected a JavaScript component export name");
for (const modulePath of [entryPath, ...Object.values(sharedModules)])
if (!files.some((file) => file.path === modulePath && file.mediaType === "text/javascript"))
fail(path, "Component entry and shared modules require declared JavaScript files");
const component: ComponentArtifact = {
id: text(entry.id, path),
subject: text(entry.subject, path),
compatibility: "org.quixos.react.esm/1",
path: entryPath,
exportName,
sharedModules,
files,
contentDigest: digest(entry.contentDigest, path),
};
const { contentDigest, ...body } = component;
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
return component;
}
export function parseArtifactGroup(value: unknown): ArtifactGroup {
const record = object(value, "manifest");
keys(
record,
["schemaVersion", "packageRevisionId", "bindingDigest", "versions", "implementations"],
["components"],
"manifest",
);
if (record.schemaVersion !== 1) fail("manifest", "Unsupported artifact manifest version");
const versions = object(record.versions, "versions");
keys(versions, ["value", "reference", "ports", "crdt"], [], "versions");
for (const [key, value] of Object.entries(versions)) text(value, `versions.${key}`);
const implementations = array(record.implementations, "implementations").map(
(value, index): ArtifactImplementation => {
const path = `implementations[${index}]`,
entry = object(value, path);
keys(
entry,
["id", "target", "compatibility", "path", "ports", "exports", "files", "contentDigest"],
["registryExport"],
path,
);
if (entry.compatibility !== "quixos-service-v1" && entry.compatibility !== "quixos-js-module-v1")
fail(path, "Unsupported artifact ABI");
if (entry.ports !== "quixos-transaction-ports-v1") fail(path, "Unsupported transaction port ABI");
if (
entry.compatibility === "quixos-service-v1" &&
(entry.target !== "server" || entry.registryExport !== undefined)
)
fail(path, "Service must target server and has no JS registry export");
if (entry.compatibility === "quixos-js-module-v1") text(entry.registryExport, `${path}.registryExport`);
const exports = array(entry.exports, `${path}.exports`).map((value) => text(value, `${path}.exports`));
unique(exports, `${path}.exports`);
const files = parseArtifactFiles(entry.files, `${path}.files`);
const entryPath = artifactPath(entry.path);
if (!files.some((file) => file.path === entryPath)) fail(path, "Entrypoint is not covered by the file manifest");
const result: ArtifactImplementation = {
id: text(entry.id, path),
target: text(entry.target, path),
compatibility: entry.compatibility as ArtifactImplementation["compatibility"],
path: entryPath,
ports: "quixos-transaction-ports-v1",
exports: exports.sort(),
files: files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
contentDigest: digest(entry.contentDigest, path),
...(entry.registryExport !== undefined ? { registryExport: entry.registryExport as string } : {}),
};
const { contentDigest, ...body } = result;
if (implementationDigest(body) !== contentDigest) fail(path, "Implementation metadata digest mismatch");
return result;
},
);
unique(
implementations.map((implementation) => implementation.id),
"implementations",
);
const components =
record.components === undefined ? undefined : array(record.components, "components").map(parseComponent);
if (components)
unique(
components.map((component) => component.id),
"components",
);
return {
schemaVersion: 1,
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
bindingDigest: digest(record.bindingDigest, "bindingDigest"),
versions: versions as unknown as PackageArtifacts["versions"],
implementations,
...(components ? { components } : {}),
};
}
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
const { sourceDigest, ...group } = object(value, "manifest");
return { ...parseArtifactGroup(group), sourceDigest: digest(sourceDigest, "sourceDigest") };
}
export function checkPackageCoverage(
expected: Pick<PlanPackage, "revision" | "modules" | "components" | "exports" | "bindingDigest" | "sourceDigest">,
manifest: PackageArtifacts,
): void {
for (const module of expected.modules ?? [])
if (!expected.components?.some((component) => component.id === module.id))
fail(module.id, `No artifact consumer for module contract ${module.contract} and its subject`);
if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest)
fail(expected.revision, "Substituted binding schema or source revision");
for (const component of manifest.components ?? []) {
const declaration = expected.components?.find((entry) => entry.id === component.id);
if (!declaration || declaration.subject !== component.subject || declaration.runtime !== "react")
fail(component.id, "Component differs from its declared subject/runtime contract");
}
for (const component of expected.components ?? [])
if (!manifest.components?.some((entry) => entry.id === component.id))
fail(component.id, "Missing component artifact");
for (const implementation of manifest.implementations)
for (const id of implementation.exports) {
const entry = expected.exports.find((e) => e.id === id);
if (!entry) fail(implementation.id, `Unknown export ${id}`);
if (
implementation.target !== "server" &&
(!entry?.execution ||
entry.execution.kind === "remote" ||
!entry.execution.targets.includes(implementation.target))
)
fail(implementation.id, `Export ${id} does not promise target ${implementation.target}`);
}
for (const entry of expected.exports) {
if (!manifest.implementations.some((i) => i.compatibility === "quixos-service-v1" && i.exports.includes(entry.id)))
fail(expected.revision, `Missing service fallback for ${entry.id}`);
if (entry.execution.kind !== "remote")
for (const target of entry.execution.targets)
if (
!manifest.implementations.some(
(i) =>
i.target === target &&
i.exports.includes(entry.id) &&
(target === "server" || i.compatibility !== "quixos-service-v1"),
)
)
fail(expected.revision, `Missing ${target} implementation for ${entry.id}`);
}
}
export type ArtifactReader = (packageRevisionId: string, path: string, maxBytes: number) => Promise<Uint8Array>;
/** Shared by standalone package checks and complete world installation. */
export async function checkArtifactFiles(manifest: ArtifactGroup, read: ArtifactReader): Promise<void> {
const files = new Map<string, ArtifactFile>();
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])])
for (const file of artifact.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file)) fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
for (const file of files.values()) {
const bytes = await read(manifest.packageRevisionId, file.path, file.bytes);
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
fail(file.path, "Artifact bytes do not match the checked content digest");
}
}
/** Caller supplies a reader confined to the resolved immutable package output, including symlink checks. */
export async function checkExecutionWorld(
plan: ExecutionPlan,
inputs: readonly unknown[],
read: ArtifactReader,
): Promise<CheckedWorld> {
if (canonicalJson(plan.definition.versions) !== canonicalJson(executionVersions))
fail("world", "Unsupported plan codec versions");
if (hexDigest(plan.definition) !== plan.digest) fail("plan", "Plan digest mismatch");
const manifests = inputs.map(parsePackageArtifacts);
unique(
manifests.map((m) => m.packageRevisionId),
"packages",
);
if (manifests.length !== plan.definition.packages.length) fail("world", "Package closure differs from plan");
const artifacts: CheckedWorld["artifacts"] = [];
for (const expected of plan.definition.packages) {
const manifest =
manifests.find((m) => m.packageRevisionId === expected.revision) ??
fail(expected.revision, "Missing package artifact");
checkPackageCoverage(expected, manifest);
if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions))
fail(expected.revision, "Incompatible runtime codecs");
await checkArtifactFiles(manifest, read);
for (const implementation of manifest.implementations) {
artifacts.push({
binding: JSON.stringify([expected.revision, implementation.id]),
target: implementation.target,
digest: implementation.contentDigest,
});
}
for (const component of manifest.components ?? []) {
artifacts.push({
binding: JSON.stringify([expected.revision, "component", component.id]),
target: "browser-renderer",
digest: component.contentDigest,
});
}
}
return {
codec: "camino-world-v1",
plan: plan.digest,
artifacts: artifacts.sort((a, b) =>
a.binding < b.binding ? -1 : a.binding > b.binding ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0,
),
versions: { ...plan.definition.versions },
};
}
/** Node counterpart of replica-core worldDigest. The wire fixture checks cross-package agreement. */
export function checkedWorldDigest(world: CheckedWorld): string {
const normalized = {
...world,
artifacts: [...world.artifacts].sort((a, b) =>
a.binding < b.binding ? -1 : a.binding > b.binding ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0,
),
};
const json = canonicalJson(normalized),
content = createHash("sha256").update(json).digest("hex");
const components = ["camino-world-v1", content].map((text) => Buffer.from(text, "utf8"));
const count = Buffer.alloc(4);
count.writeUInt32BE(components.length);
const pieces = [count];
for (const bytes of components) {
const length = Buffer.alloc(4);
length.writeUInt32BE(bytes.length);
pieces.push(length, bytes);
}
return createHash("sha256").update(Buffer.concat(pieces)).digest("hex");
}
+224
View File
@@ -0,0 +1,224 @@
import type { BindingSchema, BindingContractSchema } from "../bindings/index.js";
import type { InterfaceRevision, PackageRevision } from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { moduleContract } from "../capability-model/modules.js";
import { generatePackageComponentBindings, publicQueryContract, type PublicQueryContract } from "./client-contracts.js";
import { moduleTypeContext } from "./module-contracts.js";
import { executionVersions, hexDigest, ExecutionPlanError, type PlanPackage } from "./plan.js";
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
/** Inputs to compilation, independent of source pins and installed providers.
* Declared ports select interface contracts; the React consumer projects its
* imported interfaces, public queries and module inputs. Ordinary code
* and richer language type companions remain separate, explicit Nix inputs. */
export interface PackageBuildContract {
definition: {
codec: "camino-package-build-v1";
package: Omit<PlanPackage, "source" | "sourceDigest" | "bindingDigest">;
versions: typeof executionVersions;
bindings: BindingContractSchema;
queries: PublicQueryContract[];
component?: string;
moduleInterfaces: { revisionId: string; definitionId?: string; members: string[] }[];
diagnostics: string[];
};
digest: string;
}
const normalize = <T>(value: T): T => JSON.parse(canonicalJson(value)) as T;
const indexSchema = (schema: BindingSchema) => ({
interfaces: new Map<string, InterfaceRevision>(
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])].map((entry) => [entry.revisionId, entry]),
),
packages: new Map<string, PackageRevision>(schema.packages.map((entry) => [entry.revisionId, entry])),
});
export function packageBuildContexts(
schema: BindingSchema,
): Record<string, { contract: PackageBuildContract; sourceDigest: string }> {
const index = indexSchema(schema);
return Object.fromEntries(
schema.packages.map((pkg) => [
pkg.revisionId,
{
contract: projectPackageContract(schema, pkg.revisionId, index),
sourceDigest: hexDigest({ revision: pkg.revisionId, source: pkg.source }),
},
]),
);
}
export function packageBuildContract(schema: BindingSchema, revision: string): PackageBuildContract {
return projectPackageContract(schema, revision, indexSchema(schema));
}
function projectPackageContract(
schema: BindingSchema,
revision: string,
index: ReturnType<typeof indexSchema>,
): PackageBuildContract {
const declared = index.packages.get(revision);
if (!declared) throw new ExecutionPlanError("UNKNOWN_BUILD_PACKAGE", revision, "Missing package declaration");
const pkg = normalize(declared);
const interfaces = (ids: Iterable<string>) =>
[...ids].map((id) => {
const iface = index.interfaces.get(id);
if (!iface) throw new ExecutionPlanError("UNKNOWN_BUILD_INTERFACE", id, "Missing declared interface");
return normalize(iface);
});
const modules = [...(pkg.modules ?? [])]
.sort((a, b) => a.id.localeCompare(b.id))
.map((module) => {
const contract = moduleContract(interfaces([module.interfaceRevisionId]), module);
if (!contract) throw new ExecutionPlanError("UNKNOWN_MODULE_CONTRACT", module.id, "Missing module contract");
return {
...module,
contract: contract.contract,
...(contract.subject ? { subject: contract.subject } : {}),
...moduleTypeContext(index.interfaces.get(module.interfaceRevisionId)!),
};
});
const components = modules.flatMap((module) =>
["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
module.contract,
) && module.subject?.kind === "interface"
? [
{
id: module.id,
displayName: module.displayName,
subject: module.subject.interfaceRevisionId,
runtime: "react" as const,
},
]
: [],
);
const shape: PackageBuildContract["definition"]["package"] = {
revision,
exports: [...pkg.exports]
.sort((a, b) => a.id.localeCompare(b.id))
.map((entry) => ({ id: entry.id, execution: entry.execution ?? { kind: "remote" } })),
...(modules.length ? { modules } : {}),
...(components.length ? { components } : {}),
...(pkg.controllers?.length ? { controllers: [...pkg.controllers].sort((a, b) => a.id.localeCompare(b.id)) } : {}),
};
const portInterfaces = new Set(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) =>
port.requirement.kind === "interface" ? [port.requirement.interfaceRevisionId] : [],
),
),
);
const bindings: BindingContractSchema = {
format: "quixos-bindings",
version: 1,
packages: [
{
revisionId: pkg.revisionId,
exports: [...pkg.exports].sort((a, b) => a.id.localeCompare(b.id)),
...(pkg.genericExports ? { genericExports: pkg.genericExports } : {}),
},
],
interfaces: interfaces(portInterfaces)
.map(({ source, ...contract }) => contract)
.sort((a, b) => a.revisionId.localeCompare(b.revisionId)),
};
const preparation = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
const diagnostics = components.flatMap((component) => {
const input = preparation?.modules.find((entry) => entry.module === component.id);
return input && pkg.queryCompositions?.some((entry) => entry.id === input.composition)
? []
: [`${component.id}: declare checked inputs in quixos.react.json`];
});
// Only interfaces whose modules are exposed by these bindings need language
// type companions. Reference types themselves do not recursively import the
// referenced object's implementation or its conformance inventory.
const used = new Set<string>(Object.values(pkg.clientInterfaces ?? {}));
for (const module of modules) used.add(module.interfaceRevisionId);
for (const component of components) used.add(component.subject);
for (const entry of preparation?.modules ?? [])
for (const child of entry.children) if (child.role) used.add(child.role.interface);
const moduleInterfaces = interfaces(used)
.flatMap((iface: InterfaceRevision) => {
const members = iface.members
.filter((member) => member.kind === "module")
.map((member) => member.id)
.sort();
return members.length
? [
{
revisionId: iface.revisionId,
...(iface.application ? { definitionId: iface.application.definitionId } : {}),
members,
},
]
: [];
})
.sort((a, b) => a.revisionId.localeCompare(b.revisionId));
for (const module of modules)
for (const argument of index.interfaces.get(module.interfaceRevisionId)?.application?.arguments ?? [])
if (argument.kind === "object" && argument.target.kind === "interface")
used.add(argument.target.interfaceRevisionId);
const providers = new Set<string>(Object.values(pkg.clientPackages ?? {}));
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) providers.add(imported.packageRevisionId);
providers.delete(pkg.revisionId);
const componentSchema: BindingSchema = {
format: "quixos-bindings",
version: 1,
interfaces: interfaces(used),
packages: [
pkg,
...[...providers].map((id) => {
const provider = index.packages.get(id);
if (!provider) throw new ExecutionPlanError("UNKNOWN_BUILD_PROVIDER", id, "Missing declared package provider");
return normalize(provider);
}),
],
};
const definition: PackageBuildContract["definition"] = {
codec: "camino-package-build-v1",
package: shape,
versions: { ...executionVersions },
bindings,
queries: [
...new Map(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) => {
if (port.requirement.kind !== "query") return [];
const requirement = port.requirement;
const provider = index.packages.get(requirement.packageRevisionId);
if (!provider)
throw new ExecutionPlanError(
"UNKNOWN_QUERY_PROVIDER",
requirement.packageRevisionId,
"Missing declared query provider",
);
return [
[
JSON.stringify([requirement.packageRevisionId, requirement.queryId]),
publicQueryContract(provider, requirement.queryId),
] as const,
];
}),
),
).entries(),
]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, query]) => query),
...(components.length && !diagnostics.length
? { component: generatePackageComponentBindings(componentSchema, shape) }
: {}),
moduleInterfaces,
diagnostics,
};
const normalized = JSON.parse(canonicalJson(definition)) as typeof definition;
return { definition: normalized, digest: hexDigest(normalized) };
}
export function checkPackageBuildContract(contract: PackageBuildContract): void {
if (contract.definition.codec !== "camino-package-build-v1" || hexDigest(contract.definition) !== contract.digest)
throw new ExecutionPlanError("INVALID_BUILD_CONTRACT", contract.digest, "Package contract digest mismatch");
if (contract.definition.diagnostics.length)
throw new ExecutionPlanError(
"INCOMPLETE_BUILD_CONTRACT",
contract.definition.package.revision,
contract.definition.diagnostics.join("; "),
);
}
+169
View File
@@ -0,0 +1,169 @@
import { graphExecutionRepresentation } from "./graph-execution.js";
import { open, lstat } from "node:fs/promises";
import { constants } from "node:fs";
import { join } from "node:path";
import { hexDigest, ExecutionPlanError, type ExecutionPlan } from "./plan.js";
import { loadExecutionInstallation, type PackageInstallationSelection } from "./installation.js";
const rootPattern = /^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/;
async function readJson(root: string, file: string): Promise<any> {
const handle = await open(join(root, file), constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.size > 32 * 1024 * 1024) throw Error(`Invalid checked candidate file: ${file}`);
return JSON.parse(await handle.readFile("utf8"));
} finally {
await handle.close();
}
}
/** Load the checked Nix output, preserving its original compilation and provenance.
* The selected toolchain validates generated code during the build. This loader
* checks stored identities and artifact/runtime contracts, never recompiles the
* historical candidate with whichever compiler happens to run the host. */
export async function loadCheckedInstallationCandidate(
root: string,
selections: Readonly<Record<string, PackageInstallationSelection>> = {},
) {
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
throw Error("Candidate must be an immutable Nix output directory");
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = await Promise.all([
readJson(root, "candidate.json"),
readJson(root, "execution-plan.json"),
readJson(root, "execution-artifacts.json"),
readJson(root, "source.json"),
readJson(root, "entry.json"),
readJson(root, "kernel.json"),
readJson(root, "queries.json"),
readJson(root, "graph-execution.json"),
]);
const { executionContracts: _legacyContracts, ...workspace } = candidate;
if (
plan?.definition?.codec !== "camino-execution-plan-v1" ||
hexDigest(plan.definition) !== plan.digest ||
hexDigest(workspace) !== hexDigest(plan.definition.workspace)
)
throw new ExecutionPlanError(
"CANDIDATE_PLAN_MISMATCH",
root,
"Stored plan must match its digest and exact workspace source contract",
);
if (
source.kind !== "workspace" ||
source.commit !== plan.definition.workspace.sourceRootCommit ||
typeof source.repository !== "string"
)
throw new ExecutionPlanError("CANDIDATE_SOURCE_MISMATCH", root, "Expected exact workspace source provenance");
const repository = new URL(source.repository);
if (
!["https:", "ssh:"].includes(repository.protocol) ||
repository.username ||
repository.password ||
repository.hash ||
repository.search
)
throw new ExecutionPlanError("CANDIDATE_SOURCE_INVALID", root, "Expected a source URL without credentials");
if (
artifacts.codec !== "camino-checked-execution-v2" ||
typeof artifacts.generator !== "string" ||
!/^[0-9a-f]{64}$/.test(artifacts.generator) ||
!artifacts.roots ||
typeof artifacts.roots !== "object" ||
Array.isArray(artifacts.roots)
)
throw new ExecutionPlanError("CANDIDATE_ARTIFACTS_INVALID", root, "Expected checked package roots");
for (const entry of Object.values(artifacts.roots))
if (typeof entry !== "string" || !rootPattern.test(entry))
throw new ExecutionPlanError(
"CANDIDATE_ARTIFACTS_INVALID",
root,
"Package artifacts must be immutable Nix roots",
);
const installation = await loadExecutionInstallation(plan, artifacts.roots, selections);
const entry = compileWorkspaceEntry(plan, entryRequest);
for (const [name, program, codec] of [
["kernel", kernel, "camino-kernel-program-v1"],
["queries", queries, "camino-query-program-v1"],
] as const)
if (program?.codec !== codec || program.plan !== kernel.plan || hexDigest(program) !== artifacts.programs?.[name])
throw new ExecutionPlanError(
"CANDIDATE_PROGRAM_MISMATCH",
name,
"Stored interpreter input must match the checked build and plan",
);
const expectedGraph = graphExecutionRepresentation(plan, installation, kernel, queries);
if (hexDigest(graph) !== hexDigest(expectedGraph) || artifacts.execution !== hexDigest(graph))
throw new ExecutionPlanError(
"CANDIDATE_EXECUTION_MISMATCH",
root,
"Stored graph execution differs from checked inputs",
);
const provenance = {
workspaceRevision: workspace.id as string,
source: { repository: source.repository as string, commit: source.commit as string },
generator: artifacts.generator as string,
packages: (plan as ExecutionPlan).definition.packages.map((pkg) => ({
revision: pkg.revision,
source: pkg.source,
})),
};
const reference = {
root,
digest: hexDigest({
plan: plan.digest,
world: installation.world,
provenance,
roots: artifacts.roots,
entry,
programs: artifacts.programs,
}),
};
return {
reference,
workspace: plan.definition.workspace.workspaceId,
world: hexDigest(graph),
plan: kernel.plan as string,
graphExecution: graph,
provenance,
entry,
kernel,
queries,
installation,
executionPlan: plan as ExecutionPlan,
};
}
export function compileWorkspaceEntry(plan: ExecutionPlan, entryRequest: unknown): { operation: string } | null {
let entry: { operation: string } | null = null;
if (entryRequest !== null) {
if (
typeof entryRequest !== "object" ||
Array.isArray(entryRequest) ||
Object.keys(entryRequest).sort().join(",") !== "atom,interface,operation"
)
throw new ExecutionPlanError(
"INVALID_ENTRY_FACTORY",
"entry",
"Entry declaration must name atom, interface and operation identities",
);
const operation = plan.definition.operations.find(
(op) =>
op.atomId === (entryRequest as Record<string, unknown>).atom &&
op.interfaceRevisionId === (entryRequest as Record<string, unknown>).interface &&
op.operationId === (entryRequest as Record<string, unknown>).operation,
);
if (
!operation ||
operation.scope !== "class" ||
operation.execution.kind !== "transaction" ||
operation.input.kind !== "builtin" ||
operation.input.name !== "unit" ||
operation.output.kind !== "object-ref"
)
throw new ExecutionPlanError(
"INVALID_ENTRY_FACTORY",
"entry",
"Entry must be a checked unit-input class transaction returning one object reference",
);
entry = { operation: operation.key };
}
return entry;
}
+337
View File
@@ -0,0 +1,337 @@
import type {
InterfaceRevision,
InterfaceOperation,
ExecutionContract,
PackageRevision,
} from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { portableValueSchema, tsType } from "../bindings/portable.js";
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
import type { BindingSchema } from "../bindings/index.js";
import type { PackageBuildContract } from "./build-contract.js";
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
import { moduleTypeContext } from "./module-contracts.js";
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
export interface PublicOperationContract {
interface: string;
operation: string;
scope: "object" | "class";
execution: ExecutionContract;
input: unknown;
output: unknown;
fieldPolicy?: "register" | "crdt";
}
export function publicOperationContract(
iface: InterfaceRevision,
operation: InterfaceOperation,
): PublicOperationContract {
if (iface.template || operation.mode !== "call" || !iface.members.some((m) => m.operations.includes(operation)))
throw new ExecutionPlanError(
"UNSUPPORTED_CLIENT_CONTRACT",
iface.revisionId,
"Expected a closed interface call operation",
);
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"
? operation.displayName === "edit"
? "crdt"
: member.nativeField && operation.displayName === "set"
? "register"
: undefined
: undefined;
return {
...(fieldPolicy ? { fieldPolicy } : {}),
interface: iface.revisionId,
operation: operation.id,
scope: operation.scope ?? "object",
execution: execution.kind === "remote" ? execution : { ...execution, targets: [...execution.targets].sort() },
input: portableValueSchema(operation.inputType),
output: portableValueSchema(operation.outputType),
};
}
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
if (!iface || !operation || canonicalJson(publicOperationContract(iface, operation)) !== canonicalJson(contract))
throw new ExecutionPlanError(
"CLIENT_CONTRACT_MISMATCH",
`${contract.interface}/${contract.operation}`,
"Installed interface differs from the imported operation contract",
);
return resolvePlannedOperation(plan, atom, contract.interface, contract.operation);
}
export interface PublicQueryContract {
package: string;
query: string;
definitionDigest: string;
root: { kind: "object" | "workspace-interface"; interface: string };
placement: "server" | "replica" | "local";
sourceQuery?: string;
input: unknown;
output: unknown;
}
export function publicQueryContract(pkg: PackageRevision, queryId: string): PublicQueryContract {
const query = pkg.checkedQueries?.find((q) => q.declaration.id === queryId);
if (!query)
throw new ExecutionPlanError("UNCHECKED_QUERY", `${pkg.revisionId}/${queryId}`, "Expected a checked package query");
const placement = query.declaration.placement;
return {
package: pkg.revisionId,
query: queryId,
definitionDigest: query.definitionDigest,
root: query.declaration.root,
placement: placement?.kind ?? "server",
...(placement?.kind === "local" ? { sourceQuery: placement.sourceQueryId } : {}),
input: portableValueSchema(query.variables),
output: portableValueSchema(query.output),
};
}
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
throw new ExecutionPlanError(
"CLIENT_QUERY_MISMATCH",
`${contract.package}/${contract.query}`,
"Installed query differs from the imported query contract",
);
const query = plan.definition.queries.find(
(q) => q.query.packageRevisionId === contract.package && q.query.checked.declaration.id === contract.query,
);
if (!query) throw new ExecutionPlanError("UNINSTALLED_QUERY", contract.query, "No installed query binding");
return query;
}
/** Generate only the explicitly imported interfaces. No concrete atom inventory is needed. */
export function generateInterfaceClientBindings(
imports: Readonly<Record<string, InterfaceRevision>>,
runtimeModule = "@quixos/camino-replica-core",
moduleProps?: (interfaceId: string, memberId: string) => string,
): string {
const entries = Object.entries(imports).map(([alias, iface]) => {
if (iface.template)
throw new ExecutionPlanError(
"OPEN_CLIENT_CONTRACT",
alias,
"Specialize the interface before generating a client",
);
const members = iface.members.map((member) => {
if (member.kind === "module" && member.subject?.kind === "interface") {
const subject = member.subject.interfaceRevisionId;
return `${JSON.stringify(member.displayName)}: defineModuleMember<${JSON.stringify(subject)}, ${moduleProps?.(iface.revisionId, member.id) ?? "{}"}>(${JSON.stringify({ interface: iface.revisionId, member: member.id, subject, contract: member.contract, ...moduleTypeContext(iface) })})`;
}
const operations = member.operations
.filter((op) => op.mode === "call")
.map((op) => {
const contract = publicOperationContract(iface, op);
return contract.fieldPolicy && member.kind === "value"
? `${JSON.stringify(op.displayName)}: defineFieldOperation<${tsType(member.valueType)}, ${JSON.stringify(contract.fieldPolicy)}, ${JSON.stringify(contract.execution)}>(${JSON.stringify(contract)})`
: `${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}, ${JSON.stringify(contract.execution)}>(${JSON.stringify(contract)})`;
});
return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`;
});
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
});
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineModuleMember, defineInterfaceOperation, defineFieldOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
}
export function generatePackageQueryClientBindings(
pkg: PackageRevision,
runtimeModule = "@quixos/camino-replica-core",
packages: readonly PackageRevision[] = [pkg],
): string {
const internal =
(pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined)?.internalQueries ?? [];
pkg = {
...pkg,
queries: pkg.queries?.filter((query) => !internal.includes(query.id)),
checkedQueries: pkg.checkedQueries?.filter((query) => !internal.includes(query.declaration.id)),
};
const queries = (pkg.checkedQueries ?? [])
.filter((query) => !query.declaration.fragment)
.map(
(query) =>
`${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`,
);
const emitFragment = (provider: PackageRevision, id: string) => {
const query = provider.checkedQueries?.find((query) => query.declaration.id === id);
if (
!query?.declaration.fragment ||
query.declaration.fragment.admission ||
query.declaration.placement?.kind !== "local" ||
query.output.kind !== "record" ||
!query.output.fields.root
)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", id, "Expected a checked local fragment export");
const output = query.output.fields.root;
const descriptor = {
package: provider.revisionId,
fragment: id,
digest: query.definitionDigest,
subject: query.declaration.root.interface,
input: portableValueSchema(query.variables),
output: portableValueSchema(output),
admission: publicQueryContract(provider, query.declaration.placement.sourceQueryId),
view: publicQueryContract(provider, id),
};
return `definePackageFragment<${tsType(query.variables)}, ${tsType(output)}, ${JSON.stringify(query.declaration.root.interface)}>(${JSON.stringify(descriptor)})`;
};
const fragments = (pkg.checkedQueries ?? [])
.filter((query) => query.declaration.fragment && !query.declaration.fragment.admission)
.map((query) => `${JSON.stringify(query.declaration.displayName)}: ${emitFragment(pkg, query.declaration.id)}`);
const imports = new Map<string, Map<string, string>>();
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) {
if (imported.packageRevisionId === pkg.revisionId) continue;
const provider = packages.find((provider) => provider.revisionId === imported.packageRevisionId);
const name = provider?.queries?.find((query) => query.id === imported.fragmentId)?.displayName;
if (!provider || !name)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", imported.fragmentId, "Missing imported fragment provider");
const alias = imported.packageAlias ?? provider.displayName;
if (pkg.queries?.some((query) => query.fragment && query.displayName === alias))
throw new ExecutionPlanError(
"FRAGMENT_ALIAS_COLLISION",
alias,
"Imported fragment namespace conflicts with a local fragment export",
);
const entries = imports.get(alias) ?? new Map<string, string>();
entries.set(name, emitFragment(provider, imported.fragmentId));
imports.set(alias, entries);
}
for (const [alias, entries] of imports)
fragments.push(
`${JSON.stringify(alias)}: {${[...entries].map(([name, value]) => `${JSON.stringify(name)}: ${value}`).join(",")}}`,
);
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, definePackageFragment, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\nexport const fragments = {${fragments.join(",\n")}} as const;\n`;
}
/** Receipt-covered bindings expose the package's imports, component subjects and
* declared queries, never the workspace's complete atom/interface inventory. */
export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string {
const shape = plan.definition.packages.find((pkg) => pkg.revision === packageRevision);
if (!shape) throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Unknown package");
return generatePackageComponentBindings(plan.definition.bindingSchema, shape);
}
export function generatePackageComponentBindings(
schema: BindingSchema,
shape: PackageBuildContract["definition"]["package"],
): string {
const packageRevision = shape.revision;
const pkg = schema.packages.find((pkg) => pkg.revisionId === packageRevision);
const components = shape.components;
const modules = shape.modules ?? [];
if (!pkg || !components?.length)
throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Package declares no component entries");
const byId = new Map(schema.interfaces.map((iface) => [iface.revisionId as string, iface]));
const find = (id: string) => {
const contract = byId.get(id);
if (!contract || contract.template)
throw new ExecutionPlanError("OPEN_CLIENT_CONTRACT", id, "Component contracts must be closed");
return contract;
};
const imports = Object.fromEntries(
Object.entries(pkg.clientInterfaces ?? {}).map(([alias, id]) => [alias, find(id)]),
);
const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)]));
const props = (iface: string, member: string) =>
`ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`;
// Preserve closed type arguments for the artifact consumer. Interpretation of
// a particular module contract belongs to that consumer, not this compiler.
const moduleArguments = (iface: InterfaceRevision) =>
`[${(iface.application?.arguments ?? [])
.map((argument) => {
if (argument.kind === "value") return tsType(argument.type);
const target = argument.target;
const reference =
target.kind === "atom"
? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>`
: `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`;
const fields =
target.kind === "interface"
? find(target.interfaceRevisionId).members.flatMap((member) => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
const getter = member.operations.find(
(operation) => operation.displayName === (member.kind === "value" ? "get" : "resolve"),
);
return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : [];
})
: [];
return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`;
})
.join(",")}]`;
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props);
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props)
.split("\n")
.filter((line) => !line.startsWith("import "))
.join("\n")
.replace("export const interfaces =", "export const subjects =");
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react", schema.packages)
.split("\n")
.filter((line) => !line.startsWith("import "))
.join("\n");
return (
interfaces +
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PresentationHandle} from "@quixos/camino-react";\n' +
subjectBindings +
"\n" +
queries +
`\nexport const providers = {${Object.entries(pkg.clientPackages ?? {})
.map(([alias, id]) => {
const imported = schema.packages.find((entry) => entry.revisionId === id);
if (!imported) throw new ExecutionPlanError("UNINSTALLED_PACKAGE", id, "Missing imported module provider");
return `${JSON.stringify(alias)}:{${(imported.modules ?? []).map((module) => `${JSON.stringify(module.displayName)}:${JSON.stringify({ packageRevision: id, id: module.id })}`).join(",")}}`;
})
.join(",")}} as const;\n` +
`\nexport const renderers = {${Object.entries(imports)
.map(([name, iface]) => `${JSON.stringify(name)}:createObjectRenderer(${JSON.stringify(iface.revisionId)})`)
.join(",\n")}} as const;\n` +
"\nexport interface ModuleProps {}\n" +
"export type ExtraModuleProps<K extends string> = K extends keyof ModuleProps ? ModuleProps[K] : {};\n" +
"export interface Modules {\n" +
components
.map((component) => {
const module = modules.find((module) => module.id === component.id)!;
const key = JSON.stringify([module.interfaceRevisionId, module.memberId]);
const artifact = pkg.buildArtifacts?.["org.quixos.react.preparation/1"] as
| import("../react-build/declarations.js").ReactPreparationArtifact
| undefined;
const preparation = artifact?.modules.find((entry) => entry.module === module.id);
const composition = pkg.queryCompositions?.find((entry) => entry.id === preparation?.composition);
if (!preparation || !composition)
throw new ExecutionPlanError("MISSING_PRESENTATION_INPUTS", module.id, "Declare inputs in quixos.react.json");
const children = preparation.children.map((child) => {
let extra = "{}";
if (child.role) {
const iface = find(child.role.interface);
const member = iface.members.find(
(entry) => entry.id === child.role!.member || entry.displayName === child.role!.member,
)!;
extra = props(iface.revisionId, member.id);
}
const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`;
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
});
const input = `${tsType(preparation.input ?? composition.output)} & {readonly children: {${children.join(";")}}}`;
return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}, ${JSON.stringify(module.contract)}, ${moduleArguments(find(module.interfaceRevisionId))}, ${input}> & ExtraModuleProps<${JSON.stringify(key)}>>;`;
})
.join("\n") +
"\n}\n"
);
}
export function componentAssetDeclarations(): string {
return (
"// Generated component asset modules. Regenerate; do not edit.\n" +
["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"]
.map((extension) => `declare module "*.${extension}" { const value: string; export default value; }`)
.join("\n") +
'\ndeclare module "*.css";\n'
);
}
+259
View File
@@ -0,0 +1,259 @@
import { portableReflectionValue } from "./reflection.js";
import type {
BoundDependency,
DependencyBinding,
PackageExport,
PersistentAttachment,
} from "../capability-model/types.js";
import { type ExecutionInstallation, type ExecutionGroupIdentity } from "./installation.js";
import { type ExecutionPlan, hexDigest, ExecutionPlanError } from "./plan.js";
export interface ExecutionEnvironment {
/** Explicit host placement. Merely including an optional module does not select it. */
readonly placement: "service" | "module";
/** Exact host executable/isolation inputs, computed by the trusted installer. */
readonly hostDigest: string;
readonly policyDigest: string;
/** Identifier of the secret/config generation; never secret material. */
readonly credentialGeneration: string;
}
export interface InstalledExecutionGroup extends ExecutionGroupIdentity {
readonly packageRevision: string;
readonly consumedPackages: readonly string[];
}
/** Conservative outbound closure. Runtime graph values, reverse callers, source
* provenance and the workspace epoch are deliberately absent from this identity. */
export function compileExecutionGroups(
plan: ExecutionPlan,
installation: ExecutionInstallation,
environments: Readonly<Record<string, ExecutionEnvironment>>,
): InstalledExecutionGroup[] {
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("GROUP_PLAN_MISMATCH", plan.digest, "Expected one checked installation");
const workspace = plan.definition.workspace;
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId as string, p]));
const operations = new Map(plan.definition.operations.map((op) => [op.key, op]));
const attachments = new Map<string, PersistentAttachment>(
[...workspace.sharedAttachments, ...workspace.conformances.flatMap((c) => c.privateAttachments)].map((a) => [
a.id,
a,
]),
);
const logical = (revision: string) => {
const pkg = packages.get(revision);
if (!pkg) throw new ExecutionPlanError("GROUP_PACKAGE_MISSING", revision, "Uninstalled consumed package");
return pkg.packageId;
};
const environment = (revision: string) => {
const value = environments[revision];
if (
!value ||
!["service", "module"].includes(value.placement) ||
!/^[0-9a-f]{64}$/.test(value.hostDigest) ||
!/^[0-9a-f]{64}$/.test(value.policyDigest) ||
!value.credentialGeneration ||
value.credentialGeneration.length > 4096
)
throw new ExecutionPlanError(
"GROUP_ENVIRONMENT_MISSING",
revision,
"Expected checked host/policy digests and a credential generation",
);
return value;
};
const implementations = new Map(
installation.packages.map((pkg) => {
if (!pkg.service && !pkg.module) return [pkg.revision, undefined] as const;
const implementation = pkg[environment(pkg.revision).placement];
if (!implementation)
throw new ExecutionPlanError(
"GROUP_IMPLEMENTATION_MISSING",
pkg.revision,
"Host selected an unavailable implementation",
);
return [pkg.revision, implementation] as const;
}),
);
const attachmentContract = (value: PersistentAttachment) => {
const { displayName: _name, ...rest } = value;
return value.kind === "state"
? rest
: { ...rest, endpoints: value.endpoints.map(({ displayName: _name, ...endpoint }) => endpoint) };
};
const bindingContract = (value: DependencyBinding) =>
value.kind === "query" ? { ...value, packageRevisionId: logical(value.packageRevisionId) } : value;
return installation.packages
.filter((p) => !!implementations.get(p.revision))
.map((root) => {
const consumed = new Map<string, unknown>(),
visited = new Set<string>(),
visitedQueries = new Set<string>(),
consumedPackages = new Set<string>();
const remember = (key: string, value: unknown) => consumed.set(key, value);
const storage = (id: string) => {
const value = attachments.get(id);
if (!value) throw new ExecutionPlanError("GROUP_STORAGE_MISSING", id, "Uninstalled consumed storage");
remember(`storage:${id}`, attachmentContract(value));
};
// Deletion may visit related objects and their owned state. Conservatively
// include the connected schema component, including possible future edges.
const lifecycle = (atom: string) => {
const visited = new Set<string>(),
pending = [atom];
const atoms = (
constraint: Extract<PersistentAttachment, { kind: "edge" }>["endpoints"][number]["constraint"],
) =>
constraint.kind === "atom"
? [constraint.atomId as string]
: workspace.conformances
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
.map((c) => c.atomId as string);
while (pending.length) {
const current = pending.pop()!;
if (visited.has(current)) continue;
visited.add(current);
for (const owner of workspace.conformances.filter((c) => c.atomId === current))
for (const attachment of owner.privateAttachments) storage(attachment.id);
for (const attachment of attachments.values())
if (attachment.kind === "edge" && attachment.endpoints.some((e) => atoms(e.constraint).includes(current))) {
storage(attachment.id);
pending.push(...attachment.endpoints.flatMap((e) => atoms(e.constraint)));
}
}
};
const targets = (binding: DependencyBinding, atom: string): string[] => {
if (binding.kind === "interface" && binding.receiver === "input")
return workspace.conformances
.filter((c) => c.interfaceRevisionId === binding.interfaceRevisionId)
.map((c) => c.atomId);
if (!("via" in binding) || !binding.via) return [atom];
storage(binding.via.edgeTypeId);
const edge = attachments.get(binding.via.edgeTypeId);
if (!edge || edge.kind !== "edge")
throw new ExecutionPlanError("GROUP_EDGE_MISSING", atom, "Missing traversal edge");
const endpoint = edge.endpoints.find((e) => e.projectionId !== binding.via!.projectionId)!;
const constraint = endpoint.constraint;
return constraint.kind === "atom"
? [constraint.atomId]
: workspace.conformances
.filter((c) => c.interfaceRevisionId === constraint.interfaceRevisionId)
.map((c) => c.atomId);
};
const query = (revision: string, id: string) => {
const key = JSON.stringify([revision, id]);
if (visitedQueries.has(key)) return;
visitedQueries.add(key);
const value = plan.definition.queries.find((q) => q.id === key);
if (!value) throw new ExecutionPlanError("GROUP_QUERY_MISSING", key, "Missing consumed query");
remember(`query:${JSON.stringify([logical(revision), id])}`, value.query.checked.definitionDigest);
for (const operation of value.operations) visit(operation);
};
const dependencies = (entry: PackageExport, bindings: readonly BoundDependency[], atom: string) => {
for (const bound of bindings) {
const target = bound.binding,
port = entry.dependencyPorts.find((p) => p.id === bound.portId)!;
if ("via" in target && target.via) storage(target.via.edgeTypeId);
if (target.kind === "reflection") {
for (const selector of target.selectors)
remember("reflection:" + selector, portableReflectionValue(workspace, selector));
} else if (target.kind === "state") storage(target.slotId);
else if (target.kind === "edge") storage(target.edgeTypeId);
else if (target.kind === "query") query(target.packageRevisionId, target.queryId);
else if (target.kind === "interface" && port.requirement.kind === "interface") {
const iface = workspace.interfaceImports.find((i) => i.revisionId === target.interfaceRevisionId)!;
for (const targetAtom of targets(target, atom))
for (const op of port.requirement.operations ??
iface.members.flatMap((m) => m.operations.map((o) => o.id)))
visit(JSON.stringify([targetAtom, target.interfaceRevisionId, op]));
} else if (target.kind === "creation") {
for (const seed of target.owners) {
const owner = workspace.conformances.find(
(c) => c.atomId === target.atomId && c.interfaceRevisionId === seed.interfaceRevisionId,
)!;
remember(`birth:${owner.id}`, {
id: owner.id,
atom: owner.atomId,
interface: owner.interfaceRevisionId,
initialization: owner.initialization ?? [],
});
for (const attachment of owner.privateAttachments) storage(attachment.id);
}
} else
throw new ExecutionPlanError(
"GROUP_DEPENDENCY_UNSUPPORTED",
bound.portId,
"Cannot establish execution reuse for this dependency",
);
}
};
const visit = (key: string) => {
if (visited.has(key)) return;
visited.add(key);
const operation = operations.get(key);
if (!operation) throw new ExecutionPlanError("GROUP_OPERATION_MISSING", key, "Missing consumed operation");
const binding = operation.binding;
if (binding.kind === "package") {
const pkg = packages.get(binding.packageRevisionId)!;
const implementation = implementations.get(pkg.revisionId);
if (!implementation)
throw new ExecutionPlanError(
"GROUP_IMPLEMENTATION_MISSING",
pkg.revisionId,
"Consumed package has no selected server implementation",
);
consumedPackages.add(pkg.packageId);
remember(`implementation:${pkg.packageId}`, {
digest: implementation.contentDigest,
environment: environment(pkg.revisionId),
});
const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
const bindings = binding.dependencies.map((d) => ({ portId: d.portId, binding: bindingContract(d.binding) }));
const acceptance = (binding.acceptanceDependencies ?? []).map((d) => ({
portId: d.portId,
binding: bindingContract(d.binding),
}));
const contract = {
input: operation.input,
output: operation.output,
execution: operation.execution,
scope: operation.scope,
binding: {
package: pkg.packageId,
export: binding.exportId,
dependencies: bindings,
acceptanceDependencies: acceptance,
},
};
// Deduplicate aliases of the same bound export, rather than keying its owner by reverse callers.
remember(`binding:${hexDigest(contract)}`, contract);
dependencies(entry, binding.dependencies, operation.atomId);
if (entry.acceptanceExportId)
dependencies(
pkg.exports.find((e) => e.id === entry.acceptanceExportId)!,
binding.acceptanceDependencies ?? [],
operation.atomId,
);
} else {
remember(`operation:${key}`, operation);
if (binding.kind === "state") storage(binding.slotId);
if (binding.kind === "edge") storage(binding.edgeTypeId);
if (binding.kind === "lifecycle") lifecycle(operation.atomId);
}
};
for (const operation of plan.definition.operations)
if (operation.binding.kind === "package" && operation.binding.packageRevisionId === root.revision)
visit(operation.key);
const own = implementations.get(root.revision)!;
const exports = packages.get(root.revision)!.exports.map(({ displayName: _name, dependencyPorts, ...entry }) => ({
...entry,
dependencyPorts: dependencyPorts.map(({ displayName: _name, ...port }) => port),
}));
const entries = [...consumed].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
return {
logicalId: logical(root.revision),
packageRevision: root.revision,
executionDigest: `sha256:${hexDigest({ implementation: own.contentDigest, exports, environment: environment(root.revision), versions: plan.definition.versions, consumed: entries })}`,
consumedPackages: [...consumedPackages].sort(),
};
});
}
+174
View File
@@ -0,0 +1,174 @@
import { constants } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import {
artifactPath,
parsePackageArtifacts,
checkExecutionWorld,
checkedWorldDigest,
type ArtifactReader,
type ArtifactFile,
type PackageArtifacts,
type CheckedWorld,
} from "./artifacts.js";
import { type ExecutionPlan, ExecutionPlanError } from "./plan.js";
const maxFileBytes = 256 * 1024 * 1024;
const metadataLimit = 16 * 1024 * 1024;
const fail = (where: string, message: string): never => {
throw new ExecutionPlanError("ARTIFACT_FILE_INVALID", where, message);
};
/** Roots must be trusted immutable output directories. Refuse symlinks, including
* interior directory links. Hash verification is still required before serving bytes. */
export async function artifactFileReader(roots: Readonly<Record<string, string>>): Promise<ArtifactReader> {
const directories = new Map<string, string>();
for (const [revision, root] of Object.entries(roots)) {
if (!path.isAbsolute(root) || path.normalize(root) !== root || (await realpath(root)) !== root)
fail(revision, "Output root must be a canonical absolute directory");
const stat = await lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) fail(root, "Output root is not a directory");
directories.set(revision, root);
}
return async (revision, relative, limit) => {
const root = directories.get(revision);
if (!root) fail(revision, "Unknown package output");
artifactPath(relative);
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maxFileBytes) fail(relative, "Invalid file byte limit");
const components = relative.split("/");
let resolved = root!;
for (let index = 0; index < components.length; index++) {
resolved = path.join(resolved, components[index]!);
const stat = await lstat(resolved);
if (stat.isSymbolicLink() || (index < components.length - 1 ? !stat.isDirectory() : !stat.isFile()))
fail(relative, "Only real directories and regular files are allowed");
}
const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > limit) fail(relative, "File exceeds its declared bound");
// A fixed allocation and one extra byte detect growth without readFile's unbounded allocation.
const buffer = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < buffer.length) {
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
if (!bytesRead) break;
offset += bytesRead;
}
if (offset !== stat.size) fail(relative, "File changed while reading");
return buffer.subarray(0, offset);
} finally {
await handle.close();
}
};
}
export interface LoadedArtifactWorld {
world: CheckedWorld;
digest: string;
manifests: PackageArtifacts[];
read: ArtifactReader;
}
/** Immutable-output loader. No module code is imported into the coordinator. */
export async function loadArtifactWorld(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
): Promise<LoadedArtifactWorld> {
const read = await artifactFileReader(roots),
manifests: PackageArtifacts[] = [];
const expected = plan.definition.packages.map((p) => p.revision).sort();
if (JSON.stringify(Object.keys(roots).sort()) !== JSON.stringify(expected))
fail("world", "Output roots must exactly match the installed plan");
for (const revision of expected) {
const bytes = await read(revision, "share/quixos/package-artifacts.json", metadataLimit);
const manifest = parsePackageArtifacts(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
if (manifest.packageRevisionId !== revision) fail(revision, "Output belongs to another package revision");
manifests.push(manifest);
}
const world = await checkExecutionWorld(plan, manifests, read);
return { world, digest: checkedWorldDigest(world), manifests, read };
}
export interface BrowserArtifact {
url: string;
implementation: string;
packageRevisionId: string;
registryExport: string;
exports: readonly string[];
}
export interface ArtifactResponse {
status: 200 | 304;
headers: Record<string, string>;
body: Uint8Array;
}
/** Shared runtime imports use content identity, independent of the importing package/world.
* The file remains covered by every importing implementation's checked manifest. */
export function contentArtifactUrl(file: Pick<ArtifactFile, "path" | "digest">): string {
if (!/^[0-9a-f]{64}$/.test(file.digest)) fail(file.path, "Invalid content digest");
artifactPath(file.path);
return `/__quixos/artifacts/content/${file.digest}/${encodeURIComponent(path.basename(file.path))}`;
}
/** This registry is placed behind the workspace gateway's existing authentication.
* Lookup uses an explicit URL table; request paths never become filesystem paths. */
export class BrowserArtifactGateway {
readonly entries: readonly BrowserArtifact[];
private readonly routes = new Map<string, { revision: string; file: ArtifactFile }>();
private readonly read: ArtifactReader;
constructor(loaded: LoadedArtifactWorld) {
this.read = loaded.read;
const entries: BrowserArtifact[] = [];
for (const manifest of loaded.manifests)
for (const implementation of manifest.implementations) {
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");
const url = prefix + file.path.split("/").map(encodeURIComponent).join("/");
const previous = this.routes.get(url);
if (
previous &&
(previous.file.digest !== file.digest ||
previous.file.bytes !== file.bytes ||
previous.file.mediaType !== file.mediaType)
)
fail(url, "Conflicting browser asset route");
this.routes.set(url, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
// Exact shared-file names are a packaging convention, never a path lookup capability.
if (file.path.startsWith("share/quixos/shared/")) {
const contentUrl = contentArtifactUrl(file);
const existing = this.routes.get(contentUrl);
if (existing && (existing.file.bytes !== file.bytes || existing.file.mediaType !== file.mediaType))
fail(contentUrl, "Conflicting shared asset metadata");
this.routes.set(contentUrl, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
}
}
entries.push({
url: prefix + implementation.path.split("/").map(encodeURIComponent).join("/"),
implementation: implementation.id,
packageRevisionId: manifest.packageRevisionId,
registryExport: implementation.registryExport!,
exports: Object.freeze([...implementation.exports]),
});
}
this.entries = Object.freeze(entries.map((entry) => Object.freeze(entry)));
}
async response(url: string, ifNoneMatch?: string): Promise<ArtifactResponse | undefined> {
const route = this.routes.get(url);
if (!route) return undefined;
const bytes = await this.read(route.revision, route.file.path, route.file.bytes);
if (bytes.length !== route.file.bytes || createHash("sha256").update(bytes).digest("hex") !== route.file.digest)
fail(url, "Browser asset changed after world validation");
const etag = `"sha256-${route.file.digest}"`;
const headers = {
"content-type": route.file.mediaType,
"cache-control": "private, max-age=31536000, immutable",
etag,
"x-content-type-options": "nosniff",
};
return ifNoneMatch === etag
? { status: 304, headers, body: new Uint8Array() }
: { status: 200, headers, body: bytes };
}
}
+59
View File
@@ -0,0 +1,59 @@
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
import type { ExecutionInstallation } from "./installation.js";
/** Execution inputs, not build evidence. Nominal revision IDs remain dispatch
* keys: replacing a contract with a structurally similar one is not continuity.
* Host placement/policy/credentials are compared separately by execution groups. */
export function graphExecutionRepresentation(
plan: ExecutionPlan,
installation: ExecutionInstallation,
kernel: unknown,
queries: unknown,
) {
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("GRAPH_PLAN_MISMATCH", "graph", "Expected one checked installation");
if (
!kernel ||
typeof kernel !== "object" ||
Array.isArray(kernel) ||
!queries ||
typeof queries !== "object" ||
Array.isArray(queries)
)
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Expected interpreter program records");
const input = kernel as Record<string, unknown>,
queryInput = queries as Record<string, unknown>;
const { plan: key, ...program } = input;
if (
input.codec !== "camino-kernel-program-v1" ||
queryInput.codec !== "camino-query-program-v1" ||
queryInput.plan !== key ||
key !== hexDigest({ versions: plan.definition.versions, program })
)
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Interpreter inputs do not match execution key");
return {
codec: "camino-graph-execution-v1",
versions: plan.definition.versions,
kernel,
queries,
// Include every selected implementation, including acceptance/remote code
// and the browser dependency closure, not merely the public export names.
packages: installation.packages.map((pkg) => ({
revision: pkg.revision,
service: pkg.service ?? null,
module: pkg.module ?? null,
browser: pkg.browserImplementation ?? null,
})),
controllers: plan.definition.packages.map((pkg) => ({
revision: pkg.revision,
controllers: pkg.controllers ?? [],
})),
shared: {
core: installation.coreURL ?? null,
engine: installation.engineURL ?? null,
components: [
...new Map(installation.components.flatMap((component) => Object.entries(component.sharedModules))).entries(),
].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
},
};
}
+27
View File
@@ -0,0 +1,27 @@
export * from "./plan.js";
export * from "./native-backends.js";
export * from "./artifacts.js";
export * from "./ports.js";
export * from "./files.js";
export * from "./installation.js";
export * from "./client-contracts.js";
export * from "./execution-groups.js";
export * from "./receipts.js";
export { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
export * from "./kernel.js";
export * from "./projections.js";
export * from "./queries.js";
export * from "./query-document.js";
export { generatePortableClientBindings } from "../bindings/portable-client.js";
export * from "./candidate.js";
export { reviewSemanticMajors } from "../capability-model/evolution.js";
export * from "./public-reflection.js";
export * from "./graph-execution.js";
export * from "./build-contract.js";
+208
View File
@@ -0,0 +1,208 @@
import { compileKernelProgram } from "./kernel.js";
import { hexDigest, type ExecutionPlan } from "./plan.js";
import type { PublicQueryContract } from "./client-contracts.js";
import { portableValueSchema } from "../bindings/portable.js";
const scalar = (name: string) => ({ kind: "scalar", name });
const record = (fields: Record<string, unknown>) => ({ kind: "record", fields });
const optional = (value: unknown) => ({ kind: "optional", value });
const selection = (name: string, extra: Record<string, unknown> = {}) => ({
name,
key: name,
arguments: {},
conditions: [],
selection: [],
...extra,
});
const variable = (name: string) => ({ kind: "variable", name });
const meta = () => selection("_camino", { selection: [selection("ref")] });
export const inspectionPackage = "org.quixos.inspection/1";
export interface InspectionTemplate {
interface: string;
members: {
id: string;
name: string;
key: string;
flag: string;
lane: "local" | "server";
many: boolean;
relationship: boolean;
}[];
inputs?: PublicQueryContract;
view?: PublicQueryContract;
server?: PublicQueryContract;
completeInputs?: PublicQueryContract;
completeView?: PublicQueryContract;
}
/** A finite installed template per interface/lane, not a per-client query compiler.
* Flags select public cache boundaries; normal scope identity bounds specialization. */
export function compileInspectionTemplates(plan: ExecutionPlan) {
const kernel = compileKernelProgram(plan) as {
operations: {
key: string;
atom: string;
interface: string;
operation: string;
effect: string;
targets: string[];
unsupported: string | null;
output: unknown;
binding: { kind: string };
}[];
};
const queries: unknown[] = [],
contracts: PublicQueryContract[] = [],
templates: InspectionTemplate[] = [];
for (const iface of plan.definition.workspace.interfaceImports) {
if (iface.template) continue;
const atoms = plan.definition.workspace.conformances
.filter((c) => c.interfaceRevisionId === iface.revisionId)
.map((c) => c.atomId)
.sort();
if (!atoms.length) continue;
const members = iface.members
.filter((member) => (member.kind === "value" || member.kind === "relationship") && member.queryRead)
.sort((a, b) => (a.id < b.id ? -1 : 1));
if (!members.length || members.length > 128) continue;
const template: InspectionTemplate = { interface: iface.revisionId, members: [] };
for (const mode of ["local", "complete", "server"] as const) {
const lane = mode === "server" ? "server" : "local";
const complete = mode === "complete";
const fields: unknown[] = [],
selections: unknown[] = [],
inputFields: Record<string, unknown> = {},
outputFields: Record<string, unknown> = {};
for (const [index, member] of members.entries()) {
if (member.kind !== "value" && member.kind !== "relationship") continue;
const getter = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
if (!getter) continue;
const providers = atoms.map((atom) =>
kernel.operations.find(
(op) => op.atom === atom && op.interface === iface.revisionId && op.operation === getter.id,
),
);
if (providers.some((op) => !op || op.unsupported || !["snapshot", "remote"].includes(op.effect))) continue;
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;
const many = member.kind === "relationship" && ["many", "many-unique"].includes(member.cardinality);
// Only native edges promise the relationship traversal used by the interpreter.
if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue;
if (!complete && (lane === "local" || !portable))
template.members.push({
id: member.id,
name: member.displayName,
key,
flag,
lane,
many,
relationship: member.kind === "relationship",
});
inputFields[flag] = scalar("bool");
const field = selection(member.displayName, {
key,
interfaceRevisionId: iface.revisionId,
memberId: member.id,
conditions: [{ include: true, value: variable(flag) }],
});
let output = portableValueSchema(getter.outputType);
if (member.kind === "relationship") {
const expectation =
member.target.kind === "atom"
? { kind: "atom", id: member.target.atomId }
: { kind: "interface", id: member.target.interfaceRevisionId };
const node = record({ _camino: record({ ref: { kind: "object-ref", expectation } }) });
if (member.target.kind === "interface")
Object.assign(field, { targetInterfaceRevisionId: member.target.interfaceRevisionId });
if (many) {
const bound = key + (complete ? "All" : "First");
inputFields[bound] = scalar("int32");
if (!complete) inputFields[key + "After"] = optional(scalar("string"));
const keyFields = { key: scalar("string"), ...(member.keyType ? { mapKey: scalar(member.keyType) } : {}) };
const keySelections = [selection("key"), ...(member.keyType ? [selection("mapKey")] : [])];
Object.assign(field, {
arguments: complete
? { all: variable(bound) }
: { first: variable(bound), after: variable(key + "After") },
selection: [
selection("entries", { selection: [...keySelections, selection("node", { selection: [meta()] })] }),
...(complete
? []
: [selection("pageInfo", { selection: [selection("hasNextPage"), selection("endCursor")] })]),
],
});
output = record({
entries: { kind: "list", value: record({ ...keyFields, node }) },
...(complete
? {}
: { pageInfo: record({ hasNextPage: scalar("bool"), endCursor: optional(scalar("string")) }) }),
});
} else {
Object.assign(field, { selection: [meta()] });
output = node;
}
}
outputFields[key] = optional(output);
selections.push(field);
for (const op of providers)
fields.push({
atom: op!.atom,
interface: iface.revisionId,
member: member.id,
name: member.displayName,
operation: op!.key,
});
}
if (!selections.length) continue;
const root = { kind: "object" as const, interface: iface.revisionId };
const input = record(inputFields),
output = record({ root: record(outputFields) });
const base = {
root,
atoms,
variables: input,
defaults: {},
selection: [selection("root", { selection: selections })],
fields,
admittedOperations: [],
budgets: {
rows: 512,
depth: 8,
resultBytes: 8 * 1024 * 1024,
candidates: 1000,
rpcCalls: 128,
concurrency: 8,
deadlineMs: 10000,
},
};
for (const kind of lane === "local" ? (["inputs", "view"] as const) : (["server"] as const)) {
const templateKey = complete ? (kind === "inputs" ? "completeInputs" : "completeView") : kind;
const query = JSON.stringify([iface.revisionId, templateKey]);
const sourceQuery = JSON.stringify([iface.revisionId, complete ? "completeInputs" : "inputs"]);
const program = {
...base,
id: JSON.stringify([inspectionPackage, query]),
placement: kind === "inputs" ? "replica" : kind === "view" ? "local" : "remote",
source: kind === "view" ? JSON.stringify([inspectionPackage, sourceQuery]) : null,
};
const contract: PublicQueryContract = {
package: inspectionPackage,
query,
definitionDigest: hexDigest(program),
root,
placement: kind === "inputs" ? "replica" : kind === "view" ? "local" : "server",
...(kind === "view" ? { sourceQuery } : {}),
input,
output,
};
queries.push(program);
contracts.push(contract);
template[templateKey] = contract;
}
}
if (template.members.length) templates.push(template);
}
return { queries, contracts, templates };
}
+304
View File
@@ -0,0 +1,304 @@
import { compileInspectionTemplates, type InspectionTemplate } from "./inspection.js";
import path from "node:path";
import { loadCheckedArtifactWorld } from "./receipts.js";
import { contentArtifactUrl, loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
import {
publicOperationContract,
publicQueryContract,
type PublicOperationContract,
type PublicQueryContract,
} from "./client-contracts.js";
import { resultInterfaces } from "./projections.js";
/** Activation is assigned by graph authority, never by a package artifact. */
export interface ActiveExecutionIdentity {
readonly workspace: string;
readonly world: string;
readonly plan: string;
readonly epoch: string;
}
/** Host-owned execution selection. The lifecycle manager fills its checked inputs. */
export interface ExecutionGroupIdentity {
readonly logicalId: string;
readonly executionDigest: string;
}
export interface ClientContractCatalog {
readonly inspections: readonly InspectionTemplate[];
readonly resultInterfaces: Readonly<Record<string, readonly string[]>>;
readonly operations: readonly PublicOperationContract[];
readonly queries: readonly PublicQueryContract[];
readonly components: readonly InstalledComponent[];
readonly presentations: readonly { id: string; subject: string; packageRevision: string; component: string }[];
readonly moduleBindings: readonly {
atom: string;
interface: string;
member: string;
packageRevision: string;
id: string;
}[];
}
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
export function clientContractCatalog(
plan: ExecutionPlan,
installation: ExecutionInstallation,
requestedPackages: readonly string[],
): ClientContractCatalog {
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError(
"CATALOG_PLAN_MISMATCH",
plan.digest,
"Installation and public catalog must use the same checked plan",
);
const packages = requestedPackages.map((revision) => {
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === revision);
if (!pkg)
throw new ExecutionPlanError("UNINSTALLED_PACKAGE", revision, "Requested catalog package is not installed");
return pkg;
});
// Interface metadata contains no authority or private implementation configuration.
// A later admission optimization can narrow this set without changing operation identities.
const operations = plan.definition.workspace.interfaceImports
.filter((i) => !i.template)
.flatMap((iface) =>
iface.members.flatMap((member) =>
member.operations
.filter((o) => o.mode === "call")
.map((operation) => publicOperationContract(iface, operation)),
),
);
const inspection = compileInspectionTemplates(plan);
return {
inspections: inspection.templates,
resultInterfaces: Object.fromEntries(
plan.definition.operations.map((operation) => [operation.key, resultInterfaces(plan, operation.key)]),
),
operations,
queries: [
...inspection.contracts,
...packages.flatMap((pkg) => (pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id))),
],
components: installation.components.filter((c) => requestedPackages.includes(c.packageRevision)),
moduleBindings: plan.definition.workspace.conformances.flatMap((c) =>
(c.moduleBindings ?? [])
.filter((b) => requestedPackages.includes(b.packageRevisionId))
.map((b) => ({
atom: c.atomId,
interface: c.interfaceRevisionId,
member: b.memberId,
packageRevision: b.packageRevisionId,
id: b.moduleId,
})),
),
presentations: (plan.definition.presentations ?? [])
.filter((p) => requestedPackages.includes(p.packageRevisionId))
.map((p) => ({ id: p.id, subject: p.subject, packageRevision: p.packageRevisionId, component: p.componentId })),
};
}
export interface PackageInstallationSelection {
readonly browser?: { readonly id?: string };
readonly service?: { readonly id?: string };
/** Including a module is optional; executing it still requires host trust policy. */
readonly module?: { readonly id?: string };
}
export interface ExecutionInstallation {
readonly codec: "camino-execution-installation-v1";
readonly plan: string;
readonly world: string;
readonly packages: readonly {
readonly revision: string;
readonly root: string;
readonly service?: ArtifactImplementation;
readonly module?: ArtifactImplementation;
readonly browserImplementation?: ArtifactImplementation;
readonly browserURL?: string;
readonly browserRegistry?: string;
}[];
readonly routes: Readonly<Record<string, { readonly path: string; readonly mediaType: string }>>;
readonly coreURL?: string;
readonly engineURL?: string;
readonly components: readonly InstalledComponent[];
}
/** Safe to expose in a presentation catalog: no source paths, state slots, or host configuration. */
export interface InstalledComponent {
readonly role: {
readonly interface: string;
readonly member: string;
readonly subject: string;
readonly contract: string;
readonly typeArguments?: readonly unknown[];
readonly requiredInterfaces?: readonly string[];
};
readonly packageRevision: string;
readonly id: string;
readonly subject: string;
readonly url: string;
readonly exportName: string;
readonly styles: readonly string[];
readonly compatibility: ComponentArtifact["compatibility"];
/** Owned and interpreted by the React artifact consumer. */
readonly preparation?: unknown;
readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>;
}
/** Build-time validation uses the compiler selected by the Nix derivation. */
export async function checkExecutionInstallation(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
generator: string,
selections: Readonly<Record<string, PackageInstallationSelection>> = {},
): Promise<ExecutionInstallation> {
const loaded = await loadCheckedArtifactWorld(plan, roots, generator);
return assembleExecutionInstallation(plan, roots, loaded, selections);
}
/** Load immutable artifacts without regenerating bindings with the host compiler.
* Format, coverage, content and runtime ABI checks still apply. This grants no
* authority to activate a world or run optional modules in process. */
export async function loadExecutionInstallation(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
selections: Readonly<Record<string, PackageInstallationSelection>> = {},
): Promise<ExecutionInstallation> {
return assembleExecutionInstallation(plan, roots, await loadArtifactWorld(plan, roots), selections);
}
function assembleExecutionInstallation(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
loaded: LoadedArtifactWorld,
selections: Readonly<Record<string, PackageInstallationSelection>>,
): ExecutionInstallation {
const fail = (where: string, message: string): never => {
throw new ExecutionPlanError("INSTALLATION_INVALID", where, message);
};
for (const revision of Object.keys(selections))
if (!loaded.manifests.some((manifest) => manifest.packageRevisionId === revision))
fail(revision, "Selection names a package outside this checked world");
const routes: Record<string, { path: string; mediaType: string }> = Object.create(null);
const packages: ExecutionInstallation["packages"][number][] = [];
const components: InstalledComponent[] = [];
const sharedComponentModules = new Map<string, string>();
let coreURL: string | undefined, engineURL: string | undefined;
for (const manifest of loaded.manifests) {
const revision = manifest.packageRevisionId;
const root = roots[revision]!;
const selection = selections[revision] ?? {};
const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports;
const service =
exports.length || selection.service
? selectArtifactImplementation(manifest, {
...selection.service,
target: "server",
compatibility: "quixos-service-v1",
exports: exports.map((entry) => entry.id),
})
: undefined;
for (const component of manifest.components ?? []) {
const sharedModules = Object.fromEntries(
Object.entries(component.sharedModules).map(([name, filePath]) => {
const file = component.files.find((file) => file.path === filePath)!;
const url = contentArtifactUrl(file),
previous = sharedComponentModules.get(name);
if (previous && previous !== url) fail(component.id, `Components disagree on shared module ${name}`);
sharedComponentModules.set(name, url);
return [name, url];
}),
) as InstalledComponent["sharedModules"];
for (const file of component.files) {
const url = contentArtifactUrl(file);
if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types");
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
}
components.push({
...(() => {
const pkg = plan.definition.workspace.packageImports.find((entry) => entry.revisionId === revision);
const artifact = pkg?.buildArtifacts?.["org.quixos.react.preparation/1"] as
| { modules: { module: string }[] }
| undefined;
const preparation = artifact?.modules.find((entry) => entry.module === component.id);
return preparation ? { preparation } : {};
})(),
role: (() => {
const module = plan.definition.packages
.find((p) => p.revision === revision)!
.modules!.find((m) => m.id === component.id)!;
return {
interface: module.interfaceRevisionId,
member: module.memberId,
subject: component.subject,
contract: module.contract,
...(module.typeArguments ? { typeArguments: module.typeArguments } : {}),
...(module.requiredInterfaces ? { requiredInterfaces: module.requiredInterfaces } : {}),
};
})(),
packageRevision: revision,
id: component.id,
subject: component.subject,
compatibility: component.compatibility,
exportName: component.exportName,
styles: component.files.filter((file) => file.mediaType === "text/css").map(contentArtifactUrl),
sharedModules,
url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!),
});
}
const module = selection.module
? selectArtifactImplementation(manifest, {
...selection.module,
target: "server",
compatibility: "quixos-js-module-v1",
exports: exports.map((entry) => entry.id),
})
: undefined;
const browserExports = exports.filter(
(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");
packages.push({ revision, root, ...(service ? { service } : {}), ...(module ? { module } : {}) });
continue;
}
const browser = selectArtifactImplementation(manifest, {
...selection.browser,
target: "target:org.quixos.execution.javascript@1",
compatibility: "quixos-js-module-v1",
exports: browserExports.map((entry) => entry.id),
});
const entry = browser.files.find((file) => file.path === browser.path)!;
const shared = (name: string) => {
const files = browser.files.filter((file) => file.path.endsWith("/" + name));
if (files.length !== 1) fail(revision, "Expected one shared " + name);
return contentArtifactUrl(files[0]!);
};
const core = shared("core.mjs"),
engine = shared("engine.mjs");
if ((coreURL && coreURL !== core) || (engineURL && engineURL !== engine))
fail(revision, "Browser packages disagree on the shared runtime identity");
coreURL = core;
engineURL = engine;
for (const file of browser.files) {
const url = contentArtifactUrl(file);
if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types");
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
}
packages.push({
revision,
root,
...(service ? { service } : {}),
...(module ? { module } : {}),
browserImplementation: browser,
browserURL: contentArtifactUrl(entry),
browserRegistry: browser.registryExport,
});
}
return {
codec: "camino-execution-installation-v1",
plan: plan.digest,
world: loaded.digest,
packages,
components,
routes,
...(coreURL ? { coreURL, engineURL } : {}),
};
}
+371
View File
@@ -0,0 +1,371 @@
import { portableReflectionType, portableReflectionValue } from "./reflection.js";
import type {
ValueType,
InitializationField,
PersistentAttachment,
DependencyBinding,
BoundDependency,
PackageExport,
} from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js";
import { subscriptionContract } from "../capability-model/journals.js";
type PreparedStep = string | null | { case: string; id: string };
function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] {
if (type.kind === "change") return [path];
if (type.kind === "optional") return preparedInputPaths(type.value, path);
if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]);
if (type.kind === "variant")
return Object.entries(type.cases).flatMap(([label, entry]) =>
preparedInputPaths(entry.value, [...path, { case: label, id: entry.id }]),
);
if (type.kind === "record")
return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key]));
return [];
}
/** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */
export function compileKernelProgram(plan: ExecutionPlan): unknown {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch");
const workspace = plan.definition.workspace;
if (workspace.sharedAttachments.some((attachment) => attachment.kind !== "edge"))
throw new ExecutionPlanError(
"UNSUPPORTED_SHARED_STORAGE",
"kernel",
"Shared scalar storage is not implemented; use conformance-owned state",
);
const schema = (type: ValueType): unknown | null => {
try {
return portableValueSchema(type);
} catch {
return null;
}
};
const ownerBySlot = new Map<string, string>();
const edges: { id: string; owner: string | null; a: unknown; b: unknown }[] = [];
const lowerEdge = (storage: Extract<PersistentAttachment, { kind: "edge" }>, owner: string | null) => {
const endpoints = storage.endpoints.map((endpoint) => {
if (endpoint.ordered || (endpoint.keyType && endpoint.keyType !== "string") || endpoint.retainOther)
throw new ExecutionPlanError(
"UNSUPPORTED_GRAPH_FEATURE",
storage.id,
"Ordered relations, non-string keys and automatic retention are not implemented",
);
return {
projection: endpoint.projectionId,
constraint: {
kind: endpoint.constraint.kind,
id:
endpoint.constraint.kind === "atom" ? endpoint.constraint.atomId : endpoint.constraint.interfaceRevisionId,
},
cardinality: endpoint.cardinality,
...(endpoint.keyType ? { keyType: endpoint.keyType } : {}),
...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}),
};
});
edges.push({ id: storage.id, owner, a: endpoints[0], b: endpoints[1] });
};
for (const storage of workspace.sharedAttachments) if (storage.kind === "edge") lowerEdge(storage, null);
const owners = plan.definition.owners.map((owner) => {
const slots: unknown[] = [];
for (const storage of owner.storage as PersistentAttachment[]) {
if (storage.kind === "state") {
const shape = schema(storage.valueType);
if (!shape)
throw new ExecutionPlanError("UNSUPPORTED_STORAGE_CODEC", storage.id, "Unsupported canonical value type");
ownerBySlot.set(storage.id, owner.id);
const pinsFor = (payload: ValueType) => {
const expectations = new Map<string, { kind: "atom" | "interface"; id: string }>();
const walk = (type: ValueType): void => {
if (type.kind === "object-ref") {
const expectation = {
kind: type.expectation.kind,
id: type.expectation.kind === "atom" ? type.expectation.atomId : type.expectation.interfaceRevisionId,
};
expectations.set(JSON.stringify(expectation), expectation);
} else if (type.kind === "list" || type.kind === "optional") walk(type.value);
else if (type.kind === "record") Object.values(type.fields).forEach(walk);
else if (type.kind === "variant") Object.values(type.cases).forEach((entry) => walk(entry.value));
};
walk(payload);
const pins = [...expectations.values()].map((expectation) => {
const id = `journal-pin:${hexDigest([storage.id, expectation])}`;
const projection = `${id}:source`;
edges.push({
id,
owner: owner.id,
a: {
projection,
constraint: { kind: "atom", id: owner.atom },
cardinality: "many",
keyType: "string",
onDelete: "detach",
},
b: { projection: `${id}:target`, constraint: expectation, cardinality: "many", onDelete: "restrict" },
});
return { edge: id, projection };
});
return pins;
};
let journal: unknown, subscription: unknown;
if (storage.storagePolicy.kind === "journal") {
journal = { limits: storage.storagePolicy.limits, pins: pinsFor(storage.valueType) };
}
if (storage.storagePolicy.kind === "subscription") {
const bindings = plan.definition.operations.filter(
(operation) =>
operation.atomId === owner.atom &&
operation.binding.kind === "subscription" &&
operation.binding.slotId === storage.id &&
operation.binding.primitive === "attach",
);
if (bindings.length !== 1)
throw new ExecutionPlanError(
"SUBSCRIPTION_ATTACHMENT_REQUIRED",
storage.id,
"Native subscription storage requires one checked attachment operation",
);
const binding = bindings[0]!;
const operation = workspace.interfaceImports
.find((iface) => iface.revisionId === binding.interfaceRevisionId)!
.members.flatMap((member) => member.operations)
.find((operation) => operation.id === binding.operationId)!;
const contract = subscriptionContract(
storage.storagePolicy.payload,
storage.valueType,
operation,
workspace.interfaceImports,
);
const expectation = (value: typeof contract.source) => ({
kind: value.kind,
id: value.kind === "atom" ? value.atomId : value.interfaceRevisionId,
});
const source = expectation(contract.source),
handler = expectation(contract.handler);
const configuration: ValueType = {
kind: "record",
fields: {
source: { kind: "object-ref", expectation: contract.source },
handler: { kind: "object-ref", expectation: contract.handler },
parameters: storage.valueType,
},
};
const edge = `subscription-active:${hexDigest(storage.id)}`,
projection = `${edge}:subscription`;
edges.push({
id: edge,
owner: owner.id,
a: { projection, constraint: { kind: "atom", id: owner.atom }, cardinality: "many", onDelete: "restrict" },
b: { projection: `${edge}:stream`, constraint: source, cardinality: "many", onDelete: "restrict" },
});
subscription = {
payload: schema(storage.storagePolicy.payload),
source,
handler,
operation: contract.operation,
pins: pinsFor(configuration),
active: { edge, projection },
};
}
slots.push({
id: storage.id,
schema: shape,
policy: storage.storagePolicy.kind === "crdt-document" ? "crdt" : storage.storagePolicy.kind,
...(journal ? { journal } : {}),
...(subscription ? { subscription } : {}),
...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}),
});
} else lowerEdge(storage, owner.id);
}
return {
id: owner.id,
atom: owner.atom,
interface: owner.interface,
slots,
initialize: (owner.initialization as InitializationField[]).map((field) => ({
input: field.input,
binding:
field.binding.kind === "state"
? { kind: "state", slot: field.binding.slotId }
: { kind: "edge", edge: field.binding.edgeTypeId, projection: field.binding.projectionId },
})),
};
});
const slotOwner = (slot: string) => {
const owner = ownerBySlot.get(slot);
if (!owner) throw new ExecutionPlanError("MISSING_STORAGE_OWNER", slot, "No checked owner");
return owner;
};
const via = (binding: DependencyBinding) =>
"via" in binding && binding.via
? { via: { edge: binding.via.edgeTypeId, projection: binding.via.projectionId } }
: {};
const operations = plan.definition.operations.map((operation) => {
const binding = operation.binding;
let target: unknown;
let unsupported: string | null = null;
if (binding.kind === "lifecycle") target = { kind: "lifecycle", primitive: binding.primitive };
else if (binding.kind === "journal" || binding.kind === "subscription")
target = {
kind: binding.kind,
owner: slotOwner(binding.slotId),
slot: binding.slotId,
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 = {
kind: "edge",
edge: binding.edgeTypeId,
projection: binding.projectionId,
primitive: binding.primitive,
};
else {
const pkg = workspace.packageImports.find((p) => p.revisionId === binding.packageRevisionId)!;
const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
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 === "reflection" && requirement.kind === "reflection")
return {
...common,
kind: "reflection",
values: Object.fromEntries(
bound.selectors.map((selector) => [
selector,
{
schema: portableValueSchema(portableReflectionType(selector)),
value: portableReflectionValue(workspace, selector),
},
]),
),
};
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,
...(!bound.receiver &&
!bound.via &&
(requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id))).every(
(id) => iface.members.flatMap((m) => m.operations).find((o) => o.id === id)?.scope === "class",
)
? { staticAtom: operation.atomId }
: {}),
...(bound.receiver ? { receiver: bound.receiver } : {}),
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({
codec: "camino-creation-recipe-v1",
versions: plan.definition.versions,
operation: operation.key,
export: entry.id,
port: dependency.portId,
requirement,
binding: bound,
owners: owners.filter((owner) => owner.atom === bound.atomId),
edges: edges.filter((edge) =>
owners.some((owner) => owner.atom === bound.atomId && owner.id === edge.owner),
),
}),
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,
...(entry.preparation ? { preparation: entry.preparation } : {}),
...(check
? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } }
: {}),
};
}
const inputChanges = preparedInputPaths(operation.input);
const input = schema(operation.input),
output = schema(operation.output);
return {
key: operation.key,
atom: operation.atomId,
interface: operation.interfaceRevisionId,
operation: operation.operationId,
scope: operation.scope,
effect: operation.execution.kind,
targets: operation.availableTargets,
input,
...(inputChanges.length ? { inputChanges } : {}),
output,
unsupported: unsupported ?? (input && output ? null : "UNSUPPORTED_OPERATION_CODEC"),
binding: target,
};
});
const packages = workspace.packageImports.map((pkg) => ({
revision: pkg.revisionId,
exports: pkg.exports.map((entry) => ({
id: entry.id,
...(entry.preparation ? { preparation: entry.preparation } : {}),
...(entry.acceptanceExportId ? { acceptance: entry.acceptanceExportId } : {}),
effect: entry.execution?.kind ?? "remote",
targets: entry.execution && entry.execution.kind !== "remote" ? entry.execution.targets : [],
receiver:
entry.kind === "function"
? null
: entry.kind === "operation" && entry.receiverRequirement.kind === "exact-atom"
? { kind: "atom", id: entry.receiverRequirement.atomId }
: "object",
input: schema(entry.inputType),
output: schema(entry.outputType),
})),
}));
const body = {
codec: "camino-kernel-program-v1",
model: { workspace: workspace.workspaceId, atoms: workspace.atoms.map((atom) => atom.id), owners, edges },
operations,
packages,
};
// The interpreter's plan key covers its real input, not source/build evidence.
return { ...body, plan: hexDigest({ versions: plan.definition.versions, program: body }) };
}
+17
View File
@@ -0,0 +1,17 @@
import type { InterfaceRevision } from "../capability-model/types.js";
/** Public, checked type information for artifact consumers. It describes the
* nominal role, without exposing implementations or synthesizing any storage. */
export function moduleTypeContext(iface: InterfaceRevision): {
typeArguments?: readonly unknown[];
requiredInterfaces?: readonly string[];
} {
return {
...(iface.application?.arguments.length
? {
typeArguments: structuredClone(iface.application.arguments),
}
: {}),
...(iface.requiredInterfaces?.length ? { requiredInterfaces: [...iface.requiredInterfaces].sort() } : {}),
};
}
+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"),
},
];
+561
View File
@@ -0,0 +1,561 @@
import { standardNativeBackends, type NativeExecutionBackend } from "./native-backends.js";
import type {
WorkspaceRevision,
ExecutionContract,
ValueType,
Binding,
DependencyBinding,
EdgeTraversal,
InterfaceRevisionId,
AtomId,
OperationId,
} from "../capability-model/types.js";
import { validateWorkspaceRevision } from "../capability-model/validation.js";
import { canonicalJson, contentDigest } from "../capability-model/evolution.js";
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
import { linkQueries, type LinkedQuery } from "../query/link.js";
import { type BindingSchema } from "../bindings/index.js";
import { unsupportedQueryOperator } from "./query-operators.js";
import { packageBuildContexts } from "./build-contract.js";
export class ExecutionPlanError extends Error {
constructor(
readonly code: string,
readonly path: string,
message: string,
) {
super(`${code} ${path}: ${message}`);
this.name = "ExecutionPlanError";
}
}
export const hexDigest = (value: unknown): string => contentDigest(value).slice("sha256:".length);
const sorted = <T>(values: readonly T[], key: (value: T) => string): T[] =>
[...values].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
export const publicOperationKey = (atom: string, iface: string, operation: string): string =>
JSON.stringify([atom, iface, operation]);
export type DataDependency =
| { kind: "state"; slot: string; via?: EdgeTraversal }
| { kind: "edge"; edge: string; projection: string; via?: EdgeTraversal }
| { kind: "operation"; operation: string; port: string; via?: EdgeTraversal }
| { kind: "query"; query: string; port: string };
export interface PlannedOperation {
key: string;
atomId: string;
interfaceRevisionId: string;
operationId: string;
scope: "object" | "class";
execution: ExecutionContract;
/** Targets available for this complete installed implementation/dependency closure. */
availableTargets: string[];
input: ValueType;
output: ValueType;
/** Concrete target after public forwarding. Never chosen by a runtime caller. */
binding: Exclude<Binding, { kind: "interface" }>;
dependencies: DataDependency[];
}
export interface PlannedQuery {
id: string;
query: LinkedQuery;
/** Query-selected getter entrypoints; dependency traversal supplies canonical inputs. */
operations: string[];
/** Local query roots that admission must support, in addition to selected wire outputs. */
admittedOperations: string[];
}
export interface PlanPackage {
modules?: (NonNullable<WorkspaceRevision["packageImports"][number]["modules"]>[number] & {
contract: string;
subject?: import("../capability-model/types.js").ObjectExpectation;
typeArguments?: readonly unknown[];
requiredInterfaces?: readonly string[];
})[];
controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>;
components?: { id: string; displayName: string; subject: InterfaceRevisionId; runtime: "react" }[];
revision: string;
source: WorkspaceRevision["packageImports"][number]["source"];
sourceDigest: string;
bindingDigest: string;
exports: { id: string; execution: ExecutionContract }[];
}
export interface ExecutionPlanDefinition {
presentations?: NonNullable<WorkspaceRevision["presentations"]>;
codec: "camino-execution-plan-v1";
workspace: WorkspaceRevision;
bindingSchema: BindingSchema;
packages: PlanPackage[];
owners: { id: string; atom: string; interface: string; initialization: unknown; storage: unknown }[];
operations: PlannedOperation[];
queries: PlannedQuery[];
profiles: { slot: string; profile: string }[];
versions: {
value: "camino-value-v1";
reference: "camino-ref-v1";
ports: "quixos-transaction-ports-v1";
crdt: "camino-automerge-3.4.1-structured-v1";
};
}
export interface ExecutionPlan {
definition: ExecutionPlanDefinition;
digest: string;
}
export const executionVersions: ExecutionPlanDefinition["versions"] = {
value: "camino-value-v1",
reference: "camino-ref-v1",
ports: "quixos-transaction-ports-v1",
crdt: "camino-automerge-3.4.1-structured-v1",
};
/** Selection only: the coordinator separately verifies the caller's grant and receiver witness. */
export function resolvePlannedOperation(
plan: ExecutionPlan,
atom: string,
iface: string,
operation: string,
): PlannedOperation {
const key = publicOperationKey(atom, iface, operation);
const selected = plan.definition.operations.find((entry) => entry.key === key);
if (!selected) throw new ExecutionPlanError("UNINSTALLED_OPERATION", key, "No installed public operation binding");
return selected;
}
/** Bound argument/state codec subset. A parsed type alone does not mean a runtime implements it. */
function checkValue(type: ValueType, path: string, allowChange = false): void {
switch (type.kind) {
case "variant":
for (const [label, entry] of Object.entries(type.cases)) checkValue(entry.value, `${path}.${label}`, allowChange);
return;
case "scalar":
case "object-ref":
return;
case "builtin":
if (type.name === "unit") return;
break;
case "optional":
case "list":
checkValue(type.value, path, allowChange);
return;
case "record":
for (const [name, field] of Object.entries(type.fields)) checkValue(field, `${path}.${name}`, allowChange);
return;
case "change":
if (allowChange) {
checkValue(type.value, path);
return;
}
break;
}
throw new ExecutionPlanError("UNSUPPORTED_PORTABLE_CODEC", path, `Portable execution does not support ${type.kind}`);
}
/** Compile declarations into a closed dispatch/dependency graph before artifact selection. */
export function compileExecutionPlan(
input: WorkspaceRevision,
backends: readonly NativeExecutionBackend[] = standardNativeBackends,
): ExecutionPlan {
const workspace = structuredClone(input);
const issues = validateWorkspaceRevision(workspace);
if (issues.length)
throw new ExecutionPlanError(
"INVALID_WORKSPACE",
workspace.id,
issues.map((i) => `${i.path}: ${i.message}`).join("\n"),
);
const interfaces = new Map(workspace.interfaceImports.map((i) => [i.revisionId, i]));
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId, p]));
const attachments = [...workspace.sharedAttachments, ...workspace.conformances.flatMap((c) => c.privateAttachments)];
for (const pkg of workspace.packageImports)
for (const entry of pkg.exports)
if (entry.execution && entry.execution.kind !== "remote") {
checkValue(entry.inputType, `${pkg.revisionId}/${entry.id}/input`, true);
checkValue(entry.outputType, `${pkg.revisionId}/${entry.id}/output`, true);
}
for (const attachment of attachments)
if (attachment.kind === "state" && attachment.storagePolicy.kind === "crdt-document")
checkValue(attachment.valueType, attachment.id);
const operationMap = new Map<string, PlannedOperation>();
const queries = linkQueries(workspace);
const queryKey = (pkg: string, id: string) => JSON.stringify([pkg, id]);
const queryMap = new Map(queries.map((q) => [queryKey(q.packageRevisionId, q.checked.declaration.id), q]));
const declaration = (iface: InterfaceRevisionId, operation: OperationId) =>
interfaces
.get(iface)
?.members.flatMap((m) => m.operations)
.find((o) => o.id === operation);
const queryOperations = (query: LinkedQuery) =>
query.fields.map((f) => publicOperationKey(f.atomId, f.interfaceRevisionId, f.getter));
const targetAtoms = (binding: DependencyBinding, atom: AtomId): AtomId[] => {
if (binding.kind === "interface" && binding.receiver === "input")
return workspace.atoms
.filter((candidate) =>
workspace.conformances.some(
(conformance) =>
conformance.atomId === candidate.id && conformance.interfaceRevisionId === binding.interfaceRevisionId,
),
)
.map((candidate) => candidate.id);
if (!("via" in binding) || !binding.via) return [atom];
const edge = attachments.find((a) => a.id === binding.via!.edgeTypeId);
if (!edge || edge.kind !== "edge") throw new ExecutionPlanError("MISSING_EDGE", atom, "Missing traversal edge");
const endpoint = edge.endpoints.find((e) => e.projectionId !== binding.via!.projectionId)!;
const constraint = endpoint.constraint;
return workspace.atoms
.filter((a) =>
constraint.kind === "atom"
? constraint.atomId === a.id
: workspace.conformances.some(
(c) => c.atomId === a.id && c.interfaceRevisionId === constraint.interfaceRevisionId,
),
)
.map((a) => a.id);
};
const add = (atom: AtomId, iface: InterfaceRevisionId, operation: OperationId): PlannedOperation => {
const key = publicOperationKey(atom, iface, operation),
existing = operationMap.get(key);
if (existing) return existing;
const publicContract = declaration(iface, operation);
if (!publicContract) throw new ExecutionPlanError("MISSING_OPERATION", key, "Missing public contract");
const { binding } = resolveForwardedBinding(workspace, atom, iface, operation);
const planned: PlannedOperation = {
key,
atomId: atom,
interfaceRevisionId: iface,
operationId: operation,
scope: publicContract.scope === "class" ? "class" : "object",
execution: publicContract.execution ?? { kind: "remote" },
availableTargets: [],
input: publicContract.inputType,
output: publicContract.outputType,
binding,
dependencies: [],
};
operationMap.set(key, planned);
const portable = planned.execution.kind !== "remote";
if (portable) {
checkValue(planned.input, `${key}/input`, true);
checkValue(planned.output, `${key}/output`, true);
}
if (binding.kind === "state" || binding.kind === "journal" || binding.kind === "subscription")
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 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) {
const port = entry.dependencyPorts.find((p) => p.id === dependency.portId)!;
const target = dependency.binding;
const via = "via" in target && target.via ? { via: target.via } : {};
if ("via" in target && target.via)
planned.dependencies.push({ kind: "edge", edge: target.via.edgeTypeId, projection: target.via.projectionId });
if (target.kind === "state") planned.dependencies.push({ kind: "state", slot: target.slotId, ...via });
else if (target.kind === "edge")
planned.dependencies.push({ kind: "edge", edge: target.edgeTypeId, projection: target.projectionId, ...via });
else if (target.kind === "interface" && port.requirement.kind === "interface") {
const selected =
port.requirement.operations ??
interfaces.get(target.interfaceRevisionId)!.members.flatMap((m) => m.operations.map((o) => o.id));
for (const targetAtom of targetAtoms(target, atom))
for (const selectedOperation of selected) {
const operation = add(targetAtom, target.interfaceRevisionId, selectedOperation);
planned.dependencies.push({ kind: "operation", operation: operation.key, port: port.id, ...via });
}
} else if (target.kind === "query") {
const query = queryMap.get(queryKey(target.packageRevisionId, target.queryId));
if (!query) throw new ExecutionPlanError("MISSING_QUERY", key, `Unresolved query port ${port.id}`);
planned.dependencies.push({
kind: "query",
query: queryKey(target.packageRevisionId, target.queryId),
port: port.id,
});
}
}
}
planned.dependencies = sorted(planned.dependencies, canonicalJson);
return planned;
};
for (const conformance of workspace.conformances)
for (const entry of conformance.operationBindings)
add(conformance.atomId, conformance.interfaceRevisionId, entry.operationId);
// Check the transitive closure, retaining a dependency path for diagnostics.
for (const root of operationMap.values()) {
if (root.execution.kind === "remote") continue;
const requested = root.execution,
visited = new Set<string>();
const walk = (key: string, path: string[]) => {
if (visited.has(key)) return;
visited.add(key);
const node = operationMap.get(key)!;
if (node.execution.kind === "remote" || (requested.kind === "snapshot" && node.execution.kind !== "snapshot"))
throw new ExecutionPlanError(
"NONPORTABLE_DEPENDENCY",
path.join(" -> "),
"Execution effect is not allowed by the root contract",
);
for (const dependency of node.dependencies) {
if (dependency.kind === "operation")
walk(dependency.operation, [...path, dependency.port, dependency.operation]);
if (dependency.kind === "query") {
// An admitted presentation window is not an authoritative mutation read set.
if (requested.kind === "transaction")
throw new ExecutionPlanError(
"QUERY_IN_TRANSACTION",
[...path, dependency.port].join(" -> "),
"Capture explicit object references as mutation inputs; local presentation queries cannot define transaction membership",
);
const query = queryMap.get(dependency.query)!;
if (query.checked.declaration.placement?.kind !== "local")
throw new ExecutionPlanError(
"REMOTE_QUERY_DEPENDENCY",
path.join(" -> "),
"Portable code requires a local query",
);
for (const operation of queryOperations(query)) walk(operation, [...path, dependency.port, operation]);
}
}
};
walk(root.key, [root.key]);
}
// Additional implementation targets are opportunities, not new public requirements.
// Start with provider support and remove targets until the complete installed
// dependency closure agrees. This also handles recursive call graphs without
// mistaking an unfinished DFS for proof of availability.
for (const node of operationMap.values()) {
if (node.execution.kind === "remote") continue;
const binding = node.binding;
const provider =
binding.kind === "package"
? packages.get(binding.packageRevisionId)!.exports.find((entry) => entry.id === binding.exportId)!.execution
: {
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;
while (targetsChanged) {
targetsChanged = false;
for (const node of operationMap.values()) {
const available = node.availableTargets.filter((target) =>
node.dependencies.every((dependency) => {
if (dependency.kind === "operation")
return operationMap.get(dependency.operation)!.availableTargets.includes(target);
if (dependency.kind === "query") {
const query = queryMap.get(dependency.query)!;
return (
node.execution.kind === "snapshot" &&
query.checked.declaration.placement?.kind === "local" &&
queryOperations(query).every((key) => operationMap.get(key)!.availableTargets.includes(target))
);
}
return true;
}),
);
if (available.length !== node.availableTargets.length) {
node.availableTargets = available;
targetsChanged = true;
}
}
}
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,
operations: queryOperations(query).sort(),
admittedOperations: [],
}));
for (const query of plannedQueries) {
for (const field of query.query.fields) {
const operation = operationMap.get(publicOperationKey(field.atomId, field.interfaceRevisionId, field.getter))!;
const effect = query.query.checked.effects.find(
(effect) => effect.interfaceRevisionId === field.interfaceRevisionId && effect.memberId === field.memberId,
);
if (
operation.execution.kind === "remote" &&
(query.query.checked.declaration.placement?.kind === "replica" || effect?.uses.some((use) => use !== "select"))
)
throw new ExecutionPlanError(
"QUERY_REMOTE_MEMBERSHIP_UNSUPPORTED",
query.id,
"Remote getters may enrich selected values after native snapshot closure; they cannot determine membership, order or canonical-input admission. Expose an accepted canonical value or split the service result into a separate query.",
);
const member = interfaces.get(field.interfaceRevisionId)?.members.find((member) => member.id === field.memberId);
if (member?.kind === "relationship" && operation.binding.kind !== "edge")
throw new ExecutionPlanError(
"QUERY_NON_NATIVE_RELATION",
query.id,
"This query runtime traverses native graph edges. Expose an edge relationship, or return a computed service value through a value getter.",
);
}
const unsupported = unsupportedQueryOperator(query.query.checked);
if (unsupported)
throw new ExecutionPlanError(
"QUERY_OPERATOR_UNSUPPORTED",
query.id,
`This execution world does not support ${unsupported}. Use a bounded portable getter over admitted inputs, or a server service returning the computed value. Query placement will not change automatically.`,
);
const placement = query.query.checked.declaration.placement;
if (placement?.kind !== "local") continue;
const source = plannedQueries.find(
(q) => q.id === queryKey(query.query.packageRevisionId, placement.sourceQueryId),
);
if (!source || source.query.checked.declaration.placement?.kind !== "replica")
throw new ExecutionPlanError("MISSING_REPLICA_SOURCE", query.id, "Local query source must be a replica query");
const localRoot = query.query.checked.declaration.root,
sourceRoot = source.query.checked.declaration.root;
if (localRoot.kind !== sourceRoot.kind || localRoot.interface !== sourceRoot.interface)
throw new ExecutionPlanError(
"REPLICA_ROOT_MISMATCH",
query.id,
"Local and replica queries must use the same root contract",
);
if (localRoot.kind === "workspace-interface") {
const sourceWindows = source.query.checked.selection
.flatMap((root) => root.selection)
.filter((s) => s.name === "objects");
for (const selection of query.query.checked.selection
.flatMap((root) => root.selection)
.filter((s) => s.name === "objects")) {
if (
!sourceWindows.some(
(window) =>
canonicalJson({ arguments: window.arguments, conditions: window.conditions }) ===
canonicalJson({ arguments: selection.arguments, conditions: selection.conditions }),
)
)
throw new ExecutionPlanError(
"REPLICA_WINDOW_MISMATCH",
query.id,
"Local objects must observe the identical confirmed source window; use availableObjects for a local candidate view",
);
}
}
const traversals = (linked: LinkedQuery) => {
const paths = new Map<string, { complete: boolean; requiresComplete: boolean; windows: Set<string> }>();
const entry = (key: string) => {
let value = paths.get(key);
if (!value) {
value = { complete: false, requiresComplete: false, windows: new Set() };
paths.set(key, value);
}
return value;
};
const visit = (selections: LinkedQuery["checked"]["selection"], parents: string[]) => {
for (const selection of selections) {
for (const stage of selection.relational?.stages ?? []) {
if (stage.operation.kind === "source")
entry(JSON.stringify([...parents, stage.operation.memberId])).requiresComplete = true;
}
const member = selection.interfaceRevisionId
? interfaces.get(selection.interfaceRevisionId)?.members.find((m) => m.id === selection.memberId)
: undefined;
const path = member?.kind === "relationship" ? [...parents, member.id] : parents;
if (member?.kind === "relationship") {
const many = member.cardinality === "many" || member.cardinality === "many-unique";
const complete = !selection.predicate && (!many || !!selection.arguments.all);
const key = JSON.stringify(path);
const value = entry(key);
value.complete ||= complete;
if (selection.arguments.first)
value.windows.add(canonicalJson({ arguments: selection.arguments, conditions: selection.conditions }));
else value.requiresComplete = true;
}
visit(selection.selection, path);
}
};
visit(linked.checked.selection, []);
return paths;
};
const admittedTraversals = traversals(source.query);
for (const [traversal, needed] of traversals(query.query)) {
if (!admittedTraversals.has(traversal))
throw new ExecutionPlanError(
"REPLICA_TRAVERSAL_MISSING",
query.id,
`Replica source does not admit relationship path ${traversal}`,
);
if (
!admittedTraversals.get(traversal)!.complete &&
(needed.requiresComplete ||
!needed.windows.size ||
[...needed.windows].some((window) => !admittedTraversals.get(traversal)!.windows.has(window)))
)
throw new ExecutionPlanError(
"REPLICA_RELATION_INCOMPLETE",
query.id,
`Local relationship ${traversal} needs either complete source candidates or the identical confirmed first window; a window cannot supply unseen matches`,
);
}
// The admission executor follows these canonical dependencies on the admitted roots.
// They do not change source candidate membership or make local take/filters authoritative.
source.admittedOperations.push(...query.operations);
for (const operation of query.operations) {
const node = operationMap.get(operation)!;
if (node.execution.kind !== "snapshot" || !node.availableTargets.some((target) => target !== "server"))
throw new ExecutionPlanError(
"NONPORTABLE_QUERY_FIELD",
`${query.id}/${operation}`,
"Local query getter needs an installed local snapshot implementation",
);
}
}
for (const query of plannedQueries) query.admittedOperations = [...new Set(query.admittedOperations)].sort();
const schema: BindingSchema = {
format: "quixos-bindings",
version: 1,
interfaces: sorted(
workspace.interfaceImports.filter((i) => !i.template),
(i) => i.revisionId,
),
interfaceTemplates: sorted(
workspace.interfaceImports.filter((i) => i.template),
(i) => i.revisionId,
),
packages: sorted(workspace.packageImports, (p) => p.revisionId),
};
const normalizedSchema = JSON.parse(canonicalJson(schema)) as BindingSchema;
const buildContexts = packageBuildContexts(normalizedSchema);
const definition: ExecutionPlanDefinition = {
codec: "camino-execution-plan-v1",
workspace,
bindingSchema: normalizedSchema,
...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}),
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => {
const contract = buildContexts[p.revisionId]!.contract;
return {
...contract.definition.package,
source: p.source,
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
bindingDigest: contract.digest,
};
}),
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {
if (!c.id)
throw new ExecutionPlanError(
"OWNER_ID_REQUIRED",
`${c.atomId}/${c.interfaceRevisionId}`,
"Installed state owners require explicit stable identities",
);
return {
id: c.id,
atom: c.atomId,
interface: c.interfaceRevisionId,
initialization: c.initialization ?? [],
storage: c.privateAttachments,
};
}),
operations: sorted([...operationMap.values()], (o) => o.key),
queries: sorted(plannedQueries, (q) => q.id),
profiles: sorted(
attachments.filter((a) => a.kind === "state" && a.storagePolicy.kind === "crdt-document"),
(a) => a.id,
).map((a) => ({ slot: a.id, profile: "camino-automerge-3.4.1-structured-v1" })),
versions: { ...executionVersions },
};
const normalized = JSON.parse(canonicalJson(definition)) as ExecutionPlanDefinition;
return { definition: normalized, digest: hexDigest(normalized) };
}
+84
View File
@@ -0,0 +1,84 @@
/** Process/module ABI shared by generated adapters. Values are decoded against the checked
* operation/port schema by the coordinator, not trusted because this header parsed. */
export const transactionPortAbi = "quixos-transaction-ports-v1";
export type PortAction =
| { kind: "reflection"; selector: string }
| { kind: "state"; primitive: "read" | "write" | "edit" }
| { kind: "edge"; primitive: "resolve" | "disconnect" }
| { kind: "edge"; primitive: "connect"; key: string }
| { kind: "interface"; operationId: string; key?: string }
| { kind: "query" }
| { kind: "creation"; key: string };
export interface PortInvocation {
abi: typeof transactionPortAbi;
/** Coordinator-issued frame and invocation handles, not application object IDs. */
frame: string;
invocation: string;
sequence: number;
portId: string;
action: PortAction;
input: unknown;
}
export interface PackageInvocation {
abi: typeof transactionPortAbi;
workspace: string;
frame: string;
invocation: string;
exportId: string;
effect: "snapshot" | "transaction" | "remote";
/** Capability arguments/receiver use the versioned managed-reference codec. */
input: unknown;
receiver: unknown | null;
}
export type InvocationReply =
| { abi: typeof transactionPortAbi; invocation: string; status: "returned"; output: unknown }
| { abi: typeof transactionPortAbi; invocation: string; status: "failed"; error: { code: string; message: string } };
export type ServiceControl =
| { abi: typeof transactionPortAbi; kind: "ready" }
| { abi: typeof transactionPortAbi; kind: "cancel"; invocation: string }
| { abi: typeof transactionPortAbi; kind: "shutdown" };
/** ABI-only header check. LF-08 resolves handles and enforces the frame/port authority. */
export function checkPortInvocation(value: unknown): PortInvocation {
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
throw new Error("Invalid port invocation");
const fields = value as Record<string, unknown>;
const keys = ["abi", "frame", "invocation", "sequence", "portId", "action", "input"];
if (
Object.keys(fields).length !== keys.length ||
keys.some((key) => !Object.hasOwn(fields, key)) ||
fields.abi !== transactionPortAbi
)
throw new Error("Invalid port invocation fields or ABI");
for (const key of ["frame", "invocation", "portId"])
if (typeof fields[key] !== "string" || !fields[key] || fields[key].length > 4096)
throw new Error(`Invalid port invocation ${key}`);
if (!Number.isSafeInteger(fields.sequence) || (fields.sequence as number) < 1)
throw new Error("Invalid port invocation sequence");
const action = fields.action as Record<string, unknown>;
if (
!action ||
typeof action !== "object" ||
Array.isArray(action) ||
Object.getPrototypeOf(action) !== Object.prototype
)
throw new Error("Invalid port action");
const exact = (names: string[]) =>
Object.keys(action).length === names.length && names.every((name) => Object.hasOwn(action, name));
const identifier = (value: unknown) => typeof value === "string" && value.length > 0 && value.length <= 1024;
const valid =
action.kind === "state"
? exact(["kind", "primitive"]) && ["read", "write", "edit"].includes(action.primitive as string)
: action.kind === "edge"
? (exact(["kind", "primitive"]) && ["resolve", "disconnect"].includes(action.primitive as string)) ||
(exact(["kind", "primitive", "key"]) && action.primitive === "connect" && identifier(action.key))
: action.kind === "interface"
? (exact(["kind", "operationId"]) || (exact(["kind", "operationId", "key"]) && identifier(action.key))) &&
identifier(action.operationId)
: action.kind === "creation"
? exact(["kind", "key"]) && identifier(action.key)
: action.kind === "reflection"
? exact(["kind", "selector"]) && identifier(action.selector)
: action.kind === "query" && exact(["kind"]);
if (!valid) throw new Error("Invalid port action");
return { ...fields, action: { ...action } } as unknown as PortInvocation;
}
+44
View File
@@ -0,0 +1,44 @@
import { ExecutionPlanError, hexDigest, type ExecutionPlan, type PlannedQuery } from "./plan.js";
/** Contracts promised by a direct result. Interface dependencies are explicit; accidental
* overlap among today's implementations does not silently widen an exported interface contract. */
export function resultInterfaces(plan: ExecutionPlan, operationKey: string): string[] {
const operation = plan.definition.operations.find((operation) => operation.key === operationKey);
if (!operation) throw new ExecutionPlanError("UNKNOWN_OPERATION", operationKey, "No installed public operation");
if (operation.output.kind !== "object-ref") return [];
const expected = operation.output.expectation;
if (expected.kind === "atom")
return plan.definition.workspace.conformances
.filter((c) => c.atomId === expected.atomId)
.map((c) => c.interfaceRevisionId)
.sort();
const interfaces = new Map(plan.definition.workspace.interfaceImports.map((i) => [String(i.revisionId), i]));
const result = new Set<string>();
const visit = (id: string) => {
if (result.has(id)) return;
result.add(id);
for (const dependency of interfaces.get(id)?.requiredInterfaces ?? []) visit(dependency);
};
visit(expected.interfaceRevisionId);
return [...result].sort();
}
/** Compile-time observeResult compatibility. Observation does not grant read or mutation authority. */
export function checkResultProjection(plan: ExecutionPlan, operationKey: string, queryId: string): PlannedQuery {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", "projection", "Plan digest mismatch");
const query = plan.definition.queries.find((q) => q.id === queryId);
if (!query) throw new ExecutionPlanError("UNKNOWN_PROJECTION", queryId, "No installed query");
const declaration = query.query.checked.declaration;
if (declaration.placement?.kind !== "replica" || declaration.root.kind !== "object")
throw new ExecutionPlanError(
"INVALID_RESULT_PROJECTION",
queryId,
"Result observation requires a direct-object replica declaration",
);
if (!resultInterfaces(plan, operationKey).includes(declaration.root.interface))
throw new ExecutionPlanError(
"RESULT_PROJECTION_MISMATCH",
operationKey,
`Result does not promise ${declaration.root.interface}`,
);
return query;
}
+104
View File
@@ -0,0 +1,104 @@
import { compileInspectionTemplates } from "./inspection.js";
import { compileKernelProgram } from "./kernel.js";
import { hexDigest, type ExecutionPlan } from "./plan.js";
import { clientContractCatalog, type ExecutionInstallation } from "./installation.js";
import { publicOperationContract, publicQueryContract } from "./client-contracts.js";
/** Explicit public projection. Never serialize conformance storage or package ports. */
export function publicDefinitionSnapshot(plan: ExecutionPlan, installation: ExecutionInstallation) {
const workspace = plan.definition.workspace;
const kernel = compileKernelProgram(plan) as {
operations: {
atom: string;
interface: string;
operation: string;
unsupported: string | null;
effect: string;
targets: string[];
}[];
};
const catalog = clientContractCatalog(
plan,
installation,
installation.packages.map((pkg) => pkg.revision),
);
const rows = {
atoms: workspace.atoms.map((atom) => ({ id: atom.id, name: atom.displayName, documentation: atom.documentation })),
interfaces: workspace.interfaceImports.map((iface) => ({
id: iface.revisionId,
interface: iface.interfaceId,
name: iface.displayName,
source: iface.source,
template: iface.template,
application: iface.application,
requiredInterfaces: iface.requiredInterfaces ?? [],
argumentRequirements: iface.argumentRequirements ?? [],
members: iface.members.map((member) => ({
...member,
operations: member.operations.map((operation) => ({
...operation,
...(operation.mode === "call" && !iface.template
? { descriptor: publicOperationContract(iface, operation) }
: {}),
})),
})),
})),
conformances: workspace.conformances.map((c) => ({
id: JSON.stringify([c.atomId, c.interfaceRevisionId]),
atom: c.atomId,
interface: c.interfaceRevisionId,
semanticMajor: c.semanticMajor ?? 0,
operations: (
workspace.interfaceImports.find((iface) => iface.revisionId === c.interfaceRevisionId)?.members ?? []
).flatMap((member) =>
member.operations.map((operation) => {
const installed = kernel.operations.find(
(op) => op.atom === c.atomId && op.interface === c.interfaceRevisionId && op.operation === operation.id,
);
return {
operation: operation.id,
available: !!installed && !installed.unsupported,
unavailable: installed?.unsupported ?? (installed ? null : "NOT_CALLABLE"),
execution: installed ? { kind: installed.effect, targets: installed.targets } : null,
};
}),
),
modules: catalog.moduleBindings.filter(
(binding) => binding.atom === c.atomId && binding.interface === c.interfaceRevisionId,
),
})),
queries: [
...compileInspectionTemplates(plan).contracts.map((descriptor) => ({
id: JSON.stringify([descriptor.package, descriptor.query]),
name: "Public inspection",
package: descriptor.package,
descriptor,
system: true,
})),
...workspace.packageImports.flatMap((pkg) =>
(pkg.checkedQueries ?? []).map((query) => ({
id: JSON.stringify([pkg.revisionId, query.declaration.id]),
name: query.declaration.displayName,
package: pkg.revisionId,
descriptor: publicQueryContract(pkg, query.declaration.id),
fragment: query.declaration.fragment,
limits: query.declaration.budgets,
watch: query.declaration.watch,
})),
),
],
presentations: catalog.components.map((component) => ({
id: JSON.stringify([component.packageRevision, component.id]),
component,
registrations: catalog.presentations.filter(
(p) => p.packageRevision === component.packageRevision && p.component === component.id,
),
})),
};
const publicRows = JSON.parse(JSON.stringify(rows)) as Record<
keyof typeof rows,
{ id: string; [key: string]: unknown }[]
>;
for (const entries of Object.values(publicRows)) entries.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
return { codec: "camino-definitions-v1" as const, id: hexDigest(publicRows), rows: publicRows };
}
+110
View File
@@ -0,0 +1,110 @@
import { compileKernelProgram } from "./kernel.js";
import { compileInspectionTemplates } from "./inspection.js";
import { portableValueSchema } from "../bindings/portable.js";
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
import type { ValueType } from "../capability-model/types.js";
import { compileQueryComposition } from "../query/composition.js";
const lowerTypes = (value: unknown): unknown =>
JSON.parse(
JSON.stringify(value, (key, value: unknown) => {
if (
key === "type" &&
value &&
typeof value === "object" &&
"kind" in value &&
["scalar", "builtin", "optional", "list", "record", "object-ref"].includes(String(value.kind))
)
return portableValueSchema(value as ValueType);
return value;
}),
);
/** Checked query IR and installed getter identities, without GraphQL schemas/parsers or SQL adapters.
* Keep authored selection names only for output paths; field access uses resolved interface/member IDs. */
export function compileQueryProgram(plan: ExecutionPlan): unknown {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", "queries", "Plan digest mismatch");
const allQueries = new Map(plan.definition.queries.map(({ id, query }) => [id, query.checked]));
const compositions = plan.definition.workspace.packageImports.flatMap((pkg) =>
(pkg.queryCompositions ?? []).map((composition) => {
const scoped = new Map(allQueries);
for (const query of pkg.checkedQueries ?? []) scoped.set(query.declaration.id, query);
const checked = compileQueryComposition(
{
id: composition.id,
input: composition.input,
steps: composition.steps,
maxQueries: composition.maxQueries,
},
scoped,
);
if (checked.definitionDigest !== composition.definitionDigest)
throw new ExecutionPlanError("INVALID_QUERY_COMPOSITION", composition.id, "Checked dependencies changed");
return {
id: JSON.stringify([pkg.revisionId, composition.id]),
input: portableValueSchema(checked.input),
output: portableValueSchema(checked.output),
maxQueries: checked.maxQueries,
steps: checked.steps.map((step) => ({
...step,
query: (pkg.checkedQueries ?? []).some((query) => query.declaration.id === step.query)
? JSON.stringify([pkg.revisionId, step.query])
: step.query,
})),
};
}),
);
return {
codec: "camino-query-program-v1",
plan: (compileKernelProgram(plan) as { plan: string }).plan,
...(compositions.length ? { compositions } : {}),
queries: [
...compileInspectionTemplates(plan).queries,
...plan.definition.queries.map((entry) => queryProgramEntry(plan, entry)),
],
};
}
export function queryProgramEntry(plan: ExecutionPlan, entry: import("./plan.js").PlannedQuery) {
const { id, query, admittedOperations } = entry;
const interfaces = plan.definition.workspace.interfaceImports;
const checked = query.checked;
return {
id,
...(checked.declaration.fragment && !checked.declaration.fragment.admission
? {
fragment: {
package: query.packageRevisionId,
id: checked.declaration.id,
digest: checked.definitionDigest,
},
}
: {}),
placement: checked.declaration.placement?.kind ?? "remote",
source:
checked.declaration.placement?.kind === "local"
? JSON.stringify([query.packageRevisionId, checked.declaration.placement.sourceQueryId])
: null,
root: checked.declaration.root,
atoms: query.rootAtoms,
variables: portableValueSchema(checked.variables),
defaults: checked.variableDefaults,
selection: lowerTypes(checked.selection),
budgets: checked.declaration.budgets,
fields: query.fields.map((field) => {
const member = interfaces
.find((i) => i.revisionId === field.interfaceRevisionId)!
.members.find((m) => m.id === field.memberId)!;
return {
atom: field.atomId,
interface: field.interfaceRevisionId,
member: field.memberId,
name: member.displayName,
operation: JSON.stringify([field.atomId, field.interfaceRevisionId, field.getter]),
};
}),
admittedOperations,
};
}
+96
View File
@@ -0,0 +1,96 @@
import { compileQuery } from "../query/compile.js";
import { linkQueries } from "../query/link.js";
import { capabilityId } from "../capability-model/types.js";
import { portableValueSchema } from "../bindings/portable.js";
import { hexDigest, type ExecutionPlan, type PlannedQuery } from "./plan.js";
import { compileKernelProgram } from "./kernel.js";
import { queryProgramEntry } from "./queries.js";
import { unsupportedQueryOperator } from "./query-operators.js";
export interface QueryDocument {
readonly interface: string;
readonly document: string;
readonly operation: string;
readonly objectRoot: boolean;
readonly views?: readonly { readonly atom: string; readonly interface: string }[];
}
/** Runtime query documents use the same language, nominal contracts, linking
* and interpreter as packaged queries. They introduce no resources or code. */
export async function compilePublicQuery(plan: ExecutionPlan, request: QueryDocument) {
if (hexDigest(plan.definition) !== plan.digest) throw Error("INVALID_PLAN");
if (
typeof request.document !== "string" ||
Buffer.byteLength(request.document) > 32768 ||
typeof request.operation !== "string" ||
!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(request.operation) ||
typeof request.interface !== "string" ||
request.interface.length > 2048
)
throw Error("QUERY_DOCUMENT_LIMIT");
const id = "public-query:" + hexDigest(request);
const views = request.views ?? [];
if (!Array.isArray(views) || views.length > 16 || new Set(views.map((v) => v.atom)).size !== views.length)
throw Error("QUERY_VIEW_LIMIT");
for (const view of views) {
if (
!view ||
Object.keys(view).sort().join(",") !== "atom,interface" ||
typeof view.atom !== "string" ||
typeof view.interface !== "string" ||
view.atom.length > 2048 ||
view.interface.length > 2048 ||
!plan.definition.workspace.interfaceImports.some((i) => i.revisionId === view.interface)
)
throw Error("QUERY_VIEW_INVALID");
}
const checked = await compileQuery(
{
id,
displayName: request.operation,
root: {
kind: request.objectRoot ? "object" : "workspace-interface",
interface: capabilityId.interfaceRevision(request.interface),
},
document: "request.graphql",
operation: request.operation,
fragments: [],
views: views.map((view) => ({
atomId: capabilityId.atom(view.atom),
interfaceRevisionId: capabilityId.interfaceRevision(view.interface),
})),
allowances: [],
watch: false,
budgets: {
rows: 512,
depth: 8,
resultBytes: 256 * 1024,
candidates: 10000,
rpcCalls: 128,
concurrency: 8,
deadlineMs: 10000,
},
},
plan.definition.workspace.interfaceImports,
async () => request.document,
);
const unsupported = unsupportedQueryOperator(checked);
if (unsupported) throw Error(`QUERY_OPERATOR_UNSUPPORTED: ${unsupported}`);
const linked = linkQueries(plan.definition.workspace, [
{ packageRevisionId: "org.quixos.public-query/1", checked },
]).find((q) => q.checked === checked)!;
const kernel = compileKernelProgram(plan) as {
operations: { key: string; effect: string; unsupported: string | null; binding: { kind: string } }[];
};
for (const field of linked.fields) {
const key = JSON.stringify([field.atomId, field.interfaceRevisionId, field.getter]);
const operation = kernel.operations.find((op) => op.key === key);
const member = plan.definition.workspace.interfaceImports
.find((i) => i.revisionId === field.interfaceRevisionId)!
.members.find((m) => m.id === field.memberId)!;
if (!operation || operation.unsupported || operation.effect !== "snapshot")
throw Error("QUERY_DOCUMENT_REQUIRES_SNAPSHOT: use an installed query for remote-effect enrichment");
if (member.kind === "relationship" && operation.binding.kind !== "edge") throw Error("QUERY_NON_NATIVE_RELATION");
}
const entry: PlannedQuery = { id, query: linked, operations: [], admittedOperations: [] };
return { id, program: queryProgramEntry(plan, entry), output: portableValueSchema(checked.output) };
}
+57
View File
@@ -0,0 +1,57 @@
import type { CheckedQuery, QuerySelection } from "../query/types.js";
import type { QueryPredicate, QueryPathStep } from "../query/relational.js";
/** Supported by the fresh coordinator/replica interpreters. The language parser
* can describe more operators; that does not make them executable in this world. */
export function unsupportedQueryOperator(query: CheckedQuery): string | undefined {
const path = (steps: QueryPathStep[]) =>
steps.some((step) => step.kind !== "relation") ? "expanded source/target paths" : undefined;
const predicate = (value: QueryPredicate): string | undefined => {
if (value.kind === "reduce") return "aggregate predicates";
if (value.kind === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean);
if (value.kind === "not") return predicate(value.child);
if (value.kind === "as") return predicate(value.predicate);
if (value.kind === "compare") return path(value.expression.path);
if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate));
return undefined;
};
const conditional = (selections: readonly QuerySelection[]): boolean =>
selections.some((selection) => selection.conditions.length > 0 || conditional(selection.selection));
const walk = (selections: readonly QuerySelection[]): string | undefined => {
for (const selection of selections) {
if (selection.predicate) {
const reason = predicate(selection.predicate);
if (reason) return reason;
}
if (selection.relational) {
if (query.declaration.placement?.kind === "replica") return "aggregate selections in canonical-input scopes";
const { stages, terminals } = selection.relational;
if (stages.length !== 1 || stages[0]!.operation.kind !== "source")
return "relational expansion, filtering or distinct stages";
if (terminals.length !== 1) return "multiple aggregate terminals";
const terminal = terminals[0]!;
if (
terminal.kind !== "aggregate" ||
terminal.stage !== stages[0]!.id ||
terminal.keys.length ||
terminal.order.length ||
terminal.residual ||
terminal.where ||
terminal.having ||
terminal.first ||
terminal.all ||
terminal.after ||
terminal.reductions.some(
(reduction) => reduction.reduction.operator !== "count" || reduction.reduction.operand,
)
)
return "grouping or reductions other than a direct relationship count";
if (conditional(selection.selection)) return "conditional aggregate projections";
}
const reason = walk(selection.selection);
if (reason) return reason;
}
return undefined;
};
return walk(query.selection);
}
+168
View File
@@ -0,0 +1,168 @@
import { createHash } from "node:crypto";
import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { type ArtifactReader, type PackageArtifacts, type ArtifactGroup } from "./artifacts.js";
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
import { componentAssetDeclarations } from "./client-contracts.js";
import { checkPackageBuildContract, packageBuildContract, type PackageBuildContract } from "./build-contract.js";
export interface GroupReceipt {
schemaVersion: 1;
packageRevisionId: string;
bindingDigest: string;
manifestDigest: string;
generatorDigest: string;
generated: { target: string; path: string; digest: string; bytes: number }[];
}
export interface ArtifactReceipt extends GroupReceipt {
sourceDigest: string;
}
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(
plan: ExecutionPlan,
manifest: PackageArtifacts,
generatorDigest: string,
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId);
if (!pkg || manifest.sourceDigest !== pkg.sourceDigest)
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/source mismatch");
return expectedPackageReceipt(
packageBuildContract(plan.definition.bindingSchema, pkg.revision),
manifest,
pkg.sourceDigest,
generatorDigest,
);
}
export function expectedPackageReceipt(
contract: PackageBuildContract,
manifest: PackageArtifacts,
sourceDigest: string,
generatorDigest: string,
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
if (manifest.sourceDigest !== sourceDigest)
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt source mismatch");
const result = expectedGroupReceipt(contract, manifest, generatorDigest);
return { ...result, receipt: { ...result.receipt, sourceDigest } };
}
export function expectedGroupReceipt(
contract: PackageBuildContract,
manifest: ArtifactGroup,
generatorDigest: string,
): { receipt: GroupReceipt; sources: Map<string, Uint8Array> } {
checkPackageBuildContract(contract);
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
const pkg = contract.definition.package;
const schemaPackage = contract.definition.bindings.packages.find((p) => p.revisionId === pkg.revision)!;
if (
manifest.packageRevisionId !== pkg.revision ||
manifest.bindingDigest !== contract.digest ||
hexDigest(manifest.versions) !== hexDigest(contract.definition.versions)
)
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract mismatch");
// A process service may implement transactions in any language. Only the
// checked JS module path promises generated TypeScript adapter evidence.
const available = new Set(
manifest.implementations
.filter(
(i) =>
i.compatibility === "quixos-js-module-v1" &&
i.exports.some((id) => pkg.exports.some((entry) => entry.id === id && entry.execution.kind !== "remote")),
)
.map((i) => i.target),
);
const targets = [
...new Set(
schemaPackage.exports.flatMap((e) => (e.execution && e.execution.kind !== "remote" ? e.execution.targets : [])),
),
]
.filter((t) => available.has(t))
.sort();
const sources = new Map<string, Uint8Array>();
const generated = targets.map((target) => {
const path = generatedBindingPath(target);
const bytes = new TextEncoder().encode(
generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }),
);
sources.set(path, bytes);
return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
});
if (manifest.components?.length) {
for (const [path, source] of [
["share/quixos/generated/client.ts", contract.definition.component!],
["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()],
] as const) {
const bytes = new TextEncoder().encode(source);
sources.set(path, bytes);
generated.push({ target: "window", path, digest: hashBytes(bytes), bytes: bytes.length });
}
}
// Custom language services need no TypeScript adapter. A checked JS module
// exporting remote handlers does, and must carry the exact generated source.
if (
manifest.implementations.some(
(implementation) =>
implementation.compatibility === "quixos-js-module-v1" &&
implementation.exports.some((id) =>
pkg.exports.some((entry) => entry.id === id && entry.execution.kind === "remote"),
),
)
) {
const path = "share/quixos/generated/remote.ts";
const bytes = new TextEncoder().encode(
generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision),
);
sources.set(path, bytes);
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
}
return {
receipt: {
schemaVersion: 1,
packageRevisionId: pkg.revision,
bindingDigest: contract.digest,
manifestDigest: hexDigest(manifest),
generatorDigest,
generated,
},
sources,
};
}
export async function checkArtifactReceipt(
plan: ExecutionPlan,
manifest: PackageArtifacts,
receipt: unknown,
generatorDigest: string,
read: ArtifactReader,
): Promise<void> {
const expected = expectedArtifactReceipt(plan, manifest, generatorDigest);
if (hexDigest(receipt) !== hexDigest(expected.receipt))
throw new ExecutionPlanError(
"INVALID_RECEIPT",
manifest.packageRevisionId,
"Receipt differs from the trusted compiler/schema/artifact expectation",
);
for (const file of expected.receipt.generated) {
const actual = await read(manifest.packageRevisionId, file.path, file.bytes);
if (actual.length !== file.bytes || hashBytes(actual) !== file.digest)
throw new ExecutionPlanError("INVALID_RECEIPT", file.path, "Generated bindings were replaced or omitted");
}
}
/** Checked Nix builds validate generated sources with their selected compiler.
* Runtime loading uses the stored outputs instead of repeating compilation. */
export async function loadCheckedArtifactWorld(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
generatorDigest: string,
): Promise<LoadedArtifactWorld> {
const loaded = await loadArtifactWorld(plan, roots);
for (const manifest of loaded.manifests) {
const bytes = await loaded.read(manifest.packageRevisionId, "share/quixos/build-receipt.json", 16 * 1024 * 1024);
const receipt: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
await checkArtifactReceipt(plan, manifest, receipt, generatorDigest, loaded.read);
}
return loaded;
}
+28
View File
@@ -0,0 +1,28 @@
import type { ValueType, WorkspaceRevision } from "../capability-model/types.js";
import { ExecutionPlanError } from "./plan.js";
const string: ValueType = { kind: "scalar", name: "string" };
/** Narrow immutable installation metadata. No graph enumeration or live reflection. */
export function portableReflectionType(selector: string): ValueType {
if (selector !== "presentations")
throw new ExecutionPlanError(
"UNSUPPORTED_PORTABLE_REFLECTION",
selector,
"Only checked presentation metadata is portable",
);
return {
kind: "list",
value: { kind: "record", fields: { id: string, subject: string, atoms: { kind: "list", value: string } } },
};
}
export function portableReflectionValue(workspace: WorkspaceRevision, selector: string) {
portableReflectionType(selector);
return (workspace.presentations ?? [])
.map((p) => ({
id: p.id,
subject: p.subject,
atoms: [
...new Set(workspace.conformances.filter((c) => c.interfaceRevisionId === p.subject).map((c) => c.atomId)),
].sort(),
}))
.sort((a, b) => a.id.localeCompare(b.id));
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More