Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9265e7903 | |||
| a05f58f7fe | |||
| 93c8cae1e6 | |||
| 5a611bbc9b | |||
| aadc35581d | |||
| e61b0a36ac | |||
| ae0a5064a4 | |||
| cd13120937 | |||
| 61a410f98f | |||
| e3501772c5 | |||
| 7863ce3e12 | |||
| ea51ca89e7 | |||
| c3fe7c86b8 | |||
| 74f00fe28f | |||
| eaf6203b73 | |||
| 17f6941f64 | |||
| 59735c5e38 | |||
| 499b65b274 | |||
| 52803dda05 | |||
| 9358b5ed0e | |||
| e557a057d8 | |||
| 1afc9feb94 | |||
| 8f28ab1b93 | |||
| 00fc2b9ff1 | |||
| a174faea5c | |||
| 53708ac06f | |||
| a84c985256 | |||
| 6c2f030243 | |||
| 0d5220a6ce | |||
| e753ffe601 | |||
| 332b84f258 | |||
| 4982cb7e9f | |||
| 14b0ef25c3 | |||
| c16271510a | |||
| 8aac091f6c | |||
| 01ca965c7f | |||
| fae4e48f72 | |||
| 6ecf565549 | |||
| 73cdeadc2c | |||
| 16b28f1bc4 | |||
| dcef58d6c5 | |||
| f7fcc7d08b | |||
| 295181244f | |||
| 42af87f8bd | |||
| e9fb42dce4 | |||
| 9867bf4552 | |||
| 483bc68a94 | |||
| 1e25f391e7 | |||
| 4e17693d82 | |||
| ce793cc54f | |||
| fd6fee133d | |||
| f5ced64533 | |||
| eabf721e94 | |||
| 0c46850973 | |||
| c4d42a0ac5 | |||
| fd6160ec67 | |||
| fd039f095b | |||
| f4987093f6 | |||
| 4d52789807 | |||
| 60550bb760 | |||
| eaccec410c | |||
| c33dfc2817 | |||
| 27373d801b | |||
| f852c8f0aa | |||
| 3c2e4a7e85 | |||
| ee4d8867a3 | |||
| ffac727009 | |||
| e6b34535af | |||
| cd35e03b2d | |||
| a9134843bc | |||
| ea153c3cd7 | |||
| ca0381d280 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git",
|
||||
"sourceCommit": "435d832a1e202124e2e5ac7f99a65da9d85962e2",
|
||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
||||
"sourceCommit": "4c88728f144add3457b4273d4b9a04ee53c53be3",
|
||||
"sourcePath": "quixos-protocol",
|
||||
"exportName": "quixos-protocol",
|
||||
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+24
@@ -0,0 +1,24 @@
|
||||
enableScripts: true
|
||||
|
||||
generateDefaultNix: false
|
||||
|
||||
individualNixPackaging: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- current
|
||||
- linux
|
||||
cpu:
|
||||
- current
|
||||
- x64
|
||||
- arm64
|
||||
libc:
|
||||
- current
|
||||
- glibc
|
||||
|
||||
plugins:
|
||||
- checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b
|
||||
path: .yarn/plugins/yarn-plugin-nixify.cjs
|
||||
spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"
|
||||
@@ -1,8 +1,138 @@
|
||||
# Quixos Protocol
|
||||
# Quixos protocol and capability compiler
|
||||
|
||||
Shared protobuf definitions for Camino, camino-orch, package descriptors, and
|
||||
future package runtime RPC.
|
||||
Shared protobuf APIs for Camino, orch, package runtimes/descriptors and values,
|
||||
plus the capability language/compiler. Commands below are for backend development;
|
||||
workspace authors use `qx-workspace check` for all verification.
|
||||
|
||||
This package owns the canonical proto source. Current services still generate
|
||||
local TypeScript bindings from these protos; the generated bindings here are
|
||||
available for later consolidation.
|
||||
## Capability language
|
||||
|
||||
The grammar is `grammar/QuixosCapability.g4`. The parser lowers source into the
|
||||
parser-independent records in `src/capability-model`; validation then produces
|
||||
an immutable checked workspace JSON document. Runtime semantics do not depend
|
||||
on parse-tree shapes or declaration order.
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
quixos-workspace-compile \
|
||||
--root ../quixos-instance/workspaces/todo \
|
||||
--checkout-root /tmp/quixos-resolved-resources
|
||||
```
|
||||
|
||||
`workspace.qx` contains workspace-local atoms, attachments, conformances, and
|
||||
constructor bindings. Each imported interface has an `interface.qx` in its own
|
||||
repository; each imported package similarly has a `package.qx`. Source
|
||||
locations never appear in those declarations. `quixos.lock` supplies their
|
||||
exact Git repositories and commits, and the workspace compiler resolves that
|
||||
dependency graph recursively.
|
||||
|
||||
Inside an interface or package manifest, `import interface Named;` is a true
|
||||
source dependency: the repository lock must pin it, and tooling may use the
|
||||
full contract for generated types. `external atom` and `external interface`
|
||||
declare nominal identities which the final importing workspace must provide.
|
||||
The latter is appropriate for relationship targets and other opaque
|
||||
references; it deliberately does not grant the contract needed for an
|
||||
interface invocation port. This distinction permits mutually referential
|
||||
interface identities without creating a cycle in the Git Merkle graph.
|
||||
|
||||
The compiler validates exact identities, recursive lock/source agreement,
|
||||
external requirement satisfaction, interface operation coverage,
|
||||
attachment ownership, native state/edge providers, package receiver and
|
||||
dependency-port requirements, constructors, and the exact runtime closure.
|
||||
Generics, interface composition, declarative forwarding, automatic
|
||||
relationship materialization, and first-class bundles are intentionally absent
|
||||
from v1.
|
||||
|
||||
Local workspace fragments, source-editing APIs, and generated package contracts
|
||||
are documented in [QX bindings and tooling](../docs/QX_BINDINGS_AND_TOOLING.md).
|
||||
`quixos-resource-compile --schema-out` exports the portable generator input;
|
||||
`quixos-codegen-ts` is the first backend. `quixos-qx` provides parse, lint,
|
||||
format, and nominal-atom scaffold commands.
|
||||
|
||||
## Repository locks
|
||||
|
||||
Every workspace, interface, and package repository carries a `quixos.lock`.
|
||||
A resource lock is not a miniature workspace: it contains only the exact
|
||||
Quixos commit that resource was authored against plus its dependency list.
|
||||
It also pins exact Git sources for interfaces and packages named by that
|
||||
resource's own manifest. Resource Git sources are deliberately only a
|
||||
repository URL and full commit ID; the publisher-owned reachability tag and
|
||||
Nix fetch details are derived from those values.
|
||||
|
||||
A workspace root additionally records its Quixos selection policy and ref.
|
||||
Its `commit` is the exact compatibility baseline shared by the resource graph,
|
||||
not necessarily the runtime candidate. `pinned` requires the ref, baseline,
|
||||
and candidate to be one commit. `track-development` lets Central resolve the
|
||||
named development ref to a newer candidate while preserving the authored
|
||||
baseline. `track-release` is reserved for compatibility-filtered release-line
|
||||
advancement and is not implemented yet.
|
||||
|
||||
```sh
|
||||
quixos-lock-check quixos.lock
|
||||
quixos-resource-compile \
|
||||
--root . --kind package \
|
||||
--repository https://repos.quixos.org/example/package-example.git \
|
||||
--commit 1111111111111111111111111111111111111111 \
|
||||
--checkout-root /tmp/quixos-resource-dependencies
|
||||
qx-workspace resource publish
|
||||
```
|
||||
|
||||
The resource compiler validates the local manifest against the recursively
|
||||
resolved locks without turning the resource into a workspace. The final
|
||||
command runs from a jj checkout. It performs that same recursive compilation,
|
||||
snapshots the current working-copy commit, and pushes the immutable tag
|
||||
`refs/tags/quixos-reachability/<commit>` without advancing an authoring
|
||||
bookmark.
|
||||
|
||||
A workspace root may split its resource pins into same-repository fragments:
|
||||
|
||||
```text
|
||||
quixos-lock version 1 {
|
||||
quixos source {
|
||||
repository "https://repos.quixos.org/quixos/quixos.git";
|
||||
policy track-development;
|
||||
ref "dev/alice/main";
|
||||
commit "1111111111111111111111111111111111111111";
|
||||
}
|
||||
import "locks/web-studio.lock";
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
quixos-lock fragment version 1 {
|
||||
package CanvasRuntime source {
|
||||
repository "https://repos.quixos.org/org-quixos-web-studio/package-canvas-runtime.git";
|
||||
commit "2222222222222222222222222222222222222222";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Import paths are normalized, repository-root-relative paths. Absolute paths,
|
||||
URLs, traversal, empty segments, directories, and symbolic links are rejected.
|
||||
Fragments may import other fragments; cycles fail, shared fragments are loaded
|
||||
once, and duplicate resource bindings fail across the flattened closure.
|
||||
`quixos-lock-check` resolves the complete closure and reports its `sourceFiles`
|
||||
alongside the flattened resources. The root Git commit content-addresses every
|
||||
fragment, so fragments do not become independent repositories or identities.
|
||||
|
||||
## Package descriptors
|
||||
|
||||
Executable package metadata remains protobuf text format:
|
||||
|
||||
```sh
|
||||
quixos-descriptor-check path/to/descriptor.quixos-package.txtpb
|
||||
```
|
||||
|
||||
Descriptors identify exact package revisions and exported runtime symbols.
|
||||
The checked workspace binds interface operations to those exports and supplies
|
||||
their exact dependency ports.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
nix develop -c yarn test
|
||||
```
|
||||
|
||||
The suite covers parsing/diagnostics, recursive repository assembly, external
|
||||
requirement validation, source-order independence, ordinary call operations,
|
||||
semantic validation, exact closure/tree-shaking, private and shared
|
||||
attachments, related-object interface-port injection, and constructors.
|
||||
|
||||
@@ -6,51 +6,158 @@
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
quixos-protocol = pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = "quixos-protocol";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
npmDeps = pkgs.importNpmLock { npmRoot = ./.; };
|
||||
npmConfigHook = pkgs.importNpmLock.npmConfigHook;
|
||||
nativeBuildInputs = [
|
||||
pkgs.importNpmLock.npmConfigHook
|
||||
pkgs.nodejs_24
|
||||
nodejs = pkgs.nodejs_24;
|
||||
quixos-protocol = (pkgs.callPackage ./yarn-project.nix { inherit nodejs; }) {
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
overrideAttrs = old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
|
||||
pkgs.diffutils
|
||||
pkgs.esbuild
|
||||
pkgs.protobuf
|
||||
pkgs.git
|
||||
pkgs.jujutsu
|
||||
pkgs.gnutar
|
||||
pkgs.util-linux
|
||||
];
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
mkdir -p "$TMPDIR/generated-before"
|
||||
cp --recursive src/gen "$TMPDIR/generated-before/proto"
|
||||
cp --recursive src/capability-language/generated "$TMPDIR/generated-before/capability"
|
||||
cp --recursive src/resource-lock/generated "$TMPDIR/generated-before/lock"
|
||||
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
|
||||
npm run build
|
||||
patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin
|
||||
yarn build
|
||||
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/capability" src/capability-language/generated
|
||||
diff --recursive --unified "$TMPDIR/generated-before/lock" src/resource-lock/generated
|
||||
esbuild dist/src/descriptor-check.js \
|
||||
--bundle --platform=node --target=node24 --format=esm \
|
||||
--outfile=quixos-descriptor-check.mjs
|
||||
esbuild dist/src/capability-language/cli.js \
|
||||
--bundle --platform=node --target=node24 --format=esm \
|
||||
--outfile=quixos-capability-compile.mjs
|
||||
esbuild dist/src/capability-language/workspace-cli.js \
|
||||
--bundle --platform=node --target=node24 --format=esm \
|
||||
--outfile=quixos-workspace-compile.mjs
|
||||
esbuild dist/src/capability-language/resource-cli.js \
|
||||
--bundle --platform=node --target=node24 --format=esm \
|
||||
--outfile=quixos-resource-compile.mjs
|
||||
esbuild dist/src/resource-lock/cli.js \
|
||||
--bundle --platform=node --target=node24 --format=esm \
|
||||
--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/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
|
||||
runHook postBuild
|
||||
'';
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
runHook preCheck
|
||||
yarn test
|
||||
runHook postCheck
|
||||
'';
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p "$out"
|
||||
install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix"
|
||||
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
|
||||
--replace-fail '@nixpkgs@' '${pkgs.path}'
|
||||
cp --reflink=auto --recursive grammar "$out/grammar"
|
||||
cp --reflink=auto --recursive proto "$out/proto"
|
||||
cp --reflink=auto --recursive dist "$out/dist"
|
||||
cp package.json "$out/package.json"
|
||||
mkdir -p "$out/bin"
|
||||
for tool in quixos-codegen-ts quixos-qx; do
|
||||
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"
|
||||
done
|
||||
cat > "$out/bin/quixos-descriptor-check" <<EOF
|
||||
#!/bin/sh
|
||||
export PATH="${pkgs.protobuf}/bin:\$PATH"
|
||||
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$out/proto\''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/dist/src/descriptor-check.js" "\$@"
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs" "\$@"
|
||||
EOF
|
||||
chmod +x "$out/bin/quixos-descriptor-check"
|
||||
cat > "$out/bin/quixos-capability-compile" <<EOF
|
||||
#!/bin/sh
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-capability-compile.mjs" "\$@"
|
||||
EOF
|
||||
chmod +x "$out/bin/quixos-capability-compile"
|
||||
cat > "$out/bin/quixos-workspace-compile" <<EOF
|
||||
#!/bin/sh
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs" "\$@"
|
||||
EOF
|
||||
chmod +x "$out/bin/quixos-workspace-compile"
|
||||
cat > "$out/bin/quixos-resource-compile" <<EOF
|
||||
#!/bin/sh
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-resource-compile.mjs" "\$@"
|
||||
EOF
|
||||
chmod +x "$out/bin/quixos-resource-compile"
|
||||
cat > "$out/bin/quixos-lock-check" <<EOF
|
||||
#!/bin/sh
|
||||
exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-lock-check.mjs" "\$@"
|
||||
EOF
|
||||
chmod +x "$out/bin/quixos-lock-check"
|
||||
mkdir -p "$out/libexec/quixos-protocol"
|
||||
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 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
|
||||
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-workspace-compile.mjs "$out/libexec/quixos-protocol/quixos-workspace-compile.mjs"
|
||||
install -m644 quixos-resource-compile.mjs "$out/libexec/quixos-protocol/quixos-resource-compile.mjs"
|
||||
install -m644 quixos-lock-check.mjs "$out/libexec/quixos-protocol/quixos-lock-check.mjs"
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
in {
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.default = quixos-protocol;
|
||||
packages.inspector = (pkgs.callPackage ./yarn-project.nix { inherit nodejs; }) {
|
||||
src = pkgs.lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = pkgs.lib.fileset.unions [
|
||||
./package.json
|
||||
./yarn.lock
|
||||
./.yarnrc.yml
|
||||
./.yarn/plugins
|
||||
./src
|
||||
];
|
||||
};
|
||||
overrideAttrs = old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.esbuild ];
|
||||
doCheck = false;
|
||||
buildPhase = ''
|
||||
esbuild src/capability-language/inspect-cli.ts --bundle --platform=node --target=node24 --format=esm --outfile=inspect.mjs
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p "$out/bin" "$out/lib"
|
||||
cp inspect.mjs "$out/lib/inspect.mjs"
|
||||
printf '#!${pkgs.runtimeShell}\nexec ${nodejs}/bin/node --max-old-space-size=128 %s/lib/inspect.mjs\n' "$out" > "$out/bin/quixos-qx-inspect"
|
||||
chmod +x "$out/bin/quixos-qx-inspect"
|
||||
'';
|
||||
};
|
||||
};
|
||||
checks.default = quixos-protocol;
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.nodejs_24
|
||||
nodejs
|
||||
pkgs.protobuf
|
||||
pkgs.typescript
|
||||
quixos-protocol.yarn-freestanding
|
||||
quixos-protocol
|
||||
];
|
||||
shellHook = ''
|
||||
@@ -64,5 +171,6 @@ EOF
|
||||
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
grammar QuixosCapability;
|
||||
|
||||
document
|
||||
: workspaceDecl EOF
|
||||
| interfaceResourceDecl EOF
|
||||
| packageResourceDecl EOF
|
||||
| fragmentDecl EOF
|
||||
;
|
||||
|
||||
fragmentDecl
|
||||
: FRAGMENT LBRACE workspaceItem* RBRACE
|
||||
;
|
||||
|
||||
sourceImportDecl
|
||||
: IMPORT stringLiteral SEMI
|
||||
;
|
||||
|
||||
workspaceDecl
|
||||
: WORKSPACE identifier ID stringLiteral REVISION stringLiteral COMMIT stringLiteral
|
||||
LBRACE workspaceItem* RBRACE
|
||||
;
|
||||
|
||||
workspaceItem
|
||||
: sourceImportDecl
|
||||
| atomDecl
|
||||
| resourceImportDecl
|
||||
| sharedAttachmentDecl
|
||||
| conformanceDecl
|
||||
| constructorBindingDecl
|
||||
| typeAliasDecl
|
||||
;
|
||||
|
||||
resourceImportDecl
|
||||
: IMPORT INTERFACE identifier SEMI
|
||||
| IMPORT PACKAGE identifier SEMI
|
||||
;
|
||||
|
||||
externalAtomDecl
|
||||
: EXTERNAL ATOM identifier ID stringLiteral SEMI
|
||||
;
|
||||
|
||||
externalInterfaceDecl
|
||||
: EXTERNAL INTERFACE identifier REVISION stringLiteral SEMI
|
||||
;
|
||||
|
||||
resourcePreamble
|
||||
: resourceImportDecl
|
||||
| externalAtomDecl
|
||||
| externalInterfaceDecl
|
||||
| typeAliasDecl
|
||||
;
|
||||
|
||||
atomDecl
|
||||
: ATOM identifier ID stringLiteral (DOC stringLiteral)? SEMI
|
||||
;
|
||||
|
||||
interfaceResourceDecl
|
||||
: resourcePreamble* INTERFACE identifier typeParameters? ID stringLiteral REVISION stringLiteral
|
||||
(REQUIRES interfaceType (COMMA interfaceType)*)?
|
||||
LBRACE interfaceMember* RBRACE
|
||||
;
|
||||
|
||||
typeParameters
|
||||
: LT typeParameter (COMMA typeParameter)* GT
|
||||
;
|
||||
|
||||
typeParameter
|
||||
: VALUE identifier (COLON STORABLE)?
|
||||
| OBJECT identifier (IMPLEMENTS interfaceType (AMP interfaceType)*)?
|
||||
;
|
||||
|
||||
interfaceType
|
||||
: identifier typeArguments?
|
||||
;
|
||||
|
||||
typeArguments
|
||||
: LT typeArgument (COMMA typeArgument)* GT
|
||||
;
|
||||
|
||||
typeArgument
|
||||
: ATOM identifier
|
||||
| INTERFACE interfaceType
|
||||
| OBJECT identifier
|
||||
| valueType
|
||||
;
|
||||
|
||||
typeAliasDecl
|
||||
: TYPE identifier typeParameters? EQUAL valueType SEMI
|
||||
;
|
||||
|
||||
interfaceMember
|
||||
: valueMember
|
||||
| relationshipMember
|
||||
| operationMember
|
||||
;
|
||||
|
||||
operationMember
|
||||
: STATIC? OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
|
||||
LBRACE CALL ID stringLiteral SEMI RBRACE
|
||||
;
|
||||
|
||||
valueMember
|
||||
: queryReadContract? VALUE identifier ID stringLiteral COLON valueType
|
||||
LBRACE valueMemberOperation* RBRACE
|
||||
;
|
||||
|
||||
valueMemberOperation
|
||||
: GET ID stringLiteral SEMI
|
||||
| SET ID stringLiteral SEMI
|
||||
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
|
||||
;
|
||||
|
||||
relationshipMember
|
||||
: queryReadContract? RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED? (KEYED stringLiteral)?
|
||||
LBRACE relationshipOperation* RBRACE
|
||||
;
|
||||
|
||||
queryReadContract
|
||||
: QUERYABLE RPC?
|
||||
;
|
||||
|
||||
relationshipOperation
|
||||
: RESOLVE ID stringLiteral SEMI
|
||||
| CONNECT ID stringLiteral SEMI
|
||||
| DISCONNECT ID stringLiteral SEMI
|
||||
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI
|
||||
;
|
||||
|
||||
targetConstraint
|
||||
: ATOM identifier
|
||||
| INTERFACE identifier typeArguments?
|
||||
| OBJECT identifier
|
||||
;
|
||||
|
||||
packageResourceDecl
|
||||
: resourcePreamble* PACKAGE identifier ID stringLiteral REVISION stringLiteral
|
||||
(SEMANTIC_MAJOR INTEGER)?
|
||||
LBRACE packageExport* RBRACE
|
||||
;
|
||||
|
||||
packageExport
|
||||
: packageOperationExport
|
||||
| packageFunctionExport
|
||||
| packageConstructorExport
|
||||
| packageQuery
|
||||
| packageQuerySpecialization
|
||||
;
|
||||
|
||||
packageQuery
|
||||
: QUERY identifier typeParameters? ID stringLiteral ROOT interfaceType
|
||||
DOCUMENT stringLiteral OPERATION stringLiteral LBRACE queryClause* RBRACE
|
||||
;
|
||||
|
||||
packageQuerySpecialization
|
||||
: QUERY identifier ID stringLiteral SPECIALIZE identifier typeArguments SEMI
|
||||
;
|
||||
|
||||
queryClause
|
||||
: FRAGMENTS stringLiteral SEMI
|
||||
| VIEW OBJECT? identifier AS interfaceType SEMI
|
||||
| MAX identifier INTEGER SEMI
|
||||
| ALLOW interfaceType DOT identifier identifier stringLiteral SEMI
|
||||
| WATCH SEMI
|
||||
| POLL INTEGER stringLiteral SEMI
|
||||
;
|
||||
|
||||
packageOperationExport
|
||||
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
|
||||
MODE operationMode eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI
|
||||
;
|
||||
|
||||
packageFunctionExport
|
||||
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
|
||||
dependencyBlock? SEMI
|
||||
;
|
||||
|
||||
packageConstructorExport
|
||||
: CONSTRUCTOR identifier ID stringLiteral CONSTRUCTS identifier COLON valueType
|
||||
dependencyBlock? SEMI
|
||||
;
|
||||
|
||||
eventClause
|
||||
: EMITS valueType
|
||||
;
|
||||
|
||||
operationMode
|
||||
: CALL
|
||||
| WATCH_START
|
||||
| WATCH_STOP
|
||||
| SUBSCRIBE
|
||||
| UNSUBSCRIBE
|
||||
;
|
||||
|
||||
receiverRequirement
|
||||
: ANY
|
||||
| ATOM identifier
|
||||
| OBJECT identifier
|
||||
| INTERFACES LBRACK (interfaceType (COMMA interfaceType)*)? RBRACK
|
||||
;
|
||||
|
||||
identifierList
|
||||
: identifier (COMMA identifier)*
|
||||
;
|
||||
|
||||
dependencyBlock
|
||||
: REQUIRES LBRACE dependencyPort* RBRACE
|
||||
;
|
||||
|
||||
dependencyPort
|
||||
: STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
||||
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
||||
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? SEMI
|
||||
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
||||
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
|
||||
;
|
||||
|
||||
primitiveList
|
||||
: LBRACK primitive (COMMA primitive)* RBRACK
|
||||
;
|
||||
|
||||
primitive
|
||||
: READ
|
||||
| WRITE
|
||||
| RESOLVE
|
||||
| CONNECT
|
||||
| DISCONNECT
|
||||
| WATCH_START
|
||||
| WATCH_STOP
|
||||
;
|
||||
|
||||
sharedAttachmentDecl
|
||||
: SHARED attachmentDecl
|
||||
;
|
||||
|
||||
attachmentDecl
|
||||
: stateDecl
|
||||
| edgeDecl
|
||||
;
|
||||
|
||||
stateDecl
|
||||
: STATE identifier ID stringLiteral ON identifier COLON valueType
|
||||
POLICY storagePolicy (DEFAULT jsonLiteral)? SEMI
|
||||
;
|
||||
|
||||
storagePolicy
|
||||
: OPTIMISTIC_REGISTER
|
||||
| CRDT LPAREN valueType RPAREN
|
||||
;
|
||||
|
||||
edgeDecl
|
||||
: EDGE identifier ID stringLiteral LBRACE edgeEndpoint edgeEndpoint RBRACE
|
||||
;
|
||||
|
||||
edgeEndpoint
|
||||
: targetConstraint PROJECTION identifier ID stringLiteral cardinality ORDERED?
|
||||
(ON_DELETE stringLiteral)? RETAIN_OTHER? (KEYED stringLiteral)? PUBLIC_TRAVERSAL? SEMI
|
||||
;
|
||||
|
||||
conformanceDecl
|
||||
: CONFORM identifier AS identifier typeArguments? (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)?
|
||||
LBRACE conformanceItem* RBRACE
|
||||
;
|
||||
|
||||
conformanceItem
|
||||
: PRIVATE attachmentDecl
|
||||
| operationBindingDecl
|
||||
| stateFieldBindingDecl
|
||||
| relationshipMaterializationDecl
|
||||
;
|
||||
|
||||
stateFieldBindingDecl
|
||||
: BIND identifier TO STATE identifier SEMI
|
||||
;
|
||||
|
||||
relationshipMaterializationDecl
|
||||
: MATERIALIZE identifier IF ABSENT USING CONSTRUCTOR identifier VIA EDGE identifier DOT identifier SEMI
|
||||
;
|
||||
|
||||
operationBindingDecl
|
||||
: BIND memberOperationRef TO operationProvider (QUERY_REASON stringLiteral)? SEMI
|
||||
;
|
||||
|
||||
memberOperationRef
|
||||
: identifier DOT operationName
|
||||
;
|
||||
|
||||
operationName
|
||||
: identifier
|
||||
| CALL
|
||||
| GET
|
||||
| SET
|
||||
| RESOLVE
|
||||
| CONNECT
|
||||
| DISCONNECT
|
||||
| WATCH_START
|
||||
| WATCH_STOP
|
||||
| SUBSCRIBE
|
||||
| UNSUBSCRIBE
|
||||
;
|
||||
|
||||
operationProvider
|
||||
: STATE identifier DOT statePrimitive
|
||||
| EDGE identifier DOT identifier DOT edgePrimitive
|
||||
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock?
|
||||
;
|
||||
|
||||
statePrimitive
|
||||
: READ
|
||||
| WRITE
|
||||
| WATCH_START
|
||||
| WATCH_STOP
|
||||
;
|
||||
|
||||
edgePrimitive
|
||||
: RESOLVE
|
||||
| CONNECT
|
||||
| DISCONNECT
|
||||
| WATCH_START
|
||||
| WATCH_STOP
|
||||
;
|
||||
|
||||
dependencyBindingBlock
|
||||
: WITH LBRACE dependencyBinding* RBRACE
|
||||
;
|
||||
|
||||
dependencyBinding
|
||||
: identifier TO STATE 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 CONSTRUCTOR identifier SEMI
|
||||
| identifier TO QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||
;
|
||||
|
||||
constructorBindingDecl
|
||||
: CONSTRUCTOR identifier TO identifier DOT identifier dependencyBindingBlock? SEMI
|
||||
;
|
||||
|
||||
valueType
|
||||
: scalarType
|
||||
| UNIT
|
||||
| WATCH_HANDLE
|
||||
| MESSAGE stringLiteral
|
||||
| ATOM_REF LT identifier GT
|
||||
| INTERFACE_REF LT identifier typeArguments? GT
|
||||
| REF LT identifier GT
|
||||
| OPTIONAL LT valueType GT
|
||||
| LIST LT valueType GT
|
||||
| RECORD LBRACE recordField* RBRACE
|
||||
| identifier typeArguments?
|
||||
;
|
||||
|
||||
recordField
|
||||
: identifier COLON valueType SEMI
|
||||
;
|
||||
|
||||
scalarType
|
||||
: BOOL
|
||||
| BYTES
|
||||
| DOUBLE
|
||||
| INT32
|
||||
| INT64
|
||||
| STRING
|
||||
| UINT32
|
||||
| UINT64
|
||||
;
|
||||
|
||||
cardinality
|
||||
: OPTIONAL_ONE
|
||||
| EXACTLY_ONE
|
||||
| MANY
|
||||
| MANY_UNIQUE
|
||||
;
|
||||
|
||||
jsonLiteral
|
||||
: stringLiteral
|
||||
| INTEGER
|
||||
| JSON_NUMBER
|
||||
| TRUE
|
||||
| FALSE
|
||||
| NULL
|
||||
| jsonObject
|
||||
| jsonArray
|
||||
;
|
||||
|
||||
jsonObject
|
||||
: LBRACE (jsonMember (COMMA jsonMember)*)? RBRACE
|
||||
;
|
||||
|
||||
jsonMember
|
||||
: stringLiteral COLON jsonLiteral
|
||||
;
|
||||
|
||||
jsonArray
|
||||
: LBRACK (jsonLiteral (COMMA jsonLiteral)*)? RBRACK
|
||||
;
|
||||
|
||||
identifier
|
||||
: IDENTIFIER
|
||||
| SOURCE
|
||||
| QUERY
|
||||
| SPECIALIZE
|
||||
| ROOT
|
||||
| DOCUMENT
|
||||
| FRAGMENTS
|
||||
| VIEW
|
||||
| MAX
|
||||
| ALLOW
|
||||
| POLL
|
||||
| RPC
|
||||
| QUERYABLE
|
||||
| RESULT_BYTES
|
||||
| RPC_CALLS
|
||||
| DEADLINE_MS
|
||||
;
|
||||
|
||||
stringLiteral
|
||||
: STRING_LITERAL
|
||||
;
|
||||
|
||||
WORKSPACE: 'workspace';
|
||||
RESULT_BYTES: 'result-bytes';
|
||||
RPC_CALLS: 'rpc-calls';
|
||||
DEADLINE_MS: 'deadline-ms';
|
||||
QUERY: 'query';
|
||||
SPECIALIZE: 'specialize';
|
||||
ROOT: 'root';
|
||||
DOCUMENT: 'document';
|
||||
FRAGMENTS: 'fragments';
|
||||
VIEW: 'view';
|
||||
MAX: 'max';
|
||||
ALLOW: 'allow';
|
||||
POLL: 'poll';
|
||||
QUERYABLE: 'queryable';
|
||||
RPC: 'rpc';
|
||||
QUERY_REASON: 'query-reason';
|
||||
TYPE: 'type';
|
||||
OBJECT: 'object';
|
||||
STORABLE: 'storable';
|
||||
IMPLEMENTS: 'implements';
|
||||
REF: 'ref';
|
||||
FRAGMENT: 'fragment';
|
||||
IMPORT: 'import';
|
||||
EXTERNAL: 'external';
|
||||
ATOM: 'atom';
|
||||
INTERFACE: 'interface';
|
||||
INTERFACES: 'interfaces';
|
||||
PACKAGE: 'package';
|
||||
VALUE: 'value';
|
||||
RELATION: 'relation';
|
||||
OPERATION: 'operation';
|
||||
FUNCTION: 'function';
|
||||
CONSTRUCTOR: 'constructor';
|
||||
CONSTRUCTS: 'constructs';
|
||||
INPUT: 'input';
|
||||
CONFORM: 'conform';
|
||||
AS: 'as';
|
||||
BIND: 'bind';
|
||||
STATIC: 'static';
|
||||
TO: 'to';
|
||||
PRIVATE: 'private';
|
||||
SHARED: 'shared';
|
||||
STATE: 'state';
|
||||
EDGE: 'edge';
|
||||
PROJECTION: 'projection';
|
||||
WITH: 'with';
|
||||
USING: 'using';
|
||||
VIA: 'via';
|
||||
MATERIALIZE: 'materialize';
|
||||
IF: 'if';
|
||||
ABSENT: 'absent';
|
||||
ON: 'on';
|
||||
POLICY: 'policy';
|
||||
DEFAULT: 'default';
|
||||
SOURCE: 'source';
|
||||
REPOSITORY: 'repository';
|
||||
COMMIT: 'commit';
|
||||
REVISION: 'revision';
|
||||
SEMANTIC_MAJOR: 'semantic-major';
|
||||
ON_DELETE: 'on-delete';
|
||||
RETAIN_OTHER: 'retain-other';
|
||||
KEYED: 'keyed';
|
||||
PUBLIC_TRAVERSAL: 'public-traversal';
|
||||
ID: 'id';
|
||||
DOC: 'doc';
|
||||
MODE: 'mode';
|
||||
EMITS: 'emits';
|
||||
RECEIVER: 'receiver';
|
||||
REQUIRES: 'requires';
|
||||
ANY: 'any';
|
||||
GET: 'get';
|
||||
SET: 'set';
|
||||
WATCH: 'watch';
|
||||
START: 'start';
|
||||
STOP: 'stop';
|
||||
READ: 'read';
|
||||
WRITE: 'write';
|
||||
RESOLVE: 'resolve';
|
||||
CONNECT: 'connect';
|
||||
DISCONNECT: 'disconnect';
|
||||
CALL: 'call';
|
||||
WATCH_START: 'watch-start';
|
||||
WATCH_STOP: 'watch-stop';
|
||||
SUBSCRIBE: 'subscribe';
|
||||
UNSUBSCRIBE: 'unsubscribe';
|
||||
OPTIMISTIC_REGISTER: 'optimistic-register';
|
||||
CRDT: 'crdt';
|
||||
OPTIONAL_ONE: 'optional-one';
|
||||
EXACTLY_ONE: 'exactly-one';
|
||||
MANY_UNIQUE: 'many-unique';
|
||||
MANY: 'many';
|
||||
ORDERED: 'ordered';
|
||||
UNIT: 'unit';
|
||||
WATCH_HANDLE: 'watch-handle';
|
||||
MESSAGE: 'message';
|
||||
ATOM_REF: 'atom-ref';
|
||||
INTERFACE_REF: 'interface-ref';
|
||||
OPTIONAL: 'optional';
|
||||
LIST: 'list';
|
||||
RECORD: 'record';
|
||||
BOOL: 'bool';
|
||||
BYTES: 'bytes';
|
||||
DOUBLE: 'double';
|
||||
INT32: 'int32';
|
||||
INT64: 'int64';
|
||||
STRING: 'string';
|
||||
UINT32: 'uint32';
|
||||
UINT64: 'uint64';
|
||||
TRUE: 'true';
|
||||
FALSE: 'false';
|
||||
NULL: 'null';
|
||||
|
||||
ARROW: '->';
|
||||
COLON: ':';
|
||||
SEMI: ';';
|
||||
COMMA: ',';
|
||||
DOT: '.';
|
||||
LBRACE: '{';
|
||||
RBRACE: '}';
|
||||
LBRACK: '[';
|
||||
RBRACK: ']';
|
||||
LPAREN: '(';
|
||||
RPAREN: ')';
|
||||
LT: '<';
|
||||
GT: '>';
|
||||
AMP: '&';
|
||||
EQUAL: '=';
|
||||
|
||||
INTEGER: '-'? [0-9]+;
|
||||
JSON_NUMBER: '-'? ('0' | [1-9] [0-9]*) ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?;
|
||||
IDENTIFIER: [A-Za-z_] [A-Za-z0-9_]*;
|
||||
STRING_LITERAL: '"' (ESC | ~["\\\r\n])* '"';
|
||||
fragment ESC: '\\' (["\\/bfnrt] | 'u' HEX HEX HEX HEX);
|
||||
fragment HEX: [0-9a-fA-F];
|
||||
|
||||
LINE_COMMENT: '//' ~[\r\n]* -> channel(HIDDEN);
|
||||
BLOCK_COMMENT: '/*' .*? '*/' -> channel(HIDDEN);
|
||||
WS: [ \t\r\n]+ -> channel(HIDDEN);
|
||||
@@ -0,0 +1,81 @@
|
||||
grammar QuixosLock;
|
||||
|
||||
document
|
||||
: QUIXOS_LOCK FRAGMENT? VERSION INTEGER LBRACE
|
||||
(quixosEntry | importEntry | resourceEntry)* RBRACE EOF
|
||||
;
|
||||
|
||||
quixosEntry
|
||||
: QUIXOS SOURCE quixosSourceBlock
|
||||
;
|
||||
|
||||
resourceEntry
|
||||
: resourceKind identifier SOURCE sourceBlock
|
||||
;
|
||||
|
||||
importEntry
|
||||
: IMPORT stringLiteral SEMI
|
||||
;
|
||||
|
||||
resourceKind
|
||||
: INTERFACE
|
||||
| PACKAGE
|
||||
;
|
||||
|
||||
sourceBlock
|
||||
: LBRACE
|
||||
REPOSITORY stringLiteral SEMI
|
||||
COMMIT stringLiteral SEMI
|
||||
RBRACE
|
||||
;
|
||||
|
||||
quixosSourceBlock
|
||||
: LBRACE
|
||||
REPOSITORY stringLiteral SEMI
|
||||
(POLICY sourcePolicy SEMI REF stringLiteral SEMI)?
|
||||
COMMIT stringLiteral SEMI
|
||||
RBRACE
|
||||
;
|
||||
|
||||
sourcePolicy
|
||||
: PINNED
|
||||
| TRACK_RELEASE
|
||||
| TRACK_DEVELOPMENT
|
||||
;
|
||||
|
||||
identifier
|
||||
: IDENTIFIER
|
||||
;
|
||||
|
||||
stringLiteral
|
||||
: STRING_LITERAL
|
||||
;
|
||||
|
||||
QUIXOS_LOCK: 'quixos-lock';
|
||||
FRAGMENT: 'fragment';
|
||||
VERSION: 'version';
|
||||
QUIXOS: 'quixos';
|
||||
SOURCE: 'source';
|
||||
INTERFACE: 'interface';
|
||||
PACKAGE: 'package';
|
||||
REPOSITORY: 'repository';
|
||||
COMMIT: 'commit';
|
||||
POLICY: 'policy';
|
||||
REF: 'ref';
|
||||
PINNED: 'pinned';
|
||||
TRACK_RELEASE: 'track-release';
|
||||
TRACK_DEVELOPMENT: 'track-development';
|
||||
IMPORT: 'import';
|
||||
|
||||
LBRACE: '{';
|
||||
RBRACE: '}';
|
||||
SEMI: ';';
|
||||
INTEGER: [0-9]+;
|
||||
IDENTIFIER: [A-Za-z_] [A-Za-z0-9_]*;
|
||||
STRING_LITERAL: '"' (ESC | ~["\\\r\n])* '"';
|
||||
fragment ESC: '\\' (["\\/bfnrt] | 'u' HEX HEX HEX HEX);
|
||||
fragment HEX: [0-9a-fA-F];
|
||||
|
||||
LINE_COMMENT: '//' ~[\r\n]* -> skip;
|
||||
BLOCK_COMMENT: '/*' .*? '*/' -> skip;
|
||||
WS: [ \t\r\n]+ -> skip;
|
||||
@@ -0,0 +1,140 @@
|
||||
# All source trees and recursive locks are fetched by Nix at exact retained
|
||||
# revisions. No authoring-directory overlay or externally assembled schema.
|
||||
{
|
||||
repository,
|
||||
commit,
|
||||
kind,
|
||||
generator,
|
||||
contractOnly ? false,
|
||||
system ? builtins.currentSystem,
|
||||
}:
|
||||
let
|
||||
pkgs = import (/. + "@nixpkgs@") { inherit system; };
|
||||
protocol = builtins.storePath generator;
|
||||
sourceKey = source: "${source.kind}:${source.repository}@${source.commit}";
|
||||
fetch =
|
||||
source:
|
||||
builtins.fetchGit {
|
||||
url = source.repository;
|
||||
rev = source.commit;
|
||||
ref = "refs/tags/quixos-reachability/${source.commit}";
|
||||
shallow = true;
|
||||
};
|
||||
load =
|
||||
ancestors: source:
|
||||
if builtins.elem (sourceKey source) ancestors then
|
||||
throw "Source dependency cycle at ${sourceKey source}"
|
||||
else if builtins.length ancestors >= 100 then
|
||||
throw "Source dependency depth exceeds 100"
|
||||
else
|
||||
let
|
||||
directory = fetch source;
|
||||
lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
|
||||
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out"
|
||||
'';
|
||||
lock = builtins.fromJSON (builtins.readFile lockFile);
|
||||
children = map (
|
||||
entry: load (ancestors ++ [ (sourceKey source) ]) (entry.source // { inherit (entry) kind; })
|
||||
) lock.resources;
|
||||
in
|
||||
source // { inherit directory children; };
|
||||
root = load [ ] { inherit kind repository commit; };
|
||||
flatten = node: [ node ] ++ pkgs.lib.concatMap flatten node.children;
|
||||
nodes = builtins.attrValues (
|
||||
builtins.listToAttrs (
|
||||
map (node: {
|
||||
name = sourceKey node;
|
||||
value = node;
|
||||
}) (flatten root)
|
||||
)
|
||||
);
|
||||
snapshots = pkgs.writeText "qx-nix-source-graph.json" (
|
||||
builtins.toJSON {
|
||||
resources = map (node: {
|
||||
inherit (node)
|
||||
kind
|
||||
repository
|
||||
commit
|
||||
directory
|
||||
;
|
||||
}) (builtins.filter (node: node.kind != "workspace") nodes);
|
||||
}
|
||||
);
|
||||
compile =
|
||||
node:
|
||||
pkgs.runCommand "qx-${node.kind}-contract" { } ''
|
||||
mkdir -p "$out"
|
||||
${
|
||||
if node.kind == "workspace" then
|
||||
''
|
||||
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
|
||||
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
|
||||
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} \
|
||||
--graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" > "$out/candidate.json"
|
||||
''
|
||||
else
|
||||
''
|
||||
${protocol}/bin/quixos-resource-compile --root ${node.directory} \
|
||||
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
|
||||
--commit ${pkgs.lib.escapeShellArg node.commit} \
|
||||
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} --snapshot-only true \
|
||||
--graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json"
|
||||
''
|
||||
}
|
||||
'';
|
||||
contract = compile root;
|
||||
checkPackage =
|
||||
node:
|
||||
let
|
||||
compiled = compile node;
|
||||
candidate = builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
|
||||
package = builtins.getFlake (
|
||||
"git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}"
|
||||
);
|
||||
checked =
|
||||
package.quixosPackages.${system}.checkedServer
|
||||
or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
|
||||
artifact = checked {
|
||||
schema =
|
||||
if kind == "workspace" then
|
||||
pkgs.writeText "package-specialization-schema.json" (
|
||||
builtins.toJSON
|
||||
(builtins.fromJSON (builtins.readFile "${contract}/package-schemas.json"))
|
||||
.${candidate.revision.revisionId}
|
||||
)
|
||||
else
|
||||
"${compiled}/bindings.json";
|
||||
generator = protocol;
|
||||
packageRevisionId = candidate.revision.revisionId;
|
||||
};
|
||||
in
|
||||
{
|
||||
packageRevisionId = candidate.revision.revisionId;
|
||||
artifactPath = artifact;
|
||||
};
|
||||
checks =
|
||||
if contractOnly then
|
||||
[ ]
|
||||
else
|
||||
map checkPackage (builtins.filter (node: node.kind == "package") nodes);
|
||||
manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks);
|
||||
bindingOptions = pkgs.writeText "qx-typescript-options.json" (
|
||||
builtins.toJSON (
|
||||
(builtins.fromJSON (builtins.readFile "${root.directory}/quixos.check.json")).options or { }
|
||||
)
|
||||
);
|
||||
in
|
||||
pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } ''
|
||||
mkdir -p "$out"
|
||||
cp ${contract}/candidate.json "$out/candidate.json"
|
||||
cp ${contract}/graph.json "$out/graph.json"
|
||||
cp ${manifest} "$out/checks.json"
|
||||
${pkgs.lib.optionalString (
|
||||
kind != "workspace"
|
||||
) ''cp ${contract}/bindings.json "$out/bindings.json"''}
|
||||
${pkgs.lib.optionalString (kind == "package") ''
|
||||
${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \
|
||||
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
|
||||
"$out/bindings.ts" ${bindingOptions}
|
||||
''}
|
||||
''
|
||||
@@ -0,0 +1,35 @@
|
||||
# One derivation path for provisional checking and activation. The source and
|
||||
# schema come from exact committed inputs resolved by the Quixos compiler.
|
||||
{
|
||||
source,
|
||||
schema,
|
||||
generator,
|
||||
packageRevisionId,
|
||||
system ? builtins.currentSystem,
|
||||
}:
|
||||
let
|
||||
packageSource = builtins.path {
|
||||
path = /. + source;
|
||||
name = "quixos-package-source";
|
||||
filter =
|
||||
path: _:
|
||||
let
|
||||
name = baseNameOf path;
|
||||
in
|
||||
name != ".git" && name != ".jj";
|
||||
};
|
||||
package = builtins.getFlake (
|
||||
"path:" + builtins.unsafeDiscardStringContext (toString packageSource)
|
||||
);
|
||||
checked =
|
||||
package.quixosPackages.${system}.checkedServer
|
||||
or (throw "Package ${packageRevisionId} lacks checkedServer; use the supported package scaffold.");
|
||||
in
|
||||
checked {
|
||||
inherit packageRevisionId;
|
||||
schema = builtins.path {
|
||||
path = /. + schema;
|
||||
name = "candidate-package-bindings.json";
|
||||
};
|
||||
generator = builtins.storePath generator;
|
||||
}
|
||||
Generated
-142
@@ -1,142 +0,0 @@
|
||||
{
|
||||
"name": "@quixos/quixos-protocol",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@quixos/quixos-protocol",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.12.1",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.1"
|
||||
},
|
||||
"bin": {
|
||||
"quixos-descriptor-check": "dist/src/descriptor-check.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.1.tgz",
|
||||
"integrity": "sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bufbuild/protoc-gen-es": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.12.1.tgz",
|
||||
"integrity": "sha512-SWa7XvRYRouMo+vBQmpNFZ+ZEqQ8AC0LpL4QWAo1gvstLhFh/Y7Nf/a+MK7ZxDq5LZSThwfk974L1sFxO3OaGw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.1",
|
||||
"@bufbuild/protoplugin": "2.12.1"
|
||||
},
|
||||
"bin": {
|
||||
"protoc-gen-es": "bin/protoc-gen-es"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "2.12.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@bufbuild/protobuf": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.12.1.tgz",
|
||||
"integrity": "sha512-PY58KxQVAD1BnnKtStOctsMoegEVGfBnY5AOqVQOIu711nA13oYtTqJM8df5lUQg2J1DR3XxUXptE+fWX5oLdA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.1",
|
||||
"@typescript/vfs": "^1.6.2",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin/node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/vfs": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz",
|
||||
"integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-6
@@ -2,24 +2,42 @@
|
||||
"name": "@quixos/quixos-protocol",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.18.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"quixos-descriptor-check": "dist/src/descriptor-check.js"
|
||||
"quixos-capability-compile": "dist/src/capability-language/cli.js",
|
||||
"quixos-codegen-ts": "dist/src/bindings/cli.js",
|
||||
"quixos-descriptor-check": "dist/src/descriptor-check.js",
|
||||
"quixos-lock-check": "dist/src/resource-lock/cli.js",
|
||||
"quixos-qx": "dist/src/capability-language/tool-cli.js",
|
||||
"quixos-resource-compile": "dist/src/capability-language/resource-cli.js",
|
||||
"quixos-workspace-compile": "dist/src/capability-language/workspace-cli.js"
|
||||
},
|
||||
"exports": {
|
||||
"./bindings": "./dist/src/bindings/index.js",
|
||||
"./capability-model": "./dist/src/capability-model/index.js",
|
||||
"./capability-language": "./dist/src/capability-language/index.js",
|
||||
"./resource-lock": "./dist/src/resource-lock/index.js",
|
||||
"./gen/*": "./dist/src/gen/*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run generate && tsc -p tsconfig.build.json",
|
||||
"generate": "rm -rf src/gen && mkdir -p src/gen && protoc --plugin=protoc-gen-es=./node_modules/.bin/protoc-gen-es --es_out=src/gen --es_opt=target=ts,import_extension=js --proto_path=proto --proto_path=$QUIXOS_PROTO_PATH proto/quixos/refs.proto proto/quixos/package.proto proto/quixos/orch.proto proto/quixos/runtime.proto proto/camino/schema.proto proto/camino/api.proto proto/camino/options.proto",
|
||||
"typecheck": "npm run generate && tsc --noEmit"
|
||||
"build": "yarn generate && tsc -p tsconfig.build.json",
|
||||
"generate": "yarn generate:proto && yarn generate:parser",
|
||||
"generate:proto": "rm -rf src/gen && mkdir -p src/gen && protoc --experimental_allow_proto3_optional --plugin=protoc-gen-es=./node_modules/.bin/protoc-gen-es --es_out=src/gen --es_opt=target=ts,import_extension=js --proto_path=proto --proto_path=$QUIXOS_PROTO_PATH proto/quixos/refs.proto proto/quixos/package.proto proto/quixos/orch.proto proto/quixos/runtime.proto proto/camino/schema.proto proto/camino/api.proto",
|
||||
"generate:parser": "rm -rf src/capability-language/generated src/resource-lock/generated && mkdir -p src/capability-language/generated src/resource-lock/generated && antlr-ng -D language=TypeScript --generate-visitor --generate-listener false --output-directory src/capability-language/generated grammar/QuixosCapability.g4 && antlr-ng -D language=TypeScript --generate-visitor --generate-listener false --output-directory src/resource-lock/generated grammar/QuixosLock.g4",
|
||||
"test": "yarn generate && tsc -p tsconfig.test.json && node --test dist/test/*.test.js",
|
||||
"typecheck": "yarn generate && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@bufbuild/protobuf": "^2.12.1",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.1"
|
||||
"@bufbuild/protoc-gen-es": "^2.12.1",
|
||||
"antlr4ng": "^3.0.16",
|
||||
"graphql": "16.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24",
|
||||
"typescript": "^5.9.3"
|
||||
"antlr-ng": "^1.0.10",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
+212
-92
@@ -3,73 +3,56 @@ syntax = "proto3";
|
||||
package camino;
|
||||
|
||||
import "camino/schema.proto";
|
||||
import "quixos/refs.proto";
|
||||
|
||||
service CaminoService {
|
||||
rpc RegisterSchema(RegisterSchemaRequest) returns (RegisterSchemaResponse);
|
||||
rpc ListClasses(ListClassesRequest) returns (ListClassesResponse);
|
||||
rpc ExecuteQuery(QueryRequest) returns (QueryResponse);
|
||||
rpc QueryChanges(QueryChangesRequest) returns (QueryChangesResponse);
|
||||
rpc InstallPersistencePlan(InstallPersistencePlanRequest) returns (InstallPersistencePlanResponse);
|
||||
rpc GetPersistencePlan(GetPersistencePlanRequest) returns (GetPersistencePlanResponse);
|
||||
rpc CreateObject(CreateObjectRequest) returns (CreateObjectResponse);
|
||||
rpc GetObject(GetObjectRequest) returns (GetObjectResponse);
|
||||
rpc ListObjects(ListObjectsRequest) returns (ListObjectsResponse);
|
||||
rpc SetField(SetFieldRequest) returns (SetFieldResponse);
|
||||
rpc AddEdge(AddEdgeRequest) returns (AddEdgeResponse);
|
||||
rpc ListEdges(ListEdgesRequest) returns (ListEdgesResponse);
|
||||
rpc RemoveEdge(RemoveEdgeRequest) returns (RemoveEdgeResponse);
|
||||
rpc ReadState(ReadStateRequest) returns (ReadStateResponse);
|
||||
rpc WriteState(WriteStateRequest) returns (WriteStateResponse);
|
||||
rpc ConnectEdge(ConnectEdgeRequest) returns (ConnectEdgeResponse);
|
||||
rpc ResolveEdge(ResolveEdgeRequest) returns (ResolveEdgeResponse);
|
||||
rpc DisconnectEdge(DisconnectEdgeRequest) returns (DisconnectEdgeResponse);
|
||||
rpc ReadCollection(ResolveEdgeRequest) returns (ReadCollectionResponse);
|
||||
rpc ReplaceCollection(ReplaceCollectionRequest) returns (ReadCollectionResponse);
|
||||
rpc ListOps(ListOpsRequest) returns (ListOpsResponse);
|
||||
rpc WatchObject(WatchObjectRequest) returns (stream WatchObjectEvent);
|
||||
}
|
||||
|
||||
message RegisterSchemaRequest {
|
||||
string source_file = 1;
|
||||
}
|
||||
|
||||
message RegisterSchemaResponse {
|
||||
repeated string class_ids = 1;
|
||||
repeated ClassSchema classes = 2;
|
||||
}
|
||||
|
||||
message ListClassesRequest {}
|
||||
|
||||
message RegisteredClass {
|
||||
string id = 1;
|
||||
ClassSchema schema = 2;
|
||||
}
|
||||
|
||||
message ListClassesResponse {
|
||||
repeated RegisteredClass classes = 1;
|
||||
}
|
||||
|
||||
message NullValue {}
|
||||
|
||||
message ObjectValue {
|
||||
map<string, Value> fields = 1;
|
||||
}
|
||||
|
||||
message ListValue {
|
||||
repeated Value values = 1;
|
||||
}
|
||||
|
||||
message RefValue {
|
||||
string object_id = 1;
|
||||
}
|
||||
|
||||
message CrdtValue {
|
||||
string type = 1;
|
||||
string encoding = 2;
|
||||
bytes payload = 3;
|
||||
}
|
||||
|
||||
message FieldValueSource {
|
||||
message StateValueSource {
|
||||
string object_id = 1;
|
||||
string field_name = 2;
|
||||
string field_type = 3;
|
||||
string field_storage = 4;
|
||||
string conflict_strategy = 5;
|
||||
uint64 revision = 6;
|
||||
string slot_id = 2;
|
||||
string value_type_json = 3;
|
||||
string storage_policy_json = 4;
|
||||
uint64 revision = 5;
|
||||
// CRDT-backed state is still read as its materialized Value. The snapshot
|
||||
// travels with the writable source identity so a client can retain and
|
||||
// advance a local replica without exposing the document to package code.
|
||||
CrdtValue crdt_snapshot = 6;
|
||||
}
|
||||
|
||||
message ValueSource {
|
||||
FieldValueSource field = 1;
|
||||
StateValueSource state = 1;
|
||||
}
|
||||
|
||||
message Value {
|
||||
@@ -88,85 +71,112 @@ message Value {
|
||||
ValueSource source = 11;
|
||||
}
|
||||
|
||||
message CreateObjectRequest {
|
||||
string class_id = 1;
|
||||
map<string, Value> fields = 2;
|
||||
message InstallPersistencePlanRequest {
|
||||
PersistencePlan plan = 1;
|
||||
}
|
||||
message InstallPersistencePlanResponse {
|
||||
PersistencePlan plan = 1;
|
||||
}
|
||||
message GetPersistencePlanRequest {}
|
||||
message GetPersistencePlanResponse {
|
||||
PersistencePlan plan = 1;
|
||||
}
|
||||
|
||||
message CreateObjectRequest {
|
||||
string atom_id = 1;
|
||||
map<string, Value> initial_state = 2;
|
||||
}
|
||||
message CreateObjectResponse {
|
||||
CaminoObject object = 1;
|
||||
}
|
||||
|
||||
message GetObjectRequest {
|
||||
string object_id = 1;
|
||||
}
|
||||
|
||||
message GetObjectResponse {
|
||||
CaminoObject object = 1;
|
||||
}
|
||||
|
||||
message ListObjectsRequest {
|
||||
string class_id = 1;
|
||||
string atom_id = 1;
|
||||
}
|
||||
|
||||
message ListObjectsResponse {
|
||||
repeated CaminoObject objects = 1;
|
||||
}
|
||||
|
||||
message SetFieldRequest {
|
||||
message ReadStateRequest {
|
||||
string object_id = 1;
|
||||
string field_name = 2;
|
||||
string slot_id = 2;
|
||||
}
|
||||
message ReadStateResponse {
|
||||
Value value = 1;
|
||||
}
|
||||
message WriteStateRequest {
|
||||
string object_id = 1;
|
||||
string slot_id = 2;
|
||||
Value value = 3;
|
||||
string client_mutation_id = 4;
|
||||
}
|
||||
|
||||
message SetFieldResponse {
|
||||
CaminoObject object = 1;
|
||||
message WriteStateResponse {
|
||||
Value value = 1;
|
||||
CaminoObject object = 2;
|
||||
}
|
||||
|
||||
message AddEdgeRequest {
|
||||
string from_object_id = 1;
|
||||
string source_field = 2;
|
||||
string to_object_id = 3;
|
||||
map<string, Value> fields = 4;
|
||||
optional int32 source_ordinal = 5;
|
||||
message ConnectEdgeRequest {
|
||||
string edge_type_id = 1;
|
||||
string projection_id = 2;
|
||||
string object_id = 3;
|
||||
string target_object_id = 4;
|
||||
optional int32 ordinal = 5;
|
||||
optional int32 target_ordinal = 6;
|
||||
}
|
||||
|
||||
message AddEdgeResponse {
|
||||
message ConnectEdgeResponse {
|
||||
CaminoEdge edge = 1;
|
||||
}
|
||||
|
||||
message ListEdgesRequest {
|
||||
message ResolveEdgeRequest {
|
||||
string object_id = 1;
|
||||
string edge_type_id = 2;
|
||||
string projection_id = 3;
|
||||
}
|
||||
|
||||
message ListEdgesResponse {
|
||||
message ResolveEdgeResponse {
|
||||
repeated CaminoEdge edges = 1;
|
||||
}
|
||||
|
||||
message RemoveEdgeRequest {
|
||||
message DisconnectEdgeRequest {
|
||||
string edge_id = 1;
|
||||
}
|
||||
message DisconnectEdgeResponse {
|
||||
string edge_id = 1;
|
||||
}
|
||||
|
||||
message RemoveEdgeResponse {
|
||||
message CollectionEntry {
|
||||
// Existing entry identity to preserve; empty allocates a new canonical edge.
|
||||
string edge_id = 1;
|
||||
string target_object_id = 2;
|
||||
Value key = 3;
|
||||
}
|
||||
message ReadCollectionResponse {
|
||||
uint64 revision = 1;
|
||||
repeated CollectionEntry entries = 2;
|
||||
}
|
||||
message ReplaceCollectionRequest {
|
||||
string object_id = 1;
|
||||
string edge_type_id = 2;
|
||||
string projection_id = 3;
|
||||
uint64 expected_revision = 4;
|
||||
repeated CollectionEntry entries = 5;
|
||||
}
|
||||
|
||||
message ListOpsRequest {
|
||||
string object_id = 1;
|
||||
}
|
||||
|
||||
message ListOpsResponse {
|
||||
repeated CaminoOp ops = 1;
|
||||
}
|
||||
|
||||
message WatchObjectRequest {
|
||||
string object_id = 1;
|
||||
string after_op_id = 2;
|
||||
bool include_snapshot = 3;
|
||||
// Managed runtimes must watch only their injected attachments.
|
||||
repeated string attachment_ids = 4;
|
||||
}
|
||||
|
||||
message WatchObjectEvent {
|
||||
string object_id = 1;
|
||||
CaminoObject snapshot = 2;
|
||||
@@ -175,33 +185,26 @@ message WatchObjectEvent {
|
||||
|
||||
message CaminoObject {
|
||||
string id = 1;
|
||||
string class_id = 2;
|
||||
uint32 schema_version = 3;
|
||||
string atom_id = 2;
|
||||
string workspace_revision_id = 3;
|
||||
string created_at = 4;
|
||||
string updated_at = 5;
|
||||
map<string, Value> fields = 6;
|
||||
map<string, uint64> field_revisions = 7;
|
||||
map<string, string> field_conflicts = 8;
|
||||
map<string, Value> state = 6;
|
||||
map<string, uint64> state_revisions = 7;
|
||||
}
|
||||
|
||||
message CaminoEdge {
|
||||
string id = 1;
|
||||
string edge_type_id = 2;
|
||||
string from_object_id = 3;
|
||||
string to_object_id = 4;
|
||||
// Legacy canonical source-side projection fields. Prefer endpoint fields.
|
||||
string source_field = 5;
|
||||
string cardinality = 6;
|
||||
string inverse = 7;
|
||||
map<string, Value> fields = 8;
|
||||
optional int32 from_ordinal = 9;
|
||||
string created_at = 10;
|
||||
string updated_at = 11;
|
||||
string from_projection = 12;
|
||||
string to_projection = 13;
|
||||
string from_cardinality = 14;
|
||||
string to_cardinality = 15;
|
||||
optional int32 to_ordinal = 16;
|
||||
string first_object_id = 3;
|
||||
string second_object_id = 4;
|
||||
string first_projection_id = 5;
|
||||
string second_projection_id = 6;
|
||||
optional int32 first_ordinal = 7;
|
||||
optional int32 second_ordinal = 8;
|
||||
string created_at = 9;
|
||||
string first_key_json = 10;
|
||||
string second_key_json = 11;
|
||||
}
|
||||
|
||||
message CaminoOp {
|
||||
@@ -210,7 +213,124 @@ message CaminoOp {
|
||||
string op_kind = 3;
|
||||
Value payload = 4;
|
||||
string actor = 5;
|
||||
quixos.FunctionRef function_ref = 6;
|
||||
string created_at = 7;
|
||||
string client_mutation_id = 8;
|
||||
string created_at = 6;
|
||||
string client_mutation_id = 7;
|
||||
}
|
||||
|
||||
message QueryRequest {
|
||||
string query_id = 1;
|
||||
string object_id = 2;
|
||||
map<string, Value> variables = 3;
|
||||
// Typed callers require this exact checked definition. Administrative/raw
|
||||
// callers may omit it to explicitly select the currently installed definition.
|
||||
string expected_definition_digest = 4;
|
||||
}
|
||||
message QueryPathPart {
|
||||
oneof part {
|
||||
string field = 1;
|
||||
uint32 index = 2;
|
||||
}
|
||||
}
|
||||
message QueryPendingField {
|
||||
repeated QueryPathPart path = 1;
|
||||
string object_id = 2;
|
||||
string interface_revision_id = 3;
|
||||
string member_id = 4;
|
||||
string operation_id = 5;
|
||||
string value_type_json = 6;
|
||||
// Internal residual facts are separate from the authored result projection.
|
||||
optional uint32 residual_window = 7;
|
||||
uint32 residual_row = 8;
|
||||
string residual_field = 9;
|
||||
optional uint32 relational_capture = 10;
|
||||
uint32 captured_object = 11;
|
||||
}
|
||||
message QueryStats {
|
||||
uint32 sql_count = 1;
|
||||
double sql_ms = 2;
|
||||
uint32 rpc_count = 3;
|
||||
double rpc_ms = 4;
|
||||
uint32 result_bytes = 5;
|
||||
double preparation_ms = 6;
|
||||
double total_ms = 7;
|
||||
uint32 relational_stages = 8;
|
||||
uint32 captured_candidates = 9;
|
||||
double relational_ms = 10;
|
||||
double residual_ms = 11;
|
||||
}
|
||||
message QueryResponse {
|
||||
Value value = 1;
|
||||
string data_version = 2;
|
||||
string binding_digest = 3;
|
||||
repeated QueryPendingField pending = 4;
|
||||
QueryStats stats = 5;
|
||||
// Redeemable only through the host's private control socket, not an RPC grant.
|
||||
string preparation_token = 6;
|
||||
repeated QueryFieldFailure errors = 7;
|
||||
repeated QueryResidualWindow residual_windows = 8;
|
||||
// Native reads share one database snapshot; package enrichment does not.
|
||||
string consistency = 9; // native-snapshot | mixed
|
||||
repeated QueryRelationalCapture relational_captures = 10;
|
||||
}
|
||||
|
||||
// Private coordinator input, removed before publishing a result. Memberships
|
||||
// and native facts share one snapshot; package reads are sampled afterwards.
|
||||
message QueryCapturedMember {
|
||||
string object_id = 1;
|
||||
string entry_id = 2;
|
||||
Value map_key = 3;
|
||||
}
|
||||
message QueryCapturedMembers {
|
||||
repeated QueryCapturedMember entries = 1;
|
||||
}
|
||||
message QueryCapturedObject {
|
||||
string object_id = 1;
|
||||
map<string, Value> fields = 2;
|
||||
map<string, string> field_types = 3;
|
||||
map<string, QueryCapturedMembers> relationships = 4;
|
||||
}
|
||||
message QueryRelationalCapture {
|
||||
repeated QueryPathPart path = 1;
|
||||
QueryRelationalPlan plan = 2;
|
||||
string root_object_id = 3;
|
||||
repeated QueryCapturedObject objects = 4;
|
||||
string variables_json = 5;
|
||||
uint32 row_limit = 6;
|
||||
uint32 candidate_limit = 7;
|
||||
optional uint32 residual_window = 8;
|
||||
repeated string result_path = 9;
|
||||
}
|
||||
|
||||
message QueryResidualRow {
|
||||
string entry_id = 1;
|
||||
map<string, Value> fields = 2;
|
||||
}
|
||||
message QueryResidualOrder {
|
||||
string field = 1;
|
||||
bool descending = 2;
|
||||
}
|
||||
message QueryResidualWindow {
|
||||
repeated QueryPathPart path = 1;
|
||||
repeated QueryResidualRow rows = 2;
|
||||
string predicate_json = 3;
|
||||
repeated QueryResidualOrder order = 4;
|
||||
uint32 limit = 5;
|
||||
bool bounded_all = 6;
|
||||
repeated QuerySelection selection = 7;
|
||||
map<string, string> field_types = 8;
|
||||
bool relational = 9;
|
||||
repeated string matched_entries = 10;
|
||||
}
|
||||
|
||||
message QueryFieldFailure {
|
||||
repeated QueryPathPart path = 1;
|
||||
string error = 2;
|
||||
}
|
||||
message QueryChangesRequest {
|
||||
string query_id = 1;
|
||||
string object_id = 2;
|
||||
string data_version = 3;
|
||||
}
|
||||
message QueryChangesResponse {
|
||||
bool changed = 1;
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package camino;
|
||||
|
||||
import "google/protobuf/descriptor.proto";
|
||||
import "camino/schema.proto";
|
||||
import "quixos/refs.proto";
|
||||
|
||||
extend google.protobuf.FileOptions {
|
||||
string schema_namespace = 51000;
|
||||
string schema_version = 51001;
|
||||
}
|
||||
|
||||
extend google.protobuf.MessageOptions {
|
||||
ClassOptions class = 51010;
|
||||
repeated MethodOptions method = 51011;
|
||||
repeated MigrationOptions migration = 51012;
|
||||
InterfaceOptions interface = 51013;
|
||||
repeated ImplementsOptions implements = 51014;
|
||||
ConstructorOptions constructor = 51015;
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
ConflictStrategy conflict = 51020;
|
||||
EdgeOptions edge = 51021;
|
||||
FieldStorage field_storage = 51022;
|
||||
FieldOps field_ops = 51023;
|
||||
InterfaceFieldContract interface_field = 51024;
|
||||
bool display_label = 51025;
|
||||
}
|
||||
|
||||
extend google.protobuf.MethodOptions {
|
||||
quixos.FunctionRef impl = 51030;
|
||||
}
|
||||
|
||||
message ClassOptions {
|
||||
uint32 version = 1;
|
||||
SymbolRef id = 2;
|
||||
}
|
||||
|
||||
message InterfaceOptions {
|
||||
uint32 version = 1;
|
||||
SymbolRef id = 2;
|
||||
}
|
||||
|
||||
message ImplementsOptions {
|
||||
SymbolRef interface = 1;
|
||||
repeated TypeBinding type_binding = 2;
|
||||
}
|
||||
|
||||
message ConstructorOptions {
|
||||
quixos.FunctionRef function = 1;
|
||||
}
|
||||
|
||||
message EdgeOptions {
|
||||
SymbolRef id = 1;
|
||||
// Shorthand for other_endpoint.class.
|
||||
SymbolRef target = 2;
|
||||
// Shorthand for this_endpoint.cardinality.
|
||||
Cardinality cardinality = 3;
|
||||
// Shorthand for other_endpoint.projection.
|
||||
string inverse = 4;
|
||||
EdgeEndpointOptions this_endpoint = 5;
|
||||
EdgeEndpointOptions other_endpoint = 6;
|
||||
repeated SymbolRef tag = 7;
|
||||
repeated SymbolRef implements = 8;
|
||||
}
|
||||
|
||||
message EdgeEndpointOptions {
|
||||
SymbolRef class = 1;
|
||||
SymbolRef interface = 2;
|
||||
string projection = 3;
|
||||
Cardinality cardinality = 4;
|
||||
bool indexed = 5;
|
||||
}
|
||||
|
||||
message MethodOptions {
|
||||
string name = 1;
|
||||
quixos.FunctionRef function = 2;
|
||||
}
|
||||
|
||||
message MigrationOptions {
|
||||
uint32 from_version = 1;
|
||||
uint32 to_version = 2;
|
||||
quixos.FunctionRef function = 3;
|
||||
}
|
||||
+280
-176
@@ -2,205 +2,309 @@ syntax = "proto3";
|
||||
|
||||
package camino;
|
||||
|
||||
import "quixos/refs.proto";
|
||||
|
||||
message SymbolRef {
|
||||
string namespace = 1;
|
||||
string name = 2;
|
||||
string version = 3;
|
||||
string hash = 4;
|
||||
}
|
||||
|
||||
enum ConflictStrategy {
|
||||
CONFLICT_STRATEGY_UNSPECIFIED = 0;
|
||||
REPLACE = 1;
|
||||
PRESERVE_CONFLICTS = 2;
|
||||
CRDT = 3;
|
||||
}
|
||||
|
||||
enum Cardinality {
|
||||
CARDINALITY_UNSPECIFIED = 0;
|
||||
OPTIONAL_ONE = 1;
|
||||
EXACTLY_ONE = 2;
|
||||
MANY = 3;
|
||||
MANY_UNIQUE = 4;
|
||||
MANY_ORDERED = 5;
|
||||
MANY_UNIQUE_ORDERED = 6;
|
||||
}
|
||||
|
||||
enum DocRefStrength {
|
||||
DOC_REF_STRENGTH_UNSPECIFIED = 0;
|
||||
SOFT_REF = 1;
|
||||
PROJECTED_EDGE = 2;
|
||||
message AtomDefinition {
|
||||
string atom_id = 1;
|
||||
string display_name = 2;
|
||||
}
|
||||
|
||||
enum FieldStorageKind {
|
||||
FIELD_STORAGE_KIND_UNSPECIFIED = 0;
|
||||
STORED = 1;
|
||||
DERIVED = 2;
|
||||
LAZY = 3;
|
||||
EXTERNAL = 4;
|
||||
STATIC_FINAL = 5;
|
||||
message AtomConformance {
|
||||
string atom_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
string conformance_id = 3;
|
||||
}
|
||||
|
||||
message Ref {
|
||||
string object_id = 1;
|
||||
message StateAttachment {
|
||||
string slot_id = 1;
|
||||
string attached_atom_id = 2;
|
||||
string display_name = 3;
|
||||
string value_type_json = 4;
|
||||
string storage_policy_json = 5;
|
||||
string default_value_json = 6;
|
||||
string owner_conformance_id = 7;
|
||||
}
|
||||
|
||||
message CustomField {}
|
||||
|
||||
message WatchStartResult {
|
||||
string watch_id = 1;
|
||||
message EndpointConstraint {
|
||||
oneof kind {
|
||||
string atom_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message WatchStopRequest {
|
||||
string watch_id = 1;
|
||||
}
|
||||
|
||||
message WatchStopResult {}
|
||||
|
||||
message CrdtText {
|
||||
bytes payload = 1;
|
||||
}
|
||||
|
||||
message CrdtRichText {
|
||||
bytes payload = 1;
|
||||
}
|
||||
|
||||
message CrdtJson {
|
||||
bytes payload = 1;
|
||||
}
|
||||
|
||||
message ClassSchema {
|
||||
SymbolRef id = 1;
|
||||
uint32 version = 2;
|
||||
repeated FieldSchema fields = 3;
|
||||
repeated EdgeSchema edges = 4;
|
||||
repeated MethodSchema methods = 5;
|
||||
repeated MigrationSpec migrations = 6;
|
||||
string source_file = 7;
|
||||
string proto_message = 8;
|
||||
repeated OperationServiceSchema operation_services = 9;
|
||||
repeated InterfaceImplementationSchema implements = 10;
|
||||
ConstructorSpec constructor = 11;
|
||||
}
|
||||
|
||||
message FieldSchema {
|
||||
string name = 1;
|
||||
uint32 tag = 2;
|
||||
TypeRef type = 3;
|
||||
ConflictStrategy conflict = 4;
|
||||
bool repeated = 5;
|
||||
bool optional = 6;
|
||||
FieldStorage storage = 7;
|
||||
FieldOps ops = 8;
|
||||
InterfaceFieldContract interface_contract = 9;
|
||||
bool is_display_label = 10;
|
||||
}
|
||||
|
||||
message FieldStorage {
|
||||
FieldStorageKind kind = 1;
|
||||
quixos.FunctionRef resolver = 2;
|
||||
bool cache = 3;
|
||||
}
|
||||
|
||||
message TypeRef {
|
||||
string proto_type = 1;
|
||||
SymbolRef symbol = 2;
|
||||
}
|
||||
|
||||
message TypeBinding {
|
||||
string name = 1;
|
||||
TypeRef type = 2;
|
||||
}
|
||||
|
||||
message InterfaceFieldContract {
|
||||
bool required = 1;
|
||||
bool readable = 2;
|
||||
bool writable = 3;
|
||||
bool watchable = 4;
|
||||
string type_param = 5;
|
||||
TypeRef type = 6;
|
||||
}
|
||||
|
||||
message InterfaceImplementationSchema {
|
||||
SymbolRef interface = 1;
|
||||
repeated TypeBinding type_bindings = 2;
|
||||
}
|
||||
|
||||
message InterfaceSchema {
|
||||
SymbolRef id = 1;
|
||||
uint32 version = 2;
|
||||
repeated FieldSchema fields = 3;
|
||||
string source_file = 4;
|
||||
string proto_message = 5;
|
||||
}
|
||||
|
||||
enum FieldImplementation {
|
||||
FIELD_IMPLEMENTATION_UNSPECIFIED = 0;
|
||||
SERVICE = 1;
|
||||
}
|
||||
|
||||
message FieldOps {
|
||||
FieldImplementation implementation = 1;
|
||||
string service = 2;
|
||||
}
|
||||
|
||||
message EdgeSchema {
|
||||
SymbolRef id = 1;
|
||||
// Legacy/source-side projection fields. Prefer from_endpoint/to_endpoint.
|
||||
string source_field = 2;
|
||||
SymbolRef from_class = 3;
|
||||
SymbolRef to_class = 4;
|
||||
Cardinality cardinality = 5;
|
||||
string inverse = 6;
|
||||
repeated FieldSchema fields = 7;
|
||||
EdgeEndpointSchema from_endpoint = 8;
|
||||
EdgeEndpointSchema to_endpoint = 9;
|
||||
SymbolRef declaring_class = 10;
|
||||
repeated SymbolRef tags = 11;
|
||||
repeated SymbolRef implements = 12;
|
||||
}
|
||||
|
||||
message EdgeEndpointSchema {
|
||||
SymbolRef class = 1;
|
||||
SymbolRef interface = 2;
|
||||
string projection = 3;
|
||||
message EdgeEndpoint {
|
||||
string projection_id = 1;
|
||||
string display_name = 2;
|
||||
EndpointConstraint constraint = 3;
|
||||
Cardinality cardinality = 4;
|
||||
bool indexed = 5;
|
||||
bool ordered = 5;
|
||||
// Empty means restrict. Direction is the endpoint being deleted.
|
||||
string on_delete = 6;
|
||||
bool retain_other = 7;
|
||||
// Empty for sets/lists, otherwise string, boolean, or int64 map keys.
|
||||
string key_type = 8;
|
||||
// Explicit read-only dependency injection traversal, not mutation authority.
|
||||
bool public_traversal = 9;
|
||||
}
|
||||
|
||||
message MethodSchema {
|
||||
message EdgeAttachment {
|
||||
string edge_type_id = 1;
|
||||
string display_name = 2;
|
||||
EdgeEndpoint first = 3;
|
||||
EdgeEndpoint second = 4;
|
||||
string owner_conformance_id = 5;
|
||||
}
|
||||
|
||||
// Camino consumes this persistence-only projection of a checked workspace.
|
||||
// Interfaces, operation bindings, packages, and constructors stay in orch.
|
||||
message PersistencePlan {
|
||||
string workspace_id = 1;
|
||||
string workspace_revision_id = 2;
|
||||
repeated AtomDefinition atoms = 3;
|
||||
repeated AtomConformance conformances = 4;
|
||||
repeated StateAttachment states = 5;
|
||||
repeated EdgeAttachment edges = 6;
|
||||
repeated InstalledQuery queries = 7;
|
||||
}
|
||||
|
||||
// Immutable checked query IR. Installed only with the checked persistence plan;
|
||||
// query callers select its ID, never send or modify these definitions.
|
||||
message QueryArgument {
|
||||
oneof value {
|
||||
string variable = 1;
|
||||
string literal_json = 2;
|
||||
QueryArgumentList list = 3;
|
||||
QueryArgumentObject object = 4;
|
||||
}
|
||||
}
|
||||
message QueryArgumentList {
|
||||
repeated QueryArgument values = 1;
|
||||
}
|
||||
message QueryArgumentObject {
|
||||
map<string, QueryArgument> fields = 1;
|
||||
}
|
||||
message QueryCondition {
|
||||
bool include = 1;
|
||||
QueryArgument value = 2;
|
||||
}
|
||||
message QuerySelection {
|
||||
string name = 1;
|
||||
quixos.FunctionRef function = 2;
|
||||
string key = 2;
|
||||
string interface_revision_id = 3;
|
||||
string member_id = 4;
|
||||
string target_interface_revision_id = 5;
|
||||
repeated QueryCondition conditions = 6;
|
||||
map<string, QueryArgument> arguments = 7;
|
||||
repeated QuerySelection selection = 8;
|
||||
QueryRelationalPlan relational = 9;
|
||||
QueryPredicate predicate = 10;
|
||||
}
|
||||
|
||||
message MigrationSpec {
|
||||
uint32 from_version = 1;
|
||||
uint32 to_version = 2;
|
||||
quixos.FunctionRef function = 3;
|
||||
// Resolved IDs, not GraphQL names, determine execution. Response selections
|
||||
// remain separate so aliases and fragments cannot change relational semantics.
|
||||
message QueryPathStep {
|
||||
oneof step {
|
||||
bool source = 1;
|
||||
bool target = 2;
|
||||
QueryRelationPath relation = 3;
|
||||
}
|
||||
|
||||
message ConstructorSpec {
|
||||
quixos.FunctionRef function = 1;
|
||||
}
|
||||
|
||||
message OperationSchema {
|
||||
string name = 1;
|
||||
TypeRef input_type = 2;
|
||||
TypeRef output_type = 3;
|
||||
quixos.FunctionRef function = 4;
|
||||
message QueryRelationPath {
|
||||
string interface_revision_id = 1;
|
||||
string member_id = 2;
|
||||
string target_interface_revision_id = 3;
|
||||
bool optional = 4;
|
||||
}
|
||||
|
||||
message OperationServiceSchema {
|
||||
string name = 1;
|
||||
string full_name = 2;
|
||||
repeated OperationSchema operations = 3;
|
||||
message QueryFieldOperand {
|
||||
string interface_revision_id = 1;
|
||||
string member_id = 2;
|
||||
}
|
||||
|
||||
message DocRefDeclaration {
|
||||
string local_name = 1;
|
||||
SymbolRef type = 2;
|
||||
SymbolRef target_class = 3;
|
||||
DocRefStrength strength = 4;
|
||||
message QueryExpression {
|
||||
repeated QueryPathStep path = 1;
|
||||
oneof leaf {
|
||||
QueryFieldOperand field = 2;
|
||||
string ref = 3;
|
||||
bool entry = 4;
|
||||
bool map_key = 5;
|
||||
}
|
||||
string value_type_json = 6;
|
||||
}
|
||||
enum QueryAggregateOperator {
|
||||
QUERY_AGGREGATE_OPERATOR_UNSPECIFIED = 0;
|
||||
QUERY_AGGREGATE_OPERATOR_COUNT = 1;
|
||||
QUERY_AGGREGATE_OPERATOR_COUNT_PRESENT = 2;
|
||||
QUERY_AGGREGATE_OPERATOR_SUM = 3;
|
||||
QUERY_AGGREGATE_OPERATOR_AVG = 4;
|
||||
QUERY_AGGREGATE_OPERATOR_MIN = 5;
|
||||
QUERY_AGGREGATE_OPERATOR_MAX = 6;
|
||||
}
|
||||
message QueryReduction {
|
||||
QueryAggregateOperator operator = 1;
|
||||
QueryExpression operand = 2;
|
||||
string value_type_json = 3;
|
||||
}
|
||||
message QueryOperand {
|
||||
oneof operand {
|
||||
QueryExpression expression = 1;
|
||||
QueryReduction reduction = 2;
|
||||
}
|
||||
}
|
||||
enum QueryComparisonOperator {
|
||||
QUERY_COMPARISON_OPERATOR_UNSPECIFIED = 0;
|
||||
QUERY_COMPARISON_OPERATOR_EQ = 1;
|
||||
QUERY_COMPARISON_OPERATOR_IN = 2;
|
||||
QUERY_COMPARISON_OPERATOR_IS_NULL = 3;
|
||||
QUERY_COMPARISON_OPERATOR_LT = 4;
|
||||
QUERY_COMPARISON_OPERATOR_LTE = 5;
|
||||
QUERY_COMPARISON_OPERATOR_GT = 6;
|
||||
QUERY_COMPARISON_OPERATOR_GTE = 7;
|
||||
}
|
||||
message QueryComparison {
|
||||
QueryOperand operand = 1;
|
||||
QueryComparisonOperator operator = 2;
|
||||
QueryArgument value = 3;
|
||||
}
|
||||
message QueryPredicateList {
|
||||
repeated QueryPredicate children = 1;
|
||||
}
|
||||
enum QueryRelationPredicateOperator {
|
||||
QUERY_RELATION_PREDICATE_OPERATOR_UNSPECIFIED = 0;
|
||||
QUERY_RELATION_PREDICATE_OPERATOR_SOME = 1;
|
||||
QUERY_RELATION_PREDICATE_OPERATOR_NONE = 2;
|
||||
QUERY_RELATION_PREDICATE_OPERATOR_IS = 3;
|
||||
QUERY_RELATION_PREDICATE_OPERATOR_IS_NULL = 4;
|
||||
}
|
||||
message QueryRelationPredicate {
|
||||
repeated QueryPathStep path = 1;
|
||||
QueryRelationPath relation = 2;
|
||||
QueryRelationPredicateOperator operator = 3;
|
||||
QueryPredicate predicate = 4;
|
||||
QueryArgument value = 5;
|
||||
}
|
||||
message QueryReductionPredicate {
|
||||
repeated QueryPathStep path = 1;
|
||||
QueryRelationPath relation = 2;
|
||||
QueryPredicate where = 3;
|
||||
QueryPredicate having = 4;
|
||||
}
|
||||
message QueryPredicate {
|
||||
oneof predicate {
|
||||
QueryPredicateList and = 1;
|
||||
QueryPredicateList or = 2;
|
||||
QueryPredicate not = 3;
|
||||
QueryComparison compare = 4;
|
||||
QueryRelationPredicate relation = 5;
|
||||
QueryReductionPredicate reduce = 6;
|
||||
}
|
||||
}
|
||||
message QueryRow {
|
||||
oneof row {
|
||||
QueryObjectRow object = 1;
|
||||
QueryPairRow pair = 2;
|
||||
}
|
||||
}
|
||||
message QueryObjectRow {
|
||||
string interface_revision_id = 1;
|
||||
bool membership = 2;
|
||||
string key_type = 3;
|
||||
}
|
||||
message QueryPairRow {
|
||||
QueryRow source = 1;
|
||||
QueryRow target = 2;
|
||||
}
|
||||
message QueryKey {
|
||||
repeated string path = 1;
|
||||
QueryExpression expression = 2;
|
||||
}
|
||||
message QueryDistinct {
|
||||
repeated QueryKey keys = 1;
|
||||
}
|
||||
message QueryExpansion {
|
||||
repeated QueryPathStep path = 1;
|
||||
QueryRelationPath relation = 2;
|
||||
}
|
||||
message QueryRelationalStage {
|
||||
uint32 id = 1;
|
||||
optional uint32 input = 2;
|
||||
QueryRow row = 3;
|
||||
oneof operation {
|
||||
QueryRelationPath source = 4;
|
||||
QueryPredicate filter = 5;
|
||||
QueryExpansion expand = 6;
|
||||
QueryDistinct distinct = 7;
|
||||
}
|
||||
}
|
||||
message QueryReductionProjection {
|
||||
repeated string path = 1;
|
||||
QueryReduction reduction = 2;
|
||||
}
|
||||
message QueryAggregateOrder {
|
||||
QueryOperand operand = 1;
|
||||
bool descending = 2;
|
||||
}
|
||||
message QueryRelationalTerminal {
|
||||
uint32 stage = 1;
|
||||
repeated string path = 2;
|
||||
bool groups = 3;
|
||||
repeated QueryKey keys = 4;
|
||||
repeated QueryReductionProjection reductions = 5;
|
||||
QueryPredicate where = 6;
|
||||
QueryPredicate having = 7;
|
||||
repeated QueryAggregateOrder order = 8;
|
||||
QueryArgument first = 9;
|
||||
QueryArgument all = 10;
|
||||
QueryArgument after = 11;
|
||||
repeated QuerySelection selection = 12;
|
||||
bool residual = 13;
|
||||
// Internal ordinary-relationship residual: yields ordered membership IDs.
|
||||
bool rows = 14;
|
||||
}
|
||||
message QueryRelationalPlan {
|
||||
repeated QueryRelationalStage stages = 1;
|
||||
repeated QueryRelationalTerminal terminals = 2;
|
||||
}
|
||||
message QueryReadBinding {
|
||||
string atom_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
string member_id = 3;
|
||||
string getter_operation_id = 4;
|
||||
string slot_id = 5;
|
||||
string edge_type_id = 6;
|
||||
string projection_id = 7;
|
||||
bool rpc = 8;
|
||||
string watch_start_operation_id = 9;
|
||||
string watch_stop_operation_id = 10;
|
||||
string field_name = 11;
|
||||
string value_type_json = 12;
|
||||
Cardinality cardinality = 13;
|
||||
string key_type = 14;
|
||||
}
|
||||
message QueryBudgets {
|
||||
uint32 rows = 1;
|
||||
uint32 depth = 2;
|
||||
uint32 result_bytes = 3;
|
||||
uint32 candidates = 4;
|
||||
uint32 rpc_calls = 5;
|
||||
uint32 concurrency = 6;
|
||||
uint32 deadline_ms = 7;
|
||||
}
|
||||
message InstalledQuery {
|
||||
string id = 1;
|
||||
string definition_digest = 2;
|
||||
string binding_digest = 3;
|
||||
string root_interface_revision_id = 4;
|
||||
repeated QuerySelection selection = 5;
|
||||
repeated QueryReadBinding bindings = 6;
|
||||
QueryBudgets budgets = 7;
|
||||
string variables_type_json = 8;
|
||||
string output_type_json = 9;
|
||||
map<string, QueryArgument> variable_defaults = 10;
|
||||
bool watch = 11;
|
||||
uint32 polling_interval_ms = 12;
|
||||
bool rpc_predicate_or_order = 13;
|
||||
}
|
||||
|
||||
+145
-32
@@ -3,40 +3,113 @@ syntax = "proto3";
|
||||
package quixos.orch;
|
||||
|
||||
import "camino/api.proto";
|
||||
import "camino/schema.proto";
|
||||
import "quixos/package.proto";
|
||||
import "quixos/refs.proto";
|
||||
import "quixos/runtime.proto";
|
||||
|
||||
service OrchestratorRuntime {
|
||||
rpc InvokeFunction(InvokeFunctionRequest) returns (InvokeFunctionResponse);
|
||||
rpc WatchFunction(WatchFunctionRequest) returns (stream WatchFunctionEvent);
|
||||
rpc ExecuteQuery(camino.QueryRequest) returns (camino.QueryResponse);
|
||||
rpc WatchQuery(camino.QueryRequest) returns (stream QueryEvent);
|
||||
rpc TryConform(TryConformRequest) returns (TryConformResponse);
|
||||
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
|
||||
rpc InvokeClassCapability(InvokeClassCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||
rpc WatchCapability(WatchCapabilityRequest) returns (stream WatchCapabilityEvent);
|
||||
rpc ConstructObject(ConstructObjectRequest) returns (ConstructObjectResponse);
|
||||
rpc ResolveOrConstructRelatedObject(ResolveOrConstructRelatedObjectRequest)
|
||||
returns (ResolveOrConstructRelatedObjectResponse);
|
||||
rpc GetWorkspace(GetWorkspaceRequest) returns (GetWorkspaceResponse);
|
||||
rpc ListPackageDescriptors(ListPackageDescriptorsRequest) returns (ListPackageDescriptorsResponse);
|
||||
rpc ListPackageRuntimes(ListPackageRuntimesRequest) returns (ListPackageRuntimesResponse);
|
||||
rpc ListActivations(ListActivationsRequest) returns (ListActivationsResponse);
|
||||
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
|
||||
}
|
||||
|
||||
message InvokeFunctionRequest {
|
||||
quixos.FunctionRef function = 1;
|
||||
string object_id = 2;
|
||||
map<string, camino.Value> input = 3;
|
||||
message QueryEvent {
|
||||
string run_id = 1;
|
||||
uint64 sequence = 2;
|
||||
string kind = 3;
|
||||
camino.QueryResponse snapshot = 4;
|
||||
repeated camino.QueryPathPart path = 5;
|
||||
camino.Value value = 6;
|
||||
string error = 7;
|
||||
}
|
||||
|
||||
message InvokeFunctionResponse {
|
||||
// Exact closed interface lookup; no policy selection or competing conformances.
|
||||
message TryConformRequest {
|
||||
string object_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
}
|
||||
message TryConformResponse {
|
||||
// Absent only when this object lacks a known contract. Other failures are errors.
|
||||
quixos.ConformanceWitness conformance = 1;
|
||||
}
|
||||
|
||||
message ConstructObjectRequest {
|
||||
string atom_id = 1;
|
||||
map<string, camino.Value> input = 2;
|
||||
}
|
||||
message ConstructObjectResponse {
|
||||
camino.CaminoObject object = 1;
|
||||
}
|
||||
|
||||
message ResolveOrConstructRelatedObjectRequest {
|
||||
string object_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
string member_id = 3;
|
||||
}
|
||||
message ResolveOrConstructRelatedObjectResponse {
|
||||
camino.CaminoObject object = 1;
|
||||
bool constructed = 2;
|
||||
}
|
||||
|
||||
message InvokeCapabilityRequest {
|
||||
quixos.CapabilityRef capability = 1;
|
||||
string object_id = 2;
|
||||
map<string, camino.Value> input = 3;
|
||||
// Identifies one logical client mutation across the capability and Camino
|
||||
// layers so a live-value controller can recognize its own confirmation.
|
||||
string client_mutation_id = 4;
|
||||
}
|
||||
message InvokeClassCapabilityRequest {
|
||||
string conformance_id = 1;
|
||||
string operation_id = 2;
|
||||
map<string, camino.Value> input = 3;
|
||||
}
|
||||
message InvokeCapabilityResponse {
|
||||
string invocation_id = 1;
|
||||
Activation activation = 2;
|
||||
bool ok = 3;
|
||||
camino.Value result = 4;
|
||||
string error = 5;
|
||||
repeated quixos.runtime.DerivedDependency dependencies = 6;
|
||||
FieldEditing field_editing = 7;
|
||||
}
|
||||
|
||||
message WatchFunctionRequest {
|
||||
quixos.FunctionRef function = 1;
|
||||
// Resolved from the checked native getter/setter binding, not Value.source.
|
||||
message FieldEditing {
|
||||
string getter_operation_id = 1;
|
||||
string setter_operation_id = 2;
|
||||
string document_type = 3;
|
||||
string binding_digest = 4;
|
||||
}
|
||||
message EditCapabilityFieldRequest {
|
||||
// The public getter; setter must belong to the same value member.
|
||||
quixos.CapabilityRef capability = 1;
|
||||
string object_id = 2;
|
||||
string setter_operation_id = 3;
|
||||
string binding_digest = 4;
|
||||
camino.CrdtValue update = 5;
|
||||
string client_mutation_id = 6;
|
||||
}
|
||||
|
||||
message WatchCapabilityRequest {
|
||||
quixos.CapabilityRef capability = 1;
|
||||
string object_id = 2;
|
||||
map<string, camino.Value> input = 3;
|
||||
}
|
||||
|
||||
message WatchFunctionEvent {
|
||||
message WatchCapabilityEvent {
|
||||
string invocation_id = 1;
|
||||
Activation activation = 2;
|
||||
string watch_id = 3;
|
||||
@@ -44,38 +117,80 @@ message WatchFunctionEvent {
|
||||
repeated quixos.runtime.DerivedDependency dependencies = 5;
|
||||
string error = 6;
|
||||
bool initial = 7;
|
||||
FieldEditing field_editing = 8;
|
||||
}
|
||||
|
||||
message GetWorkspaceRequest {
|
||||
// Revision polling must not download the entire interface graph.
|
||||
bool include_interface_contracts = 1;
|
||||
bool include_query_contracts = 2;
|
||||
}
|
||||
message GetWorkspaceResponse {
|
||||
string workspace_id = 1;
|
||||
string workspace_revision_id = 2;
|
||||
string source_root_commit = 3;
|
||||
// Checked constructors whose wire input can be empty. The create panel uses
|
||||
// class factory conformances instead of this constructor inventory.
|
||||
repeated string empty_input_constructible_atom_ids = 4;
|
||||
repeated CapabilityInputContract capability_inputs = 5;
|
||||
repeated ConstructorInputContract constructor_inputs = 6;
|
||||
// Exact closed interface contracts used by checked presentation consumers.
|
||||
string interfaces_json = 7;
|
||||
repeated ClassCapability class_capabilities = 8;
|
||||
repeated QueryDescription queries = 9;
|
||||
uint32 active_query_executions = 10;
|
||||
}
|
||||
message QueryDescription {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string package_revision_id = 3;
|
||||
string document = 4;
|
||||
string schema = 5;
|
||||
repeated string source_files = 6;
|
||||
string effects_json = 7;
|
||||
camino.InstalledQuery plan = 8;
|
||||
}
|
||||
message ClassCapability {
|
||||
string conformance_id = 1;
|
||||
string atom_id = 2;
|
||||
string interface_revision_id = 3;
|
||||
string definition_id = 4;
|
||||
string operation_id = 5;
|
||||
string input_type_json = 6;
|
||||
string output_type_json = 7;
|
||||
}
|
||||
message CapabilityInputContract {
|
||||
string interface_revision_id = 1;
|
||||
string operation_id = 2;
|
||||
string type_json = 3;
|
||||
}
|
||||
message ConstructorInputContract {
|
||||
string atom_id = 1;
|
||||
string type_json = 2;
|
||||
}
|
||||
message ListActivationsRequest {}
|
||||
|
||||
message ListPackageDescriptorsRequest {}
|
||||
|
||||
message ListPackageDescriptorsResponse {
|
||||
repeated quixos.PackageDescriptor descriptors = 1;
|
||||
}
|
||||
|
||||
message ListPackageRuntimesRequest {}
|
||||
|
||||
message ListPackageRuntimesResponse {
|
||||
repeated PackageRuntimeStatus runtimes = 1;
|
||||
}
|
||||
|
||||
message ListActivationsResponse {
|
||||
repeated Activation activations = 1;
|
||||
}
|
||||
|
||||
message CloseActivationRequest {
|
||||
string activation_id = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
message CloseActivationResponse {
|
||||
Activation activation = 1;
|
||||
}
|
||||
|
||||
message Activation {
|
||||
string activation_id = 1;
|
||||
quixos.FunctionRef function = 2;
|
||||
quixos.PackageExportRef export = 2;
|
||||
string object_id = 3;
|
||||
string state = 4;
|
||||
uint32 demand = 5;
|
||||
@@ -88,17 +203,15 @@ message Activation {
|
||||
|
||||
message PackageRuntimeStatus {
|
||||
string runtime_key = 1;
|
||||
string package_namespace = 2;
|
||||
string package_name = 3;
|
||||
uint32 descriptor_version = 4;
|
||||
string source_repo = 5;
|
||||
string server_installable = 6;
|
||||
string source_path = 7;
|
||||
string server_path = 8;
|
||||
uint32 pid = 9;
|
||||
string state = 10;
|
||||
string started_at = 11;
|
||||
string last_handshake_at = 12;
|
||||
string runtime_protocol_version = 13;
|
||||
uint32 advertised_function_count = 14;
|
||||
string package_revision_id = 2;
|
||||
string source_repository = 3;
|
||||
string source_commit = 4;
|
||||
string build_target = 5;
|
||||
string server_path = 6;
|
||||
uint32 pid = 7;
|
||||
string state = 8;
|
||||
string started_at = 9;
|
||||
string last_handshake_at = 10;
|
||||
string runtime_protocol_version = 11;
|
||||
uint32 advertised_export_count = 12;
|
||||
}
|
||||
|
||||
@@ -2,70 +2,14 @@ syntax = "proto3";
|
||||
|
||||
package quixos;
|
||||
|
||||
import "camino/schema.proto";
|
||||
import "quixos/refs.proto";
|
||||
|
||||
message PackageRef {
|
||||
string package_namespace = 1;
|
||||
string package_name = 2;
|
||||
uint32 descriptor_version = 3;
|
||||
string version_ref = 4;
|
||||
}
|
||||
|
||||
message PackageDescriptor {
|
||||
string package_namespace = 1;
|
||||
string package_name = 2;
|
||||
uint32 descriptor_version = 3;
|
||||
uint32 interface_compatible_back_to = 4;
|
||||
uint32 runner_compatible_back_to = 5;
|
||||
string source_repo = 6;
|
||||
string source_ref = 7;
|
||||
string resolved_source_ref = 8;
|
||||
string server_installable = 9;
|
||||
string runtime_protocol_version = 10;
|
||||
repeated ClassExport class_exports = 11;
|
||||
repeated FunctionExport function_exports = 12;
|
||||
repeated ComponentExport component_exports = 13;
|
||||
repeated SidecarExport sidecar_exports = 14;
|
||||
repeated PackageDependency dependencies = 15;
|
||||
string package_id = 1;
|
||||
string package_revision_id = 2;
|
||||
string runtime_protocol_version = 3;
|
||||
repeated RuntimeExport exports = 4;
|
||||
}
|
||||
|
||||
enum FunctionCapabilityKind {
|
||||
FUNCTION_CAPABILITY_KIND_UNSPECIFIED = 0;
|
||||
METHOD = 1;
|
||||
FIELD_RESOLVER = 2;
|
||||
MIGRATION = 3;
|
||||
CONSTRUCTOR = 4;
|
||||
}
|
||||
|
||||
message ClassExport {
|
||||
camino.SymbolRef class_id = 1;
|
||||
string source_file = 2;
|
||||
}
|
||||
|
||||
message FunctionExport {
|
||||
FunctionRef function = 1;
|
||||
string input_type = 2;
|
||||
string output_type = 3;
|
||||
uint32 interface_version = 4;
|
||||
uint32 interface_compatible_back_to = 5;
|
||||
FunctionCapabilityKind capability_kind = 6;
|
||||
}
|
||||
|
||||
message ComponentExport {
|
||||
string symbol = 1;
|
||||
string props_type = 2;
|
||||
repeated camino.SymbolRef supported_classes = 3;
|
||||
}
|
||||
|
||||
message SidecarExport {
|
||||
string symbol = 1;
|
||||
string side_effect_mode = 2;
|
||||
}
|
||||
|
||||
message PackageDependency {
|
||||
PackageRef package = 1;
|
||||
uint32 interface_compatible_back_to = 2;
|
||||
bool refactor_required = 3;
|
||||
string compatibility_note = 4;
|
||||
message RuntimeExport {
|
||||
string export_id = 1;
|
||||
string runtime_symbol = 2;
|
||||
}
|
||||
|
||||
+37
-6
@@ -2,10 +2,41 @@ syntax = "proto3";
|
||||
|
||||
package quixos;
|
||||
|
||||
message FunctionRef {
|
||||
string package_namespace = 1;
|
||||
string package_name = 2;
|
||||
string symbol = 3;
|
||||
string version_ref = 4;
|
||||
string operation = 5;
|
||||
message CapabilityRef {
|
||||
string interface_revision_id = 1;
|
||||
string operation_id = 2;
|
||||
// Optional fence for a view acquired through TryConform. Not an authority grant.
|
||||
ConformanceWitness conformance = 3;
|
||||
}
|
||||
|
||||
message ConformanceWitness {
|
||||
string object_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
string conformance_id = 3;
|
||||
string workspace_revision_id = 4;
|
||||
string workspace_epoch = 5;
|
||||
}
|
||||
|
||||
message PackageExportRef {
|
||||
string package_revision_id = 1;
|
||||
string export_id = 2;
|
||||
}
|
||||
|
||||
message InjectedDependency {
|
||||
string port_id = 1;
|
||||
oneof binding {
|
||||
string state_slot_id = 2;
|
||||
EdgeDependency edge = 3;
|
||||
string interface_revision_id = 4;
|
||||
string constructor_atom_id = 5;
|
||||
string query_id = 7;
|
||||
}
|
||||
// Defaults to the invocation receiver. A checked dependency traversal can
|
||||
// select a related object explicitly before the package runtime starts.
|
||||
string object_id = 6;
|
||||
}
|
||||
|
||||
message EdgeDependency {
|
||||
string edge_type_id = 1;
|
||||
string projection_id = 2;
|
||||
}
|
||||
|
||||
+36
-10
@@ -9,44 +9,70 @@ service PackageRuntime {
|
||||
rpc Handshake(HandshakeRequest) returns (HandshakeResponse);
|
||||
rpc Invoke(InvokeRequest) returns (InvokeResponse);
|
||||
rpc Watch(WatchRequest) returns (stream WatchEvent);
|
||||
rpc GetInvocationStatus(InvocationControlRequest) returns (InvocationStatus);
|
||||
rpc CancelInvocation(InvocationControlRequest) returns (InvocationStatus);
|
||||
}
|
||||
|
||||
message HandshakeRequest {
|
||||
string orch_protocol_version = 1;
|
||||
string nonce = 2;
|
||||
}
|
||||
message HandshakeResponse {
|
||||
string package_revision_id = 1;
|
||||
string runtime_protocol_version = 2;
|
||||
repeated string export_ids = 3;
|
||||
string instance_id = 4;
|
||||
string authentication_proof = 5;
|
||||
repeated string capabilities = 6;
|
||||
}
|
||||
|
||||
message HandshakeResponse {
|
||||
string package_namespace = 1;
|
||||
string package_name = 2;
|
||||
string runtime_protocol_version = 3;
|
||||
repeated quixos.FunctionRef functions = 4;
|
||||
message InvocationContext {
|
||||
string workspace_epoch = 1;
|
||||
string instance_id = 2;
|
||||
string binding_digest = 3;
|
||||
string grant = 4;
|
||||
string session_id = 5;
|
||||
// Host-selected owner; packages must not invent workspace-local ownership.
|
||||
string owner_conformance_id = 6;
|
||||
}
|
||||
message InvocationControlRequest {
|
||||
string invocation_id = 1;
|
||||
}
|
||||
message InvocationStatus {
|
||||
string invocation_id = 1;
|
||||
// unknown, running, cancellation-requested, completed, failed
|
||||
string state = 2;
|
||||
}
|
||||
|
||||
message InvokeRequest {
|
||||
string invocation_id = 1;
|
||||
quixos.FunctionRef function = 2;
|
||||
quixos.PackageExportRef export = 2;
|
||||
string object_id = 3;
|
||||
map<string, camino.Value> input = 4;
|
||||
repeated quixos.InjectedDependency dependencies = 5;
|
||||
InvocationContext context = 6;
|
||||
}
|
||||
|
||||
message InvokeResponse {
|
||||
bool ok = 1;
|
||||
camino.Value result = 2;
|
||||
string error = 3;
|
||||
repeated DerivedDependency dependencies = 4;
|
||||
}
|
||||
|
||||
message WatchRequest {
|
||||
string invocation_id = 1;
|
||||
quixos.FunctionRef function = 2;
|
||||
quixos.PackageExportRef export = 2;
|
||||
string object_id = 3;
|
||||
map<string, camino.Value> input = 4;
|
||||
repeated quixos.InjectedDependency dependencies = 5;
|
||||
InvocationContext context = 6;
|
||||
}
|
||||
|
||||
message DerivedDependency {
|
||||
string kind = 1;
|
||||
string object_id = 2;
|
||||
string field_name = 3;
|
||||
string source_field = 4;
|
||||
string attachment_id = 3;
|
||||
string projection_id = 4;
|
||||
}
|
||||
|
||||
message WatchEvent {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
/** Enforce the authored bundled-code contract, not a security sandbox. */
|
||||
export function bundlePolicyErrors(source: string, filename: string): string[] {
|
||||
const ast = parse(source, { sourceType: "module", plugins: ["typescript", "jsx"] });
|
||||
const errors: string[] = [];
|
||||
const visit = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(visit);
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
const n = value as Record<string, any>;
|
||||
if (typeof n.type !== "string") return;
|
||||
const fail = (message: string) => errors.push(`${filename}:${n.loc?.start.line ?? 1}: ${message}`);
|
||||
if (n.type === "MetaProperty" && n.meta.name === "import")
|
||||
fail("Bundled package code cannot use import.meta; import packaged assets statically");
|
||||
if (n.type === "Identifier" && ["__dirname", "__filename"].includes(n.name))
|
||||
fail("Bundled package code cannot depend on module filesystem locations");
|
||||
if (["CallExpression", "NewExpression"].includes(n.type)) {
|
||||
if (n.callee?.type === "Identifier" && ["eval", "Function"].includes(n.callee.name))
|
||||
fail("Dynamic code generation is unsupported in bundled package code");
|
||||
if (
|
||||
(n.callee?.type === "Import" || (n.callee?.type === "Identifier" && n.callee.name === "require")) &&
|
||||
(n.arguments.length !== 1 || n.arguments[0].type !== "StringLiteral")
|
||||
)
|
||||
fail("Module imports must have a static string specifier");
|
||||
}
|
||||
if (n.type === "ImportExpression" && n.source.type !== "StringLiteral")
|
||||
fail("Module imports must have a static string specifier");
|
||||
Object.values(n).forEach(visit);
|
||||
};
|
||||
visit(ast);
|
||||
return errors;
|
||||
}
|
||||
export async function checkBundleSources(directory: string): Promise<void> {
|
||||
const errors: string[] = [];
|
||||
async function walk(current: string) {
|
||||
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
||||
if (["gen", "node_modules"].includes(entry.name)) continue;
|
||||
const file = path.join(current, entry.name);
|
||||
if (entry.isSymbolicLink()) throw new Error(`Authored source symlinks are unsupported: ${file}`);
|
||||
if (entry.isDirectory()) await walk(file);
|
||||
else if (/\.[cm]?[jt]sx?$/.test(entry.name) && !entry.name.endsWith(".d.ts"))
|
||||
errors.push(...bundlePolicyErrors(await fs.readFile(file, "utf8"), file));
|
||||
}
|
||||
}
|
||||
await walk(directory);
|
||||
if (errors.length) throw new Error(errors.join("\n"));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { generateTypeScriptBindings } from "./index.js";
|
||||
import path from "node:path";
|
||||
import { reactPlatformTypes } from "./react-platform.js";
|
||||
import { generateReactBindings } from "./react.js";
|
||||
|
||||
const main = async () => {
|
||||
const [schema, revision, output, options, ...rest] = process.argv.slice(2);
|
||||
if (!schema || !revision || !output || rest.length)
|
||||
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 contracts = JSON.parse(await readFile(schema, "utf8"));
|
||||
const generated = generateTypeScriptBindings(contracts, revision, config);
|
||||
await writeFile(output, generated);
|
||||
if (config.react) {
|
||||
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
|
||||
await writeFile(
|
||||
path.join(path.dirname(output), "react-props.gen.ts"),
|
||||
generateReactBindings(contracts, revision, config.react.propsExports, config.react.components),
|
||||
);
|
||||
}
|
||||
};
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
||||
|
||||
/** Host clients have no package receiver, but must use the same checked
|
||||
* interface signatures and argument framing as generated package ports. */
|
||||
export const generateClientContracts = (
|
||||
interfaces: InterfaceRevision[],
|
||||
messages: Record<string, string>,
|
||||
qualified = false,
|
||||
) => {
|
||||
if (interfaces.some((entry) => entry.template))
|
||||
throw new Error("Host contracts require closed interface applications, not generic definitions");
|
||||
const type = (value: ValueType): string => {
|
||||
switch (value.kind) {
|
||||
case "builtin":
|
||||
return value.name === "unit" ? "undefined" : "string";
|
||||
case "scalar":
|
||||
return {
|
||||
bool: "boolean",
|
||||
string: "string",
|
||||
bytes: "Uint8Array",
|
||||
int32: "number",
|
||||
uint32: "number",
|
||||
double: "number",
|
||||
int64: "bigint",
|
||||
uint64: "bigint",
|
||||
}[value.name];
|
||||
case "object-ref":
|
||||
return qualified
|
||||
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
|
||||
: `{readonly $quixosRef: string}`;
|
||||
case "optional":
|
||||
return `(${type(value.value)} | null)`;
|
||||
case "list":
|
||||
return `Array<${type(value.value)}>`;
|
||||
case "record":
|
||||
return `{${Object.entries(value.fields)
|
||||
.map(([name, field]) => `${JSON.stringify(name)}${field.kind === "optional" ? "?" : ""}: ${type(field)}`)
|
||||
.join("; ")}}`;
|
||||
case "message": {
|
||||
const binding = messages[value.descriptorId];
|
||||
if (!binding) throw new Error(`Missing host message type ${value.descriptorId}`);
|
||||
return binding;
|
||||
}
|
||||
}
|
||||
};
|
||||
const operations = interfaces.flatMap((iface) =>
|
||||
iface.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call")
|
||||
.map((operation) => ({ ...operation, interfaceRevisionId: iface.revisionId })),
|
||||
),
|
||||
);
|
||||
if (qualified) {
|
||||
const contracts = interfaces.map((iface) => {
|
||||
const members = iface.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map((op) => ` ${JSON.stringify(op.id)}: {input: ${type(op.inputType)}; output: ${type(op.outputType)}};`),
|
||||
);
|
||||
return `${JSON.stringify(iface.revisionId)}: {\n${members.join("\n")}\n}`;
|
||||
});
|
||||
if (new Set(interfaces.map((iface) => iface.revisionId)).size !== interfaces.length)
|
||||
throw new Error("Duplicate closed interface identity");
|
||||
return (
|
||||
`// Generated closed capability contracts. Dispatch by interface AND operation.\n` +
|
||||
`declare const referenceType: unique symbol;\nexport type CapabilityReference<T extends string> = {readonly $quixosRef: string; readonly [referenceType]: T};\n` +
|
||||
`export type CapabilityContracts = {${contracts.join(";\n")}};\n` +
|
||||
`export type CapabilityInput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {input: infer T} ? T : never;\n` +
|
||||
`export type CapabilityOutput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {output: infer T} ? T : never;\n` +
|
||||
`export const capabilityApplications = ${JSON.stringify(Object.fromEntries(interfaces.map((iface) => [iface.revisionId, { definitionId: iface.application?.definitionId ?? iface.revisionId, arguments: iface.application?.arguments ?? [], ...(iface.application?.self ? { self: iface.application.self } : {}) }])))} as const;\n`
|
||||
);
|
||||
}
|
||||
if (new Set(operations.map((entry) => entry.id)).size !== operations.length)
|
||||
throw new Error(
|
||||
"Host operation IDs are ambiguous across interfaces; select a closed interface application explicitly",
|
||||
);
|
||||
return (
|
||||
`// Generated from checked QX interfaces. Regenerate with scripts/generate-platform-contracts.mjs.\n` +
|
||||
`export type PlatformInputs = {\n${operations.map((operation) => ` ${JSON.stringify(operation.id)}: ${type(operation.inputType)};`).join("\n")}\n};\n` +
|
||||
`export const platformOperations = ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
operations.map((operation) => [
|
||||
operation.id,
|
||||
{
|
||||
interfaceRevisionId: operation.interfaceRevisionId,
|
||||
input:
|
||||
operation.inputType.kind === "builtin" && operation.inputType.name === "unit"
|
||||
? "unit"
|
||||
: ["record", "message"].includes(operation.inputType.kind)
|
||||
? "fields"
|
||||
: "value",
|
||||
},
|
||||
]),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)} as const;\n`
|
||||
);
|
||||
};
|
||||
|
||||
/** New consumers use exact closed interface identities; operation IDs alone are not unique. */
|
||||
export const generateAppliedClientContracts = (
|
||||
interfaces: InterfaceRevision[],
|
||||
messages: Record<string, string> = {},
|
||||
) => generateClientContracts(interfaces, messages, true);
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { GenericPackageExport, GenericDependencyPort } from "../capability-model/generic-packages.js";
|
||||
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
||||
import type {
|
||||
ValueTypeExpression,
|
||||
ObjectTypeExpression,
|
||||
TypeArgumentExpression,
|
||||
TypeParameter,
|
||||
ValueAliasDefinition,
|
||||
} from "../capability-model/generics.js";
|
||||
|
||||
/** Universal source contracts. Only closed exports get executable codecs. */
|
||||
export const genericImplementationType = (
|
||||
definition: GenericPackageExport,
|
||||
interfaces: InterfaceRevision[],
|
||||
concrete: (type: ValueType) => string,
|
||||
queryPort: (requirement: Extract<GenericDependencyPort["requirement"], { kind: "query" }>) => string = () => {
|
||||
throw new Error("Query binding schema required");
|
||||
},
|
||||
): string => {
|
||||
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
||||
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
||||
const rootScope = (): Scope => ({ arguments: new Map(), aliases: definition.aliases });
|
||||
const object = (entries: [string, string][]) =>
|
||||
`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(";")}}`;
|
||||
const target = (type: ObjectTypeExpression, scope: Scope): string => {
|
||||
if (type.kind === "parameter") {
|
||||
const bound = scope.arguments.get(type.parameterId);
|
||||
if (bound) return bound;
|
||||
const name = names.get(type.parameterId);
|
||||
if (!name) throw new Error(`Unbound object parameter ${type.parameterId}`);
|
||||
return name;
|
||||
}
|
||||
if (type.kind === "atom") return JSON.stringify(`atom:${type.atomId}`);
|
||||
if (type.kind === "interface") return JSON.stringify(`interface:${type.interfaceRevisionId}`);
|
||||
if (type.kind === "application")
|
||||
return `QxApplied<${JSON.stringify(type.application.definitionId)}, [${type.application.arguments.map((arg) => argument(arg, scope)).join(",")}]>`;
|
||||
throw new Error("Generic package Self must be expressed as an explicit object parameter");
|
||||
};
|
||||
const argument = (arg: TypeArgumentExpression, scope: Scope): string =>
|
||||
arg.kind === "value" ? value(arg.type, scope) : target(arg.target, scope);
|
||||
const bind = (parameters: TypeParameter[], args: TypeArgumentExpression[], scope: Scope): Scope => {
|
||||
if (parameters.length !== args.length) throw new Error("Wrong generic arity during code generation");
|
||||
const result = new Map(scope.arguments);
|
||||
// Render arguments in their original lexical scope before introducing binders.
|
||||
const rendered = args.map((arg) => argument(arg, scope));
|
||||
parameters.forEach((parameter, index) => result.set(parameter.id, rendered[index]));
|
||||
return { ...scope, arguments: result };
|
||||
};
|
||||
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
|
||||
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
|
||||
switch (type.kind) {
|
||||
case "parameter": {
|
||||
const bound = scope.arguments.get(type.parameterId);
|
||||
if (bound) return bound;
|
||||
const name = names.get(type.parameterId);
|
||||
if (!name) throw new Error(`Unbound value parameter ${type.parameterId}`);
|
||||
return name;
|
||||
}
|
||||
case "object-ref":
|
||||
return `QxObjectRef<${target(type.expectation, scope)}>`;
|
||||
case "record":
|
||||
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
|
||||
case "optional":
|
||||
return `(${value(type.value, scope, depth + 1)} | null)`;
|
||||
case "list":
|
||||
return `Array<${value(type.value, scope, depth + 1)}>`;
|
||||
case "alias": {
|
||||
const alias = scope.aliases.find((entry) => entry.id === type.definitionId);
|
||||
if (!alias) throw new Error(`Missing alias ${type.definitionId}`);
|
||||
return value(alias.body, bind(alias.parameters, type.arguments, scope), depth + 1);
|
||||
}
|
||||
default:
|
||||
return concrete(type);
|
||||
}
|
||||
};
|
||||
const params = (type: ValueTypeExpression, scope: Scope) =>
|
||||
type.kind === "builtin" && type.name === "unit" ? "" : `input:${value(type, scope)}`;
|
||||
const port = (entry: GenericDependencyPort): string => {
|
||||
const requirement = entry.requirement,
|
||||
scope = rootScope();
|
||||
switch (requirement.kind) {
|
||||
case "state":
|
||||
return object([
|
||||
...requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "read") return ["get", `()=>Promise<${value(requirement.valueType, scope)}>`];
|
||||
if (primitive === "write") return ["set", `(value:${value(requirement.valueType, scope)})=>Promise<void>`];
|
||||
throw new Error(`Unsupported generic state primitive ${primitive}`);
|
||||
}),
|
||||
...(requirement.primitives.includes("read")
|
||||
? [["live", "()=>Promise<QxLiveValue>"] as [string, string]]
|
||||
: []),
|
||||
]);
|
||||
case "edge": {
|
||||
const ref = `QxObjectRef<${target(requirement.target, scope)}>`;
|
||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "resolve") return ["resolve", `()=>Promise<Array<${ref}>>`];
|
||||
if (primitive === "connect" || primitive === "disconnect")
|
||||
return [primitive, `(target:${ref})=>Promise<void>`];
|
||||
throw new Error(`Unsupported generic edge primitive ${primitive}`);
|
||||
});
|
||||
if (requirement.primitives.includes("resolve"))
|
||||
methods.push(["collection", `()=>Promise<RelationshipCollection<${ref}>>`]);
|
||||
if (
|
||||
["resolve", "connect", "disconnect"].every((primitive) =>
|
||||
requirement.primitives.includes(primitive as "resolve"),
|
||||
)
|
||||
)
|
||||
methods.push([
|
||||
"replace",
|
||||
`(entries:RelationshipEntry<${ref}>[],expectedRevision:bigint)=>Promise<RelationshipCollection<${ref}>>`,
|
||||
]);
|
||||
return object(methods);
|
||||
}
|
||||
case "query":
|
||||
return queryPort(requirement);
|
||||
case "constructor":
|
||||
return object([
|
||||
[
|
||||
"construct",
|
||||
`(${params(requirement.inputType, scope)})=>Promise<QxObjectRef<${target(requirement.target, scope)}>>`,
|
||||
],
|
||||
]);
|
||||
case "interface": {
|
||||
const contract = interfaces.find((entry) => entry.revisionId === requirement.application.definitionId);
|
||||
if (!contract) throw new Error(`Missing generic port contract ${requirement.application.definitionId}`);
|
||||
const local = {
|
||||
...bind(contract.template?.parameters ?? [], requirement.application.arguments, scope),
|
||||
aliases: contract.template?.aliases ?? [],
|
||||
};
|
||||
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call" && op.scope !== "class")
|
||||
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
||||
);
|
||||
const methods: [string, string][] = [
|
||||
["objectId", `QxObjectRef<${target({ kind: "application", application: requirement.application }, scope)}>`],
|
||||
["live", object(operations.map((op) => [op.name, `(${params(op.inputType, local)})=>Promise<QxLiveValue>`]))],
|
||||
...operations.map(
|
||||
(op) =>
|
||||
[op.name, `(${params(op.inputType, local)})=>Promise<${value(op.outputType, local)}>`] as [
|
||||
string,
|
||||
string,
|
||||
],
|
||||
),
|
||||
];
|
||||
return object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]);
|
||||
}
|
||||
}
|
||||
};
|
||||
const declarations = definition.parameters
|
||||
.map((parameter) => `${names.get(parameter.id)}${parameter.kind === "object" ? " extends string" : ""}`)
|
||||
.join(",");
|
||||
const receiver = definition.receiverRequirement;
|
||||
const context = object([
|
||||
["conform", "QxConformer"],
|
||||
...(definition.kind === "function"
|
||||
? []
|
||||
: [
|
||||
[
|
||||
"objectId",
|
||||
`QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`,
|
||||
] as [string, string],
|
||||
]),
|
||||
["input", value(definition.inputType, rootScope())],
|
||||
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
|
||||
]);
|
||||
const contextWithLifecycle =
|
||||
definition.kind === "function"
|
||||
? `${context} & {signal?: AbortSignal}`
|
||||
: `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
|
||||
const result = value(definition.eventType ?? definition.outputType, rootScope());
|
||||
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
||||
const derived = `{kind:"derived";get:${handler}}`;
|
||||
return definition.eventType
|
||||
? derived
|
||||
: `(${handler})${definition.kind === "operation" && definition.mode === "call" ? ` | ${derived}` : ""}`;
|
||||
};
|
||||
@@ -0,0 +1,408 @@
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
PackageRevision,
|
||||
ValueType,
|
||||
DependencyPort,
|
||||
WorkspaceRevision,
|
||||
} from "../capability-model/types.js";
|
||||
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js";
|
||||
import { genericImplementationType } from "./generics.js";
|
||||
import { capabilityId } from "../capability-model/types.js";
|
||||
|
||||
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
|
||||
export type BindingSchema = {
|
||||
format: "quixos-bindings";
|
||||
version: 1;
|
||||
interfaces: InterfaceRevision[];
|
||||
interfaceTemplates?: InterfaceRevision[];
|
||||
packages: PackageRevision[];
|
||||
};
|
||||
export const bindingSchema = (compiled: Pick<CompiledCapabilityResourceRepository, "resources">): BindingSchema => ({
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaceTemplates: compiled.resources.flatMap((node) =>
|
||||
node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [],
|
||||
),
|
||||
interfaces: [
|
||||
...new Map(
|
||||
compiled.resources
|
||||
.flatMap((node) => [
|
||||
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
||||
...(node.resource.specializations ?? []),
|
||||
])
|
||||
.filter((entry) => !entry.template)
|
||||
.map((entry) => [entry.revisionId, entry]),
|
||||
).values(),
|
||||
],
|
||||
packages: compiled.resources.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
||||
});
|
||||
|
||||
export const specializeBindingSchema = (
|
||||
base: BindingSchema,
|
||||
workspace: WorkspaceRevision,
|
||||
revisionId: string,
|
||||
): BindingSchema => {
|
||||
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
|
||||
if (!pkg) throw new Error(`Missing candidate package ${revisionId}`);
|
||||
const needed = new Set<string>();
|
||||
const visit = (value: unknown): void => {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if ("interfaceRevisionId" in value && typeof value.interfaceRevisionId === "string")
|
||||
needed.add(value.interfaceRevisionId);
|
||||
Object.values(value).forEach(visit);
|
||||
};
|
||||
visit(pkg.exports);
|
||||
const interfaces = new Map<string, InterfaceRevision>(base.interfaces.map((entry) => [entry.revisionId, entry]));
|
||||
for (const id of needed) {
|
||||
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === id);
|
||||
if (iface) {
|
||||
interfaces.set(id, iface);
|
||||
visit(iface.members);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
interfaces: [...interfaces.values()],
|
||||
packages: base.packages.map((entry) => {
|
||||
const candidate = workspace.packageImports.find((value) => value.revisionId === entry.revisionId);
|
||||
if (!candidate) throw new Error(`Missing dependency package ${entry.revisionId}`);
|
||||
return candidate;
|
||||
}),
|
||||
};
|
||||
};
|
||||
export type TypeScriptBindingOptions = {
|
||||
runtimeModule?: string;
|
||||
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
|
||||
messages?: Record<string, { module: string; export: string }>;
|
||||
};
|
||||
export function generatePackageDescriptor(schema: BindingSchema, revisionId: string): string {
|
||||
const pkg = schema.packages.find((entry) => entry.revisionId === revisionId);
|
||||
if (!pkg) throw new Error(`Unknown package revision ${revisionId}`);
|
||||
return (
|
||||
`# Generated from the checked package contract\npackage_id: ${JSON.stringify(pkg.packageId)}\npackage_revision_id: ${JSON.stringify(pkg.revisionId)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` +
|
||||
pkg.exports
|
||||
.map(
|
||||
(entry) =>
|
||||
`exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.displayName)} }\n`,
|
||||
)
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
const q = JSON.stringify;
|
||||
const object = (entries: [string, string][]) =>
|
||||
`{ ${entries.map(([key, value]) => `${q(key)}: ${value}`).join("; ")} }`;
|
||||
const unit = (type: ValueType) => type.kind === "builtin" && type.name === "unit";
|
||||
|
||||
export const generateTypeScriptBindings = (
|
||||
schema: BindingSchema,
|
||||
packageRevisionId: string,
|
||||
options: TypeScriptBindingOptions = {},
|
||||
) => {
|
||||
if (schema.format !== "quixos-bindings" || schema.version !== 1)
|
||||
throw new Error("Unsupported binding schema version");
|
||||
if (schema.interfaces.some((entry) => entry.template))
|
||||
throw new Error("Package bindings require closed interface applications, not generic definitions");
|
||||
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
|
||||
if (!pkg) throw new Error(`Unknown package revision ${packageRevisionId}`);
|
||||
const messages = new Map<string, string>();
|
||||
const type = (value: ValueType): string => {
|
||||
switch (value.kind) {
|
||||
case "builtin":
|
||||
return value.name === "unit" ? "null" : "QxWatchHandle";
|
||||
case "scalar":
|
||||
return {
|
||||
bool: "boolean",
|
||||
bytes: "Uint8Array",
|
||||
string: "string",
|
||||
int64: "bigint",
|
||||
uint64: "bigint",
|
||||
double: "number",
|
||||
int32: "number",
|
||||
uint32: "number",
|
||||
}[value.name];
|
||||
case "object-ref":
|
||||
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
|
||||
case "optional":
|
||||
return `(${type(value.value)} | null)`;
|
||||
case "list":
|
||||
return `Array<${type(value.value)}>`;
|
||||
case "record":
|
||||
return `{ ${Object.entries(value.fields)
|
||||
.map(([name, field]) => `${q(name)}: ${type(field)}`)
|
||||
.join("; ")} }`;
|
||||
case "message": {
|
||||
if (!options.messages?.[value.descriptorId])
|
||||
throw new Error(`Missing TypeScript message binding for ${value.descriptorId}`);
|
||||
if (!messages.has(value.descriptorId)) messages.set(value.descriptorId, `message${messages.size}`);
|
||||
return `BindingValue<typeof ${messages.get(value.descriptorId)}>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
const ref = (target: { kind: "atom"; atomId: string } | { kind: "interface"; interfaceRevisionId: string }) =>
|
||||
`QxObjectRef<${q(target.kind === "atom" ? `atom:${target.atomId}` : `interface:${target.interfaceRevisionId}`)}>`;
|
||||
const params = (input: ValueType) => (unit(input) ? "" : `input: ${type(input)}`);
|
||||
const port = (entry: DependencyPort): { type: string; spec: unknown } => {
|
||||
const requirement = entry.requirement;
|
||||
switch (requirement.kind) {
|
||||
case "query": {
|
||||
const query = schema.packages
|
||||
.find((candidate) => candidate.revisionId === requirement.packageRevisionId)
|
||||
?.checkedQueries?.find((query) => query.declaration.id === requirement.queryId);
|
||||
if (!query) throw new Error(`Missing checked query ${requirement.packageRevisionId}:${requirement.queryId}`);
|
||||
return {
|
||||
type: object([
|
||||
["execute", `(variables: ${type(query.variables)}) => Promise<${type(query.output)}>`],
|
||||
...(query.declaration.watch
|
||||
? ([
|
||||
[
|
||||
"watch",
|
||||
`(variables: ${type(query.variables)}, signal: AbortSignal) => AsyncIterable<QxQuerySnapshot<${type(query.output)}>>`,
|
||||
],
|
||||
] as [string, string][])
|
||||
: []),
|
||||
]),
|
||||
spec: {
|
||||
kind: "query",
|
||||
id: entry.id,
|
||||
definitionDigest: query.definitionDigest,
|
||||
variables: query.variables,
|
||||
output: query.output,
|
||||
watch: query.declaration.watch,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "state": {
|
||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`];
|
||||
if (primitive === "write") return ["set", `(value: ${type(requirement.valueType)}) => Promise<void>`];
|
||||
throw new Error(`State primitive ${primitive} is not supported by the TypeScript runtime binding yet`);
|
||||
});
|
||||
if (requirement.primitives.includes("read")) methods.push(["live", "() => Promise<QxLiveValue>"]);
|
||||
return { type: object(methods), spec: { ...requirement, id: entry.id } };
|
||||
}
|
||||
case "edge": {
|
||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "resolve") return [primitive, `() => Promise<Array<${ref(requirement.target)}>>`];
|
||||
if (primitive === "connect" || primitive === "disconnect")
|
||||
return [primitive, `(target: ${ref(requirement.target)}) => Promise<void>`];
|
||||
throw new Error(`Edge primitive ${primitive} is not supported by the TypeScript runtime binding yet`);
|
||||
});
|
||||
if (requirement.primitives.includes("resolve"))
|
||||
methods.push(["collection", `() => Promise<RelationshipCollection<${ref(requirement.target)}>>`]);
|
||||
if (
|
||||
["resolve", "connect", "disconnect"].every((primitive) =>
|
||||
requirement.primitives.includes(primitive as "resolve"),
|
||||
)
|
||||
)
|
||||
methods.push([
|
||||
"replace",
|
||||
`(entries: RelationshipEntry<${ref(requirement.target)}>[], expectedRevision: bigint) => Promise<RelationshipCollection<${ref(requirement.target)}>>`,
|
||||
]);
|
||||
return { type: object(methods), spec: { kind: "edge", id: entry.id, primitives: requirement.primitives } };
|
||||
}
|
||||
case "interface": {
|
||||
const contract = schema.interfaces.find(
|
||||
(candidate) => candidate.revisionId === requirement.interfaceRevisionId,
|
||||
);
|
||||
if (!contract) throw new Error(`Missing imported interface contract ${requirement.interfaceRevisionId}`);
|
||||
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
|
||||
const operations = contract.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
|
||||
);
|
||||
const methods: [string, string][] = [
|
||||
["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })],
|
||||
[
|
||||
"live",
|
||||
object(
|
||||
operations.map((operation) => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<QxLiveValue>`,
|
||||
]),
|
||||
),
|
||||
],
|
||||
...operations.map((operation): [string, string] => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`,
|
||||
]),
|
||||
];
|
||||
return {
|
||||
type: object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]),
|
||||
spec: {
|
||||
kind: "interface",
|
||||
id: entry.id,
|
||||
interfaceRevisionId: requirement.interfaceRevisionId,
|
||||
operations: Object.fromEntries(
|
||||
operations.map(({ name, id, inputType, outputType }) => [name, { id, inputType, outputType }]),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
case "constructor": {
|
||||
const input = requirement.inputType;
|
||||
if (!input)
|
||||
throw new Error(
|
||||
`Constructor port ${entry.id} needs an explicit input contract: add 'input TYPE' after ${requirement.atomId} in QX`,
|
||||
);
|
||||
return {
|
||||
type: object([
|
||||
["construct", `(${params(input)}) => Promise<${ref({ kind: "atom", atomId: requirement.atomId })}>`],
|
||||
]),
|
||||
spec: { kind: "constructor", id: entry.id, inputType: input },
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
const exports = [...pkg.exports].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
||||
const names = new Set<string>();
|
||||
const specs: Record<string, unknown> = {};
|
||||
const contexts: [string, string][] = [];
|
||||
const handlers: [string, string][] = [];
|
||||
const results: [string, string][] = [];
|
||||
for (const entry of exports) {
|
||||
if (names.has(entry.displayName)) throw new Error(`Duplicate export name ${entry.displayName}`);
|
||||
names.add(entry.displayName);
|
||||
const ports = entry.dependencyPorts.map((dependency) => ({ name: dependency.displayName, ...port(dependency) }));
|
||||
if (new Set(ports.map((p) => p.name)).size !== ports.length)
|
||||
throw new Error(`Duplicate dependency name in ${entry.displayName}`);
|
||||
const receiver =
|
||||
entry.kind === "constructor"
|
||||
? ref({ kind: "atom", atomId: entry.constructsAtom })
|
||||
: entry.kind === "operation" && entry.receiverRequirement.kind === "exact-atom"
|
||||
? ref({ kind: "atom", atomId: entry.receiverRequirement.atomId })
|
||||
: entry.kind === "operation" && entry.receiverRequirement.kind === "all-interfaces"
|
||||
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
|
||||
: "QxObjectRef<string>";
|
||||
const contextShape = object([
|
||||
["conform", "QxConformer"],
|
||||
...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]),
|
||||
["input", type(entry.inputType)],
|
||||
["ports", object(ports.map((port) => [port.name, port.type]))],
|
||||
]);
|
||||
contexts.push([
|
||||
entry.displayName,
|
||||
entry.kind === "function"
|
||||
? `${contextShape} & {signal?: AbortSignal}`
|
||||
: `${contextShape} & QxContextLifecycle<${contextShape} & {signal?: AbortSignal}>`,
|
||||
]);
|
||||
const event = entry.kind === "operation" ? entry.eventType : undefined;
|
||||
const contextType = `Contexts[${q(entry.displayName)}]`;
|
||||
const outputType = type(event ?? entry.outputType);
|
||||
results.push([entry.displayName, outputType]);
|
||||
// Watch-start handlers produce events through the runtime's derived stream protocol.
|
||||
if (!entry.application)
|
||||
handlers.push([
|
||||
entry.displayName,
|
||||
event
|
||||
? `QxDerived<${contextType}, ${outputType}>`
|
||||
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
||||
]);
|
||||
specs[entry.displayName] = {
|
||||
...(entry.kind === "function" ? { receiver: "none" } : {}),
|
||||
inputType: entry.inputType,
|
||||
outputType: entry.outputType,
|
||||
...(event ? { eventType: event } : {}),
|
||||
ports: Object.fromEntries(ports.map((port) => [port.name, port.spec])),
|
||||
};
|
||||
}
|
||||
for (const definition of pkg.genericExports ?? []) {
|
||||
handlers.push([
|
||||
definition.displayName,
|
||||
genericImplementationType(
|
||||
definition,
|
||||
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])],
|
||||
type,
|
||||
(requirement) => port({ id: capabilityId.dependencyPort("query"), displayName: "query", requirement }).type,
|
||||
),
|
||||
]);
|
||||
}
|
||||
// Unused imported interfaces must not introduce new message-codec obligations.
|
||||
// A used port still fails above if its own required codec is missing.
|
||||
const hasCodec = (value: ValueType): boolean => {
|
||||
if (value.kind === "message") return !!options.messages?.[value.descriptorId];
|
||||
if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
|
||||
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
|
||||
return true;
|
||||
};
|
||||
const contracts = schema.interfaces
|
||||
.filter((iface) =>
|
||||
iface.members.every((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.every((operation) => hasCodec(operation.inputType) && hasCodec(operation.outputType)),
|
||||
),
|
||||
)
|
||||
.map((iface) => {
|
||||
const generated = port({
|
||||
id: capabilityId.dependencyPort("contract"),
|
||||
displayName: iface.displayName,
|
||||
requirement: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
||||
});
|
||||
const spec = generated.spec as { operations: unknown };
|
||||
return {
|
||||
iface,
|
||||
type: generated.type,
|
||||
code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`,
|
||||
};
|
||||
});
|
||||
const imports = [...messages].map(([id, alias]) => {
|
||||
const binding = options.messages![id]!;
|
||||
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
|
||||
throw new Error(`Invalid message binding export ${binding.export}`);
|
||||
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
|
||||
});
|
||||
const queryVariables = object(
|
||||
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.variables)]),
|
||||
);
|
||||
const queryResults = object(
|
||||
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.output)]),
|
||||
);
|
||||
const queryDescriptors = object(
|
||||
(pkg.checkedQueries ?? []).map((query) => [
|
||||
query.declaration.displayName,
|
||||
`QxQueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root)}>`,
|
||||
]),
|
||||
);
|
||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${queryDescriptors} ${contracts.map((entry) => entry.type).join(" ")}`;
|
||||
const typeImports = [
|
||||
"BindingValue",
|
||||
"QxObjectRef",
|
||||
"QxWatchHandle",
|
||||
"QxHandler",
|
||||
"QxDerived",
|
||||
"QxContextLifecycle",
|
||||
"QxConformer",
|
||||
"QxInterfaceContract",
|
||||
"QxLiveValue",
|
||||
"RelationshipCollection",
|
||||
"RelationshipEntry",
|
||||
"QxQuerySnapshot",
|
||||
"QxQueryDescriptor",
|
||||
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
|
||||
return (
|
||||
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
|
||||
`import { ${contracts.length ? "defineQxInterfaceContract, " : ""}${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
|
||||
imports.join("\n") +
|
||||
`\nexport const packageRevisionId = ${q(pkg.revisionId)};\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 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 contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
|
||||
`export const interfaces = {${contracts
|
||||
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
|
||||
.map((entry) => `${q(entry.iface.displayName)}: contracts[${q(entry.iface.revisionId)}]`)
|
||||
.join(",\n")}};\n` +
|
||||
`const messages = { ${[...messages].map(([id, alias]) => `${q(id)}: ${alias}`).join(", ")} } satisfies QxMessages;\n` +
|
||||
`const specs = ${JSON.stringify(specs, null, 2)} satisfies Record<string, QxHandlerSpec>;\n` +
|
||||
`export const createRuntime = (implementation: Implementation) => ({\n packageRevisionId,\n exports: {\n` +
|
||||
exports
|
||||
.map(
|
||||
(entry) =>
|
||||
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.application ? pkg.genericExports!.find((definition) => definition.id === entry.application!.exportId)!.displayName : entry.displayName)}], messages),`,
|
||||
)
|
||||
.join("\n") +
|
||||
`\n },\n});\n`
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/** One authoring contract, distributed by scaffolding and immutable codegen.
|
||||
* Web Studio checks its concrete exports against this contract during its build. */
|
||||
export const reactPlatformTypes = `// Generated by the Quixos React platform contract. Do not edit.
|
||||
declare module "@quixos/web-studio-react-runtime" {
|
||||
import type * as React from "react";
|
||||
export function useComponentOverlayContainer(): HTMLElement;
|
||||
export function useComponentStyleRoot(): ShadowRoot;
|
||||
export type ObjectRef<AtomId extends string> = string & {
|
||||
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 capability: FieldCapability};
|
||||
/** set(T) uses the resolved capability: native CRDT fields send incremental edits;
|
||||
* 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 LiveFieldProp<T> = ReadableField<T>;
|
||||
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 function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
|
||||
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
||||
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
||||
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
|
||||
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 interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||
export type QueryPartial<T> = T extends QueryReference ? 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 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 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 type ReactComponentHostProps<Action> = {
|
||||
onAction?: (action: Action) => void;
|
||||
fallback?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
/** Optional extra action. The host still displays and reports errors by default. */
|
||||
extraErrorAction?: (error: Error) => void;
|
||||
};
|
||||
export type ReactComponentImplementationProps<CaminoProps, RenderProps, Action> = {
|
||||
camino: CaminoProps;
|
||||
render: RenderProps;
|
||||
dispatch: (action: Action) => void;
|
||||
};
|
||||
export const createWebStudioComponent: <
|
||||
ForObject extends ObjectRef<string>, RenderProps extends object, Action,
|
||||
>(config: { expectedAtomId: string }) => (props: {
|
||||
forObject: ForObject;
|
||||
} & RenderProps & ReactComponentHostProps<Action>) => any;
|
||||
export const invokeCapability: <Result = unknown>(
|
||||
objectId: string,
|
||||
interfaceRevisionId: string,
|
||||
operationId: string,
|
||||
value?: unknown,
|
||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||
) => Promise<Result>;
|
||||
export const h: typeof React.createElement;
|
||||
export function useLiveField<T>(field: ReadableField<T>, options: {write: (value: T) => Promise<void>}): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: WritableField<T>): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: ReadableField<T>): readonly [T];
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { BindingSchema } from "./index.js";
|
||||
import type { ValueType } from "../capability-model/types.js";
|
||||
|
||||
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
||||
export function generateReactBindings(
|
||||
schema: BindingSchema,
|
||||
packageRevisionId: string,
|
||||
propsExports: readonly string[],
|
||||
components: readonly { module: string; propsExport: string }[] = [],
|
||||
) {
|
||||
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
|
||||
if (!pkg) throw new Error(`Unknown package ${packageRevisionId}`);
|
||||
const q = JSON.stringify;
|
||||
const references = new Map<string, string>();
|
||||
const declarations: string[] = [];
|
||||
const type = (value: ValueType): string => {
|
||||
switch (value.kind) {
|
||||
case "builtin":
|
||||
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
|
||||
return "null";
|
||||
case "scalar":
|
||||
return {
|
||||
string: "string",
|
||||
bool: "boolean",
|
||||
bytes: "Uint8Array",
|
||||
int64: "bigint",
|
||||
uint64: "bigint",
|
||||
int32: "number",
|
||||
uint32: "number",
|
||||
double: "number",
|
||||
}[value.name];
|
||||
case "optional":
|
||||
return `(${type(value.value)} | null)`;
|
||||
case "list":
|
||||
return `Array<${type(value.value)}>`;
|
||||
case "record":
|
||||
return `{${Object.entries(value.fields)
|
||||
.map(([name, field]) => `${q(name)}: ${type(field)}`)
|
||||
.join(";")}}`;
|
||||
case "message":
|
||||
throw new Error(`React props require checked values, not opaque message ${value.descriptorId}`);
|
||||
case "object-ref": {
|
||||
if (value.expectation.kind === "atom") return `ObjectRef<${q(value.expectation.atomId)}>`;
|
||||
const id = value.expectation.interfaceRevisionId;
|
||||
const existing = references.get(id);
|
||||
if (existing) return existing;
|
||||
const name = `Interface${references.size}`;
|
||||
references.set(id, name);
|
||||
const iface = schema.interfaces.find((entry) => entry.revisionId === id);
|
||||
if (!iface) throw new Error(`Missing React reference contract ${id}`);
|
||||
const fields = iface.members.flatMap((member) => {
|
||||
if (member.kind === "operation") return [];
|
||||
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
|
||||
if (!get) return [];
|
||||
const writable = member.kind === "value" && member.operations.some((op) => op.displayName === "set");
|
||||
return [`${q(member.displayName)}: ${writable ? "WritableField" : "ReadableField"}<${type(get.outputType)}>`];
|
||||
});
|
||||
declarations.push(`export type ${name} = InterfaceReference<${q(id)}, {${fields.join(";")}}> ;`);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Only exports explicitly selected as props are projected. Non-props exports
|
||||
// may legitimately use opaque messages or types unrelated to the browser.
|
||||
const selected = propsExports.map((name) => {
|
||||
const entry = pkg.exports.find((candidate) => candidate.displayName === name);
|
||||
if (!entry) throw new Error(`Unknown React props export ${name}`);
|
||||
return entry;
|
||||
});
|
||||
const results = selected.map(
|
||||
(entry) =>
|
||||
`${q(entry.displayName)}: ${type(entry.kind === "operation" ? (entry.eventType ?? entry.outputType) : entry.outputType)}`,
|
||||
);
|
||||
const checks = components.map((component, i) => {
|
||||
if (!propsExports.includes(component.propsExport))
|
||||
throw new Error(`Component refers to unselected props export ${component.propsExport}`);
|
||||
if (!component.module.startsWith("."))
|
||||
throw new Error("React component check must name a local module relative to generated bindings");
|
||||
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
|
||||
});
|
||||
// Emit optional discovery descriptors only when the browser has all codecs.
|
||||
// Unrelated opaque-message imports must not break an otherwise checked UI.
|
||||
const browserValue = (value: ValueType, seen: Set<string>): boolean => {
|
||||
if (value.kind === "message") return false;
|
||||
if (value.kind === "builtin") return value.name === "unit";
|
||||
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 === "object-ref" && value.expectation.kind === "interface") {
|
||||
const id = value.expectation.interfaceRevisionId;
|
||||
if (seen.has(id)) return true;
|
||||
const contract = schema.interfaces.find((entry) => entry.revisionId === id);
|
||||
return !!contract && browserInterface(contract, new Set([...seen, id]));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const browserInterface = (iface: (typeof schema.interfaces)[number], seen: Set<string>): boolean =>
|
||||
iface.members.every((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call" && op.scope !== "class")
|
||||
.every((op) => browserValue(op.inputType, seen) && browserValue(op.outputType, seen)),
|
||||
);
|
||||
const contracts = schema.interfaces
|
||||
.filter((iface) => browserInterface(iface, new Set([iface.revisionId])))
|
||||
.map((iface) => {
|
||||
const reference = type({
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
||||
});
|
||||
const calls = iface.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.map(
|
||||
(operation) =>
|
||||
`${q(`${member.displayName}.${operation.displayName}`)}: (${operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "" : `input: ${type(operation.inputType)}`}) => Promise<${type(operation.outputType)}>`,
|
||||
),
|
||||
);
|
||||
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
||||
});
|
||||
const queryType = (value: ValueType): string => {
|
||||
if (value.kind === "object-ref")
|
||||
return `QueryReference<${q(value.expectation.kind === "atom" ? value.expectation.atomId : value.expectation.interfaceRevisionId)}>`;
|
||||
if (value.kind === "optional") return `(${queryType(value.value)} | null)`;
|
||||
if (value.kind === "list") return `Array<${queryType(value.value)}>`;
|
||||
if (value.kind === "record")
|
||||
return `{${Object.entries(value.fields)
|
||||
.map(([name, field]) => `${q(name)}: ${queryType(field)}`)
|
||||
.join(";")}}`;
|
||||
return type(value);
|
||||
};
|
||||
const queries = pkg.checkedQueries ?? [];
|
||||
const queryCode =
|
||||
`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)}: ${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(
|
||||
Object.fromEntries(
|
||||
queries.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,
|
||||
},
|
||||
]),
|
||||
),
|
||||
)};\n`;
|
||||
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` +
|
||||
queryCode +
|
||||
`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 reactInterfaces = {${contracts
|
||||
.filter(({ iface }) => contracts.filter((other) => other.iface.displayName === iface.displayName).length === 1)
|
||||
.map(({ iface }) => `${q(iface.displayName)}: reactContracts[${q(iface.revisionId)}]`)
|
||||
.join(",\n")}};\n` +
|
||||
(checks.length
|
||||
? `type CheckedComponent<K extends keyof ReactResults, C extends (props: {camino: ReactResults[K]; render: any; dispatch: (action: any) => void}) => unknown> = C;\n${checks.join("\n")}\n`
|
||||
: "")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { loadQxSources, resolveQxSources } from "./source-loader.js";
|
||||
import { compileRepositoryQuery } from "../query/compile.js";
|
||||
import { checkQueryTemplate } from "../query/templates.js";
|
||||
import { readRepositorySource } from "./source-loader.js";
|
||||
import { linkQueries } from "../query/link.js";
|
||||
import {
|
||||
capabilityId,
|
||||
compileWorkspaceRevision,
|
||||
validateMigrationCatalog,
|
||||
contentDigest,
|
||||
type AtomDefinition,
|
||||
type CompiledWorkspaceRevision,
|
||||
type InterfaceRevision,
|
||||
type PackageRevision,
|
||||
type SourceRevision,
|
||||
type WorkspaceRevision,
|
||||
} from "../capability-model/index.js";
|
||||
import {
|
||||
loadQuixosLock,
|
||||
type GitSource,
|
||||
type LockedResource,
|
||||
type QuixosRepositoryLock,
|
||||
} from "../resource-lock/index.js";
|
||||
import {
|
||||
compileCapabilityResourceSource,
|
||||
compileCapabilitySource,
|
||||
type CapabilityImportEnvironment,
|
||||
type CapabilityResource,
|
||||
type CapabilityResourceImport,
|
||||
} from "./parser.js";
|
||||
|
||||
export type CapabilityRepositorySnapshot = {
|
||||
directory: string;
|
||||
};
|
||||
|
||||
export type CapabilityRepositoryResolver = (
|
||||
source: GitSource,
|
||||
kind: LockedResource["kind"],
|
||||
) => Promise<CapabilityRepositorySnapshot>;
|
||||
|
||||
export type ResolvedCapabilityResource = {
|
||||
key: string;
|
||||
kind: LockedResource["kind"];
|
||||
source: GitSource;
|
||||
directory: string;
|
||||
lock: QuixosRepositoryLock;
|
||||
resource: CapabilityResource;
|
||||
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
export type CompiledWorkspaceRepository = {
|
||||
workspace: WorkspaceRevision;
|
||||
plan: CompiledWorkspaceRevision;
|
||||
lock: QuixosRepositoryLock;
|
||||
resources: ResolvedCapabilityResource[];
|
||||
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
export type CompiledCapabilityResourceRepository = {
|
||||
resource: CapabilityResource;
|
||||
lock: QuixosRepositoryLock;
|
||||
resources: ResolvedCapabilityResource[];
|
||||
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
const sourceKey = (kind: LockedResource["kind"], source: GitSource) =>
|
||||
`${kind}\0${source.repository}\0${source.commit.toLowerCase()}`;
|
||||
|
||||
const bindingKey = (kind: LockedResource["kind"], binding: string) => `${kind}\0${binding}`;
|
||||
|
||||
const importsKey = (entry: CapabilityResourceImport | LockedResource) => bindingKey(entry.kind, entry.binding);
|
||||
|
||||
const assertImportsMatchLock = (
|
||||
label: string,
|
||||
imports: readonly CapabilityResourceImport[],
|
||||
resources: readonly LockedResource[],
|
||||
) => {
|
||||
const authored = [...imports].map(importsKey).sort();
|
||||
const locked = [...resources].map(importsKey).sort();
|
||||
if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) {
|
||||
throw new Error(
|
||||
`${label} imports do not match quixos.lock:\n` +
|
||||
`authored: ${authored.join(", ") || "none"}\n` +
|
||||
`locked: ${locked.join(", ") || "none"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const exactRevisions = <
|
||||
Revision extends {
|
||||
revisionId: string;
|
||||
source: SourceRevision;
|
||||
},
|
||||
>(
|
||||
revisions: readonly Revision[],
|
||||
) => {
|
||||
const seen = new Set<string>();
|
||||
return revisions.filter((revision) => {
|
||||
const key = `${revision.revisionId}\0${revision.source.repository}\0${revision.source.commit}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const exactAtoms = (atoms: readonly AtomDefinition[]) => {
|
||||
const seen = new Set<string>();
|
||||
return atoms.filter((atom) => {
|
||||
if (seen.has(atom.id)) return false;
|
||||
seen.add(atom.id);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const diagnosticsMessage = (
|
||||
label: string,
|
||||
diagnostics: readonly {
|
||||
fileName: string;
|
||||
line: number;
|
||||
column: number;
|
||||
phase: string;
|
||||
code: string;
|
||||
message: string;
|
||||
path?: string;
|
||||
}[],
|
||||
) =>
|
||||
`${label} did not compile:\n${diagnostics
|
||||
.map((entry) => {
|
||||
const location =
|
||||
entry.line > 0
|
||||
? `${entry.fileName}:${entry.line}:${entry.column + 1}`
|
||||
: `${entry.fileName}${entry.path ? `:${entry.path}` : ""}`;
|
||||
return `${location}: ${entry.phase} ${entry.code}: ${entry.message}`;
|
||||
})
|
||||
.join("\n")}`;
|
||||
|
||||
const revisionFor = (node: ResolvedCapabilityResource) => node.resource.revision;
|
||||
|
||||
const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): ResolvedCapabilityResource[] => {
|
||||
const result: ResolvedCapabilityResource[] = [];
|
||||
const seen = new Set<string>();
|
||||
const visit = (node: ResolvedCapabilityResource) => {
|
||||
if (seen.has(node.key)) return;
|
||||
seen.add(node.key);
|
||||
for (const dependency of node.dependencies.values()) visit(dependency);
|
||||
result.push(node);
|
||||
};
|
||||
for (const root of roots) visit(root);
|
||||
return result;
|
||||
};
|
||||
|
||||
const environmentFor = (
|
||||
direct: readonly [LockedResource, ResolvedCapabilityResource][],
|
||||
closure: readonly ResolvedCapabilityResource[],
|
||||
): CapabilityImportEnvironment => ({
|
||||
interfaces: new Map(
|
||||
direct.flatMap(([locked, node]) =>
|
||||
node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [],
|
||||
),
|
||||
),
|
||||
packages: new Map(
|
||||
direct.flatMap(([locked, node]) =>
|
||||
node.resource.kind === "package" ? [[locked.binding, node.resource.revision] as const] : [],
|
||||
),
|
||||
),
|
||||
interfaceClosure: exactRevisions(
|
||||
closure.flatMap((node) => [
|
||||
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
||||
...(node.resource.specializations ?? []),
|
||||
]),
|
||||
),
|
||||
packageClosure: exactRevisions(
|
||||
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
||||
),
|
||||
externalAtoms: exactAtoms(closure.flatMap((node) => node.resource.externalAtoms)),
|
||||
});
|
||||
|
||||
const createResourceGraphResolver = (quixosCommit: string, resolveResource: CapabilityRepositoryResolver) => {
|
||||
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
|
||||
const active: string[] = [];
|
||||
|
||||
const visit = async (
|
||||
locked: LockedResource,
|
||||
suppliedSnapshot?: CapabilityRepositorySnapshot,
|
||||
): Promise<ResolvedCapabilityResource> => {
|
||||
const key = sourceKey(locked.kind, locked.source);
|
||||
const cached = resolved.get(key);
|
||||
if (cached) {
|
||||
if (active.includes(key)) {
|
||||
throw new Error(`Resource dependency cycle: ${[...active, key].join(" -> ")}`);
|
||||
}
|
||||
return await cached;
|
||||
}
|
||||
const pending = (async () => {
|
||||
active.push(key);
|
||||
try {
|
||||
const snapshot = suppliedSnapshot ?? (await resolveResource(locked.source, locked.kind));
|
||||
const lockResult = await loadQuixosLock(path.join(snapshot.directory, "quixos.lock"));
|
||||
if (!lockResult.ok) {
|
||||
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding} lock`, lockResult.diagnostics));
|
||||
}
|
||||
if (lockResult.lock.quixos.policy) {
|
||||
throw new Error(
|
||||
`${locked.kind} ${locked.binding} lock declares workspace Quixos policy ` +
|
||||
`${lockResult.lock.quixos.policy}; resource locks may only declare their exact authored-against commit`,
|
||||
);
|
||||
}
|
||||
if (lockResult.lock.quixos.commit.toLowerCase() !== quixosCommit.toLowerCase()) {
|
||||
throw new Error(
|
||||
`${locked.kind} ${locked.binding} selects Quixos ${lockResult.lock.quixos.commit}, ` +
|
||||
`but the repository graph selects ${quixosCommit}`,
|
||||
);
|
||||
}
|
||||
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
|
||||
for (const dependency of lockResult.lock.resources) {
|
||||
directPairs.push([dependency, await visit(dependency)]);
|
||||
}
|
||||
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
|
||||
const environment = environmentFor(directPairs, dependencyClosure);
|
||||
const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx";
|
||||
const manifestPath = path.join(snapshot.directory, manifestName);
|
||||
const sourceText = await readFile(manifestPath, "utf8");
|
||||
const compiled = compileCapabilityResourceSource(sourceText, {
|
||||
source: locked.source,
|
||||
fileName: manifestPath,
|
||||
environment,
|
||||
});
|
||||
if (!compiled.ok) {
|
||||
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding}`, compiled.diagnostics));
|
||||
}
|
||||
if (compiled.resource.kind !== locked.kind) {
|
||||
throw new Error(`${manifestPath} declares ${compiled.resource.kind}, not ${locked.kind}`);
|
||||
}
|
||||
assertImportsMatchLock(manifestPath, compiled.resource.imports, lockResult.lock.resources);
|
||||
if (compiled.resource.kind === "package") {
|
||||
const queryInterfaces = [
|
||||
...(environment.interfaceClosure ?? []),
|
||||
...(compiled.resource.specializations ?? []),
|
||||
];
|
||||
for (const template of compiled.resource.revision.queryTemplates ?? [])
|
||||
await checkQueryTemplate(template, queryInterfaces, (name) =>
|
||||
readRepositorySource(snapshot.directory, name),
|
||||
);
|
||||
if (compiled.resource.revision.queries?.length)
|
||||
compiled.resource.revision.checkedQueries = await Promise.all(
|
||||
compiled.resource.revision.queries.map((query) =>
|
||||
compileRepositoryQuery(snapshot.directory, query, queryInterfaces),
|
||||
),
|
||||
);
|
||||
let catalogText: string | undefined;
|
||||
try {
|
||||
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (catalogText !== undefined) {
|
||||
const catalog = validateMigrationCatalog(
|
||||
JSON.parse(catalogText),
|
||||
new Set(compiled.resource.revision.exports.map((entry) => entry.id)),
|
||||
);
|
||||
const root = await realpath(snapshot.directory);
|
||||
for (const migration of catalog.migrations) {
|
||||
const implementation = await realpath(path.join(root, migration.implementation.file));
|
||||
if (!implementation.startsWith(`${root}${path.sep}`))
|
||||
throw new Error("Migration implementation escapes its package");
|
||||
if (contentDigest(await readFile(implementation, "utf8")) !== migration.implementation.digest)
|
||||
throw new Error(`Migration implementation digest mismatch: ${migration.id}`);
|
||||
}
|
||||
compiled.resource.revision.migrationCatalog = catalog;
|
||||
}
|
||||
}
|
||||
return {
|
||||
key,
|
||||
kind: locked.kind,
|
||||
source: locked.source,
|
||||
directory: snapshot.directory,
|
||||
lock: lockResult.lock,
|
||||
resource: compiled.resource,
|
||||
dependencies: new Map(
|
||||
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
active.pop();
|
||||
}
|
||||
})();
|
||||
resolved.set(key, pending);
|
||||
return await pending;
|
||||
};
|
||||
|
||||
return {
|
||||
visit,
|
||||
nodes: async () => await Promise.all(resolved.values()),
|
||||
};
|
||||
};
|
||||
|
||||
export const compileCapabilityResourceRepository = async (options: {
|
||||
rootDirectory: string;
|
||||
kind: LockedResource["kind"];
|
||||
source: GitSource;
|
||||
resolveResource: CapabilityRepositoryResolver;
|
||||
}): Promise<CompiledCapabilityResourceRepository> => {
|
||||
const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
|
||||
if (!lockResult.ok) {
|
||||
throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics));
|
||||
}
|
||||
const resolver = createResourceGraphResolver(lockResult.lock.quixos.commit, options.resolveResource);
|
||||
const root = await resolver.visit(
|
||||
{
|
||||
kind: options.kind,
|
||||
binding: "<root>",
|
||||
source: options.source,
|
||||
},
|
||||
{ directory: options.rootDirectory },
|
||||
);
|
||||
return {
|
||||
resource: root.resource,
|
||||
lock: root.lock,
|
||||
resources: await resolver.nodes(),
|
||||
directResources: root.dependencies,
|
||||
};
|
||||
};
|
||||
|
||||
export const compileWorkspaceRepository = async (options: {
|
||||
rootDirectory: string;
|
||||
resolveResource: CapabilityRepositoryResolver;
|
||||
workspaceId?: string;
|
||||
workspaceRevisionId?: string;
|
||||
sourceRootCommit?: string;
|
||||
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
|
||||
readSource?: (name: string) => Promise<string>;
|
||||
}): Promise<CompiledWorkspaceRepository> => {
|
||||
const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
|
||||
if (!rootLockResult.ok) {
|
||||
throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics));
|
||||
}
|
||||
const rootLock = rootLockResult.lock;
|
||||
const resolver = createResourceGraphResolver(rootLock.quixos.commit, options.resolveResource);
|
||||
|
||||
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
|
||||
for (const locked of rootLock.resources) {
|
||||
directPairs.push([locked, await resolver.visit(locked)]);
|
||||
}
|
||||
const nodes = await resolver.nodes();
|
||||
const environment = environmentFor(directPairs, nodes);
|
||||
const workspacePath = path.join(options.rootDirectory, "workspace.qx");
|
||||
const sources = options.readSource
|
||||
? await resolveQxSources(options.readSource)
|
||||
: await loadQxSources(options.rootDirectory);
|
||||
const compiled = compileCapabilitySource(sources.source, workspacePath, environment);
|
||||
if (!compiled.ok) {
|
||||
throw new Error(
|
||||
diagnosticsMessage(
|
||||
"Workspace",
|
||||
compiled.diagnostics.map((diagnostic) =>
|
||||
diagnostic.line > 0
|
||||
? { ...diagnostic, ...sources.originalPosition(diagnostic.line, diagnostic.column) }
|
||||
: diagnostic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
|
||||
const availableInterfaceIds = new Set(
|
||||
nodes.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision.revisionId] : [])),
|
||||
);
|
||||
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
|
||||
for (const node of nodes) {
|
||||
for (const requirement of node.resource.externalInterfaces) {
|
||||
if (!availableInterfaceIds.has(requirement.revisionId)) {
|
||||
throw new Error(
|
||||
`${node.kind} ${node.resource.revision.displayName} requires external interface ` +
|
||||
`${requirement.binding} (${requirement.revisionId}), but the workspace resource graph does not provide it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const atom of node.resource.externalAtoms) {
|
||||
if (!availableAtomIds.has(atom.id)) {
|
||||
throw new Error(
|
||||
`${node.kind} ${node.resource.revision.displayName} requires external atom ` +
|
||||
`${atom.displayName} (${atom.id}), but the workspace does not define it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const workspace: WorkspaceRevision = {
|
||||
...compiled.workspace,
|
||||
...(options.workspaceId ? { workspaceId: capabilityId.workspace(options.workspaceId) } : {}),
|
||||
...(options.workspaceRevisionId
|
||||
? { id: capabilityId.workspaceRevision(options.workspaceRevisionId) }
|
||||
: options.sourceRootCommit
|
||||
? {
|
||||
id: capabilityId.workspaceRevision(
|
||||
`workspace-revision:${options.workspaceId ?? compiled.workspace.workspaceId}:${options.sourceRootCommit}`,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}),
|
||||
};
|
||||
const checked = compileWorkspaceRevision(workspace);
|
||||
if (!checked.ok) {
|
||||
throw new Error(
|
||||
`Instantiated workspace did not compile:\n${checked.issues
|
||||
.map((entry) => `${entry.path}: ${entry.message}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
const linkedQueries = linkQueries(workspace);
|
||||
if (linkedQueries.length) {
|
||||
workspace.linkedQueries = linkedQueries;
|
||||
checked.plan.source.linkedQueries = structuredClone(linkedQueries);
|
||||
}
|
||||
return {
|
||||
workspace,
|
||||
plan: checked.plan,
|
||||
lock: rootLock,
|
||||
resources: nodes,
|
||||
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { appendFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import type { convergeAuthoring } from "./authoring-converge.js";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js";
|
||||
import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../capability-model/index.js";
|
||||
|
||||
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
|
||||
export async function checkAuthoring(
|
||||
start: string,
|
||||
output: string,
|
||||
options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {},
|
||||
) {
|
||||
const started = performance.now();
|
||||
const timings: Record<string, number> = {};
|
||||
const context = await authoringContext(start);
|
||||
const location = await fs.realpath(start);
|
||||
const directory = location === context.workbench ? "root" : path.relative(context.workbench, location);
|
||||
const resource = context.resources.find((entry) => entry.directory === directory);
|
||||
if (!resource) throw new Error("Run check from a registered repository root or the workbench");
|
||||
await fs.mkdir(output, { mode: 0o700 });
|
||||
const report: {
|
||||
directory: string;
|
||||
checker: string;
|
||||
candidateOnly: true;
|
||||
activationEvidence: false;
|
||||
commit?: string;
|
||||
artifactPath?: string;
|
||||
blockers: string[];
|
||||
phase: string;
|
||||
output: string;
|
||||
compilation?: "passed";
|
||||
activationReadiness?: "preserve" | "migration-required" | "blocked";
|
||||
migrationRequired?: string[];
|
||||
} = {
|
||||
directory,
|
||||
checker: checkerIdentity(),
|
||||
candidateOnly: true,
|
||||
activationEvidence: false,
|
||||
blockers: [],
|
||||
phase: "convergence",
|
||||
output,
|
||||
};
|
||||
const progress = async (running = true) => {
|
||||
const file = path.join(output, "report.json"),
|
||||
temp = `${file}.tmp`;
|
||||
await fs.writeFile(temp, JSON.stringify({ ...report, timings, running }, null, 2));
|
||||
await fs.rename(temp, file);
|
||||
};
|
||||
await progress();
|
||||
try {
|
||||
console.error(`[${new Date().toISOString()}] Check: capture source and converge dependencies`);
|
||||
// Serialize only source capture, not the potentially slow Nix build.
|
||||
// Repository-scoped agents can check separate immutable candidates in parallel.
|
||||
const capture = promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_TRACE_CAPTURE: "1" },
|
||||
});
|
||||
console.error(`Capture details: ${path.join(output, "capture.log")}`);
|
||||
capture.child.stderr?.on("data", (chunk) => appendFileSync(path.join(output, "capture.log"), chunk));
|
||||
const captured = await capture.catch((error) => {
|
||||
if (typeof error.stdout === "string" && error.stdout.trim().startsWith("{")) return { stdout: error.stdout };
|
||||
throw error;
|
||||
});
|
||||
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
|
||||
timings.captureMs = Math.round(performance.now() - started);
|
||||
console.error(`[${new Date().toISOString()}] Check: source captured in ${(timings.captureMs / 1000).toFixed(1)}s`);
|
||||
if (!converged.candidate) {
|
||||
report.phase = converged.worklist.find((entry) => entry.phase !== "dependency")?.phase ?? "convergence";
|
||||
throw new Error(
|
||||
converged.worklist.map((entry) => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"),
|
||||
);
|
||||
}
|
||||
report.commit = converged.candidate.commit;
|
||||
report.phase = "verification";
|
||||
await progress();
|
||||
const buildStarted = performance.now();
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Check: immutable Nix ${options.contractOnly ? "contract" : "verification"}; build output: ${path.join(output, "nix.log")}`,
|
||||
);
|
||||
try {
|
||||
report.artifactPath = await buildImmutableCandidate(
|
||||
converged.candidate,
|
||||
resource.kind,
|
||||
path.join(output, "nix.log"),
|
||||
options.contractOnly,
|
||||
);
|
||||
} finally {
|
||||
timings.immutableCheckMs = Math.round(performance.now() - buildStarted);
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Check: immutable phase ended after ${(timings.immutableCheckMs / 1000).toFixed(1)}s`,
|
||||
);
|
||||
}
|
||||
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
||||
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
||||
report.compilation = "passed";
|
||||
if (resource.kind === "workspace" && !options.contractOnly) {
|
||||
report.phase = "evolution";
|
||||
await progress();
|
||||
let baseline = options.baseline;
|
||||
if (!baseline) {
|
||||
try {
|
||||
const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8"));
|
||||
if ((await fs.realpath(host.workbenchRoot)) === context.workbench)
|
||||
baseline = JSON.parse(
|
||||
await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8"),
|
||||
).workspacePlanPath;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
const before = baseline ? (JSON.parse(await fs.readFile(baseline, "utf8")) as WorkspaceRevision) : null;
|
||||
const reviews = options.reviews
|
||||
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
|
||||
: [];
|
||||
const evolution = planEvolution(before, JSON.parse(candidateText), { reviews });
|
||||
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
|
||||
report.blockers.push(...evolution.blockers);
|
||||
report.migrationRequired = evolution.migrationRequired;
|
||||
report.activationReadiness = evolution.blockers.length
|
||||
? "blocked"
|
||||
: evolution.migrationRequired.length
|
||||
? "migration-required"
|
||||
: "preserve";
|
||||
if (evolution.migrationRequired.length)
|
||||
report.blockers.push(
|
||||
`Explicit migration required for: ${evolution.migrationRequired.join(", ")}. Compilation passed; supply a migration path before cutover.`,
|
||||
);
|
||||
}
|
||||
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
|
||||
} catch (error) {
|
||||
report.blockers.push(String(error instanceof Error ? error.message : error));
|
||||
}
|
||||
timings.totalMs = Math.round(performance.now() - started);
|
||||
Object.assign(report, { timings });
|
||||
await progress(false);
|
||||
if (options.contractOnly) return report;
|
||||
const records = path.join(context.workbench, ".quixos/checks");
|
||||
await fs.mkdir(records, { recursive: true });
|
||||
const remember = async (value: typeof report) => {
|
||||
const filename = path.join(records, checkRecordName(value.directory)),
|
||||
temporary = `${filename}.${randomUUID()}.tmp`;
|
||||
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
|
||||
await fs.rename(temporary, filename);
|
||||
};
|
||||
if (report.artifactPath) {
|
||||
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
|
||||
for (const checked of graph.resources) {
|
||||
const managed = context.resources.find(
|
||||
(entry) => entry.kind === checked.kind && entry.source?.repository === checked.source.repository,
|
||||
);
|
||||
if (managed && managed.directory !== directory)
|
||||
await remember({
|
||||
...report,
|
||||
directory: managed.directory,
|
||||
commit: checked.source.commit,
|
||||
blockers: [],
|
||||
phase: "checked",
|
||||
});
|
||||
}
|
||||
}
|
||||
await remember(report);
|
||||
return report;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
|
||||
|
||||
export type AuthoringResource = {
|
||||
kind: "workspace" | "interface" | "package";
|
||||
directory: string;
|
||||
resourceId?: string;
|
||||
source?: GitSource;
|
||||
};
|
||||
|
||||
/** Registration, not directory co-location, defines the editable selection. */
|
||||
export async function authoringContext(start: string) {
|
||||
let workbench = await realpath(start);
|
||||
for (;;) {
|
||||
let text: string | undefined;
|
||||
try {
|
||||
text = await readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (text !== undefined) {
|
||||
const graph = JSON.parse(text) as { resources: AuthoringResource[] };
|
||||
if (!Array.isArray(graph.resources)) throw new Error("Managed resource inventory is malformed");
|
||||
const resources: AuthoringResource[] = [{ kind: "workspace", directory: "root" }];
|
||||
const identities = new Set<string>(),
|
||||
directories = new Set<string>(["root"]);
|
||||
for (const entry of graph.resources) {
|
||||
// Compiler graphs carry resolved paths; the authoring API presents
|
||||
// stable workbench-relative names and validates containment here.
|
||||
if (typeof entry.directory === "string" && path.isAbsolute(entry.directory))
|
||||
entry.directory = path.relative(workbench, entry.directory);
|
||||
if (
|
||||
!["interface", "package"].includes(entry.kind) ||
|
||||
!entry.source ||
|
||||
!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory)
|
||||
) {
|
||||
throw new Error("Invalid managed resource registration");
|
||||
}
|
||||
const identity = `${entry.kind}\0${entry.resourceId ?? entry.source.repository}`;
|
||||
if (identities.has(identity) || directories.has(entry.directory)) {
|
||||
throw new Error(`Multiple editable selections for ${entry.resourceId ?? entry.source.repository}`);
|
||||
}
|
||||
identities.add(identity);
|
||||
directories.add(entry.directory);
|
||||
resources.push({
|
||||
kind: entry.kind,
|
||||
directory: entry.directory,
|
||||
resourceId: entry.resourceId,
|
||||
source: entry.source,
|
||||
});
|
||||
}
|
||||
return {
|
||||
workbench,
|
||||
resources,
|
||||
async baseline() {
|
||||
const result = await loadQuixosLock(path.join(workbench, "root/quixos.lock"));
|
||||
if (!result.ok)
|
||||
throw new Error(
|
||||
`Workspace source baseline is invalid: ${result.diagnostics.map((d) => d.message).join("; ")}`,
|
||||
);
|
||||
return result.lock.quixos;
|
||||
},
|
||||
};
|
||||
}
|
||||
const parent = path.dirname(workbench);
|
||||
if (parent === workbench) throw new Error("Not in a managed workbench; select one with --workbench DIRECTORY");
|
||||
workbench = parent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { readFile, writeFile, rename, rm, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { snapshotCommit } from "./checked-build.js";
|
||||
import {
|
||||
loadQuixosLock,
|
||||
parseQuixosLockDocument,
|
||||
formatQuixosLockDocument,
|
||||
retentionTagForCommit,
|
||||
type GitSource,
|
||||
} from "../resource-lock/index.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
const command = async (cwd: string, executable: string, args: string[]) => {
|
||||
const start = performance.now();
|
||||
// Do not log arguments: transports may contain credentials. Source identities
|
||||
// remain in the normal checked result, not in this timing channel.
|
||||
const label = `${path.basename(cwd)} ${executable} ${args[0]}`;
|
||||
try {
|
||||
return (
|
||||
await execFile(executable, args, {
|
||||
cwd,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0", GIT_TERMINAL_PROMPT: "0" },
|
||||
})
|
||||
).stdout.trim();
|
||||
} finally {
|
||||
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
|
||||
console.error(`[${new Date().toISOString()}] Capture: ${label}: ${Math.round(performance.now() - start)}ms`);
|
||||
}
|
||||
};
|
||||
const identity = (kind: string, repository: string) => `${kind}\0${repository}`;
|
||||
|
||||
export type AuthoringBlocker = {
|
||||
directory: string;
|
||||
phase: "resolution" | "dependency" | "source" | "publication" | "concurrent-edit";
|
||||
message: string;
|
||||
};
|
||||
|
||||
/** Source retention only. Neither successful convergence nor an empty source
|
||||
* worklist grants typechecking, semantic review or activation approval.
|
||||
* Caller serializes coordinators; package authors may still be editing. */
|
||||
export async function convergeAuthoring(start: string, target = "root") {
|
||||
const context = await authoringContext(start);
|
||||
const graphFile = path.join(context.workbench, ".quixos/resource-graph.json");
|
||||
const graphBefore = await readFile(graphFile, "utf8");
|
||||
const blockers: AuthoringBlocker[] = [];
|
||||
const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>();
|
||||
const selected = new Map<string, string>();
|
||||
for (const entry of context.resources) {
|
||||
const root = path.join(context.workbench, entry.directory);
|
||||
let repository = entry.source?.repository;
|
||||
try {
|
||||
if ((await realpath(root)) !== root) throw new Error(`Managed checkout crosses a symlink: ${entry.directory}`);
|
||||
// Transport rewrites must not become committed source identities.
|
||||
const origin = await command(root, "git", ["config", "--get", "remote.origin.url"]);
|
||||
if (repository && origin !== repository)
|
||||
throw new Error(`Origin differs from registered source for ${entry.directory}`);
|
||||
repository ??= origin;
|
||||
} catch (error) {
|
||||
blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) });
|
||||
if (!repository) throw error; // The root has no separate registered source.
|
||||
}
|
||||
const key = identity(entry.kind, repository);
|
||||
if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`);
|
||||
selected.set(key, entry.directory);
|
||||
nodes.set(entry.directory, {
|
||||
...entry,
|
||||
source: { resolver: "git", repository, commit: entry.source?.commit ?? "" },
|
||||
dependencies: [],
|
||||
});
|
||||
}
|
||||
for (const node of nodes.values()) {
|
||||
try {
|
||||
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"));
|
||||
node.dependencies = [
|
||||
...new Set(
|
||||
lock.lock.resources.flatMap((entry) => {
|
||||
const directory = selected.get(identity(entry.kind, entry.source.repository));
|
||||
return directory ? [directory] : [];
|
||||
}),
|
||||
),
|
||||
];
|
||||
} catch (error) {
|
||||
blockers.push({ directory: node.directory, phase: "resolution", message: String(error) });
|
||||
}
|
||||
}
|
||||
const complete = new Map<string, GitSource>(),
|
||||
active = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
|
||||
const visit = async (directory: string): Promise<boolean> => {
|
||||
visited.add(directory);
|
||||
if (complete.has(directory)) return true;
|
||||
if (blockers.some((entry) => entry.directory === directory)) return false;
|
||||
if (active.has(directory)) {
|
||||
blockers.push({
|
||||
directory,
|
||||
phase: "dependency",
|
||||
message: `Source dependency cycle: ${[...active, directory].join(" -> ")}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
active.add(directory);
|
||||
const node = nodes.get(directory)!;
|
||||
for (const dependency of node.dependencies)
|
||||
if (!(await visit(dependency))) {
|
||||
blockers.push({ directory, phase: "dependency", message: `Waiting for ${dependency}` });
|
||||
active.delete(directory);
|
||||
return false;
|
||||
}
|
||||
const root = path.join(context.workbench, directory);
|
||||
let phase: AuthoringBlocker["phase"] = "source";
|
||||
try {
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error("Lock changed during convergence; retry after joining writers");
|
||||
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
|
||||
const filename = path.join(root, file),
|
||||
before = await readFile(filename, "utf8");
|
||||
const parsed = parseQuixosLockDocument(before, file);
|
||||
if (!parsed.ok) throw new Error(`Invalid lock ${file}`);
|
||||
let changed = false;
|
||||
for (const dependency of parsed.document.resources) {
|
||||
const target = selected.get(identity(dependency.kind, dependency.source.repository));
|
||||
const source = target ? complete.get(target) : undefined;
|
||||
if (target && !source)
|
||||
throw new Error(`Dependencies changed during convergence (${dependency.binding}); join writers and retry`);
|
||||
if (source && source.commit !== dependency.source.commit) {
|
||||
dependency.source = source;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
const temporary = `${filename}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
await writeFile(temporary, formatQuixosLockDocument(parsed.document), { flag: "wx" });
|
||||
if ((await readFile(filename, "utf8")) !== before)
|
||||
throw new Error(`Concurrent edit to ${file}; retry after joining writers`);
|
||||
await rename(temporary, filename);
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
const snapshotStarted = performance.now();
|
||||
const commit = await snapshotCommit(root);
|
||||
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Capture: ${directory} snapshot: ${Math.round(performance.now() - snapshotStarted)}ms`,
|
||||
);
|
||||
phase = "publication";
|
||||
const ref = retentionTagForCommit(commit);
|
||||
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) 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");
|
||||
complete.set(directory, { ...node.source, commit });
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
|
||||
}
|
||||
active.delete(directory);
|
||||
return complete.has(directory);
|
||||
};
|
||||
// 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);
|
||||
await visit(target);
|
||||
for (let index = blockers.length - 1; index >= 0; index--)
|
||||
if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
|
||||
for (const [directory, source] of complete) {
|
||||
try {
|
||||
if ((await snapshotCommit(path.join(context.workbench, directory))) !== source.commit)
|
||||
throw new Error("Source advanced while converging; join writers and retry");
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase: "concurrent-edit", message: String(error) });
|
||||
}
|
||||
}
|
||||
// Persist successful selections even if another repository is still broken.
|
||||
// Recovery must not depend on all parents succeeding in the same invocation.
|
||||
const graph = JSON.parse(graphBefore);
|
||||
for (const resource of graph.resources)
|
||||
resource.directory = path.relative(context.workbench, path.resolve(context.workbench, resource.directory));
|
||||
const replacements = new Map<string, string>();
|
||||
for (const resource of graph.resources) {
|
||||
const source = complete.get(resource.directory);
|
||||
if (!source) continue;
|
||||
const key = `${resource.kind}\0${source.repository}\0${source.commit}`;
|
||||
replacements.set(resource.key, key);
|
||||
if (resource.source.commit !== source.commit) delete resource.revisionId;
|
||||
resource.source = source;
|
||||
resource.key = key;
|
||||
}
|
||||
for (const resource of graph.resources)
|
||||
for (const dependency of resource.dependencies ?? []) {
|
||||
dependency.resourceKey = replacements.get(dependency.resourceKey) ?? dependency.resourceKey;
|
||||
}
|
||||
for (const direct of graph.directResources ?? [])
|
||||
direct.resourceKey = replacements.get(direct.resourceKey) ?? direct.resourceKey;
|
||||
// Inventory is a projection of actual locks, including newly added/removed
|
||||
// imports. Never require a successful parent compilation to repair it.
|
||||
for (const [directory] of complete) {
|
||||
const lock = await loadQuixosLock(path.join(context.workbench, directory, "quixos.lock"));
|
||||
if (!lock.ok) continue;
|
||||
const dependencies = lock.lock.resources.map((dependency) => ({
|
||||
binding: `${dependency.kind}\0${dependency.binding}`,
|
||||
resourceKey: `${dependency.kind}\0${dependency.source.repository}\0${dependency.source.commit}`,
|
||||
}));
|
||||
if (directory === "root") {
|
||||
graph.quixos = lock.lock.quixos;
|
||||
graph.directResources = lock.lock.resources.map((dependency, index) => ({
|
||||
kind: dependency.kind,
|
||||
binding: dependency.binding,
|
||||
resourceKey: dependencies[index].resourceKey,
|
||||
...(selected.has(identity(dependency.kind, dependency.source.repository))
|
||||
? { directory: selected.get(identity(dependency.kind, dependency.source.repository)) }
|
||||
: {}),
|
||||
}));
|
||||
} else {
|
||||
const resource = graph.resources.find((entry: { directory: string }) => entry.directory === directory);
|
||||
if (resource) resource.dependencies = dependencies;
|
||||
}
|
||||
}
|
||||
const graphAfter = JSON.stringify(graph, null, 2) + "\n";
|
||||
if (graphBefore !== graphAfter) {
|
||||
const temporary = `${graphFile}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
await writeFile(temporary, graphAfter, { flag: "wx", mode: 0o600 });
|
||||
if ((await readFile(graphFile, "utf8")) !== graphBefore)
|
||||
throw new Error(
|
||||
"Managed inventory changed during convergence; source is retained, retry after joining writers",
|
||||
);
|
||||
await rename(temporary, graphFile);
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
return {
|
||||
workbench: context.workbench,
|
||||
converged: blockers.length === 0,
|
||||
candidate: blockers.length ? null : (complete.get(target) ?? null),
|
||||
retained: [...complete].map(([directory, source]) => ({ directory, source })),
|
||||
worklist: blockers,
|
||||
verificationEvidence: false,
|
||||
activated: false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parseQx, walkSyntax } from "./source.js";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { readQxSource } from "./source-loader.js";
|
||||
import { loadQuixosLock } from "../resource-lock/index.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
const git = async (root: string, args: string[]) =>
|
||||
(
|
||||
await execFile("git", ["-C", root, ...args], {
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
||||
})
|
||||
).stdout;
|
||||
const message = (error: unknown) => String(error instanceof Error ? error.message : error).slice(0, 2000);
|
||||
|
||||
/** Syntax-only contract inspection is deliberately NOT verification evidence.
|
||||
* Each file can recover independently; current valid files always win. */
|
||||
export async function inspectAuthoringRepository(root: string, historyLimit = 100) {
|
||||
const names = (await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]))
|
||||
.split("\0")
|
||||
.filter((name) => name.endsWith(".qx"));
|
||||
if (names.length > 128)
|
||||
throw new Error("Repository inspection exceeds 128 QX files; split the resource into smaller repositories");
|
||||
const files = [];
|
||||
for (const name of [...new Set(names)].sort()) {
|
||||
let source = "",
|
||||
errors: unknown[] = [],
|
||||
revision: string | null = null;
|
||||
try {
|
||||
source = await readQxSource(root, name);
|
||||
if (source.length > 262144) throw new Error(`Inspection file exceeds 256 KiB: ${name}`);
|
||||
errors = parseQx(source, name).diagnostics;
|
||||
} catch (error) {
|
||||
errors = [{ message: message(error) }];
|
||||
}
|
||||
const currentErrors = errors;
|
||||
if (errors.length) {
|
||||
// Git can traverse jj's immutable commit DAG without mutating/snapshotting @.
|
||||
const head = await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {
|
||||
cwd: root,
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" },
|
||||
}).then(
|
||||
(r) => r.stdout.trim(),
|
||||
() => "HEAD",
|
||||
);
|
||||
const commits = await git(root, ["rev-list", `--max-count=${historyLimit}`, head, "--", name]).catch(() => "");
|
||||
for (const commit of commits.trim().split("\n").filter(Boolean)) {
|
||||
const historical = await git(root, ["show", `${commit}:${name}`]).catch(() => null);
|
||||
if (historical === null || historical.length > 262144) continue;
|
||||
if (!parseQx(historical, name).diagnostics.length) {
|
||||
source = historical;
|
||||
revision = commit;
|
||||
errors = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const syntax = errors.length ? null : parseQx(source, name);
|
||||
files.push({
|
||||
file: name,
|
||||
status: errors.length ? "unavailable" : revision ? "historical" : "current",
|
||||
revision,
|
||||
currentErrors: currentErrors.slice(0, 20),
|
||||
omittedErrors: Math.max(0, currentErrors.length - 20),
|
||||
declarations: syntax
|
||||
? [...walkSyntax(syntax.root)]
|
||||
.filter((node) =>
|
||||
/^(?:interface|package|atom|state|edge|method|function|event|conformance)\w*Decl$/.test(node.kind),
|
||||
)
|
||||
.map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) }))
|
||||
: [],
|
||||
});
|
||||
}
|
||||
return { verificationEvidence: false as const, resolutionChecked: false as const, files };
|
||||
}
|
||||
|
||||
export async function inspectWorkbench(start: string, selector?: string) {
|
||||
const context = await authoringContext(start);
|
||||
if (!selector || selector === ".") {
|
||||
const relative = path.relative(context.workbench, await realpath(start));
|
||||
selector =
|
||||
context.resources.find((entry) => relative === entry.directory || relative.startsWith(entry.directory + path.sep))
|
||||
?.directory ?? "root";
|
||||
}
|
||||
const lock = await loadQuixosLock(path.join(context.workbench, "root/quixos.lock"));
|
||||
const aliases = lock.ok ? lock.lock.resources.filter((entry) => entry.binding === selector) : [];
|
||||
const selected = context.resources.filter(
|
||||
(entry) =>
|
||||
!selector ||
|
||||
selector === entry.directory ||
|
||||
selector === entry.resourceId ||
|
||||
selector === path.basename(entry.directory) ||
|
||||
aliases.some((alias) => alias.kind === entry.kind && alias.source.repository === entry.source?.repository),
|
||||
);
|
||||
if (!selected.length) throw new Error(`No registered resource matches ${selector}`);
|
||||
if (selector && selected.length > 1)
|
||||
throw new Error(`Ambiguous resource ${selector}; use its resource ID or directory`);
|
||||
const resources = [];
|
||||
for (const entry of selected) {
|
||||
try {
|
||||
const root = path.join(context.workbench, entry.directory);
|
||||
if ((await realpath(root)) !== root) throw new Error("Managed checkout crosses a symlink");
|
||||
resources.push({ ...entry, ...(await inspectAuthoringRepository(root)) });
|
||||
} catch (error) {
|
||||
resources.push({ ...entry, error: message(error) });
|
||||
}
|
||||
}
|
||||
return { workbench: context.workbench, verificationEvidence: false, resources };
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { inspectAuthoringRepository } from "./authoring-inspect.js";
|
||||
import { checkRecordName } from "./authoring-check.js";
|
||||
import { loadQuixosLock } from "../resource-lock/index.js";
|
||||
import { checkerIdentity } from "./checked-build.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
export async function authoringWorklist(start: string) {
|
||||
const context = await authoringContext(start);
|
||||
const entries: { directory: string; resourceId?: string; phase: string; message: string; next: string }[] = [];
|
||||
const dependencies = new Map<string, string[]>();
|
||||
for (const resource of context.resources) {
|
||||
const root = path.join(context.workbench, resource.directory);
|
||||
const add = (phase: string, message: string) =>
|
||||
entries.push({
|
||||
directory: resource.directory,
|
||||
resourceId: resource.resourceId,
|
||||
phase,
|
||||
message,
|
||||
next:
|
||||
phase === "syntax"
|
||||
? `qx-workspace inspect ${resource.directory}`
|
||||
: phase === "evolution"
|
||||
? "Inspect evolution.json in the check output; resolve its named migration/review requirements before cutover"
|
||||
: `cd ${resource.directory} && qx-workspace check`,
|
||||
});
|
||||
try {
|
||||
if ((await fs.realpath(root)) !== root) throw new Error("Registered checkout crosses a symlink");
|
||||
const inspected = await inspectAuthoringRepository(root);
|
||||
for (const file of inspected.files)
|
||||
if (file.currentErrors.length)
|
||||
add(
|
||||
"syntax",
|
||||
`${file.file}: ${JSON.stringify(file.currentErrors)}${file.status === "historical" ? `; historical contract available at ${file.revision}` : ""}`,
|
||||
);
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok)
|
||||
add("resolution", lock.diagnostics.map((entry) => `${entry.fileName}: ${entry.message}`).join("\n"));
|
||||
else
|
||||
dependencies.set(
|
||||
resource.directory,
|
||||
lock.lock.resources.flatMap((dependency) => {
|
||||
const selected = context.resources.find(
|
||||
(entry) => entry.kind === dependency.kind && entry.source?.repository === dependency.source.repository,
|
||||
);
|
||||
if (selected?.source && selected.source.commit !== dependency.source.commit)
|
||||
add("propagation", `Dependency ${dependency.binding} has advanced; check will repin it automatically`);
|
||||
return selected ? [selected.directory] : [];
|
||||
}),
|
||||
);
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(
|
||||
await fs.readFile(
|
||||
path.join(context.workbench, ".quixos/checks", checkRecordName(resource.directory)),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (!record?.commit) {
|
||||
if (record?.blockers?.length) add(record.phase, record.blockers.join("\n"));
|
||||
else add("unchecked", "No immutable candidate check recorded yet");
|
||||
continue;
|
||||
}
|
||||
if (record.checker !== checkerIdentity()) add("unchecked", "The installed checker changed since the last check");
|
||||
const env = { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" };
|
||||
const commit = (
|
||||
await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {
|
||||
cwd: root,
|
||||
env,
|
||||
})
|
||||
).stdout.trim();
|
||||
const dirty = await execFile("git", ["diff", "--quiet", "--no-ext-diff", record.commit, "--"], {
|
||||
cwd: root,
|
||||
}).then(
|
||||
() => false,
|
||||
() => true,
|
||||
);
|
||||
const untracked = (await execFile("git", ["ls-files", "--others", "--exclude-standard"], { cwd: root })).stdout;
|
||||
if (commit !== record.commit || dirty || untracked)
|
||||
add("unchecked", `Edits are newer than the last check (${record.commit.slice(0, 12)})`);
|
||||
else if (record.blockers?.length) add(record.phase, record.blockers.join("\n"));
|
||||
} catch (error) {
|
||||
add("inspection", String(error).slice(0, 3000));
|
||||
}
|
||||
}
|
||||
// Fixed-point propagation, independent of registration order.
|
||||
const blocked = new Set(entries.map((entry) => entry.directory));
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [directory, required] of dependencies)
|
||||
if (!blocked.has(directory)) {
|
||||
const waiting = required.filter((dependency) => blocked.has(dependency));
|
||||
if (waiting.length) {
|
||||
blocked.add(directory);
|
||||
changed = true;
|
||||
entries.push({
|
||||
directory,
|
||||
phase: "dependency",
|
||||
message: `Waiting for ${waiting.join(", ")}`,
|
||||
next: "Resolve the named repositories, then rerun check",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
workbench: context.workbench,
|
||||
verificationEvidence: false,
|
||||
worklist: entries,
|
||||
note: "Derived authoring guidance, not activation approval. Independent repositories can be delegated separately; join writers before a root check.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createHash } from "node:crypto";
|
||||
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import {
|
||||
contentDigest,
|
||||
planEvolution,
|
||||
type EvolutionReview,
|
||||
type WorkspaceRevision,
|
||||
} from "../capability-model/index.js";
|
||||
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
|
||||
import { snapshotCommit, checkoutCommit, buildCheckedPackage } from "./checked-build.js";
|
||||
const execFile = promisify(execFileCallback);
|
||||
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
||||
|
||||
export const localResourceSnapshots = async (
|
||||
root: string,
|
||||
filename?: string,
|
||||
): Promise<{ resources: { kind: string; repository: string; commit: string; directory: string }[] }> => {
|
||||
if (filename) {
|
||||
const document = JSON.parse(await fs.readFile(filename, "utf8"));
|
||||
return {
|
||||
resources: document.resources.map((entry: { directory: string }) => ({
|
||||
...entry,
|
||||
directory: path.resolve(path.dirname(filename), entry.directory),
|
||||
})),
|
||||
};
|
||||
}
|
||||
let directory = await fs.realpath(root);
|
||||
for (;;) {
|
||||
let graphText: string | undefined;
|
||||
try {
|
||||
graphText = await fs.readFile(path.join(directory, ".quixos/resource-graph.json"), "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (graphText !== undefined) {
|
||||
const graph = JSON.parse(graphText);
|
||||
const resources = [];
|
||||
for (const entry of graph.resources) {
|
||||
const location = await fs.realpath(path.resolve(directory, entry.directory));
|
||||
if (!location.startsWith(`${directory}/resources/`))
|
||||
throw new Error("Workbench resource escapes managed directory");
|
||||
resources.push({ kind: entry.kind, ...entry.source, directory: location });
|
||||
}
|
||||
return { resources };
|
||||
}
|
||||
const parent = path.dirname(directory);
|
||||
if (parent === directory) return { resources: [] };
|
||||
directory = parent;
|
||||
}
|
||||
};
|
||||
|
||||
/** Copy actual authoring files without snapshotting jj or creating a Git commit. */
|
||||
export const snapshotRepository = async (source: string, destination: string) => {
|
||||
const root = await fs.realpath(source);
|
||||
const files = async () =>
|
||||
(
|
||||
await execFile("git", ["-C", root, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
})
|
||||
).stdout
|
||||
.split("\0")
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
const names = [...new Set(await files())];
|
||||
if (names.length > 50_000) throw new Error("Candidate source exceeds 50000 files");
|
||||
const contents: { name: string; digest: string; mode: number }[] = [];
|
||||
let bytes = 0;
|
||||
await fs.mkdir(destination, { recursive: true, mode: 0o700 });
|
||||
for (const name of names) {
|
||||
if (path.isAbsolute(name) || name.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".jj"))
|
||||
throw new Error("Invalid candidate source path");
|
||||
const file = path.join(root, name);
|
||||
let metadata;
|
||||
try {
|
||||
metadata = await fs.lstat(file);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
||||
throw error;
|
||||
}
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink() || !(await fs.realpath(file)).startsWith(`${root}${path.sep}`))
|
||||
throw new Error(`Candidate source must be a regular file: ${name}`);
|
||||
const data = await fs.readFile(file);
|
||||
bytes += data.length;
|
||||
if (bytes > 128 * 1024 * 1024) throw new Error("Candidate source exceeds 128 MiB");
|
||||
contents.push({ name, digest: bytesDigest(data), mode: metadata.mode & 0o777 });
|
||||
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
|
||||
await fs.writeFile(path.join(destination, name), data, { flag: "wx", mode: metadata.mode & 0o777 });
|
||||
}
|
||||
if (JSON.stringify([...new Set(await files())]) !== JSON.stringify(names))
|
||||
throw new Error("Source files changed during candidate snapshot");
|
||||
for (const entry of contents)
|
||||
if (bytesDigest(await fs.readFile(path.join(root, entry.name))) !== entry.digest)
|
||||
throw new Error(`Source changed during candidate snapshot: ${entry.name}`);
|
||||
return { source: root, directory: destination, treeDigest: contentDigest(contents), files: contents };
|
||||
};
|
||||
|
||||
// Local mirrors accelerate resolution, but only their committed locked trees
|
||||
// may stand in for published dependencies. Never relabel dirty files as a pin.
|
||||
async function committedResolver(root: string, temporary: string, filename?: string, publishedOnly = false) {
|
||||
const map = publishedOnly ? { resources: [] } : await localResourceSnapshots(root, filename);
|
||||
const resources = [];
|
||||
for (const [index, entry] of map.resources.entries()) {
|
||||
const directory = path.join(temporary, `dependency-${index}`);
|
||||
await checkoutCommit(entry.directory, entry.commit, directory);
|
||||
resources.push({ ...entry, directory });
|
||||
}
|
||||
const snapshotMap = path.join(temporary, "snapshots.json");
|
||||
await fs.writeFile(snapshotMap, JSON.stringify({ resources }));
|
||||
return createGitCapabilityResolver({ checkoutRoot: path.join(temporary, "resolved"), snapshotMap });
|
||||
}
|
||||
|
||||
export const checkResourceCandidate = async (options: {
|
||||
root: string;
|
||||
output: string;
|
||||
kind: "package" | "interface";
|
||||
source: { repository: string; commit: string };
|
||||
snapshotMap?: string;
|
||||
publishedOnly?: boolean;
|
||||
}) => {
|
||||
await fs.mkdir(options.output, { mode: 0o700 });
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-resource-check-"));
|
||||
const blockers: string[] = [];
|
||||
let treeDigest: string | undefined, commit: string | undefined, artifactPath: string | undefined;
|
||||
try {
|
||||
commit = await snapshotCommit(options.root);
|
||||
treeDigest = (await snapshotRepository(options.root, path.join(temporary, "observed"))).treeDigest;
|
||||
const root = path.join(temporary, "source");
|
||||
await checkoutCommit(options.root, commit, root);
|
||||
const resolveResource = await committedResolver(
|
||||
options.root,
|
||||
temporary,
|
||||
options.snapshotMap,
|
||||
options.publishedOnly,
|
||||
);
|
||||
const compiled = await compileCapabilityResourceRepository({
|
||||
rootDirectory: root,
|
||||
kind: options.kind,
|
||||
source: { resolver: "git", repository: options.source.repository, commit },
|
||||
resolveResource,
|
||||
});
|
||||
if (compiled.resource.kind === "package") {
|
||||
const schema = path.join(temporary, "bindings.json");
|
||||
await fs.writeFile(schema, JSON.stringify(bindingSchema(compiled)));
|
||||
artifactPath = await buildCheckedPackage(root, schema, compiled.resource.revision.revisionId);
|
||||
}
|
||||
if ((await snapshotCommit(options.root)) !== commit)
|
||||
throw new Error("Source changed during verification; run the check again");
|
||||
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.resource, null, 2));
|
||||
} catch (error) {
|
||||
blockers.push(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
const result = {
|
||||
candidateOnly: true,
|
||||
activationEvidence: false,
|
||||
commit,
|
||||
treeDigest,
|
||||
artifactPath,
|
||||
blockers,
|
||||
note: "Checked immutable candidate; cutover independently checks current migration/review requirements. No publication or activation performed.",
|
||||
};
|
||||
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
};
|
||||
|
||||
export const checkWorkspaceCandidate = async (options: {
|
||||
root: string;
|
||||
output: string;
|
||||
snapshotMap?: string;
|
||||
baseline?: string;
|
||||
reviews?: string;
|
||||
}) => {
|
||||
await fs.mkdir(options.output, { mode: 0o700 });
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-workspace-check-"));
|
||||
const blockers: string[] = [],
|
||||
checks: { packageRevisionId: string; artifactPath: string }[] = [];
|
||||
let commit: string | undefined;
|
||||
try {
|
||||
commit = await snapshotCommit(options.root);
|
||||
for (const entry of (await localResourceSnapshots(options.root, options.snapshotMap)).resources) {
|
||||
const current = await snapshotCommit(entry.directory);
|
||||
const tree = async (revision: string) =>
|
||||
(await execFile("git", ["rev-parse", `${revision}^{tree}`], { cwd: entry.directory })).stdout.trim();
|
||||
if ((await tree(current)) !== (await tree(entry.commit)))
|
||||
throw new Error(
|
||||
`Edited resource is not in the root's locked candidate: ${entry.directory}. Check that resource, then run qx-workspace resource upgrade --publish to propagate its revision.`,
|
||||
);
|
||||
}
|
||||
const root = path.join(temporary, "source");
|
||||
await checkoutCommit(options.root, commit, root);
|
||||
const resolveResource = await committedResolver(options.root, temporary, options.snapshotMap);
|
||||
const compiled = await compileWorkspaceRepository({
|
||||
rootDirectory: root,
|
||||
sourceRootCommit: commit,
|
||||
resolveResource,
|
||||
});
|
||||
const baseline = options.baseline
|
||||
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
|
||||
: null;
|
||||
const reviews = options.reviews
|
||||
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
|
||||
: [];
|
||||
const evolution = planEvolution(baseline, compiled.workspace, { reviews });
|
||||
blockers.push(...evolution.blockers);
|
||||
for (const resource of compiled.resources.filter((entry) => entry.kind === "package")) {
|
||||
// Per-package recursive schema, identical to host activation, not unrelated
|
||||
// workspace declarations that would unnecessarily invalidate build caches.
|
||||
const candidate = await compileCapabilityResourceRepository({
|
||||
rootDirectory: resource.directory,
|
||||
kind: "package",
|
||||
source: resource.source,
|
||||
resolveResource,
|
||||
});
|
||||
const schema = path.join(temporary, "bindings.json");
|
||||
await fs.writeFile(
|
||||
schema,
|
||||
JSON.stringify(
|
||||
specializeBindingSchema(bindingSchema(candidate), compiled.workspace, candidate.resource.revision.revisionId),
|
||||
),
|
||||
);
|
||||
const artifactPath = await buildCheckedPackage(
|
||||
resource.directory,
|
||||
schema,
|
||||
candidate.resource.revision.revisionId,
|
||||
);
|
||||
checks.push({ packageRevisionId: candidate.resource.revision.revisionId, artifactPath });
|
||||
}
|
||||
if ((await snapshotCommit(options.root)) !== commit)
|
||||
throw new Error("Source changed during verification; run the check again");
|
||||
const result = {
|
||||
schemaVersion: 1,
|
||||
candidateOnly: true,
|
||||
activationEvidence: false,
|
||||
commit,
|
||||
evolution,
|
||||
checks,
|
||||
blockers,
|
||||
note: "Checks the committed root and its exact locked dependencies. Resource edits must be verified and repinned before they enter this candidate. No publication or activation performed.",
|
||||
};
|
||||
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.workspace, null, 2));
|
||||
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = {
|
||||
schemaVersion: 1,
|
||||
candidateOnly: true,
|
||||
activationEvidence: false,
|
||||
commit,
|
||||
checks,
|
||||
blockers: [...blockers, error instanceof Error ? error.message : String(error)],
|
||||
};
|
||||
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execFile as callback, spawn } from "node:child_process";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const execFile = promisify(callback);
|
||||
const environment = () => ({ ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", GIT_TERMINAL_PROMPT: "0" });
|
||||
export const checkerIdentity = () =>
|
||||
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
/** Checking snapshots jj, but never publishes or activates the working copy. */
|
||||
export async function snapshotCommit(root: string): Promise<string> {
|
||||
const run = async (...args: string[]) =>
|
||||
(await execFile("jj", args, { cwd: root, env: environment() })).stdout.trim();
|
||||
await run("status");
|
||||
// jj resolve --list exits 1 on a clean revision. Query structured revision
|
||||
// metadata instead of depending on diagnostic wording or swallowing errors.
|
||||
if ((await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "conflict")) !== "false")
|
||||
throw new Error("Resolve source conflicts before verification");
|
||||
const commit = await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id");
|
||||
if (!/^(?:[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, "--"], {
|
||||
cwd: root,
|
||||
env: environment(),
|
||||
});
|
||||
const { stdout } = await execFile("git", ["ls-files", "--others", "--exclude-standard", "-z"], { cwd: root });
|
||||
if (stdout) throw new Error("Source contains files not captured by jj; inspect jj tracking before verification");
|
||||
return commit;
|
||||
}
|
||||
|
||||
/** Use Git's actual committed tree, never dirty overlays labelled as old pins. */
|
||||
export async function checkoutCommit(root: string, commit: string, destination: string) {
|
||||
await fs.mkdir(destination, { recursive: true });
|
||||
const archive = await execFile("git", ["archive", "--format=tar", commit], {
|
||||
cwd: root,
|
||||
encoding: "buffer",
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn("tar", ["-xf", "-", "-C", destination], { stdio: ["pipe", "ignore", "pipe"] });
|
||||
let error = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
error += chunk;
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) =>
|
||||
code === 0 ? resolve() : reject(new Error(`Cannot extract committed source: ${error}`)),
|
||||
);
|
||||
child.stdin.on("error", reject);
|
||||
child.stdin.end(archive.stdout);
|
||||
});
|
||||
}
|
||||
|
||||
/** Shared by provisional checks, template publication and host activation.
|
||||
* The Nix derivation is the cached check; its metadata is internal provenance,
|
||||
* not a certificate authored or approved by the workspace agent.
|
||||
*/
|
||||
export async function buildCheckedPackage(source: string, schema: string, packageRevisionId: string): Promise<string> {
|
||||
const generator =
|
||||
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
if (!/^\/nix\/store\/[^/]+$/.test(generator))
|
||||
throw new Error("Run verification with the installed Quixos tooling (its exact Nix checker is required)");
|
||||
const builder = path.join(generator, "share/checked-package.nix");
|
||||
await fs.access(builder);
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
"nix",
|
||||
[
|
||||
"build",
|
||||
"--impure",
|
||||
"--file",
|
||||
builder,
|
||||
"--argstr",
|
||||
"source",
|
||||
source,
|
||||
"--argstr",
|
||||
"schema",
|
||||
schema,
|
||||
"--argstr",
|
||||
"generator",
|
||||
generator,
|
||||
"--argstr",
|
||||
"packageRevisionId",
|
||||
packageRevisionId,
|
||||
"--no-link",
|
||||
"--print-out-paths",
|
||||
"-L",
|
||||
],
|
||||
{ env: environment(), stdio: ["ignore", "pipe", "inherit"] },
|
||||
);
|
||||
let output = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
output += chunk;
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
const artifact = output.trim();
|
||||
if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact))
|
||||
reject(new Error(`Checked Nix build failed (${code}); see build diagnostics above`));
|
||||
else resolve(artifact);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Exact remote source DAG; the Nix checker owns schema construction and all
|
||||
* nested fetches. Keep build noise in a named log, with a bounded failure tail. */
|
||||
export async function buildImmutableCandidate(
|
||||
source: { repository: string; commit: string },
|
||||
kind: "workspace" | "interface" | "package",
|
||||
logFile: string,
|
||||
contractOnly = false,
|
||||
): Promise<string> {
|
||||
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit))
|
||||
throw new Error("An immutable candidate requires an exact commit");
|
||||
const url = new URL(source.repository);
|
||||
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
|
||||
throw new Error("Candidate origin must be credential-free HTTPS");
|
||||
const generator =
|
||||
process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
if (!/^\/nix\/store\/[^/]+$/.test(generator)) throw new Error("Use the installed Quixos checker");
|
||||
const builder = path.join(generator, "share/checked-candidate.nix");
|
||||
await fs.access(builder);
|
||||
const log = createWriteStream(logFile, { flags: "wx", mode: 0o600 });
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
log.once("open", () => resolve());
|
||||
log.once("error", reject);
|
||||
});
|
||||
return await new Promise((resolve, reject) => {
|
||||
let output = "",
|
||||
tail = "",
|
||||
failure: Error | undefined;
|
||||
const child = spawn(
|
||||
"nix",
|
||||
[
|
||||
"build",
|
||||
"--impure",
|
||||
"--file",
|
||||
builder,
|
||||
"--argstr",
|
||||
"repository",
|
||||
source.repository,
|
||||
"--argstr",
|
||||
"commit",
|
||||
source.commit,
|
||||
"--argstr",
|
||||
"kind",
|
||||
kind,
|
||||
"--argstr",
|
||||
"generator",
|
||||
generator,
|
||||
"--arg",
|
||||
"contractOnly",
|
||||
contractOnly ? "true" : "false",
|
||||
"--no-link",
|
||||
"--print-out-paths",
|
||||
"-L",
|
||||
],
|
||||
{ env: environment(), stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
log.on("error", (error) => {
|
||||
failure = error;
|
||||
child.kill();
|
||||
});
|
||||
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.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,123 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import process from "node:process";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "./parser.js";
|
||||
import { capabilityId } from "../capability-model/types.js";
|
||||
import { compileWorkspaceRevision } from "../capability-model/validation.js";
|
||||
|
||||
const usage = `usage: quixos-capability-compile [--check] [--resource]
|
||||
[--workspace-id ID] [--workspace-revision-id ID]
|
||||
[--source-root-commit GIT_REV] <workspace.qx | ->
|
||||
|
||||
Parses, lowers, and validates a Quixos capability workspace. --resource instead
|
||||
expects one standalone interface or package document. By default the checked
|
||||
semantic workspace revision or resource is written as JSON. --check emits no JSON.
|
||||
The identity overrides instantiate a checked built-in/template assembly for one
|
||||
real workspace; both must be supplied together.`;
|
||||
|
||||
const parseArgs = (args: string[]) => {
|
||||
let checkOnly = false;
|
||||
let resource = false;
|
||||
let workspaceId: string | undefined;
|
||||
let workspaceRevisionId: string | undefined;
|
||||
let sourceRootCommit: string | undefined;
|
||||
const positional: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const argument = args[index]!;
|
||||
if (argument === "--check") checkOnly = true;
|
||||
else if (argument === "--resource") resource = true;
|
||||
else if (argument === "--workspace-id") workspaceId = args[++index];
|
||||
else if (argument === "--workspace-revision-id") workspaceRevisionId = args[++index];
|
||||
else if (argument === "--source-root-commit") sourceRootCommit = args[++index];
|
||||
else if (argument === "-") positional.push(argument);
|
||||
else if (argument.startsWith("-")) throw new Error(usage);
|
||||
else positional.push(argument);
|
||||
}
|
||||
if (
|
||||
positional.length !== 1 ||
|
||||
Boolean(workspaceId) !== Boolean(workspaceRevisionId) ||
|
||||
(resource && Boolean(workspaceId || workspaceRevisionId || sourceRootCommit))
|
||||
) {
|
||||
throw new Error(usage);
|
||||
}
|
||||
return { checkOnly, resource, workspaceId, workspaceRevisionId, sourceRootCommit, fileName: positional[0]! };
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
process.stdout.write(`${usage}\n`);
|
||||
return;
|
||||
}
|
||||
const { checkOnly, resource, workspaceId, workspaceRevisionId, sourceRootCommit, fileName } = parseArgs(args);
|
||||
const source =
|
||||
fileName === "-"
|
||||
? await new Promise<string>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
process.stdin.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
process.stdin.on("error", reject);
|
||||
})
|
||||
: await readFile(fileName, "utf8");
|
||||
const reportDiagnostics = (
|
||||
diagnostics: readonly {
|
||||
fileName: string;
|
||||
line: number;
|
||||
column: number;
|
||||
phase: string;
|
||||
code: string;
|
||||
message: string;
|
||||
path?: string;
|
||||
}[],
|
||||
) => {
|
||||
for (const diagnostic of diagnostics) {
|
||||
const location = diagnostic.line
|
||||
? `${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column + 1}`
|
||||
: `${diagnostic.fileName}${diagnostic.path ? `:${diagnostic.path}` : ""}`;
|
||||
process.stderr.write(`${location}: ${diagnostic.phase} ${diagnostic.code}: ${diagnostic.message}\n`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
};
|
||||
if (resource) {
|
||||
const result = compileCapabilityResourceSource(source, {
|
||||
source: {
|
||||
repository: "https://compiler.invalid/resource.git",
|
||||
commit: "0000000000000000000000000000000000000000",
|
||||
},
|
||||
fileName,
|
||||
});
|
||||
if (!result.ok) {
|
||||
reportDiagnostics(result.diagnostics);
|
||||
return;
|
||||
}
|
||||
if (!checkOnly) process.stdout.write(`${JSON.stringify(result.resource, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
const result = compileCapabilitySource(source, fileName);
|
||||
if (!result.ok) {
|
||||
reportDiagnostics(result.diagnostics);
|
||||
return;
|
||||
}
|
||||
const workspace =
|
||||
workspaceId && workspaceRevisionId
|
||||
? {
|
||||
...result.workspace,
|
||||
workspaceId: capabilityId.workspace(workspaceId),
|
||||
id: capabilityId.workspaceRevision(workspaceRevisionId),
|
||||
...(sourceRootCommit ? { sourceRootCommit } : {}),
|
||||
}
|
||||
: { ...result.workspace, ...(sourceRootCommit ? { sourceRootCommit } : {}) };
|
||||
const instantiated = compileWorkspaceRevision(workspace);
|
||||
if (!instantiated.ok) {
|
||||
throw new Error(instantiated.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
|
||||
}
|
||||
if (!checkOnly) {
|
||||
process.stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
/** Kernel-owned lock: a crashed coordinator cannot leave a stale ownership file.
|
||||
* The persistent file is just an inode; EOF releases the helper's lock. */
|
||||
export async function withFileLock<T>(filename: string, work: () => Promise<T>): Promise<T> {
|
||||
const child = spawn(
|
||||
"flock",
|
||||
[
|
||||
"--exclusive",
|
||||
"--timeout",
|
||||
"120",
|
||||
"--conflict-exit-code",
|
||||
"75",
|
||||
filename,
|
||||
process.execPath,
|
||||
"-e",
|
||||
'process.stdout.write("locked\\n"); process.stdin.resume();',
|
||||
],
|
||||
{ stdio: ["pipe", "pipe", "pipe"] },
|
||||
);
|
||||
let diagnostics = "";
|
||||
child.stdin.on("error", () => {
|
||||
/* acquisition/exit handling reports helper failure */
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
diagnostics = (diagnostics + String(chunk)).slice(-2000);
|
||||
});
|
||||
const closed = new Promise<void>((resolve) => {
|
||||
child.once("close", () => resolve());
|
||||
});
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let output = "";
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) =>
|
||||
reject(
|
||||
new Error(
|
||||
code === 75
|
||||
? "Timed out after 120 seconds waiting for another authoring command; inspect that command before retrying"
|
||||
: `Cannot acquire authoring lock: ${diagnostics}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
child.stdout.on("data", (chunk) => {
|
||||
output += chunk;
|
||||
if (output.includes("locked\n")) resolve();
|
||||
});
|
||||
});
|
||||
return await work();
|
||||
} finally {
|
||||
child.stdin.end();
|
||||
await closed;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,259 @@
|
||||
WORKSPACE=1
|
||||
RESULT_BYTES=2
|
||||
RPC_CALLS=3
|
||||
DEADLINE_MS=4
|
||||
QUERY=5
|
||||
SPECIALIZE=6
|
||||
ROOT=7
|
||||
DOCUMENT=8
|
||||
FRAGMENTS=9
|
||||
VIEW=10
|
||||
MAX=11
|
||||
ALLOW=12
|
||||
POLL=13
|
||||
QUERYABLE=14
|
||||
RPC=15
|
||||
QUERY_REASON=16
|
||||
TYPE=17
|
||||
OBJECT=18
|
||||
STORABLE=19
|
||||
IMPLEMENTS=20
|
||||
REF=21
|
||||
FRAGMENT=22
|
||||
IMPORT=23
|
||||
EXTERNAL=24
|
||||
ATOM=25
|
||||
INTERFACE=26
|
||||
INTERFACES=27
|
||||
PACKAGE=28
|
||||
VALUE=29
|
||||
RELATION=30
|
||||
OPERATION=31
|
||||
FUNCTION=32
|
||||
CONSTRUCTOR=33
|
||||
CONSTRUCTS=34
|
||||
INPUT=35
|
||||
CONFORM=36
|
||||
AS=37
|
||||
BIND=38
|
||||
STATIC=39
|
||||
TO=40
|
||||
PRIVATE=41
|
||||
SHARED=42
|
||||
STATE=43
|
||||
EDGE=44
|
||||
PROJECTION=45
|
||||
WITH=46
|
||||
USING=47
|
||||
VIA=48
|
||||
MATERIALIZE=49
|
||||
IF=50
|
||||
ABSENT=51
|
||||
ON=52
|
||||
POLICY=53
|
||||
DEFAULT=54
|
||||
SOURCE=55
|
||||
REPOSITORY=56
|
||||
COMMIT=57
|
||||
REVISION=58
|
||||
SEMANTIC_MAJOR=59
|
||||
ON_DELETE=60
|
||||
RETAIN_OTHER=61
|
||||
KEYED=62
|
||||
PUBLIC_TRAVERSAL=63
|
||||
ID=64
|
||||
DOC=65
|
||||
MODE=66
|
||||
EMITS=67
|
||||
RECEIVER=68
|
||||
REQUIRES=69
|
||||
ANY=70
|
||||
GET=71
|
||||
SET=72
|
||||
WATCH=73
|
||||
START=74
|
||||
STOP=75
|
||||
READ=76
|
||||
WRITE=77
|
||||
RESOLVE=78
|
||||
CONNECT=79
|
||||
DISCONNECT=80
|
||||
CALL=81
|
||||
WATCH_START=82
|
||||
WATCH_STOP=83
|
||||
SUBSCRIBE=84
|
||||
UNSUBSCRIBE=85
|
||||
OPTIMISTIC_REGISTER=86
|
||||
CRDT=87
|
||||
OPTIONAL_ONE=88
|
||||
EXACTLY_ONE=89
|
||||
MANY_UNIQUE=90
|
||||
MANY=91
|
||||
ORDERED=92
|
||||
UNIT=93
|
||||
WATCH_HANDLE=94
|
||||
MESSAGE=95
|
||||
ATOM_REF=96
|
||||
INTERFACE_REF=97
|
||||
OPTIONAL=98
|
||||
LIST=99
|
||||
RECORD=100
|
||||
BOOL=101
|
||||
BYTES=102
|
||||
DOUBLE=103
|
||||
INT32=104
|
||||
INT64=105
|
||||
STRING=106
|
||||
UINT32=107
|
||||
UINT64=108
|
||||
TRUE=109
|
||||
FALSE=110
|
||||
NULL=111
|
||||
ARROW=112
|
||||
COLON=113
|
||||
SEMI=114
|
||||
COMMA=115
|
||||
DOT=116
|
||||
LBRACE=117
|
||||
RBRACE=118
|
||||
LBRACK=119
|
||||
RBRACK=120
|
||||
LPAREN=121
|
||||
RPAREN=122
|
||||
LT=123
|
||||
GT=124
|
||||
AMP=125
|
||||
EQUAL=126
|
||||
INTEGER=127
|
||||
JSON_NUMBER=128
|
||||
IDENTIFIER=129
|
||||
STRING_LITERAL=130
|
||||
LINE_COMMENT=131
|
||||
BLOCK_COMMENT=132
|
||||
WS=133
|
||||
'workspace'=1
|
||||
'result-bytes'=2
|
||||
'rpc-calls'=3
|
||||
'deadline-ms'=4
|
||||
'query'=5
|
||||
'specialize'=6
|
||||
'root'=7
|
||||
'document'=8
|
||||
'fragments'=9
|
||||
'view'=10
|
||||
'max'=11
|
||||
'allow'=12
|
||||
'poll'=13
|
||||
'queryable'=14
|
||||
'rpc'=15
|
||||
'query-reason'=16
|
||||
'type'=17
|
||||
'object'=18
|
||||
'storable'=19
|
||||
'implements'=20
|
||||
'ref'=21
|
||||
'fragment'=22
|
||||
'import'=23
|
||||
'external'=24
|
||||
'atom'=25
|
||||
'interface'=26
|
||||
'interfaces'=27
|
||||
'package'=28
|
||||
'value'=29
|
||||
'relation'=30
|
||||
'operation'=31
|
||||
'function'=32
|
||||
'constructor'=33
|
||||
'constructs'=34
|
||||
'input'=35
|
||||
'conform'=36
|
||||
'as'=37
|
||||
'bind'=38
|
||||
'static'=39
|
||||
'to'=40
|
||||
'private'=41
|
||||
'shared'=42
|
||||
'state'=43
|
||||
'edge'=44
|
||||
'projection'=45
|
||||
'with'=46
|
||||
'using'=47
|
||||
'via'=48
|
||||
'materialize'=49
|
||||
'if'=50
|
||||
'absent'=51
|
||||
'on'=52
|
||||
'policy'=53
|
||||
'default'=54
|
||||
'source'=55
|
||||
'repository'=56
|
||||
'commit'=57
|
||||
'revision'=58
|
||||
'semantic-major'=59
|
||||
'on-delete'=60
|
||||
'retain-other'=61
|
||||
'keyed'=62
|
||||
'public-traversal'=63
|
||||
'id'=64
|
||||
'doc'=65
|
||||
'mode'=66
|
||||
'emits'=67
|
||||
'receiver'=68
|
||||
'requires'=69
|
||||
'any'=70
|
||||
'get'=71
|
||||
'set'=72
|
||||
'watch'=73
|
||||
'start'=74
|
||||
'stop'=75
|
||||
'read'=76
|
||||
'write'=77
|
||||
'resolve'=78
|
||||
'connect'=79
|
||||
'disconnect'=80
|
||||
'call'=81
|
||||
'watch-start'=82
|
||||
'watch-stop'=83
|
||||
'subscribe'=84
|
||||
'unsubscribe'=85
|
||||
'optimistic-register'=86
|
||||
'crdt'=87
|
||||
'optional-one'=88
|
||||
'exactly-one'=89
|
||||
'many-unique'=90
|
||||
'many'=91
|
||||
'ordered'=92
|
||||
'unit'=93
|
||||
'watch-handle'=94
|
||||
'message'=95
|
||||
'atom-ref'=96
|
||||
'interface-ref'=97
|
||||
'optional'=98
|
||||
'list'=99
|
||||
'record'=100
|
||||
'bool'=101
|
||||
'bytes'=102
|
||||
'double'=103
|
||||
'int32'=104
|
||||
'int64'=105
|
||||
'string'=106
|
||||
'uint32'=107
|
||||
'uint64'=108
|
||||
'true'=109
|
||||
'false'=110
|
||||
'null'=111
|
||||
'->'=112
|
||||
':'=113
|
||||
';'=114
|
||||
','=115
|
||||
'.'=116
|
||||
'{'=117
|
||||
'}'=118
|
||||
'['=119
|
||||
']'=120
|
||||
'('=121
|
||||
')'=122
|
||||
'<'=123
|
||||
'>'=124
|
||||
'&'=125
|
||||
'='=126
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,259 @@
|
||||
WORKSPACE=1
|
||||
RESULT_BYTES=2
|
||||
RPC_CALLS=3
|
||||
DEADLINE_MS=4
|
||||
QUERY=5
|
||||
SPECIALIZE=6
|
||||
ROOT=7
|
||||
DOCUMENT=8
|
||||
FRAGMENTS=9
|
||||
VIEW=10
|
||||
MAX=11
|
||||
ALLOW=12
|
||||
POLL=13
|
||||
QUERYABLE=14
|
||||
RPC=15
|
||||
QUERY_REASON=16
|
||||
TYPE=17
|
||||
OBJECT=18
|
||||
STORABLE=19
|
||||
IMPLEMENTS=20
|
||||
REF=21
|
||||
FRAGMENT=22
|
||||
IMPORT=23
|
||||
EXTERNAL=24
|
||||
ATOM=25
|
||||
INTERFACE=26
|
||||
INTERFACES=27
|
||||
PACKAGE=28
|
||||
VALUE=29
|
||||
RELATION=30
|
||||
OPERATION=31
|
||||
FUNCTION=32
|
||||
CONSTRUCTOR=33
|
||||
CONSTRUCTS=34
|
||||
INPUT=35
|
||||
CONFORM=36
|
||||
AS=37
|
||||
BIND=38
|
||||
STATIC=39
|
||||
TO=40
|
||||
PRIVATE=41
|
||||
SHARED=42
|
||||
STATE=43
|
||||
EDGE=44
|
||||
PROJECTION=45
|
||||
WITH=46
|
||||
USING=47
|
||||
VIA=48
|
||||
MATERIALIZE=49
|
||||
IF=50
|
||||
ABSENT=51
|
||||
ON=52
|
||||
POLICY=53
|
||||
DEFAULT=54
|
||||
SOURCE=55
|
||||
REPOSITORY=56
|
||||
COMMIT=57
|
||||
REVISION=58
|
||||
SEMANTIC_MAJOR=59
|
||||
ON_DELETE=60
|
||||
RETAIN_OTHER=61
|
||||
KEYED=62
|
||||
PUBLIC_TRAVERSAL=63
|
||||
ID=64
|
||||
DOC=65
|
||||
MODE=66
|
||||
EMITS=67
|
||||
RECEIVER=68
|
||||
REQUIRES=69
|
||||
ANY=70
|
||||
GET=71
|
||||
SET=72
|
||||
WATCH=73
|
||||
START=74
|
||||
STOP=75
|
||||
READ=76
|
||||
WRITE=77
|
||||
RESOLVE=78
|
||||
CONNECT=79
|
||||
DISCONNECT=80
|
||||
CALL=81
|
||||
WATCH_START=82
|
||||
WATCH_STOP=83
|
||||
SUBSCRIBE=84
|
||||
UNSUBSCRIBE=85
|
||||
OPTIMISTIC_REGISTER=86
|
||||
CRDT=87
|
||||
OPTIONAL_ONE=88
|
||||
EXACTLY_ONE=89
|
||||
MANY_UNIQUE=90
|
||||
MANY=91
|
||||
ORDERED=92
|
||||
UNIT=93
|
||||
WATCH_HANDLE=94
|
||||
MESSAGE=95
|
||||
ATOM_REF=96
|
||||
INTERFACE_REF=97
|
||||
OPTIONAL=98
|
||||
LIST=99
|
||||
RECORD=100
|
||||
BOOL=101
|
||||
BYTES=102
|
||||
DOUBLE=103
|
||||
INT32=104
|
||||
INT64=105
|
||||
STRING=106
|
||||
UINT32=107
|
||||
UINT64=108
|
||||
TRUE=109
|
||||
FALSE=110
|
||||
NULL=111
|
||||
ARROW=112
|
||||
COLON=113
|
||||
SEMI=114
|
||||
COMMA=115
|
||||
DOT=116
|
||||
LBRACE=117
|
||||
RBRACE=118
|
||||
LBRACK=119
|
||||
RBRACK=120
|
||||
LPAREN=121
|
||||
RPAREN=122
|
||||
LT=123
|
||||
GT=124
|
||||
AMP=125
|
||||
EQUAL=126
|
||||
INTEGER=127
|
||||
JSON_NUMBER=128
|
||||
IDENTIFIER=129
|
||||
STRING_LITERAL=130
|
||||
LINE_COMMENT=131
|
||||
BLOCK_COMMENT=132
|
||||
WS=133
|
||||
'workspace'=1
|
||||
'result-bytes'=2
|
||||
'rpc-calls'=3
|
||||
'deadline-ms'=4
|
||||
'query'=5
|
||||
'specialize'=6
|
||||
'root'=7
|
||||
'document'=8
|
||||
'fragments'=9
|
||||
'view'=10
|
||||
'max'=11
|
||||
'allow'=12
|
||||
'poll'=13
|
||||
'queryable'=14
|
||||
'rpc'=15
|
||||
'query-reason'=16
|
||||
'type'=17
|
||||
'object'=18
|
||||
'storable'=19
|
||||
'implements'=20
|
||||
'ref'=21
|
||||
'fragment'=22
|
||||
'import'=23
|
||||
'external'=24
|
||||
'atom'=25
|
||||
'interface'=26
|
||||
'interfaces'=27
|
||||
'package'=28
|
||||
'value'=29
|
||||
'relation'=30
|
||||
'operation'=31
|
||||
'function'=32
|
||||
'constructor'=33
|
||||
'constructs'=34
|
||||
'input'=35
|
||||
'conform'=36
|
||||
'as'=37
|
||||
'bind'=38
|
||||
'static'=39
|
||||
'to'=40
|
||||
'private'=41
|
||||
'shared'=42
|
||||
'state'=43
|
||||
'edge'=44
|
||||
'projection'=45
|
||||
'with'=46
|
||||
'using'=47
|
||||
'via'=48
|
||||
'materialize'=49
|
||||
'if'=50
|
||||
'absent'=51
|
||||
'on'=52
|
||||
'policy'=53
|
||||
'default'=54
|
||||
'source'=55
|
||||
'repository'=56
|
||||
'commit'=57
|
||||
'revision'=58
|
||||
'semantic-major'=59
|
||||
'on-delete'=60
|
||||
'retain-other'=61
|
||||
'keyed'=62
|
||||
'public-traversal'=63
|
||||
'id'=64
|
||||
'doc'=65
|
||||
'mode'=66
|
||||
'emits'=67
|
||||
'receiver'=68
|
||||
'requires'=69
|
||||
'any'=70
|
||||
'get'=71
|
||||
'set'=72
|
||||
'watch'=73
|
||||
'start'=74
|
||||
'stop'=75
|
||||
'read'=76
|
||||
'write'=77
|
||||
'resolve'=78
|
||||
'connect'=79
|
||||
'disconnect'=80
|
||||
'call'=81
|
||||
'watch-start'=82
|
||||
'watch-stop'=83
|
||||
'subscribe'=84
|
||||
'unsubscribe'=85
|
||||
'optimistic-register'=86
|
||||
'crdt'=87
|
||||
'optional-one'=88
|
||||
'exactly-one'=89
|
||||
'many-unique'=90
|
||||
'many'=91
|
||||
'ordered'=92
|
||||
'unit'=93
|
||||
'watch-handle'=94
|
||||
'message'=95
|
||||
'atom-ref'=96
|
||||
'interface-ref'=97
|
||||
'optional'=98
|
||||
'list'=99
|
||||
'record'=100
|
||||
'bool'=101
|
||||
'bytes'=102
|
||||
'double'=103
|
||||
'int32'=104
|
||||
'int64'=105
|
||||
'string'=106
|
||||
'uint32'=107
|
||||
'uint64'=108
|
||||
'true'=109
|
||||
'false'=110
|
||||
'null'=111
|
||||
'->'=112
|
||||
':'=113
|
||||
';'=114
|
||||
','=115
|
||||
'.'=116
|
||||
'{'=117
|
||||
'}'=118
|
||||
'['=119
|
||||
']'=120
|
||||
'('=121
|
||||
')'=122
|
||||
'<'=123
|
||||
'>'=124
|
||||
'&'=125
|
||||
'='=126
|
||||
@@ -0,0 +1,723 @@
|
||||
|
||||
import * as antlr from "antlr4ng";
|
||||
import { Token } from "antlr4ng";
|
||||
|
||||
|
||||
export class QuixosCapabilityLexer extends antlr.Lexer {
|
||||
public static readonly WORKSPACE = 1;
|
||||
public static readonly RESULT_BYTES = 2;
|
||||
public static readonly RPC_CALLS = 3;
|
||||
public static readonly DEADLINE_MS = 4;
|
||||
public static readonly QUERY = 5;
|
||||
public static readonly SPECIALIZE = 6;
|
||||
public static readonly ROOT = 7;
|
||||
public static readonly DOCUMENT = 8;
|
||||
public static readonly FRAGMENTS = 9;
|
||||
public static readonly VIEW = 10;
|
||||
public static readonly MAX = 11;
|
||||
public static readonly ALLOW = 12;
|
||||
public static readonly POLL = 13;
|
||||
public static readonly QUERYABLE = 14;
|
||||
public static readonly RPC = 15;
|
||||
public static readonly QUERY_REASON = 16;
|
||||
public static readonly TYPE = 17;
|
||||
public static readonly OBJECT = 18;
|
||||
public static readonly STORABLE = 19;
|
||||
public static readonly IMPLEMENTS = 20;
|
||||
public static readonly REF = 21;
|
||||
public static readonly FRAGMENT = 22;
|
||||
public static readonly IMPORT = 23;
|
||||
public static readonly EXTERNAL = 24;
|
||||
public static readonly ATOM = 25;
|
||||
public static readonly INTERFACE = 26;
|
||||
public static readonly INTERFACES = 27;
|
||||
public static readonly PACKAGE = 28;
|
||||
public static readonly VALUE = 29;
|
||||
public static readonly RELATION = 30;
|
||||
public static readonly OPERATION = 31;
|
||||
public static readonly FUNCTION = 32;
|
||||
public static readonly CONSTRUCTOR = 33;
|
||||
public static readonly CONSTRUCTS = 34;
|
||||
public static readonly INPUT = 35;
|
||||
public static readonly CONFORM = 36;
|
||||
public static readonly AS = 37;
|
||||
public static readonly BIND = 38;
|
||||
public static readonly STATIC = 39;
|
||||
public static readonly TO = 40;
|
||||
public static readonly PRIVATE = 41;
|
||||
public static readonly SHARED = 42;
|
||||
public static readonly STATE = 43;
|
||||
public static readonly EDGE = 44;
|
||||
public static readonly PROJECTION = 45;
|
||||
public static readonly WITH = 46;
|
||||
public static readonly USING = 47;
|
||||
public static readonly VIA = 48;
|
||||
public static readonly MATERIALIZE = 49;
|
||||
public static readonly IF = 50;
|
||||
public static readonly ABSENT = 51;
|
||||
public static readonly ON = 52;
|
||||
public static readonly POLICY = 53;
|
||||
public static readonly DEFAULT = 54;
|
||||
public static readonly SOURCE = 55;
|
||||
public static readonly REPOSITORY = 56;
|
||||
public static readonly COMMIT = 57;
|
||||
public static readonly REVISION = 58;
|
||||
public static readonly SEMANTIC_MAJOR = 59;
|
||||
public static readonly ON_DELETE = 60;
|
||||
public static readonly RETAIN_OTHER = 61;
|
||||
public static readonly KEYED = 62;
|
||||
public static readonly PUBLIC_TRAVERSAL = 63;
|
||||
public static readonly ID = 64;
|
||||
public static readonly DOC = 65;
|
||||
public static readonly MODE = 66;
|
||||
public static readonly EMITS = 67;
|
||||
public static readonly RECEIVER = 68;
|
||||
public static readonly REQUIRES = 69;
|
||||
public static readonly ANY = 70;
|
||||
public static readonly GET = 71;
|
||||
public static readonly SET = 72;
|
||||
public static readonly WATCH = 73;
|
||||
public static readonly START = 74;
|
||||
public static readonly STOP = 75;
|
||||
public static readonly READ = 76;
|
||||
public static readonly WRITE = 77;
|
||||
public static readonly RESOLVE = 78;
|
||||
public static readonly CONNECT = 79;
|
||||
public static readonly DISCONNECT = 80;
|
||||
public static readonly CALL = 81;
|
||||
public static readonly WATCH_START = 82;
|
||||
public static readonly WATCH_STOP = 83;
|
||||
public static readonly SUBSCRIBE = 84;
|
||||
public static readonly UNSUBSCRIBE = 85;
|
||||
public static readonly OPTIMISTIC_REGISTER = 86;
|
||||
public static readonly CRDT = 87;
|
||||
public static readonly OPTIONAL_ONE = 88;
|
||||
public static readonly EXACTLY_ONE = 89;
|
||||
public static readonly MANY_UNIQUE = 90;
|
||||
public static readonly MANY = 91;
|
||||
public static readonly ORDERED = 92;
|
||||
public static readonly UNIT = 93;
|
||||
public static readonly WATCH_HANDLE = 94;
|
||||
public static readonly MESSAGE = 95;
|
||||
public static readonly ATOM_REF = 96;
|
||||
public static readonly INTERFACE_REF = 97;
|
||||
public static readonly OPTIONAL = 98;
|
||||
public static readonly LIST = 99;
|
||||
public static readonly RECORD = 100;
|
||||
public static readonly BOOL = 101;
|
||||
public static readonly BYTES = 102;
|
||||
public static readonly DOUBLE = 103;
|
||||
public static readonly INT32 = 104;
|
||||
public static readonly INT64 = 105;
|
||||
public static readonly STRING = 106;
|
||||
public static readonly UINT32 = 107;
|
||||
public static readonly UINT64 = 108;
|
||||
public static readonly TRUE = 109;
|
||||
public static readonly FALSE = 110;
|
||||
public static readonly NULL = 111;
|
||||
public static readonly ARROW = 112;
|
||||
public static readonly COLON = 113;
|
||||
public static readonly SEMI = 114;
|
||||
public static readonly COMMA = 115;
|
||||
public static readonly DOT = 116;
|
||||
public static readonly LBRACE = 117;
|
||||
public static readonly RBRACE = 118;
|
||||
public static readonly LBRACK = 119;
|
||||
public static readonly RBRACK = 120;
|
||||
public static readonly LPAREN = 121;
|
||||
public static readonly RPAREN = 122;
|
||||
public static readonly LT = 123;
|
||||
public static readonly GT = 124;
|
||||
public static readonly AMP = 125;
|
||||
public static readonly EQUAL = 126;
|
||||
public static readonly INTEGER = 127;
|
||||
public static readonly JSON_NUMBER = 128;
|
||||
public static readonly IDENTIFIER = 129;
|
||||
public static readonly STRING_LITERAL = 130;
|
||||
public static readonly LINE_COMMENT = 131;
|
||||
public static readonly BLOCK_COMMENT = 132;
|
||||
public static readonly WS = 133;
|
||||
|
||||
public static readonly channelNames = [
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
|
||||
];
|
||||
|
||||
public static readonly literalNames = [
|
||||
null, "'workspace'", "'result-bytes'", "'rpc-calls'", "'deadline-ms'",
|
||||
"'query'", "'specialize'", "'root'", "'document'", "'fragments'",
|
||||
"'view'", "'max'", "'allow'", "'poll'", "'queryable'", "'rpc'",
|
||||
"'query-reason'", "'type'", "'object'", "'storable'", "'implements'",
|
||||
"'ref'", "'fragment'", "'import'", "'external'", "'atom'", "'interface'",
|
||||
"'interfaces'", "'package'", "'value'", "'relation'", "'operation'",
|
||||
"'function'", "'constructor'", "'constructs'", "'input'", "'conform'",
|
||||
"'as'", "'bind'", "'static'", "'to'", "'private'", "'shared'", "'state'",
|
||||
"'edge'", "'projection'", "'with'", "'using'", "'via'", "'materialize'",
|
||||
"'if'", "'absent'", "'on'", "'policy'", "'default'", "'source'",
|
||||
"'repository'", "'commit'", "'revision'", "'semantic-major'", "'on-delete'",
|
||||
"'retain-other'", "'keyed'", "'public-traversal'", "'id'", "'doc'",
|
||||
"'mode'", "'emits'", "'receiver'", "'requires'", "'any'", "'get'",
|
||||
"'set'", "'watch'", "'start'", "'stop'", "'read'", "'write'", "'resolve'",
|
||||
"'connect'", "'disconnect'", "'call'", "'watch-start'", "'watch-stop'",
|
||||
"'subscribe'", "'unsubscribe'", "'optimistic-register'", "'crdt'",
|
||||
"'optional-one'", "'exactly-one'", "'many-unique'", "'many'", "'ordered'",
|
||||
"'unit'", "'watch-handle'", "'message'", "'atom-ref'", "'interface-ref'",
|
||||
"'optional'", "'list'", "'record'", "'bool'", "'bytes'", "'double'",
|
||||
"'int32'", "'int64'", "'string'", "'uint32'", "'uint64'", "'true'",
|
||||
"'false'", "'null'", "'->'", "':'", "';'", "','", "'.'", "'{'",
|
||||
"'}'", "'['", "']'", "'('", "')'", "'<'", "'>'", "'&'", "'='"
|
||||
];
|
||||
|
||||
public static readonly symbolicNames = [
|
||||
null, "WORKSPACE", "RESULT_BYTES", "RPC_CALLS", "DEADLINE_MS", "QUERY",
|
||||
"SPECIALIZE", "ROOT", "DOCUMENT", "FRAGMENTS", "VIEW", "MAX", "ALLOW",
|
||||
"POLL", "QUERYABLE", "RPC", "QUERY_REASON", "TYPE", "OBJECT", "STORABLE",
|
||||
"IMPLEMENTS", "REF", "FRAGMENT", "IMPORT", "EXTERNAL", "ATOM", "INTERFACE",
|
||||
"INTERFACES", "PACKAGE", "VALUE", "RELATION", "OPERATION", "FUNCTION",
|
||||
"CONSTRUCTOR", "CONSTRUCTS", "INPUT", "CONFORM", "AS", "BIND", "STATIC",
|
||||
"TO", "PRIVATE", "SHARED", "STATE", "EDGE", "PROJECTION", "WITH",
|
||||
"USING", "VIA", "MATERIALIZE", "IF", "ABSENT", "ON", "POLICY", "DEFAULT",
|
||||
"SOURCE", "REPOSITORY", "COMMIT", "REVISION", "SEMANTIC_MAJOR",
|
||||
"ON_DELETE", "RETAIN_OTHER", "KEYED", "PUBLIC_TRAVERSAL", "ID",
|
||||
"DOC", "MODE", "EMITS", "RECEIVER", "REQUIRES", "ANY", "GET", "SET",
|
||||
"WATCH", "START", "STOP", "READ", "WRITE", "RESOLVE", "CONNECT",
|
||||
"DISCONNECT", "CALL", "WATCH_START", "WATCH_STOP", "SUBSCRIBE",
|
||||
"UNSUBSCRIBE", "OPTIMISTIC_REGISTER", "CRDT", "OPTIONAL_ONE", "EXACTLY_ONE",
|
||||
"MANY_UNIQUE", "MANY", "ORDERED", "UNIT", "WATCH_HANDLE", "MESSAGE",
|
||||
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "RECORD", "BOOL",
|
||||
"BYTES", "DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64",
|
||||
"TRUE", "FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT",
|
||||
"LBRACE", "RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT",
|
||||
"GT", "AMP", "EQUAL", "INTEGER", "JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL",
|
||||
"LINE_COMMENT", "BLOCK_COMMENT", "WS"
|
||||
];
|
||||
|
||||
public static readonly modeNames = [
|
||||
"DEFAULT_MODE",
|
||||
];
|
||||
|
||||
public static readonly ruleNames = [
|
||||
"WORKSPACE", "RESULT_BYTES", "RPC_CALLS", "DEADLINE_MS", "QUERY",
|
||||
"SPECIALIZE", "ROOT", "DOCUMENT", "FRAGMENTS", "VIEW", "MAX", "ALLOW",
|
||||
"POLL", "QUERYABLE", "RPC", "QUERY_REASON", "TYPE", "OBJECT", "STORABLE",
|
||||
"IMPLEMENTS", "REF", "FRAGMENT", "IMPORT", "EXTERNAL", "ATOM", "INTERFACE",
|
||||
"INTERFACES", "PACKAGE", "VALUE", "RELATION", "OPERATION", "FUNCTION",
|
||||
"CONSTRUCTOR", "CONSTRUCTS", "INPUT", "CONFORM", "AS", "BIND", "STATIC",
|
||||
"TO", "PRIVATE", "SHARED", "STATE", "EDGE", "PROJECTION", "WITH",
|
||||
"USING", "VIA", "MATERIALIZE", "IF", "ABSENT", "ON", "POLICY", "DEFAULT",
|
||||
"SOURCE", "REPOSITORY", "COMMIT", "REVISION", "SEMANTIC_MAJOR",
|
||||
"ON_DELETE", "RETAIN_OTHER", "KEYED", "PUBLIC_TRAVERSAL", "ID",
|
||||
"DOC", "MODE", "EMITS", "RECEIVER", "REQUIRES", "ANY", "GET", "SET",
|
||||
"WATCH", "START", "STOP", "READ", "WRITE", "RESOLVE", "CONNECT",
|
||||
"DISCONNECT", "CALL", "WATCH_START", "WATCH_STOP", "SUBSCRIBE",
|
||||
"UNSUBSCRIBE", "OPTIMISTIC_REGISTER", "CRDT", "OPTIONAL_ONE", "EXACTLY_ONE",
|
||||
"MANY_UNIQUE", "MANY", "ORDERED", "UNIT", "WATCH_HANDLE", "MESSAGE",
|
||||
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "RECORD", "BOOL",
|
||||
"BYTES", "DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64",
|
||||
"TRUE", "FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT",
|
||||
"LBRACE", "RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT",
|
||||
"GT", "AMP", "EQUAL", "INTEGER", "JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL",
|
||||
"ESC", "HEX", "LINE_COMMENT", "BLOCK_COMMENT", "WS",
|
||||
];
|
||||
|
||||
|
||||
public constructor(input: antlr.CharStream) {
|
||||
super(input);
|
||||
this.interpreter = new antlr.LexerATNSimulator(this, QuixosCapabilityLexer._ATN, QuixosCapabilityLexer.decisionsToDFA, new antlr.PredictionContextCache());
|
||||
}
|
||||
|
||||
public get grammarFileName(): string { return "QuixosCapability.g4"; }
|
||||
|
||||
public get literalNames(): (string | null)[] { return QuixosCapabilityLexer.literalNames; }
|
||||
public get symbolicNames(): (string | null)[] { return QuixosCapabilityLexer.symbolicNames; }
|
||||
public get ruleNames(): string[] { return QuixosCapabilityLexer.ruleNames; }
|
||||
|
||||
public get serializedATN(): number[] { return QuixosCapabilityLexer._serializedATN; }
|
||||
|
||||
public get channelNames(): string[] { return QuixosCapabilityLexer.channelNames; }
|
||||
|
||||
public get modeNames(): string[] { return QuixosCapabilityLexer.modeNames; }
|
||||
|
||||
public static readonly _serializedATN: number[] = [
|
||||
4,0,133,1272,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,
|
||||
5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,
|
||||
2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,
|
||||
7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,
|
||||
2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,
|
||||
7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,
|
||||
2,39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,
|
||||
7,45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,
|
||||
2,52,7,52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,
|
||||
7,58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63,2,64,7,64,
|
||||
2,65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70,2,71,
|
||||
7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,
|
||||
2,78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,
|
||||
7,84,2,85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,
|
||||
2,91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97,
|
||||
7,97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103,
|
||||
7,103,2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108,
|
||||
2,109,7,109,2,110,7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114,
|
||||
7,114,2,115,7,115,2,116,7,116,2,117,7,117,2,118,7,118,2,119,7,119,
|
||||
2,120,7,120,2,121,7,121,2,122,7,122,2,123,7,123,2,124,7,124,2,125,
|
||||
7,125,2,126,7,126,2,127,7,127,2,128,7,128,2,129,7,129,2,130,7,130,
|
||||
2,131,7,131,2,132,7,132,2,133,7,133,2,134,7,134,1,0,1,0,1,0,1,0,
|
||||
1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,
|
||||
1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,5,
|
||||
1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,7,
|
||||
1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,8,
|
||||
1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,
|
||||
11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,
|
||||
13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,
|
||||
15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,
|
||||
16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,
|
||||
18,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,
|
||||
19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,
|
||||
21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,
|
||||
23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,
|
||||
25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,
|
||||
26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,
|
||||
28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,
|
||||
29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,31,1,
|
||||
31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,
|
||||
32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,
|
||||
33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,34,1,35,1,35,1,
|
||||
35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,
|
||||
37,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,40,1,40,1,
|
||||
40,1,40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,
|
||||
42,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,43,1,44,1,44,1,
|
||||
44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,45,1,45,1,45,1,45,1,
|
||||
45,1,46,1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,48,1,48,1,
|
||||
48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,
|
||||
50,1,50,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,52,1,52,1,52,1,
|
||||
52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,54,1,
|
||||
54,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,
|
||||
55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,
|
||||
57,1,57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,
|
||||
58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,
|
||||
59,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,
|
||||
60,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,62,1,
|
||||
62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,
|
||||
62,1,62,1,62,1,63,1,63,1,63,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,
|
||||
65,1,65,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,1,67,1,
|
||||
67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,
|
||||
69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,71,1,71,1,71,1,71,1,72,1,
|
||||
72,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,
|
||||
74,1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,
|
||||
76,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,
|
||||
78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,
|
||||
79,1,79,1,80,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81,1,81,1,
|
||||
81,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,
|
||||
82,1,82,1,82,1,82,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,
|
||||
83,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,
|
||||
85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,
|
||||
85,1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1,86,1,87,1,
|
||||
87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,88,1,
|
||||
88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,89,1,89,1,
|
||||
89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,
|
||||
90,1,90,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,92,1,92,1,92,1,
|
||||
92,1,92,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,
|
||||
93,1,93,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,95,1,95,1,95,1,
|
||||
95,1,95,1,95,1,95,1,95,1,95,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,
|
||||
96,1,96,1,96,1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,97,1,97,1,
|
||||
97,1,97,1,97,1,98,1,98,1,98,1,98,1,98,1,99,1,99,1,99,1,99,1,99,1,
|
||||
99,1,99,1,100,1,100,1,100,1,100,1,100,1,101,1,101,1,101,1,101,1,
|
||||
101,1,101,1,102,1,102,1,102,1,102,1,102,1,102,1,102,1,103,1,103,
|
||||
1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,104,1,104,1,105,
|
||||
1,105,1,105,1,105,1,105,1,105,1,105,1,106,1,106,1,106,1,106,1,106,
|
||||
1,106,1,106,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,108,1,108,
|
||||
1,108,1,108,1,108,1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110,
|
||||
1,110,1,110,1,110,1,111,1,111,1,111,1,112,1,112,1,113,1,113,1,114,
|
||||
1,114,1,115,1,115,1,116,1,116,1,117,1,117,1,118,1,118,1,119,1,119,
|
||||
1,120,1,120,1,121,1,121,1,122,1,122,1,123,1,123,1,124,1,124,1,125,
|
||||
1,125,1,126,3,126,1173,8,126,1,126,4,126,1176,8,126,11,126,12,126,
|
||||
1177,1,127,3,127,1181,8,127,1,127,1,127,1,127,5,127,1186,8,127,10,
|
||||
127,12,127,1189,9,127,3,127,1191,8,127,1,127,1,127,4,127,1195,8,
|
||||
127,11,127,12,127,1196,3,127,1199,8,127,1,127,1,127,3,127,1203,8,
|
||||
127,1,127,4,127,1206,8,127,11,127,12,127,1207,3,127,1210,8,127,1,
|
||||
128,1,128,5,128,1214,8,128,10,128,12,128,1217,9,128,1,129,1,129,
|
||||
1,129,5,129,1222,8,129,10,129,12,129,1225,9,129,1,129,1,129,1,130,
|
||||
1,130,1,130,1,130,1,130,1,130,1,130,1,130,3,130,1237,8,130,1,131,
|
||||
1,131,1,132,1,132,1,132,1,132,5,132,1245,8,132,10,132,12,132,1248,
|
||||
9,132,1,132,1,132,1,133,1,133,1,133,1,133,5,133,1256,8,133,10,133,
|
||||
12,133,1259,9,133,1,133,1,133,1,133,1,133,1,133,1,134,4,134,1267,
|
||||
8,134,11,134,12,134,1268,1,134,1,134,1,1257,0,135,1,1,3,2,5,3,7,
|
||||
4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
|
||||
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,
|
||||
27,55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,
|
||||
38,77,39,79,40,81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,
|
||||
49,99,50,101,51,103,52,105,53,107,54,109,55,111,56,113,57,115,58,
|
||||
117,59,119,60,121,61,123,62,125,63,127,64,129,65,131,66,133,67,135,
|
||||
68,137,69,139,70,141,71,143,72,145,73,147,74,149,75,151,76,153,77,
|
||||
155,78,157,79,159,80,161,81,163,82,165,83,167,84,169,85,171,86,173,
|
||||
87,175,88,177,89,179,90,181,91,183,92,185,93,187,94,189,95,191,96,
|
||||
193,97,195,98,197,99,199,100,201,101,203,102,205,103,207,104,209,
|
||||
105,211,106,213,107,215,108,217,109,219,110,221,111,223,112,225,
|
||||
113,227,114,229,115,231,116,233,117,235,118,237,119,239,120,241,
|
||||
121,243,122,245,123,247,124,249,125,251,126,253,127,255,128,257,
|
||||
129,259,130,261,0,263,0,265,131,267,132,269,133,1,0,11,1,0,48,57,
|
||||
1,0,49,57,2,0,69,69,101,101,2,0,43,43,45,45,3,0,65,90,95,95,97,122,
|
||||
4,0,48,57,65,90,95,95,97,122,4,0,10,10,13,13,34,34,92,92,8,0,34,
|
||||
34,47,47,92,92,98,98,102,102,110,110,114,114,116,116,3,0,48,57,65,
|
||||
70,97,102,2,0,10,10,13,13,3,0,9,10,13,13,32,32,1286,0,1,1,0,0,0,
|
||||
0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,
|
||||
1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,
|
||||
1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,
|
||||
1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,
|
||||
1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,
|
||||
1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,
|
||||
1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,
|
||||
1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,
|
||||
1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,
|
||||
1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,
|
||||
1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0,0,109,1,0,0,0,0,111,1,0,0,0,
|
||||
0,113,1,0,0,0,0,115,1,0,0,0,0,117,1,0,0,0,0,119,1,0,0,0,0,121,1,
|
||||
0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0,127,1,0,0,0,0,129,1,0,0,0,0,
|
||||
131,1,0,0,0,0,133,1,0,0,0,0,135,1,0,0,0,0,137,1,0,0,0,0,139,1,0,
|
||||
0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145,1,0,0,0,0,147,1,0,0,0,0,149,
|
||||
1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0,155,1,0,0,0,0,157,1,0,0,0,
|
||||
0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,0,165,1,0,0,0,0,167,1,
|
||||
0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0,175,1,0,0,0,0,
|
||||
177,1,0,0,0,0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0,0,0,0,185,1,0,
|
||||
0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191,1,0,0,0,0,193,1,0,0,0,0,195,
|
||||
1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0,0,201,1,0,0,0,0,203,1,0,0,0,
|
||||
0,205,1,0,0,0,0,207,1,0,0,0,0,209,1,0,0,0,0,211,1,0,0,0,0,213,1,
|
||||
0,0,0,0,215,1,0,0,0,0,217,1,0,0,0,0,219,1,0,0,0,0,221,1,0,0,0,0,
|
||||
223,1,0,0,0,0,225,1,0,0,0,0,227,1,0,0,0,0,229,1,0,0,0,0,231,1,0,
|
||||
0,0,0,233,1,0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,0,239,1,0,0,0,0,241,
|
||||
1,0,0,0,0,243,1,0,0,0,0,245,1,0,0,0,0,247,1,0,0,0,0,249,1,0,0,0,
|
||||
0,251,1,0,0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1,0,0,0,0,259,1,
|
||||
0,0,0,0,265,1,0,0,0,0,267,1,0,0,0,0,269,1,0,0,0,1,271,1,0,0,0,3,
|
||||
281,1,0,0,0,5,294,1,0,0,0,7,304,1,0,0,0,9,316,1,0,0,0,11,322,1,0,
|
||||
0,0,13,333,1,0,0,0,15,338,1,0,0,0,17,347,1,0,0,0,19,357,1,0,0,0,
|
||||
21,362,1,0,0,0,23,366,1,0,0,0,25,372,1,0,0,0,27,377,1,0,0,0,29,387,
|
||||
1,0,0,0,31,391,1,0,0,0,33,404,1,0,0,0,35,409,1,0,0,0,37,416,1,0,
|
||||
0,0,39,425,1,0,0,0,41,436,1,0,0,0,43,440,1,0,0,0,45,449,1,0,0,0,
|
||||
47,456,1,0,0,0,49,465,1,0,0,0,51,470,1,0,0,0,53,480,1,0,0,0,55,491,
|
||||
1,0,0,0,57,499,1,0,0,0,59,505,1,0,0,0,61,514,1,0,0,0,63,524,1,0,
|
||||
0,0,65,533,1,0,0,0,67,545,1,0,0,0,69,556,1,0,0,0,71,562,1,0,0,0,
|
||||
73,570,1,0,0,0,75,573,1,0,0,0,77,578,1,0,0,0,79,585,1,0,0,0,81,588,
|
||||
1,0,0,0,83,596,1,0,0,0,85,603,1,0,0,0,87,609,1,0,0,0,89,614,1,0,
|
||||
0,0,91,625,1,0,0,0,93,630,1,0,0,0,95,636,1,0,0,0,97,640,1,0,0,0,
|
||||
99,652,1,0,0,0,101,655,1,0,0,0,103,662,1,0,0,0,105,665,1,0,0,0,107,
|
||||
672,1,0,0,0,109,680,1,0,0,0,111,687,1,0,0,0,113,698,1,0,0,0,115,
|
||||
705,1,0,0,0,117,714,1,0,0,0,119,729,1,0,0,0,121,739,1,0,0,0,123,
|
||||
752,1,0,0,0,125,758,1,0,0,0,127,775,1,0,0,0,129,778,1,0,0,0,131,
|
||||
782,1,0,0,0,133,787,1,0,0,0,135,793,1,0,0,0,137,802,1,0,0,0,139,
|
||||
811,1,0,0,0,141,815,1,0,0,0,143,819,1,0,0,0,145,823,1,0,0,0,147,
|
||||
829,1,0,0,0,149,835,1,0,0,0,151,840,1,0,0,0,153,845,1,0,0,0,155,
|
||||
851,1,0,0,0,157,859,1,0,0,0,159,867,1,0,0,0,161,878,1,0,0,0,163,
|
||||
883,1,0,0,0,165,895,1,0,0,0,167,906,1,0,0,0,169,916,1,0,0,0,171,
|
||||
928,1,0,0,0,173,948,1,0,0,0,175,953,1,0,0,0,177,966,1,0,0,0,179,
|
||||
978,1,0,0,0,181,990,1,0,0,0,183,995,1,0,0,0,185,1003,1,0,0,0,187,
|
||||
1008,1,0,0,0,189,1021,1,0,0,0,191,1029,1,0,0,0,193,1038,1,0,0,0,
|
||||
195,1052,1,0,0,0,197,1061,1,0,0,0,199,1066,1,0,0,0,201,1073,1,0,
|
||||
0,0,203,1078,1,0,0,0,205,1084,1,0,0,0,207,1091,1,0,0,0,209,1097,
|
||||
1,0,0,0,211,1103,1,0,0,0,213,1110,1,0,0,0,215,1117,1,0,0,0,217,1124,
|
||||
1,0,0,0,219,1129,1,0,0,0,221,1135,1,0,0,0,223,1140,1,0,0,0,225,1143,
|
||||
1,0,0,0,227,1145,1,0,0,0,229,1147,1,0,0,0,231,1149,1,0,0,0,233,1151,
|
||||
1,0,0,0,235,1153,1,0,0,0,237,1155,1,0,0,0,239,1157,1,0,0,0,241,1159,
|
||||
1,0,0,0,243,1161,1,0,0,0,245,1163,1,0,0,0,247,1165,1,0,0,0,249,1167,
|
||||
1,0,0,0,251,1169,1,0,0,0,253,1172,1,0,0,0,255,1180,1,0,0,0,257,1211,
|
||||
1,0,0,0,259,1218,1,0,0,0,261,1228,1,0,0,0,263,1238,1,0,0,0,265,1240,
|
||||
1,0,0,0,267,1251,1,0,0,0,269,1266,1,0,0,0,271,272,5,119,0,0,272,
|
||||
273,5,111,0,0,273,274,5,114,0,0,274,275,5,107,0,0,275,276,5,115,
|
||||
0,0,276,277,5,112,0,0,277,278,5,97,0,0,278,279,5,99,0,0,279,280,
|
||||
5,101,0,0,280,2,1,0,0,0,281,282,5,114,0,0,282,283,5,101,0,0,283,
|
||||
284,5,115,0,0,284,285,5,117,0,0,285,286,5,108,0,0,286,287,5,116,
|
||||
0,0,287,288,5,45,0,0,288,289,5,98,0,0,289,290,5,121,0,0,290,291,
|
||||
5,116,0,0,291,292,5,101,0,0,292,293,5,115,0,0,293,4,1,0,0,0,294,
|
||||
295,5,114,0,0,295,296,5,112,0,0,296,297,5,99,0,0,297,298,5,45,0,
|
||||
0,298,299,5,99,0,0,299,300,5,97,0,0,300,301,5,108,0,0,301,302,5,
|
||||
108,0,0,302,303,5,115,0,0,303,6,1,0,0,0,304,305,5,100,0,0,305,306,
|
||||
5,101,0,0,306,307,5,97,0,0,307,308,5,100,0,0,308,309,5,108,0,0,309,
|
||||
310,5,105,0,0,310,311,5,110,0,0,311,312,5,101,0,0,312,313,5,45,0,
|
||||
0,313,314,5,109,0,0,314,315,5,115,0,0,315,8,1,0,0,0,316,317,5,113,
|
||||
0,0,317,318,5,117,0,0,318,319,5,101,0,0,319,320,5,114,0,0,320,321,
|
||||
5,121,0,0,321,10,1,0,0,0,322,323,5,115,0,0,323,324,5,112,0,0,324,
|
||||
325,5,101,0,0,325,326,5,99,0,0,326,327,5,105,0,0,327,328,5,97,0,
|
||||
0,328,329,5,108,0,0,329,330,5,105,0,0,330,331,5,122,0,0,331,332,
|
||||
5,101,0,0,332,12,1,0,0,0,333,334,5,114,0,0,334,335,5,111,0,0,335,
|
||||
336,5,111,0,0,336,337,5,116,0,0,337,14,1,0,0,0,338,339,5,100,0,0,
|
||||
339,340,5,111,0,0,340,341,5,99,0,0,341,342,5,117,0,0,342,343,5,109,
|
||||
0,0,343,344,5,101,0,0,344,345,5,110,0,0,345,346,5,116,0,0,346,16,
|
||||
1,0,0,0,347,348,5,102,0,0,348,349,5,114,0,0,349,350,5,97,0,0,350,
|
||||
351,5,103,0,0,351,352,5,109,0,0,352,353,5,101,0,0,353,354,5,110,
|
||||
0,0,354,355,5,116,0,0,355,356,5,115,0,0,356,18,1,0,0,0,357,358,5,
|
||||
118,0,0,358,359,5,105,0,0,359,360,5,101,0,0,360,361,5,119,0,0,361,
|
||||
20,1,0,0,0,362,363,5,109,0,0,363,364,5,97,0,0,364,365,5,120,0,0,
|
||||
365,22,1,0,0,0,366,367,5,97,0,0,367,368,5,108,0,0,368,369,5,108,
|
||||
0,0,369,370,5,111,0,0,370,371,5,119,0,0,371,24,1,0,0,0,372,373,5,
|
||||
112,0,0,373,374,5,111,0,0,374,375,5,108,0,0,375,376,5,108,0,0,376,
|
||||
26,1,0,0,0,377,378,5,113,0,0,378,379,5,117,0,0,379,380,5,101,0,0,
|
||||
380,381,5,114,0,0,381,382,5,121,0,0,382,383,5,97,0,0,383,384,5,98,
|
||||
0,0,384,385,5,108,0,0,385,386,5,101,0,0,386,28,1,0,0,0,387,388,5,
|
||||
114,0,0,388,389,5,112,0,0,389,390,5,99,0,0,390,30,1,0,0,0,391,392,
|
||||
5,113,0,0,392,393,5,117,0,0,393,394,5,101,0,0,394,395,5,114,0,0,
|
||||
395,396,5,121,0,0,396,397,5,45,0,0,397,398,5,114,0,0,398,399,5,101,
|
||||
0,0,399,400,5,97,0,0,400,401,5,115,0,0,401,402,5,111,0,0,402,403,
|
||||
5,110,0,0,403,32,1,0,0,0,404,405,5,116,0,0,405,406,5,121,0,0,406,
|
||||
407,5,112,0,0,407,408,5,101,0,0,408,34,1,0,0,0,409,410,5,111,0,0,
|
||||
410,411,5,98,0,0,411,412,5,106,0,0,412,413,5,101,0,0,413,414,5,99,
|
||||
0,0,414,415,5,116,0,0,415,36,1,0,0,0,416,417,5,115,0,0,417,418,5,
|
||||
116,0,0,418,419,5,111,0,0,419,420,5,114,0,0,420,421,5,97,0,0,421,
|
||||
422,5,98,0,0,422,423,5,108,0,0,423,424,5,101,0,0,424,38,1,0,0,0,
|
||||
425,426,5,105,0,0,426,427,5,109,0,0,427,428,5,112,0,0,428,429,5,
|
||||
108,0,0,429,430,5,101,0,0,430,431,5,109,0,0,431,432,5,101,0,0,432,
|
||||
433,5,110,0,0,433,434,5,116,0,0,434,435,5,115,0,0,435,40,1,0,0,0,
|
||||
436,437,5,114,0,0,437,438,5,101,0,0,438,439,5,102,0,0,439,42,1,0,
|
||||
0,0,440,441,5,102,0,0,441,442,5,114,0,0,442,443,5,97,0,0,443,444,
|
||||
5,103,0,0,444,445,5,109,0,0,445,446,5,101,0,0,446,447,5,110,0,0,
|
||||
447,448,5,116,0,0,448,44,1,0,0,0,449,450,5,105,0,0,450,451,5,109,
|
||||
0,0,451,452,5,112,0,0,452,453,5,111,0,0,453,454,5,114,0,0,454,455,
|
||||
5,116,0,0,455,46,1,0,0,0,456,457,5,101,0,0,457,458,5,120,0,0,458,
|
||||
459,5,116,0,0,459,460,5,101,0,0,460,461,5,114,0,0,461,462,5,110,
|
||||
0,0,462,463,5,97,0,0,463,464,5,108,0,0,464,48,1,0,0,0,465,466,5,
|
||||
97,0,0,466,467,5,116,0,0,467,468,5,111,0,0,468,469,5,109,0,0,469,
|
||||
50,1,0,0,0,470,471,5,105,0,0,471,472,5,110,0,0,472,473,5,116,0,0,
|
||||
473,474,5,101,0,0,474,475,5,114,0,0,475,476,5,102,0,0,476,477,5,
|
||||
97,0,0,477,478,5,99,0,0,478,479,5,101,0,0,479,52,1,0,0,0,480,481,
|
||||
5,105,0,0,481,482,5,110,0,0,482,483,5,116,0,0,483,484,5,101,0,0,
|
||||
484,485,5,114,0,0,485,486,5,102,0,0,486,487,5,97,0,0,487,488,5,99,
|
||||
0,0,488,489,5,101,0,0,489,490,5,115,0,0,490,54,1,0,0,0,491,492,5,
|
||||
112,0,0,492,493,5,97,0,0,493,494,5,99,0,0,494,495,5,107,0,0,495,
|
||||
496,5,97,0,0,496,497,5,103,0,0,497,498,5,101,0,0,498,56,1,0,0,0,
|
||||
499,500,5,118,0,0,500,501,5,97,0,0,501,502,5,108,0,0,502,503,5,117,
|
||||
0,0,503,504,5,101,0,0,504,58,1,0,0,0,505,506,5,114,0,0,506,507,5,
|
||||
101,0,0,507,508,5,108,0,0,508,509,5,97,0,0,509,510,5,116,0,0,510,
|
||||
511,5,105,0,0,511,512,5,111,0,0,512,513,5,110,0,0,513,60,1,0,0,0,
|
||||
514,515,5,111,0,0,515,516,5,112,0,0,516,517,5,101,0,0,517,518,5,
|
||||
114,0,0,518,519,5,97,0,0,519,520,5,116,0,0,520,521,5,105,0,0,521,
|
||||
522,5,111,0,0,522,523,5,110,0,0,523,62,1,0,0,0,524,525,5,102,0,0,
|
||||
525,526,5,117,0,0,526,527,5,110,0,0,527,528,5,99,0,0,528,529,5,116,
|
||||
0,0,529,530,5,105,0,0,530,531,5,111,0,0,531,532,5,110,0,0,532,64,
|
||||
1,0,0,0,533,534,5,99,0,0,534,535,5,111,0,0,535,536,5,110,0,0,536,
|
||||
537,5,115,0,0,537,538,5,116,0,0,538,539,5,114,0,0,539,540,5,117,
|
||||
0,0,540,541,5,99,0,0,541,542,5,116,0,0,542,543,5,111,0,0,543,544,
|
||||
5,114,0,0,544,66,1,0,0,0,545,546,5,99,0,0,546,547,5,111,0,0,547,
|
||||
548,5,110,0,0,548,549,5,115,0,0,549,550,5,116,0,0,550,551,5,114,
|
||||
0,0,551,552,5,117,0,0,552,553,5,99,0,0,553,554,5,116,0,0,554,555,
|
||||
5,115,0,0,555,68,1,0,0,0,556,557,5,105,0,0,557,558,5,110,0,0,558,
|
||||
559,5,112,0,0,559,560,5,117,0,0,560,561,5,116,0,0,561,70,1,0,0,0,
|
||||
562,563,5,99,0,0,563,564,5,111,0,0,564,565,5,110,0,0,565,566,5,102,
|
||||
0,0,566,567,5,111,0,0,567,568,5,114,0,0,568,569,5,109,0,0,569,72,
|
||||
1,0,0,0,570,571,5,97,0,0,571,572,5,115,0,0,572,74,1,0,0,0,573,574,
|
||||
5,98,0,0,574,575,5,105,0,0,575,576,5,110,0,0,576,577,5,100,0,0,577,
|
||||
76,1,0,0,0,578,579,5,115,0,0,579,580,5,116,0,0,580,581,5,97,0,0,
|
||||
581,582,5,116,0,0,582,583,5,105,0,0,583,584,5,99,0,0,584,78,1,0,
|
||||
0,0,585,586,5,116,0,0,586,587,5,111,0,0,587,80,1,0,0,0,588,589,5,
|
||||
112,0,0,589,590,5,114,0,0,590,591,5,105,0,0,591,592,5,118,0,0,592,
|
||||
593,5,97,0,0,593,594,5,116,0,0,594,595,5,101,0,0,595,82,1,0,0,0,
|
||||
596,597,5,115,0,0,597,598,5,104,0,0,598,599,5,97,0,0,599,600,5,114,
|
||||
0,0,600,601,5,101,0,0,601,602,5,100,0,0,602,84,1,0,0,0,603,604,5,
|
||||
115,0,0,604,605,5,116,0,0,605,606,5,97,0,0,606,607,5,116,0,0,607,
|
||||
608,5,101,0,0,608,86,1,0,0,0,609,610,5,101,0,0,610,611,5,100,0,0,
|
||||
611,612,5,103,0,0,612,613,5,101,0,0,613,88,1,0,0,0,614,615,5,112,
|
||||
0,0,615,616,5,114,0,0,616,617,5,111,0,0,617,618,5,106,0,0,618,619,
|
||||
5,101,0,0,619,620,5,99,0,0,620,621,5,116,0,0,621,622,5,105,0,0,622,
|
||||
623,5,111,0,0,623,624,5,110,0,0,624,90,1,0,0,0,625,626,5,119,0,0,
|
||||
626,627,5,105,0,0,627,628,5,116,0,0,628,629,5,104,0,0,629,92,1,0,
|
||||
0,0,630,631,5,117,0,0,631,632,5,115,0,0,632,633,5,105,0,0,633,634,
|
||||
5,110,0,0,634,635,5,103,0,0,635,94,1,0,0,0,636,637,5,118,0,0,637,
|
||||
638,5,105,0,0,638,639,5,97,0,0,639,96,1,0,0,0,640,641,5,109,0,0,
|
||||
641,642,5,97,0,0,642,643,5,116,0,0,643,644,5,101,0,0,644,645,5,114,
|
||||
0,0,645,646,5,105,0,0,646,647,5,97,0,0,647,648,5,108,0,0,648,649,
|
||||
5,105,0,0,649,650,5,122,0,0,650,651,5,101,0,0,651,98,1,0,0,0,652,
|
||||
653,5,105,0,0,653,654,5,102,0,0,654,100,1,0,0,0,655,656,5,97,0,0,
|
||||
656,657,5,98,0,0,657,658,5,115,0,0,658,659,5,101,0,0,659,660,5,110,
|
||||
0,0,660,661,5,116,0,0,661,102,1,0,0,0,662,663,5,111,0,0,663,664,
|
||||
5,110,0,0,664,104,1,0,0,0,665,666,5,112,0,0,666,667,5,111,0,0,667,
|
||||
668,5,108,0,0,668,669,5,105,0,0,669,670,5,99,0,0,670,671,5,121,0,
|
||||
0,671,106,1,0,0,0,672,673,5,100,0,0,673,674,5,101,0,0,674,675,5,
|
||||
102,0,0,675,676,5,97,0,0,676,677,5,117,0,0,677,678,5,108,0,0,678,
|
||||
679,5,116,0,0,679,108,1,0,0,0,680,681,5,115,0,0,681,682,5,111,0,
|
||||
0,682,683,5,117,0,0,683,684,5,114,0,0,684,685,5,99,0,0,685,686,5,
|
||||
101,0,0,686,110,1,0,0,0,687,688,5,114,0,0,688,689,5,101,0,0,689,
|
||||
690,5,112,0,0,690,691,5,111,0,0,691,692,5,115,0,0,692,693,5,105,
|
||||
0,0,693,694,5,116,0,0,694,695,5,111,0,0,695,696,5,114,0,0,696,697,
|
||||
5,121,0,0,697,112,1,0,0,0,698,699,5,99,0,0,699,700,5,111,0,0,700,
|
||||
701,5,109,0,0,701,702,5,109,0,0,702,703,5,105,0,0,703,704,5,116,
|
||||
0,0,704,114,1,0,0,0,705,706,5,114,0,0,706,707,5,101,0,0,707,708,
|
||||
5,118,0,0,708,709,5,105,0,0,709,710,5,115,0,0,710,711,5,105,0,0,
|
||||
711,712,5,111,0,0,712,713,5,110,0,0,713,116,1,0,0,0,714,715,5,115,
|
||||
0,0,715,716,5,101,0,0,716,717,5,109,0,0,717,718,5,97,0,0,718,719,
|
||||
5,110,0,0,719,720,5,116,0,0,720,721,5,105,0,0,721,722,5,99,0,0,722,
|
||||
723,5,45,0,0,723,724,5,109,0,0,724,725,5,97,0,0,725,726,5,106,0,
|
||||
0,726,727,5,111,0,0,727,728,5,114,0,0,728,118,1,0,0,0,729,730,5,
|
||||
111,0,0,730,731,5,110,0,0,731,732,5,45,0,0,732,733,5,100,0,0,733,
|
||||
734,5,101,0,0,734,735,5,108,0,0,735,736,5,101,0,0,736,737,5,116,
|
||||
0,0,737,738,5,101,0,0,738,120,1,0,0,0,739,740,5,114,0,0,740,741,
|
||||
5,101,0,0,741,742,5,116,0,0,742,743,5,97,0,0,743,744,5,105,0,0,744,
|
||||
745,5,110,0,0,745,746,5,45,0,0,746,747,5,111,0,0,747,748,5,116,0,
|
||||
0,748,749,5,104,0,0,749,750,5,101,0,0,750,751,5,114,0,0,751,122,
|
||||
1,0,0,0,752,753,5,107,0,0,753,754,5,101,0,0,754,755,5,121,0,0,755,
|
||||
756,5,101,0,0,756,757,5,100,0,0,757,124,1,0,0,0,758,759,5,112,0,
|
||||
0,759,760,5,117,0,0,760,761,5,98,0,0,761,762,5,108,0,0,762,763,5,
|
||||
105,0,0,763,764,5,99,0,0,764,765,5,45,0,0,765,766,5,116,0,0,766,
|
||||
767,5,114,0,0,767,768,5,97,0,0,768,769,5,118,0,0,769,770,5,101,0,
|
||||
0,770,771,5,114,0,0,771,772,5,115,0,0,772,773,5,97,0,0,773,774,5,
|
||||
108,0,0,774,126,1,0,0,0,775,776,5,105,0,0,776,777,5,100,0,0,777,
|
||||
128,1,0,0,0,778,779,5,100,0,0,779,780,5,111,0,0,780,781,5,99,0,0,
|
||||
781,130,1,0,0,0,782,783,5,109,0,0,783,784,5,111,0,0,784,785,5,100,
|
||||
0,0,785,786,5,101,0,0,786,132,1,0,0,0,787,788,5,101,0,0,788,789,
|
||||
5,109,0,0,789,790,5,105,0,0,790,791,5,116,0,0,791,792,5,115,0,0,
|
||||
792,134,1,0,0,0,793,794,5,114,0,0,794,795,5,101,0,0,795,796,5,99,
|
||||
0,0,796,797,5,101,0,0,797,798,5,105,0,0,798,799,5,118,0,0,799,800,
|
||||
5,101,0,0,800,801,5,114,0,0,801,136,1,0,0,0,802,803,5,114,0,0,803,
|
||||
804,5,101,0,0,804,805,5,113,0,0,805,806,5,117,0,0,806,807,5,105,
|
||||
0,0,807,808,5,114,0,0,808,809,5,101,0,0,809,810,5,115,0,0,810,138,
|
||||
1,0,0,0,811,812,5,97,0,0,812,813,5,110,0,0,813,814,5,121,0,0,814,
|
||||
140,1,0,0,0,815,816,5,103,0,0,816,817,5,101,0,0,817,818,5,116,0,
|
||||
0,818,142,1,0,0,0,819,820,5,115,0,0,820,821,5,101,0,0,821,822,5,
|
||||
116,0,0,822,144,1,0,0,0,823,824,5,119,0,0,824,825,5,97,0,0,825,826,
|
||||
5,116,0,0,826,827,5,99,0,0,827,828,5,104,0,0,828,146,1,0,0,0,829,
|
||||
830,5,115,0,0,830,831,5,116,0,0,831,832,5,97,0,0,832,833,5,114,0,
|
||||
0,833,834,5,116,0,0,834,148,1,0,0,0,835,836,5,115,0,0,836,837,5,
|
||||
116,0,0,837,838,5,111,0,0,838,839,5,112,0,0,839,150,1,0,0,0,840,
|
||||
841,5,114,0,0,841,842,5,101,0,0,842,843,5,97,0,0,843,844,5,100,0,
|
||||
0,844,152,1,0,0,0,845,846,5,119,0,0,846,847,5,114,0,0,847,848,5,
|
||||
105,0,0,848,849,5,116,0,0,849,850,5,101,0,0,850,154,1,0,0,0,851,
|
||||
852,5,114,0,0,852,853,5,101,0,0,853,854,5,115,0,0,854,855,5,111,
|
||||
0,0,855,856,5,108,0,0,856,857,5,118,0,0,857,858,5,101,0,0,858,156,
|
||||
1,0,0,0,859,860,5,99,0,0,860,861,5,111,0,0,861,862,5,110,0,0,862,
|
||||
863,5,110,0,0,863,864,5,101,0,0,864,865,5,99,0,0,865,866,5,116,0,
|
||||
0,866,158,1,0,0,0,867,868,5,100,0,0,868,869,5,105,0,0,869,870,5,
|
||||
115,0,0,870,871,5,99,0,0,871,872,5,111,0,0,872,873,5,110,0,0,873,
|
||||
874,5,110,0,0,874,875,5,101,0,0,875,876,5,99,0,0,876,877,5,116,0,
|
||||
0,877,160,1,0,0,0,878,879,5,99,0,0,879,880,5,97,0,0,880,881,5,108,
|
||||
0,0,881,882,5,108,0,0,882,162,1,0,0,0,883,884,5,119,0,0,884,885,
|
||||
5,97,0,0,885,886,5,116,0,0,886,887,5,99,0,0,887,888,5,104,0,0,888,
|
||||
889,5,45,0,0,889,890,5,115,0,0,890,891,5,116,0,0,891,892,5,97,0,
|
||||
0,892,893,5,114,0,0,893,894,5,116,0,0,894,164,1,0,0,0,895,896,5,
|
||||
119,0,0,896,897,5,97,0,0,897,898,5,116,0,0,898,899,5,99,0,0,899,
|
||||
900,5,104,0,0,900,901,5,45,0,0,901,902,5,115,0,0,902,903,5,116,0,
|
||||
0,903,904,5,111,0,0,904,905,5,112,0,0,905,166,1,0,0,0,906,907,5,
|
||||
115,0,0,907,908,5,117,0,0,908,909,5,98,0,0,909,910,5,115,0,0,910,
|
||||
911,5,99,0,0,911,912,5,114,0,0,912,913,5,105,0,0,913,914,5,98,0,
|
||||
0,914,915,5,101,0,0,915,168,1,0,0,0,916,917,5,117,0,0,917,918,5,
|
||||
110,0,0,918,919,5,115,0,0,919,920,5,117,0,0,920,921,5,98,0,0,921,
|
||||
922,5,115,0,0,922,923,5,99,0,0,923,924,5,114,0,0,924,925,5,105,0,
|
||||
0,925,926,5,98,0,0,926,927,5,101,0,0,927,170,1,0,0,0,928,929,5,111,
|
||||
0,0,929,930,5,112,0,0,930,931,5,116,0,0,931,932,5,105,0,0,932,933,
|
||||
5,109,0,0,933,934,5,105,0,0,934,935,5,115,0,0,935,936,5,116,0,0,
|
||||
936,937,5,105,0,0,937,938,5,99,0,0,938,939,5,45,0,0,939,940,5,114,
|
||||
0,0,940,941,5,101,0,0,941,942,5,103,0,0,942,943,5,105,0,0,943,944,
|
||||
5,115,0,0,944,945,5,116,0,0,945,946,5,101,0,0,946,947,5,114,0,0,
|
||||
947,172,1,0,0,0,948,949,5,99,0,0,949,950,5,114,0,0,950,951,5,100,
|
||||
0,0,951,952,5,116,0,0,952,174,1,0,0,0,953,954,5,111,0,0,954,955,
|
||||
5,112,0,0,955,956,5,116,0,0,956,957,5,105,0,0,957,958,5,111,0,0,
|
||||
958,959,5,110,0,0,959,960,5,97,0,0,960,961,5,108,0,0,961,962,5,45,
|
||||
0,0,962,963,5,111,0,0,963,964,5,110,0,0,964,965,5,101,0,0,965,176,
|
||||
1,0,0,0,966,967,5,101,0,0,967,968,5,120,0,0,968,969,5,97,0,0,969,
|
||||
970,5,99,0,0,970,971,5,116,0,0,971,972,5,108,0,0,972,973,5,121,0,
|
||||
0,973,974,5,45,0,0,974,975,5,111,0,0,975,976,5,110,0,0,976,977,5,
|
||||
101,0,0,977,178,1,0,0,0,978,979,5,109,0,0,979,980,5,97,0,0,980,981,
|
||||
5,110,0,0,981,982,5,121,0,0,982,983,5,45,0,0,983,984,5,117,0,0,984,
|
||||
985,5,110,0,0,985,986,5,105,0,0,986,987,5,113,0,0,987,988,5,117,
|
||||
0,0,988,989,5,101,0,0,989,180,1,0,0,0,990,991,5,109,0,0,991,992,
|
||||
5,97,0,0,992,993,5,110,0,0,993,994,5,121,0,0,994,182,1,0,0,0,995,
|
||||
996,5,111,0,0,996,997,5,114,0,0,997,998,5,100,0,0,998,999,5,101,
|
||||
0,0,999,1000,5,114,0,0,1000,1001,5,101,0,0,1001,1002,5,100,0,0,1002,
|
||||
184,1,0,0,0,1003,1004,5,117,0,0,1004,1005,5,110,0,0,1005,1006,5,
|
||||
105,0,0,1006,1007,5,116,0,0,1007,186,1,0,0,0,1008,1009,5,119,0,0,
|
||||
1009,1010,5,97,0,0,1010,1011,5,116,0,0,1011,1012,5,99,0,0,1012,1013,
|
||||
5,104,0,0,1013,1014,5,45,0,0,1014,1015,5,104,0,0,1015,1016,5,97,
|
||||
0,0,1016,1017,5,110,0,0,1017,1018,5,100,0,0,1018,1019,5,108,0,0,
|
||||
1019,1020,5,101,0,0,1020,188,1,0,0,0,1021,1022,5,109,0,0,1022,1023,
|
||||
5,101,0,0,1023,1024,5,115,0,0,1024,1025,5,115,0,0,1025,1026,5,97,
|
||||
0,0,1026,1027,5,103,0,0,1027,1028,5,101,0,0,1028,190,1,0,0,0,1029,
|
||||
1030,5,97,0,0,1030,1031,5,116,0,0,1031,1032,5,111,0,0,1032,1033,
|
||||
5,109,0,0,1033,1034,5,45,0,0,1034,1035,5,114,0,0,1035,1036,5,101,
|
||||
0,0,1036,1037,5,102,0,0,1037,192,1,0,0,0,1038,1039,5,105,0,0,1039,
|
||||
1040,5,110,0,0,1040,1041,5,116,0,0,1041,1042,5,101,0,0,1042,1043,
|
||||
5,114,0,0,1043,1044,5,102,0,0,1044,1045,5,97,0,0,1045,1046,5,99,
|
||||
0,0,1046,1047,5,101,0,0,1047,1048,5,45,0,0,1048,1049,5,114,0,0,1049,
|
||||
1050,5,101,0,0,1050,1051,5,102,0,0,1051,194,1,0,0,0,1052,1053,5,
|
||||
111,0,0,1053,1054,5,112,0,0,1054,1055,5,116,0,0,1055,1056,5,105,
|
||||
0,0,1056,1057,5,111,0,0,1057,1058,5,110,0,0,1058,1059,5,97,0,0,1059,
|
||||
1060,5,108,0,0,1060,196,1,0,0,0,1061,1062,5,108,0,0,1062,1063,5,
|
||||
105,0,0,1063,1064,5,115,0,0,1064,1065,5,116,0,0,1065,198,1,0,0,0,
|
||||
1066,1067,5,114,0,0,1067,1068,5,101,0,0,1068,1069,5,99,0,0,1069,
|
||||
1070,5,111,0,0,1070,1071,5,114,0,0,1071,1072,5,100,0,0,1072,200,
|
||||
1,0,0,0,1073,1074,5,98,0,0,1074,1075,5,111,0,0,1075,1076,5,111,0,
|
||||
0,1076,1077,5,108,0,0,1077,202,1,0,0,0,1078,1079,5,98,0,0,1079,1080,
|
||||
5,121,0,0,1080,1081,5,116,0,0,1081,1082,5,101,0,0,1082,1083,5,115,
|
||||
0,0,1083,204,1,0,0,0,1084,1085,5,100,0,0,1085,1086,5,111,0,0,1086,
|
||||
1087,5,117,0,0,1087,1088,5,98,0,0,1088,1089,5,108,0,0,1089,1090,
|
||||
5,101,0,0,1090,206,1,0,0,0,1091,1092,5,105,0,0,1092,1093,5,110,0,
|
||||
0,1093,1094,5,116,0,0,1094,1095,5,51,0,0,1095,1096,5,50,0,0,1096,
|
||||
208,1,0,0,0,1097,1098,5,105,0,0,1098,1099,5,110,0,0,1099,1100,5,
|
||||
116,0,0,1100,1101,5,54,0,0,1101,1102,5,52,0,0,1102,210,1,0,0,0,1103,
|
||||
1104,5,115,0,0,1104,1105,5,116,0,0,1105,1106,5,114,0,0,1106,1107,
|
||||
5,105,0,0,1107,1108,5,110,0,0,1108,1109,5,103,0,0,1109,212,1,0,0,
|
||||
0,1110,1111,5,117,0,0,1111,1112,5,105,0,0,1112,1113,5,110,0,0,1113,
|
||||
1114,5,116,0,0,1114,1115,5,51,0,0,1115,1116,5,50,0,0,1116,214,1,
|
||||
0,0,0,1117,1118,5,117,0,0,1118,1119,5,105,0,0,1119,1120,5,110,0,
|
||||
0,1120,1121,5,116,0,0,1121,1122,5,54,0,0,1122,1123,5,52,0,0,1123,
|
||||
216,1,0,0,0,1124,1125,5,116,0,0,1125,1126,5,114,0,0,1126,1127,5,
|
||||
117,0,0,1127,1128,5,101,0,0,1128,218,1,0,0,0,1129,1130,5,102,0,0,
|
||||
1130,1131,5,97,0,0,1131,1132,5,108,0,0,1132,1133,5,115,0,0,1133,
|
||||
1134,5,101,0,0,1134,220,1,0,0,0,1135,1136,5,110,0,0,1136,1137,5,
|
||||
117,0,0,1137,1138,5,108,0,0,1138,1139,5,108,0,0,1139,222,1,0,0,0,
|
||||
1140,1141,5,45,0,0,1141,1142,5,62,0,0,1142,224,1,0,0,0,1143,1144,
|
||||
5,58,0,0,1144,226,1,0,0,0,1145,1146,5,59,0,0,1146,228,1,0,0,0,1147,
|
||||
1148,5,44,0,0,1148,230,1,0,0,0,1149,1150,5,46,0,0,1150,232,1,0,0,
|
||||
0,1151,1152,5,123,0,0,1152,234,1,0,0,0,1153,1154,5,125,0,0,1154,
|
||||
236,1,0,0,0,1155,1156,5,91,0,0,1156,238,1,0,0,0,1157,1158,5,93,0,
|
||||
0,1158,240,1,0,0,0,1159,1160,5,40,0,0,1160,242,1,0,0,0,1161,1162,
|
||||
5,41,0,0,1162,244,1,0,0,0,1163,1164,5,60,0,0,1164,246,1,0,0,0,1165,
|
||||
1166,5,62,0,0,1166,248,1,0,0,0,1167,1168,5,38,0,0,1168,250,1,0,0,
|
||||
0,1169,1170,5,61,0,0,1170,252,1,0,0,0,1171,1173,5,45,0,0,1172,1171,
|
||||
1,0,0,0,1172,1173,1,0,0,0,1173,1175,1,0,0,0,1174,1176,7,0,0,0,1175,
|
||||
1174,1,0,0,0,1176,1177,1,0,0,0,1177,1175,1,0,0,0,1177,1178,1,0,0,
|
||||
0,1178,254,1,0,0,0,1179,1181,5,45,0,0,1180,1179,1,0,0,0,1180,1181,
|
||||
1,0,0,0,1181,1190,1,0,0,0,1182,1191,5,48,0,0,1183,1187,7,1,0,0,1184,
|
||||
1186,7,0,0,0,1185,1184,1,0,0,0,1186,1189,1,0,0,0,1187,1185,1,0,0,
|
||||
0,1187,1188,1,0,0,0,1188,1191,1,0,0,0,1189,1187,1,0,0,0,1190,1182,
|
||||
1,0,0,0,1190,1183,1,0,0,0,1191,1198,1,0,0,0,1192,1194,5,46,0,0,1193,
|
||||
1195,7,0,0,0,1194,1193,1,0,0,0,1195,1196,1,0,0,0,1196,1194,1,0,0,
|
||||
0,1196,1197,1,0,0,0,1197,1199,1,0,0,0,1198,1192,1,0,0,0,1198,1199,
|
||||
1,0,0,0,1199,1209,1,0,0,0,1200,1202,7,2,0,0,1201,1203,7,3,0,0,1202,
|
||||
1201,1,0,0,0,1202,1203,1,0,0,0,1203,1205,1,0,0,0,1204,1206,7,0,0,
|
||||
0,1205,1204,1,0,0,0,1206,1207,1,0,0,0,1207,1205,1,0,0,0,1207,1208,
|
||||
1,0,0,0,1208,1210,1,0,0,0,1209,1200,1,0,0,0,1209,1210,1,0,0,0,1210,
|
||||
256,1,0,0,0,1211,1215,7,4,0,0,1212,1214,7,5,0,0,1213,1212,1,0,0,
|
||||
0,1214,1217,1,0,0,0,1215,1213,1,0,0,0,1215,1216,1,0,0,0,1216,258,
|
||||
1,0,0,0,1217,1215,1,0,0,0,1218,1223,5,34,0,0,1219,1222,3,261,130,
|
||||
0,1220,1222,8,6,0,0,1221,1219,1,0,0,0,1221,1220,1,0,0,0,1222,1225,
|
||||
1,0,0,0,1223,1221,1,0,0,0,1223,1224,1,0,0,0,1224,1226,1,0,0,0,1225,
|
||||
1223,1,0,0,0,1226,1227,5,34,0,0,1227,260,1,0,0,0,1228,1236,5,92,
|
||||
0,0,1229,1237,7,7,0,0,1230,1231,5,117,0,0,1231,1232,3,263,131,0,
|
||||
1232,1233,3,263,131,0,1233,1234,3,263,131,0,1234,1235,3,263,131,
|
||||
0,1235,1237,1,0,0,0,1236,1229,1,0,0,0,1236,1230,1,0,0,0,1237,262,
|
||||
1,0,0,0,1238,1239,7,8,0,0,1239,264,1,0,0,0,1240,1241,5,47,0,0,1241,
|
||||
1242,5,47,0,0,1242,1246,1,0,0,0,1243,1245,8,9,0,0,1244,1243,1,0,
|
||||
0,0,1245,1248,1,0,0,0,1246,1244,1,0,0,0,1246,1247,1,0,0,0,1247,1249,
|
||||
1,0,0,0,1248,1246,1,0,0,0,1249,1250,6,132,0,0,1250,266,1,0,0,0,1251,
|
||||
1252,5,47,0,0,1252,1253,5,42,0,0,1253,1257,1,0,0,0,1254,1256,9,0,
|
||||
0,0,1255,1254,1,0,0,0,1256,1259,1,0,0,0,1257,1258,1,0,0,0,1257,1255,
|
||||
1,0,0,0,1258,1260,1,0,0,0,1259,1257,1,0,0,0,1260,1261,5,42,0,0,1261,
|
||||
1262,5,47,0,0,1262,1263,1,0,0,0,1263,1264,6,133,0,0,1264,268,1,0,
|
||||
0,0,1265,1267,7,10,0,0,1266,1265,1,0,0,0,1267,1268,1,0,0,0,1268,
|
||||
1266,1,0,0,0,1268,1269,1,0,0,0,1269,1270,1,0,0,0,1270,1271,6,134,
|
||||
0,0,1271,270,1,0,0,0,18,0,1172,1177,1180,1187,1190,1196,1198,1202,
|
||||
1207,1209,1215,1221,1223,1236,1246,1257,1268,1,0,1,0
|
||||
];
|
||||
|
||||
private static __ATN: antlr.ATN;
|
||||
public static get _ATN(): antlr.ATN {
|
||||
if (!QuixosCapabilityLexer.__ATN) {
|
||||
QuixosCapabilityLexer.__ATN = new antlr.ATNDeserializer().deserialize(QuixosCapabilityLexer._serializedATN);
|
||||
}
|
||||
|
||||
return QuixosCapabilityLexer.__ATN;
|
||||
}
|
||||
|
||||
|
||||
private static readonly vocabulary = new antlr.Vocabulary(QuixosCapabilityLexer.literalNames, QuixosCapabilityLexer.symbolicNames, []);
|
||||
|
||||
public override get vocabulary(): antlr.Vocabulary {
|
||||
return QuixosCapabilityLexer.vocabulary;
|
||||
}
|
||||
|
||||
private static readonly decisionsToDFA = QuixosCapabilityLexer._ATN.decisionToState.map( (ds: antlr.DecisionState, index: number) => new antlr.DFA(ds, index) );
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,506 @@
|
||||
|
||||
import { AbstractParseTreeVisitor } from "antlr4ng";
|
||||
|
||||
|
||||
import { DocumentContext } from "./QuixosCapabilityParser.js";
|
||||
import { FragmentDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { SourceImportDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { WorkspaceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { WorkspaceItemContext } from "./QuixosCapabilityParser.js";
|
||||
import { ResourceImportDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ExternalAtomDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
|
||||
import { AtomDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeParametersContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeParameterContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceTypeContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js";
|
||||
import { RelationshipMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { QueryReadContractContext } from "./QuixosCapabilityParser.js";
|
||||
import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
|
||||
import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
|
||||
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { EventClauseContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationModeContext } from "./QuixosCapabilityParser.js";
|
||||
import { ReceiverRequirementContext } from "./QuixosCapabilityParser.js";
|
||||
import { IdentifierListContext } from "./QuixosCapabilityParser.js";
|
||||
import { DependencyBlockContext } from "./QuixosCapabilityParser.js";
|
||||
import { DependencyPortContext } from "./QuixosCapabilityParser.js";
|
||||
import { PrimitiveListContext } from "./QuixosCapabilityParser.js";
|
||||
import { PrimitiveContext } from "./QuixosCapabilityParser.js";
|
||||
import { SharedAttachmentDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { AttachmentDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { StateDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { StoragePolicyContext } from "./QuixosCapabilityParser.js";
|
||||
import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
|
||||
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
|
||||
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationNameContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationProviderContext } from "./QuixosCapabilityParser.js";
|
||||
import { StatePrimitiveContext } from "./QuixosCapabilityParser.js";
|
||||
import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js";
|
||||
import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
|
||||
import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
|
||||
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ValueTypeContext } from "./QuixosCapabilityParser.js";
|
||||
import { RecordFieldContext } from "./QuixosCapabilityParser.js";
|
||||
import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
|
||||
import { CardinalityContext } from "./QuixosCapabilityParser.js";
|
||||
import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
|
||||
import { JsonObjectContext } from "./QuixosCapabilityParser.js";
|
||||
import { JsonMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { JsonArrayContext } from "./QuixosCapabilityParser.js";
|
||||
import { IdentifierContext } from "./QuixosCapabilityParser.js";
|
||||
import { StringLiteralContext } from "./QuixosCapabilityParser.js";
|
||||
|
||||
|
||||
/**
|
||||
* This interface defines a complete generic visitor for a parse tree produced
|
||||
* by `QuixosCapabilityParser`.
|
||||
*
|
||||
* @param <Result> The return type of the visit operation. Use `void` for
|
||||
* operations with no return type.
|
||||
*/
|
||||
export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Result> {
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.document`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDocument?: (ctx: DocumentContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitFragmentDecl?: (ctx: FragmentDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.sourceImportDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSourceImportDecl?: (ctx: SourceImportDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.workspaceDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitWorkspaceDecl?: (ctx: WorkspaceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.workspaceItem`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitWorkspaceItem?: (ctx: WorkspaceItemContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.resourceImportDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitResourceImportDecl?: (ctx: ResourceImportDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.externalAtomDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitExternalAtomDecl?: (ctx: ExternalAtomDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.externalInterfaceDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitExternalInterfaceDecl?: (ctx: ExternalInterfaceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.resourcePreamble`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitResourcePreamble?: (ctx: ResourcePreambleContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.atomDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitAtomDecl?: (ctx: AtomDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceResourceDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitInterfaceResourceDecl?: (ctx: InterfaceResourceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameters`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeParameters?: (ctx: TypeParametersContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameter`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeParameter?: (ctx: TypeParameterContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceType`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitInterfaceType?: (ctx: InterfaceTypeContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeArguments`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeArguments?: (ctx: TypeArgumentsContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeArgument`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeArgument?: (ctx: TypeArgumentContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeAliasDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeAliasDecl?: (ctx: TypeAliasDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceMember`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitInterfaceMember?: (ctx: InterfaceMemberContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.operationMember`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitOperationMember?: (ctx: OperationMemberContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.valueMember`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitValueMember?: (ctx: ValueMemberContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.valueMemberOperation`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitValueMemberOperation?: (ctx: ValueMemberOperationContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMember`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitRelationshipMember?: (ctx: RelationshipMemberContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.queryReadContract`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitQueryReadContract?: (ctx: QueryReadContractContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipOperation`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitRelationshipOperation?: (ctx: RelationshipOperationContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.targetConstraint`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTargetConstraint?: (ctx: TargetConstraintContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageResourceDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageResourceDecl?: (ctx: PackageResourceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageExport`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageExport?: (ctx: PackageExportContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuerySpecialization`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageQuerySpecialization?: (ctx: PackageQuerySpecializationContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitQueryClause?: (ctx: QueryClauseContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageOperationExport?: (ctx: PackageOperationExportContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageFunctionExport`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.packageConstructorExport`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPackageConstructorExport?: (ctx: PackageConstructorExportContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.eventClause`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitEventClause?: (ctx: EventClauseContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.operationMode`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitOperationMode?: (ctx: OperationModeContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.receiverRequirement`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitReceiverRequirement?: (ctx: ReceiverRequirementContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.identifierList`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitIdentifierList?: (ctx: IdentifierListContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.dependencyBlock`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDependencyBlock?: (ctx: DependencyBlockContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.dependencyPort`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDependencyPort?: (ctx: DependencyPortContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.primitiveList`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPrimitiveList?: (ctx: PrimitiveListContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.primitive`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitPrimitive?: (ctx: PrimitiveContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.sharedAttachmentDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSharedAttachmentDecl?: (ctx: SharedAttachmentDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.attachmentDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitAttachmentDecl?: (ctx: AttachmentDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.stateDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStateDecl?: (ctx: StateDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.storagePolicy`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStoragePolicy?: (ctx: StoragePolicyContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.edgeDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitEdgeDecl?: (ctx: EdgeDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.edgeEndpoint`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitEdgeEndpoint?: (ctx: EdgeEndpointContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitConformanceDecl?: (ctx: ConformanceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.conformanceItem`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStateFieldBindingDecl?: (ctx: StateFieldBindingDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMaterializationDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitRelationshipMaterializationDecl?: (ctx: RelationshipMaterializationDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.operationBindingDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitOperationBindingDecl?: (ctx: OperationBindingDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.memberOperationRef`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitMemberOperationRef?: (ctx: MemberOperationRefContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.operationName`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitOperationName?: (ctx: OperationNameContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.operationProvider`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitOperationProvider?: (ctx: OperationProviderContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStatePrimitive?: (ctx: StatePrimitiveContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.edgePrimitive`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitEdgePrimitive?: (ctx: EdgePrimitiveContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.dependencyBindingBlock`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDependencyBindingBlock?: (ctx: DependencyBindingBlockContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.dependencyBinding`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDependencyBinding?: (ctx: DependencyBindingContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.constructorBindingDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitConstructorBindingDecl?: (ctx: ConstructorBindingDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.valueType`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitValueType?: (ctx: ValueTypeContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.recordField`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitRecordField?: (ctx: RecordFieldContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitScalarType?: (ctx: ScalarTypeContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.cardinality`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitCardinality?: (ctx: CardinalityContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.jsonLiteral`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitJsonLiteral?: (ctx: JsonLiteralContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.jsonObject`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitJsonObject?: (ctx: JsonObjectContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.jsonMember`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitJsonMember?: (ctx: JsonMemberContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.jsonArray`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitJsonArray?: (ctx: JsonArrayContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.identifier`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitIdentifier?: (ctx: IdentifierContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.stringLiteral`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStringLiteral?: (ctx: StringLiteralContext) => Result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
import {
|
||||
TypeSubstitution,
|
||||
GenericTypeError,
|
||||
instantiateInterface,
|
||||
appliedInterfaceId,
|
||||
valueType,
|
||||
type AtomId,
|
||||
type ClosedTypeArgument,
|
||||
type GenericTypeEnvironment,
|
||||
type InterfaceApplicationExpression,
|
||||
type InterfaceRevision,
|
||||
type ObjectTypeExpression,
|
||||
type TypeArgumentExpression,
|
||||
type TypeParameter,
|
||||
type ValueAliasDefinition,
|
||||
type ValueType,
|
||||
type ValueTypeExpression,
|
||||
} from "../capability-model/index.js";
|
||||
import type {
|
||||
InterfaceTypeContext,
|
||||
TargetConstraintContext,
|
||||
TypeArgumentsContext,
|
||||
TypeParametersContext,
|
||||
TypeAliasDeclContext,
|
||||
ValueTypeContext,
|
||||
} from "./generated/QuixosCapabilityParser.js";
|
||||
|
||||
const literal = (context: { getText(): string }) => JSON.parse(context.getText()) as string;
|
||||
|
||||
/** Lexical authoring scope; only `value`/`target` return installed types. */
|
||||
export class GenericSourceTypes {
|
||||
readonly interfaces = new Map<string, InterfaceRevision>();
|
||||
readonly definitions = new Map<string, InterfaceRevision>();
|
||||
readonly applications = new Map<string, InterfaceRevision>();
|
||||
readonly aliases = new Map<string, ValueAliasDefinition>();
|
||||
parameters = new Map<string, TypeParameter>();
|
||||
self?: AtomId;
|
||||
private active: string[] = [];
|
||||
private applicationCount = 0;
|
||||
|
||||
constructor(readonly atoms: Map<string, AtomId>) {}
|
||||
|
||||
environment(): GenericTypeEnvironment {
|
||||
return {
|
||||
arguments: new Map(),
|
||||
aliases: this.aliases,
|
||||
self: this.self,
|
||||
applyInterface: (id, args) => this.apply(id, args).revisionId,
|
||||
// Obligations are retained on the application and discharged by workspace
|
||||
// validation, where all atom conformances are known (not source order).
|
||||
implementsInterface: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
register(name: string, definition: InterfaceRevision) {
|
||||
this.interfaces.set(name, definition);
|
||||
this.definitions.set(definition.revisionId, definition);
|
||||
}
|
||||
|
||||
/** Check authored applications even when nobody has instantiated this template yet. */
|
||||
validateTemplate(definition: InterfaceRevision) {
|
||||
const template = definition.template;
|
||||
if (!template) return;
|
||||
const aliases = new Map((template.aliases ?? []).map((alias) => [alias.id, alias]));
|
||||
const parametersById = new Map(
|
||||
[...template.parameters, ...(template.aliases ?? []).flatMap((alias) => alias.parameters)].map((parameter) => [
|
||||
parameter.id,
|
||||
parameter,
|
||||
]),
|
||||
);
|
||||
const replace = (node: unknown, arguments_: Map<string, TypeArgumentExpression>): unknown => {
|
||||
if (!node || typeof node !== "object") return node;
|
||||
if ("kind" in node && node.kind === "parameter" && "parameterId" in node) {
|
||||
const arg = arguments_.get(String(node.parameterId));
|
||||
if (arg) return arg.kind === "value" ? arg.type : arg.target;
|
||||
}
|
||||
if (Array.isArray(node)) return node.map((entry) => replace(entry, arguments_));
|
||||
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, replace(value, arguments_)]));
|
||||
};
|
||||
const implies = (
|
||||
actual: InterfaceApplicationExpression,
|
||||
required: InterfaceApplicationExpression,
|
||||
seen = new Set<string>(),
|
||||
): boolean => {
|
||||
const key = JSON.stringify(actual);
|
||||
if (key === JSON.stringify(required)) return true;
|
||||
if (seen.has(key) || seen.size > 128) return false;
|
||||
seen.add(key);
|
||||
const contract = this.definitions.get(actual.definitionId);
|
||||
if (!contract?.template)
|
||||
return (contract?.requiredInterfaces ?? []).includes(required.definitionId) && required.arguments.length === 0;
|
||||
const args = new Map(
|
||||
contract.template.parameters.map((parameter, index) => [parameter.id, actual.arguments[index]]),
|
||||
);
|
||||
return contract.template.requires.some((parent) =>
|
||||
implies(replace(parent, args) as InterfaceApplicationExpression, required, seen),
|
||||
);
|
||||
};
|
||||
const storable = (type: ValueTypeExpression, depth = 0): boolean => {
|
||||
if (depth > 128)
|
||||
throw new GenericTypeError(
|
||||
"type-complexity-limit",
|
||||
definition.displayName,
|
||||
"Storable alias expansion exceeds depth limit",
|
||||
);
|
||||
if (type.kind === "parameter") {
|
||||
const parameter = parametersById.get(type.parameterId);
|
||||
return parameter?.kind === "value" && Boolean(parameter.storable);
|
||||
}
|
||||
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
|
||||
if (type.kind === "alias") {
|
||||
const alias = aliases.get(type.definitionId);
|
||||
if (!alias || alias.parameters.length !== type.arguments.length) return false;
|
||||
return storable(
|
||||
replace(
|
||||
alias.body,
|
||||
new Map(alias.parameters.map((parameter, index) => [parameter.id, type.arguments[index]])),
|
||||
) as ValueTypeExpression,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
return type.kind === "scalar" || (type.kind === "builtin" && type.name === "unit");
|
||||
};
|
||||
let remaining = 10000;
|
||||
const visit = (node: unknown, path: string, depth = 0): void => {
|
||||
if (depth > 128 || --remaining < 0)
|
||||
throw new GenericTypeError("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
|
||||
if (!node || typeof node !== "object") return;
|
||||
if ("definitionId" in node && "arguments" in node) {
|
||||
const application = node as InterfaceApplicationExpression | Extract<ValueTypeExpression, { kind: "alias" }>;
|
||||
const alias = "kind" in application && application.kind === "alias";
|
||||
const target = alias ? aliases.get(application.definitionId) : this.definitions.get(application.definitionId);
|
||||
if (!target)
|
||||
throw new GenericTypeError(
|
||||
alias ? "unknown-alias" : "unknown-interface",
|
||||
path,
|
||||
`Unknown definition ${application.definitionId}`,
|
||||
);
|
||||
if ("template" in target && target.template?.usesSelf) template.usesSelf = true;
|
||||
const parameters = "parameters" in target ? target.parameters : (target.template?.parameters ?? []);
|
||||
if (parameters.length !== application.arguments.length)
|
||||
throw new GenericTypeError(
|
||||
"type-arity",
|
||||
path,
|
||||
`Expected ${parameters.length} type arguments, received ${application.arguments.length}`,
|
||||
);
|
||||
parameters.forEach((parameter, index) => {
|
||||
if (parameter.kind !== application.arguments[index].kind)
|
||||
throw new GenericTypeError(
|
||||
"parameter-kind",
|
||||
path,
|
||||
`Expected ${parameter.kind}, received ${application.arguments[index].kind}`,
|
||||
);
|
||||
const argument = application.arguments[index];
|
||||
if (parameter.kind === "value" && parameter.storable && argument.kind === "value" && !storable(argument.type))
|
||||
throw new GenericTypeError(
|
||||
"non-storable-argument",
|
||||
path,
|
||||
"Generic application cannot prove its value argument is storable",
|
||||
);
|
||||
if (parameter.kind === "object" && argument.kind === "object" && argument.target.kind === "parameter") {
|
||||
const offered = parametersById.get(argument.target.parameterId);
|
||||
const mapping = new Map(parameters.map((p, i) => [p.id, application.arguments[i]]));
|
||||
for (const bound of parameter.implements) {
|
||||
const required = replace(bound, mapping) as InterfaceApplicationExpression;
|
||||
if (offered?.kind !== "object" || !offered.implements.some((evidence) => implies(evidence, required)))
|
||||
throw new GenericTypeError(
|
||||
"unsatisfied-bound",
|
||||
path,
|
||||
`Object parameter does not prove ${required.definitionId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const [name, child] of Object.entries(node)) visit(child, `${path}.${name}`, depth + 1);
|
||||
};
|
||||
visit(template, definition.displayName);
|
||||
const active = new Set<string>();
|
||||
const done = new Set<string>();
|
||||
const checkAlias = (id: string) => {
|
||||
if (done.has(id)) return;
|
||||
if (active.has(id))
|
||||
throw new GenericTypeError("recursive-alias", id, `Recursive value alias: ${[...active, id].join(" -> ")}`);
|
||||
active.add(id);
|
||||
const walk = (node: unknown): void => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if ("kind" in node && node.kind === "alias")
|
||||
checkAlias((node as ValueAliasDefinition["body"] & { definitionId: string }).definitionId);
|
||||
Object.values(node).forEach(walk);
|
||||
};
|
||||
walk(aliases.get(id)?.body);
|
||||
active.delete(id);
|
||||
done.add(id);
|
||||
};
|
||||
for (const id of aliases.keys()) checkAlias(id);
|
||||
}
|
||||
|
||||
apply(id: string, arguments_: readonly ClosedTypeArgument[]): InterfaceRevision {
|
||||
const definition = this.definitions.get(id);
|
||||
if (!definition) throw new GenericTypeError("unknown-interface", id, "Unknown interface definition");
|
||||
const application = {
|
||||
definitionId: definition.revisionId,
|
||||
source: definition.source,
|
||||
arguments: [...arguments_],
|
||||
...(definition.template?.usesSelf ? { self: this.self } : {}),
|
||||
};
|
||||
const appliedId = definition.template ? appliedInterfaceId(application) : definition.revisionId;
|
||||
const cached = this.applications.get(appliedId);
|
||||
if (cached) return cached;
|
||||
if (this.active.includes(id))
|
||||
throw new GenericTypeError(
|
||||
"recursive-application",
|
||||
id,
|
||||
`Expanding recursive interface application: ${[...this.active, id].join(" -> ")}`,
|
||||
);
|
||||
if (++this.applicationCount > 10000)
|
||||
throw new GenericTypeError("type-complexity-limit", id, "Too many interface applications");
|
||||
this.active.push(id);
|
||||
if (definition.template)
|
||||
this.applications.set(appliedId, {
|
||||
interfaceId: definition.interfaceId,
|
||||
revisionId: appliedId,
|
||||
displayName: definition.displayName,
|
||||
source: definition.source,
|
||||
members: [],
|
||||
application,
|
||||
});
|
||||
try {
|
||||
const obligations: NonNullable<InterfaceRevision["argumentRequirements"]> = [];
|
||||
const instance = instantiateInterface(definition, arguments_, {
|
||||
...this.environment(),
|
||||
implementsInterface: (target, required) => {
|
||||
obligations.push({ target, required });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (instance === definition) return definition;
|
||||
instance.argumentRequirements = obligations;
|
||||
this.applications.set(instance.revisionId, instance);
|
||||
this.definitions.set(instance.revisionId, instance);
|
||||
return instance;
|
||||
} catch (error) {
|
||||
this.applications.delete(appliedId);
|
||||
throw error;
|
||||
} finally {
|
||||
this.active.pop();
|
||||
}
|
||||
}
|
||||
|
||||
interface(context: InterfaceTypeContext): InterfaceApplicationExpression {
|
||||
return this.interfaceByName(context.identifier().getText(), context.typeArguments());
|
||||
}
|
||||
|
||||
interfaceByName(name: string, arguments_: TypeArgumentsContext | null): InterfaceApplicationExpression {
|
||||
const definition = this.interfaces.get(name);
|
||||
if (!definition) throw new GenericTypeError("unknown-interface", name, "Unknown interface");
|
||||
return { definitionId: definition.revisionId, arguments: this.arguments(arguments_) };
|
||||
}
|
||||
|
||||
arguments(context: TypeArgumentsContext | null): TypeArgumentExpression[] {
|
||||
return (context?.typeArgument() ?? []).map((argument): TypeArgumentExpression => {
|
||||
if (argument.INTERFACE())
|
||||
return {
|
||||
kind: "object",
|
||||
target: { kind: "application", application: this.interface(argument.interfaceType()!) },
|
||||
};
|
||||
if (argument.ATOM()) return { kind: "object", target: this.atom(argument.identifier()!.getText()) };
|
||||
if (argument.OBJECT()) return { kind: "object", target: this.objectParameter(argument.identifier()!.getText()) };
|
||||
const type = argument.valueType()!;
|
||||
if (type.getText() === "Self") return { kind: "object", target: { kind: "self" } };
|
||||
const parameter = type.identifier() && this.parameters.get(type.identifier()!.getText());
|
||||
if (parameter?.kind === "object" && !type.typeArguments())
|
||||
return { kind: "object", target: { kind: "parameter", parameterId: parameter.id } };
|
||||
return { kind: "value", type: this.expression(type) };
|
||||
});
|
||||
}
|
||||
|
||||
private atom(name: string): ObjectTypeExpression {
|
||||
if (name === "Self") return { kind: "self" };
|
||||
const atomId = this.atoms.get(name);
|
||||
if (!atomId) throw new GenericTypeError("unknown-atom", name, "Unknown atom");
|
||||
return { kind: "atom", atomId };
|
||||
}
|
||||
|
||||
private objectParameter(name: string): ObjectTypeExpression {
|
||||
if (name === "Self") return { kind: "self" };
|
||||
const parameter = this.parameters.get(name);
|
||||
if (!parameter || parameter.kind !== "object")
|
||||
throw new GenericTypeError("parameter-kind", name, "Expected an object parameter");
|
||||
return { kind: "parameter", parameterId: parameter.id };
|
||||
}
|
||||
|
||||
targetExpression(context: TargetConstraintContext): ObjectTypeExpression {
|
||||
const name = context.identifier().getText();
|
||||
if (context.ATOM()) return this.atom(name);
|
||||
if (context.OBJECT()) return this.objectParameter(name);
|
||||
return { kind: "application", application: this.interfaceByName(name, context.typeArguments()) };
|
||||
}
|
||||
|
||||
expression(context: ValueTypeContext): ValueTypeExpression {
|
||||
if (context.scalarType())
|
||||
return {
|
||||
kind: "scalar",
|
||||
name: context.scalarType()!.getText() as Extract<ValueType, { kind: "scalar" }>["name"],
|
||||
};
|
||||
if (context.UNIT()) return valueType.unit;
|
||||
if (context.WATCH_HANDLE()) return valueType.watchHandle;
|
||||
if (context.MESSAGE()) return valueType.message(literal(context.stringLiteral()!));
|
||||
if (context.OPTIONAL() || context.LIST())
|
||||
return { kind: context.LIST() ? "list" : "optional", value: this.expression(context.valueType()!) };
|
||||
if (context.RECORD()) {
|
||||
const fields = context
|
||||
.recordField()
|
||||
.map((field) => [field.identifier().getText(), this.expression(field.valueType())] as const);
|
||||
if (new Set(fields.map(([name]) => name)).size !== fields.length)
|
||||
throw new GenericTypeError("duplicate-field", "record", "Duplicate record field");
|
||||
return { kind: "record", fields: Object.fromEntries(fields) };
|
||||
}
|
||||
const name = context.identifier()!.getText();
|
||||
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
|
||||
if (context.INTERFACE_REF())
|
||||
return {
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "application", application: this.interfaceByName(name, context.typeArguments()) },
|
||||
};
|
||||
if (context.REF()) return { kind: "object-ref", expectation: this.objectParameter(name) };
|
||||
const parameter = this.parameters.get(name);
|
||||
if (parameter) {
|
||||
if (parameter.kind !== "value" || context.typeArguments())
|
||||
throw new GenericTypeError("parameter-kind", name, "Expected a value parameter; object parameters need ref<T>");
|
||||
return { kind: "parameter", parameterId: parameter.id };
|
||||
}
|
||||
if (!this.aliases.has(name)) throw new GenericTypeError("unknown-type", name, "Unknown value type");
|
||||
return { kind: "alias", definitionId: name, arguments: this.arguments(context.typeArguments()) };
|
||||
}
|
||||
|
||||
value(context: ValueTypeContext): ValueType {
|
||||
return new TypeSubstitution(this.environment()).value(this.expression(context));
|
||||
}
|
||||
|
||||
target(context: TargetConstraintContext) {
|
||||
return new TypeSubstitution(this.environment()).object(this.targetExpression(context));
|
||||
}
|
||||
|
||||
declareParameters(context: TypeParametersContext | null, owner: string): TypeParameter[] {
|
||||
const entries = context?.typeParameter() ?? [];
|
||||
const result: TypeParameter[] = entries.map((parameter, index) => {
|
||||
const name = parameter.identifier().getText();
|
||||
if (name === "Self" || this.parameters.has(name))
|
||||
throw new GenericTypeError("duplicate-parameter", owner, `Duplicate or reserved parameter ${name}`);
|
||||
const declaration: TypeParameter = parameter.VALUE()
|
||||
? {
|
||||
id: `${owner}/parameter/${index}`,
|
||||
name,
|
||||
kind: "value",
|
||||
...(parameter.STORABLE() ? { storable: true } : {}),
|
||||
}
|
||||
: { id: `${owner}/parameter/${index}`, name, kind: "object", implements: [] };
|
||||
this.parameters.set(name, declaration);
|
||||
return declaration;
|
||||
});
|
||||
entries.forEach((entry, index) => {
|
||||
const parameter = result[index];
|
||||
if (parameter.kind === "object")
|
||||
parameter.implements = entry.interfaceType().map((bound) => this.interface(bound));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
declareAliases(contexts: readonly TypeAliasDeclContext[]) {
|
||||
for (const context of contexts) {
|
||||
const name = context.identifier().getText();
|
||||
if (this.aliases.has(name)) throw new GenericTypeError("duplicate-alias", name, "Duplicate type alias");
|
||||
this.aliases.set(name, { id: name, parameters: [], body: valueType.unit });
|
||||
}
|
||||
for (const context of contexts) {
|
||||
const name = context.identifier().getText();
|
||||
const previous = this.parameters;
|
||||
this.parameters = new Map();
|
||||
try {
|
||||
this.aliases.set(name, {
|
||||
id: name,
|
||||
parameters: this.declareParameters(context.typeParameters(), JSON.stringify(["alias", name])),
|
||||
body: this.expression(context.valueType()),
|
||||
});
|
||||
} finally {
|
||||
this.parameters = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import childProcess from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { CapabilityRepositoryResolver } from "./assembly.js";
|
||||
|
||||
const execFile = promisify(childProcess.execFile);
|
||||
|
||||
const sourceKey = (kind: string, repository: string, commit: string) =>
|
||||
`${kind}\0${repository}\0${commit.toLowerCase()}`;
|
||||
|
||||
const checkoutName = (kind: string, repository: string, commit: string) => {
|
||||
const digest = crypto
|
||||
.createHash("sha256")
|
||||
.update(sourceKey(kind, repository, commit))
|
||||
.digest("hex")
|
||||
.slice(0, 24);
|
||||
return `${kind}-${digest}`;
|
||||
};
|
||||
|
||||
export const createGitCapabilityResolver = async (options: {
|
||||
checkoutRoot: string;
|
||||
snapshotMap?: string;
|
||||
snapshotOnly?: boolean;
|
||||
}): Promise<CapabilityRepositoryResolver> => {
|
||||
await mkdir(options.checkoutRoot, { recursive: true });
|
||||
const checkoutRoot = await realpath(options.checkoutRoot);
|
||||
const snapshots = new Map<string, string>();
|
||||
if (options.snapshotMap) {
|
||||
const snapshotMapPath = await realpath(options.snapshotMap);
|
||||
const document = JSON.parse(await readFile(snapshotMapPath, "utf8")) as {
|
||||
resources?: Array<{
|
||||
kind: string;
|
||||
repository: string;
|
||||
commit: string;
|
||||
directory: string;
|
||||
}>;
|
||||
};
|
||||
for (const entry of document.resources ?? []) {
|
||||
if (entry.kind !== "interface" && entry.kind !== "package") {
|
||||
throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`);
|
||||
}
|
||||
const key = sourceKey(entry.kind, entry.repository, entry.commit);
|
||||
if (snapshots.has(key)) throw new Error(`Snapshot map repeats ${key}`);
|
||||
const directory = path.resolve(path.dirname(snapshotMapPath), entry.directory);
|
||||
snapshots.set(key, await realpath(directory));
|
||||
}
|
||||
}
|
||||
|
||||
const checkouts = new Map<string, Promise<{ directory: string }>>();
|
||||
return async (source, kind) => {
|
||||
const key = sourceKey(kind, source.repository, source.commit);
|
||||
const snapshot = snapshots.get(key);
|
||||
if (snapshot) return { directory: snapshot };
|
||||
if (options.snapshotOnly) throw new Error(`No offline snapshot for ${kind} ${source.repository}@${source.commit}`);
|
||||
const existing = checkouts.get(key);
|
||||
if (existing) return await existing;
|
||||
const pending = (async () => {
|
||||
const directory = path.join(checkoutRoot, checkoutName(kind, source.repository, source.commit));
|
||||
const verify = async (checkout: string) => {
|
||||
const { stdout } = await execFile("git", ["-C", checkout, "rev-parse", "HEAD"]);
|
||||
if (stdout.trim().toLowerCase() !== source.commit.toLowerCase()) {
|
||||
throw new Error(
|
||||
`Locked commit mismatch for ${source.repository}: wanted ${source.commit}, fetched ${stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
const { stdout: changes } = await execFile("git", [
|
||||
"-C",
|
||||
checkout,
|
||||
"status",
|
||||
"--porcelain",
|
||||
"--untracked-files=all",
|
||||
]);
|
||||
if (changes.trim()) throw new Error(`Dependency checkout was modified: ${checkout}`);
|
||||
};
|
||||
// Only complete, checked clones become visible under the deterministic name.
|
||||
// Concurrent resolvers may fetch independently, but cannot observe a partial clone.
|
||||
if (
|
||||
await stat(directory).then(
|
||||
() => true,
|
||||
(error: NodeJS.ErrnoException) => {
|
||||
if (error.code === "ENOENT") return false;
|
||||
throw error;
|
||||
},
|
||||
)
|
||||
) {
|
||||
await verify(directory);
|
||||
return { directory };
|
||||
}
|
||||
const staging = await mkdtemp(path.join(checkoutRoot, ".fetch-"));
|
||||
const checkout = path.join(staging, "checkout");
|
||||
try {
|
||||
await execFile("git", [
|
||||
"-c",
|
||||
"advice.detachedHead=false",
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--single-branch",
|
||||
"--branch",
|
||||
`quixos-reachability/${source.commit.toLowerCase()}`,
|
||||
source.repository,
|
||||
checkout,
|
||||
]);
|
||||
await verify(checkout);
|
||||
try {
|
||||
await rename(checkout, directory);
|
||||
} catch (error) {
|
||||
if (!["EEXIST", "ENOTEMPTY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
||||
await verify(directory);
|
||||
}
|
||||
} finally {
|
||||
await rm(staging, { recursive: true, force: true });
|
||||
}
|
||||
return { directory };
|
||||
})();
|
||||
checkouts.set(key, pending);
|
||||
try {
|
||||
return await pending;
|
||||
} catch (error) {
|
||||
checkouts.delete(key);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { parse } from "@babel/parser";
|
||||
type Node = { type: string; start: number; end: number; [key: string]: unknown };
|
||||
const node = (value: unknown): value is Node =>
|
||||
!!value && typeof value === "object" && typeof (value as Node).type === "string";
|
||||
|
||||
/** One requested insertion into ordinary authored TypeScript, not regeneration.
|
||||
* Ambiguous/custom wiring is left alone with an actionable error. */
|
||||
export function addImplementation(
|
||||
text: string,
|
||||
factory: "createRuntime" | "serveMigration",
|
||||
key: string,
|
||||
importPath: string,
|
||||
migrationOnly = false,
|
||||
): string {
|
||||
const ast = parse(text, { sourceType: "module", plugins: ["typescript"] });
|
||||
const objects: Node[] = [];
|
||||
const names = new Set<string>();
|
||||
const visit = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(visit);
|
||||
return;
|
||||
}
|
||||
if (!node(value)) return;
|
||||
if (value.type === "Identifier") names.add(String(value.name));
|
||||
if (
|
||||
value.type === "CallExpression" &&
|
||||
node(value.callee) &&
|
||||
value.callee.type === "Identifier" &&
|
||||
value.callee.name === factory &&
|
||||
Array.isArray(value.arguments) &&
|
||||
value.arguments.length === 1 &&
|
||||
node(value.arguments[0]) &&
|
||||
value.arguments[0].type === "ObjectExpression"
|
||||
)
|
||||
objects.push(value.arguments[0]);
|
||||
Object.values(value).forEach(visit);
|
||||
};
|
||||
visit(ast);
|
||||
if (objects.length !== 1)
|
||||
throw new Error(
|
||||
`Cannot safely add implementation: expected one ${factory}({...}) literal. Wire the handler in your authored server code instead.`,
|
||||
);
|
||||
const object = objects[0];
|
||||
if (
|
||||
(object.properties as Node[]).some((p) => node(p.key) && !p.computed && (p.key.name === key || p.key.value === key))
|
||||
)
|
||||
throw new Error(`Implementation already exists for ${key}`);
|
||||
let alias = "qxImplementation";
|
||||
for (let index = 1; names.has(alias); index++) alias = `qxImplementation${index}`;
|
||||
const value = migrationOnly ? 'async () => { throw new Error("Migration-only export"); }' : alias;
|
||||
const insertion = object.start + 1;
|
||||
const edited = text.slice(0, insertion) + `\n ${JSON.stringify(key)}: ${value},\n` + text.slice(insertion);
|
||||
return (migrationOnly ? "" : `import {handler as ${alias}} from ${JSON.stringify(importPath)};\n`) + edited;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./parser.js";
|
||||
export * from "./assembly.js";
|
||||
export * from "./source.js";
|
||||
export * from "./source-loader.js";
|
||||
export * from "./scaffold.js";
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// Data only: never load files, resolve repositories, or evaluate code.
|
||||
import { parseQx } from "./source.js";
|
||||
import { parseQuixosLockDocument } from "../resource-lock/parser.js";
|
||||
import { instantiateWorkspaceIdentity } from "./structural-edits.js";
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) {
|
||||
input += chunk;
|
||||
if (Buffer.byteLength(input) > 6 * 1024 * 1024) throw new Error("Inspection input exceeds limit");
|
||||
}
|
||||
const document = JSON.parse(input);
|
||||
if (!Array.isArray(document) && document?.operation === "instantiate-workspace") {
|
||||
if (
|
||||
typeof document.source !== "string" ||
|
||||
document.source.length > 262144 ||
|
||||
typeof document.workspaceId !== "string"
|
||||
)
|
||||
throw new Error("Invalid template identity request");
|
||||
process.stdout.write(JSON.stringify({ source: instantiateWorkspaceIdentity(document.source, document.workspaceId) }));
|
||||
process.exit(0);
|
||||
}
|
||||
const files: { path: string; source: string }[] = document;
|
||||
if (!Array.isArray(files) || files.length > 128) throw new Error("Too many source files");
|
||||
const result = files.map(({ path, source }) => {
|
||||
if (typeof path !== "string" || typeof source !== "string" || source.length > 262144)
|
||||
throw new Error("Invalid source input");
|
||||
if (path.endsWith(".qx")) {
|
||||
const parsed = parseQx(source, path);
|
||||
return { path, root: parsed.root, diagnostics: parsed.diagnostics };
|
||||
}
|
||||
return { path, lock: parseQuixosLockDocument(source, path) };
|
||||
});
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { contentDigest } from "../capability-model/evolution.js";
|
||||
import { validateMigrationCatalog, type MigrationCatalog } from "../capability-model/migrations.js";
|
||||
import { applyStructure, planStructure } from "./structural-plan.js";
|
||||
|
||||
/** Explicitly acknowledge edited migration code. This does not change retained
|
||||
* contracts or grant activation approval; the immutable checker verifies it. */
|
||||
export async function sealMigrations(directory: string) {
|
||||
const root = await fs.realpath(directory);
|
||||
const file = "quixos.migrations.json";
|
||||
const before = await fs.readFile(path.join(root, file), "utf8");
|
||||
const catalog = JSON.parse(before) as MigrationCatalog;
|
||||
for (const migration of catalog.migrations) {
|
||||
const implementation = await fs.realpath(path.join(root, migration.implementation.file));
|
||||
if (!implementation.startsWith(root + path.sep)) throw new Error("Migration implementation escapes package");
|
||||
migration.implementation.digest = contentDigest(await fs.readFile(implementation, "utf8"));
|
||||
}
|
||||
validateMigrationCatalog(catalog);
|
||||
return applyStructure(
|
||||
await planStructure(root, {
|
||||
kind: "package",
|
||||
validation: "syntax",
|
||||
files: [{ file, expected: before, replace: JSON.stringify(catalog, null, 2) + "\n" }],
|
||||
}),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { loadQuixosLock, parseQuixosLockDocument } from "../resource-lock/index.js";
|
||||
import { contentDigest } from "../capability-model/evolution.js";
|
||||
import { planStructure, applyStructure, type StructuralRequest } from "./structural-plan.js";
|
||||
import { snapshotRepository } from "./candidate-check.js";
|
||||
import {
|
||||
compileWorkspaceRepository,
|
||||
compileCapabilityResourceRepository,
|
||||
type ResolvedCapabilityResource,
|
||||
} from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { snapshotCommit, buildImmutableCandidate } from "./checked-build.js";
|
||||
import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../capability-model/index.js";
|
||||
import { withFileLock } from "./file-lock.js";
|
||||
const execFile = promisify(callback);
|
||||
type Source = { repository: string; commit: string };
|
||||
export type UpgradeNode = { kind: "workspace" | "package" | "interface"; directory: string; source: Source };
|
||||
export type UpgradeSpec = {
|
||||
nodes: UpgradeNode[];
|
||||
quixos?: Source;
|
||||
baseline?: string;
|
||||
reviews?: string;
|
||||
bootstrap?: boolean;
|
||||
};
|
||||
type NodePlan = UpgradeNode & { treeDigest: string; dependencies: string[]; lockFiles: string[] };
|
||||
export type UpgradePlan = { schemaVersion: 1; workbench: string; spec: UpgradeSpec; nodes: NodePlan[]; digest: string };
|
||||
type Step = {
|
||||
directory: string;
|
||||
phase: "editing" | "prepared" | "refactor" | "checked" | "publishing" | "published";
|
||||
commit?: string;
|
||||
treeDigest?: string;
|
||||
structuralJournal?: string;
|
||||
structuralPlan?: Awaited<ReturnType<typeof planStructure>>;
|
||||
};
|
||||
type Journal = { schemaVersion: 1; plan: UpgradePlan; steps: Step[] };
|
||||
const sourceKey = (node: { kind: string; source: Source }) =>
|
||||
JSON.stringify([node.kind, node.source.repository, node.source.commit]);
|
||||
const command = async (cwd: string, tool: string, args: string[]) =>
|
||||
(
|
||||
await execFile(tool, args, {
|
||||
cwd,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" },
|
||||
})
|
||||
).stdout.trim();
|
||||
const validSource = (value: Source) => {
|
||||
const url = new URL(value.repository);
|
||||
if (
|
||||
url.protocol !== "https:" ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.search ||
|
||||
url.hash ||
|
||||
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.commit)
|
||||
)
|
||||
throw new Error("Upgrade sources must be exact credential-free HTTPS revisions");
|
||||
};
|
||||
const location = async (root: string, directory: string) => {
|
||||
if (directory !== "root" && !/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(directory))
|
||||
throw new Error("Upgrade target must be a managed root/resource repository");
|
||||
const resolved = await fs.realpath(path.join(root, directory));
|
||||
if (resolved !== path.join(root, directory)) throw new Error("Upgrade target crosses a symlink");
|
||||
return resolved;
|
||||
};
|
||||
const treeDigest = async (root: string) => {
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-tree-"));
|
||||
try {
|
||||
return (await snapshotRepository(root, temporary)).treeDigest;
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
const writeJournal = async (filename: string, journal: unknown) => {
|
||||
const temp = `${filename}.${randomUUID()}.tmp`;
|
||||
const handle = await fs.open(temp, "wx", 0o600);
|
||||
try {
|
||||
await handle.writeFile(JSON.stringify(journal, null, 2));
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await fs.rename(temp, filename);
|
||||
const directory = await fs.open(path.dirname(filename), "r");
|
||||
try {
|
||||
await directory.sync();
|
||||
} finally {
|
||||
await directory.close();
|
||||
}
|
||||
};
|
||||
|
||||
export const discoverUpgradeSpec = async (workbench: string): Promise<UpgradeSpec> => {
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
const root = await location(workbench, "root");
|
||||
const nodes: UpgradeNode[] = [
|
||||
{
|
||||
kind: "workspace",
|
||||
directory: "root",
|
||||
source: {
|
||||
repository: await command(root, "git", ["config", "--get", "remote.origin.url"]),
|
||||
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 }) => ({
|
||||
kind: entry.kind,
|
||||
source: entry.source,
|
||||
directory: path.relative(workbench, path.resolve(workbench, entry.directory)),
|
||||
})),
|
||||
];
|
||||
let baseline: string | undefined;
|
||||
try {
|
||||
const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8"));
|
||||
if ((await fs.realpath(host.workbenchRoot)) === (await fs.realpath(workbench)))
|
||||
baseline = JSON.parse(
|
||||
await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8"),
|
||||
).workspacePlanPath;
|
||||
} catch (error) {
|
||||
if (!["ENOENT", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
||||
}
|
||||
return { nodes, baseline };
|
||||
};
|
||||
|
||||
/** Read-only source plan. Repositories are selected explicitly, including any
|
||||
* parallel versions of the same resource; no guesses at a floating 'latest'. */
|
||||
export const planPinUpgrades = async (workbenchPath: string, spec: UpgradeSpec): Promise<UpgradePlan> => {
|
||||
const workbench = await fs.realpath(workbenchPath);
|
||||
if (
|
||||
!Array.isArray(spec.nodes) ||
|
||||
!spec.nodes.length ||
|
||||
spec.nodes.length > 100 ||
|
||||
spec.nodes.filter((node) => node.kind === "workspace").length !== 1
|
||||
)
|
||||
throw new Error("Upgrade graph requires one workspace and at most 100 repositories");
|
||||
if (spec.quixos) validSource(spec.quixos);
|
||||
const keys = new Map<string, string>();
|
||||
for (const node of spec.nodes) {
|
||||
validSource(node.source);
|
||||
if (!["workspace", "package", "interface"].includes(node.kind) || keys.has(sourceKey(node)))
|
||||
throw new Error("Duplicate/invalid upgrade resource identity");
|
||||
keys.set(sourceKey(node), node.directory);
|
||||
}
|
||||
if (new Set(spec.nodes.map((node) => node.directory)).size !== spec.nodes.length)
|
||||
throw new Error("Upgrade directories must be distinct");
|
||||
const nodes: NodePlan[] = [];
|
||||
for (const node of spec.nodes) {
|
||||
const root = await location(workbench, node.directory);
|
||||
if ((await command(root, "git", ["config", "--get", "remote.origin.url"])) !== node.source.repository)
|
||||
throw new Error(`Upgrade origin differs from selected source: ${node.directory}`);
|
||||
const loaded = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!loaded.ok)
|
||||
throw new Error(
|
||||
`Invalid lock in ${node.directory}: ${loaded.diagnostics.map((entry) => entry.message).join("; ")}`,
|
||||
);
|
||||
const dependencies = loaded.lock.resources
|
||||
.map((resource) => keys.get(sourceKey(resource)))
|
||||
.filter((value): value is string => Boolean(value));
|
||||
nodes.push({
|
||||
...node,
|
||||
treeDigest: await treeDigest(root),
|
||||
dependencies: [...new Set(dependencies)],
|
||||
lockFiles: loaded.lock.sourceFiles ?? ["quixos.lock"],
|
||||
});
|
||||
}
|
||||
const ordered: NodePlan[] = [],
|
||||
remaining = [...nodes];
|
||||
while (remaining.length) {
|
||||
const index = remaining.findIndex((node) =>
|
||||
node.dependencies.every((dependency) => ordered.some((entry) => entry.directory === dependency)),
|
||||
);
|
||||
if (index < 0) throw new Error("Cyclic source publication graph");
|
||||
ordered.push(remaining.splice(index, 1)[0]);
|
||||
}
|
||||
const workspace = ordered.find((node) => node.kind === "workspace")!;
|
||||
// Even unreferenced new resources are published before the root.
|
||||
ordered.splice(ordered.indexOf(workspace), 1);
|
||||
ordered.push(workspace);
|
||||
const plan = { schemaVersion: 1 as const, workbench, spec, nodes: ordered };
|
||||
return { ...plan, digest: contentDigest(plan) };
|
||||
};
|
||||
|
||||
export type UpgradeEffects = {
|
||||
check(node: NodePlan, root: string, output: string, spec: UpgradeSpec): Promise<void>;
|
||||
snapshot(root: string): Promise<string>;
|
||||
publish(root: string, commit: string): Promise<void>;
|
||||
};
|
||||
const effects: UpgradeEffects = {
|
||||
async check(node, root, output, spec) {
|
||||
if (node.kind === "workspace" && !spec.baseline && !spec.bootstrap)
|
||||
throw new Error(
|
||||
"Upgrading a workspace requires its checked active baseline for major-review checks (or explicit bootstrap:true for a new workspace)",
|
||||
);
|
||||
// Explicit baseline upgrades use the same immutable Nix checker. Retaining
|
||||
// an unverified source is safe and must precede a remote flake fetch.
|
||||
await fs.mkdir(output);
|
||||
const commit = await snapshotCommit(root);
|
||||
await effects.publish(root, commit);
|
||||
const artifact = await buildImmutableCandidate({ ...node.source, commit }, node.kind, path.join(output, "nix.log"));
|
||||
const candidate = await fs.readFile(path.join(artifact, "candidate.json"), "utf8");
|
||||
await fs.writeFile(path.join(output, "candidate.json"), candidate);
|
||||
if (node.kind === "workspace") {
|
||||
const baseline = spec.baseline
|
||||
? (JSON.parse(await fs.readFile(spec.baseline, "utf8")) as WorkspaceRevision)
|
||||
: null;
|
||||
const reviews = spec.reviews ? (JSON.parse(await fs.readFile(spec.reviews, "utf8")) as EvolutionReview[]) : [];
|
||||
const evolution = planEvolution(baseline, JSON.parse(candidate), { reviews });
|
||||
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
|
||||
if (evolution.blockers.length)
|
||||
throw new Error(`Refactor required in ${node.directory}: ${evolution.blockers.join("; ")}`);
|
||||
}
|
||||
},
|
||||
async snapshot(root) {
|
||||
const commit = await snapshotCommit(root);
|
||||
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Publication did not resolve an exact commit");
|
||||
await command(root, "git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"]);
|
||||
const tracked = new Set((await command(root, "git", ["ls-tree", "-r", "--name-only", "-z", commit])).split("\0"));
|
||||
for (const file of (await command(root, "git", ["ls-files", "--others", "--exclude-standard", "-z"]))
|
||||
.split("\0")
|
||||
.filter(Boolean))
|
||||
if (!tracked.has(file)) throw new Error(`Uncaptured source file ${file}`);
|
||||
return commit;
|
||||
},
|
||||
async publish(root, commit) {
|
||||
const ref = `refs/tags/quixos-reachability/${commit}`;
|
||||
const remote = await command(root, "git", ["ls-remote", "--refs", "origin", ref]);
|
||||
if (remote && remote !== `${commit}\t${ref}`) throw new Error("Immutable publication ref conflict");
|
||||
if (!remote) await command(root, "git", ["push", "origin", `${commit}:${ref}`]);
|
||||
if ((await command(root, "git", ["ls-remote", "--refs", "origin", ref])) !== `${commit}\t${ref}`)
|
||||
throw new Error("Publication response uncertain; retry the same journal");
|
||||
},
|
||||
};
|
||||
|
||||
/** Explicit --publish only. Append-only remote retention; never moves the
|
||||
* workspace branch, activates code, or rolls back previously published nodes. */
|
||||
export const applyPinUpgrades = async (
|
||||
plan: UpgradePlan,
|
||||
journalId?: string,
|
||||
implementation: UpgradeEffects = effects,
|
||||
options: { acceptEdits?: boolean } = {},
|
||||
) => {
|
||||
if (implementation === effects && !plan.spec.baseline && !plan.spec.bootstrap)
|
||||
throw new Error("Publication requires an active checked baseline or explicit bootstrap:true");
|
||||
const { digest, ...body } = plan;
|
||||
if (contentDigest(body) !== digest) throw new Error("Upgrade plan digest mismatch");
|
||||
const directory = path.join(plan.workbench, ".quixos", "upgrades");
|
||||
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
if ((await fs.realpath(directory)) !== directory) throw new Error("Upgrade journals must not cross symlinks");
|
||||
const id = journalId ?? randomUUID();
|
||||
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid upgrade journal ID");
|
||||
const filename = path.join(directory, `${id}.json`);
|
||||
return withFileLock(path.join(directory, "writer.lock"), async () => {
|
||||
try {
|
||||
const journal: Journal = journalId
|
||||
? JSON.parse(await fs.readFile(filename, "utf8"))
|
||||
: { schemaVersion: 1, plan, steps: [] };
|
||||
if (journal.plan.digest !== plan.digest) throw new Error("Upgrade journal belongs to another plan");
|
||||
if (!journalId) await writeJournal(filename, journal);
|
||||
for (const node of plan.nodes) {
|
||||
const root = await location(plan.workbench, node.directory);
|
||||
if ((await command(root, "git", ["config", "--get", "remote.origin.url"])) !== node.source.repository)
|
||||
throw new Error("Upgrade remote changed after planning");
|
||||
let step = journal.steps.find((entry) => entry.directory === node.directory);
|
||||
if (step?.phase === "published") continue;
|
||||
if (!step) {
|
||||
if ((await treeDigest(root)) !== node.treeDigest) throw new Error(`Stale upgrade plan: ${node.directory}`);
|
||||
const files: StructuralRequest["files"] = [];
|
||||
for (const file of node.lockFiles) {
|
||||
const parsed = parseQuixosLockDocument(await fs.readFile(path.join(root, file), "utf8"));
|
||||
if (!parsed.ok) throw new Error("Invalid lock during upgrade");
|
||||
const edits = [];
|
||||
for (const resource of parsed.document.resources) {
|
||||
const dependency = plan.nodes.find((entry) => sourceKey(entry) === sourceKey(resource));
|
||||
const published =
|
||||
dependency &&
|
||||
journal.steps.find((entry) => entry.directory === dependency.directory && entry.phase === "published");
|
||||
if (published?.commit && published.commit !== resource.source.commit)
|
||||
edits.push({
|
||||
operation: "dependency" as const,
|
||||
kind: resource.kind,
|
||||
name: resource.binding,
|
||||
source: { ...resource.source, commit: published.commit },
|
||||
});
|
||||
}
|
||||
if (parsed.document.kind === "root" && plan.spec.quixos)
|
||||
edits.push({ operation: "quixos-pin" as const, source: plan.spec.quixos });
|
||||
if (edits.length) files.push({ file, edits });
|
||||
}
|
||||
const structuralPlan = files.length
|
||||
? await planStructure(
|
||||
root,
|
||||
{ kind: node.kind, source: node.source, files },
|
||||
process.env.QUIXOS_SNAPSHOT_MAP,
|
||||
)
|
||||
: undefined;
|
||||
step = {
|
||||
directory: node.directory,
|
||||
phase: "editing",
|
||||
treeDigest: node.treeDigest,
|
||||
structuralPlan,
|
||||
structuralJournal: structuralPlan ? randomUUID() : undefined,
|
||||
};
|
||||
journal.steps.push(step);
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
if (step.phase === "editing") {
|
||||
if (step.structuralPlan) await applyStructure(step.structuralPlan, step.structuralJournal);
|
||||
else if ((await treeDigest(root)) !== step.treeDigest)
|
||||
throw new Error("Source changed before upgrade editing");
|
||||
step.treeDigest = await treeDigest(root);
|
||||
step.phase = "prepared";
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
if (step.phase === "refactor") {
|
||||
const current = await treeDigest(root);
|
||||
if (current !== step.treeDigest && !options.acceptEdits)
|
||||
throw new Error("Refactored source requires --accept-edits when resuming");
|
||||
step.treeDigest = current;
|
||||
step.phase = "prepared";
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
if ((await treeDigest(root)) !== step.treeDigest)
|
||||
throw new Error(`Source changed during upgrade: ${node.directory}; inspect ${filename}`);
|
||||
if (step.phase === "prepared") {
|
||||
try {
|
||||
await implementation.check(
|
||||
node,
|
||||
root,
|
||||
path.join(directory, `${id}-${node.directory.replaceAll("/", "-")}-${randomUUID()}`),
|
||||
plan.spec,
|
||||
);
|
||||
} catch (error) {
|
||||
step.phase = "refactor";
|
||||
await writeJournal(filename, journal);
|
||||
throw error;
|
||||
}
|
||||
if ((await treeDigest(root)) !== step.treeDigest) throw new Error("Source changed while checking");
|
||||
step.phase = "checked";
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
if (step.phase === "checked") {
|
||||
step.commit = await implementation.snapshot(root);
|
||||
if ((await treeDigest(root)) !== step.treeDigest)
|
||||
throw new Error("Publication snapshot changed checked files");
|
||||
step.phase = "publishing";
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
await implementation.publish(root, step.commit!);
|
||||
step.phase = "published";
|
||||
await writeJournal(filename, journal);
|
||||
}
|
||||
// Keep subsequent automatic upgrades associated with the newly published
|
||||
// identities, without renaming repositories or changing any selected branch.
|
||||
// Explicit-spec callers without a managed graph retain the journal as their
|
||||
// source of revisions instead.
|
||||
const graphFile = path.join(plan.workbench, ".quixos/resource-graph.json");
|
||||
let graphText: string | undefined;
|
||||
try {
|
||||
graphText = await fs.readFile(graphFile, "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (graphText !== undefined) {
|
||||
const previous = JSON.parse(graphText);
|
||||
const snapshots = await Promise.all(
|
||||
previous.resources.map(async (entry: { kind: string; source: Source; directory: string }) => ({
|
||||
kind: entry.kind,
|
||||
...entry.source,
|
||||
directory: await location(
|
||||
plan.workbench,
|
||||
path.relative(plan.workbench, path.resolve(plan.workbench, entry.directory)),
|
||||
),
|
||||
})),
|
||||
);
|
||||
for (const node of plan.nodes.filter((entry) => entry.kind !== "workspace")) {
|
||||
const step = journal.steps.find((entry) => entry.directory === node.directory)!;
|
||||
if ((await treeDigest(await location(plan.workbench, node.directory))) !== step.treeDigest)
|
||||
throw new Error("Published source changed before workbench graph refresh");
|
||||
snapshots.push({
|
||||
kind: node.kind,
|
||||
repository: node.source.repository,
|
||||
commit: step.commit,
|
||||
directory: path.join(plan.workbench, node.directory),
|
||||
});
|
||||
}
|
||||
const unique = [
|
||||
...new Map(
|
||||
snapshots.map((entry: { kind: string; repository: string; commit: string }) => [
|
||||
JSON.stringify([entry.kind, entry.repository, entry.commit]),
|
||||
entry,
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
const snapshotMap = path.join(directory, `${id}-published-snapshots.json`);
|
||||
await fs.writeFile(snapshotMap, JSON.stringify({ resources: unique }));
|
||||
const resolveResource = await createGitCapabilityResolver({
|
||||
checkoutRoot: path.join(directory, `${id}-graph-resources`),
|
||||
snapshotMap,
|
||||
snapshotOnly: true,
|
||||
});
|
||||
const rootNode = plan.nodes.find((node) => node.kind === "workspace")!;
|
||||
if (
|
||||
(await treeDigest(await location(plan.workbench, rootNode.directory))) !==
|
||||
journal.steps.find((step) => step.directory === rootNode.directory)!.treeDigest
|
||||
)
|
||||
throw new Error("Root source changed before workbench graph refresh");
|
||||
const compiled = await compileWorkspaceRepository({
|
||||
rootDirectory: await location(plan.workbench, rootNode.directory),
|
||||
resolveResource,
|
||||
});
|
||||
// Managed repositories need not currently be reachable from the workspace.
|
||||
// Keep them discoverable/checkpointed until explicitly removed by the user.
|
||||
const resources = new Map<string, ResolvedCapabilityResource>(
|
||||
compiled.resources.map((node) => [node.key, node]),
|
||||
);
|
||||
for (const node of plan.nodes.filter((entry) => entry.kind !== "workspace")) {
|
||||
const step = journal.steps.find((entry) => entry.directory === node.directory)!;
|
||||
const source = { resolver: "git" as const, repository: node.source.repository, commit: step.commit! };
|
||||
const key = `${node.kind}\0${source.repository}\0${source.commit}`;
|
||||
if (resources.has(key)) continue;
|
||||
const directory = await location(plan.workbench, node.directory);
|
||||
const standalone = await compileCapabilityResourceRepository({
|
||||
rootDirectory: directory,
|
||||
kind: node.kind as "package" | "interface",
|
||||
source,
|
||||
resolveResource,
|
||||
});
|
||||
for (const dependency of standalone.resources) resources.set(dependency.key, dependency);
|
||||
resources.set(key, {
|
||||
key,
|
||||
kind: node.kind as "package" | "interface",
|
||||
source,
|
||||
directory,
|
||||
lock: standalone.lock,
|
||||
resource: standalone.resource,
|
||||
dependencies: standalone.directResources,
|
||||
});
|
||||
}
|
||||
await writeJournal(graphFile, {
|
||||
formatVersion: 1,
|
||||
quixos: compiled.lock.quixos,
|
||||
directResources: [...compiled.directResources.entries()].map(([bindingKey, node]) => {
|
||||
const [kind, binding] = bindingKey.split("\0");
|
||||
return { kind, binding, resourceKey: node.key, directory: node.directory };
|
||||
}),
|
||||
resources: [...resources.values()].map((node) => ({
|
||||
key: node.key,
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
return {
|
||||
id,
|
||||
journal: filename,
|
||||
revisions: journal.steps.map((step) => ({ directory: step.directory, commit: step.commit })),
|
||||
activated: false,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`${error instanceof Error ? error.message : String(error)}; upgrade journal ${filename}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import process from "node:process";
|
||||
import { bindingSchema } from "../bindings/index.js";
|
||||
import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
|
||||
const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package
|
||||
--repository URL --commit GIT_REV --checkout-root DIRECTORY
|
||||
[--snapshot-map PATH] [--snapshot-only true] [--graph-out PATH] [--schema-out PATH]
|
||||
|
||||
Resolves a standalone resource's recursive dependency-lock graph, validates
|
||||
its interface.qx or package.qx manifest, and emits the checked resource as JSON.`;
|
||||
|
||||
const parseArgs = (args: string[]) => {
|
||||
const values = new Map<string, string>();
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const key = args[index];
|
||||
const value = args[index + 1];
|
||||
if (!key?.startsWith("--") || !value) throw new Error(usage);
|
||||
if (
|
||||
![
|
||||
"--root",
|
||||
"--kind",
|
||||
"--repository",
|
||||
"--commit",
|
||||
"--checkout-root",
|
||||
"--snapshot-map",
|
||||
"--snapshot-only",
|
||||
"--graph-out",
|
||||
"--schema-out",
|
||||
].includes(key) ||
|
||||
values.has(key)
|
||||
)
|
||||
throw new Error(usage);
|
||||
values.set(key, value);
|
||||
}
|
||||
const rootDirectory = values.get("--root");
|
||||
const kind = values.get("--kind");
|
||||
const repository = values.get("--repository");
|
||||
const commit = values.get("--commit")?.toLowerCase();
|
||||
const checkoutRoot = values.get("--checkout-root");
|
||||
if (!rootDirectory || !repository || !commit || !checkoutRoot || (kind !== "interface" && kind !== "package")) {
|
||||
throw new Error(usage);
|
||||
}
|
||||
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) {
|
||||
throw new Error("--commit must be a full Git object ID");
|
||||
}
|
||||
if (values.has("--snapshot-only") && values.get("--snapshot-only") !== "true")
|
||||
throw new Error("--snapshot-only accepts true");
|
||||
return {
|
||||
rootDirectory,
|
||||
kind,
|
||||
repository,
|
||||
commit,
|
||||
checkoutRoot,
|
||||
snapshotMap: values.get("--snapshot-map"),
|
||||
snapshotOnly: values.get("--snapshot-only") === "true",
|
||||
graphOut: values.get("--graph-out"),
|
||||
schemaOut: values.get("--schema-out"),
|
||||
} as const;
|
||||
};
|
||||
|
||||
const graphEntry = (node: ResolvedCapabilityResource) => ({
|
||||
key: node.key,
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
})),
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
process.stdout.write(`${usage}\n`);
|
||||
return;
|
||||
}
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const resolveResource = await createGitCapabilityResolver({
|
||||
checkoutRoot: options.checkoutRoot,
|
||||
snapshotMap: options.snapshotMap,
|
||||
snapshotOnly: options.snapshotOnly,
|
||||
});
|
||||
const compiled = await compileCapabilityResourceRepository({
|
||||
rootDirectory: options.rootDirectory,
|
||||
kind: options.kind,
|
||||
source: {
|
||||
resolver: "git",
|
||||
repository: options.repository,
|
||||
commit: options.commit,
|
||||
},
|
||||
resolveResource,
|
||||
});
|
||||
if (options.graphOut) {
|
||||
await writeFile(
|
||||
options.graphOut,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
formatVersion: 1,
|
||||
quixos: compiled.lock.quixos,
|
||||
root: graphEntry(
|
||||
compiled.resources.find(
|
||||
(node) =>
|
||||
node.source.repository === options.repository &&
|
||||
node.source.commit === options.commit &&
|
||||
node.kind === options.kind,
|
||||
)!,
|
||||
),
|
||||
resources: compiled.resources.map(graphEntry),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
if (options.schemaOut) await writeFile(options.schemaOut, `${JSON.stringify(bindingSchema(compiled), null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(compiled.resource, null, 2)}\n`);
|
||||
};
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,421 @@
|
||||
import fs from "node:fs/promises";
|
||||
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 { addImplementation } from "./implementation-edit.js";
|
||||
import { reactPlatformTypes } from "../bindings/react-platform.js";
|
||||
|
||||
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 = {
|
||||
template?: "typescript" | "typescript-react";
|
||||
source: Source;
|
||||
directory?: string;
|
||||
name?: string;
|
||||
id?: string;
|
||||
revision?: string;
|
||||
declaration?: string;
|
||||
/** Initial authored files for a composed recipe; only used for a new package. */
|
||||
initialFiles?: Record<string, string>;
|
||||
tools?: { quixos: Source; protocol: Source; helpers: Source; sdk: Source };
|
||||
nixifyPluginUrl?: string;
|
||||
migration?: Omit<MigrationDeclaration, "implementation"> & { contracts: Record<string, unknown> };
|
||||
};
|
||||
const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
|
||||
const source = (value: Source): GitSource => {
|
||||
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");
|
||||
const url = new URL(value.repository);
|
||||
if (
|
||||
url.protocol !== "https:" ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.search ||
|
||||
url.hash ||
|
||||
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value.commit)
|
||||
)
|
||||
throw new Error("Scaffolds require credential-free HTTPS sources and full exact commits");
|
||||
return { resolver: "git", ...value };
|
||||
};
|
||||
const nixSource = (value: Source) =>
|
||||
`git+${source(value).repository}?ref=refs/tags/quixos-reachability/${value.commit}&rev=${value.commit}`;
|
||||
const nixString = (value: string) => JSON.stringify(value).replaceAll("${", "\\${");
|
||||
const safeName = (name: string | undefined): string => {
|
||||
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,
|
||||
command: "package" | "function" | "migration" | "refresh",
|
||||
spec: ScaffoldRecipe,
|
||||
): Promise<StructuralRequest> => {
|
||||
if (command === "refresh")
|
||||
throw new 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.",
|
||||
);
|
||||
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))
|
||||
throw new Error("Scaffold directory must be contained");
|
||||
const prefix = spec.directory ? `${spec.directory}/` : "";
|
||||
const files: StructuralRequest["files"] = [];
|
||||
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
|
||||
const generated = (file: string, content: string) => files.push({ file: prefix + file, generated: content });
|
||||
let packageModel: PackageEditModel;
|
||||
let catalog: MigrationCatalog & { generatedBy: "qx-scaffold-v1" };
|
||||
if (command === "package") {
|
||||
const name = safeName(spec.name);
|
||||
if (spec.template && !["typescript", "typescript-react"].includes(spec.template))
|
||||
throw new Error("Unknown package template");
|
||||
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"];
|
||||
@@ -0,0 +1,82 @@
|
||||
import { lstat, open, rename, unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { addWorkspaceImport } from "./source.js";
|
||||
import { readQxSource } from "./source-loader.js";
|
||||
import { compileWorkspaceRepository, type CapabilityRepositoryResolver } from "./assembly.js";
|
||||
|
||||
export const planAtomScaffold = (workspace: string, name: string, id: string) => {
|
||||
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error("Atom name must be PascalCase");
|
||||
if (!id.trim()) throw new Error("Atom ID must not be empty");
|
||||
const fileName = `${name}.qx`;
|
||||
return {
|
||||
before: workspace,
|
||||
workspace: addWorkspaceImport(workspace, fileName),
|
||||
fileName,
|
||||
fragment: `fragment {\n atom ${name} id ${JSON.stringify(id)};\n}\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/** Validate an in-memory proposal before creating files. Never replace a fragment. */
|
||||
export const scaffoldAtom = async (options: {
|
||||
root: string;
|
||||
name: string;
|
||||
id: string;
|
||||
write: boolean;
|
||||
resolveResource: CapabilityRepositoryResolver;
|
||||
}) => {
|
||||
const before = await readQxSource(options.root, "workspace.qx");
|
||||
const plan = planAtomScaffold(before, options.name, options.id);
|
||||
const fragmentPath = path.join(options.root, plan.fileName);
|
||||
try {
|
||||
await lstat(fragmentPath);
|
||||
throw new Error(`Scaffold target already exists: ${plan.fileName}`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
const observed = new Map<string, string>();
|
||||
await compileWorkspaceRepository({
|
||||
rootDirectory: options.root,
|
||||
resolveResource: options.resolveResource,
|
||||
readSource: async (name) => {
|
||||
if (name === "workspace.qx") return plan.workspace;
|
||||
if (name === plan.fileName) return plan.fragment;
|
||||
const text = await readQxSource(options.root, name);
|
||||
observed.set(name, text);
|
||||
return text;
|
||||
},
|
||||
});
|
||||
if (!options.write) return plan;
|
||||
const workspacePath = path.join(options.root, "workspace.qx");
|
||||
const temporary = path.join(options.root, `.qx-scaffold-${randomUUID()}.tmp`);
|
||||
let createdFragment = false;
|
||||
let createdTemporary = false;
|
||||
try {
|
||||
const fragment = await open(fragmentPath, "wx");
|
||||
createdFragment = true;
|
||||
try {
|
||||
await fragment.writeFile(plan.fragment);
|
||||
} finally {
|
||||
await fragment.close();
|
||||
}
|
||||
const file = await open(temporary, "wx", (await lstat(workspacePath)).mode);
|
||||
createdTemporary = true;
|
||||
try {
|
||||
await file.writeFile(plan.workspace);
|
||||
} finally {
|
||||
await file.close();
|
||||
}
|
||||
observed.set("workspace.qx", before);
|
||||
for (const [name, text] of observed) {
|
||||
if ((await readQxSource(options.root, name)) !== text)
|
||||
throw new Error(`QX source changed during scaffolding: ${name}`);
|
||||
}
|
||||
await rename(temporary, workspacePath);
|
||||
createdTemporary = false;
|
||||
createdFragment = false;
|
||||
} finally {
|
||||
if (createdTemporary) await unlink(temporary);
|
||||
if (createdFragment) await unlink(fragmentPath);
|
||||
}
|
||||
return plan;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { lstat, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parseQx, validateQxImportPath, walkSyntax } from "./source.js";
|
||||
|
||||
export const readQxSource = async (root: string, name: string) => {
|
||||
validateQxImportPath(name);
|
||||
return readRepositorySource(root, name);
|
||||
};
|
||||
|
||||
/** Refuse symlinks at every component, including the final source file. */
|
||||
export const readRepositorySource = async (root: string, name: string) => {
|
||||
if (!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$/.test(name))
|
||||
throw new Error(`Invalid repository-relative source path: ${name}`);
|
||||
let current = path.resolve(root);
|
||||
const segments = name.split("/");
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
current = path.join(current, segment);
|
||||
const stat = await lstat(current);
|
||||
if (stat.isSymbolicLink() || (index === segments.length - 1 ? !stat.isFile() : !stat.isDirectory()))
|
||||
throw new Error(`Sources must be ordinary files beneath ordinary directories: ${name}`);
|
||||
}
|
||||
return readFile(current, "utf8");
|
||||
};
|
||||
|
||||
/** Local imports share one workspace scope; paths are always repository-relative. */
|
||||
export const resolveQxSources = async (read: (name: string) => Promise<string>, entry = "workspace.qx") => {
|
||||
const files = new Map<string, string>();
|
||||
const active: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
const segments: { start: number; end: number; fileName: string; sourceStart: number }[] = [];
|
||||
let source = "";
|
||||
const append = (fileName: string, text: string, sourceStart: number) => {
|
||||
segments.push({ start: source.length, end: source.length + text.length, fileName, sourceStart });
|
||||
source += text;
|
||||
};
|
||||
const visit = async (name: string, root: boolean) => {
|
||||
validateQxImportPath(name);
|
||||
if (active.includes(name)) throw new Error(`QX import cycle: ${[...active, name].join(" -> ")}`);
|
||||
if (visited.has(name)) return;
|
||||
const text = await read(name);
|
||||
const syntax = parseQx(text, name);
|
||||
if (syntax.diagnostics.length)
|
||||
throw new Error(syntax.diagnostics.map((d) => `${name}:${d.line}:${d.column + 1}: ${d.message}`).join("\n"));
|
||||
const declaration = syntax.root.children[0]!;
|
||||
if (declaration.kind !== (root ? "workspaceDecl" : "fragmentDecl"))
|
||||
throw new Error(`${name}: expected ${root ? "workspace" : "fragment"} document`);
|
||||
files.set(name, text);
|
||||
active.push(name);
|
||||
visited.add(name);
|
||||
const braces = syntax.tokens.filter((token) => !token.trivia);
|
||||
let cursor = root ? 0 : braces.find((token) => token.kind === "LBRACE")!.end;
|
||||
const end = root ? text.length : braces.filter((token) => token.kind === "RBRACE").at(-1)!.start;
|
||||
for (const node of walkSyntax(declaration)) {
|
||||
if (node.kind !== "sourceImportDecl") continue;
|
||||
append(name, text.slice(cursor, node.start), cursor);
|
||||
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
|
||||
// Separators prevent adjacent tokens/comments joining across files.
|
||||
append(name, "\n", node.start);
|
||||
await visit(JSON.parse(text.slice(literal.start, literal.end)), false);
|
||||
append(name, "\n", node.end);
|
||||
cursor = node.end;
|
||||
}
|
||||
append(name, text.slice(cursor, end), cursor);
|
||||
active.pop();
|
||||
};
|
||||
await visit(entry, true);
|
||||
return {
|
||||
source,
|
||||
sourceFiles: [...files.keys()],
|
||||
files,
|
||||
originalPosition(line: number, column: number) {
|
||||
const lines = source.split("\n");
|
||||
const offset =
|
||||
lines.slice(0, line - 1).reduce((sum, value) => sum + value.length + 1, 0) +
|
||||
[...(lines[line - 1] ?? "")].slice(0, column).join("").length;
|
||||
const segment = segments.find((entry) => entry.start <= offset && entry.end > offset);
|
||||
if (!segment) return { fileName: entry, line, column };
|
||||
const prefix = files.get(segment.fileName)!.slice(0, segment.sourceStart + offset - segment.start);
|
||||
return {
|
||||
fileName: segment.fileName,
|
||||
line: prefix.split("\n").length,
|
||||
column: prefix.length - prefix.lastIndexOf("\n") - 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const loadQxSources = (root: string) => resolveQxSources((name) => readQxSource(root, name));
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ParserRuleContext } from "antlr4ng";
|
||||
import { parseDocument } from "./parser.js";
|
||||
import { QuixosCapabilityParser } from "./generated/QuixosCapabilityParser.js";
|
||||
|
||||
/** Offsets and columns use UTF-16, as do JavaScript and LSP. End is exclusive. */
|
||||
export type SourceRange = { start: number; end: number };
|
||||
export type SourceEdit = SourceRange & { text: string };
|
||||
export type SyntaxNode = SourceRange & { kind: string; children: SyntaxNode[] };
|
||||
|
||||
export const parseQx = (source: string, fileName = "<memory>") => {
|
||||
const parsed = parseDocument(source, fileName);
|
||||
// ANTLR indexes Unicode code points; editors index UTF-16 code units.
|
||||
const offsets = [0];
|
||||
for (const character of source) offsets.push(offsets[offsets.length - 1]! + character.length);
|
||||
const offset = (index: number) => offsets[Math.max(0, index)] ?? source.length;
|
||||
const node = (context: ParserRuleContext): SyntaxNode => ({
|
||||
kind: QuixosCapabilityParser.ruleNames[context.ruleIndex]!,
|
||||
start: offset(context.start?.start ?? 0),
|
||||
end: offset((context.stop?.stop ?? -1) + 1),
|
||||
children: context.children.flatMap((child) => (child instanceof ParserRuleContext ? [node(child)] : [])),
|
||||
});
|
||||
return {
|
||||
source,
|
||||
fileName,
|
||||
root: node(parsed.tree),
|
||||
diagnostics: parsed.diagnostics.map((diagnostic) => {
|
||||
const line = source.split("\n")[diagnostic.line - 1] ?? "";
|
||||
return { ...diagnostic, column: [...line].slice(0, diagnostic.column).join("").length };
|
||||
}),
|
||||
tokens: parsed.tokens
|
||||
.getTokens()
|
||||
.filter((token) => token.type !== -1)
|
||||
.map((token) => ({
|
||||
kind: QuixosCapabilityParser.symbolicNames[token.type] ?? "token",
|
||||
start: offset(token.start),
|
||||
end: offset(token.stop + 1),
|
||||
text: token.text ?? "",
|
||||
trivia: token.channel !== 0,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
/** Edits refer to one immutable source snapshot; overlapping edits are errors. */
|
||||
export const applySourceEdits = (source: string, edits: readonly SourceEdit[]) => {
|
||||
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
||||
let end = 0;
|
||||
let previousStart = -1;
|
||||
let result = "";
|
||||
for (const edit of sorted) {
|
||||
if (
|
||||
!Number.isInteger(edit.start) ||
|
||||
!Number.isInteger(edit.end) ||
|
||||
edit.start < end ||
|
||||
edit.start === previousStart ||
|
||||
edit.end < edit.start ||
|
||||
edit.end > source.length
|
||||
) {
|
||||
throw new Error("Invalid or overlapping source edits");
|
||||
}
|
||||
result += source.slice(end, edit.start) + edit.text;
|
||||
end = edit.end;
|
||||
previousStart = edit.start;
|
||||
}
|
||||
return result + source.slice(end);
|
||||
};
|
||||
|
||||
export const walkSyntax = function* (node: SyntaxNode): Generator<SyntaxNode> {
|
||||
yield node;
|
||||
for (const child of node.children) yield* walkSyntax(child);
|
||||
};
|
||||
|
||||
export const lintQx = (source: string, fileName = "<memory>") => {
|
||||
const syntax = parseQx(source, fileName);
|
||||
const diagnostics = syntax.diagnostics.map((entry) => ({ ...entry, severity: "error" as "error" | "warning" }));
|
||||
if (diagnostics.length) return diagnostics;
|
||||
const seen = new Set<string>();
|
||||
for (const node of walkSyntax(syntax.root)) {
|
||||
if (node.kind !== "sourceImportDecl") continue;
|
||||
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
|
||||
const importPath: string = JSON.parse(source.slice(literal.start, literal.end));
|
||||
let message: string | undefined;
|
||||
let code = "invalid-source-import";
|
||||
try {
|
||||
validateQxImportPath(importPath);
|
||||
} catch (error) {
|
||||
message = (error as Error).message;
|
||||
}
|
||||
if (!message && seen.has(importPath)) {
|
||||
code = "duplicate-source-import";
|
||||
message = `Repeated local import ${importPath}`;
|
||||
}
|
||||
seen.add(importPath);
|
||||
if (message) {
|
||||
const prefix = source.slice(0, node.start);
|
||||
diagnostics.push({
|
||||
phase: "syntax",
|
||||
code,
|
||||
message,
|
||||
fileName,
|
||||
line: prefix.split("\n").length,
|
||||
column: prefix.length - prefix.lastIndexOf("\n") - 1,
|
||||
severity: code === "duplicate-source-import" ? "warning" : "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
};
|
||||
|
||||
/** Conservative formatter: indentation only, preserving strings and comments verbatim. */
|
||||
export const formatQx = (source: string) => {
|
||||
const syntax = parseQx(source);
|
||||
if (syntax.diagnostics.length) throw new Error("Cannot format QX with syntax errors");
|
||||
const edits: SourceEdit[] = [];
|
||||
let depth = 0;
|
||||
let lineStart = 0;
|
||||
for (const token of syntax.tokens) {
|
||||
if (token.kind === "WS") continue;
|
||||
lineStart = source.lastIndexOf("\n", token.start - 1) + 1;
|
||||
if (/^[ \t]*$/.test(source.slice(lineStart, token.start))) {
|
||||
const indentation = " ".repeat(Math.max(0, depth - (token.kind === "RBRACE" ? 1 : 0)));
|
||||
if (source.slice(lineStart, token.start) !== indentation)
|
||||
edits.push({ start: lineStart, end: token.start, text: indentation });
|
||||
}
|
||||
if (!token.trivia) {
|
||||
if (token.kind === "LBRACE") depth++;
|
||||
if (token.kind === "RBRACE") depth--;
|
||||
}
|
||||
}
|
||||
return applySourceEdits(source, edits);
|
||||
};
|
||||
|
||||
export const addWorkspaceImport = (source: string, importPath: string) => {
|
||||
validateQxImportPath(importPath);
|
||||
const syntax = parseQx(source);
|
||||
if (syntax.diagnostics.length || syntax.root.children[0]?.kind !== "workspaceDecl")
|
||||
throw new Error("Expected a syntactically valid workspace");
|
||||
for (const node of walkSyntax(syntax.root)) {
|
||||
if (node.kind === "sourceImportDecl") {
|
||||
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
|
||||
if (JSON.parse(source.slice(literal.start, literal.end)) === importPath) return source;
|
||||
}
|
||||
}
|
||||
const brace = syntax.tokens.find((token) => token.kind === "LBRACE")!;
|
||||
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
||||
return applySourceEdits(source, [
|
||||
{ start: brace.end, end: brace.end, text: `${newline} import ${JSON.stringify(importPath)};` },
|
||||
]);
|
||||
};
|
||||
|
||||
export const validateQxImportPath = (value: string) => {
|
||||
if (
|
||||
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.qx$/.test(value) ||
|
||||
value.split("/").some((part) => part === "." || part === "..")
|
||||
)
|
||||
throw new Error(`Invalid repository-relative QX import path: ${value}`);
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
import { CharStream, CommonTokenStream } from "antlr4ng";
|
||||
import { QuixosLockLexer } from "../resource-lock/generated/QuixosLockLexer.js";
|
||||
import { QuixosLockParser } from "../resource-lock/generated/QuixosLockParser.js";
|
||||
import { parseQuixosLockDocument } from "../resource-lock/parser.js";
|
||||
import { parseQx, walkSyntax, applySourceEdits, type SyntaxNode } from "./source.js";
|
||||
|
||||
export type StructuralSelector = { kind: string; id?: string; name?: string; names?: string[] };
|
||||
export type StructuralEdit =
|
||||
| { operation: "append"; parent: StructuralSelector; source: string }
|
||||
| { operation: "replace"; target: StructuralSelector; source: string }
|
||||
| { operation: "remove"; target: StructuralSelector }
|
||||
| { operation: "import"; kind: "interface" | "package"; name: string }
|
||||
| { operation: "semantic-major"; target: StructuralSelector; major: number }
|
||||
| { operation: "conformance-id"; target: StructuralSelector; id: string }
|
||||
| { operation: "quixos-pin"; source: { repository: string; commit: string } }
|
||||
| {
|
||||
operation: "dependency";
|
||||
kind: "interface" | "package";
|
||||
name: string;
|
||||
source: { repository: string; commit: string } | null;
|
||||
};
|
||||
|
||||
// Deliberately exclude valueType/identifier/stringLiteral: callers operate on
|
||||
// declaration structure, not arbitrary token offsets or lockfile text patches.
|
||||
const selectable = new Set([
|
||||
"workspaceDecl",
|
||||
"fragmentDecl",
|
||||
"interfaceResourceDecl",
|
||||
"packageResourceDecl",
|
||||
"atomDecl",
|
||||
"valueMember",
|
||||
"relationshipMember",
|
||||
"operationMember",
|
||||
"packageOperationExport",
|
||||
"packageFunctionExport",
|
||||
"packageConstructorExport",
|
||||
"conformanceDecl",
|
||||
"stateDecl",
|
||||
"edgeDecl",
|
||||
"constructorBindingDecl",
|
||||
"resourceImportDecl",
|
||||
"sourceImportDecl",
|
||||
"operationBindingDecl",
|
||||
]);
|
||||
|
||||
/** Bind only the template root identity; schema/atom identities are reusable.
|
||||
* The revision's real identity is derived from its containing commit at compile time. */
|
||||
export const instantiateWorkspaceIdentity = (source: string, workspaceId: string): string => {
|
||||
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(workspaceId))
|
||||
throw new Error("Workspace identity must be a UUID");
|
||||
const syntax = parseQx(source);
|
||||
if (syntax.diagnostics.length) throw new Error("Cannot instantiate a malformed template workspace");
|
||||
const declaration = syntax.root.children.find((node) => node.kind === "workspaceDecl");
|
||||
const literals = declaration?.children.filter((node) => node.kind === "stringLiteral");
|
||||
if (!literals || literals.length !== 3) throw new Error("Template must contain a workspace declaration");
|
||||
return applySourceEdits(source, [
|
||||
{ ...literals[0], text: JSON.stringify(workspaceId) },
|
||||
{ ...literals[1], text: JSON.stringify(`workspace-revision:${workspaceId}:source`) },
|
||||
]);
|
||||
};
|
||||
|
||||
const select = (
|
||||
source: string,
|
||||
selector: StructuralSelector,
|
||||
): { node: SyntaxNode; syntax: ReturnType<typeof parseQx> } => {
|
||||
if (!selectable.has(selector.kind)) throw new Error(`Unsupported structural selector ${selector.kind}`);
|
||||
const syntax = parseQx(source);
|
||||
if (syntax.diagnostics.length) throw new Error("Cannot scaffold syntactically invalid QX");
|
||||
const matches = [...walkSyntax(syntax.root)].filter((node) => {
|
||||
if (node.kind !== selector.kind) return false;
|
||||
if (
|
||||
selector.name &&
|
||||
!node.children.some(
|
||||
(child) => child.kind === "identifier" && source.slice(child.start, child.end) === selector.name,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
selector.names &&
|
||||
JSON.stringify(
|
||||
node.children
|
||||
.filter((child) => child.kind === "identifier")
|
||||
.map((child) => source.slice(child.start, child.end)),
|
||||
) !== JSON.stringify(selector.names)
|
||||
)
|
||||
return false;
|
||||
if (selector.id) {
|
||||
// Only an explicit ID field counts, not a coincidentally equal revision,
|
||||
// default value, nested declaration, or comment.
|
||||
const tokens = syntax.tokens.filter(
|
||||
(token) => !token.trivia && token.start >= node.start && token.end <= node.end,
|
||||
);
|
||||
const literal = node.children.find(
|
||||
(child) =>
|
||||
child.kind === "stringLiteral" &&
|
||||
tokens.some((token, index) => token.start === child.start && tokens[index - 1]?.kind === "ID"),
|
||||
);
|
||||
if (!literal || JSON.parse(source.slice(literal.start, literal.end)) !== selector.id) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (matches.length !== 1) throw new Error(`Structural selector must resolve exactly once (found ${matches.length})`);
|
||||
return { node: matches[0], syntax };
|
||||
};
|
||||
|
||||
/** Comment-preserving structural edits; every result is parsed before returning. */
|
||||
export const editStructure = (source: string, edit: StructuralEdit): string => {
|
||||
if (edit.operation === "quixos-pin") {
|
||||
const parsed = parseQuixosLockDocument(source);
|
||||
if (!parsed.ok || parsed.document.kind !== "root") throw new Error("Quixos pins belong in a valid root lockfile");
|
||||
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
|
||||
const entries = parser.document().quixosEntry();
|
||||
if (entries.length !== 1) throw new Error("Expected one Quixos source declaration");
|
||||
const literals = entries[0].quixosSourceBlock().stringLiteral();
|
||||
const offsets = [0];
|
||||
for (const character of source) offsets.push(offsets[offsets.length - 1] + character.length);
|
||||
const result = applySourceEdits(source, [
|
||||
{
|
||||
start: offsets[literals[0].start!.start],
|
||||
end: offsets[literals[0].stop!.stop + 1],
|
||||
text: JSON.stringify(edit.source.repository),
|
||||
},
|
||||
{
|
||||
start: offsets[literals[literals.length - 1].start!.start],
|
||||
end: offsets[literals[literals.length - 1].stop!.stop + 1],
|
||||
text: JSON.stringify(edit.source.commit),
|
||||
},
|
||||
]);
|
||||
const checked = parseQuixosLockDocument(result);
|
||||
if (!checked.ok)
|
||||
throw new Error(`Invalid Quixos pin: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
|
||||
return result;
|
||||
}
|
||||
if (edit.operation === "import") {
|
||||
if (!["interface", "package"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
|
||||
throw new Error("Invalid resource import");
|
||||
const syntax = parseQx(source);
|
||||
if (syntax.diagnostics.length) throw new Error("Cannot scaffold invalid QX");
|
||||
const text = `import ${edit.kind} ${edit.name};`;
|
||||
if (
|
||||
[...walkSyntax(syntax.root)].some(
|
||||
(entry) =>
|
||||
entry.kind === "resourceImportDecl" && source.slice(entry.start, entry.end).replace(/\s+/g, " ") === text,
|
||||
)
|
||||
)
|
||||
return source;
|
||||
const root = syntax.root.children[0];
|
||||
const position = ["workspaceDecl", "fragmentDecl"].includes(root.kind)
|
||||
? syntax.tokens.find((token) => token.kind === "LBRACE")!.end
|
||||
: root.start;
|
||||
const result = applySourceEdits(source, [{ start: position, end: position, text: `\n${text}\n` }]);
|
||||
if (parseQx(result).diagnostics.length) throw new Error("Invalid resource import position");
|
||||
return result;
|
||||
}
|
||||
if (edit.operation === "dependency") {
|
||||
const parsed = parseQuixosLockDocument(source);
|
||||
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))
|
||||
throw new Error("Invalid dependency selector");
|
||||
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
|
||||
const tree = parser.document();
|
||||
const offsets = [0];
|
||||
for (const character of source) offsets.push(offsets[offsets.length - 1] + character.length);
|
||||
const entries = tree
|
||||
.resourceEntry()
|
||||
.filter((entry) => entry.resourceKind().getText() === edit.kind && entry.identifier().getText() === edit.name);
|
||||
if (entries.length > 1) throw new Error("Ambiguous dependency selector");
|
||||
const entry = entries[0];
|
||||
const replacement = edit.source
|
||||
? `${edit.kind} ${edit.name} source {\n repository ${JSON.stringify(edit.source.repository)};\n commit ${JSON.stringify(edit.source.commit)};\n}`
|
||||
: "";
|
||||
if (!entry && !edit.source) throw new Error("Cannot remove an absent dependency");
|
||||
const start = entry ? offsets[entry.start!.start] : offsets[tree.RBRACE().symbol.start];
|
||||
const end = entry ? offsets[entry.stop!.stop + 1] : start;
|
||||
const result = applySourceEdits(source, [{ start, end, text: entry ? replacement : `${replacement}\n` }]);
|
||||
const checked = parseQuixosLockDocument(result);
|
||||
if (!checked.ok)
|
||||
throw new Error(`Invalid dependency change: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
|
||||
return result;
|
||||
}
|
||||
const { node, syntax } = select(source, edit.operation === "append" ? edit.parent : edit.target);
|
||||
let result: string;
|
||||
if (edit.operation === "semantic-major") {
|
||||
if (
|
||||
!["conformanceDecl", "packageResourceDecl"].includes(node.kind) ||
|
||||
!Number.isSafeInteger(edit.major) ||
|
||||
edit.major < 1
|
||||
)
|
||||
throw new Error("Semantic major requires a package/conformance and positive integer");
|
||||
const tokens = syntax.tokens.filter((token) => !token.trivia && token.start >= node.start && token.end <= node.end);
|
||||
const marker = tokens.findIndex((token) => token.kind === "SEMANTIC_MAJOR");
|
||||
const value = marker < 0 ? undefined : tokens[marker + 1];
|
||||
const brace = tokens.find((token) => token.kind === "LBRACE")!;
|
||||
result = applySourceEdits(source, [
|
||||
{
|
||||
start: value?.start ?? brace.start,
|
||||
end: value?.end ?? brace.start,
|
||||
text: value ? String(edit.major) : `semantic-major ${edit.major} `,
|
||||
},
|
||||
]);
|
||||
} else if (edit.operation === "conformance-id") {
|
||||
if (node.kind !== "conformanceDecl" || !edit.id)
|
||||
throw new Error("Identity enrollment requires a conformance and stable ID");
|
||||
const existing = node.children.find((entry) => entry.kind === "stringLiteral");
|
||||
if (existing) {
|
||||
if (JSON.parse(source.slice(existing.start, existing.end)) !== edit.id)
|
||||
throw new Error("Cannot change an enrolled conformance identity; create a new conformance explicitly");
|
||||
return source;
|
||||
}
|
||||
const identifiers = node.children.filter((entry) => entry.kind === "identifier");
|
||||
const position = identifiers[identifiers.length - 1].end;
|
||||
result = applySourceEdits(source, [{ start: position, end: position, text: ` id ${JSON.stringify(edit.id)}` }]);
|
||||
} else if (edit.operation === "append") {
|
||||
const closing = syntax.tokens.find((token) => token.kind === "RBRACE" && token.end === node.end);
|
||||
if (!closing) throw new Error("Append requires a declaration with a body");
|
||||
result = applySourceEdits(source, [{ start: closing.start, end: closing.start, text: `\n${edit.source}\n` }]);
|
||||
} else {
|
||||
// A resource parser node includes imports/external declarations preceding
|
||||
// its header. Replacing the declaration must not delete that preamble.
|
||||
const identifier = node.children.find((child) => child.kind === "identifier");
|
||||
const header =
|
||||
["packageResourceDecl", "interfaceResourceDecl"].includes(node.kind) && identifier
|
||||
? syntax.tokens
|
||||
.filter(
|
||||
(token) =>
|
||||
token.start >= node.start &&
|
||||
token.end <= identifier.start &&
|
||||
token.kind === (node.kind === "packageResourceDecl" ? "PACKAGE" : "INTERFACE"),
|
||||
)
|
||||
.at(-1)?.start
|
||||
: undefined;
|
||||
const wrapper =
|
||||
edit.operation === "remove" && ["stateDecl", "edgeDecl"].includes(node.kind)
|
||||
? [...walkSyntax(syntax.root)]
|
||||
.filter(
|
||||
(entry) =>
|
||||
["conformanceItem", "sharedAttachmentDecl"].includes(entry.kind) &&
|
||||
entry.start <= node.start &&
|
||||
entry.end >= node.end,
|
||||
)
|
||||
.sort((a, b) => a.end - a.start - (b.end - b.start))[0]
|
||||
: undefined;
|
||||
result = applySourceEdits(source, [
|
||||
{
|
||||
start: wrapper?.start ?? header ?? node.start,
|
||||
end: wrapper?.end ?? node.end,
|
||||
text: edit.operation === "replace" ? edit.source : "",
|
||||
},
|
||||
]);
|
||||
}
|
||||
const checked = parseQx(result);
|
||||
if (checked.diagnostics.length)
|
||||
throw new Error(`Invalid structural change: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const scaffoldResourceSource = (kind: "interface" | "package", name: string, id: string, revision: string) => {
|
||||
if (!/^[A-Z][A-Za-z0-9]*$/.test(name) || !id || !revision)
|
||||
throw new Error("Resource scaffold requires a PascalCase name and explicit identities");
|
||||
const source = `${kind} ${name} id ${JSON.stringify(id)} revision ${JSON.stringify(revision)} {\n}\n`;
|
||||
if (parseQx(source).diagnostics.length) throw new Error("Invalid resource scaffold");
|
||||
return source;
|
||||
};
|
||||
@@ -0,0 +1,321 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { contentDigest } from "../capability-model/evolution.js";
|
||||
import { editStructure, type StructuralEdit } from "./structural-edits.js";
|
||||
import { snapshotRepository, localResourceSnapshots } from "./candidate-check.js";
|
||||
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { bindingSchema, generateTypeScriptBindings } from "../bindings/index.js";
|
||||
import { parseQx } from "./source.js";
|
||||
import { parseQuixosLockDocument } from "../resource-lock/index.js";
|
||||
import { withFileLock } from "./file-lock.js";
|
||||
|
||||
export type StructuralRequest = {
|
||||
kind: "workspace" | "interface" | "package";
|
||||
source?: { repository: string; commit: string };
|
||||
resourceRoot?: string;
|
||||
validation?: "syntax" | "resource-graph";
|
||||
files: (
|
||||
| { file: string; edits: StructuralEdit[] }
|
||||
| { file: string; create: string }
|
||||
| { file: string; generated: string }
|
||||
| { file: string; expected: string; replace: string }
|
||||
)[];
|
||||
};
|
||||
type Change = { file: string; before: string | null; after: string; mode: number };
|
||||
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
||||
const safeFile = (file: string) => {
|
||||
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(
|
||||
file,
|
||||
) ||
|
||||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
||||
)
|
||||
throw new Error(`Unsafe scaffold path ${file}`);
|
||||
};
|
||||
const read = async (root: string, file: string): Promise<string | null> => {
|
||||
safeFile(file);
|
||||
const target = path.join(root, file);
|
||||
try {
|
||||
const metadata = await fs.lstat(target);
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink() || !(await fs.realpath(target)).startsWith(`${root}/`))
|
||||
throw new Error(`Scaffold target is not a contained regular file: ${file}`);
|
||||
return await fs.readFile(target, "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const containedParent = async (root: string, file: string) => {
|
||||
let current = root;
|
||||
for (const part of file.split("/").slice(0, -1)) {
|
||||
current = path.join(current, part);
|
||||
await fs.mkdir(current).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code !== "EEXIST") throw error;
|
||||
});
|
||||
const metadata = await fs.lstat(current);
|
||||
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
||||
throw new Error("Scaffold parent must be a real directory");
|
||||
}
|
||||
};
|
||||
const durableJson = async (file: string, value: unknown) => {
|
||||
const temporary = `${file}.${randomUUID()}.tmp`;
|
||||
const handle = await fs.open(temporary, "wx", 0o600);
|
||||
try {
|
||||
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await fs.rename(temporary, file);
|
||||
const directory = await fs.open(path.dirname(file), "r");
|
||||
try {
|
||||
await directory.sync();
|
||||
} finally {
|
||||
await directory.close();
|
||||
}
|
||||
};
|
||||
|
||||
/** Validate the entire edited resource graph in a private snapshot before writes. */
|
||||
export const planStructure = async (rootPath: string, request: StructuralRequest, snapshotMap?: string) => {
|
||||
if (request.validation && !["syntax", "resource-graph"].includes(request.validation))
|
||||
throw new Error("Unknown structural validation mode");
|
||||
const root = await fs.realpath(rootPath);
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-structure-"));
|
||||
try {
|
||||
const snapshot = await snapshotRepository(root, path.join(temporary, "source"));
|
||||
const observed = await Promise.all(
|
||||
snapshot.files.map(async ({ name }) => ({
|
||||
file: name,
|
||||
digest: contentDigest(await fs.readFile(path.join(snapshot.directory, name), "utf8")),
|
||||
})),
|
||||
);
|
||||
const changes: Change[] = [];
|
||||
if (!Array.isArray(request.files) || !request.files.length || request.files.length > 100)
|
||||
throw new Error("Structural plan requires 1–100 files");
|
||||
for (const input of request.files) {
|
||||
safeFile(input.file);
|
||||
if (changes.some((entry) => entry.file === input.file)) throw new Error("Repeated structural file target");
|
||||
const before = await read(root, input.file);
|
||||
let after: string;
|
||||
if ("create" in input) {
|
||||
if (before !== null || typeof input.create !== "string")
|
||||
throw new Error("Scaffold creation cannot replace an existing file");
|
||||
after = input.create;
|
||||
} else if ("replace" in input) {
|
||||
if (before !== input.expected || typeof input.replace !== "string")
|
||||
throw new Error(`Stale imperative edit: ${input.file}`);
|
||||
after = input.replace;
|
||||
} else if ("generated" in input) {
|
||||
const generated = (text: string) =>
|
||||
text.startsWith("// Generated by qx-scaffold-v1\n") ||
|
||||
text.startsWith("# Generated by qx-scaffold-v1\n") ||
|
||||
(() => {
|
||||
try {
|
||||
return JSON.parse(text).generatedBy === "qx-scaffold-v1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (
|
||||
typeof input.generated !== "string" ||
|
||||
!generated(input.generated) ||
|
||||
(before !== null && !generated(before))
|
||||
)
|
||||
throw new Error("Only scaffold-owned generated files may be regenerated");
|
||||
after = input.generated;
|
||||
} else {
|
||||
if (before === null || !Array.isArray(input.edits))
|
||||
throw new Error("Structural edit requires an existing source");
|
||||
after = input.edits.reduce(editStructure, before);
|
||||
}
|
||||
if (Buffer.byteLength(after) > 1024 * 1024) throw new Error("Scaffold file exceeds 1 MiB");
|
||||
const mode = before === null ? 0o644 : (await fs.stat(path.join(root, input.file))).mode & 0o777;
|
||||
changes.push({ file: input.file, before, after, mode });
|
||||
await containedParent(snapshot.directory, input.file);
|
||||
await fs.writeFile(path.join(snapshot.directory, input.file), after);
|
||||
}
|
||||
if (request.validation === "syntax") {
|
||||
for (const change of changes) {
|
||||
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length)
|
||||
throw new Error(`Invalid QX syntax in ${change.file}`);
|
||||
if (change.file.endsWith(".lock") && !parseQuixosLockDocument(change.after, change.file).ok)
|
||||
throw new Error(`Invalid lock syntax in ${change.file}`);
|
||||
}
|
||||
} else {
|
||||
const localMap = path.join(temporary, "local-resources.json");
|
||||
await fs.writeFile(localMap, JSON.stringify(await localResourceSnapshots(root, snapshotMap)));
|
||||
const resolveResource = await createGitCapabilityResolver({
|
||||
checkoutRoot: path.join(temporary, "resources"),
|
||||
snapshotMap: localMap,
|
||||
});
|
||||
if (
|
||||
request.resourceRoot &&
|
||||
!/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(request.resourceRoot)
|
||||
)
|
||||
throw new Error("Resource root must be a contained relative directory");
|
||||
const resourceRoot = path.join(snapshot.directory, request.resourceRoot ?? "");
|
||||
if (request.kind === "workspace")
|
||||
await compileWorkspaceRepository({ rootDirectory: resourceRoot, resolveResource });
|
||||
else if (["package", "interface"].includes(request.kind) && request.source) {
|
||||
const compiled = await compileCapabilityResourceRepository({
|
||||
rootDirectory: resourceRoot,
|
||||
kind: request.kind as "package" | "interface",
|
||||
source: { resolver: "git", ...request.source },
|
||||
resolveResource,
|
||||
});
|
||||
if (compiled.resource.kind === "package") {
|
||||
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
|
||||
const artifacts = [
|
||||
{
|
||||
file: configuration.bindingOutput as string,
|
||||
after: generateTypeScriptBindings(
|
||||
bindingSchema(compiled),
|
||||
compiled.resource.revision.revisionId,
|
||||
configuration.options,
|
||||
),
|
||||
},
|
||||
];
|
||||
for (const artifact of artifacts) {
|
||||
const file = request.resourceRoot ? `${request.resourceRoot}/${artifact.file}` : artifact.file;
|
||||
safeFile(file);
|
||||
if (Buffer.byteLength(artifact.after) > 1024 * 1024)
|
||||
throw new Error("Generated scaffold file exceeds 1 MiB");
|
||||
const before = await read(root, file);
|
||||
if (
|
||||
before !== null &&
|
||||
!before.startsWith("// Generated by quixos-codegen-ts.") &&
|
||||
(() => {
|
||||
try {
|
||||
return JSON.parse(before).generatedBy !== "qx-scaffold-v1";
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
})()
|
||||
)
|
||||
throw new Error(`Refusing to overwrite hand-authored generated artifact ${file}`);
|
||||
const previous = changes.find((entry) => entry.file === file);
|
||||
if (previous) previous.after = artifact.after;
|
||||
else changes.push({ file, before, after: artifact.after, mode: 0o644 });
|
||||
}
|
||||
}
|
||||
} else throw new Error("Resource plans require kind and exact authored source identity");
|
||||
}
|
||||
if (changes.length > 100) throw new Error("Structural plan including generated artifacts exceeds 100 files");
|
||||
// Validation may fetch dependencies; reject edits made while it was running.
|
||||
for (const entry of changes)
|
||||
if ((await read(root, entry.file)) !== entry.before)
|
||||
throw new Error(`Source changed while planning: ${entry.file}`);
|
||||
for (const entry of observed)
|
||||
if (contentDigest(await fs.readFile(path.join(root, entry.file), "utf8")) !== entry.digest)
|
||||
throw new Error(`Validation input changed while planning: ${entry.file}`);
|
||||
return {
|
||||
root,
|
||||
changes,
|
||||
observed,
|
||||
digest: contentDigest(changes),
|
||||
validation: request.validation ?? ("resource-graph" as const),
|
||||
};
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
/** Replay only exact before/after states. A crash never loses the original text. */
|
||||
const replayStructure = async (rootPath: string, id: string) => {
|
||||
const root = await fs.realpath(rootPath);
|
||||
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid scaffold journal ID");
|
||||
const journalPath = path.join(root, ".quixos", "scaffolds", `${id}.json`);
|
||||
const journal = JSON.parse(await fs.readFile(journalPath, "utf8")) as Journal;
|
||||
if (journal.schemaVersion !== 1 || journal.root !== root || journal.id !== id)
|
||||
throw new Error("Scaffold journal identity mismatch");
|
||||
for (const entry of journal.changes) {
|
||||
const current = await read(root, entry.file);
|
||||
if (current !== entry.before && current !== entry.after)
|
||||
throw new Error(
|
||||
`Scaffold conflicts with newer edits: ${entry.file}; original text is retained in ${journalPath}`,
|
||||
);
|
||||
}
|
||||
if (journal.phase === "complete") return { id, journalPath, phase: journal.phase };
|
||||
for (const entry of journal.changes) {
|
||||
if ((await read(root, entry.file)) === entry.after) continue;
|
||||
await containedParent(root, entry.file);
|
||||
const target = path.join(root, entry.file);
|
||||
const temporary = `${target}.qx-${randomUUID()}.tmp`;
|
||||
const handle = await fs.open(temporary, "wx", entry.mode);
|
||||
try {
|
||||
await handle.writeFile(entry.after);
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
if (entry.before === null) {
|
||||
// link is atomic and fails if another author created the destination.
|
||||
await fs.link(temporary, target);
|
||||
await fs.unlink(temporary);
|
||||
} else {
|
||||
if ((await read(root, entry.file)) !== entry.before)
|
||||
throw new Error(`Source changed during scaffold: ${entry.file}`);
|
||||
await fs.rename(temporary, target);
|
||||
}
|
||||
const directory = await fs.open(path.dirname(target), "r");
|
||||
try {
|
||||
await directory.sync();
|
||||
} finally {
|
||||
await directory.close();
|
||||
}
|
||||
}
|
||||
journal.phase = "complete";
|
||||
await durableJson(journalPath, journal);
|
||||
return { id, journalPath, phase: journal.phase };
|
||||
};
|
||||
|
||||
const withStructureLock = async <T>(root: string, work: () => Promise<T>) => {
|
||||
await containedParent(root, ".quixos/scaffolds/placeholder.json");
|
||||
const lock = path.join(root, ".quixos", "scaffolds", "writer.lock");
|
||||
return withFileLock(lock, work);
|
||||
};
|
||||
export const resumeStructure = async (rootPath: string, id: string) => {
|
||||
const root = await fs.realpath(rootPath);
|
||||
return withStructureLock(root, () => replayStructure(root, id));
|
||||
};
|
||||
export const applyStructure = async (plan: Awaited<ReturnType<typeof planStructure>>, id: string = randomUUID()) =>
|
||||
withStructureLock(plan.root, async () => {
|
||||
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid scaffold journal ID");
|
||||
const directory = path.join(plan.root, ".quixos", "scaffolds");
|
||||
try {
|
||||
const existing = JSON.parse(await fs.readFile(path.join(directory, `${id}.json`), "utf8")) as Journal;
|
||||
if (existing.root !== plan.root || contentDigest(existing.changes) !== plan.digest)
|
||||
throw new Error("Scaffold journal identity conflict");
|
||||
for (const entry of plan.observed)
|
||||
if (
|
||||
!plan.changes.some((change) => change.file === entry.file) &&
|
||||
contentDigest(await fs.readFile(path.join(plan.root, entry.file), "utf8")) !== entry.digest
|
||||
)
|
||||
throw new Error(`Stale scaffold validation input: ${entry.file}`);
|
||||
return replayStructure(plan.root, id);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
// An unfinished journal must be recovered before another structural mutation.
|
||||
for (const file of await fs.readdir(directory))
|
||||
if (file.endsWith(".json")) {
|
||||
const prior = JSON.parse(await fs.readFile(path.join(directory, file), "utf8")) as Journal;
|
||||
if (prior.phase !== "complete") throw new Error(`Unfinished scaffold ${prior.id}; resume it first`);
|
||||
}
|
||||
for (const entry of plan.changes)
|
||||
if ((await read(plan.root, entry.file)) !== entry.before) throw new Error(`Stale scaffold plan: ${entry.file}`);
|
||||
for (const entry of plan.observed)
|
||||
if (contentDigest(await fs.readFile(path.join(plan.root, entry.file), "utf8")) !== entry.digest)
|
||||
throw new Error(`Stale scaffold validation input: ${entry.file}`);
|
||||
await durableJson(path.join(directory, `${id}.json`), {
|
||||
schemaVersion: 1,
|
||||
id,
|
||||
root: plan.root,
|
||||
phase: "prepared",
|
||||
changes: plan.changes,
|
||||
} satisfies Journal);
|
||||
return replayStructure(plan.root, id);
|
||||
});
|
||||
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises";
|
||||
import { parseQx, formatQx, lintQx } from "./source.js";
|
||||
import { scaffoldAtom } from "./scaffold.js";
|
||||
import { loadQxSources } from "./source-loader.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { planEvolution } from "../capability-model/index.js";
|
||||
import { snapshotRepository } from "./candidate-check.js";
|
||||
import { planPinUpgrades, applyPinUpgrades, discoverUpgradeSpec, type UpgradeSpec } from "./pin-upgrades.js";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
|
||||
import { scaffoldRecipe, type ScaffoldRecipe } from "./scaffold-recipes.js";
|
||||
import { checkBundleSources } from "../bindings/bundle-policy.js";
|
||||
import { sealMigrations } from "./migration-seal.js";
|
||||
import { generatePackageDescriptor } from "../bindings/index.js";
|
||||
import { buildCheckedPackage, buildImmutableCandidate, snapshotCommit } from "./checked-build.js";
|
||||
import { formatQuixosLock, loadQuixosLock, parseQuixosLockDocument } from "../resource-lock/index.js";
|
||||
import { walkSyntax } from "./source.js";
|
||||
import { inspectWorkbench } from "./authoring-inspect.js";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { convergeAuthoring } from "./authoring-converge.js";
|
||||
import { checkAuthoring } from "./authoring-check.js";
|
||||
import { authoringWorklist } from "./authoring-worklist.js";
|
||||
|
||||
const authorSource = async (root: string) => {
|
||||
const result = spawnSync("git", ["config", "--get", "remote.origin.url"], { cwd: root, encoding: "utf8" });
|
||||
if (result.error || result.status !== 0) throw new Error("Managed resource has no origin");
|
||||
return { repository: result.stdout.trim(), commit: await snapshotCommit(root) };
|
||||
};
|
||||
|
||||
const readSpec = async (value: string) => {
|
||||
if (value === "-") {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) {
|
||||
input += chunk;
|
||||
if (input.length > 1024 * 1024) throw new Error("Scaffold specification exceeds 1 MiB");
|
||||
}
|
||||
return JSON.parse(input);
|
||||
}
|
||||
return JSON.parse(value.trimStart().startsWith("{") ? value : await readFile(value, "utf8"));
|
||||
};
|
||||
const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
|
||||
root: plan.root,
|
||||
validation: plan.validation,
|
||||
changes: plan.changes.map((change) => ({
|
||||
file: change.file,
|
||||
beforeBytes: change.before?.length ?? 0,
|
||||
afterBytes: change.after?.length ?? 0,
|
||||
})),
|
||||
note: "Structural plan only, not implementation verification. Run qx-workspace check while iterating.",
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (command === "scaffold-validate-qx") {
|
||||
if (args.length !== 1) throw new Error("scaffold-validate-qx SOURCES_JSON");
|
||||
const sources = JSON.parse(args[0]);
|
||||
if (!Array.isArray(sources) || sources.length > 100) throw new Error("Expected at most 100 scaffold sources");
|
||||
for (const entry of sources) {
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry.file !== "string" ||
|
||||
typeof entry.source !== "string" ||
|
||||
entry.source.length > 1024 * 1024
|
||||
)
|
||||
throw new Error("Invalid scaffold source");
|
||||
const diagnostics = parseQx(entry.source, entry.file).diagnostics;
|
||||
if (diagnostics.length)
|
||||
throw new Error(diagnostics.map((d) => `${entry.file}:${d.line}:${d.column + 1}: ${d.message}`).join("\n"));
|
||||
}
|
||||
process.stdout.write('{"syntaxValid":true,"verificationEvidence":false}\n');
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-placement-binding") {
|
||||
if (args.length !== 1) throw new Error("scaffold-placement-binding ROOT");
|
||||
const { source } = await loadQxSources(args[0]);
|
||||
const syntax = parseQx(source);
|
||||
const text = (n: { start: number; end: number }) => source.slice(n.start, n.end);
|
||||
const matches: string[] = [];
|
||||
for (const edge of walkSyntax(syntax.root)) {
|
||||
if (edge.kind !== "edgeDecl") continue;
|
||||
for (const endpoint of edge.children.filter((n) => n.kind === "edgeEndpoint")) {
|
||||
const target = endpoint.children.find((n) => n.kind === "targetConstraint");
|
||||
if (
|
||||
!target ||
|
||||
!syntax.tokens.some((t) => t.start >= target.start && t.end <= target.end && t.kind === "INTERFACE") ||
|
||||
!target.children.some((n) => n.kind === "identifier" && text(n) === "WebStudioPlaceable")
|
||||
)
|
||||
continue;
|
||||
const edgeName = text(edge.children.find((n) => n.kind === "identifier")!);
|
||||
const projection = text(endpoint.children.find((n) => n.kind === "identifier")!);
|
||||
matches.push(`bind placements.resolve to edge ${edgeName}.${projection}.resolve;`);
|
||||
}
|
||||
}
|
||||
if (matches.length !== 1)
|
||||
throw new Error(
|
||||
`Expected one canvas placement edge for WebStudioPlaceable; found ${matches.length}. Configure the workspace canvas before adding a bundle.`,
|
||||
);
|
||||
process.stdout.write(JSON.stringify({ binding: matches[0] }) + "\n");
|
||||
return;
|
||||
}
|
||||
if (command === "package-identity") {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx package-identity PACKAGE_QX");
|
||||
const authored = await readFile(args[0], "utf8");
|
||||
const syntax = parseQx(authored);
|
||||
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "packageResourceDecl");
|
||||
if (syntax.diagnostics.length || declarations.length !== 1)
|
||||
throw new Error("Expected one valid package declaration");
|
||||
const literals = declarations[0].children.filter((node) => node.kind === "stringLiteral");
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
id: JSON.parse(authored.slice(literals[0].start, literals[0].end)),
|
||||
revision: JSON.parse(authored.slice(literals[1].start, literals[1].end)),
|
||||
}) + "\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "package-descriptor") {
|
||||
if (args.length !== 2) throw new Error("usage: quixos-qx package-descriptor SCHEMA REVISION_ID");
|
||||
process.stdout.write(generatePackageDescriptor(JSON.parse(await readFile(args[0], "utf8")), args[1]));
|
||||
return;
|
||||
}
|
||||
if (command === "migration-seal") {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx migration-seal PACKAGE_DIRECTORY");
|
||||
process.stdout.write(JSON.stringify(await sealMigrations(args[0])) + "\n");
|
||||
return;
|
||||
}
|
||||
if (command === "bundle-policy") {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx bundle-policy SOURCE_DIRECTORY");
|
||||
await checkBundleSources(args[0]);
|
||||
return;
|
||||
}
|
||||
if (command === "worklist" && !args.includes("--help")) {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
|
||||
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (["author-check", "author-contract"].includes(command) && !args.includes("--help")) {
|
||||
const [root, output, ...flags] = args;
|
||||
if (!root || !output)
|
||||
throw new Error("usage: quixos-qx author-check ROOT OUTPUT [--baseline FILE] [--reviews FILE]");
|
||||
const options: { baseline?: string; reviews?: string } = {};
|
||||
for (let index = 0; index < flags.length; index += 2) {
|
||||
if (!flags[index + 1]) throw new Error("Missing check option value");
|
||||
if (flags[index] === "--baseline") options.baseline = flags[index + 1];
|
||||
else if (flags[index] === "--reviews") options.reviews = flags[index + 1];
|
||||
else throw new Error(`Unknown check option ${flags[index]}`);
|
||||
}
|
||||
const result = await checkAuthoring(root, output, { ...options, contractOnly: command === "author-contract" });
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (result.blockers.length) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (["converge", "_converge"].includes(command) && !args.includes("--help")) {
|
||||
if (!args.length || args.length > 2)
|
||||
throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
|
||||
const context = await authoringContext(args[0]);
|
||||
if (command === "converge") {
|
||||
console.error(`[${new Date().toISOString()}] Capture: waiting for coordinator lock (120s limit)`);
|
||||
const result = spawnSync(
|
||||
"flock",
|
||||
[
|
||||
"--exclusive",
|
||||
"--timeout",
|
||||
"120",
|
||||
"--conflict-exit-code",
|
||||
"75",
|
||||
path.join(context.workbench, ".quixos/converge.lock"),
|
||||
process.execPath,
|
||||
process.argv[1],
|
||||
"_converge",
|
||||
context.workbench,
|
||||
...(args[1] ? [args[1]] : []),
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.status === 75)
|
||||
process.stderr.write(
|
||||
"Timed out after 120 seconds waiting for source capture; inspect the active coordinator. No build lock is held.\n",
|
||||
);
|
||||
process.exitCode = result.status ?? 1;
|
||||
return;
|
||||
}
|
||||
console.error(`[${new Date().toISOString()}] Capture: coordinator lock acquired`);
|
||||
const result = await convergeAuthoring(context.workbench, args[1]);
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (!result.converged) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (command === "check-committed" && !args.includes("--help")) {
|
||||
const [kind, repository, commit, log, ...extra] = args;
|
||||
if (!["workspace", "interface", "package"].includes(kind) || !log || extra.length)
|
||||
throw new Error("usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE");
|
||||
process.stdout.write(
|
||||
`${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package", log)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!command || command === "--help" || args.includes("--help")) {
|
||||
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" +
|
||||
"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",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "inspect" || command === "resources") {
|
||||
if (!args[0] || args.length > (command === "inspect" ? 2 : 1))
|
||||
throw new Error(`usage: quixos-qx ${command} WORKBENCH${command === "inspect" ? " [RESOURCE]" : ""}`);
|
||||
const result = command === "inspect" ? await inspectWorkbench(args[0], args[1]) : await authoringContext(args[0]);
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "source-baseline") {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx source-baseline WORKBENCH");
|
||||
process.stdout.write(`${JSON.stringify(await (await authoringContext(args[0])).baseline())}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-dependency") {
|
||||
const [root, kind, name, ...remaining] = args;
|
||||
let repository: string | undefined, commit: string | undefined;
|
||||
const flags = [...remaining];
|
||||
if (flags.length && !flags[0].startsWith("--")) {
|
||||
repository = flags.shift();
|
||||
commit = flags.shift();
|
||||
} else if (root && ["interface", "package"].includes(kind) && name) {
|
||||
const context = await authoringContext(root);
|
||||
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
|
||||
const matches = context.resources.filter(
|
||||
(entry) =>
|
||||
entry.kind === kind &&
|
||||
(entry.resourceId === name ||
|
||||
path.basename(entry.directory) === name ||
|
||||
path.join(context.workbench, entry.directory) === alias),
|
||||
);
|
||||
if (matches.length !== 1 || !matches[0].source)
|
||||
throw new Error(`Select exactly one registered ${kind} with qx-workspace resources; no match for ${name}`);
|
||||
const selected = matches[0];
|
||||
let source = selected.source!;
|
||||
if (flags.includes("--write")) {
|
||||
const retained = spawnSync("quixos-qx", ["converge", context.workbench, selected.directory], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" },
|
||||
});
|
||||
if (retained.error || retained.status !== 0)
|
||||
throw new Error(
|
||||
`Dependency source needs attention: ${retained.error?.message ?? retained.stdout ?? retained.stderr}`,
|
||||
);
|
||||
source = JSON.parse(retained.stdout).candidate;
|
||||
}
|
||||
repository = source.repository;
|
||||
commit = source.commit;
|
||||
}
|
||||
if (
|
||||
!root ||
|
||||
!["interface", "package"].includes(kind) ||
|
||||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") ||
|
||||
!repository ||
|
||||
!commit ||
|
||||
flags.some((flag) => flag !== "--write")
|
||||
)
|
||||
throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]");
|
||||
const resourceKind = kind as "package" | "interface";
|
||||
let entrypoint: "workspace" | "package" | "interface" | undefined;
|
||||
for (const candidate of ["workspace", "package", "interface"] as const) {
|
||||
try {
|
||||
await readFile(path.join(root, `${candidate}.qx`));
|
||||
if (entrypoint) throw new Error("Ambiguous repository entrypoint");
|
||||
entrypoint = candidate;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
if (!entrypoint) throw new Error("No QX repository entrypoint");
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error("Invalid resource lock");
|
||||
let target = "quixos.lock";
|
||||
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
|
||||
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
|
||||
if (parsed.ok && parsed.document.resources.some((entry) => entry.kind === kind && entry.binding === name))
|
||||
target = file;
|
||||
}
|
||||
const request: StructuralRequest = {
|
||||
kind: entrypoint,
|
||||
source: await authorSource(root),
|
||||
validation: "syntax",
|
||||
files: [
|
||||
{ file: `${entrypoint}.qx`, edits: [{ operation: "import", kind: resourceKind, name }] },
|
||||
{
|
||||
file: target,
|
||||
edits: [{ operation: "dependency", kind: resourceKind, name, source: { repository, commit } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined }, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-interface") {
|
||||
const [root, specFile, ...flags] = args;
|
||||
if (!root || !specFile || flags.some((flag) => flag !== "--write"))
|
||||
throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]");
|
||||
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)
|
||||
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||
const request: StructuralRequest = {
|
||||
kind: "interface",
|
||||
// Like package scaffolding, declaration creation is provisional. Imports
|
||||
// can be attached next; the normal check verifies the complete graph.
|
||||
validation: "syntax",
|
||||
source: spec.source,
|
||||
files: [
|
||||
{
|
||||
file: "interface.qx",
|
||||
create:
|
||||
spec.declaration ??
|
||||
`interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`,
|
||||
},
|
||||
{
|
||||
file: "quixos.lock",
|
||||
create: formatQuixosLock({
|
||||
formatVersion: 1,
|
||||
quixos: { resolver: "git", ...spec.tools.quixos },
|
||||
resources: [],
|
||||
}),
|
||||
},
|
||||
{ file: ".gitignore", create: ".quixos/\n" },
|
||||
],
|
||||
};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined }, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "build-package") {
|
||||
if (args.length !== 3)
|
||||
throw new Error("usage: quixos-qx build-package COMMITTED_SOURCE SCHEMA PACKAGE_REVISION_ID");
|
||||
process.stdout.write(`${await buildCheckedPackage(args[0], args[1], args[2])}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "source-digest") {
|
||||
if (!args[0] || args.length !== 1) throw new Error("usage: quixos-qx source-digest ROOT");
|
||||
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-source-digest-"));
|
||||
try {
|
||||
process.stdout.write(`${(await snapshotRepository(args[0], temporary)).treeDigest}\n`);
|
||||
} finally {
|
||||
await rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (command === "pin-upgrade") {
|
||||
const [workbench, specFile, ...flags] = args;
|
||||
if (!workbench || !specFile)
|
||||
throw new Error("usage: quixos-qx pin-upgrade WORKBENCH SPEC_JSON [--publish] [--resume UUID]");
|
||||
let publish = false,
|
||||
acceptEdits = false,
|
||||
resume: string | undefined;
|
||||
for (let index = 0; index < flags.length; index++) {
|
||||
if (flags[index] === "--publish") publish = true;
|
||||
else if (flags[index] === "--accept-edits") acceptEdits = true;
|
||||
else if (flags[index] === "--resume" && /^[a-f0-9-]{36}$/.test(flags[index + 1] ?? "")) resume = flags[++index];
|
||||
else throw new Error(`Unknown pin-upgrade option ${flags[index]}`);
|
||||
}
|
||||
if (acceptEdits && !resume) throw new Error("--accept-edits requires an existing refactor journal (--resume)");
|
||||
const plan = resume
|
||||
? JSON.parse(await readFile(path.join(workbench, ".quixos/upgrades", `${resume}.json`), "utf8")).plan
|
||||
: await planPinUpgrades(
|
||||
workbench,
|
||||
specFile === "auto"
|
||||
? await discoverUpgradeSpec(workbench)
|
||||
: (JSON.parse(await readFile(specFile, "utf8")) as UpgradeSpec),
|
||||
);
|
||||
if (resume && path.resolve(workbench) !== plan.workbench)
|
||||
throw new Error("Upgrade journal belongs to another workbench");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, { acceptEdits }) : plan, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-refresh")
|
||||
throw new Error(
|
||||
"Refresh is no longer required: edit declarations and typed implementation wiring, then run qx-workspace check. Dependency installation uses scaffold install.",
|
||||
);
|
||||
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-install"].includes(command)) {
|
||||
const [root, specFile, ...flags] = args;
|
||||
let spec: ScaffoldRecipe;
|
||||
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
|
||||
const authored = parseQx(await readFile(path.join(root, "package.qx"), "utf8"));
|
||||
const declarationNode = [...walkSyntax(authored.root)].find((node) => node.kind === "packageResourceDecl");
|
||||
const nameNode = declarationNode?.children.find((node) => node.kind === "identifier");
|
||||
if (!nameNode) throw new Error("Expected a package declaration");
|
||||
const name = authored.source.slice(nameNode.start, nameNode.end);
|
||||
spec = { source: await authorSource(root), name: specFile, id: `export:${name}:${specFile}` };
|
||||
const declaration = flags.indexOf("--declaration");
|
||||
if (declaration >= 0) {
|
||||
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
|
||||
spec.declaration = await readFile(flags[declaration + 1], "utf8");
|
||||
const parsed = parseQx(`package Draft id "package:draft" revision "package:draft@1" { ${spec.declaration} }`);
|
||||
if (parsed.diagnostics.length) throw new Error(parsed.diagnostics.map((d) => d.message).join("\n"));
|
||||
const exported = [...walkSyntax(parsed.root)].filter((node) =>
|
||||
["packageOperationExport", "packageFunctionExport", "packageConstructorExport"].includes(node.kind),
|
||||
);
|
||||
if (exported.length !== 1)
|
||||
throw new Error("--declaration must contain exactly one function, operation or constructor export");
|
||||
const literal = exported[0].children.find((node) => node.kind === "stringLiteral");
|
||||
if (!literal) throw new Error("Declaration requires an authored export ID");
|
||||
spec.id = JSON.parse(parsed.source.slice(literal.start, literal.end));
|
||||
flags.splice(declaration, 2);
|
||||
}
|
||||
} else spec = (await readSpec(specFile)) as ScaffoldRecipe;
|
||||
if (
|
||||
!root ||
|
||||
!specFile ||
|
||||
flags.some((flag) => !["--write", "--install"].includes(flag)) ||
|
||||
(flags.includes("--install") && !flags.includes("--write"))
|
||||
)
|
||||
throw new Error(
|
||||
"usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]",
|
||||
);
|
||||
const plan =
|
||||
command === "scaffold-install"
|
||||
? undefined
|
||||
: await planStructure(
|
||||
root,
|
||||
await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration", spec),
|
||||
process.env.QUIXOS_SNAPSHOT_MAP,
|
||||
);
|
||||
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(
|
||||
`${JSON.stringify({ ...(plan ? planSummary(plan) : { installed: true }), applied }, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-structure") {
|
||||
const [root, spec, ...flags] = args;
|
||||
if (!root || !spec || flags.some((flag) => flag !== "--write"))
|
||||
throw new Error("usage: quixos-qx scaffold-structure ROOT SPEC_JSON [--write]");
|
||||
const request = (await readSpec(spec)) as StructuralRequest;
|
||||
if (request.kind !== "workspace" && !request.source) request.source = await authorSource(root);
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
|
||||
process.stdout.write(`${JSON.stringify({ ...planSummary(plan), applied }, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-resume") {
|
||||
const [root, id, ...extra] = args;
|
||||
if (!root || !id || extra.length) throw new Error("usage: quixos-qx scaffold-resume ROOT JOURNAL_ID");
|
||||
process.stdout.write(`${JSON.stringify(await resumeStructure(root, id), null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (["check", "check-resource"].includes(command))
|
||||
throw new Error(
|
||||
"Use qx-workspace check in the registered repository; handwritten source/snapshot-map candidates are no longer an authoring check path",
|
||||
);
|
||||
if (command === "evolution") {
|
||||
const [baseline, candidate, reviews, ...extra] = args;
|
||||
if (!baseline || !candidate || extra.length)
|
||||
throw new Error("usage: quixos-qx evolution BASELINE_JSON CANDIDATE_JSON [REVIEWS_JSON]");
|
||||
const before = baseline === "none" ? null : JSON.parse(await readFile(baseline, "utf8"));
|
||||
const after = JSON.parse(await readFile(candidate, "utf8"));
|
||||
const decisions = reviews ? JSON.parse(await readFile(reviews, "utf8")) : [];
|
||||
if (!Array.isArray(decisions)) throw new Error("Reviews must be an array");
|
||||
process.stdout.write(`${JSON.stringify(planEvolution(before, after, { reviews: decisions }), null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-atom") {
|
||||
const [root, name, id, ...flags] = args;
|
||||
if (!root || !name || !id || flags.some((flag) => flag !== "--write"))
|
||||
throw new Error("usage: quixos-qx scaffold-atom ROOT NAME ID [--write]");
|
||||
const resolveResource = await createGitCapabilityResolver({ checkoutRoot: `${root}/.quixos/resource-checkouts` });
|
||||
const plan = await scaffoldAtom({ root, name, id, write: flags.includes("--write"), resolveResource });
|
||||
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
const [file, flag, ...rest] = args;
|
||||
if (
|
||||
!file ||
|
||||
rest.length ||
|
||||
(flag && flag !== "--write") ||
|
||||
!["parse", "lint", "format"].includes(command ?? "") ||
|
||||
(flag && command !== "format")
|
||||
)
|
||||
throw new Error("usage: quixos-qx parse|lint|format FILE [--write (format only)]");
|
||||
const source = await readFile(file, "utf8");
|
||||
if (command === "format") {
|
||||
const formatted = formatQx(source);
|
||||
if (flag) await writeFile(file, formatted);
|
||||
else process.stdout.write(formatted);
|
||||
} else {
|
||||
const result = command === "parse" ? parseQx(source, file) : lintQx(source, file);
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (Array.isArray(result) ? result.some((entry) => entry.severity === "error") : result.diagnostics.length)
|
||||
process.exitCode = 1;
|
||||
}
|
||||
};
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import process from "node:process";
|
||||
import { compileWorkspaceRepository } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
|
||||
import {
|
||||
planEvolution,
|
||||
runtimeContracts,
|
||||
type EvolutionReview,
|
||||
type WorkspaceRevision,
|
||||
} from "../capability-model/index.js";
|
||||
|
||||
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
|
||||
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
|
||||
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
|
||||
[--reviews REVIEW_JSON] [--schemas-out PATH]
|
||||
|
||||
Resolves a workspace's recursive resource-lock graph, clones every exact
|
||||
resource revision, validates standalone interface/package manifests, and emits
|
||||
the checked workspace plan as JSON.`;
|
||||
|
||||
const parseArgs = (args: string[]) => {
|
||||
const values = new Map<string, string>();
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const key = args[index];
|
||||
const value = args[index + 1];
|
||||
if (!key?.startsWith("--") || !value) throw new Error(usage);
|
||||
values.set(key, value);
|
||||
}
|
||||
const rootDirectory = values.get("--root");
|
||||
const checkoutRoot = values.get("--checkout-root");
|
||||
if (!rootDirectory || !checkoutRoot) throw new Error(usage);
|
||||
return {
|
||||
rootDirectory,
|
||||
checkoutRoot,
|
||||
graphOut: values.get("--graph-out"),
|
||||
schemasOut: values.get("--schemas-out"),
|
||||
snapshotMap: values.get("--snapshot-map"),
|
||||
workspaceId: values.get("--workspace-id"),
|
||||
workspaceRevisionId: values.get("--workspace-revision-id"),
|
||||
sourceRootCommit: values.get("--source-root-commit"),
|
||||
baseline: values.get("--baseline"),
|
||||
evolutionOut: values.get("--evolution-out"),
|
||||
reviews: values.get("--reviews"),
|
||||
};
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
process.stdout.write(`${usage}\n`);
|
||||
return;
|
||||
}
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const resolveResource = await createGitCapabilityResolver({
|
||||
checkoutRoot: options.checkoutRoot,
|
||||
snapshotMap: options.snapshotMap,
|
||||
});
|
||||
const assembled = await compileWorkspaceRepository({
|
||||
rootDirectory: options.rootDirectory,
|
||||
workspaceId: options.workspaceId,
|
||||
workspaceRevisionId: options.workspaceRevisionId,
|
||||
sourceRootCommit: options.sourceRootCommit,
|
||||
resolveResource,
|
||||
});
|
||||
|
||||
if (options.graphOut) {
|
||||
await writeFile(
|
||||
options.graphOut,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
formatVersion: 1,
|
||||
quixos: assembled.lock.quixos,
|
||||
directResources: [...assembled.directResources.entries()].map(([bindingKey, node]) => {
|
||||
const [kind, binding] = bindingKey.split("\0");
|
||||
return { kind, binding, resourceKey: node.key, directory: node.directory };
|
||||
}),
|
||||
resources: assembled.resources.map((node) => ({
|
||||
key: node.key,
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
|
||||
if (options.schemasOut) {
|
||||
const schemas: Record<string, unknown> = {};
|
||||
for (const node of assembled.resources.filter((entry) => entry.kind === "package")) {
|
||||
const closure = new Map<string, typeof node>();
|
||||
const visit = (entry: typeof node) => {
|
||||
if (closure.has(entry.key)) return;
|
||||
closure.set(entry.key, entry);
|
||||
entry.dependencies.forEach(visit);
|
||||
};
|
||||
visit(node);
|
||||
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
|
||||
bindingSchema({ resources: [...closure.values()] }),
|
||||
candidate,
|
||||
node.resource.revision.revisionId,
|
||||
);
|
||||
}
|
||||
await writeFile(options.schemasOut, JSON.stringify(schemas));
|
||||
}
|
||||
if (options.evolutionOut) {
|
||||
const baseline = options.baseline
|
||||
? (JSON.parse(await readFile(options.baseline, "utf8")) as WorkspaceRevision)
|
||||
: null;
|
||||
const reviews = options.reviews ? (JSON.parse(await readFile(options.reviews, "utf8")) as EvolutionReview[]) : [];
|
||||
if (!Array.isArray(reviews)) throw new Error("Review file must contain an array");
|
||||
await writeFile(
|
||||
options.evolutionOut,
|
||||
`${JSON.stringify(planEvolution(baseline, candidate, { reviews }), null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(candidate, null, 2)}\n`);
|
||||
};
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,414 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Binding, Conformance, DependencyBinding, PersistentAttachment, WorkspaceRevision } from "./types.js";
|
||||
import { validateWorkspaceRevision } from "./validation.js";
|
||||
|
||||
/** Content hashing is independent of JSON object insertion order, not array order. */
|
||||
const compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
export const canonicalJson = (value: unknown): string => {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
||||
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
||||
throw new Error("Expected a plain JSON object");
|
||||
return `{${Object.entries(value)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([a], [b]) => compareText(a, b))
|
||||
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
throw new Error(`Cannot hash non-JSON value: ${typeof value}`);
|
||||
};
|
||||
export const contentDigest = (value: unknown) =>
|
||||
`sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
|
||||
const semantic = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) return value.map(semantic);
|
||||
if (value && typeof value === "object")
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
|
||||
// These are authored data/maps, not schema nodes. A user field literally
|
||||
// named displayName or documentation is semantic and must stay in the hash.
|
||||
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]),
|
||||
);
|
||||
return value;
|
||||
};
|
||||
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) =>
|
||||
[...entries].sort((a, b) => compareText(key(a), key(b)));
|
||||
export const conformanceIdentity = (entry: Conformance): string =>
|
||||
entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
|
||||
|
||||
export type StorageContract = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
kind: "state" | "edge";
|
||||
digest: string;
|
||||
definition: unknown;
|
||||
};
|
||||
/** Automatic evolution preserves values; it never interprets migration code or
|
||||
* guesses that a new nominal message descriptor means the same representation. */
|
||||
export const storageChangeRequiresMigration = (
|
||||
previous: StorageContract | undefined,
|
||||
next: StorageContract | undefined,
|
||||
oldAtomIds: ReadonlySet<string>,
|
||||
): boolean => {
|
||||
if (!next) return true;
|
||||
const after = next.definition as PersistentAttachment;
|
||||
if (!previous) {
|
||||
if (after.kind === "state")
|
||||
return (
|
||||
oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional"
|
||||
);
|
||||
return after.endpoints.some(
|
||||
(endpoint) =>
|
||||
endpoint.cardinality === "exactly-one" &&
|
||||
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)),
|
||||
);
|
||||
}
|
||||
if (previous.ownerId !== next.ownerId || previous.kind !== next.kind) return true;
|
||||
const before = previous.definition as PersistentAttachment;
|
||||
if (before.kind === "state" && after.kind === "state") {
|
||||
// Capture materializes old defaults, so changing a default affects only
|
||||
// newly constructed objects, not existing sparse state.
|
||||
const { defaultValue: _beforeDefault, ...beforeStorage } = before;
|
||||
const { defaultValue: _afterDefault, ...afterStorage } = after;
|
||||
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
|
||||
}
|
||||
return canonicalJson(before) !== canonicalJson(after);
|
||||
};
|
||||
export const storageContracts = (workspace: WorkspaceRevision): StorageContract[] => {
|
||||
const result: StorageContract[] = [];
|
||||
const add = (attachment: PersistentAttachment, ownerId: string) => {
|
||||
const definition = semantic(attachment);
|
||||
result.push({
|
||||
id: attachment.id,
|
||||
ownerId,
|
||||
kind: attachment.kind,
|
||||
definition,
|
||||
digest: contentDigest({ ownerId, definition }),
|
||||
});
|
||||
};
|
||||
for (const attachment of workspace.sharedAttachments) add(attachment, "legacy:workspace");
|
||||
for (const conformance of workspace.conformances)
|
||||
for (const attachment of conformance.privateAttachments) add(attachment, conformanceIdentity(conformance));
|
||||
return sorted(result, (entry) => entry.id);
|
||||
};
|
||||
|
||||
type GraphNode = { value: unknown; dependencies: Set<string>; reviewProviders: Set<string> };
|
||||
export type RuntimeContract = {
|
||||
groupId: string;
|
||||
packageId: string;
|
||||
packageRevisionId: string;
|
||||
digest: string;
|
||||
reviewProviders: string[];
|
||||
dependencies: Array<{ id: string; digest: string }>;
|
||||
};
|
||||
|
||||
/** Build only outbound execution dependencies. Incoming callers never retain or invalidate a provider. */
|
||||
export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[] => {
|
||||
const nodes = new Map<string, GraphNode>();
|
||||
const node = (key: string, value: unknown) => {
|
||||
const result = { value: semantic(value), dependencies: new Set<string>(), reviewProviders: new Set<string>() };
|
||||
nodes.set(key, result);
|
||||
return result;
|
||||
};
|
||||
const conformanceKey = (atom: string, iface: string) => `conformance:${atom}:${iface}`;
|
||||
for (const storage of storageContracts(workspace)) node(`attachment:${storage.id}`, storage);
|
||||
for (const iface of workspace.interfaceImports)
|
||||
node(`interface:${iface.revisionId}`, {
|
||||
...iface,
|
||||
members: sorted(iface.members, (entry) => entry.id).map((entry) => ({
|
||||
...entry,
|
||||
operations: sorted(entry.operations, (operation) => operation.id),
|
||||
})),
|
||||
});
|
||||
for (const pkg of workspace.packageImports)
|
||||
node(`package:${pkg.revisionId}`, {
|
||||
...pkg,
|
||||
semanticMajor: pkg.semanticMajor ?? 1,
|
||||
exports: sorted(pkg.exports, (entry) => entry.id).map((entry) => ({
|
||||
...entry,
|
||||
dependencyPorts: sorted(entry.dependencyPorts, (port) => port.id),
|
||||
})),
|
||||
});
|
||||
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
|
||||
if (binding.kind === "query") {
|
||||
const pkg = workspace.packageImports.find((pkg) => pkg.revisionId === binding.packageRevisionId);
|
||||
const query = pkg?.checkedQueries?.find((query) => query.declaration.id === binding.queryId);
|
||||
if (!query) throw new Error(`Missing checked query contract ${binding.packageRevisionId}:${binding.queryId}`);
|
||||
const key = `query:${binding.packageRevisionId}:${binding.queryId}`;
|
||||
const queryNode = nodes.get(key) ?? node(key, query);
|
||||
parent.dependencies.add(key);
|
||||
for (const effect of query.effects) {
|
||||
queryNode.dependencies.add(`interface:${effect.interfaceRevisionId}`);
|
||||
for (const conformance of workspace.conformances.filter(
|
||||
(entry) => entry.interfaceRevisionId === effect.interfaceRevisionId,
|
||||
))
|
||||
queryNode.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
|
||||
}
|
||||
}
|
||||
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
|
||||
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
|
||||
if (binding.kind === "constructor") {
|
||||
parent.dependencies.add(`constructor:${binding.atomId}`);
|
||||
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
|
||||
const pkg = workspace.packageImports.find((entry) => entry.revisionId === ctor?.packageRevisionId);
|
||||
if (pkg) reviews?.add(pkg.packageId);
|
||||
}
|
||||
if (binding.kind !== "constructor" && binding.via) parent.dependencies.add(`attachment:${binding.via.edgeTypeId}`);
|
||||
if (binding.kind === "interface") {
|
||||
parent.dependencies.add(`interface:${binding.interfaceRevisionId}`);
|
||||
// An edge traversal may select any matching target. Conservatively include every possible witness.
|
||||
for (const conformance of workspace.conformances) {
|
||||
if (
|
||||
conformance.interfaceRevisionId === binding.interfaceRevisionId &&
|
||||
(binding.via || conformance.atomId === atomId)
|
||||
) {
|
||||
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
|
||||
reviews?.add(conformanceIdentity(conformance));
|
||||
for (const operation of conformance.operationBindings) {
|
||||
if (operation.binding.kind !== "package") continue;
|
||||
const revisionId = operation.binding.packageRevisionId;
|
||||
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
|
||||
if (pkg) reviews?.add(pkg.packageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
|
||||
if (value.kind !== "package") {
|
||||
dependency(parent, value, atomId);
|
||||
return;
|
||||
}
|
||||
const packageNode = nodes.get(`package:${value.packageRevisionId}`)!;
|
||||
parent.dependencies.add(`package:${value.packageRevisionId}`);
|
||||
const normalized = { ...value, dependencies: sorted(value.dependencies, (entry) => entry.portId) };
|
||||
const key = `binding:${contentDigest({ atomId, context, value: semantic(normalized) })}`;
|
||||
const bound = node(key, { atomId, context, binding: normalized });
|
||||
packageNode.dependencies.add(key);
|
||||
for (const port of value.dependencies) dependency(bound, port.binding, atomId, packageNode.reviewProviders);
|
||||
};
|
||||
for (const conformance of workspace.conformances) {
|
||||
const parent = node(conformanceKey(conformance.atomId, conformance.interfaceRevisionId), {
|
||||
id: conformanceIdentity(conformance),
|
||||
semanticMajor: conformance.semanticMajor ?? 1,
|
||||
atomId: conformance.atomId,
|
||||
interfaceRevisionId: conformance.interfaceRevisionId,
|
||||
operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
|
||||
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
|
||||
});
|
||||
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
|
||||
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
|
||||
for (const required of contract?.requiredInterfaces ?? [])
|
||||
parent.dependencies.add(conformanceKey(conformance.atomId, required));
|
||||
for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`);
|
||||
for (const operation of conformance.operationBindings)
|
||||
binding(parent, operation.binding, conformance.atomId, {
|
||||
conformanceId: conformanceIdentity(conformance),
|
||||
semanticMajor: conformance.semanticMajor ?? 1,
|
||||
operationId: operation.operationId,
|
||||
});
|
||||
for (const materialization of conformance.relationshipMaterializations) {
|
||||
parent.dependencies.add(`constructor:${materialization.constructorAtomId}`);
|
||||
parent.dependencies.add(`attachment:${materialization.edgeTypeId}`);
|
||||
}
|
||||
}
|
||||
for (const constructor of workspace.constructors) {
|
||||
const parent = node(`constructor:${constructor.atomId}`, constructor);
|
||||
binding(parent, { kind: "package", ...constructor }, constructor.atomId, { constructor: constructor.atomId });
|
||||
}
|
||||
const counts = new Map<string, number>();
|
||||
for (const pkg of workspace.packageImports) counts.set(pkg.packageId, (counts.get(pkg.packageId) ?? 0) + 1);
|
||||
return sorted(
|
||||
workspace.packageImports.map((pkg): RuntimeContract => {
|
||||
const visited = new Set<string>();
|
||||
const walk = (key: string) => {
|
||||
if (visited.has(key)) return;
|
||||
const entry = nodes.get(key);
|
||||
if (!entry) throw new Error(`Unresolved execution dependency ${key}`);
|
||||
visited.add(key);
|
||||
for (const target of entry.dependencies) walk(target);
|
||||
};
|
||||
walk(`package:${pkg.revisionId}`);
|
||||
const dependencies = [...visited].sort().map((id) => ({ id, digest: contentDigest(nodes.get(id)!.value) }));
|
||||
return {
|
||||
groupId: counts.get(pkg.packageId) === 1 ? pkg.packageId : `${pkg.packageId}#${pkg.revisionId}`,
|
||||
packageId: pkg.packageId,
|
||||
packageRevisionId: pkg.revisionId,
|
||||
digest: contentDigest(dependencies),
|
||||
reviewProviders: [...nodes.get(`package:${pkg.revisionId}`)!.reviewProviders].sort(),
|
||||
dependencies,
|
||||
};
|
||||
}),
|
||||
(entry) => entry.groupId,
|
||||
);
|
||||
};
|
||||
|
||||
export type EvolutionReview = {
|
||||
requirementDigest: string;
|
||||
decision: "changed" | "accepted-unchanged";
|
||||
rationale: string;
|
||||
agentId: string;
|
||||
};
|
||||
export type ReviewRequirement = {
|
||||
consumerId: string;
|
||||
providerId: string;
|
||||
oldMajor: number;
|
||||
newMajor: number;
|
||||
requirementDigest: string;
|
||||
};
|
||||
export type RuntimeAction = {
|
||||
groupId: string;
|
||||
action: "keep" | "start" | "replace" | "retire";
|
||||
previous?: RuntimeContract;
|
||||
candidate?: RuntimeContract;
|
||||
reasons: string[];
|
||||
};
|
||||
export type EvolutionReport = {
|
||||
schemaVersion: 1;
|
||||
baselineDigest: string | null;
|
||||
candidateDigest: string;
|
||||
checkerVersion: string;
|
||||
runtimeActions: RuntimeAction[];
|
||||
storageChanges: Array<{
|
||||
id: string;
|
||||
kind: "add" | "remove" | "change";
|
||||
requiresMigration: boolean;
|
||||
previous?: StorageContract;
|
||||
candidate?: StorageContract;
|
||||
}>;
|
||||
migrationRequired: string[];
|
||||
reviews: Array<ReviewRequirement & { accepted: boolean }>;
|
||||
packageChecks: Array<{ groupId: string; contractDigest: string }>;
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
export const planEvolution = (
|
||||
baseline: WorkspaceRevision | null,
|
||||
candidate: WorkspaceRevision,
|
||||
options: { reviews?: EvolutionReview[]; allowLegacy?: boolean } = {},
|
||||
): EvolutionReport => {
|
||||
const issues = validateWorkspaceRevision(candidate);
|
||||
if (issues.length)
|
||||
throw new Error(
|
||||
`Invalid candidate workspace:\n${issues.map((entry) => `${entry.path}: ${entry.message}`).join("\n")}`,
|
||||
);
|
||||
if (baseline && baseline.workspaceId !== candidate.workspaceId)
|
||||
throw new Error("Cannot evolve a different workspace");
|
||||
const candidateDigest = contentDigest(candidate);
|
||||
const checkerVersion = "quixos-evolution-v1";
|
||||
const blockers: string[] = [];
|
||||
if (!options.allowLegacy) {
|
||||
if (candidate.sharedAttachments.length)
|
||||
blockers.push("Assign legacy workspace-shared attachments to explicit conformance owners");
|
||||
for (const entry of candidate.conformances)
|
||||
if (!entry.id)
|
||||
blockers.push(`Conformance ${entry.atomId} as ${entry.interfaceRevisionId} requires an authored ID`);
|
||||
}
|
||||
const previousRuntimes = new Map((baseline ? runtimeContracts(baseline) : []).map((entry) => [entry.groupId, entry]));
|
||||
const nextRuntimes = new Map(runtimeContracts(candidate).map((entry) => [entry.groupId, entry]));
|
||||
const runtimeActions: RuntimeAction[] = [...new Set([...previousRuntimes.keys(), ...nextRuntimes.keys()])]
|
||||
.sort()
|
||||
.map((groupId) => {
|
||||
const previous = previousRuntimes.get(groupId),
|
||||
next = nextRuntimes.get(groupId);
|
||||
const before = new Map(previous?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const after = new Map(next?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const reasons = [...new Set([...before.keys(), ...after.keys()])]
|
||||
.sort()
|
||||
.filter((id) => before.get(id) !== after.get(id));
|
||||
return {
|
||||
groupId,
|
||||
action: !previous ? "start" : !next ? "retire" : previous.digest === next.digest ? "keep" : "replace",
|
||||
...(previous ? { previous } : {}),
|
||||
...(next ? { candidate: next } : {}),
|
||||
reasons,
|
||||
};
|
||||
});
|
||||
const beforeStorage = new Map((baseline ? storageContracts(baseline) : []).map((entry) => [entry.id, entry]));
|
||||
const afterStorage = new Map(storageContracts(candidate).map((entry) => [entry.id, entry]));
|
||||
const storageChanges: EvolutionReport["storageChanges"] = [];
|
||||
for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) {
|
||||
const previous = beforeStorage.get(id),
|
||||
next = afterStorage.get(id);
|
||||
if (previous?.digest !== next?.digest)
|
||||
storageChanges.push({
|
||||
id,
|
||||
kind: !previous ? "add" : !next ? "remove" : "change",
|
||||
requiresMigration: storageChangeRequiresMigration(
|
||||
previous,
|
||||
next,
|
||||
new Set(baseline?.atoms.map((atom) => atom.id) ?? []),
|
||||
),
|
||||
...(previous ? { previous } : {}),
|
||||
...(next ? { candidate: next } : {}),
|
||||
});
|
||||
}
|
||||
const providers = (workspace: WorkspaceRevision) => [
|
||||
...workspace.packageImports.map((entry) => ({
|
||||
id: entry.packageId as string,
|
||||
revision: entry.revisionId as string,
|
||||
major: entry.semanticMajor ?? 1,
|
||||
node: `package:${entry.revisionId}`,
|
||||
digest: contentDigest(entry),
|
||||
})),
|
||||
...workspace.conformances.map((entry) => ({
|
||||
id: conformanceIdentity(entry),
|
||||
revision: contentDigest(entry),
|
||||
major: entry.semanticMajor ?? 1,
|
||||
node: `conformance:${entry.atomId}:${entry.interfaceRevisionId}`,
|
||||
digest: contentDigest(entry),
|
||||
})),
|
||||
];
|
||||
const oldProviders = baseline ? providers(baseline) : [];
|
||||
const reviews: EvolutionReport["reviews"] = [];
|
||||
for (const provider of providers(candidate)) {
|
||||
const old = oldProviders.filter((entry) => entry.id === provider.id);
|
||||
if (old.length > 1) {
|
||||
blockers.push(`Ambiguous semantic-major lineage for ${provider.id}`);
|
||||
continue;
|
||||
}
|
||||
if (!old[0] || old[0].major === provider.major) continue;
|
||||
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
|
||||
for (const consumer of nextRuntimes.values()) {
|
||||
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
|
||||
const requirement = {
|
||||
consumerId: consumer.groupId,
|
||||
providerId: provider.id,
|
||||
oldMajor: old[0].major,
|
||||
newMajor: provider.major,
|
||||
};
|
||||
const requirementDigest = contentDigest({
|
||||
...requirement,
|
||||
oldProvider: old[0].digest,
|
||||
newProvider: provider.digest,
|
||||
consumer: consumer.digest,
|
||||
checkerVersion,
|
||||
});
|
||||
const accepted = (options.reviews ?? []).some(
|
||||
(entry) =>
|
||||
entry.requirementDigest === requirementDigest &&
|
||||
["changed", "accepted-unchanged"].includes(entry.decision) &&
|
||||
entry.rationale.trim() &&
|
||||
entry.agentId.trim(),
|
||||
);
|
||||
reviews.push({ ...requirement, requirementDigest, accepted });
|
||||
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
capabilityId,
|
||||
type PackageExport,
|
||||
type PackageExportId,
|
||||
type DependencyPort,
|
||||
type PackageRevision,
|
||||
} from "./types.js";
|
||||
import {
|
||||
TypeSubstitution,
|
||||
GenericTypeError,
|
||||
bindTypeParameters,
|
||||
canonicalTypeArgument,
|
||||
type TypeParameter,
|
||||
type ValueTypeExpression,
|
||||
type ObjectTypeExpression,
|
||||
type InterfaceApplicationExpression,
|
||||
type GenericTypeEnvironment,
|
||||
type ClosedTypeArgument,
|
||||
type ValueAliasDefinition,
|
||||
} from "./generics.js";
|
||||
|
||||
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
||||
requirement:
|
||||
| Extract<DependencyPort["requirement"], { kind: "query" }>
|
||||
| {
|
||||
kind: "state";
|
||||
valueType: ValueTypeExpression;
|
||||
primitives: Extract<DependencyPort["requirement"], { kind: "state" }>["primitives"];
|
||||
}
|
||||
| {
|
||||
kind: "edge";
|
||||
target: ObjectTypeExpression;
|
||||
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
|
||||
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
|
||||
}
|
||||
| { kind: "interface"; application: InterfaceApplicationExpression }
|
||||
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
|
||||
};
|
||||
|
||||
export interface GenericPackageExport {
|
||||
id: PackageExportId;
|
||||
displayName: string;
|
||||
parameters: TypeParameter[];
|
||||
kind: "operation" | "function";
|
||||
inputType: ValueTypeExpression;
|
||||
outputType: ValueTypeExpression;
|
||||
eventType?: ValueTypeExpression;
|
||||
mode?: Extract<PackageExport, { kind: "operation" }>["mode"];
|
||||
receiverRequirement:
|
||||
| { kind: "any-object" }
|
||||
| { kind: "target"; target: ObjectTypeExpression }
|
||||
| { kind: "interfaces"; interfaces: InterfaceApplicationExpression[] };
|
||||
dependencyPorts: GenericDependencyPort[];
|
||||
aliases: ValueAliasDefinition[];
|
||||
}
|
||||
|
||||
/** A closed manifest is a Nix build input, never a mutable source checkout. */
|
||||
export const specializePackageExport = (
|
||||
pkg: PackageRevision,
|
||||
definition: GenericPackageExport,
|
||||
arguments_: ClosedTypeArgument[],
|
||||
environment: GenericTypeEnvironment,
|
||||
): PackageExport => {
|
||||
const lexical = { ...environment, aliases: new Map(definition.aliases.map((alias) => [alias.id, alias])) };
|
||||
const bindings = bindTypeParameters(definition.parameters, arguments_, lexical, definition.displayName);
|
||||
const substitution = new TypeSubstitution({ ...lexical, arguments: bindings });
|
||||
const digest = createHash("sha256")
|
||||
.update(
|
||||
JSON.stringify([
|
||||
"quixos-package-specialization-v1",
|
||||
pkg.revisionId,
|
||||
[pkg.source.repository, pkg.source.commit],
|
||||
definition.id,
|
||||
arguments_.map(canonicalTypeArgument),
|
||||
environment.self ?? null,
|
||||
]),
|
||||
)
|
||||
.digest("hex");
|
||||
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
|
||||
const requirement = port.requirement;
|
||||
switch (requirement.kind) {
|
||||
case "query":
|
||||
return { ...port, requirement };
|
||||
case "state":
|
||||
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
|
||||
case "edge":
|
||||
return { ...port, requirement: { ...requirement, target: substitution.object(requirement.target) } };
|
||||
case "interface":
|
||||
return {
|
||||
...port,
|
||||
requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) },
|
||||
};
|
||||
case "constructor": {
|
||||
const target = substitution.object(requirement.target);
|
||||
if (target.kind !== "atom")
|
||||
throw new GenericTypeError(
|
||||
"constructor-target",
|
||||
port.displayName,
|
||||
"Constructor ports require a concrete atom argument, not an interface view",
|
||||
);
|
||||
return {
|
||||
...port,
|
||||
requirement: {
|
||||
kind: "constructor",
|
||||
atomId: target.atomId,
|
||||
inputType: substitution.value(requirement.inputType),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
const common = {
|
||||
id: capabilityId.packageExport(`export-application:sha256:${digest}`),
|
||||
displayName: `${definition.displayName}$${digest}`,
|
||||
inputType: substitution.value(definition.inputType),
|
||||
outputType: substitution.value(definition.outputType),
|
||||
dependencyPorts: ports,
|
||||
application: {
|
||||
exportId: definition.id,
|
||||
arguments: structuredClone(arguments_),
|
||||
...(environment.self ? { self: environment.self } : {}),
|
||||
},
|
||||
};
|
||||
if (definition.kind === "function") return { ...common, kind: "function" };
|
||||
const receiver = definition.receiverRequirement;
|
||||
const target = receiver.kind === "target" ? substitution.object(receiver.target) : undefined;
|
||||
return {
|
||||
...common,
|
||||
kind: "operation",
|
||||
mode: definition.mode!,
|
||||
...(definition.eventType ? { eventType: substitution.value(definition.eventType) } : {}),
|
||||
receiverRequirement: target
|
||||
? target.kind === "atom"
|
||||
? { kind: "exact-atom", atomId: target.atomId }
|
||||
: { kind: "all-interfaces", interfaceRevisionIds: [target.interfaceRevisionId] }
|
||||
: receiver.kind === "interfaces"
|
||||
? {
|
||||
kind: "all-interfaces",
|
||||
interfaceRevisionIds: receiver.interfaces.map((value) => substitution.application(value)),
|
||||
}
|
||||
: { kind: "any-object" },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,427 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
capabilityId,
|
||||
type AtomId,
|
||||
type InterfaceMember,
|
||||
type InterfaceOperation,
|
||||
type InterfaceRevision,
|
||||
type InterfaceRevisionId,
|
||||
type ObjectExpectation,
|
||||
type SourceRevision,
|
||||
type ValueType,
|
||||
} from "./types.js";
|
||||
|
||||
/** Authoring-only expressions. Installed ValueType deliberately has no variable case. */
|
||||
export type ValueTypeExpression =
|
||||
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
|
||||
| { kind: "parameter"; parameterId: string }
|
||||
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
|
||||
| { kind: "list" | "optional"; value: ValueTypeExpression }
|
||||
| { kind: "object-ref"; expectation: ObjectTypeExpression }
|
||||
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
|
||||
|
||||
export type ObjectTypeExpression =
|
||||
| { kind: "atom"; atomId: AtomId }
|
||||
| { kind: "interface"; interfaceRevisionId: InterfaceRevisionId }
|
||||
| { kind: "parameter"; parameterId: string }
|
||||
| { kind: "self" }
|
||||
| { kind: "application"; application: InterfaceApplicationExpression };
|
||||
|
||||
export type TypeArgumentExpression =
|
||||
| { kind: "value"; type: ValueTypeExpression }
|
||||
| { kind: "object"; target: ObjectTypeExpression };
|
||||
|
||||
export type ClosedTypeArgument = { kind: "value"; type: ValueType } | { kind: "object"; target: ObjectExpectation };
|
||||
|
||||
export interface InterfaceApplicationExpression {
|
||||
definitionId: InterfaceRevisionId;
|
||||
arguments: TypeArgumentExpression[];
|
||||
}
|
||||
|
||||
export type TypeParameter =
|
||||
| { id: string; name: string; kind: "value"; storable?: boolean }
|
||||
| { id: string; name: string; kind: "object"; implements: InterfaceApplicationExpression[] };
|
||||
|
||||
export interface ValueAliasDefinition {
|
||||
id: string;
|
||||
parameters: TypeParameter[];
|
||||
body: ValueTypeExpression;
|
||||
}
|
||||
|
||||
export interface GenericInterfaceTemplate {
|
||||
parameters: TypeParameter[];
|
||||
members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[];
|
||||
requires: InterfaceApplicationExpression[];
|
||||
usesSelf: boolean;
|
||||
aliases?: ValueAliasDefinition[];
|
||||
}
|
||||
|
||||
/** Instantiation produces the existing closed runtime IR, with explicit provenance. */
|
||||
export const instantiateInterface = (
|
||||
definition: InterfaceRevision,
|
||||
arguments_: readonly ClosedTypeArgument[],
|
||||
environment: GenericTypeEnvironment,
|
||||
): InterfaceRevision => {
|
||||
const template = definition.template;
|
||||
if (!template) {
|
||||
if (arguments_.length) fail("type-arity", definition.displayName, "Non-generic interface takes no type arguments");
|
||||
return definition;
|
||||
}
|
||||
const lexicalEnvironment = {
|
||||
...environment,
|
||||
aliases: new Map((template.aliases ?? []).map((alias) => [alias.id, alias])),
|
||||
};
|
||||
const argumentsMap = bindTypeParameters(template.parameters, arguments_, lexicalEnvironment, definition.displayName);
|
||||
if (template.usesSelf && !environment.self)
|
||||
fail("unbound-self", definition.displayName, "Self requires an implementing atom");
|
||||
const substitution = new TypeSubstitution({ ...lexicalEnvironment, arguments: argumentsMap });
|
||||
const operation = (entry: InterfaceOperation<ValueTypeExpression>): InterfaceOperation => ({
|
||||
...entry,
|
||||
inputType: substitution.value(entry.inputType, `${definition.displayName}.${entry.displayName}.input`),
|
||||
outputType: substitution.value(entry.outputType, `${definition.displayName}.${entry.displayName}.output`),
|
||||
eventType: entry.eventType
|
||||
? substitution.value(entry.eventType, `${definition.displayName}.${entry.displayName}.event`)
|
||||
: undefined,
|
||||
});
|
||||
const members: InterfaceMember[] = template.members.map((member) => {
|
||||
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
|
||||
switch (member.kind) {
|
||||
case "value":
|
||||
return {
|
||||
...common,
|
||||
kind: "value",
|
||||
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
|
||||
valueType: substitution.value(member.valueType, member.displayName),
|
||||
};
|
||||
case "relationship":
|
||||
return {
|
||||
...common,
|
||||
kind: "relationship",
|
||||
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
|
||||
target: substitution.object(member.target, member.displayName),
|
||||
cardinality: member.cardinality,
|
||||
ordered: member.ordered,
|
||||
...(member.keyType ? { keyType: member.keyType } : {}),
|
||||
};
|
||||
case "operation":
|
||||
return {
|
||||
...common,
|
||||
kind: "operation",
|
||||
inputType: substitution.value(member.inputType, member.displayName),
|
||||
outputType: substitution.value(member.outputType, member.displayName),
|
||||
};
|
||||
}
|
||||
});
|
||||
const application: AppliedInterfaceIdentity = {
|
||||
definitionId: definition.revisionId,
|
||||
source: definition.source,
|
||||
arguments: structuredClone([...arguments_]),
|
||||
...(template.usesSelf ? { self: environment.self } : {}),
|
||||
};
|
||||
return {
|
||||
interfaceId: definition.interfaceId,
|
||||
revisionId: appliedInterfaceId(application),
|
||||
displayName: definition.displayName,
|
||||
source: definition.source,
|
||||
members,
|
||||
application,
|
||||
requiredInterfaces: template.requires.map((required) => substitution.application(required)),
|
||||
};
|
||||
};
|
||||
|
||||
export interface GenericTypeEnvironment {
|
||||
arguments: ReadonlyMap<string, ClosedTypeArgument>;
|
||||
self?: AtomId;
|
||||
aliases?: ReadonlyMap<string, ValueAliasDefinition>;
|
||||
/** Resolves only checked declarations, never a caller-supplied runtime type string. */
|
||||
applyInterface: (definitionId: InterfaceRevisionId, arguments_: readonly ClosedTypeArgument[]) => InterfaceRevisionId;
|
||||
/** Proof in the candidate, not an authorization grant. */
|
||||
implementsInterface: (target: ObjectExpectation, required: InterfaceRevisionId) => boolean;
|
||||
/** External codecs must explicitly declare reference-free persistence support. */
|
||||
storableMessage?: (descriptorId: string) => boolean;
|
||||
}
|
||||
|
||||
export class GenericTypeError extends Error {
|
||||
constructor(
|
||||
readonly code: string,
|
||||
readonly path: string,
|
||||
message: string,
|
||||
) {
|
||||
super(`${path}: ${message}`);
|
||||
this.name = "GenericTypeError";
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (code: string, path: string, message: string): never => {
|
||||
throw new GenericTypeError(code, path, message);
|
||||
};
|
||||
|
||||
/** One budget across nested aliases/substitutions, including concrete arguments. */
|
||||
class Budget {
|
||||
private remaining = 10000;
|
||||
enter(path: string, depth: number) {
|
||||
if (depth > 128 || --this.remaining < 0)
|
||||
fail("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
|
||||
}
|
||||
}
|
||||
|
||||
export const isStorableType = (
|
||||
type: ValueType,
|
||||
storableMessage: (descriptorId: string) => boolean = () => false,
|
||||
): boolean => {
|
||||
const budget = new Budget();
|
||||
const visit = (value: ValueType, depth: number): boolean => {
|
||||
budget.enter("storable", depth);
|
||||
switch (value.kind) {
|
||||
case "scalar":
|
||||
return true;
|
||||
case "builtin":
|
||||
return value.name === "unit";
|
||||
case "message":
|
||||
return storableMessage(value.descriptorId);
|
||||
case "object-ref":
|
||||
return false;
|
||||
case "list":
|
||||
case "optional":
|
||||
return visit(value.value, depth + 1);
|
||||
// Records currently have RPC codecs, not ordinary-state persistence codecs.
|
||||
// A generic bound must not promise storage that the installed model rejects.
|
||||
case "record":
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return visit(type, 0);
|
||||
};
|
||||
|
||||
/** Canonical closed-type encoding, independent of record insertion order. */
|
||||
export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
|
||||
const budget = new Budget();
|
||||
const target = (value: ObjectExpectation): unknown => {
|
||||
switch (value.kind) {
|
||||
case "atom":
|
||||
return ["atom", value.atomId];
|
||||
case "interface":
|
||||
return ["interface", value.interfaceRevisionId];
|
||||
default:
|
||||
return fail("unresolved-type", "argument", "Expected a closed object target");
|
||||
}
|
||||
};
|
||||
const type = (value: ValueType, depth: number): unknown => {
|
||||
budget.enter("argument", depth);
|
||||
switch (value.kind) {
|
||||
case "builtin":
|
||||
case "scalar":
|
||||
return [value.kind, value.name];
|
||||
case "message":
|
||||
return ["message", value.descriptorId];
|
||||
case "object-ref":
|
||||
return ["object-ref", target(value.expectation)];
|
||||
case "list":
|
||||
case "optional":
|
||||
return [value.kind, type(value.value, depth + 1)];
|
||||
case "record":
|
||||
return [
|
||||
"record",
|
||||
Object.keys(value.fields)
|
||||
.sort()
|
||||
.map((key) => [key, type(value.fields[key], depth + 1)]),
|
||||
];
|
||||
default:
|
||||
return fail("unresolved-type", "argument", "Expected a closed value type");
|
||||
}
|
||||
};
|
||||
switch (argument.kind) {
|
||||
case "value":
|
||||
return JSON.stringify(["value", type(argument.type, 0)]);
|
||||
case "object":
|
||||
return JSON.stringify(["object", target(argument.target)]);
|
||||
default:
|
||||
return fail("invalid-kind", "argument", "Expected a value or object type argument");
|
||||
}
|
||||
};
|
||||
|
||||
export interface AppliedInterfaceIdentity {
|
||||
definitionId: InterfaceRevisionId;
|
||||
source: SourceRevision;
|
||||
arguments: ClosedTypeArgument[];
|
||||
/** Only include Self when it is actually part of the closed contract. */
|
||||
self?: AtomId;
|
||||
}
|
||||
|
||||
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
|
||||
const encoding = JSON.stringify([
|
||||
"quixos-applied-interface-v1",
|
||||
application.definitionId,
|
||||
application.source.repository,
|
||||
application.source.commit.toLowerCase(),
|
||||
application.arguments.map(canonicalTypeArgument),
|
||||
application.self ?? null,
|
||||
]);
|
||||
return capabilityId.interfaceRevision(
|
||||
`interface-application:sha256:${createHash("sha256").update(encoding).digest("hex")}`,
|
||||
);
|
||||
};
|
||||
|
||||
export class TypeSubstitution {
|
||||
private readonly budget = new Budget();
|
||||
private readonly aliases: string[] = [];
|
||||
|
||||
constructor(private environment: GenericTypeEnvironment) {}
|
||||
|
||||
argument(expression: TypeArgumentExpression, path = "argument", depth = 0): ClosedTypeArgument {
|
||||
this.budget.enter(path, depth);
|
||||
switch (expression.kind) {
|
||||
case "value":
|
||||
return { kind: "value", type: this.value(expression.type, path, depth + 1) };
|
||||
case "object":
|
||||
return { kind: "object", target: this.object(expression.target, path, depth + 1) };
|
||||
default:
|
||||
return fail("invalid-kind", path, "Expected a value or object type argument");
|
||||
}
|
||||
}
|
||||
|
||||
application(expression: InterfaceApplicationExpression, path = "interface", depth = 0): InterfaceRevisionId {
|
||||
this.budget.enter(path, depth);
|
||||
return this.environment.applyInterface(
|
||||
expression.definitionId,
|
||||
expression.arguments.map((argument, index) => this.argument(argument, `${path}.arguments[${index}]`, depth + 1)),
|
||||
);
|
||||
}
|
||||
|
||||
object(expression: ObjectTypeExpression, path = "target", depth = 0): ObjectExpectation {
|
||||
this.budget.enter(path, depth);
|
||||
switch (expression.kind) {
|
||||
case "atom":
|
||||
return { kind: "atom", atomId: expression.atomId };
|
||||
case "interface":
|
||||
return { kind: "interface", interfaceRevisionId: expression.interfaceRevisionId };
|
||||
case "self":
|
||||
return this.environment.self
|
||||
? { kind: "atom", atomId: this.environment.self }
|
||||
: fail("unbound-self", path, "Self requires an implementing atom");
|
||||
case "parameter": {
|
||||
const argument = this.environment.arguments.get(expression.parameterId);
|
||||
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
|
||||
if (argument.kind !== "object")
|
||||
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is a value, not an object target`);
|
||||
canonicalTypeArgument(argument);
|
||||
return structuredClone(argument.target);
|
||||
}
|
||||
case "application":
|
||||
return { kind: "interface", interfaceRevisionId: this.application(expression.application, path, depth + 1) };
|
||||
default:
|
||||
return fail("invalid-type", path, "Unknown object type expression");
|
||||
}
|
||||
}
|
||||
|
||||
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
|
||||
this.budget.enter(path, depth);
|
||||
switch (expression.kind) {
|
||||
case "builtin":
|
||||
return { kind: "builtin", name: expression.name };
|
||||
case "scalar":
|
||||
return { kind: "scalar", name: expression.name };
|
||||
case "message":
|
||||
return { kind: "message", descriptorId: expression.descriptorId };
|
||||
case "list":
|
||||
case "optional":
|
||||
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
|
||||
case "record":
|
||||
return {
|
||||
kind: "record",
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(expression.fields).map(([name, field]) => [
|
||||
name,
|
||||
this.value(field, `${path}.${name}`, depth + 1),
|
||||
]),
|
||||
),
|
||||
};
|
||||
case "object-ref":
|
||||
return { kind: "object-ref", expectation: this.object(expression.expectation, `${path}.ref`, depth + 1) };
|
||||
case "parameter": {
|
||||
const argument = this.environment.arguments.get(expression.parameterId);
|
||||
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
|
||||
if (argument.kind !== "value")
|
||||
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is an object target; use ref<T>`);
|
||||
canonicalTypeArgument(argument);
|
||||
return this.value(argument.type, path, depth + 1);
|
||||
}
|
||||
case "alias": {
|
||||
const alias = this.environment.aliases?.get(expression.definitionId);
|
||||
if (!alias) return fail("unknown-alias", path, `Unknown type alias ${expression.definitionId}`);
|
||||
if (this.aliases.includes(alias.id))
|
||||
return fail("recursive-alias", path, `Recursive value alias: ${[...this.aliases, alias.id].join(" -> ")}`);
|
||||
const arguments_ = expression.arguments.map((argument, index) =>
|
||||
this.argument(argument, `${path}.arguments[${index}]`, depth + 1),
|
||||
);
|
||||
const bindings = bindTypeParameters(alias.parameters, arguments_, this.environment, path);
|
||||
this.aliases.push(alias.id);
|
||||
try {
|
||||
// Reuse this expansion budget/stack; lexical parameter maps are restored.
|
||||
const previous = this.environment;
|
||||
this.environment = { ...previous, arguments: bindings };
|
||||
try {
|
||||
return this.value(alias.body, `${path}.${alias.id}`, depth + 1);
|
||||
} finally {
|
||||
this.environment = previous;
|
||||
}
|
||||
} finally {
|
||||
this.aliases.pop();
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fail("invalid-type", path, "Unknown value type expression");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bindTypeParameters = (
|
||||
parameters: readonly TypeParameter[],
|
||||
arguments_: readonly ClosedTypeArgument[],
|
||||
environment: GenericTypeEnvironment,
|
||||
path = "parameters",
|
||||
): ReadonlyMap<string, ClosedTypeArgument> => {
|
||||
if (parameters.length !== arguments_.length)
|
||||
fail("type-arity", path, `Expected ${parameters.length} type arguments, received ${arguments_.length}`);
|
||||
const bindings = new Map<string, ClosedTypeArgument>();
|
||||
const names = new Set<string>();
|
||||
for (const [index, parameter] of parameters.entries()) {
|
||||
if (
|
||||
!parameter.id ||
|
||||
!parameter.name ||
|
||||
parameter.name === "Self" ||
|
||||
bindings.has(parameter.id) ||
|
||||
names.has(parameter.name)
|
||||
)
|
||||
fail("duplicate-parameter", path, `Invalid or duplicate parameter ${parameter.name}`);
|
||||
names.add(parameter.name);
|
||||
const argument = arguments_[index];
|
||||
if (parameter.kind !== argument.kind)
|
||||
fail("parameter-kind", `${path}.${parameter.name}`, `Expected ${parameter.kind}, received ${argument.kind}`);
|
||||
canonicalTypeArgument(argument);
|
||||
bindings.set(parameter.id, structuredClone(argument));
|
||||
}
|
||||
const substitution = new TypeSubstitution({ ...environment, arguments: bindings });
|
||||
for (const parameter of parameters) {
|
||||
const argument = bindings.get(parameter.id)!;
|
||||
if (
|
||||
parameter.kind === "value" &&
|
||||
argument.kind === "value" &&
|
||||
parameter.storable &&
|
||||
!isStorableType(argument.type, environment.storableMessage)
|
||||
)
|
||||
fail(
|
||||
"non-storable-argument",
|
||||
`${path}.${parameter.name}`,
|
||||
"State values cannot contain managed references or unsupported transport values",
|
||||
);
|
||||
if (parameter.kind === "object" && argument.kind === "object") {
|
||||
for (const bound of parameter.implements) {
|
||||
const required = substitution.application(bound, `${path}.${parameter.name}.implements`);
|
||||
if (!environment.implementsInterface(argument.target, required))
|
||||
fail("unsatisfied-bound", `${path}.${parameter.name}`, `Object target does not implement ${required}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./types.js";
|
||||
export * from "./validation.js";
|
||||
export * from "./evolution.js";
|
||||
export * from "./migrations.js";
|
||||
export * from "./generics.js";
|
||||
export * from "./generic-packages.js";
|
||||
@@ -0,0 +1,156 @@
|
||||
import { contentDigest } from "./evolution.js";
|
||||
import type { PersistentAttachment } from "./types.js";
|
||||
|
||||
/** Portable storage shape: local owner/slot/projection identities are supplied
|
||||
* by the consuming workspace's explicit bindings, never baked into this hash. */
|
||||
export const migrationPortContract = (attachment: PersistentAttachment): unknown => {
|
||||
if (attachment.kind === "state")
|
||||
return {
|
||||
kind: "state",
|
||||
valueType: attachment.valueType,
|
||||
storagePolicy: attachment.storagePolicy,
|
||||
...(attachment.defaultValue === undefined ? {} : { defaultValue: attachment.defaultValue }),
|
||||
};
|
||||
const endpoint = (value: (typeof attachment.endpoints)[number]) => ({
|
||||
constraint: value.constraint,
|
||||
cardinality: value.cardinality,
|
||||
ordered: value.ordered,
|
||||
onDelete: value.onDelete ?? "restrict",
|
||||
retainOther: value.retainOther ?? false,
|
||||
...(value.keyType ? { keyType: value.keyType } : {}),
|
||||
...(value.publicTraversal ? { publicTraversal: true } : {}),
|
||||
});
|
||||
return { kind: "edge", first: endpoint(attachment.endpoints[0]), second: endpoint(attachment.endpoints[1]) };
|
||||
};
|
||||
|
||||
export type MigrationDeclaration = {
|
||||
id: string;
|
||||
scopeId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
implementation: { exportId: string; file: string; digest: string };
|
||||
predecessors: string[];
|
||||
ports: {
|
||||
name: string;
|
||||
view: "old" | "new";
|
||||
access: ("read" | "write" | "create" | "edge")[];
|
||||
contractDigest: string;
|
||||
}[];
|
||||
preservesOldReaders?: boolean;
|
||||
preservesOldWriters?: boolean;
|
||||
};
|
||||
export type MigrationCatalog = {
|
||||
schemaVersion: 1;
|
||||
contracts: Record<string, unknown>;
|
||||
migrations: MigrationDeclaration[];
|
||||
};
|
||||
export const validateMigrationCatalog = (value: unknown, exportIds?: ReadonlySet<string>): MigrationCatalog => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value))
|
||||
throw new Error("Migration catalog must be an object");
|
||||
const catalog = value as MigrationCatalog;
|
||||
if (
|
||||
catalog.schemaVersion !== 1 ||
|
||||
!catalog.contracts ||
|
||||
Array.isArray(catalog.contracts) ||
|
||||
!Array.isArray(catalog.migrations)
|
||||
)
|
||||
throw new Error("Unsupported migration catalog");
|
||||
for (const [digest, contract] of Object.entries(catalog.contracts))
|
||||
if (contentDigest(contract) !== digest) throw new Error(`Migration contract digest mismatch: ${digest}`);
|
||||
const ids = new Set<string>();
|
||||
for (const migration of catalog.migrations) {
|
||||
if (!migration.id || !migration.scopeId || ids.has(migration.id))
|
||||
throw new Error("Migration IDs must be stable and unique");
|
||||
ids.add(migration.id);
|
||||
if (!catalog.contracts[migration.from] || !catalog.contracts[migration.to] || migration.from === migration.to)
|
||||
throw new Error(`Migration ${migration.id} requires distinct retained source and target contracts`);
|
||||
if (!migration.implementation?.exportId || !/^sha256:[0-9a-f]{64}$/.test(migration.implementation.digest))
|
||||
throw new Error(`Migration ${migration.id} requires an exact implementation digest`);
|
||||
if (
|
||||
!migration.implementation.file ||
|
||||
migration.implementation.file.startsWith("/") ||
|
||||
migration.implementation.file.split(/[\\/]/).some((part) => !part || part === "." || part === "..")
|
||||
)
|
||||
throw new Error("Migration implementation must be a relative package file");
|
||||
if (exportIds && !exportIds.has(migration.implementation.exportId))
|
||||
throw new Error(`Migration ${migration.id} refers to an undeclared package export`);
|
||||
if (!Array.isArray(migration.predecessors) || !Array.isArray(migration.ports))
|
||||
throw new Error(`Migration ${migration.id} requires predecessors and ports`);
|
||||
if (new Set(migration.predecessors).size !== migration.predecessors.length)
|
||||
throw new Error(`Duplicate predecessor in ${migration.id}`);
|
||||
for (const promise of [migration.preservesOldReaders, migration.preservesOldWriters])
|
||||
if (promise !== undefined && typeof promise !== "boolean")
|
||||
throw new Error("Migration compatibility promises must be booleans");
|
||||
const ports = new Set<string>();
|
||||
for (const port of migration.ports) {
|
||||
if (
|
||||
!port.name ||
|
||||
ports.has(port.name) ||
|
||||
!["old", "new"].includes(port.view) ||
|
||||
!Array.isArray(port.access) ||
|
||||
!port.access.length ||
|
||||
port.access.some((access) => !["read", "write", "create", "edge"].includes(access)) ||
|
||||
!catalog.contracts[port.contractDigest]
|
||||
)
|
||||
throw new Error(`Invalid migration port in ${migration.id}`);
|
||||
if (port.view === "old" && port.access.some((access) => access !== "read"))
|
||||
throw new Error("Old migration views are read-only");
|
||||
ports.add(port.name);
|
||||
}
|
||||
}
|
||||
for (const migration of catalog.migrations)
|
||||
for (const predecessor of migration.predecessors)
|
||||
if (!ids.has(predecessor)) throw new Error(`Missing retained predecessor ${predecessor}`);
|
||||
// Catalogs are retained across releases. Reject impossible histories at
|
||||
// publication/check time, not only when someone tries to select a path.
|
||||
const remaining = new Map(catalog.migrations.map((entry) => [entry.id, new Set(entry.predecessors)]));
|
||||
const ready = [...remaining].filter(([, dependencies]) => dependencies.size === 0).map(([id]) => id);
|
||||
for (let index = 0; index < ready.length; index++) {
|
||||
remaining.delete(ready[index]);
|
||||
for (const [id, dependencies] of remaining)
|
||||
if (dependencies.delete(ready[index]) && dependencies.size === 0) ready.push(id);
|
||||
}
|
||||
if (remaining.size) throw new Error(`Cyclic migration predecessors: ${[...remaining.keys()].join(", ")}`);
|
||||
return catalog;
|
||||
};
|
||||
|
||||
export type MigrationSelection = {
|
||||
scopeId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
path: string[];
|
||||
bindings: Record<string, string>;
|
||||
};
|
||||
/** Explicit paths, not shortest-path guesses. Receipts identify code plus local scope mapping. */
|
||||
export const selectMigrationPath = (
|
||||
catalog: MigrationCatalog,
|
||||
selection: MigrationSelection,
|
||||
previousReceipts: ReadonlyMap<string, string> = new Map(),
|
||||
) => {
|
||||
validateMigrationCatalog(catalog);
|
||||
let current = selection.from;
|
||||
const seen = new Set<string>();
|
||||
const transitions = [];
|
||||
for (const id of selection.path) {
|
||||
const declaration = catalog.migrations.find((entry) => entry.id === id);
|
||||
if (!declaration || declaration.scopeId !== selection.scopeId || declaration.from !== current || seen.has(id))
|
||||
throw new Error(`Invalid selected migration transition ${id}`);
|
||||
for (const predecessor of declaration.predecessors)
|
||||
if (!seen.has(predecessor) && !previousReceipts.has(predecessor))
|
||||
throw new Error(`Unsatisfied predecessor ${predecessor}`);
|
||||
const usedBindings: Record<string, string> = {};
|
||||
for (const port of declaration.ports) {
|
||||
if (!selection.bindings[port.name]) throw new Error(`Missing local migration binding ${port.name}`);
|
||||
usedBindings[port.name] = selection.bindings[port.name];
|
||||
}
|
||||
const digest = contentDigest({ declaration, bindings: usedBindings });
|
||||
const previous = previousReceipts.get(id);
|
||||
if (previous && previous !== digest)
|
||||
throw new Error(`Migration identity ${id} was previously used with different code or scope`);
|
||||
transitions.push({ declaration, bindings: usedBindings, digest, alreadyApplied: Boolean(previous) });
|
||||
seen.add(id);
|
||||
current = declaration.to;
|
||||
}
|
||||
if (current !== selection.to) throw new Error("Selected migration path does not cover the target storage contract");
|
||||
return transitions;
|
||||
};
|
||||
@@ -0,0 +1,402 @@
|
||||
declare const opaqueIdBrand: unique symbol;
|
||||
|
||||
type OpaqueId<Kind extends string> = string & {
|
||||
readonly [opaqueIdBrand]: Kind;
|
||||
};
|
||||
|
||||
export type WorkspaceId = OpaqueId<"WorkspaceId">;
|
||||
export type WorkspaceRevisionId = OpaqueId<"WorkspaceRevisionId">;
|
||||
export type AtomId = OpaqueId<"AtomId">;
|
||||
export type ConformanceId = OpaqueId<"ConformanceId">;
|
||||
export type InterfaceId = OpaqueId<"InterfaceId">;
|
||||
export type InterfaceRevisionId = OpaqueId<"InterfaceRevisionId">;
|
||||
export type MemberId = OpaqueId<"MemberId">;
|
||||
export type OperationId = OpaqueId<"OperationId">;
|
||||
export type SlotId = OpaqueId<"SlotId">;
|
||||
export type EdgeTypeId = OpaqueId<"EdgeTypeId">;
|
||||
export type EdgeProjectionId = OpaqueId<"EdgeProjectionId">;
|
||||
export type PackageId = OpaqueId<"PackageId">;
|
||||
export type PackageRevisionId = OpaqueId<"PackageRevisionId">;
|
||||
export type PackageExportId = OpaqueId<"PackageExportId">;
|
||||
export type DependencyPortId = OpaqueId<"DependencyPortId">;
|
||||
export type ObjectId = OpaqueId<"ObjectId">;
|
||||
|
||||
const opaque = <Kind extends string>(value: string) => value as OpaqueId<Kind>;
|
||||
|
||||
/**
|
||||
* Textual IDs remain opaque to the semantic model. Authoring tools may mint
|
||||
* UUID-backed values later without changing the checked IR.
|
||||
*/
|
||||
export const capabilityId = {
|
||||
workspace: (value: string) => opaque<"WorkspaceId">(value),
|
||||
workspaceRevision: (value: string) => opaque<"WorkspaceRevisionId">(value),
|
||||
atom: (value: string) => opaque<"AtomId">(value),
|
||||
conformance: (value: string) => opaque<"ConformanceId">(value),
|
||||
interface: (value: string) => opaque<"InterfaceId">(value),
|
||||
interfaceRevision: (value: string) => opaque<"InterfaceRevisionId">(value),
|
||||
member: (value: string) => opaque<"MemberId">(value),
|
||||
operation: (value: string) => opaque<"OperationId">(value),
|
||||
slot: (value: string) => opaque<"SlotId">(value),
|
||||
edgeType: (value: string) => opaque<"EdgeTypeId">(value),
|
||||
edgeProjection: (value: string) => opaque<"EdgeProjectionId">(value),
|
||||
package: (value: string) => opaque<"PackageId">(value),
|
||||
packageRevision: (value: string) => opaque<"PackageRevisionId">(value),
|
||||
packageExport: (value: string) => opaque<"PackageExportId">(value),
|
||||
dependencyPort: (value: string) => opaque<"DependencyPortId">(value),
|
||||
object: (value: string) => opaque<"ObjectId">(value),
|
||||
} as const;
|
||||
|
||||
export interface SourceRevision {
|
||||
repository: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export type ScalarValueTypeName = "bool" | "bytes" | "double" | "int32" | "int64" | "string" | "uint32" | "uint64";
|
||||
|
||||
export type ObjectExpectation =
|
||||
| { kind: "atom"; atomId: AtomId }
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
};
|
||||
|
||||
export type ValueType =
|
||||
| { kind: "builtin"; name: "unit" | "watch-handle" }
|
||||
| { kind: "scalar"; name: ScalarValueTypeName }
|
||||
| { kind: "message"; descriptorId: string }
|
||||
| { kind: "record"; fields: Record<string, ValueType> }
|
||||
| { kind: "object-ref"; expectation: ObjectExpectation }
|
||||
| { kind: "optional"; value: ValueType }
|
||||
| { kind: "list"; value: ValueType };
|
||||
|
||||
export const valueType = {
|
||||
unit: { kind: "builtin", name: "unit" } as const,
|
||||
watchHandle: { kind: "builtin", name: "watch-handle" } as const,
|
||||
bool: { kind: "scalar", name: "bool" } as const,
|
||||
bytes: { kind: "scalar", name: "bytes" } as const,
|
||||
double: { kind: "scalar", name: "double" } as const,
|
||||
int32: { kind: "scalar", name: "int32" } as const,
|
||||
int64: { kind: "scalar", name: "int64" } as const,
|
||||
string: { kind: "scalar", name: "string" } as const,
|
||||
uint32: { kind: "scalar", name: "uint32" } as const,
|
||||
uint64: { kind: "scalar", name: "uint64" } as const,
|
||||
message: (descriptorId: string): ValueType => ({
|
||||
kind: "message",
|
||||
descriptorId,
|
||||
}),
|
||||
atomRef: (atomId: AtomId): ValueType => ({
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "atom", atomId },
|
||||
}),
|
||||
interfaceRef: (interfaceRevisionId: InterfaceRevisionId): ValueType => ({
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "interface", interfaceRevisionId },
|
||||
}),
|
||||
optional: (value: ValueType): ValueType => ({ kind: "optional", value }),
|
||||
list: (value: ValueType): ValueType => ({ kind: "list", value }),
|
||||
} as const;
|
||||
|
||||
export interface AtomDefinition {
|
||||
id: AtomId;
|
||||
displayName: string;
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
|
||||
|
||||
export interface InterfaceOperation<Type = ValueType> {
|
||||
id: OperationId;
|
||||
displayName: string;
|
||||
inputType: Type;
|
||||
outputType: Type;
|
||||
mode: InterfaceOperationMode;
|
||||
/** Class capabilities have no object receiver and bind free functions. */
|
||||
scope?: "class";
|
||||
/** Required for watch-start/subscribe and absent for other modes. */
|
||||
eventType?: Type;
|
||||
}
|
||||
|
||||
export type QueryReadContract = { execution: "native" | "rpc-permitted" };
|
||||
|
||||
interface InterfaceMemberBase<Type = ValueType> {
|
||||
id: MemberId;
|
||||
displayName: string;
|
||||
operations: InterfaceOperation<Type>[];
|
||||
}
|
||||
|
||||
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
|
||||
kind: "value";
|
||||
queryRead?: QueryReadContract;
|
||||
valueType: Type;
|
||||
}
|
||||
|
||||
export type EdgeCardinality = "optional-one" | "exactly-one" | "many" | "many-unique";
|
||||
|
||||
export type EdgeEndpointConstraint =
|
||||
| { kind: "atom"; atomId: AtomId }
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
};
|
||||
|
||||
export interface RelationshipInterfaceMember<
|
||||
Type = ValueType,
|
||||
Target = EdgeEndpointConstraint,
|
||||
> extends InterfaceMemberBase<Type> {
|
||||
kind: "relationship";
|
||||
queryRead?: QueryReadContract;
|
||||
target: Target;
|
||||
cardinality: EdgeCardinality;
|
||||
ordered: boolean;
|
||||
keyType?: "string" | "boolean" | "int64";
|
||||
}
|
||||
|
||||
/** A named callable capability that is not value or relationship sugar. */
|
||||
export interface OperationInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
|
||||
kind: "operation";
|
||||
inputType: Type;
|
||||
outputType: Type;
|
||||
}
|
||||
|
||||
export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> =
|
||||
| ValueInterfaceMember<Type>
|
||||
| RelationshipInterfaceMember<Type, Target>
|
||||
| OperationInterfaceMember<Type>;
|
||||
|
||||
export interface InterfaceRevision {
|
||||
interfaceId: InterfaceId;
|
||||
revisionId: InterfaceRevisionId;
|
||||
displayName: string;
|
||||
source: SourceRevision;
|
||||
members: InterfaceMember[];
|
||||
/** Authored generic definition; never an executable contract by itself. */
|
||||
template?: import("./generics.js").GenericInterfaceTemplate;
|
||||
/** Immutable provenance of a closed generic application. */
|
||||
application?: import("./generics.js").AppliedInterfaceIdentity;
|
||||
requiredInterfaces?: InterfaceRevisionId[];
|
||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||
}
|
||||
|
||||
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType };
|
||||
|
||||
export interface StateSlotDefinition {
|
||||
kind: "state";
|
||||
id: SlotId;
|
||||
attachedTo: AtomId;
|
||||
displayName: string;
|
||||
valueType: ValueType;
|
||||
storagePolicy: StoragePolicy;
|
||||
defaultValue?: unknown;
|
||||
}
|
||||
|
||||
export interface EdgeEndpoint {
|
||||
keyType?: "string" | "boolean" | "int64";
|
||||
publicTraversal?: boolean;
|
||||
projectionId: EdgeProjectionId;
|
||||
displayName: string;
|
||||
constraint: EdgeEndpointConstraint;
|
||||
cardinality: EdgeCardinality;
|
||||
ordered: boolean;
|
||||
onDelete?: "restrict" | "detach" | "cascade-other";
|
||||
retainOther?: boolean;
|
||||
}
|
||||
|
||||
export interface EdgeDefinition {
|
||||
kind: "edge";
|
||||
id: EdgeTypeId;
|
||||
displayName: string;
|
||||
endpoints: [EdgeEndpoint, EdgeEndpoint];
|
||||
}
|
||||
|
||||
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
|
||||
|
||||
export type StatePrimitive = "read" | "write" | "watch-start" | "watch-stop";
|
||||
|
||||
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
|
||||
|
||||
export type PackageReceiverRequirement =
|
||||
| { kind: "any-object" }
|
||||
| { kind: "exact-atom"; atomId: AtomId }
|
||||
| {
|
||||
kind: "all-interfaces";
|
||||
interfaceRevisionIds: InterfaceRevisionId[];
|
||||
};
|
||||
|
||||
export type DependencyPortRequirement =
|
||||
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string }
|
||||
| {
|
||||
kind: "state";
|
||||
valueType: ValueType;
|
||||
primitives: StatePrimitive[];
|
||||
}
|
||||
| {
|
||||
kind: "edge";
|
||||
target: EdgeEndpointConstraint;
|
||||
cardinality: EdgeCardinality;
|
||||
primitives: EdgePrimitive[];
|
||||
}
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
}
|
||||
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
|
||||
|
||||
export interface DependencyPort {
|
||||
id: DependencyPortId;
|
||||
displayName: string;
|
||||
requirement: DependencyPortRequirement;
|
||||
}
|
||||
|
||||
interface PackageExportBase {
|
||||
id: PackageExportId;
|
||||
displayName: string;
|
||||
inputType: ValueType;
|
||||
outputType: ValueType;
|
||||
dependencyPorts: DependencyPort[];
|
||||
/** Closed adapter served by the same immutable package build. */
|
||||
application?: { exportId: PackageExportId; arguments: import("./generics.js").ClosedTypeArgument[]; self?: AtomId };
|
||||
}
|
||||
|
||||
export interface PackageOperationExport extends PackageExportBase {
|
||||
kind: "operation";
|
||||
mode: InterfaceOperationMode;
|
||||
eventType?: ValueType;
|
||||
receiverRequirement: PackageReceiverRequirement;
|
||||
}
|
||||
|
||||
export interface PackageFunctionExport extends PackageExportBase {
|
||||
kind: "function";
|
||||
}
|
||||
|
||||
export interface PackageConstructorExport extends PackageExportBase {
|
||||
kind: "constructor";
|
||||
constructsAtom: AtomId;
|
||||
}
|
||||
|
||||
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
|
||||
|
||||
export interface PackageRevision {
|
||||
queries?: import("../query/types.js").QueryDeclaration[];
|
||||
checkedQueries?: import("../query/types.js").CheckedQuery[];
|
||||
queryTemplates?: import("../query/types.js").QueryTemplate[];
|
||||
genericExports?: import("./generic-packages.js").GenericPackageExport[];
|
||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||
migrationCatalog?: import("./migrations.js").MigrationCatalog;
|
||||
packageId: PackageId;
|
||||
revisionId: PackageRevisionId;
|
||||
displayName: string;
|
||||
source: SourceRevision;
|
||||
exports: PackageExport[];
|
||||
/** Author-declared implementation semantics; omitted legacy values mean 1. */
|
||||
semanticMajor?: number;
|
||||
}
|
||||
|
||||
export type DependencyBinding =
|
||||
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string; via?: EdgeTraversal }
|
||||
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
|
||||
| {
|
||||
kind: "edge";
|
||||
edgeTypeId: EdgeTypeId;
|
||||
projectionId: EdgeProjectionId;
|
||||
via?: EdgeTraversal;
|
||||
}
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
via?: EdgeTraversal;
|
||||
}
|
||||
| { kind: "constructor"; atomId: AtomId };
|
||||
|
||||
export interface BoundDependency {
|
||||
portId: DependencyPortId;
|
||||
binding: DependencyBinding;
|
||||
}
|
||||
|
||||
/** Selects the object at the other end of an exactly-one relationship. */
|
||||
export interface EdgeTraversal {
|
||||
edgeTypeId: EdgeTypeId;
|
||||
projectionId: EdgeProjectionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisional get-or-create recipe for a relationship implementation. The
|
||||
* host projection is supplied by the relationship's resolve binding; this
|
||||
* projection originates at the object constructed for the relationship.
|
||||
*/
|
||||
export interface RelationshipMaterialization {
|
||||
memberId: MemberId;
|
||||
constructorAtomId: AtomId;
|
||||
edgeTypeId: EdgeTypeId;
|
||||
constructedProjectionId: EdgeProjectionId;
|
||||
}
|
||||
|
||||
export type Binding =
|
||||
| {
|
||||
kind: "state";
|
||||
slotId: SlotId;
|
||||
primitive: StatePrimitive;
|
||||
}
|
||||
| {
|
||||
kind: "edge";
|
||||
edgeTypeId: EdgeTypeId;
|
||||
projectionId: EdgeProjectionId;
|
||||
primitive: EdgePrimitive;
|
||||
}
|
||||
| {
|
||||
kind: "package";
|
||||
packageRevisionId: PackageRevisionId;
|
||||
exportId: PackageExportId;
|
||||
dependencies: BoundDependency[];
|
||||
};
|
||||
|
||||
export interface OperationBinding {
|
||||
operationId: OperationId;
|
||||
binding: Binding;
|
||||
/** Required for package-backed reads of an RPC-permitted queryable field. */
|
||||
queryReason?: string;
|
||||
}
|
||||
|
||||
export interface Conformance {
|
||||
/** Absent only for legacy assemblies awaiting explicit ownership enrollment. */
|
||||
id?: ConformanceId;
|
||||
semanticMajor?: number;
|
||||
atomId: AtomId;
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
privateAttachments: PersistentAttachment[];
|
||||
operationBindings: OperationBinding[];
|
||||
relationshipMaterializations: RelationshipMaterialization[];
|
||||
}
|
||||
|
||||
export interface AtomConstructorBinding {
|
||||
atomId: AtomId;
|
||||
packageRevisionId: PackageRevisionId;
|
||||
exportId: PackageExportId;
|
||||
dependencies: BoundDependency[];
|
||||
}
|
||||
|
||||
export interface WorkspaceRevision {
|
||||
linkedQueries?: import("../query/link.js").LinkedQuery[];
|
||||
id: WorkspaceRevisionId;
|
||||
workspaceId: WorkspaceId;
|
||||
parentRevisionIds: WorkspaceRevisionId[];
|
||||
sourceRootCommit: string;
|
||||
atoms: AtomDefinition[];
|
||||
sharedAttachments: PersistentAttachment[];
|
||||
interfaceImports: InterfaceRevision[];
|
||||
packageImports: PackageRevision[];
|
||||
conformances: Conformance[];
|
||||
constructors: AtomConstructorBinding[];
|
||||
}
|
||||
|
||||
export type AttachmentOwner =
|
||||
| { kind: "workspace" }
|
||||
| {
|
||||
kind: "conformance";
|
||||
atomId: AtomId;
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
};
|
||||
|
||||
export interface OwnedAttachment {
|
||||
attachment: PersistentAttachment;
|
||||
owner: AttachmentOwner;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
packageDescriptorToJson,
|
||||
parsePackageDescriptorTextproto,
|
||||
validatePackageDescriptor,
|
||||
} from "./descriptor.js";
|
||||
import { packageDescriptorToJson, parsePackageDescriptorTextproto, validatePackageDescriptor } from "./descriptor.js";
|
||||
|
||||
const usage = (): never => {
|
||||
console.error("Usage: quixos-descriptor-check <descriptor.txtpb>");
|
||||
@@ -14,9 +10,7 @@ const usage = (): never => {
|
||||
const path = process.argv[2] ?? usage();
|
||||
|
||||
try {
|
||||
const descriptor = parsePackageDescriptorTextproto(
|
||||
fs.readFileSync(path, "utf8"),
|
||||
);
|
||||
const descriptor = parsePackageDescriptorTextproto(fs.readFileSync(path, "utf8"));
|
||||
const errors = validatePackageDescriptor(descriptor);
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
|
||||
+20
-108
@@ -3,23 +3,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fromBinary, toJsonString } from "@bufbuild/protobuf";
|
||||
import type { PackageDescriptor } from "./gen/quixos/package_pb.js";
|
||||
import {
|
||||
FunctionCapabilityKind,
|
||||
PackageDescriptorSchema,
|
||||
} from "./gen/quixos/package_pb.js";
|
||||
|
||||
const functionKey = (value: {
|
||||
packageNamespace: string;
|
||||
packageName: string;
|
||||
symbol: string;
|
||||
versionRef: string;
|
||||
}) =>
|
||||
[
|
||||
value.packageNamespace,
|
||||
value.packageName,
|
||||
value.symbol,
|
||||
value.versionRef,
|
||||
].join(":");
|
||||
import { PackageDescriptorSchema } from "./gen/quixos/package_pb.js";
|
||||
|
||||
const protoPaths = () => {
|
||||
const candidates = [
|
||||
@@ -30,19 +14,13 @@ const protoPaths = () => {
|
||||
path.resolve(process.cwd(), "proto"),
|
||||
path.resolve(process.cwd(), "quixos-protocol/proto"),
|
||||
];
|
||||
return [...new Set(candidates)].filter((candidate) =>
|
||||
fs.existsSync(path.join(candidate, "quixos/package.proto")),
|
||||
);
|
||||
return [...new Set(candidates)].filter((candidate) => fs.existsSync(path.join(candidate, "quixos/package.proto")));
|
||||
};
|
||||
|
||||
export const parsePackageDescriptorTextproto = (
|
||||
text: string,
|
||||
): PackageDescriptor => {
|
||||
export const parsePackageDescriptorTextproto = (text: string): PackageDescriptor => {
|
||||
const paths = protoPaths();
|
||||
if (paths.length === 0) {
|
||||
throw new Error(
|
||||
"QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors",
|
||||
);
|
||||
throw new Error("QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors");
|
||||
}
|
||||
const result = childProcess.spawnSync(
|
||||
"protoc",
|
||||
@@ -60,9 +38,7 @@ export const parsePackageDescriptorTextproto = (
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`,
|
||||
);
|
||||
throw new Error(`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`);
|
||||
}
|
||||
return fromBinary(PackageDescriptorSchema, result.stdout);
|
||||
};
|
||||
@@ -72,91 +48,27 @@ export const packageDescriptorToJson = (descriptor: PackageDescriptor) =>
|
||||
prettySpaces: 2,
|
||||
});
|
||||
|
||||
export const validatePackageDescriptor = (
|
||||
descriptor: PackageDescriptor,
|
||||
): string[] => {
|
||||
export const validatePackageDescriptor = (descriptor: PackageDescriptor): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!descriptor.packageNamespace) {
|
||||
errors.push("packageNamespace is required");
|
||||
if (!descriptor.packageId) {
|
||||
errors.push("packageId is required");
|
||||
}
|
||||
if (!descriptor.packageName) {
|
||||
errors.push("packageName is required");
|
||||
if (!descriptor.packageRevisionId) {
|
||||
errors.push("packageRevisionId is required");
|
||||
}
|
||||
if (descriptor.descriptorVersion === 0) {
|
||||
errors.push("descriptorVersion must be greater than zero");
|
||||
if (!descriptor.runtimeProtocolVersion) {
|
||||
errors.push("runtimeProtocolVersion is required");
|
||||
}
|
||||
if (
|
||||
descriptor.interfaceCompatibleBackTo !== 0 &&
|
||||
descriptor.interfaceCompatibleBackTo > descriptor.descriptorVersion
|
||||
) {
|
||||
errors.push("interfaceCompatibleBackTo must not exceed descriptorVersion");
|
||||
const seenExports = new Set<string>();
|
||||
for (const [index, entry] of descriptor.exports.entries()) {
|
||||
if (!entry.exportId) errors.push(`exports[${index}].exportId is required`);
|
||||
if (!entry.runtimeSymbol) {
|
||||
errors.push(`exports[${index}].runtimeSymbol is required`);
|
||||
}
|
||||
if (
|
||||
descriptor.runnerCompatibleBackTo !== 0 &&
|
||||
descriptor.runnerCompatibleBackTo > descriptor.descriptorVersion
|
||||
) {
|
||||
errors.push("runnerCompatibleBackTo must not exceed descriptorVersion");
|
||||
}
|
||||
|
||||
const seenFunctions = new Set<string>();
|
||||
for (const [index, entry] of descriptor.functionExports.entries()) {
|
||||
const fn = entry.function;
|
||||
if (!fn) {
|
||||
errors.push(`functionExports[${index}].function is required`);
|
||||
continue;
|
||||
}
|
||||
if (!fn.packageNamespace) {
|
||||
fn.packageNamespace = descriptor.packageNamespace;
|
||||
}
|
||||
if (!fn.packageName) {
|
||||
fn.packageName = descriptor.packageName;
|
||||
}
|
||||
if (
|
||||
fn.packageNamespace !== descriptor.packageNamespace ||
|
||||
fn.packageName !== descriptor.packageName
|
||||
) {
|
||||
errors.push(
|
||||
`functionExports[${index}] must export from this package (${descriptor.packageNamespace}/${descriptor.packageName})`,
|
||||
);
|
||||
}
|
||||
if (!fn.symbol) {
|
||||
errors.push(`functionExports[${index}].function.symbol is required`);
|
||||
}
|
||||
if (entry.interfaceVersion === 0) {
|
||||
errors.push(`functionExports[${index}].interfaceVersion is required`);
|
||||
}
|
||||
if (entry.capabilityKind === FunctionCapabilityKind.FUNCTION_CAPABILITY_KIND_UNSPECIFIED) {
|
||||
errors.push(`functionExports[${index}].capabilityKind is required`);
|
||||
}
|
||||
if (
|
||||
entry.interfaceCompatibleBackTo !== 0 &&
|
||||
entry.interfaceCompatibleBackTo > entry.interfaceVersion
|
||||
) {
|
||||
errors.push(
|
||||
`functionExports[${index}].interfaceCompatibleBackTo must not exceed interfaceVersion`,
|
||||
);
|
||||
}
|
||||
const key = functionKey(fn);
|
||||
if (seenFunctions.has(key)) {
|
||||
errors.push(`duplicate function export: ${key}`);
|
||||
}
|
||||
seenFunctions.add(key);
|
||||
}
|
||||
|
||||
for (const [index, entry] of descriptor.dependencies.entries()) {
|
||||
if (!entry.package) {
|
||||
errors.push(`dependencies[${index}].package is required`);
|
||||
continue;
|
||||
}
|
||||
if (!entry.package.packageNamespace) {
|
||||
errors.push(`dependencies[${index}].package.packageNamespace is required`);
|
||||
}
|
||||
if (!entry.package.packageName) {
|
||||
errors.push(`dependencies[${index}].package.packageName is required`);
|
||||
}
|
||||
if (entry.package.descriptorVersion === 0) {
|
||||
errors.push(`dependencies[${index}].package.descriptorVersion is required`);
|
||||
if (seenExports.has(entry.exportId)) {
|
||||
errors.push(`duplicate export: ${entry.exportId}`);
|
||||
}
|
||||
seenExports.add(entry.exportId);
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
||||
+1003
-300
File diff suppressed because one or more lines are too long
@@ -1,337 +0,0 @@
|
||||
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated from file camino/options.proto (package camino, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenExtension, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { extDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { FieldOptions, FileOptions, MessageOptions, MethodOptions as MethodOptions$1 } from "@bufbuild/protobuf/wkt";
|
||||
import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt";
|
||||
import type { Cardinality, ConflictStrategy, FieldOps, FieldStorage, InterfaceFieldContract, SymbolRef, TypeBinding } from "./schema_pb.js";
|
||||
import { file_camino_schema } from "./schema_pb.js";
|
||||
import type { FunctionRef } from "../quixos/refs_pb.js";
|
||||
import { file_quixos_refs } from "../quixos/refs_pb.js";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file camino/options.proto.
|
||||
*/
|
||||
export const file_camino_options: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChRjYW1pbm8vb3B0aW9ucy5wcm90bxIGY2FtaW5vIj4KDENsYXNzT3B0aW9ucxIPCgd2ZXJzaW9uGAEgASgNEh0KAmlkGAIgASgLMhEuY2FtaW5vLlN5bWJvbFJlZiJCChBJbnRlcmZhY2VPcHRpb25zEg8KB3ZlcnNpb24YASABKA0SHQoCaWQYAiABKAsyES5jYW1pbm8uU3ltYm9sUmVmImQKEUltcGxlbWVudHNPcHRpb25zEiQKCWludGVyZmFjZRgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSKQoMdHlwZV9iaW5kaW5nGAIgAygLMhMuY2FtaW5vLlR5cGVCaW5kaW5nIjsKEkNvbnN0cnVjdG9yT3B0aW9ucxIlCghmdW5jdGlvbhgBIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZiK6AgoLRWRnZU9wdGlvbnMSHQoCaWQYASABKAsyES5jYW1pbm8uU3ltYm9sUmVmEiEKBnRhcmdldBgCIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSKAoLY2FyZGluYWxpdHkYAyABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHaW52ZXJzZRgEIAEoCRIyCg10aGlzX2VuZHBvaW50GAUgASgLMhsuY2FtaW5vLkVkZ2VFbmRwb2ludE9wdGlvbnMSMwoOb3RoZXJfZW5kcG9pbnQYBiABKAsyGy5jYW1pbm8uRWRnZUVuZHBvaW50T3B0aW9ucxIeCgN0YWcYByADKAsyES5jYW1pbm8uU3ltYm9sUmVmEiUKCmltcGxlbWVudHMYCCADKAsyES5jYW1pbm8uU3ltYm9sUmVmIqwBChNFZGdlRW5kcG9pbnRPcHRpb25zEiAKBWNsYXNzGAEgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIkCglpbnRlcmZhY2UYAiABKAsyES5jYW1pbm8uU3ltYm9sUmVmEhIKCnByb2plY3Rpb24YAyABKAkSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHaW5kZXhlZBgFIAEoCCJECg1NZXRob2RPcHRpb25zEgwKBG5hbWUYASABKAkSJQoIZnVuY3Rpb24YAiABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWYiYwoQTWlncmF0aW9uT3B0aW9ucxIUCgxmcm9tX3ZlcnNpb24YASABKA0SEgoKdG9fdmVyc2lvbhgCIAEoDRIlCghmdW5jdGlvbhgDIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZjpJChBzY2hlbWFfbmFtZXNwYWNlEhwuZ29vZ2xlLnByb3RvYnVmLkZpbGVPcHRpb25zGLiOAyABKAlSD3NjaGVtYU5hbWVzcGFjZTpFCg5zY2hlbWFfdmVyc2lvbhIcLmdvb2dsZS5wcm90b2J1Zi5GaWxlT3B0aW9ucxi5jgMgASgJUg1zY2hlbWFWZXJzaW9uOk0KBWNsYXNzEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGMKOAyABKAsyFC5jYW1pbm8uQ2xhc3NPcHRpb25zUgVjbGFzczpQCgZtZXRob2QSHy5nb29nbGUucHJvdG9idWYuTWVzc2FnZU9wdGlvbnMYw44DIAMoCzIVLmNhbWluby5NZXRob2RPcHRpb25zUgZtZXRob2Q6WQoJbWlncmF0aW9uEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGMSOAyADKAsyGC5jYW1pbm8uTWlncmF0aW9uT3B0aW9uc1IJbWlncmF0aW9uOlkKCWludGVyZmFjZRIfLmdvb2dsZS5wcm90b2J1Zi5NZXNzYWdlT3B0aW9ucxjFjgMgASgLMhguY2FtaW5vLkludGVyZmFjZU9wdGlvbnNSCWludGVyZmFjZTpcCgppbXBsZW1lbnRzEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGMaOAyADKAsyGS5jYW1pbm8uSW1wbGVtZW50c09wdGlvbnNSCmltcGxlbWVudHM6XwoLY29uc3RydWN0b3ISHy5nb29nbGUucHJvdG9idWYuTWVzc2FnZU9wdGlvbnMYx44DIAEoCzIaLmNhbWluby5Db25zdHJ1Y3Rvck9wdGlvbnNSC2NvbnN0cnVjdG9yOlUKCGNvbmZsaWN0Eh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxjMjgMgASgOMhguY2FtaW5vLkNvbmZsaWN0U3RyYXRlZ3lSCGNvbmZsaWN0OkgKBGVkZ2USHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGM2OAyABKAsyEy5jYW1pbm8uRWRnZU9wdGlvbnNSBGVkZ2U6WgoNZmllbGRfc3RvcmFnZRIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMYzo4DIAEoCzIULmNhbWluby5GaWVsZFN0b3JhZ2VSDGZpZWxkU3RvcmFnZTpOCglmaWVsZF9vcHMSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGM+OAyABKAsyEC5jYW1pbm8uRmllbGRPcHNSCGZpZWxkT3BzOmgKD2ludGVyZmFjZV9maWVsZBIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMY0I4DIAEoCzIeLmNhbWluby5JbnRlcmZhY2VGaWVsZENvbnRyYWN0Ug5pbnRlcmZhY2VGaWVsZDpECg1kaXNwbGF5X2xhYmVsEh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxjRjgMgASgIUgxkaXNwbGF5TGFiZWw6SQoEaW1wbBIeLmdvb2dsZS5wcm90b2J1Zi5NZXRob2RPcHRpb25zGNaOAyABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWZSBGltcGxiBnByb3RvMw", [file_google_protobuf_descriptor, file_camino_schema, file_quixos_refs]);
|
||||
|
||||
/**
|
||||
* @generated from message camino.ClassOptions
|
||||
*/
|
||||
export type ClassOptions = Message<"camino.ClassOptions"> & {
|
||||
/**
|
||||
* @generated from field: uint32 version = 1;
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef id = 2;
|
||||
*/
|
||||
id?: SymbolRef | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.ClassOptions.
|
||||
* Use `create(ClassOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const ClassOptionsSchema: GenMessage<ClassOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 0);
|
||||
|
||||
/**
|
||||
* @generated from message camino.InterfaceOptions
|
||||
*/
|
||||
export type InterfaceOptions = Message<"camino.InterfaceOptions"> & {
|
||||
/**
|
||||
* @generated from field: uint32 version = 1;
|
||||
*/
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef id = 2;
|
||||
*/
|
||||
id?: SymbolRef | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.InterfaceOptions.
|
||||
* Use `create(InterfaceOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const InterfaceOptionsSchema: GenMessage<InterfaceOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 1);
|
||||
|
||||
/**
|
||||
* @generated from message camino.ImplementsOptions
|
||||
*/
|
||||
export type ImplementsOptions = Message<"camino.ImplementsOptions"> & {
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef interface = 1;
|
||||
*/
|
||||
interface?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated camino.TypeBinding type_binding = 2;
|
||||
*/
|
||||
typeBinding: TypeBinding[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.ImplementsOptions.
|
||||
* Use `create(ImplementsOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const ImplementsOptionsSchema: GenMessage<ImplementsOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 2);
|
||||
|
||||
/**
|
||||
* @generated from message camino.ConstructorOptions
|
||||
*/
|
||||
export type ConstructorOptions = Message<"camino.ConstructorOptions"> & {
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 1;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.ConstructorOptions.
|
||||
* Use `create(ConstructorOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const ConstructorOptionsSchema: GenMessage<ConstructorOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 3);
|
||||
|
||||
/**
|
||||
* @generated from message camino.EdgeOptions
|
||||
*/
|
||||
export type EdgeOptions = Message<"camino.EdgeOptions"> & {
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef id = 1;
|
||||
*/
|
||||
id?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* Shorthand for other_endpoint.class.
|
||||
*
|
||||
* @generated from field: camino.SymbolRef target = 2;
|
||||
*/
|
||||
target?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* Shorthand for this_endpoint.cardinality.
|
||||
*
|
||||
* @generated from field: camino.Cardinality cardinality = 3;
|
||||
*/
|
||||
cardinality: Cardinality;
|
||||
|
||||
/**
|
||||
* Shorthand for other_endpoint.projection.
|
||||
*
|
||||
* @generated from field: string inverse = 4;
|
||||
*/
|
||||
inverse: string;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.EdgeEndpointOptions this_endpoint = 5;
|
||||
*/
|
||||
thisEndpoint?: EdgeEndpointOptions | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.EdgeEndpointOptions other_endpoint = 6;
|
||||
*/
|
||||
otherEndpoint?: EdgeEndpointOptions | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated camino.SymbolRef tag = 7;
|
||||
*/
|
||||
tag: SymbolRef[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated camino.SymbolRef implements = 8;
|
||||
*/
|
||||
implements: SymbolRef[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.EdgeOptions.
|
||||
* Use `create(EdgeOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const EdgeOptionsSchema: GenMessage<EdgeOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 4);
|
||||
|
||||
/**
|
||||
* @generated from message camino.EdgeEndpointOptions
|
||||
*/
|
||||
export type EdgeEndpointOptions = Message<"camino.EdgeEndpointOptions"> & {
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef class = 1;
|
||||
*/
|
||||
class?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef interface = 2;
|
||||
*/
|
||||
interface?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string projection = 3;
|
||||
*/
|
||||
projection: string;
|
||||
|
||||
/**
|
||||
* @generated from field: camino.Cardinality cardinality = 4;
|
||||
*/
|
||||
cardinality: Cardinality;
|
||||
|
||||
/**
|
||||
* @generated from field: bool indexed = 5;
|
||||
*/
|
||||
indexed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.EdgeEndpointOptions.
|
||||
* Use `create(EdgeEndpointOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const EdgeEndpointOptionsSchema: GenMessage<EdgeEndpointOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 5);
|
||||
|
||||
/**
|
||||
* @generated from message camino.MethodOptions
|
||||
*/
|
||||
export type MethodOptions = Message<"camino.MethodOptions"> & {
|
||||
/**
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 2;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.MethodOptions.
|
||||
* Use `create(MethodOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const MethodOptionsSchema: GenMessage<MethodOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 6);
|
||||
|
||||
/**
|
||||
* @generated from message camino.MigrationOptions
|
||||
*/
|
||||
export type MigrationOptions = Message<"camino.MigrationOptions"> & {
|
||||
/**
|
||||
* @generated from field: uint32 from_version = 1;
|
||||
*/
|
||||
fromVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 to_version = 2;
|
||||
*/
|
||||
toVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 3;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message camino.MigrationOptions.
|
||||
* Use `create(MigrationOptionsSchema)` to create a new message.
|
||||
*/
|
||||
export const MigrationOptionsSchema: GenMessage<MigrationOptions> = /*@__PURE__*/
|
||||
messageDesc(file_camino_options, 7);
|
||||
|
||||
/**
|
||||
* @generated from extension: string schema_namespace = 51000;
|
||||
*/
|
||||
export const schema_namespace: GenExtension<FileOptions, string> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 0);
|
||||
|
||||
/**
|
||||
* @generated from extension: string schema_version = 51001;
|
||||
*/
|
||||
export const schema_version: GenExtension<FileOptions, string> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 1);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.ClassOptions class = 51010;
|
||||
*/
|
||||
export const class$: GenExtension<MessageOptions, ClassOptions> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 2);
|
||||
|
||||
/**
|
||||
* @generated from extension: repeated camino.MethodOptions method = 51011;
|
||||
*/
|
||||
export const method: GenExtension<MessageOptions, MethodOptions[]> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 3);
|
||||
|
||||
/**
|
||||
* @generated from extension: repeated camino.MigrationOptions migration = 51012;
|
||||
*/
|
||||
export const migration: GenExtension<MessageOptions, MigrationOptions[]> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 4);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.InterfaceOptions interface = 51013;
|
||||
*/
|
||||
export const interface$: GenExtension<MessageOptions, InterfaceOptions> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 5);
|
||||
|
||||
/**
|
||||
* @generated from extension: repeated camino.ImplementsOptions implements = 51014;
|
||||
*/
|
||||
export const implements$: GenExtension<MessageOptions, ImplementsOptions[]> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 6);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.ConstructorOptions constructor = 51015;
|
||||
*/
|
||||
export const constructor: GenExtension<MessageOptions, ConstructorOptions> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 7);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.ConflictStrategy conflict = 51020;
|
||||
*/
|
||||
export const conflict: GenExtension<FieldOptions, ConflictStrategy> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 8);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.EdgeOptions edge = 51021;
|
||||
*/
|
||||
export const edge: GenExtension<FieldOptions, EdgeOptions> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 9);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.FieldStorage field_storage = 51022;
|
||||
*/
|
||||
export const field_storage: GenExtension<FieldOptions, FieldStorage> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 10);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.FieldOps field_ops = 51023;
|
||||
*/
|
||||
export const field_ops: GenExtension<FieldOptions, FieldOps> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 11);
|
||||
|
||||
/**
|
||||
* @generated from extension: camino.InterfaceFieldContract interface_field = 51024;
|
||||
*/
|
||||
export const interface_field: GenExtension<FieldOptions, InterfaceFieldContract> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 12);
|
||||
|
||||
/**
|
||||
* @generated from extension: bool display_label = 51025;
|
||||
*/
|
||||
export const display_label: GenExtension<FieldOptions, boolean> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 13);
|
||||
|
||||
/**
|
||||
* @generated from extension: quixos.FunctionRef impl = 51030;
|
||||
*/
|
||||
export const impl: GenExtension<MethodOptions$1, FunctionRef> = /*@__PURE__*/
|
||||
extDesc(file_camino_options, 14);
|
||||
|
||||
+1286
-682
File diff suppressed because one or more lines are too long
+679
-78
File diff suppressed because one or more lines are too long
+33
-283
@@ -1,131 +1,40 @@
|
||||
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated from file quixos/package.proto (package quixos, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { SymbolRef } from "../camino/schema_pb.js";
|
||||
import { file_camino_schema } from "../camino/schema_pb.js";
|
||||
import type { FunctionRef } from "./refs_pb.js";
|
||||
import { file_quixos_refs } from "./refs_pb.js";
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file quixos/package.proto.
|
||||
*/
|
||||
export const file_quixos_package: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChRxdWl4b3MvcGFja2FnZS5wcm90bxIGcXVpeG9zIm4KClBhY2thZ2VSZWYSGQoRcGFja2FnZV9uYW1lc3BhY2UYASABKAkSFAoMcGFja2FnZV9uYW1lGAIgASgJEhoKEmRlc2NyaXB0b3JfdmVyc2lvbhgDIAEoDRITCgt2ZXJzaW9uX3JlZhgEIAEoCSKgBAoRUGFja2FnZURlc2NyaXB0b3ISGQoRcGFja2FnZV9uYW1lc3BhY2UYASABKAkSFAoMcGFja2FnZV9uYW1lGAIgASgJEhoKEmRlc2NyaXB0b3JfdmVyc2lvbhgDIAEoDRIkChxpbnRlcmZhY2VfY29tcGF0aWJsZV9iYWNrX3RvGAQgASgNEiEKGXJ1bm5lcl9jb21wYXRpYmxlX2JhY2tfdG8YBSABKA0SEwoLc291cmNlX3JlcG8YBiABKAkSEgoKc291cmNlX3JlZhgHIAEoCRIbChNyZXNvbHZlZF9zb3VyY2VfcmVmGAggASgJEhoKEnNlcnZlcl9pbnN0YWxsYWJsZRgJIAEoCRIgChhydW50aW1lX3Byb3RvY29sX3ZlcnNpb24YCiABKAkSKgoNY2xhc3NfZXhwb3J0cxgLIAMoCzITLnF1aXhvcy5DbGFzc0V4cG9ydBIwChBmdW5jdGlvbl9leHBvcnRzGAwgAygLMhYucXVpeG9zLkZ1bmN0aW9uRXhwb3J0EjIKEWNvbXBvbmVudF9leHBvcnRzGA0gAygLMhcucXVpeG9zLkNvbXBvbmVudEV4cG9ydBIuCg9zaWRlY2FyX2V4cG9ydHMYDiADKAsyFS5xdWl4b3MuU2lkZWNhckV4cG9ydBIvCgxkZXBlbmRlbmNpZXMYDyADKAsyGS5xdWl4b3MuUGFja2FnZURlcGVuZGVuY3kiRwoLQ2xhc3NFeHBvcnQSIwoIY2xhc3NfaWQYASABKAsyES5jYW1pbm8uU3ltYm9sUmVmEhMKC3NvdXJjZV9maWxlGAIgASgJItoBCg5GdW5jdGlvbkV4cG9ydBIlCghmdW5jdGlvbhgBIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZhISCgppbnB1dF90eXBlGAIgASgJEhMKC291dHB1dF90eXBlGAMgASgJEhkKEWludGVyZmFjZV92ZXJzaW9uGAQgASgNEiQKHGludGVyZmFjZV9jb21wYXRpYmxlX2JhY2tfdG8YBSABKA0SNwoPY2FwYWJpbGl0eV9raW5kGAYgASgOMh4ucXVpeG9zLkZ1bmN0aW9uQ2FwYWJpbGl0eUtpbmQiYwoPQ29tcG9uZW50RXhwb3J0Eg4KBnN5bWJvbBgBIAEoCRISCgpwcm9wc190eXBlGAIgASgJEiwKEXN1cHBvcnRlZF9jbGFzc2VzGAMgAygLMhEuY2FtaW5vLlN5bWJvbFJlZiI5Cg1TaWRlY2FyRXhwb3J0Eg4KBnN5bWJvbBgBIAEoCRIYChBzaWRlX2VmZmVjdF9tb2RlGAIgASgJIpUBChFQYWNrYWdlRGVwZW5kZW5jeRIjCgdwYWNrYWdlGAEgASgLMhIucXVpeG9zLlBhY2thZ2VSZWYSJAocaW50ZXJmYWNlX2NvbXBhdGlibGVfYmFja190bxgCIAEoDRIZChFyZWZhY3Rvcl9yZXF1aXJlZBgDIAEoCBIaChJjb21wYXRpYmlsaXR5X25vdGUYBCABKAkqggEKFkZ1bmN0aW9uQ2FwYWJpbGl0eUtpbmQSKAokRlVOQ1RJT05fQ0FQQUJJTElUWV9LSU5EX1VOU1BFQ0lGSUVEEAASCgoGTUVUSE9EEAESEgoORklFTERfUkVTT0xWRVIQAhINCglNSUdSQVRJT04QAxIPCgtDT05TVFJVQ1RPUhAEYgZwcm90bzM", [file_camino_schema, file_quixos_refs]);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageRef
|
||||
*/
|
||||
export type PackageRef = Message<"quixos.PackageRef"> & {
|
||||
/**
|
||||
* @generated from field: string package_namespace = 1;
|
||||
*/
|
||||
packageNamespace: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string package_name = 2;
|
||||
*/
|
||||
packageName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 descriptor_version = 3;
|
||||
*/
|
||||
descriptorVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string version_ref = 4;
|
||||
*/
|
||||
versionRef: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.PackageRef.
|
||||
* Use `create(PackageRefSchema)` to create a new message.
|
||||
*/
|
||||
export const PackageRefSchema: GenMessage<PackageRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 0);
|
||||
fileDesc("ChRxdWl4b3MvcGFja2FnZS5wcm90bxIGcXVpeG9zIo4BChFQYWNrYWdlRGVzY3JpcHRvchISCgpwYWNrYWdlX2lkGAEgASgJEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYAiABKAkSIAoYcnVudGltZV9wcm90b2NvbF92ZXJzaW9uGAMgASgJEiYKB2V4cG9ydHMYBCADKAsyFS5xdWl4b3MuUnVudGltZUV4cG9ydCI6Cg1SdW50aW1lRXhwb3J0EhEKCWV4cG9ydF9pZBgBIAEoCRIWCg5ydW50aW1lX3N5bWJvbBgCIAEoCWIGcHJvdG8z");
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageDescriptor
|
||||
*/
|
||||
export type PackageDescriptor = Message<"quixos.PackageDescriptor"> & {
|
||||
/**
|
||||
* @generated from field: string package_namespace = 1;
|
||||
* @generated from field: string package_id = 1;
|
||||
*/
|
||||
packageNamespace: string;
|
||||
packageId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string package_name = 2;
|
||||
* @generated from field: string package_revision_id = 2;
|
||||
*/
|
||||
packageName: string;
|
||||
packageRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 descriptor_version = 3;
|
||||
*/
|
||||
descriptorVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 interface_compatible_back_to = 4;
|
||||
*/
|
||||
interfaceCompatibleBackTo: number;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 runner_compatible_back_to = 5;
|
||||
*/
|
||||
runnerCompatibleBackTo: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string source_repo = 6;
|
||||
*/
|
||||
sourceRepo: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string source_ref = 7;
|
||||
*/
|
||||
sourceRef: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string resolved_source_ref = 8;
|
||||
*/
|
||||
resolvedSourceRef: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string server_installable = 9;
|
||||
*/
|
||||
serverInstallable: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string runtime_protocol_version = 10;
|
||||
* @generated from field: string runtime_protocol_version = 3;
|
||||
*/
|
||||
runtimeProtocolVersion: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.ClassExport class_exports = 11;
|
||||
* @generated from field: repeated quixos.RuntimeExport exports = 4;
|
||||
*/
|
||||
classExports: ClassExport[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.FunctionExport function_exports = 12;
|
||||
*/
|
||||
functionExports: FunctionExport[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.ComponentExport component_exports = 13;
|
||||
*/
|
||||
componentExports: ComponentExport[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.SidecarExport sidecar_exports = 14;
|
||||
*/
|
||||
sidecarExports: SidecarExport[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.PackageDependency dependencies = 15;
|
||||
*/
|
||||
dependencies: PackageDependency[];
|
||||
exports: RuntimeExport[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -133,186 +42,27 @@ export type PackageDescriptor = Message<"quixos.PackageDescriptor"> & {
|
||||
* Use `create(PackageDescriptorSchema)` to create a new message.
|
||||
*/
|
||||
export const PackageDescriptorSchema: GenMessage<PackageDescriptor> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 0);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.RuntimeExport
|
||||
*/
|
||||
export type RuntimeExport = Message<"quixos.RuntimeExport"> & {
|
||||
/**
|
||||
* @generated from field: string export_id = 1;
|
||||
*/
|
||||
exportId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string runtime_symbol = 2;
|
||||
*/
|
||||
runtimeSymbol: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.RuntimeExport.
|
||||
* Use `create(RuntimeExportSchema)` to create a new message.
|
||||
*/
|
||||
export const RuntimeExportSchema: GenMessage<RuntimeExport> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 1);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.ClassExport
|
||||
*/
|
||||
export type ClassExport = Message<"quixos.ClassExport"> & {
|
||||
/**
|
||||
* @generated from field: camino.SymbolRef class_id = 1;
|
||||
*/
|
||||
classId?: SymbolRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string source_file = 2;
|
||||
*/
|
||||
sourceFile: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.ClassExport.
|
||||
* Use `create(ClassExportSchema)` to create a new message.
|
||||
*/
|
||||
export const ClassExportSchema: GenMessage<ClassExport> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 2);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.FunctionExport
|
||||
*/
|
||||
export type FunctionExport = Message<"quixos.FunctionExport"> & {
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 1;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string input_type = 2;
|
||||
*/
|
||||
inputType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string output_type = 3;
|
||||
*/
|
||||
outputType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 interface_version = 4;
|
||||
*/
|
||||
interfaceVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 interface_compatible_back_to = 5;
|
||||
*/
|
||||
interfaceCompatibleBackTo: number;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FunctionCapabilityKind capability_kind = 6;
|
||||
*/
|
||||
capabilityKind: FunctionCapabilityKind;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.FunctionExport.
|
||||
* Use `create(FunctionExportSchema)` to create a new message.
|
||||
*/
|
||||
export const FunctionExportSchema: GenMessage<FunctionExport> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 3);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.ComponentExport
|
||||
*/
|
||||
export type ComponentExport = Message<"quixos.ComponentExport"> & {
|
||||
/**
|
||||
* @generated from field: string symbol = 1;
|
||||
*/
|
||||
symbol: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string props_type = 2;
|
||||
*/
|
||||
propsType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated camino.SymbolRef supported_classes = 3;
|
||||
*/
|
||||
supportedClasses: SymbolRef[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.ComponentExport.
|
||||
* Use `create(ComponentExportSchema)` to create a new message.
|
||||
*/
|
||||
export const ComponentExportSchema: GenMessage<ComponentExport> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 4);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.SidecarExport
|
||||
*/
|
||||
export type SidecarExport = Message<"quixos.SidecarExport"> & {
|
||||
/**
|
||||
* @generated from field: string symbol = 1;
|
||||
*/
|
||||
symbol: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string side_effect_mode = 2;
|
||||
*/
|
||||
sideEffectMode: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.SidecarExport.
|
||||
* Use `create(SidecarExportSchema)` to create a new message.
|
||||
*/
|
||||
export const SidecarExportSchema: GenMessage<SidecarExport> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 5);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageDependency
|
||||
*/
|
||||
export type PackageDependency = Message<"quixos.PackageDependency"> & {
|
||||
/**
|
||||
* @generated from field: quixos.PackageRef package = 1;
|
||||
*/
|
||||
package?: PackageRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 interface_compatible_back_to = 2;
|
||||
*/
|
||||
interfaceCompatibleBackTo: number;
|
||||
|
||||
/**
|
||||
* @generated from field: bool refactor_required = 3;
|
||||
*/
|
||||
refactorRequired: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: string compatibility_note = 4;
|
||||
*/
|
||||
compatibilityNote: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.PackageDependency.
|
||||
* Use `create(PackageDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const PackageDependencySchema: GenMessage<PackageDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_package, 6);
|
||||
|
||||
/**
|
||||
* @generated from enum quixos.FunctionCapabilityKind
|
||||
*/
|
||||
export enum FunctionCapabilityKind {
|
||||
/**
|
||||
* @generated from enum value: FUNCTION_CAPABILITY_KIND_UNSPECIFIED = 0;
|
||||
*/
|
||||
FUNCTION_CAPABILITY_KIND_UNSPECIFIED = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: METHOD = 1;
|
||||
*/
|
||||
METHOD = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: FIELD_RESOLVER = 2;
|
||||
*/
|
||||
FIELD_RESOLVER = 2,
|
||||
|
||||
/**
|
||||
* @generated from enum value: MIGRATION = 3;
|
||||
*/
|
||||
MIGRATION = 3,
|
||||
|
||||
/**
|
||||
* @generated from enum value: CONSTRUCTOR = 4;
|
||||
*/
|
||||
CONSTRUCTOR = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum quixos.FunctionCapabilityKind.
|
||||
*/
|
||||
export const FunctionCapabilityKindSchema: GenEnum<FunctionCapabilityKind> = /*@__PURE__*/
|
||||
enumDesc(file_quixos_package, 0);
|
||||
|
||||
|
||||
+156
-23
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated from file quixos/refs.proto (package quixos, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
@@ -10,42 +10,175 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file quixos/refs.proto.
|
||||
*/
|
||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInYKC0Z1bmN0aW9uUmVmEhkKEXBhY2thZ2VfbmFtZXNwYWNlGAEgASgJEhQKDHBhY2thZ2VfbmFtZRgCIAEoCRIOCgZzeW1ib2wYAyABKAkSEwoLdmVyc2lvbl9yZWYYBCABKAkSEQoJb3BlcmF0aW9uGAUgASgJYgZwcm90bzM");
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLYAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEgoIcXVlcnlfaWQYByABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
|
||||
|
||||
/**
|
||||
* @generated from message quixos.FunctionRef
|
||||
* @generated from message quixos.CapabilityRef
|
||||
*/
|
||||
export type FunctionRef = Message<"quixos.FunctionRef"> & {
|
||||
export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
|
||||
/**
|
||||
* @generated from field: string package_namespace = 1;
|
||||
* @generated from field: string interface_revision_id = 1;
|
||||
*/
|
||||
packageNamespace: string;
|
||||
interfaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string package_name = 2;
|
||||
* @generated from field: string operation_id = 2;
|
||||
*/
|
||||
packageName: string;
|
||||
operationId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string symbol = 3;
|
||||
* Optional fence for a view acquired through TryConform. Not an authority grant.
|
||||
*
|
||||
* @generated from field: quixos.ConformanceWitness conformance = 3;
|
||||
*/
|
||||
symbol: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string version_ref = 4;
|
||||
*/
|
||||
versionRef: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string operation = 5;
|
||||
*/
|
||||
operation: string;
|
||||
conformance?: ConformanceWitness | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.FunctionRef.
|
||||
* Use `create(FunctionRefSchema)` to create a new message.
|
||||
* Describes the message quixos.CapabilityRef.
|
||||
* Use `create(CapabilityRefSchema)` to create a new message.
|
||||
*/
|
||||
export const FunctionRefSchema: GenMessage<FunctionRef> = /*@__PURE__*/
|
||||
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 0);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.ConformanceWitness
|
||||
*/
|
||||
export type ConformanceWitness = Message<"quixos.ConformanceWitness"> & {
|
||||
/**
|
||||
* @generated from field: string object_id = 1;
|
||||
*/
|
||||
objectId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string interface_revision_id = 2;
|
||||
*/
|
||||
interfaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string conformance_id = 3;
|
||||
*/
|
||||
conformanceId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_revision_id = 4;
|
||||
*/
|
||||
workspaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_epoch = 5;
|
||||
*/
|
||||
workspaceEpoch: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.ConformanceWitness.
|
||||
* Use `create(ConformanceWitnessSchema)` to create a new message.
|
||||
*/
|
||||
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 1);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageExportRef
|
||||
*/
|
||||
export type PackageExportRef = Message<"quixos.PackageExportRef"> & {
|
||||
/**
|
||||
* @generated from field: string package_revision_id = 1;
|
||||
*/
|
||||
packageRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string export_id = 2;
|
||||
*/
|
||||
exportId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.PackageExportRef.
|
||||
* Use `create(PackageExportRefSchema)` to create a new message.
|
||||
*/
|
||||
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 2);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.InjectedDependency
|
||||
*/
|
||||
export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
||||
/**
|
||||
* @generated from field: string port_id = 1;
|
||||
*/
|
||||
portId: string;
|
||||
|
||||
/**
|
||||
* @generated from oneof quixos.InjectedDependency.binding
|
||||
*/
|
||||
binding: {
|
||||
/**
|
||||
* @generated from field: string state_slot_id = 2;
|
||||
*/
|
||||
value: string;
|
||||
case: "stateSlotId";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: quixos.EdgeDependency edge = 3;
|
||||
*/
|
||||
value: EdgeDependency;
|
||||
case: "edge";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: string interface_revision_id = 4;
|
||||
*/
|
||||
value: string;
|
||||
case: "interfaceRevisionId";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: string constructor_atom_id = 5;
|
||||
*/
|
||||
value: string;
|
||||
case: "constructorAtomId";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: string query_id = 7;
|
||||
*/
|
||||
value: string;
|
||||
case: "queryId";
|
||||
} | { case: undefined; value?: undefined };
|
||||
|
||||
/**
|
||||
* Defaults to the invocation receiver. A checked dependency traversal can
|
||||
* select a related object explicitly before the package runtime starts.
|
||||
*
|
||||
* @generated from field: string object_id = 6;
|
||||
*/
|
||||
objectId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.InjectedDependency.
|
||||
* Use `create(InjectedDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 3);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.EdgeDependency
|
||||
*/
|
||||
export type EdgeDependency = Message<"quixos.EdgeDependency"> & {
|
||||
/**
|
||||
* @generated from field: string edge_type_id = 1;
|
||||
*/
|
||||
edgeTypeId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string projection_id = 2;
|
||||
*/
|
||||
projectionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.EdgeDependency.
|
||||
* Use `create(EdgeDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 4);
|
||||
|
||||
|
||||
+167
-26
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
|
||||
// @generated from file quixos/runtime.proto (package quixos.runtime, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegen
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Value } from "../camino/api_pb.js";
|
||||
import { file_camino_api } from "../camino/api_pb.js";
|
||||
import type { FunctionRef } from "./refs_pb.js";
|
||||
import type { InjectedDependency, PackageExportRef } from "./refs_pb.js";
|
||||
import { file_quixos_refs } from "./refs_pb.js";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file quixos/runtime.proto.
|
||||
*/
|
||||
export const file_quixos_runtime: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChRxdWl4b3MvcnVudGltZS5wcm90bxIOcXVpeG9zLnJ1bnRpbWUiMQoQSGFuZHNoYWtlUmVxdWVzdBIdChVvcmNoX3Byb3RvY29sX3ZlcnNpb24YASABKAkijgEKEUhhbmRzaGFrZVJlc3BvbnNlEhkKEXBhY2thZ2VfbmFtZXNwYWNlGAEgASgJEhQKDHBhY2thZ2VfbmFtZRgCIAEoCRIgChhydW50aW1lX3Byb3RvY29sX3ZlcnNpb24YAyABKAkSJgoJZnVuY3Rpb25zGAQgAygLMhMucXVpeG9zLkZ1bmN0aW9uUmVmItYBCg1JbnZva2VSZXF1ZXN0EhUKDWludm9jYXRpb25faWQYASABKAkSJQoIZnVuY3Rpb24YAiABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWYSEQoJb2JqZWN0X2lkGAMgASgJEjcKBWlucHV0GAQgAygLMigucXVpeG9zLnJ1bnRpbWUuSW52b2tlUmVxdWVzdC5JbnB1dEVudHJ5GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASJKCg5JbnZva2VSZXNwb25zZRIKCgJvaxgBIAEoCBIdCgZyZXN1bHQYAiABKAsyDS5jYW1pbm8uVmFsdWUSDQoFZXJyb3IYAyABKAki1AEKDFdhdGNoUmVxdWVzdBIVCg1pbnZvY2F0aW9uX2lkGAEgASgJEiUKCGZ1bmN0aW9uGAIgASgLMhMucXVpeG9zLkZ1bmN0aW9uUmVmEhEKCW9iamVjdF9pZBgDIAEoCRI2CgVpbnB1dBgEIAMoCzInLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdC5JbnB1dEVudHJ5GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASJeChFEZXJpdmVkRGVwZW5kZW5jeRIMCgRraW5kGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRISCgpmaWVsZF9uYW1lGAMgASgJEhQKDHNvdXJjZV9maWVsZBgEIAEoCSKVAQoKV2F0Y2hFdmVudBIQCgh3YXRjaF9pZBgBIAEoCRIcCgV2YWx1ZRgCIAEoCzINLmNhbWluby5WYWx1ZRI3CgxkZXBlbmRlbmNpZXMYAyADKAsyIS5xdWl4b3MucnVudGltZS5EZXJpdmVkRGVwZW5kZW5jeRINCgVlcnJvchgEIAEoCRIPCgdpbml0aWFsGAUgASgIMvABCg5QYWNrYWdlUnVudGltZRJQCglIYW5kc2hha2USIC5xdWl4b3MucnVudGltZS5IYW5kc2hha2VSZXF1ZXN0GiEucXVpeG9zLnJ1bnRpbWUuSGFuZHNoYWtlUmVzcG9uc2USRwoGSW52b2tlEh0ucXVpeG9zLnJ1bnRpbWUuSW52b2tlUmVxdWVzdBoeLnF1aXhvcy5ydW50aW1lLkludm9rZVJlc3BvbnNlEkMKBVdhdGNoEhwucXVpeG9zLnJ1bnRpbWUuV2F0Y2hSZXF1ZXN0GhoucXVpeG9zLnJ1bnRpbWUuV2F0Y2hFdmVudDABYgZwcm90bzM", [file_camino_api, file_quixos_refs]);
|
||||
fileDesc("ChRxdWl4b3MvcnVudGltZS5wcm90bxIOcXVpeG9zLnJ1bnRpbWUiQAoQSGFuZHNoYWtlUmVxdWVzdBIdChVvcmNoX3Byb3RvY29sX3ZlcnNpb24YASABKAkSDQoFbm9uY2UYAiABKAkirwEKEUhhbmRzaGFrZVJlc3BvbnNlEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSIAoYcnVudGltZV9wcm90b2NvbF92ZXJzaW9uGAIgASgJEhIKCmV4cG9ydF9pZHMYAyADKAkSEwoLaW5zdGFuY2VfaWQYBCABKAkSHAoUYXV0aGVudGljYXRpb25fcHJvb2YYBSABKAkSFAoMY2FwYWJpbGl0aWVzGAYgAygJIpoBChFJbnZvY2F0aW9uQ29udGV4dBIXCg93b3Jrc3BhY2VfZXBvY2gYASABKAkSEwoLaW5zdGFuY2VfaWQYAiABKAkSFgoOYmluZGluZ19kaWdlc3QYAyABKAkSDQoFZ3JhbnQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIcChRvd25lcl9jb25mb3JtYW5jZV9pZBgGIAEoCSIxChhJbnZvY2F0aW9uQ29udHJvbFJlcXVlc3QSFQoNaW52b2NhdGlvbl9pZBgBIAEoCSI4ChBJbnZvY2F0aW9uU3RhdHVzEhUKDWludm9jYXRpb25faWQYASABKAkSDQoFc3RhdGUYAiABKAkivwIKDUludm9rZVJlcXVlc3QSFQoNaW52b2NhdGlvbl9pZBgBIAEoCRIoCgZleHBvcnQYAiABKAsyGC5xdWl4b3MuUGFja2FnZUV4cG9ydFJlZhIRCglvYmplY3RfaWQYAyABKAkSNwoFaW5wdXQYBCADKAsyKC5xdWl4b3MucnVudGltZS5JbnZva2VSZXF1ZXN0LklucHV0RW50cnkSMAoMZGVwZW5kZW5jaWVzGAUgAygLMhoucXVpeG9zLkluamVjdGVkRGVwZW5kZW5jeRIyCgdjb250ZXh0GAYgASgLMiEucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRleHQaOwoKSW5wdXRFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIoMBCg5JbnZva2VSZXNwb25zZRIKCgJvaxgBIAEoCBIdCgZyZXN1bHQYAiABKAsyDS5jYW1pbm8uVmFsdWUSDQoFZXJyb3IYAyABKAkSNwoMZGVwZW5kZW5jaWVzGAQgAygLMiEucXVpeG9zLnJ1bnRpbWUuRGVyaXZlZERlcGVuZGVuY3kivQIKDFdhdGNoUmVxdWVzdBIVCg1pbnZvY2F0aW9uX2lkGAEgASgJEigKBmV4cG9ydBgCIAEoCzIYLnF1aXhvcy5QYWNrYWdlRXhwb3J0UmVmEhEKCW9iamVjdF9pZBgDIAEoCRI2CgVpbnB1dBgEIAMoCzInLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdC5JbnB1dEVudHJ5EjAKDGRlcGVuZGVuY2llcxgFIAMoCzIaLnF1aXhvcy5JbmplY3RlZERlcGVuZGVuY3kSMgoHY29udGV4dBgGIAEoCzIhLnF1aXhvcy5ydW50aW1lLkludm9jYXRpb25Db250ZXh0GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASJiChFEZXJpdmVkRGVwZW5kZW5jeRIMCgRraW5kGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIVCg1hdHRhY2htZW50X2lkGAMgASgJEhUKDXByb2plY3Rpb25faWQYBCABKAkilQEKCldhdGNoRXZlbnQSEAoId2F0Y2hfaWQYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWUSNwoMZGVwZW5kZW5jaWVzGAMgAygLMiEucXVpeG9zLnJ1bnRpbWUuRGVyaXZlZERlcGVuZGVuY3kSDQoFZXJyb3IYBCABKAkSDwoHaW5pdGlhbBgFIAEoCDKzAwoOUGFja2FnZVJ1bnRpbWUSUAoJSGFuZHNoYWtlEiAucXVpeG9zLnJ1bnRpbWUuSGFuZHNoYWtlUmVxdWVzdBohLnF1aXhvcy5ydW50aW1lLkhhbmRzaGFrZVJlc3BvbnNlEkcKBkludm9rZRIdLnF1aXhvcy5ydW50aW1lLkludm9rZVJlcXVlc3QaHi5xdWl4b3MucnVudGltZS5JbnZva2VSZXNwb25zZRJDCgVXYXRjaBIcLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdBoaLnF1aXhvcy5ydW50aW1lLldhdGNoRXZlbnQwARJhChNHZXRJbnZvY2F0aW9uU3RhdHVzEigucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRyb2xSZXF1ZXN0GiAucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvblN0YXR1cxJeChBDYW5jZWxJbnZvY2F0aW9uEigucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRyb2xSZXF1ZXN0GiAucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvblN0YXR1c2IGcHJvdG8z", [file_camino_api, file_quixos_refs]);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.HandshakeRequest
|
||||
@@ -24,6 +24,11 @@ export type HandshakeRequest = Message<"quixos.runtime.HandshakeRequest"> & {
|
||||
* @generated from field: string orch_protocol_version = 1;
|
||||
*/
|
||||
orchProtocolVersion: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string nonce = 2;
|
||||
*/
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,24 +43,34 @@ export const HandshakeRequestSchema: GenMessage<HandshakeRequest> = /*@__PURE__*
|
||||
*/
|
||||
export type HandshakeResponse = Message<"quixos.runtime.HandshakeResponse"> & {
|
||||
/**
|
||||
* @generated from field: string package_namespace = 1;
|
||||
* @generated from field: string package_revision_id = 1;
|
||||
*/
|
||||
packageNamespace: string;
|
||||
packageRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string package_name = 2;
|
||||
*/
|
||||
packageName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string runtime_protocol_version = 3;
|
||||
* @generated from field: string runtime_protocol_version = 2;
|
||||
*/
|
||||
runtimeProtocolVersion: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.FunctionRef functions = 4;
|
||||
* @generated from field: repeated string export_ids = 3;
|
||||
*/
|
||||
functions: FunctionRef[];
|
||||
exportIds: string[];
|
||||
|
||||
/**
|
||||
* @generated from field: string instance_id = 4;
|
||||
*/
|
||||
instanceId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string authentication_proof = 5;
|
||||
*/
|
||||
authenticationProof: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string capabilities = 6;
|
||||
*/
|
||||
capabilities: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -65,6 +80,91 @@ export type HandshakeResponse = Message<"quixos.runtime.HandshakeResponse"> & {
|
||||
export const HandshakeResponseSchema: GenMessage<HandshakeResponse> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 1);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.InvocationContext
|
||||
*/
|
||||
export type InvocationContext = Message<"quixos.runtime.InvocationContext"> & {
|
||||
/**
|
||||
* @generated from field: string workspace_epoch = 1;
|
||||
*/
|
||||
workspaceEpoch: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string instance_id = 2;
|
||||
*/
|
||||
instanceId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string binding_digest = 3;
|
||||
*/
|
||||
bindingDigest: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string grant = 4;
|
||||
*/
|
||||
grant: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string session_id = 5;
|
||||
*/
|
||||
sessionId: string;
|
||||
|
||||
/**
|
||||
* Host-selected owner; packages must not invent workspace-local ownership.
|
||||
*
|
||||
* @generated from field: string owner_conformance_id = 6;
|
||||
*/
|
||||
ownerConformanceId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.runtime.InvocationContext.
|
||||
* Use `create(InvocationContextSchema)` to create a new message.
|
||||
*/
|
||||
export const InvocationContextSchema: GenMessage<InvocationContext> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 2);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.InvocationControlRequest
|
||||
*/
|
||||
export type InvocationControlRequest = Message<"quixos.runtime.InvocationControlRequest"> & {
|
||||
/**
|
||||
* @generated from field: string invocation_id = 1;
|
||||
*/
|
||||
invocationId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.runtime.InvocationControlRequest.
|
||||
* Use `create(InvocationControlRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const InvocationControlRequestSchema: GenMessage<InvocationControlRequest> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 3);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.InvocationStatus
|
||||
*/
|
||||
export type InvocationStatus = Message<"quixos.runtime.InvocationStatus"> & {
|
||||
/**
|
||||
* @generated from field: string invocation_id = 1;
|
||||
*/
|
||||
invocationId: string;
|
||||
|
||||
/**
|
||||
* unknown, running, cancellation-requested, completed, failed
|
||||
*
|
||||
* @generated from field: string state = 2;
|
||||
*/
|
||||
state: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.runtime.InvocationStatus.
|
||||
* Use `create(InvocationStatusSchema)` to create a new message.
|
||||
*/
|
||||
export const InvocationStatusSchema: GenMessage<InvocationStatus> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 4);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.InvokeRequest
|
||||
*/
|
||||
@@ -75,9 +175,9 @@ export type InvokeRequest = Message<"quixos.runtime.InvokeRequest"> & {
|
||||
invocationId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 2;
|
||||
* @generated from field: quixos.PackageExportRef export = 2;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
export?: PackageExportRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string object_id = 3;
|
||||
@@ -88,6 +188,16 @@ export type InvokeRequest = Message<"quixos.runtime.InvokeRequest"> & {
|
||||
* @generated from field: map<string, camino.Value> input = 4;
|
||||
*/
|
||||
input: { [key: string]: Value };
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.InjectedDependency dependencies = 5;
|
||||
*/
|
||||
dependencies: InjectedDependency[];
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.runtime.InvocationContext context = 6;
|
||||
*/
|
||||
context?: InvocationContext | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -95,7 +205,7 @@ export type InvokeRequest = Message<"quixos.runtime.InvokeRequest"> & {
|
||||
* Use `create(InvokeRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const InvokeRequestSchema: GenMessage<InvokeRequest> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 2);
|
||||
messageDesc(file_quixos_runtime, 5);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.InvokeResponse
|
||||
@@ -115,6 +225,11 @@ export type InvokeResponse = Message<"quixos.runtime.InvokeResponse"> & {
|
||||
* @generated from field: string error = 3;
|
||||
*/
|
||||
error: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.runtime.DerivedDependency dependencies = 4;
|
||||
*/
|
||||
dependencies: DerivedDependency[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -122,7 +237,7 @@ export type InvokeResponse = Message<"quixos.runtime.InvokeResponse"> & {
|
||||
* Use `create(InvokeResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const InvokeResponseSchema: GenMessage<InvokeResponse> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 3);
|
||||
messageDesc(file_quixos_runtime, 6);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.WatchRequest
|
||||
@@ -134,9 +249,9 @@ export type WatchRequest = Message<"quixos.runtime.WatchRequest"> & {
|
||||
invocationId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FunctionRef function = 2;
|
||||
* @generated from field: quixos.PackageExportRef export = 2;
|
||||
*/
|
||||
function?: FunctionRef | undefined;
|
||||
export?: PackageExportRef | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string object_id = 3;
|
||||
@@ -147,6 +262,16 @@ export type WatchRequest = Message<"quixos.runtime.WatchRequest"> & {
|
||||
* @generated from field: map<string, camino.Value> input = 4;
|
||||
*/
|
||||
input: { [key: string]: Value };
|
||||
|
||||
/**
|
||||
* @generated from field: repeated quixos.InjectedDependency dependencies = 5;
|
||||
*/
|
||||
dependencies: InjectedDependency[];
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.runtime.InvocationContext context = 6;
|
||||
*/
|
||||
context?: InvocationContext | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -154,7 +279,7 @@ export type WatchRequest = Message<"quixos.runtime.WatchRequest"> & {
|
||||
* Use `create(WatchRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const WatchRequestSchema: GenMessage<WatchRequest> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 4);
|
||||
messageDesc(file_quixos_runtime, 7);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.DerivedDependency
|
||||
@@ -171,14 +296,14 @@ export type DerivedDependency = Message<"quixos.runtime.DerivedDependency"> & {
|
||||
objectId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string field_name = 3;
|
||||
* @generated from field: string attachment_id = 3;
|
||||
*/
|
||||
fieldName: string;
|
||||
attachmentId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string source_field = 4;
|
||||
* @generated from field: string projection_id = 4;
|
||||
*/
|
||||
sourceField: string;
|
||||
projectionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,7 +311,7 @@ export type DerivedDependency = Message<"quixos.runtime.DerivedDependency"> & {
|
||||
* Use `create(DerivedDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const DerivedDependencySchema: GenMessage<DerivedDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 5);
|
||||
messageDesc(file_quixos_runtime, 8);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.runtime.WatchEvent
|
||||
@@ -223,7 +348,7 @@ export type WatchEvent = Message<"quixos.runtime.WatchEvent"> & {
|
||||
* Use `create(WatchEventSchema)` to create a new message.
|
||||
*/
|
||||
export const WatchEventSchema: GenMessage<WatchEvent> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_runtime, 6);
|
||||
messageDesc(file_quixos_runtime, 9);
|
||||
|
||||
/**
|
||||
* @generated from service quixos.runtime.PackageRuntime
|
||||
@@ -253,6 +378,22 @@ export const PackageRuntime: GenService<{
|
||||
input: typeof WatchRequestSchema;
|
||||
output: typeof WatchEventSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc quixos.runtime.PackageRuntime.GetInvocationStatus
|
||||
*/
|
||||
getInvocationStatus: {
|
||||
methodKind: "unary";
|
||||
input: typeof InvocationControlRequestSchema;
|
||||
output: typeof InvocationStatusSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc quixos.runtime.PackageRuntime.CancelInvocation
|
||||
*/
|
||||
cancelInvocation: {
|
||||
methodKind: "unary";
|
||||
input: typeof InvocationControlRequestSchema;
|
||||
output: typeof InvocationStatusSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_quixos_runtime, 0);
|
||||
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
import {
|
||||
parse,
|
||||
Source,
|
||||
validate,
|
||||
specifiedRules,
|
||||
print,
|
||||
printSchema,
|
||||
visit,
|
||||
TypeInfo,
|
||||
visitWithTypeInfo,
|
||||
getNamedType,
|
||||
isNonNullType,
|
||||
isListType,
|
||||
isObjectType,
|
||||
isInputObjectType,
|
||||
isScalarType,
|
||||
isEnumType,
|
||||
typeFromAST,
|
||||
type GraphQLType,
|
||||
type SelectionSetNode,
|
||||
type FragmentDefinitionNode,
|
||||
type ValueNode,
|
||||
type ASTNode,
|
||||
type DocumentNode,
|
||||
type DirectiveNode,
|
||||
} from "graphql";
|
||||
import { createHash } from "node:crypto";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { readRepositorySource } from "../capability-language/source-loader.js";
|
||||
import {
|
||||
valueType,
|
||||
type InterfaceRevision,
|
||||
type ValueType,
|
||||
type InterfaceRevisionId,
|
||||
} from "../capability-model/types.js";
|
||||
import { querySchema } from "./schema.js";
|
||||
import { objectRow } from "./relational-schema.js";
|
||||
import { relationalCompiler } from "./relational-compile.js";
|
||||
import {
|
||||
QueryCompileError,
|
||||
type QueryDeclaration,
|
||||
type CheckedQuery,
|
||||
type QueryFieldEffect,
|
||||
type QueryUse,
|
||||
type QueryArgument,
|
||||
type QuerySelection,
|
||||
} from "./types.js";
|
||||
|
||||
const fail = (code: string, message: string, node?: ASTNode): never => {
|
||||
const token = node?.loc?.startToken;
|
||||
throw new QueryCompileError(
|
||||
code,
|
||||
message,
|
||||
token
|
||||
? {
|
||||
file: node!.loc!.source.name,
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
|
||||
/** Only immutable repository sources call this. Runtime requests never compile documents. */
|
||||
export async function compileQuery(
|
||||
declaration: QueryDeclaration,
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
read: (name: string) => Promise<string>,
|
||||
): Promise<CheckedQuery> {
|
||||
const paths = [declaration.document, ...declaration.fragments];
|
||||
if (paths.length > 128 || new Set(paths).size !== paths.length)
|
||||
fail("QUERY_SOURCE_LIMIT", "Query files must be distinct and bounded to 128");
|
||||
const definitions: DocumentNode["definitions"][number][] = [];
|
||||
let totalBytes = 0;
|
||||
for (const path of paths) {
|
||||
if (
|
||||
!path.endsWith(".graphql") ||
|
||||
path.startsWith("/") ||
|
||||
path.split(/[\\/]/).some((part) => !part || part === "." || part === "..")
|
||||
)
|
||||
fail("QUERY_SOURCE_PATH", `Invalid query source path ${path}`);
|
||||
const text = await read(path);
|
||||
totalBytes += Buffer.byteLength(text);
|
||||
if (totalBytes > 262144) fail("QUERY_SOURCE_LIMIT", "Query sources exceed 256 KiB");
|
||||
const document = parse(new Source(text, path), { maxTokens: 10000 });
|
||||
definitions.push(...document.definitions);
|
||||
}
|
||||
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
|
||||
const generated = querySchema(declaration, interfaces, document);
|
||||
const operations = definitions.filter((entry) => entry.kind === "OperationDefinition");
|
||||
if (
|
||||
operations.length !== 1 ||
|
||||
operations[0]!.operation !== "query" ||
|
||||
operations[0]!.name?.value !== declaration.operation
|
||||
)
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Exactly one named query matching the declaration is required");
|
||||
visit(document, {
|
||||
Field(node) {
|
||||
if (node.name.value === "_unavailable")
|
||||
fail("QUERY_AGGREGATE_TYPE", "No supported field exists at this operator path", node);
|
||||
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
|
||||
},
|
||||
Directive(node) {
|
||||
if (!["include", "skip"].includes(node.name.value))
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
|
||||
},
|
||||
InlineFragment(node) {
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Use named fragments on the exact selected type", node);
|
||||
},
|
||||
});
|
||||
const errors = validate(generated.schema, document, specifiedRules, { maxErrors: 20 });
|
||||
if (errors.length) {
|
||||
const error = errors[0]!;
|
||||
fail("QUERY_VALIDATION", error.message, error.nodes?.[0]);
|
||||
}
|
||||
const effects = new Map<string, QueryFieldEffect>();
|
||||
const fragments = new Map(
|
||||
definitions
|
||||
.filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition")
|
||||
.map((entry) => [entry.name.value, entry]),
|
||||
);
|
||||
const mark = (id: InterfaceRevisionId, name: string, use: QueryUse, node: ASTNode) => {
|
||||
const contract = generated.contracts.get(id)!;
|
||||
const member = contract.members.find((entry) => entry.displayName === name);
|
||||
if (!member || member.kind === "operation" || !member.queryRead)
|
||||
return fail("QUERY_FIELD_NOT_QUERYABLE", `${contract.displayName}.${name} is not queryable`, node);
|
||||
const key = `${id}\0${member.id}`;
|
||||
const effect = effects.get(key) ?? {
|
||||
interfaceRevisionId: id,
|
||||
memberId: member.id,
|
||||
uses: [],
|
||||
execution: member.queryRead.execution,
|
||||
};
|
||||
if (!effect.uses.includes(use)) effect.uses.push(use);
|
||||
effects.set(key, effect);
|
||||
if (
|
||||
effect.execution === "rpc-permitted" &&
|
||||
!declaration.allowances.some(
|
||||
(allow) =>
|
||||
allow.interfaceRevisionId === id &&
|
||||
allow.memberId === member.id &&
|
||||
allow.uses.includes(use) &&
|
||||
allow.reason.trim(),
|
||||
)
|
||||
)
|
||||
fail("QUERY_RPC_CONSUMER_REASON", `${contract.displayName}.${name} needs an allowance for ${use}`, node);
|
||||
if (
|
||||
declaration.watch &&
|
||||
effect.execution === "rpc-permitted" &&
|
||||
!declaration.polling &&
|
||||
!member.operations.some((op) => op.displayName === "watch-start")
|
||||
)
|
||||
fail(
|
||||
"QUERY_WATCH_UNSUPPORTED",
|
||||
`${contract.displayName}.${name} has no watch; explicitly acknowledge polling or use a one-shot query`,
|
||||
node,
|
||||
);
|
||||
};
|
||||
const relational = relationalCompiler(generated, declaration, mark, fragments);
|
||||
const compiledPredicates = new WeakMap<ASTNode, import("./relational.js").QueryPredicate>();
|
||||
const inputEffects = (id: InterfaceRevisionId, value: ValueNode, use: "predicate" | "order") => {
|
||||
if (use === "predicate") {
|
||||
relational.predicate(objectRow(id), value);
|
||||
return;
|
||||
}
|
||||
if (value.kind === "Variable") {
|
||||
// A dynamic filter may name any field in its declared input type.
|
||||
for (const member of generated.contracts.get(id)!.members)
|
||||
if (member.kind === "value" && member.queryRead) mark(id, member.displayName, use, value);
|
||||
} else if (value.kind === "ListValue") value.values.forEach((child) => inputEffects(id, child, use));
|
||||
else if (value.kind === "ObjectValue")
|
||||
for (const field of value.fields) {
|
||||
mark(id, field.name.value, use, field);
|
||||
}
|
||||
};
|
||||
const info = new TypeInfo(generated.schema);
|
||||
visit(
|
||||
document,
|
||||
visitWithTypeInfo(info, {
|
||||
Field(node) {
|
||||
const parent = info.getParentType();
|
||||
const contract = parent && generated.byName.get(parent.name);
|
||||
if (!contract || node.name.value === "_qx") return;
|
||||
mark(contract.revisionId, node.name.value, "select", node);
|
||||
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
||||
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
|
||||
return;
|
||||
const target = generated.target(member);
|
||||
const bounds = (node.arguments ?? []).filter(
|
||||
(argument) => argument.name.value === "first" || argument.name.value === "all",
|
||||
);
|
||||
if (bounds.length !== 1) fail("QUERY_ROW_LIMIT", "Specify exactly one of first or all", node);
|
||||
if (bounds[0]!.name.value === "all" && node.arguments?.some((argument) => argument.name.value === "after"))
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
|
||||
for (const argument of node.arguments ?? []) {
|
||||
if (argument.name.value === "where")
|
||||
compiledPredicates.set(node, relational.predicate(objectRow(target), argument.value));
|
||||
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
||||
if (
|
||||
["first", "all"].includes(argument.name.value) &&
|
||||
argument.value.kind !== "Variable" &&
|
||||
(argument.value.kind !== "IntValue" ||
|
||||
Number(argument.value.value) < 1 ||
|
||||
Number(argument.value.value) > declaration.budgets.rows)
|
||||
)
|
||||
fail(
|
||||
"QUERY_ROW_LIMIT",
|
||||
`${argument.name.value} must be between 1 and ${declaration.budgets.rows}`,
|
||||
argument,
|
||||
);
|
||||
}
|
||||
if (node.arguments?.some((a) => a.name.value === "after")) {
|
||||
const rpcOrder = (value: ValueNode): boolean =>
|
||||
value.kind === "Variable"
|
||||
? generated.contracts
|
||||
.get(target)!
|
||||
.members.some((m) => m.kind === "value" && m.queryRead?.execution === "rpc-permitted")
|
||||
: value.kind === "ListValue"
|
||||
? value.values.some(rpcOrder)
|
||||
: value.kind === "ObjectValue" &&
|
||||
value.fields.some((f) =>
|
||||
generated.contracts
|
||||
.get(target)!
|
||||
.members.some(
|
||||
(m) =>
|
||||
m.displayName === f.name.value &&
|
||||
m.kind === "value" &&
|
||||
m.queryRead?.execution === "rpc-permitted",
|
||||
),
|
||||
);
|
||||
const order = node.arguments.find((a) => a.name.value === "orderBy");
|
||||
if (relational.needsRpc(compiledPredicates.get(node)) || (order && rpcOrder(order.value)))
|
||||
fail(
|
||||
"QUERY_UNSUPPORTED_FEATURE",
|
||||
"RPC predicates/order require a bounded first/all window without continuation",
|
||||
node,
|
||||
);
|
||||
}
|
||||
},
|
||||
FragmentSpread(node) {
|
||||
const fragment = definitions.find(
|
||||
(entry) => entry.kind === "FragmentDefinition" && entry.name.value === node.name.value,
|
||||
) as FragmentDefinitionNode;
|
||||
if (fragment.typeCondition.name.value !== info.getParentType()?.name)
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Fragments must select the exact current type", node);
|
||||
},
|
||||
}),
|
||||
);
|
||||
let expandedFields = 0;
|
||||
// GraphQL merges repeated response keys. In particular, fragments may each
|
||||
// contribute different children of one relationship; last-write-wins would
|
||||
// silently remove fields from both the generated type and the wire codec.
|
||||
const mergeOutput = (left: ValueType, right: ValueType): ValueType => {
|
||||
const a = left.kind === "optional" ? left.value : left;
|
||||
const b = right.kind === "optional" ? right.value : right;
|
||||
let result = a;
|
||||
if (a.kind === "record" && b.kind === "record") {
|
||||
const fields = { ...a.fields };
|
||||
for (const [key, type] of Object.entries(b.fields))
|
||||
fields[key] = fields[key] ? mergeOutput(fields[key], type) : type;
|
||||
result = { kind: "record", fields };
|
||||
} else if (a.kind === "list" && b.kind === "list") result = valueType.list(mergeOutput(a.value, b.value));
|
||||
return left.kind === "optional" && right.kind === "optional" ? valueType.optional(result) : result;
|
||||
};
|
||||
const output = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
|
||||
if (depth > declaration.budgets.depth * 4 + 4) fail("QUERY_DEPTH_LIMIT", "Expanded query exceeds its depth budget");
|
||||
if (isNonNullType(type)) return required(type.ofType, selections, depth, conditional);
|
||||
return valueType.optional(required(type, selections, depth, conditional));
|
||||
};
|
||||
const required = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
|
||||
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth, conditional));
|
||||
if (isObjectType(type)) {
|
||||
const fields: Record<string, ValueType> = {};
|
||||
const add = (set: SelectionSetNode, conditional = false) => {
|
||||
for (const selection of set.selections) {
|
||||
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
|
||||
if (selection.kind === "FragmentSpread") {
|
||||
add(fragments.get(selection.name.value)!.selectionSet, conditional || !!selection.directives?.length);
|
||||
continue;
|
||||
}
|
||||
if (selection.kind !== "Field") continue;
|
||||
const key = selection.alias?.value ?? selection.name.value;
|
||||
const field = type.getFields()[selection.name.value]!;
|
||||
const previous = fields[key];
|
||||
fields[key] = output(
|
||||
field.type,
|
||||
selection.selectionSet,
|
||||
depth + 1,
|
||||
conditional || !!selection.directives?.length,
|
||||
);
|
||||
if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional")
|
||||
fields[key] = valueType.optional(fields[key]!);
|
||||
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
||||
}
|
||||
};
|
||||
if (selections) add(selections, conditional);
|
||||
return { kind: "record", fields };
|
||||
}
|
||||
return (
|
||||
generated.scalarTypes.get(getNamedType(type)!.name) ??
|
||||
fail("QUERY_TYPE", `Unsupported query scalar ${getNamedType(type)!.name}`)
|
||||
);
|
||||
};
|
||||
const variables: Record<string, ValueType> = {};
|
||||
const inputShape = (type: GraphQLType, seen = new Set<string>()): ValueType => {
|
||||
if (isNonNullType(type)) return inputRequired(type.ofType, seen);
|
||||
return valueType.optional(inputRequired(type, seen));
|
||||
};
|
||||
const inputRequired = (type: GraphQLType, seen: Set<string>): ValueType => {
|
||||
if (isListType(type)) return valueType.list(inputShape(type.ofType, seen));
|
||||
if (isInputObjectType(type)) {
|
||||
// Recursive boolean filter inputs need generated recursive TS shapes; never degrade to any.
|
||||
if (seen.has(type.name))
|
||||
fail(
|
||||
"QUERY_UNSUPPORTED_FEATURE",
|
||||
"Pass scalar variables inside fixed filter expressions instead of a whole recursive filter",
|
||||
);
|
||||
return {
|
||||
kind: "record",
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(type.getFields()).map(([key, field]) => [
|
||||
key,
|
||||
inputShape(field.type, new Set([...seen, type.name])),
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
if (isEnumType(type)) return valueType.string;
|
||||
if (isScalarType(type))
|
||||
return generated.scalarTypes.get(type.name) ?? fail("QUERY_TYPE", `Unsupported query scalar ${type.name}`);
|
||||
return fail("QUERY_VARIABLE_TYPE", "Unsupported variable type");
|
||||
};
|
||||
for (const variable of operations[0]!.variableDefinitions ?? [])
|
||||
variables[variable.variable.name.value] = inputShape(typeFromAST(generated.schema, variable.type)!);
|
||||
const result = required(generated.schema.getQueryType()!, operations[0]!.selectionSet);
|
||||
const argument = (node: ValueNode): QueryArgument => {
|
||||
switch (node.kind) {
|
||||
case "Variable":
|
||||
return { kind: "variable", name: node.name.value };
|
||||
case "ListValue":
|
||||
return { kind: "list", values: node.values.map(argument) };
|
||||
case "ObjectValue":
|
||||
return {
|
||||
kind: "object",
|
||||
fields: Object.fromEntries(node.fields.map((field) => [field.name.value, argument(field.value)])),
|
||||
};
|
||||
case "NullValue":
|
||||
return { kind: "literal", value: null };
|
||||
case "BooleanValue":
|
||||
return { kind: "literal", value: node.value };
|
||||
// Int literals stay decimal until their checked field codec interprets them.
|
||||
default:
|
||||
return { kind: "literal", value: node.value };
|
||||
}
|
||||
};
|
||||
const conditions = (directives: readonly DirectiveNode[] = []) =>
|
||||
directives.map((directive) => ({
|
||||
include: directive.name.value === "include",
|
||||
value: argument(directive.arguments!.find((entry) => entry.name.value === "if")!.value),
|
||||
}));
|
||||
const selection = (
|
||||
set: SelectionSetNode,
|
||||
parent: import("graphql").GraphQLObjectType,
|
||||
inherited: QuerySelection["conditions"] = [],
|
||||
): QuerySelection[] =>
|
||||
set.selections.flatMap((node): QuerySelection[] => {
|
||||
if (node.kind === "FragmentSpread")
|
||||
return selection(fragments.get(node.name.value)!.selectionSet, parent, [
|
||||
...inherited,
|
||||
...conditions(node.directives),
|
||||
]);
|
||||
if (node.kind !== "Field") return [];
|
||||
const contract = generated.byName.get(parent.name);
|
||||
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
|
||||
const type = getNamedType(parent.getFields()[node.name.value]!.type);
|
||||
const role = generated.relational.roles.get(parent.name);
|
||||
const relationship =
|
||||
role?.kind === "relations"
|
||||
? generated.relational.relations(role.row).find((m) => m.displayName === node.name.value)
|
||||
: undefined;
|
||||
const plan =
|
||||
relationship && role?.row.kind === "object" && node.selectionSet
|
||||
? relational.plan(role.row.interfaceRevisionId, relationship, node.selectionSet, (set, name) => {
|
||||
const t = generated.schema.getType(name);
|
||||
if (!t || !isObjectType(t)) return fail("QUERY_TYPE", `Missing generated output type ${name}`, node);
|
||||
return selection(set, t);
|
||||
})
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
name: node.name.value,
|
||||
key: node.alias?.value ?? node.name.value,
|
||||
...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
|
||||
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
||||
conditions: [...inherited, ...conditions(node.directives)],
|
||||
arguments: Object.fromEntries(
|
||||
(node.arguments ?? []).map((entry) => [entry.name.value, argument(entry.value)]),
|
||||
),
|
||||
selection: node.selectionSet && isObjectType(type) ? selection(node.selectionSet, type) : [],
|
||||
...(plan ? { relational: plan } : {}),
|
||||
...(compiledPredicates.has(node) ? { predicate: compiledPredicates.get(node)! } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
const normalized = print(document),
|
||||
schema = printSchema(generated.schema);
|
||||
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
||||
const definitionDigest = createHash("sha256")
|
||||
.update(
|
||||
canonicalJson({
|
||||
semantics: 2,
|
||||
declaration,
|
||||
normalized,
|
||||
interfaces: [...generated.byName.values()].sort((a, b) =>
|
||||
a.revisionId < b.revisionId ? -1 : a.revisionId > b.revisionId ? 1 : 0,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.digest("hex");
|
||||
return {
|
||||
declaration,
|
||||
definitionDigest,
|
||||
document: normalized,
|
||||
schema,
|
||||
variables: { kind: "record", fields: variables },
|
||||
output: result,
|
||||
effects: [...effects.values()],
|
||||
selection: selections,
|
||||
variableDefaults: Object.fromEntries(
|
||||
(operations[0]!.variableDefinitions ?? [])
|
||||
.filter((entry) => entry.defaultValue)
|
||||
.map((entry) => [entry.variable.name.value, argument(entry.defaultValue!)]),
|
||||
),
|
||||
sourceFiles: paths,
|
||||
};
|
||||
}
|
||||
|
||||
export const compileRepositoryQuery = async (
|
||||
root: string,
|
||||
declaration: QueryDeclaration,
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
) => {
|
||||
const started = performance.now();
|
||||
try {
|
||||
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
|
||||
} finally {
|
||||
// Timing belongs in check logs, never in immutable artifact identities.
|
||||
console.error(
|
||||
`[query-check] ${JSON.stringify(declaration.displayName)}: ${(performance.now() - started).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type {
|
||||
WorkspaceRevision,
|
||||
Binding,
|
||||
AtomId,
|
||||
InterfaceRevisionId,
|
||||
MemberId,
|
||||
PersistentAttachment,
|
||||
} from "../capability-model/types.js";
|
||||
import { QueryCompileError, type CheckedQuery } from "./types.js";
|
||||
import { queryRuntimePlan } from "./proto.js";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
|
||||
export interface LinkedQueryField {
|
||||
atomId: AtomId;
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
getter: string;
|
||||
binding: Binding;
|
||||
watch?: { start: string; stop: string; binding: Binding };
|
||||
}
|
||||
export interface LinkedQuery {
|
||||
runtime?: import("@bufbuild/protobuf").JsonValue;
|
||||
id: string;
|
||||
packageRevisionId: string;
|
||||
checked: CheckedQuery;
|
||||
bindingDigest: string;
|
||||
fields: LinkedQueryField[];
|
||||
attachments: PersistentAttachment[];
|
||||
}
|
||||
|
||||
/** Link every possible implementation, including atoms absent from today's data. */
|
||||
export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
||||
const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry]));
|
||||
const attachments = [
|
||||
...workspace.sharedAttachments,
|
||||
...workspace.conformances.flatMap((entry) => entry.privateAttachments),
|
||||
];
|
||||
const linked: LinkedQuery[] = [];
|
||||
for (const pkg of workspace.packageImports)
|
||||
for (const declaration of pkg.queries ?? []) {
|
||||
const checked = pkg.checkedQueries?.find((query) => query.declaration.id === declaration.id);
|
||||
if (!checked || JSON.stringify(checked.declaration) !== JSON.stringify(declaration))
|
||||
throw new QueryCompileError(
|
||||
"QUERY_ARTIFACT_MISSING",
|
||||
`${pkg.displayName}.${declaration.displayName} has no checked source artifact`,
|
||||
);
|
||||
const fields: LinkedQueryField[] = [];
|
||||
const needed = new Set<string>();
|
||||
for (const view of declaration.views)
|
||||
if (
|
||||
!workspace.conformances.some(
|
||||
(entry) => entry.atomId === view.atomId && entry.interfaceRevisionId === view.interfaceRevisionId,
|
||||
)
|
||||
)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_VIEW_REQUIRED",
|
||||
`${view.atomId} does not conform to declared query view ${view.interfaceRevisionId}`,
|
||||
);
|
||||
for (const effect of checked.effects) {
|
||||
const contract = interfaces.get(effect.interfaceRevisionId);
|
||||
const member = contract?.members.find((entry) => entry.id === effect.memberId);
|
||||
if (!member || member.kind === "operation" || member.queryRead?.execution !== effect.execution)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_STALE_CONTRACT",
|
||||
`Query field ${effect.interfaceRevisionId}.${effect.memberId} changed`,
|
||||
);
|
||||
const getter = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
|
||||
if (!getter) throw new QueryCompileError("QUERY_CONTRACT", `Missing getter ${effect.memberId}`);
|
||||
for (const conformance of workspace.conformances.filter(
|
||||
(entry) => entry.interfaceRevisionId === effect.interfaceRevisionId,
|
||||
)) {
|
||||
const provider = conformance.operationBindings.find((entry) => entry.operationId === getter.id);
|
||||
if (!provider)
|
||||
throw new QueryCompileError("QUERY_CONTRACT", `Missing binding ${conformance.atomId}.${getter.id}`);
|
||||
const binding = provider.binding;
|
||||
if (binding.kind === "package") {
|
||||
if (effect.execution !== "rpc-permitted")
|
||||
throw new QueryCompileError(
|
||||
"QUERY_NATIVE_BINDING_REQUIRED",
|
||||
`Native query field ${effect.memberId} binds package code`,
|
||||
);
|
||||
if (!provider.queryReason?.trim())
|
||||
throw new QueryCompileError(
|
||||
"QUERY_RPC_PROVIDER_REASON",
|
||||
`Package query field ${effect.memberId} needs query-reason`,
|
||||
);
|
||||
} else if (binding.kind === "state" && binding.primitive === "read") needed.add(binding.slotId);
|
||||
else if (binding.kind === "edge" && binding.primitive === "resolve") needed.add(binding.edgeTypeId);
|
||||
else
|
||||
throw new QueryCompileError(
|
||||
"QUERY_NATIVE_BINDING_REQUIRED",
|
||||
`Unsupported query read binding ${effect.memberId}`,
|
||||
);
|
||||
const start = member.operations.find((op) => op.displayName === "watch-start");
|
||||
const stop = member.operations.find((op) => op.displayName === "watch-stop");
|
||||
const watch = start && stop && conformance.operationBindings.find((entry) => entry.operationId === start.id);
|
||||
if (
|
||||
binding.kind === "state" &&
|
||||
watch &&
|
||||
(watch.binding.kind !== "state" ||
|
||||
watch.binding.slotId !== binding.slotId ||
|
||||
watch.binding.primitive !== "watch-start")
|
||||
)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_WATCH_CONTRACT",
|
||||
`Native query field ${effect.memberId} watch disagrees with its getter`,
|
||||
);
|
||||
if (
|
||||
binding.kind === "edge" &&
|
||||
watch &&
|
||||
(watch.binding.kind !== "edge" ||
|
||||
watch.binding.edgeTypeId !== binding.edgeTypeId ||
|
||||
watch.binding.projectionId !== binding.projectionId)
|
||||
)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_WATCH_CONTRACT",
|
||||
`Native query relation ${effect.memberId} watch disagrees with its resolver`,
|
||||
);
|
||||
fields.push({
|
||||
atomId: conformance.atomId,
|
||||
interfaceRevisionId: effect.interfaceRevisionId,
|
||||
memberId: effect.memberId,
|
||||
getter: getter.id,
|
||||
binding,
|
||||
...(watch && start && stop ? { watch: { start: start.id, stop: stop.id, binding: watch.binding } } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
const storage = attachments.filter((entry) => needed.has(entry.id));
|
||||
const bindingDigest = createHash("sha256")
|
||||
.update(
|
||||
canonicalJson({
|
||||
definition: checked.definitionDigest,
|
||||
fields: [...fields].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
||||
storage: [...storage].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
}),
|
||||
)
|
||||
.digest("hex");
|
||||
linked.push({
|
||||
id: `${pkg.revisionId}:${declaration.id}`,
|
||||
packageRevisionId: pkg.revisionId,
|
||||
checked,
|
||||
bindingDigest,
|
||||
fields,
|
||||
attachments: storage,
|
||||
});
|
||||
}
|
||||
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
||||
if (new Set(linked.map((entry) => entry.id)).size !== linked.length)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_ID_COLLISION",
|
||||
"Query export identities collide; choose distinct package/query IDs",
|
||||
);
|
||||
return linked;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { create, toJson } from "@bufbuild/protobuf";
|
||||
import {
|
||||
InstalledQuerySchema,
|
||||
QueryArgumentSchema,
|
||||
QuerySelectionSchema,
|
||||
QueryReadBindingSchema,
|
||||
Cardinality,
|
||||
type QueryArgument as WireArgument,
|
||||
} from "../gen/camino/schema_pb.js";
|
||||
import type { LinkedQuery } from "./link.js";
|
||||
import type { QueryArgument, QuerySelection } from "./types.js";
|
||||
import type { WorkspaceRevision } from "../capability-model/types.js";
|
||||
import { relationalWire } from "./relational-proto.js";
|
||||
|
||||
const argument = (entry: QueryArgument): WireArgument => {
|
||||
switch (entry.kind) {
|
||||
case "variable":
|
||||
return create(QueryArgumentSchema, { value: { case: "variable", value: entry.name } });
|
||||
case "literal":
|
||||
return create(QueryArgumentSchema, { value: { case: "literalJson", value: JSON.stringify(entry.value) } });
|
||||
case "list":
|
||||
return create(QueryArgumentSchema, { value: { case: "list", value: { values: entry.values.map(argument) } } });
|
||||
case "object":
|
||||
return create(QueryArgumentSchema, {
|
||||
value: {
|
||||
case: "object",
|
||||
value: { fields: Object.fromEntries(Object.entries(entry.fields).map(([k, v]) => [k, argument(v)])) },
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const selection = (entry: QuerySelection): import("../gen/camino/schema_pb.js").QuerySelection =>
|
||||
create(QuerySelectionSchema, {
|
||||
...entry,
|
||||
conditions: entry.conditions.map((condition) => ({ include: condition.include, value: argument(condition.value) })),
|
||||
arguments: Object.fromEntries(Object.entries(entry.arguments).map(([k, v]) => [k, argument(v)])),
|
||||
selection: entry.selection.map(selection),
|
||||
relational: entry.relational && relationalWire(argument, selection).plan(entry.relational),
|
||||
predicate: entry.predicate && relationalWire(argument, selection).predicate(entry.predicate),
|
||||
});
|
||||
|
||||
export const querySelectionToWire = selection;
|
||||
|
||||
export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevision) =>
|
||||
toJson(
|
||||
InstalledQuerySchema,
|
||||
create(InstalledQuerySchema, {
|
||||
id: linked.id,
|
||||
definitionDigest: linked.checked.definitionDigest,
|
||||
bindingDigest: linked.bindingDigest,
|
||||
rootInterfaceRevisionId: linked.checked.declaration.root,
|
||||
selection: linked.checked.selection.map(selection),
|
||||
budgets: linked.checked.declaration.budgets,
|
||||
variablesTypeJson: JSON.stringify(linked.checked.variables),
|
||||
outputTypeJson: JSON.stringify(linked.checked.output),
|
||||
variableDefaults: Object.fromEntries(
|
||||
Object.entries(linked.checked.variableDefaults).map(([k, v]) => [k, argument(v)]),
|
||||
),
|
||||
watch: linked.checked.declaration.watch,
|
||||
pollingIntervalMs: linked.checked.declaration.polling?.intervalMs,
|
||||
rpcPredicateOrOrder: linked.checked.effects.some(
|
||||
(effect) => effect.execution === "rpc-permitted" && effect.uses.some((use) => use !== "select"),
|
||||
),
|
||||
bindings: linked.fields.map((field) => {
|
||||
const member = workspace.interfaceImports
|
||||
.find((entry) => entry.revisionId === field.interfaceRevisionId)!
|
||||
.members.find((entry) => entry.id === field.memberId)!;
|
||||
return create(QueryReadBindingSchema, {
|
||||
atomId: field.atomId,
|
||||
interfaceRevisionId: field.interfaceRevisionId,
|
||||
memberId: field.memberId,
|
||||
getterOperationId: field.getter,
|
||||
fieldName: member.displayName,
|
||||
keyType: member.kind === "relationship" ? member.keyType : undefined,
|
||||
valueTypeJson: member.kind === "value" ? JSON.stringify(member.valueType) : "",
|
||||
cardinality:
|
||||
member.kind === "relationship"
|
||||
? {
|
||||
"optional-one": Cardinality.OPTIONAL_ONE,
|
||||
"exactly-one": Cardinality.EXACTLY_ONE,
|
||||
many: Cardinality.MANY,
|
||||
"many-unique": Cardinality.MANY_UNIQUE,
|
||||
}[member.cardinality]
|
||||
: undefined,
|
||||
slotId: field.binding.kind === "state" ? field.binding.slotId : "",
|
||||
edgeTypeId: field.binding.kind === "edge" ? field.binding.edgeTypeId : "",
|
||||
projectionId: field.binding.kind === "edge" ? field.binding.projectionId : "",
|
||||
rpc: field.binding.kind === "package",
|
||||
watchStartOperationId: field.watch?.start,
|
||||
watchStopOperationId: field.watch?.stop,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,605 @@
|
||||
import type { ASTNode, FieldNode, FragmentDefinitionNode, SelectionSetNode, ValueNode } from "graphql";
|
||||
import {
|
||||
valueType,
|
||||
type InterfaceRevisionId,
|
||||
type RelationshipInterfaceMember,
|
||||
type ValueType,
|
||||
} from "../capability-model/types.js";
|
||||
import {
|
||||
QueryCompileError,
|
||||
type QueryArgument,
|
||||
type QueryDeclaration,
|
||||
type QuerySelection,
|
||||
type QueryUse,
|
||||
} from "./types.js";
|
||||
import {
|
||||
aggregateType,
|
||||
queryKeyType,
|
||||
type AggregateOperator,
|
||||
type QueryAggregatePredicate,
|
||||
type QueryEffectRecorder,
|
||||
type QueryExpression,
|
||||
type QueryKey,
|
||||
type QueryPathStep,
|
||||
type QueryPredicate,
|
||||
type QueryReduction,
|
||||
type QueryRelationalPlan,
|
||||
type QueryRelationalTerminal,
|
||||
type QueryRow,
|
||||
} from "./relational.js";
|
||||
import { objectRow } from "./relational-schema.js";
|
||||
|
||||
export function queryArgument(node: ValueNode): QueryArgument {
|
||||
if (node.kind === "Variable") return { kind: "variable", name: node.name.value };
|
||||
if (node.kind === "ListValue") return { kind: "list", values: node.values.map(queryArgument) };
|
||||
if (node.kind === "ObjectValue")
|
||||
return {
|
||||
kind: "object",
|
||||
fields: Object.fromEntries(node.fields.map((f) => [f.name.value, queryArgument(f.value)])),
|
||||
};
|
||||
return { kind: "literal", value: node.kind === "NullValue" ? null : node.value };
|
||||
}
|
||||
type Environment = ReturnType<typeof import("./schema.js").querySchema>;
|
||||
type Availability = readonly QueryKey[] | undefined;
|
||||
function fail(code: string, message: string, node: ASTNode): never {
|
||||
const token = node.loc?.startToken;
|
||||
throw new QueryCompileError(
|
||||
code,
|
||||
message,
|
||||
token ? { file: node.loc!.source.name, line: token.line, column: token.column } : undefined,
|
||||
);
|
||||
}
|
||||
const pairs = (node: ValueNode): readonly import("graphql").ObjectFieldNode[] =>
|
||||
node.kind === "ObjectValue"
|
||||
? node.fields
|
||||
: fail("QUERY_UNSUPPORTED_FEATURE", "Query structure must be a literal object", node);
|
||||
const ops = new Set(["eq", "in", "isNull", "lt", "lte", "gt", "gte"]);
|
||||
const prefix = (a: readonly string[], b: readonly string[]) => a.every((part, i) => part === b[i]);
|
||||
|
||||
export function relationalCompiler(
|
||||
env: Environment,
|
||||
declaration: QueryDeclaration,
|
||||
mark: QueryEffectRecorder,
|
||||
fragments: Map<string, FragmentDefinitionNode>,
|
||||
) {
|
||||
const rowFields = env.relational.fields;
|
||||
const relationships = env.relational.relations;
|
||||
const needsRpc = (value: unknown): boolean => {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
if ("leaf" in value) {
|
||||
const leaf = (value as QueryExpression).leaf;
|
||||
if (leaf.kind === "field")
|
||||
return (
|
||||
env.contracts
|
||||
.get(leaf.interfaceRevisionId)
|
||||
?.members.some(
|
||||
(m) => m.id === leaf.memberId && m.kind === "value" && m.queryRead?.execution === "rpc-permitted",
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
return Object.values(value).some(needsRpc);
|
||||
};
|
||||
let stageCount = 0;
|
||||
const fields = (set: SelectionSetNode): FieldNode[] =>
|
||||
set.selections.flatMap((n) =>
|
||||
n.kind === "Field" ? [n] : n.kind === "FragmentSpread" ? fields(fragments.get(n.name.value)!.selectionSet) : [],
|
||||
);
|
||||
const requireAvailable = (path: string[], available: Availability, node: ASTNode) => {
|
||||
if (!available) return;
|
||||
const ok = available.some((key) => {
|
||||
if (key.path.join(".") === path.join(".")) return true;
|
||||
const last = key.expression.leaf.kind;
|
||||
if (last !== "ref" && last !== "entry") return false;
|
||||
const base = key.path.slice(0, -2);
|
||||
if (!prefix(base, path)) return false;
|
||||
// Object identity does not determine which incoming membership carried it.
|
||||
return (
|
||||
last === "entry" || !(path[base.length] === "_qx" && ["entry", "mapKey"].includes(path[base.length + 1] ?? ""))
|
||||
);
|
||||
});
|
||||
if (!ok)
|
||||
fail(
|
||||
"QUERY_DISTINCT_FIELD_UNAVAILABLE",
|
||||
`${path.join(".")} was dropped by distinct; retain its identity or key explicitly`,
|
||||
node,
|
||||
);
|
||||
};
|
||||
const walk = (
|
||||
root: QueryRow,
|
||||
names: string[],
|
||||
uses: QueryUse[],
|
||||
node: ASTNode,
|
||||
): { row: QueryRow; path: QueryPathStep[]; nullable: boolean } => {
|
||||
let row = root,
|
||||
nullable = false;
|
||||
const path: QueryPathStep[] = [];
|
||||
for (const name of names) {
|
||||
if (row.kind === "pair") {
|
||||
if (name !== "source" && name !== "target")
|
||||
return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown row binding ${name}`, node);
|
||||
path.push({ kind: name });
|
||||
row = row[name];
|
||||
continue;
|
||||
}
|
||||
const relation = relationships(row).find((m) => m.displayName === name);
|
||||
if (!relation) return fail("QUERY_FIELD_NOT_QUERYABLE", `Expected queryable relationship ${name}`, node);
|
||||
if (relation.cardinality === "many" || relation.cardinality === "many-unique")
|
||||
return fail(
|
||||
"QUERY_EXPANSION_REQUIRED",
|
||||
`${name} is to-many; explicitly expand it, quantify it or aggregate it`,
|
||||
node,
|
||||
);
|
||||
for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
|
||||
const target = env.target(relation),
|
||||
optional = relation.cardinality === "optional-one";
|
||||
path.push({
|
||||
kind: "relation",
|
||||
interfaceRevisionId: row.interfaceRevisionId,
|
||||
memberId: relation.id,
|
||||
targetInterfaceRevisionId: target,
|
||||
optional,
|
||||
});
|
||||
nullable ||= optional;
|
||||
row = objectRow(target);
|
||||
}
|
||||
if (path.length > declaration.budgets.depth) fail("QUERY_DEPTH_LIMIT", "Expression path exceeds query depth", node);
|
||||
return { row, path, nullable };
|
||||
};
|
||||
const expression = (
|
||||
root: QueryRow,
|
||||
names: string[],
|
||||
uses: QueryUse[],
|
||||
node: ASTNode,
|
||||
available?: Availability,
|
||||
): QueryExpression => {
|
||||
requireAvailable(names, available, node);
|
||||
const meta = names.at(-2) === "_qx";
|
||||
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : -1), uses, node);
|
||||
const name = names.at(-1)!;
|
||||
let type: ValueType, leaf: QueryExpression["leaf"];
|
||||
if (meta) {
|
||||
if (name === "ref" && row.kind === "object") {
|
||||
type = valueType.interfaceRef(row.interfaceRevisionId);
|
||||
leaf = { kind: "ref", interfaceRevisionId: row.interfaceRevisionId };
|
||||
} else if (name === "entry" && (row.kind === "pair" || row.membership)) {
|
||||
type = valueType.string;
|
||||
leaf = { kind: "entry" };
|
||||
} else if (name === "mapKey" && row.kind === "object" && row.membership?.keyType) {
|
||||
type =
|
||||
row.membership.keyType === "boolean"
|
||||
? valueType.bool
|
||||
: row.membership.keyType === "int64"
|
||||
? valueType.int64
|
||||
: valueType.string;
|
||||
leaf = { kind: "mapKey" };
|
||||
} else return fail("QUERY_KEY_INVALID", `Unavailable metadata ${names.join(".")}`, node);
|
||||
} else {
|
||||
if (row.kind !== "object")
|
||||
return fail("QUERY_FIELD_NOT_QUERYABLE", `Expected source or target, not ${name}`, node);
|
||||
const field = rowFields(row).get(name);
|
||||
if (!field || field.kind !== "leaf") {
|
||||
if (relationships(row).some((m) => m.displayName === name))
|
||||
fail("QUERY_EXPANSION_REQUIRED", `${name} is not a scalar operand`, node);
|
||||
return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown queryable scalar ${name}`, node);
|
||||
}
|
||||
for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
|
||||
const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!;
|
||||
type = field.type;
|
||||
leaf = { kind: "field", interfaceRevisionId: row.interfaceRevisionId, memberId: member.id };
|
||||
}
|
||||
return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type };
|
||||
};
|
||||
const keyList = (row: QueryRow, node: ValueNode, use: "group" | "distinct", available?: Availability): QueryKey[] => {
|
||||
const result: QueryKey[] = [];
|
||||
const visit = (node: ValueNode, path: string[]) => {
|
||||
if (node.kind === "BooleanValue") {
|
||||
if (!node.value) fail("QUERY_KEY_INVALID", "Key selector leaves must be literal true", node);
|
||||
const expr = expression(row, path, [use], node, available);
|
||||
if (!queryKeyType(expr.type)) fail("QUERY_KEY_INVALID", `Unsupported key ${path.join(".")}`, node);
|
||||
result.push({ path, expression: expr });
|
||||
return;
|
||||
}
|
||||
for (const field of pairs(node)) visit(field.value, [...path, field.name.value]);
|
||||
};
|
||||
visit(node, []);
|
||||
if (!result.length) fail("QUERY_KEY_INVALID", "Key selectors must not be empty", node);
|
||||
return result.sort((a, b) => JSON.stringify(a.expression).localeCompare(JSON.stringify(b.expression)));
|
||||
};
|
||||
const comparisons = (
|
||||
node: ValueNode,
|
||||
make: (
|
||||
operator: Extract<QueryPredicate, { kind: "compare" }>["operator"],
|
||||
value: QueryArgument,
|
||||
) => QueryPredicate | QueryAggregatePredicate,
|
||||
) =>
|
||||
pairs(node).map((field) => {
|
||||
if (!ops.has(field.name.value))
|
||||
return fail("QUERY_PREDICATE_INVALID", `Unsupported comparison ${field.name.value}`, field);
|
||||
if (field.name.value === "in" && field.value.kind === "ListValue" && field.value.values.length > 1000)
|
||||
fail("QUERY_WORK_LIMIT", "in supports at most 1000 operands", field);
|
||||
return make(
|
||||
field.name.value as Extract<QueryPredicate, { kind: "compare" }>["operator"],
|
||||
queryArgument(field.value),
|
||||
);
|
||||
});
|
||||
const reduction = (
|
||||
row: QueryRow,
|
||||
op: AggregateOperator,
|
||||
path: string[],
|
||||
node: ASTNode,
|
||||
uses: QueryUse[],
|
||||
available?: Availability,
|
||||
): QueryReduction => {
|
||||
if (op === "count") return { operator: op, type: aggregateType(op) };
|
||||
const operand = expression(row, path, ["aggregate", ...uses], node, available);
|
||||
return { operator: op, operand, type: aggregateType(op, operand.type) };
|
||||
};
|
||||
const aggregatePredicate = (
|
||||
row: QueryRow,
|
||||
node: ValueNode,
|
||||
keys: QueryKey[] = [],
|
||||
available?: Availability,
|
||||
): QueryAggregatePredicate => {
|
||||
const children: QueryAggregatePredicate[] = [];
|
||||
for (const field of pairs(node)) {
|
||||
const name = field.name.value;
|
||||
if (name === "and" || name === "or") {
|
||||
if (field.value.kind !== "ListValue")
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Boolean predicate structure is fixed in the document", field);
|
||||
children.push({
|
||||
kind: name,
|
||||
children: field.value.values.map((v) => aggregatePredicate(row, v, keys, available)),
|
||||
});
|
||||
} else if (name === "not")
|
||||
children.push({ kind: "not", child: aggregatePredicate(row, field.value, keys, available) });
|
||||
else if (name === "count")
|
||||
children.push(
|
||||
...(comparisons(field.value, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: reduction(row, "count", [], field, []),
|
||||
operator,
|
||||
value,
|
||||
})) as QueryAggregatePredicate[]),
|
||||
);
|
||||
else {
|
||||
const visit = (n: ValueNode, path: string[]) => {
|
||||
const entries = pairs(n);
|
||||
if (entries.some((f) => ops.has(f.name.value))) {
|
||||
if (name === "group" && !keys.some((k) => k.path.join(".") === path.join(".")))
|
||||
fail("QUERY_GROUP_FIELD_UNAVAILABLE", `${path.join(".")} is not a grouping key`, n);
|
||||
const expr =
|
||||
name === "group"
|
||||
? expression(row, path, ["predicate"], n, available)
|
||||
: reduction(row, name as AggregateOperator, path, n, ["predicate"], available);
|
||||
children.push(
|
||||
...(comparisons(n, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: expr,
|
||||
operator,
|
||||
value,
|
||||
})) as QueryAggregatePredicate[]),
|
||||
);
|
||||
} else for (const entry of entries) visit(entry.value, [...path, entry.name.value]);
|
||||
};
|
||||
visit(field.value, []);
|
||||
}
|
||||
}
|
||||
return { kind: "and", children };
|
||||
};
|
||||
const predicate = (row: QueryRow, node: ValueNode, available?: Availability): QueryPredicate => {
|
||||
const children: QueryPredicate[] = [];
|
||||
const visit = (current: QueryRow, n: ValueNode, base: string[]) => {
|
||||
for (const field of pairs(n)) {
|
||||
const name = field.name.value;
|
||||
if (name === "and" || name === "or") {
|
||||
if (field.value.kind !== "ListValue")
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Boolean predicate structure is fixed in the document", field);
|
||||
const branches = field.value.values.map((v) => {
|
||||
const before = children.length;
|
||||
visit(current, v, base);
|
||||
return { kind: "and", children: children.splice(before) } as QueryPredicate;
|
||||
});
|
||||
children.push({ kind: name, children: branches });
|
||||
} else if (name === "not") {
|
||||
const before = children.length;
|
||||
visit(current, field.value, base);
|
||||
children.push({ kind: "not", child: { kind: "and", children: children.splice(before) } });
|
||||
} else if (current.kind === "pair" && (name === "source" || name === "target"))
|
||||
visit(current[name], field.value, [...base, name]);
|
||||
else if (name === "_qx") {
|
||||
for (const meta of pairs(field.value)) {
|
||||
if (meta.name.value !== "relations") {
|
||||
const expr = expression(row, [...base, "_qx", meta.name.value], ["predicate"], meta, available);
|
||||
children.push(
|
||||
...(comparisons(meta.value, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: expr,
|
||||
operator,
|
||||
value,
|
||||
})) as QueryPredicate[]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const edge of pairs(meta.value)) {
|
||||
if (current.kind !== "object")
|
||||
fail("QUERY_CONTRACT", "A pair is not an object with graph relationships", edge);
|
||||
const member = relationships(current).find((m) => m.displayName === edge.name.value)!;
|
||||
requireAvailable([...base, member.displayName], available, edge);
|
||||
mark(current.interfaceRevisionId, member.displayName, "predicate", edge);
|
||||
const common = {
|
||||
path: walk(row, base, ["predicate"], edge).path,
|
||||
interfaceRevisionId: current.interfaceRevisionId,
|
||||
memberId: member.id,
|
||||
targetInterfaceRevisionId: env.target(member),
|
||||
};
|
||||
for (const op of pairs(edge.value)) {
|
||||
if (op.name.value === "aggregate") {
|
||||
const config = pairs(op.value),
|
||||
where = config.find((f) => f.name.value === "where"),
|
||||
having = config.find((f) => f.name.value === "having")!;
|
||||
children.push({
|
||||
kind: "reduce",
|
||||
...common,
|
||||
where: where ? predicate(env.relational.relationRow(member), where.value) : undefined,
|
||||
having: aggregatePredicate(env.relational.relationRow(member), having.value),
|
||||
});
|
||||
} else if (op.name.value === "isNull")
|
||||
children.push({ kind: "relation", ...common, operator: "isNull", value: queryArgument(op.value) });
|
||||
else
|
||||
children.push({
|
||||
kind: "relation",
|
||||
...common,
|
||||
operator: op.name.value as "some" | "none" | "is",
|
||||
predicate: predicate(env.relational.relationRow(member), op.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const expr = expression(row, [...base, name], ["predicate"], field, available);
|
||||
children.push(
|
||||
...(comparisons(field.value, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: expr,
|
||||
operator,
|
||||
value,
|
||||
})) as QueryPredicate[]),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(row, node, []);
|
||||
return { kind: "and", children };
|
||||
};
|
||||
|
||||
const plan = (
|
||||
owner: InterfaceRevisionId,
|
||||
member: RelationshipInterfaceMember,
|
||||
set: SelectionSetNode,
|
||||
selection: (set: SelectionSetNode, typeName: string) => QuerySelection[],
|
||||
): QueryRelationalPlan => {
|
||||
const result: QueryRelationalPlan = { stages: [], terminals: [] };
|
||||
const add = (row: QueryRow, operation: QueryRelationalPlan["stages"][number]["operation"], input?: number) => {
|
||||
if (++stageCount > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 relational stages");
|
||||
const id = result.stages.length;
|
||||
result.stages.push({ id, row, operation, input });
|
||||
return id;
|
||||
};
|
||||
const root = env.relational.relationRow(member);
|
||||
const initial = add(root, {
|
||||
kind: "source",
|
||||
interfaceRevisionId: owner,
|
||||
memberId: member.id,
|
||||
targetInterfaceRevisionId: env.target(member),
|
||||
});
|
||||
mark(owner, member.displayName, "select", set);
|
||||
const bound = (node: FieldNode) => {
|
||||
const args = node.arguments ?? [],
|
||||
bounds = args.filter((a) => a.name.value === "first" || a.name.value === "all");
|
||||
if (bounds.length !== 1) fail("QUERY_ROW_LIMIT", "Specify exactly one first or all for groups", node);
|
||||
if (bounds[0]!.name.value === "all" && args.some((a) => a.name.value === "after"))
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all cannot continue", node);
|
||||
for (const b of bounds)
|
||||
if (
|
||||
b.value.kind !== "Variable" &&
|
||||
(b.value.kind !== "IntValue" || Number(b.value.value) < 1 || Number(b.value.value) > declaration.budgets.rows)
|
||||
)
|
||||
fail("QUERY_ROW_LIMIT", "Group page exceeds row budget", b);
|
||||
};
|
||||
const reductions = (
|
||||
row: QueryRow,
|
||||
set: SelectionSetNode,
|
||||
available: Availability,
|
||||
): QueryRelationalTerminal["reductions"] => {
|
||||
const output: QueryRelationalTerminal["reductions"] = [];
|
||||
const visit = (set: SelectionSetNode, path: string[], response: string[], op: AggregateOperator) => {
|
||||
for (const n of fields(set)) {
|
||||
const key = n.alias?.value ?? n.name.value,
|
||||
next = [...path, n.name.value],
|
||||
out = [...response, key];
|
||||
if (n.selectionSet) visit(n.selectionSet, next, out, op);
|
||||
else output.push({ path: out, reduction: reduction(row, op, next, n, [], available) });
|
||||
}
|
||||
};
|
||||
for (const n of fields(set)) {
|
||||
const op = n.name.value as AggregateOperator,
|
||||
key = n.alias?.value ?? n.name.value;
|
||||
if (op === "count") output.push({ path: [key], reduction: reduction(row, op, [], n, []) });
|
||||
else if (n.selectionSet) visit(n.selectionSet, [], [key], op);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
const groupsProjection = (row: QueryRow, set: SelectionSetNode, keys: QueryKey[], available: Availability) => {
|
||||
let values: QueryRelationalTerminal["reductions"] = [];
|
||||
for (const entries of fields(set))
|
||||
if (entries.name.value === "entries" && entries.selectionSet)
|
||||
for (const n of fields(entries.selectionSet)) {
|
||||
if (n.name.value === "aggregate" && n.selectionSet)
|
||||
values.push(
|
||||
...reductions(row, n.selectionSet, available).map((v) => ({
|
||||
...v,
|
||||
path: [entries.alias?.value ?? entries.name.value, n.alias?.value ?? n.name.value, ...v.path],
|
||||
})),
|
||||
);
|
||||
if (n.name.value === "group" && n.selectionSet) {
|
||||
const visit = (set: SelectionSetNode, path: string[]) => {
|
||||
for (const f of fields(set)) {
|
||||
const next = [...path, f.name.value];
|
||||
if (f.selectionSet) visit(f.selectionSet, next);
|
||||
else if (!keys.some((k) => k.path.join(".") === next.join(".")))
|
||||
fail("QUERY_GROUP_FIELD_UNAVAILABLE", `${next.join(".")} is not a selected group key`, f);
|
||||
}
|
||||
};
|
||||
visit(n.selectionSet, []);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
||||
const run = (row: QueryRow, stage: number, set: SelectionSetNode, path: string[], available?: Availability) => {
|
||||
for (const node of fields(set)) {
|
||||
if (!node.selectionSet) continue;
|
||||
if (node.directives?.length)
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Relational stages cannot be conditionally selected", node);
|
||||
const name = node.name.value,
|
||||
outputPath = [...path, node.alias?.value ?? name];
|
||||
const args = new Map((node.arguments ?? []).map((a) => [a.name.value, a.value]));
|
||||
if (name === "filter")
|
||||
run(
|
||||
row,
|
||||
add(row, { kind: "filter", predicate: predicate(row, args.get("where")!, available) }, stage),
|
||||
node.selectionSet,
|
||||
outputPath,
|
||||
available,
|
||||
);
|
||||
else if (name === "distinct") {
|
||||
const keys = keyList(row, args.get("by")!, "distinct", available);
|
||||
run(row, add(row, { kind: "distinct", keys }, stage), node.selectionSet, outputPath, keys);
|
||||
} else if (name === "expand") {
|
||||
const expand = (current: QueryRow, set: SelectionSetNode, memberPath: string[], output: string[]) => {
|
||||
for (const n of fields(set)) {
|
||||
if (!n.selectionSet) continue;
|
||||
const member = relationships(current).find((m) => m.displayName === n.name.value);
|
||||
const next = [...memberPath, n.name.value],
|
||||
out = [...output, n.alias?.value ?? n.name.value];
|
||||
if (member && (member.cardinality === "many" || member.cardinality === "many-unique")) {
|
||||
requireAvailable(next, available, n);
|
||||
if (current.kind !== "object") fail("QUERY_CONTRACT", "Expected object expansion source", n);
|
||||
const walked = walk(row, memberPath, ["select"], n);
|
||||
mark(current.interfaceRevisionId, member.displayName, "select", n);
|
||||
const target = env.relational.relationRow(member),
|
||||
pair: QueryRow = { kind: "pair", source: row, target };
|
||||
const newAvailable = available
|
||||
? [
|
||||
...available.map((k) => ({
|
||||
...k,
|
||||
path: ["source", ...k.path],
|
||||
expression: { ...k.expression, path: [{ kind: "source" as const }, ...k.expression.path] },
|
||||
})),
|
||||
{
|
||||
path: ["target", "_qx", "entry"],
|
||||
expression: {
|
||||
path: [{ kind: "target" as const }],
|
||||
leaf: { kind: "entry" as const },
|
||||
type: valueType.string,
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
run(
|
||||
pair,
|
||||
add(
|
||||
pair,
|
||||
{
|
||||
kind: "expand",
|
||||
path: walked.path,
|
||||
interfaceRevisionId: current.interfaceRevisionId,
|
||||
memberId: member.id,
|
||||
targetInterfaceRevisionId: env.target(member),
|
||||
},
|
||||
stage,
|
||||
),
|
||||
n.selectionSet,
|
||||
out,
|
||||
newAvailable,
|
||||
);
|
||||
} else {
|
||||
const f = rowFields(current).get(n.name.value);
|
||||
if (f?.kind === "row") expand(f.row, n.selectionSet, next, out);
|
||||
else fail("QUERY_EXPANSION_REQUIRED", "Expected a declared expansion path", n);
|
||||
}
|
||||
}
|
||||
};
|
||||
expand(row, node.selectionSet, [], outputPath);
|
||||
} else if (name === "aggregate" || name === "groups") {
|
||||
if (name === "groups") bound(node);
|
||||
const keys = name === "groups" ? keyList(row, args.get("by")!, "group", available) : [];
|
||||
const terminal: QueryRelationalTerminal = {
|
||||
stage,
|
||||
path: outputPath,
|
||||
kind: name,
|
||||
keys,
|
||||
reductions:
|
||||
name === "aggregate"
|
||||
? reductions(row, node.selectionSet, available)
|
||||
: groupsProjection(row, node.selectionSet, keys, available),
|
||||
order: [],
|
||||
residual: false,
|
||||
where: args.has("where") ? predicate(row, args.get("where")!, available) : undefined,
|
||||
having: args.has("having") ? aggregatePredicate(row, args.get("having")!, keys, available) : undefined,
|
||||
first: args.has("first") ? queryArgument(args.get("first")!) : undefined,
|
||||
all: args.has("all") ? queryArgument(args.get("all")!) : undefined,
|
||||
after: args.has("after") ? queryArgument(args.get("after")!) : undefined,
|
||||
selection: selection(
|
||||
node.selectionSet,
|
||||
name === "aggregate"
|
||||
? env.relational.rowset(row).getFields().aggregate!.type.toString().replace(/!$/u, "")
|
||||
: env.relational.rowset(row).getFields().groups!.type.toString().replace(/!$/u, ""),
|
||||
),
|
||||
};
|
||||
const order = args.get("orderBy");
|
||||
if (order) {
|
||||
if (order.kind !== "ListValue")
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Ordering is a fixed list of field directions", order);
|
||||
for (const item of order.values) {
|
||||
const start = terminal.order.length;
|
||||
const visit = (n: ValueNode, path: string[]) => {
|
||||
if (n.kind === "EnumValue") {
|
||||
const [op, ...operand] = path;
|
||||
let expr: QueryExpression | QueryReduction;
|
||||
if (op === "group") {
|
||||
if (!keys.some((k) => k.path.join(".") === operand.join(".")))
|
||||
fail("QUERY_GROUP_FIELD_UNAVAILABLE", "Sort key was not grouped", n);
|
||||
expr = expression(row, operand, ["order"], n, available);
|
||||
} else expr = reduction(row, op as AggregateOperator, operand, n, ["order"], available);
|
||||
terminal.order.push({ expression: expr, descending: n.value === "DESC" });
|
||||
} else for (const f of pairs(n)) visit(f.value, [...path, f.name.value]);
|
||||
};
|
||||
visit(item, []);
|
||||
if (terminal.order.length !== start + 1)
|
||||
fail("QUERY_ORDER_INVALID", "Each orderBy element must name exactly one field", item);
|
||||
}
|
||||
}
|
||||
result.terminals.push(terminal);
|
||||
} else fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported relational stage ${name}`, node);
|
||||
}
|
||||
};
|
||||
run(root, initial, set, []);
|
||||
for (const terminal of result.terminals) {
|
||||
let stage: QueryRelationalPlan["stages"][number] | undefined = result.stages[terminal.stage];
|
||||
let rpc = needsRpc(terminal);
|
||||
while (stage) {
|
||||
rpc ||= needsRpc(stage.operation);
|
||||
stage = stage.input === undefined ? undefined : result.stages[stage.input];
|
||||
}
|
||||
terminal.residual = rpc;
|
||||
if (rpc && terminal.after)
|
||||
fail(
|
||||
"QUERY_UNSUPPORTED_FEATURE",
|
||||
"RPC relational inputs require a bounded first/all window without continuation",
|
||||
set,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { expression, predicate, aggregatePredicate, keyList, plan, needsRpc };
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import * as wire from "../gen/camino/schema_pb.js";
|
||||
import type { QueryArgument, QuerySelection } from "./types.js";
|
||||
import type {
|
||||
QueryAggregatePredicate,
|
||||
QueryExpression,
|
||||
QueryPathStep,
|
||||
QueryPredicate,
|
||||
QueryReduction,
|
||||
QueryRelationalPlan,
|
||||
QueryRow,
|
||||
} from "./relational.js";
|
||||
|
||||
export function relationalWire(
|
||||
argument: (v: QueryArgument) => wire.QueryArgument,
|
||||
selection: (v: QuerySelection) => wire.QuerySelection,
|
||||
) {
|
||||
const path = (p: QueryPathStep): wire.QueryPathStep =>
|
||||
create(wire.QueryPathStepSchema, {
|
||||
step: p.kind === "relation" ? { case: "relation", value: p } : { case: p.kind, value: true },
|
||||
});
|
||||
const expression = (e: QueryExpression) =>
|
||||
create(wire.QueryExpressionSchema, {
|
||||
path: e.path.map(path),
|
||||
valueTypeJson: JSON.stringify(e.type),
|
||||
leaf:
|
||||
e.leaf.kind === "field"
|
||||
? { case: "field", value: e.leaf }
|
||||
: e.leaf.kind === "ref"
|
||||
? { case: "ref", value: e.leaf.interfaceRevisionId }
|
||||
: { case: e.leaf.kind, value: true },
|
||||
});
|
||||
const reduction = (r: QueryReduction) =>
|
||||
create(wire.QueryReductionSchema, {
|
||||
operator: {
|
||||
count: wire.QueryAggregateOperator.COUNT,
|
||||
countPresent: wire.QueryAggregateOperator.COUNT_PRESENT,
|
||||
sum: wire.QueryAggregateOperator.SUM,
|
||||
avg: wire.QueryAggregateOperator.AVG,
|
||||
min: wire.QueryAggregateOperator.MIN,
|
||||
max: wire.QueryAggregateOperator.MAX,
|
||||
}[r.operator],
|
||||
operand: r.operand && expression(r.operand),
|
||||
valueTypeJson: JSON.stringify(r.type),
|
||||
});
|
||||
const operand = (v: QueryExpression | QueryReduction) =>
|
||||
create(wire.QueryOperandSchema, {
|
||||
operand:
|
||||
"operator" in v ? { case: "reduction", value: reduction(v) } : { case: "expression", value: expression(v) },
|
||||
});
|
||||
const predicate = (p: QueryPredicate | QueryAggregatePredicate): wire.QueryPredicate => {
|
||||
switch (p.kind) {
|
||||
case "and":
|
||||
case "or":
|
||||
return create(wire.QueryPredicateSchema, {
|
||||
predicate: { case: p.kind, value: { children: p.children.map(predicate) } },
|
||||
});
|
||||
case "not":
|
||||
return create(wire.QueryPredicateSchema, { predicate: { case: "not", value: predicate(p.child) } });
|
||||
case "compare":
|
||||
return create(wire.QueryPredicateSchema, {
|
||||
predicate: {
|
||||
case: "compare",
|
||||
value: {
|
||||
operand: operand(p.expression),
|
||||
value: argument(p.value),
|
||||
operator: {
|
||||
eq: wire.QueryComparisonOperator.EQ,
|
||||
in: wire.QueryComparisonOperator.IN,
|
||||
isNull: wire.QueryComparisonOperator.IS_NULL,
|
||||
lt: wire.QueryComparisonOperator.LT,
|
||||
lte: wire.QueryComparisonOperator.LTE,
|
||||
gt: wire.QueryComparisonOperator.GT,
|
||||
gte: wire.QueryComparisonOperator.GTE,
|
||||
}[p.operator],
|
||||
},
|
||||
},
|
||||
});
|
||||
case "relation":
|
||||
return create(wire.QueryPredicateSchema, {
|
||||
predicate: {
|
||||
case: "relation",
|
||||
value: {
|
||||
path: p.path.map(path),
|
||||
relation: p,
|
||||
operator: {
|
||||
some: wire.QueryRelationPredicateOperator.SOME,
|
||||
none: wire.QueryRelationPredicateOperator.NONE,
|
||||
is: wire.QueryRelationPredicateOperator.IS,
|
||||
isNull: wire.QueryRelationPredicateOperator.IS_NULL,
|
||||
}[p.operator],
|
||||
predicate: p.predicate && predicate(p.predicate),
|
||||
value: p.value && argument(p.value),
|
||||
},
|
||||
},
|
||||
});
|
||||
case "reduce":
|
||||
return create(wire.QueryPredicateSchema, {
|
||||
predicate: {
|
||||
case: "reduce",
|
||||
value: {
|
||||
path: p.path.map(path),
|
||||
relation: p,
|
||||
where: p.where && predicate(p.where),
|
||||
having: predicate(p.having),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const row = (r: QueryRow): wire.QueryRow =>
|
||||
create(wire.QueryRowSchema, {
|
||||
row:
|
||||
r.kind === "object"
|
||||
? {
|
||||
case: "object",
|
||||
value: {
|
||||
interfaceRevisionId: r.interfaceRevisionId,
|
||||
membership: !!r.membership,
|
||||
keyType: r.membership?.keyType,
|
||||
},
|
||||
}
|
||||
: { case: "pair", value: { source: row(r.source), target: row(r.target) } },
|
||||
});
|
||||
const plan = (p: QueryRelationalPlan) =>
|
||||
create(wire.QueryRelationalPlanSchema, {
|
||||
stages: p.stages.map((s) => {
|
||||
const op = s.operation;
|
||||
const operation: wire.QueryRelationalStage["operation"] =
|
||||
op.kind === "source"
|
||||
? { case: "source", value: create(wire.QueryRelationPathSchema, op) }
|
||||
: op.kind === "filter"
|
||||
? { case: "filter", value: predicate(op.predicate) }
|
||||
: op.kind === "expand"
|
||||
? {
|
||||
case: "expand",
|
||||
value: create(wire.QueryExpansionSchema, { path: op.path.map(path), relation: op }),
|
||||
}
|
||||
: {
|
||||
case: "distinct",
|
||||
value: create(wire.QueryDistinctSchema, {
|
||||
keys: op.keys.map((k) => ({ ...k, expression: expression(k.expression) })),
|
||||
}),
|
||||
};
|
||||
return create(wire.QueryRelationalStageSchema, { id: s.id, input: s.input, row: row(s.row), operation });
|
||||
}),
|
||||
terminals: p.terminals.map((t) => ({
|
||||
stage: t.stage,
|
||||
path: t.path,
|
||||
groups: t.kind === "groups",
|
||||
keys: t.keys.map((k) => ({ ...k, expression: expression(k.expression) })),
|
||||
reductions: t.reductions.map((r) => ({ ...r, reduction: reduction(r.reduction) })),
|
||||
where: t.where && predicate(t.where),
|
||||
having: t.having && predicate(t.having),
|
||||
order: t.order.map((o) => ({ operand: operand(o.expression), descending: o.descending })),
|
||||
first: t.first && argument(t.first),
|
||||
all: t.all && argument(t.all),
|
||||
after: t.after && argument(t.after),
|
||||
selection: t.selection.map(selection),
|
||||
residual: t.residual,
|
||||
})),
|
||||
});
|
||||
return { predicate, plan };
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
GraphQLInt,
|
||||
GraphQLScalarType,
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
type GraphQLInputType,
|
||||
type GraphQLOutputType,
|
||||
type GraphQLInputFieldConfigMap,
|
||||
type GraphQLFieldConfigMap,
|
||||
type GraphQLEnumType,
|
||||
type DocumentNode,
|
||||
type SelectionSetNode,
|
||||
type FragmentDefinitionNode,
|
||||
} from "graphql";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
valueType,
|
||||
type ValueType,
|
||||
type InterfaceRevision,
|
||||
type InterfaceRevisionId,
|
||||
type RelationshipInterfaceMember,
|
||||
} from "../capability-model/types.js";
|
||||
import {
|
||||
aggregateType,
|
||||
aggregateOperators,
|
||||
queryKeyType,
|
||||
type QueryRow,
|
||||
type AggregateOperator,
|
||||
} from "./relational.js";
|
||||
import { QueryCompileError } from "./types.js";
|
||||
|
||||
export const rowKey = (row: QueryRow): string => JSON.stringify(row);
|
||||
export const objectRow = (
|
||||
interfaceRevisionId: InterfaceRevisionId,
|
||||
membership?: { keyType?: "string" | "boolean" | "int64" },
|
||||
): QueryRow => ({ kind: "object", interfaceRevisionId, ...(membership ? { membership } : {}) });
|
||||
export type SchemaRowField = { kind: "leaf"; type: ValueType } | { kind: "row"; row: QueryRow; optional: boolean };
|
||||
export interface RelationalSchemaEnvironment {
|
||||
contracts: Map<InterfaceRevisionId, InterfaceRevision>;
|
||||
target(member: RelationshipInterfaceMember): InterfaceRevisionId;
|
||||
name(id: InterfaceRevisionId): string;
|
||||
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
|
||||
comparison(type: ValueType): GraphQLInputObjectType;
|
||||
scalarTypes: Map<string, ValueType>;
|
||||
direction: GraphQLEnumType;
|
||||
cursor: GraphQLScalarType;
|
||||
pageInfo: GraphQLObjectType;
|
||||
}
|
||||
|
||||
export function relationalSchema(env: RelationalSchemaEnvironment, document?: DocumentNode) {
|
||||
const outputCache = new Map<string, GraphQLObjectType>();
|
||||
const inputCache = new Map<string, GraphQLInputObjectType>();
|
||||
const referenceCache = new Map<string, GraphQLScalarType>();
|
||||
const rowsets = new Map<string, QueryRow>();
|
||||
const expansionPaths = new Map<string, Map<string, string[]>>();
|
||||
const roles = new Map<
|
||||
string,
|
||||
{ kind: "rowset" | "aggregate" | "groups" | "keys" | "expand" | "relations"; row: QueryRow }
|
||||
>();
|
||||
const many = (member: RelationshipInterfaceMember) =>
|
||||
member.cardinality === "many" || member.cardinality === "many-unique";
|
||||
const relationRow = (member: RelationshipInterfaceMember) =>
|
||||
objectRow(env.target(member), { keyType: member.keyType });
|
||||
const relations = (row: QueryRow) =>
|
||||
row.kind === "object"
|
||||
? env.contracts
|
||||
.get(row.interfaceRevisionId)!
|
||||
.members.filter(
|
||||
(member): member is RelationshipInterfaceMember => member.kind === "relationship" && !!member.queryRead,
|
||||
)
|
||||
: [];
|
||||
const fields = (row: QueryRow): Map<string, SchemaRowField> => {
|
||||
if (row.kind === "pair")
|
||||
return new Map([
|
||||
["source", { kind: "row", row: row.source, optional: false }],
|
||||
["target", { kind: "row", row: row.target, optional: false }],
|
||||
]);
|
||||
return new Map(
|
||||
env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => {
|
||||
if (member.kind === "operation" || !member.queryRead) return [];
|
||||
if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]];
|
||||
if (many(member)) return [];
|
||||
return [
|
||||
[
|
||||
member.displayName,
|
||||
{ kind: "row", row: objectRow(env.target(member)), optional: member.cardinality === "optional-one" },
|
||||
],
|
||||
];
|
||||
}),
|
||||
);
|
||||
};
|
||||
const refs = (id: InterfaceRevisionId) => {
|
||||
let result = referenceCache.get(id);
|
||||
if (!result) {
|
||||
result = new GraphQLScalarType({
|
||||
name: `QxRef_${env.name(id)}`,
|
||||
parseValue: (value) => value,
|
||||
parseLiteral() {
|
||||
throw new Error("Managed references must be supplied as typed variables");
|
||||
},
|
||||
});
|
||||
referenceCache.set(id, result);
|
||||
env.scalarTypes.set(result.name, valueType.interfaceRef(id));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const scalar = (type: ValueType): GraphQLInputType & GraphQLOutputType => {
|
||||
if (type.kind === "optional") return scalar(type.value);
|
||||
return type.kind === "object-ref" && type.expectation.kind === "interface"
|
||||
? refs(type.expectation.interfaceRevisionId)
|
||||
: env.scalar(type);
|
||||
};
|
||||
const leafFields = (row: QueryRow) => {
|
||||
const result = new Map<string, ValueType>();
|
||||
if (row.kind === "object") result.set("ref", valueType.interfaceRef(row.interfaceRevisionId));
|
||||
if (row.kind === "pair" || row.membership) result.set("entry", valueType.string);
|
||||
if (row.kind === "object" && row.membership?.keyType)
|
||||
result.set(
|
||||
"mapKey",
|
||||
row.membership.keyType === "boolean"
|
||||
? valueType.bool
|
||||
: row.membership.keyType === "int64"
|
||||
? valueType.int64
|
||||
: valueType.string,
|
||||
);
|
||||
return result;
|
||||
};
|
||||
const typeName = (role: string, row: QueryRow, suffix = "") =>
|
||||
`Qx${role}_${createHash("sha256")
|
||||
.update(rowKey(row) + suffix)
|
||||
.digest("hex")
|
||||
.slice(0, 16)}`;
|
||||
const out = (role: string, row: QueryRow, make: () => GraphQLFieldConfigMap<unknown, unknown>, suffix = "") => {
|
||||
const key = typeName(role, row, suffix);
|
||||
let result = outputCache.get(key);
|
||||
if (!result) {
|
||||
result = new GraphQLObjectType({ name: key, fields: make });
|
||||
outputCache.set(key, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const inp = (role: string, row: QueryRow, make: () => GraphQLInputFieldConfigMap, suffix = "") => {
|
||||
const key = typeName(role, row, suffix);
|
||||
let result = inputCache.get(key);
|
||||
if (!result) {
|
||||
result = new GraphQLInputObjectType({ name: key, fields: make });
|
||||
inputCache.set(key, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const compare = (type: ValueType) => {
|
||||
const base = type.kind === "optional" ? type.value : type;
|
||||
if (base.kind !== "object-ref") return env.comparison(type);
|
||||
return inp(
|
||||
"RefCompare",
|
||||
objectRow(
|
||||
base.expectation.kind === "interface"
|
||||
? base.expectation.interfaceRevisionId
|
||||
: (() => {
|
||||
throw new Error("Expected interface reference");
|
||||
})(),
|
||||
),
|
||||
() => ({ eq: { type: scalar(type) }, in: { type: new GraphQLList(new GraphQLNonNull(scalar(type))) } }),
|
||||
);
|
||||
};
|
||||
// Recursive interface graphs are interned. Synthetic pair graphs are finite and document-driven.
|
||||
const columns = (
|
||||
row: QueryRow,
|
||||
operator?: Exclude<AggregateOperator, "count">,
|
||||
nullable = false,
|
||||
): GraphQLObjectType =>
|
||||
out(
|
||||
"Columns",
|
||||
row,
|
||||
() => {
|
||||
const result: GraphQLFieldConfigMap<unknown, unknown> = {};
|
||||
for (const [name, field] of fields(row)) {
|
||||
if (field.kind === "row")
|
||||
result[name] = { type: new GraphQLNonNull(columns(field.row, operator, nullable || field.optional)) };
|
||||
else {
|
||||
let type = field.type;
|
||||
if (operator) {
|
||||
try {
|
||||
type = aggregateType(operator, type);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} else if (!queryKeyType(type)) continue;
|
||||
result[name] = {
|
||||
type:
|
||||
type.kind === "optional" || (!operator && nullable) ? scalar(type) : new GraphQLNonNull(scalar(type)),
|
||||
};
|
||||
}
|
||||
}
|
||||
const metadata: GraphQLFieldConfigMap<unknown, unknown> = {};
|
||||
for (const [name, type] of leafFields(row)) {
|
||||
if (operator && operator !== "countPresent") continue;
|
||||
const t = scalar(operator ? aggregateType(operator, type) : type);
|
||||
metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) };
|
||||
}
|
||||
if (Object.keys(metadata).length)
|
||||
result._qx = { type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)) };
|
||||
// A type may have no matching scalar at this node; a private schema sentinel
|
||||
// keeps it valid while compiler validation forbids selecting that sentinel.
|
||||
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
||||
return result;
|
||||
},
|
||||
`${operator}:${nullable}`,
|
||||
);
|
||||
const aggregate = (row: QueryRow): GraphQLObjectType => {
|
||||
const result = out("Aggregate", row, () =>
|
||||
Object.fromEntries(
|
||||
aggregateOperators.map((op) => [
|
||||
op,
|
||||
{ type: new GraphQLNonNull(op === "count" ? env.scalar(valueType.uint64) : columns(row, op)) },
|
||||
]),
|
||||
),
|
||||
);
|
||||
roles.set(result.name, { kind: "aggregate", row });
|
||||
return result;
|
||||
};
|
||||
const keys = (row: QueryRow): GraphQLInputObjectType =>
|
||||
inp("Keys", row, () => {
|
||||
const result: GraphQLInputFieldConfigMap = {};
|
||||
for (const [name, field] of fields(row)) {
|
||||
if (field.kind === "row") result[name] = { type: keys(field.row) };
|
||||
else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean };
|
||||
}
|
||||
const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }]));
|
||||
if (Object.keys(meta).length) result._qx = { type: inp("KeyMeta", row, () => meta) };
|
||||
return result;
|
||||
});
|
||||
const comparisons = (
|
||||
row: QueryRow,
|
||||
operator?: Exclude<AggregateOperator, "count">,
|
||||
ordering = false,
|
||||
): GraphQLInputObjectType =>
|
||||
inp(
|
||||
"Expressions",
|
||||
row,
|
||||
() => {
|
||||
const result: GraphQLInputFieldConfigMap = {};
|
||||
for (const [name, field] of fields(row)) {
|
||||
if (field.kind === "row") result[name] = { type: comparisons(field.row, operator, ordering) };
|
||||
else {
|
||||
let type = field.type;
|
||||
if (operator) {
|
||||
try {
|
||||
type = aggregateType(operator, type);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result[name] = { type: ordering ? env.direction : compare(type) };
|
||||
}
|
||||
}
|
||||
const meta: GraphQLInputFieldConfigMap = {};
|
||||
for (const [name, type] of leafFields(row))
|
||||
if (!operator || operator === "countPresent")
|
||||
meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) };
|
||||
if (Object.keys(meta).length)
|
||||
result._qx = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) };
|
||||
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
||||
return result;
|
||||
},
|
||||
`${operator}:${ordering}`,
|
||||
);
|
||||
const having = (row: QueryRow, ordering = false): GraphQLInputObjectType =>
|
||||
inp(
|
||||
"Having",
|
||||
row,
|
||||
() => {
|
||||
const result: GraphQLInputFieldConfigMap = { group: { type: comparisons(row, undefined, ordering) } };
|
||||
for (const op of aggregateOperators)
|
||||
result[op] = {
|
||||
type:
|
||||
op === "count" ? (ordering ? env.direction : compare(valueType.uint64)) : comparisons(row, op, ordering),
|
||||
};
|
||||
if (!ordering)
|
||||
Object.assign(result, {
|
||||
and: { type: new GraphQLList(new GraphQLNonNull(having(row))) },
|
||||
or: { type: new GraphQLList(new GraphQLNonNull(having(row))) },
|
||||
not: { type: having(row) },
|
||||
});
|
||||
return result;
|
||||
},
|
||||
String(ordering),
|
||||
);
|
||||
const where = (row: QueryRow): GraphQLInputObjectType =>
|
||||
inp("Where", row, () => {
|
||||
const result: GraphQLInputFieldConfigMap = {
|
||||
and: { type: new GraphQLList(new GraphQLNonNull(where(row))) },
|
||||
or: { type: new GraphQLList(new GraphQLNonNull(where(row))) },
|
||||
not: { type: where(row) },
|
||||
};
|
||||
for (const [name, field] of fields(row)) {
|
||||
if (row.kind === "object" && field.kind === "row") continue;
|
||||
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
|
||||
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
||||
}
|
||||
result._qx = {
|
||||
type: inp("WhereMeta", row, () => {
|
||||
const meta: GraphQLInputFieldConfigMap = {};
|
||||
for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) };
|
||||
const edges = relations(row);
|
||||
if (edges.length)
|
||||
meta.relations = {
|
||||
type: inp("WhereRelations", row, () =>
|
||||
Object.fromEntries(
|
||||
edges.map((member) => {
|
||||
const target = relationRow(member);
|
||||
return [
|
||||
member.displayName,
|
||||
{
|
||||
type: inp(
|
||||
"RelationPredicate",
|
||||
target,
|
||||
(): GraphQLInputFieldConfigMap =>
|
||||
many(member)
|
||||
? {
|
||||
some: { type: where(target) },
|
||||
none: { type: where(target) },
|
||||
aggregate: {
|
||||
type: inp("ReductionPredicate", target, () => ({
|
||||
where: { type: where(target) },
|
||||
having: { type: new GraphQLNonNull(having(target)) },
|
||||
})),
|
||||
},
|
||||
}
|
||||
: { is: { type: where(target) }, isNull: { type: GraphQLBoolean } },
|
||||
rowKey(row) + member.id,
|
||||
),
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
),
|
||||
};
|
||||
return meta;
|
||||
}),
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
const fragments = new Map(
|
||||
(document?.definitions ?? [])
|
||||
.filter((d): d is FragmentDefinitionNode => d.kind === "FragmentDefinition")
|
||||
.map((d) => [d.name.value, d]),
|
||||
);
|
||||
let visited = 0;
|
||||
const selections = (set: SelectionSetNode, active: ReadonlySet<string> = new Set()): import("graphql").FieldNode[] =>
|
||||
set.selections.flatMap((node) => {
|
||||
if (++visited > 10000)
|
||||
throw new QueryCompileError("QUERY_WORK_LIMIT", "Query schema discovery exceeds 10000 nodes");
|
||||
if (node.kind === "Field") return [node];
|
||||
if (node.kind === "FragmentSpread") {
|
||||
if (active.has(node.name.value) || active.size >= 128)
|
||||
throw new QueryCompileError("QUERY_VALIDATION", "Cyclic or excessively nested fragments");
|
||||
const fragment = fragments.get(node.name.value);
|
||||
return fragment ? selections(fragment.selectionSet, new Set([...active, node.name.value])) : [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const discoverExpansion = (root: QueryRow, current: QueryRow, set: SelectionSetNode, path: string[] = []) => {
|
||||
for (const node of selections(set)) {
|
||||
if (!node.selectionSet) continue;
|
||||
const member = relations(current).find((m) => m.displayName === node.name.value);
|
||||
const next = [...path, node.name.value];
|
||||
if (member && many(member)) {
|
||||
const map = expansionPaths.get(rowKey(root)) ?? new Map();
|
||||
map.set(next.join("."), next);
|
||||
expansionPaths.set(rowKey(root), map);
|
||||
discoverRows({ kind: "pair", source: root, target: relationRow(member) }, node.selectionSet);
|
||||
} else {
|
||||
const field = fields(current).get(node.name.value);
|
||||
if (field?.kind === "row") discoverExpansion(root, field.row, node.selectionSet, next);
|
||||
}
|
||||
}
|
||||
};
|
||||
const discoverRows = (row: QueryRow, set: SelectionSetNode) => {
|
||||
rowsets.set(rowKey(row), row);
|
||||
if (rowsets.size > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 row shapes");
|
||||
for (const node of selections(set))
|
||||
if (node.selectionSet) {
|
||||
if (node.name.value === "expand") discoverExpansion(row, row, node.selectionSet);
|
||||
else if (node.name.value === "filter" || node.name.value === "distinct") discoverRows(row, node.selectionSet);
|
||||
}
|
||||
};
|
||||
const discoverObject = (row: QueryRow, set: SelectionSetNode) => {
|
||||
for (const node of selections(set))
|
||||
if (node.selectionSet) {
|
||||
if (node.name.value === "_qx") {
|
||||
for (const meta of selections(node.selectionSet))
|
||||
if (meta.name.value === "relations" && meta.selectionSet)
|
||||
for (const edge of selections(meta.selectionSet)) {
|
||||
const member = relations(row).find((m) => m.displayName === edge.name.value);
|
||||
if (member && many(member) && edge.selectionSet) discoverRows(relationRow(member), edge.selectionSet);
|
||||
}
|
||||
} else {
|
||||
const member = relations(row).find((m) => m.displayName === node.name.value);
|
||||
if (member) {
|
||||
if (!many(member)) discoverObject(objectRow(env.target(member)), node.selectionSet);
|
||||
else
|
||||
for (const entries of selections(node.selectionSet))
|
||||
if (entries.name.value === "entries" && entries.selectionSet)
|
||||
for (const child of selections(entries.selectionSet))
|
||||
if (child.name.value === "node" && child.selectionSet)
|
||||
discoverObject(objectRow(env.target(member)), child.selectionSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const discover = (root: InterfaceRevisionId) => {
|
||||
for (const operation of document?.definitions ?? [])
|
||||
if (operation.kind === "OperationDefinition")
|
||||
for (const node of selections(operation.selectionSet))
|
||||
if (node.name.value === "root" && node.selectionSet) discoverObject(objectRow(root), node.selectionSet);
|
||||
};
|
||||
const expand = (root: QueryRow, current: QueryRow, path: string[] = []): GraphQLObjectType =>
|
||||
out(
|
||||
"Expand",
|
||||
current,
|
||||
() => {
|
||||
const result: GraphQLFieldConfigMap<unknown, unknown> = {};
|
||||
const paths = [...(expansionPaths.get(rowKey(root))?.values() ?? [])].filter((p) =>
|
||||
path.every((part, i) => p[i] === part),
|
||||
);
|
||||
for (const name of new Set(paths.map((p) => p[path.length]).filter((v): v is string => !!v))) {
|
||||
const member = relations(current).find((m) => m.displayName === name);
|
||||
if (member && many(member))
|
||||
result[name] = {
|
||||
type: new GraphQLNonNull(rowset({ kind: "pair", source: root, target: relationRow(member) })),
|
||||
};
|
||||
else {
|
||||
const field = fields(current).get(name);
|
||||
if (field?.kind === "row")
|
||||
result[name] = { type: new GraphQLNonNull(expand(root, field.row, [...path, name])) };
|
||||
}
|
||||
}
|
||||
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
||||
return result;
|
||||
},
|
||||
rowKey(root) + JSON.stringify(path),
|
||||
);
|
||||
const rowset = (row: QueryRow): GraphQLObjectType => {
|
||||
const result = out("Rows", row, () => {
|
||||
const entry = out("GroupEntry", row, () => ({
|
||||
key: { type: new GraphQLNonNull(GraphQLString) },
|
||||
cursor: { type: env.cursor },
|
||||
group: { type: new GraphQLNonNull(columns(row)) },
|
||||
aggregate: { type: new GraphQLNonNull(aggregate(row)) },
|
||||
}));
|
||||
const connection = out("Groups", row, () => ({
|
||||
entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) },
|
||||
pageInfo: { type: new GraphQLNonNull(env.pageInfo) },
|
||||
}));
|
||||
roles.set(connection.name, { kind: "groups", row });
|
||||
return {
|
||||
filter: { type: new GraphQLNonNull(rowset(row)), args: { where: { type: new GraphQLNonNull(where(row)) } } },
|
||||
distinct: { type: new GraphQLNonNull(rowset(row)), args: { by: { type: new GraphQLNonNull(keys(row)) } } },
|
||||
expand: { type: new GraphQLNonNull(expand(row, row)) },
|
||||
aggregate: { type: new GraphQLNonNull(aggregate(row)), args: { where: { type: where(row) } } },
|
||||
groups: {
|
||||
type: new GraphQLNonNull(connection),
|
||||
args: {
|
||||
by: { type: new GraphQLNonNull(keys(row)) },
|
||||
where: { type: where(row) },
|
||||
having: { type: having(row) },
|
||||
orderBy: { type: new GraphQLList(new GraphQLNonNull(having(row, true))) },
|
||||
first: { type: GraphQLInt },
|
||||
all: { type: GraphQLInt },
|
||||
after: { type: env.cursor },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
roles.set(result.name, { kind: "rowset", row });
|
||||
return result;
|
||||
};
|
||||
const helpers = (id: InterfaceRevisionId): GraphQLObjectType | undefined => {
|
||||
const row = objectRow(id),
|
||||
edges = relations(row).filter(many);
|
||||
if (!edges.length) return undefined;
|
||||
const result = out("Relations", row, () =>
|
||||
Object.fromEntries(
|
||||
edges.map((member) => [member.displayName, { type: new GraphQLNonNull(rowset(relationRow(member))) }]),
|
||||
),
|
||||
);
|
||||
roles.set(result.name, { kind: "relations", row });
|
||||
return result;
|
||||
};
|
||||
return { refs, fields, relations, relationRow, where, rowset, helpers, roles, discover, scalar, compare };
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js";
|
||||
import { valueType } from "../capability-model/types.js";
|
||||
import { QueryCompileError, type QueryArgument, type QueryUse } from "./types.js";
|
||||
|
||||
/** A row is an input membership, not a newly allocated Camino object. */
|
||||
export type QueryRow =
|
||||
| {
|
||||
kind: "object";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
membership?: { keyType?: "string" | "boolean" | "int64" };
|
||||
}
|
||||
| { kind: "pair"; source: QueryRow; target: QueryRow };
|
||||
|
||||
export type AggregateOperator = "count" | "countPresent" | "sum" | "avg" | "min" | "max";
|
||||
export const aggregateOperators: readonly AggregateOperator[] = ["count", "countPresent", "sum", "avg", "min", "max"];
|
||||
|
||||
export function aggregateType(operator: AggregateOperator, input?: ValueType): ValueType {
|
||||
if (operator === "count") return valueType.uint64;
|
||||
const base = input?.kind === "optional" ? input.value : input;
|
||||
if (operator === "countPresent" && (base?.kind === "scalar" || base?.kind === "object-ref")) return valueType.uint64;
|
||||
if (base?.kind !== "scalar")
|
||||
throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} requires a supported scalar operand`);
|
||||
const numeric = ["int32", "uint32", "int64", "uint64", "double"].includes(base.name);
|
||||
if ((operator === "min" || operator === "max") && (numeric || base.name === "string"))
|
||||
return valueType.optional(base);
|
||||
if (numeric && operator === "avg") return valueType.optional(valueType.double);
|
||||
if (numeric && operator === "sum")
|
||||
return valueType.optional(
|
||||
base.name === "double" ? valueType.double : base.name.startsWith("u") ? valueType.uint64 : valueType.int64,
|
||||
);
|
||||
throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} does not accept ${base.name}`);
|
||||
}
|
||||
|
||||
export function queryKeyType(type: ValueType): boolean {
|
||||
if (type.kind === "optional") return queryKeyType(type.value);
|
||||
return type.kind === "object-ref" || (type.kind === "scalar" && type.name !== "bytes");
|
||||
}
|
||||
|
||||
/** Paths are resolved once by the compiler. Runtime never resolves authored names. */
|
||||
export type QueryPathStep =
|
||||
| { kind: "source" | "target" }
|
||||
| {
|
||||
kind: "relation";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
targetInterfaceRevisionId: InterfaceRevisionId;
|
||||
optional: boolean;
|
||||
};
|
||||
export interface QueryExpression {
|
||||
path: QueryPathStep[];
|
||||
leaf:
|
||||
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId }
|
||||
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
|
||||
| { kind: "entry" | "mapKey" };
|
||||
type: ValueType;
|
||||
}
|
||||
export interface QueryReduction {
|
||||
operator: AggregateOperator;
|
||||
operand?: QueryExpression;
|
||||
type: ValueType;
|
||||
}
|
||||
export type QueryPredicate =
|
||||
| { kind: "and" | "or"; children: QueryPredicate[] }
|
||||
| { kind: "not"; child: QueryPredicate }
|
||||
| {
|
||||
kind: "compare";
|
||||
expression: QueryExpression;
|
||||
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
||||
value: QueryArgument;
|
||||
}
|
||||
| {
|
||||
kind: "relation";
|
||||
path: QueryPathStep[];
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
targetInterfaceRevisionId: InterfaceRevisionId;
|
||||
operator: "some" | "none" | "is" | "isNull";
|
||||
predicate?: QueryPredicate;
|
||||
value?: QueryArgument;
|
||||
}
|
||||
| {
|
||||
kind: "reduce";
|
||||
path: QueryPathStep[];
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
targetInterfaceRevisionId: InterfaceRevisionId;
|
||||
where?: QueryPredicate;
|
||||
having: QueryAggregatePredicate;
|
||||
};
|
||||
export type QueryAggregatePredicate =
|
||||
| { kind: "and" | "or"; children: QueryAggregatePredicate[] }
|
||||
| { kind: "not"; child: QueryAggregatePredicate }
|
||||
| {
|
||||
kind: "compare";
|
||||
expression: QueryExpression | QueryReduction;
|
||||
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
||||
value: QueryArgument;
|
||||
};
|
||||
export interface QueryKey {
|
||||
path: string[];
|
||||
expression: QueryExpression;
|
||||
}
|
||||
export interface QueryRelationalStage {
|
||||
id: number;
|
||||
input?: number;
|
||||
row: QueryRow;
|
||||
operation:
|
||||
| {
|
||||
kind: "source";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
targetInterfaceRevisionId: InterfaceRevisionId;
|
||||
}
|
||||
| { kind: "filter"; predicate: QueryPredicate }
|
||||
| {
|
||||
kind: "expand";
|
||||
path: QueryPathStep[];
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
targetInterfaceRevisionId: InterfaceRevisionId;
|
||||
}
|
||||
| { kind: "distinct"; keys: QueryKey[] };
|
||||
}
|
||||
export interface QueryRelationalTerminal {
|
||||
stage: number;
|
||||
path: string[];
|
||||
kind: "aggregate" | "groups";
|
||||
keys: QueryKey[];
|
||||
reductions: { path: string[]; reduction: QueryReduction }[];
|
||||
where?: QueryPredicate;
|
||||
having?: QueryAggregatePredicate;
|
||||
order: { expression: QueryExpression | QueryReduction; descending: boolean }[];
|
||||
first?: QueryArgument;
|
||||
all?: QueryArgument;
|
||||
after?: QueryArgument;
|
||||
/** Projection tree remains separate from the operator plan. */
|
||||
selection: import("./types.js").QuerySelection[];
|
||||
residual: boolean;
|
||||
}
|
||||
export interface QueryRelationalPlan {
|
||||
stages: QueryRelationalStage[];
|
||||
terminals: QueryRelationalTerminal[];
|
||||
}
|
||||
export type QueryEffectRecorder = (
|
||||
id: InterfaceRevisionId,
|
||||
name: string,
|
||||
use: QueryUse,
|
||||
node: import("graphql").ASTNode,
|
||||
) => void;
|
||||
@@ -0,0 +1,250 @@
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
GraphQLInt,
|
||||
GraphQLFloat,
|
||||
GraphQLScalarType,
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLEnumType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLSchema,
|
||||
type GraphQLOutputType,
|
||||
type GraphQLInputType,
|
||||
type GraphQLFieldConfigMap,
|
||||
type GraphQLInputFieldConfigMap,
|
||||
type DocumentNode,
|
||||
} from "graphql";
|
||||
import { createHash } from "node:crypto";
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
InterfaceRevisionId,
|
||||
ValueType,
|
||||
RelationshipInterfaceMember,
|
||||
} from "../capability-model/types.js";
|
||||
import { QueryCompileError, type QueryDeclaration } from "./types.js";
|
||||
import { relationalSchema, objectRow } from "./relational-schema.js";
|
||||
|
||||
// Validate losslessly; callers carry decimal strings across JSON transports.
|
||||
function integerParser(name: string, min: bigint, max: bigint, value: unknown): string {
|
||||
if (
|
||||
typeof value !== "string" &&
|
||||
typeof value !== "bigint" &&
|
||||
!(typeof value === "number" && Number.isSafeInteger(value))
|
||||
)
|
||||
throw new Error(`${name} requires a canonical integer`);
|
||||
const text = String(value);
|
||||
if (!/^-?(0|[1-9][0-9]*)$/.test(text) || text === "-0") throw new Error(`${name} requires a canonical integer`);
|
||||
const n = BigInt(text);
|
||||
if (n < min || n > max) throw new Error(`${name} out of range`);
|
||||
return text;
|
||||
}
|
||||
const integerScalar = (name: string, min: bigint, max: bigint) => {
|
||||
const parse = (v: unknown) => integerParser(name, min, max, v);
|
||||
return new GraphQLScalarType({
|
||||
name,
|
||||
serialize: parse,
|
||||
parseValue: parse,
|
||||
parseLiteral(node) {
|
||||
if (node.kind !== "IntValue" && node.kind !== "StringValue") throw new Error(`${name} requires an integer`);
|
||||
return parse(node.value);
|
||||
},
|
||||
});
|
||||
};
|
||||
export const queryScalars = {
|
||||
bool: GraphQLBoolean,
|
||||
string: GraphQLString,
|
||||
int32: GraphQLInt,
|
||||
int64: integerScalar("Int64", -(1n << 63n), (1n << 63n) - 1n),
|
||||
uint32: integerScalar("UInt32", 0n, (1n << 32n) - 1n),
|
||||
uint64: integerScalar("UInt64", 0n, (1n << 64n) - 1n),
|
||||
double: GraphQLFloat,
|
||||
bytes: new GraphQLScalarType({ name: "Bytes" }),
|
||||
};
|
||||
export const cursorScalar = new GraphQLScalarType({
|
||||
name: "Cursor",
|
||||
parseValue(value) {
|
||||
if (typeof value !== "string" || value.length > 4096) throw new Error("Invalid cursor");
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
export function querySchema(
|
||||
declaration: QueryDeclaration,
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
document?: DocumentNode,
|
||||
) {
|
||||
const contracts = new Map(interfaces.map((entry) => [entry.revisionId, entry]));
|
||||
const objects = new Map<InterfaceRevisionId, GraphQLObjectType>();
|
||||
const orders = new Map<InterfaceRevisionId, GraphQLInputObjectType>();
|
||||
const byName = new Map<string, InterfaceRevision>();
|
||||
const comparisons = new Map<string, GraphQLInputObjectType>();
|
||||
const scalarTypes = new Map<string, ValueType>(
|
||||
Object.entries(queryScalars).map(([name, type]) => [type.name, { kind: "scalar", name } as ValueType]),
|
||||
);
|
||||
scalarTypes.set("Cursor", { kind: "scalar", name: "string" });
|
||||
const pageInfo = new GraphQLObjectType({
|
||||
name: "QxPageInfo",
|
||||
fields: {
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
endCursor: { type: cursorScalar },
|
||||
},
|
||||
});
|
||||
const direction = new GraphQLEnumType({ name: "QxDirection", values: { ASC: {}, DESC: {} } });
|
||||
const contract = (id: InterfaceRevisionId) => {
|
||||
const found = contracts.get(id);
|
||||
if (!found || found.template) throw new QueryCompileError("QUERY_CONTRACT", `Expected closed interface ${id}`);
|
||||
return found;
|
||||
};
|
||||
const name = (id: InterfaceRevisionId) => {
|
||||
const revision = contract(id);
|
||||
const result =
|
||||
revision.displayName +
|
||||
(revision.application ? `_${createHash("sha256").update(id).digest("hex").slice(0, 12)}` : "");
|
||||
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(result) || result.startsWith("__") || result.startsWith("Qx"))
|
||||
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query interface name ${result}`);
|
||||
const existing = byName.get(result);
|
||||
if (existing && existing.revisionId !== id)
|
||||
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Conflicting query interface name ${result}`);
|
||||
byName.set(result, revision);
|
||||
return result;
|
||||
};
|
||||
const target = (member: RelationshipInterfaceMember): InterfaceRevisionId => {
|
||||
if (member.target.kind === "interface") return member.target.interfaceRevisionId;
|
||||
const atomId = member.target.atomId;
|
||||
const views = declaration.views.filter((entry) => entry.atomId === atomId);
|
||||
if (views.length !== 1)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_VIEW_REQUIRED",
|
||||
`Declare exactly one interface view for ${member.target.atomId}`,
|
||||
);
|
||||
return views[0]!.interfaceRevisionId;
|
||||
};
|
||||
const scalar = (type: ValueType): GraphQLOutputType & GraphQLInputType => {
|
||||
if (type.kind === "optional") return scalar(type.value);
|
||||
if (type.kind !== "scalar")
|
||||
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
|
||||
return queryScalars[type.name];
|
||||
};
|
||||
const comparison = (type: ValueType) => {
|
||||
const base = type.kind === "optional" ? type.value : type;
|
||||
const value = scalar(base);
|
||||
const key = String(value);
|
||||
let result = comparisons.get(key);
|
||||
if (!result) {
|
||||
const fields: GraphQLInputFieldConfigMap = { eq: { type: value }, isNull: { type: GraphQLBoolean } };
|
||||
if (base.kind === "scalar" && base.name !== "bytes")
|
||||
fields.in = { type: new GraphQLList(new GraphQLNonNull(value)) };
|
||||
if (base.kind === "scalar" && base.name !== "bool" && base.name !== "bytes")
|
||||
for (const op of ["lt", "lte", "gt", "gte"]) fields[op] = { type: value };
|
||||
result = new GraphQLInputObjectType({ name: `QxCompare${key}`, fields });
|
||||
comparisons.set(key, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const relational = relationalSchema(
|
||||
{ contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo },
|
||||
document,
|
||||
);
|
||||
relational.discover(declaration.root);
|
||||
const filter = (id: InterfaceRevisionId) => relational.where(objectRow(id));
|
||||
const order = (id: InterfaceRevisionId) => {
|
||||
let result = orders.get(id);
|
||||
if (!result) {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
for (const member of contract(id).members) {
|
||||
if (member.kind !== "value" || !member.queryRead) continue;
|
||||
const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
||||
if (t.kind === "scalar" && t.name !== "bytes") fields[member.displayName] = { type: direction };
|
||||
}
|
||||
if (Object.keys(fields).length === 0) return undefined;
|
||||
result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields });
|
||||
orders.set(id, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const object = (id: InterfaceRevisionId): GraphQLObjectType => {
|
||||
const existing = objects.get(id);
|
||||
if (existing) return existing;
|
||||
const result = new GraphQLObjectType({
|
||||
name: name(id),
|
||||
fields: () => {
|
||||
const helpers = relational.helpers(id);
|
||||
const metadata = new GraphQLObjectType({
|
||||
name: `QxMetadata_${name(id)}`,
|
||||
fields: {
|
||||
ref: { type: new GraphQLNonNull(relational.refs(id)) },
|
||||
...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}),
|
||||
},
|
||||
});
|
||||
const fields: GraphQLFieldConfigMap<unknown, unknown> = { _qx: { type: new GraphQLNonNull(metadata) } };
|
||||
for (const member of contract(id).members) {
|
||||
if (member.kind === "operation" || !member.queryRead) continue;
|
||||
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
||||
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`);
|
||||
if (member.kind === "value") {
|
||||
const type = scalar(member.valueType);
|
||||
fields[member.displayName] = {
|
||||
type: member.valueType.kind === "optional" ? type : new GraphQLNonNull(type),
|
||||
};
|
||||
} else {
|
||||
const targetId = target(member),
|
||||
node = object(targetId);
|
||||
if (member.cardinality === "exactly-one" || member.cardinality === "optional-one")
|
||||
fields[member.displayName] = {
|
||||
type: member.cardinality === "exactly-one" ? new GraphQLNonNull(node) : node,
|
||||
};
|
||||
else {
|
||||
const entry = new GraphQLObjectType({
|
||||
name: `${name(id)}_${member.displayName}_Entry`,
|
||||
fields: {
|
||||
key: { type: new GraphQLNonNull(GraphQLString) },
|
||||
...(member.keyType
|
||||
? {
|
||||
mapKey: {
|
||||
type: new GraphQLNonNull(
|
||||
queryScalars[member.keyType === "boolean" ? "bool" : member.keyType],
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
cursor: { type: cursorScalar },
|
||||
node: { type: new GraphQLNonNull(node) },
|
||||
},
|
||||
});
|
||||
const connection = new GraphQLObjectType({
|
||||
name: `${name(id)}_${member.displayName}_Connection`,
|
||||
fields: {
|
||||
entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) },
|
||||
pageInfo: { type: new GraphQLNonNull(pageInfo) },
|
||||
},
|
||||
});
|
||||
const ordering = order(targetId);
|
||||
fields[member.displayName] = {
|
||||
type: new GraphQLNonNull(connection),
|
||||
args: {
|
||||
first: { type: GraphQLInt },
|
||||
all: { type: GraphQLInt },
|
||||
after: { type: cursorScalar },
|
||||
where: { type: filter(targetId) },
|
||||
...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
});
|
||||
objects.set(id, result);
|
||||
return result;
|
||||
};
|
||||
const schema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: "QxQuery",
|
||||
fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } },
|
||||
}),
|
||||
});
|
||||
return { schema, byName, target, contracts, relational, scalarTypes };
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { capabilityId, type InterfaceRevision, type InterfaceRevisionId } from "../capability-model/types.js";
|
||||
import {
|
||||
TypeSubstitution,
|
||||
bindTypeParameters,
|
||||
type ClosedTypeArgument,
|
||||
type GenericTypeEnvironment,
|
||||
} from "../capability-model/generics.js";
|
||||
import { GenericSourceTypes } from "../capability-language/generic-types.js";
|
||||
import { compileQuery } from "./compile.js";
|
||||
import { QueryCompileError, type QueryDeclaration, type QueryTemplate } from "./types.js";
|
||||
|
||||
export function specializeQueryTemplate(
|
||||
template: QueryTemplate,
|
||||
arguments_: ClosedTypeArgument[],
|
||||
environment: GenericTypeEnvironment,
|
||||
interfaces: () => readonly InterfaceRevision[],
|
||||
identity: { id: string; displayName: string },
|
||||
): QueryDeclaration {
|
||||
const obligations: NonNullable<QueryDeclaration["argumentRequirements"]> = [];
|
||||
const checked = {
|
||||
...environment,
|
||||
implementsInterface: (
|
||||
target: Parameters<GenericTypeEnvironment["implementsInterface"]>[0],
|
||||
required: InterfaceRevisionId,
|
||||
) => {
|
||||
obligations.push({ target, required });
|
||||
return environment.implementsInterface(target, required);
|
||||
},
|
||||
};
|
||||
const substitution = new TypeSubstitution({
|
||||
...checked,
|
||||
arguments: bindTypeParameters(template.parameters, arguments_, checked, template.declaration.displayName),
|
||||
});
|
||||
return {
|
||||
...structuredClone(template.declaration),
|
||||
...identity,
|
||||
root: substitution.application(template.root),
|
||||
views: template.views
|
||||
.map((view) => {
|
||||
const target = substitution.object(view.target);
|
||||
const interfaceRevisionId = substitution.application(view.interface);
|
||||
if (target.kind === "interface") {
|
||||
if (target.interfaceRevisionId !== interfaceRevisionId)
|
||||
throw new QueryCompileError(
|
||||
"QUERY_VIEW_REQUIRED",
|
||||
"An interface argument must use its exact selected query view",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return { atomId: target.atomId, interfaceRevisionId };
|
||||
})
|
||||
.filter((view): view is NonNullable<typeof view> => !!view),
|
||||
allowances: template.allowances.map((allowance) => {
|
||||
const interfaceRevisionId = substitution.application(allowance.interface);
|
||||
const member = interfaces()
|
||||
.find((entry) => entry.revisionId === interfaceRevisionId)
|
||||
?.members.find((entry) => entry.displayName === allowance.memberName);
|
||||
if (!member) throw new QueryCompileError("QUERY_ALLOWANCE", `Unknown template field ${allowance.memberName}`);
|
||||
return { interfaceRevisionId, memberId: member.id, uses: allowance.uses, reason: allowance.reason };
|
||||
}),
|
||||
application: { templateId: template.declaration.id, arguments: structuredClone(arguments_) },
|
||||
argumentRequirements: obligations,
|
||||
};
|
||||
}
|
||||
|
||||
/** Check a document with only its declared bounds available, even if no concrete
|
||||
* specialization is installed. No concrete atom fields can leak into this check. */
|
||||
export async function checkQueryTemplate(
|
||||
template: QueryTemplate,
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
read: (name: string) => Promise<string>,
|
||||
) {
|
||||
const types = new GenericSourceTypes(new Map());
|
||||
for (const contract of interfaces) types.register(contract.displayName, contract);
|
||||
const arguments_: ClosedTypeArgument[] = template.parameters.map((parameter) => {
|
||||
if (parameter.kind !== "object")
|
||||
throw new QueryCompileError(
|
||||
"QUERY_TEMPLATE_PARAMETER",
|
||||
"Query templates currently require object parameters with explicit interface views; scalar/value polymorphism is not queryable",
|
||||
);
|
||||
return {
|
||||
kind: "object",
|
||||
target: { kind: "atom", atomId: capabilityId.atom(`query-bound:${template.declaration.id}:${parameter.id}`) },
|
||||
};
|
||||
});
|
||||
const bindings = new Map(template.parameters.map((parameter, index) => [parameter.id, arguments_[index]!]));
|
||||
const substitution = new TypeSubstitution({ ...types.environment(), arguments: bindings });
|
||||
const evidence = new Map(
|
||||
arguments_.map((argument, index) => {
|
||||
const parameter = template.parameters[index]!;
|
||||
const target = argument.kind === "object" && argument.target.kind === "atom" ? argument.target.atomId : "";
|
||||
return [
|
||||
target,
|
||||
new Set(
|
||||
parameter.kind === "object" ? parameter.implements.map((bound) => substitution.application(bound)) : [],
|
||||
),
|
||||
];
|
||||
}),
|
||||
);
|
||||
const implies = (offered: string, required: string, seen = new Set<string>()): boolean => {
|
||||
if (offered === required) return true;
|
||||
if (seen.has(offered)) return false;
|
||||
seen.add(offered);
|
||||
const contract = types.definitions.get(offered);
|
||||
return (contract?.requiredInterfaces ?? []).some((parent) => implies(parent, required, seen));
|
||||
};
|
||||
const environment = {
|
||||
...types.environment(),
|
||||
implementsInterface: (
|
||||
target: Parameters<GenericTypeEnvironment["implementsInterface"]>[0],
|
||||
required: InterfaceRevisionId,
|
||||
) =>
|
||||
target.kind === "atom" && [...(evidence.get(target.atomId) ?? [])].some((offered) => implies(offered, required)),
|
||||
};
|
||||
const declaration = specializeQueryTemplate(
|
||||
template,
|
||||
arguments_,
|
||||
environment,
|
||||
() => [...types.definitions.values()],
|
||||
template.declaration,
|
||||
);
|
||||
for (const view of declaration.views)
|
||||
if (!environment.implementsInterface({ kind: "atom", atomId: view.atomId }, view.interfaceRevisionId))
|
||||
throw new QueryCompileError(
|
||||
"QUERY_TEMPLATE_BOUND",
|
||||
`The declared bounds do not guarantee the selected view ${view.interfaceRevisionId}`,
|
||||
);
|
||||
for (const application of types.applications.values())
|
||||
for (const obligation of application.argumentRequirements ?? [])
|
||||
if (!environment.implementsInterface(obligation.target, obligation.required))
|
||||
throw new QueryCompileError(
|
||||
"QUERY_TEMPLATE_BOUND",
|
||||
`Template arguments do not guarantee ${obligation.required}`,
|
||||
);
|
||||
return compileQuery(declaration, [...types.definitions.values()], read);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { AtomId, InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js";
|
||||
import type {
|
||||
TypeParameter,
|
||||
InterfaceApplicationExpression,
|
||||
ObjectTypeExpression,
|
||||
ClosedTypeArgument,
|
||||
} from "../capability-model/generics.js";
|
||||
|
||||
export type QueryUse = "select" | "predicate" | "order" | "aggregate" | "group" | "distinct";
|
||||
export interface QueryAllowance {
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
uses: QueryUse[];
|
||||
reason: string;
|
||||
}
|
||||
export interface QueryBudgets {
|
||||
rows: number;
|
||||
depth: number;
|
||||
resultBytes: number;
|
||||
candidates: number;
|
||||
rpcCalls: number;
|
||||
concurrency: number;
|
||||
deadlineMs: number;
|
||||
}
|
||||
export const defaultQueryBudgets: Readonly<QueryBudgets> = Object.freeze({
|
||||
rows: 100,
|
||||
depth: 8,
|
||||
resultBytes: 1048576,
|
||||
candidates: 100,
|
||||
rpcCalls: 200,
|
||||
concurrency: 8,
|
||||
deadlineMs: 10000,
|
||||
});
|
||||
export interface QueryDeclaration {
|
||||
id: string;
|
||||
displayName: string;
|
||||
root: InterfaceRevisionId;
|
||||
document: string;
|
||||
operation: string;
|
||||
fragments: string[];
|
||||
views: { atomId: AtomId; interfaceRevisionId: InterfaceRevisionId }[];
|
||||
allowances: QueryAllowance[];
|
||||
budgets: QueryBudgets;
|
||||
watch: boolean;
|
||||
polling?: { intervalMs: number; reason: string };
|
||||
application?: { templateId: string; arguments: ClosedTypeArgument[] };
|
||||
argumentRequirements?: {
|
||||
target: import("../capability-model/types.js").ObjectExpectation;
|
||||
required: InterfaceRevisionId;
|
||||
}[];
|
||||
}
|
||||
/** Source-only template; neither parameters nor synthetic bound witnesses are installed. */
|
||||
export interface QueryTemplate {
|
||||
declaration: Omit<QueryDeclaration, "root" | "views" | "allowances">;
|
||||
parameters: TypeParameter[];
|
||||
root: InterfaceApplicationExpression;
|
||||
views: { target: ObjectTypeExpression; interface: InterfaceApplicationExpression }[];
|
||||
allowances: { interface: InterfaceApplicationExpression; memberName: string; uses: QueryUse[]; reason: string }[];
|
||||
}
|
||||
export interface QueryFieldEffect {
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
memberId: MemberId;
|
||||
uses: QueryUse[];
|
||||
execution: "native" | "rpc-permitted";
|
||||
}
|
||||
export interface QuerySourceLocation {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
export class QueryCompileError extends Error {
|
||||
constructor(
|
||||
readonly code: string,
|
||||
message: string,
|
||||
readonly location?: QuerySourceLocation,
|
||||
) {
|
||||
super(`${code}${location ? ` ${location.file}:${location.line}:${location.column}` : ""}: ${message}`);
|
||||
this.name = "QueryCompileError";
|
||||
}
|
||||
}
|
||||
export interface CheckedQuery {
|
||||
declaration: QueryDeclaration;
|
||||
definitionDigest: string;
|
||||
/** Fixed, validated document: never supplied by runtime callers. */
|
||||
document: string;
|
||||
schema: string;
|
||||
variables: ValueType;
|
||||
output: ValueType;
|
||||
effects: QueryFieldEffect[];
|
||||
selection: QuerySelection[];
|
||||
variableDefaults: Record<string, QueryArgument>;
|
||||
sourceFiles: string[];
|
||||
}
|
||||
|
||||
export type QueryArgument =
|
||||
| { kind: "variable"; name: string }
|
||||
| { kind: "literal"; value: string | number | boolean | null }
|
||||
| { kind: "list"; values: QueryArgument[] }
|
||||
| { kind: "object"; fields: Record<string, QueryArgument> };
|
||||
export interface QuerySelection {
|
||||
name: string;
|
||||
key: string;
|
||||
interfaceRevisionId?: InterfaceRevisionId;
|
||||
memberId?: MemberId;
|
||||
targetInterfaceRevisionId?: InterfaceRevisionId;
|
||||
conditions: { include: boolean; value: QueryArgument }[];
|
||||
arguments: Record<string, QueryArgument>;
|
||||
selection: QuerySelection[];
|
||||
relational?: import("./relational.js").QueryRelationalPlan;
|
||||
predicate?: import("./relational.js").QueryPredicate;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
import { loadQuixosLock } from "./loader.js";
|
||||
|
||||
const fileName = process.argv[2];
|
||||
if (!fileName || process.argv.length !== 3) {
|
||||
console.error("Usage: quixos-lock-check <quixos.lock>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const result = await loadQuixosLock(fileName);
|
||||
if (!result.ok) {
|
||||
for (const diagnostic of result.diagnostics) {
|
||||
console.error(
|
||||
`${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column + 1}: ${diagnostic.phase}/${diagnostic.code}: ${diagnostic.message}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(result.lock, null, 2));
|
||||
@@ -0,0 +1,69 @@
|
||||
token literal names:
|
||||
null
|
||||
'quixos-lock'
|
||||
'fragment'
|
||||
'version'
|
||||
'quixos'
|
||||
'source'
|
||||
'interface'
|
||||
'package'
|
||||
'repository'
|
||||
'commit'
|
||||
'policy'
|
||||
'ref'
|
||||
'pinned'
|
||||
'track-release'
|
||||
'track-development'
|
||||
'import'
|
||||
'{'
|
||||
'}'
|
||||
';'
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
|
||||
token symbolic names:
|
||||
null
|
||||
QUIXOS_LOCK
|
||||
FRAGMENT
|
||||
VERSION
|
||||
QUIXOS
|
||||
SOURCE
|
||||
INTERFACE
|
||||
PACKAGE
|
||||
REPOSITORY
|
||||
COMMIT
|
||||
POLICY
|
||||
REF
|
||||
PINNED
|
||||
TRACK_RELEASE
|
||||
TRACK_DEVELOPMENT
|
||||
IMPORT
|
||||
LBRACE
|
||||
RBRACE
|
||||
SEMI
|
||||
INTEGER
|
||||
IDENTIFIER
|
||||
STRING_LITERAL
|
||||
LINE_COMMENT
|
||||
BLOCK_COMMENT
|
||||
WS
|
||||
|
||||
rule names:
|
||||
document
|
||||
quixosEntry
|
||||
resourceEntry
|
||||
importEntry
|
||||
resourceKind
|
||||
sourceBlock
|
||||
quixosSourceBlock
|
||||
sourcePolicy
|
||||
identifier
|
||||
stringLiteral
|
||||
|
||||
|
||||
atn:
|
||||
[4, 1, 24, 87, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 1, 0, 1, 0, 3, 0, 23, 8, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 5, 0, 31, 8, 0, 10, 0, 12, 0, 34, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 74, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 0, 0, 10, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 2, 1, 0, 6, 7, 1, 0, 12, 14, 81, 0, 20, 1, 0, 0, 0, 2, 38, 1, 0, 0, 0, 4, 42, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 51, 1, 0, 0, 0, 10, 53, 1, 0, 0, 0, 12, 62, 1, 0, 0, 0, 14, 80, 1, 0, 0, 0, 16, 82, 1, 0, 0, 0, 18, 84, 1, 0, 0, 0, 20, 22, 5, 1, 0, 0, 21, 23, 5, 2, 0, 0, 22, 21, 1, 0, 0, 0, 22, 23, 1, 0, 0, 0, 23, 24, 1, 0, 0, 0, 24, 25, 5, 3, 0, 0, 25, 26, 5, 19, 0, 0, 26, 32, 5, 16, 0, 0, 27, 31, 3, 2, 1, 0, 28, 31, 3, 6, 3, 0, 29, 31, 3, 4, 2, 0, 30, 27, 1, 0, 0, 0, 30, 28, 1, 0, 0, 0, 30, 29, 1, 0, 0, 0, 31, 34, 1, 0, 0, 0, 32, 30, 1, 0, 0, 0, 32, 33, 1, 0, 0, 0, 33, 35, 1, 0, 0, 0, 34, 32, 1, 0, 0, 0, 35, 36, 5, 17, 0, 0, 36, 37, 5, 0, 0, 1, 37, 1, 1, 0, 0, 0, 38, 39, 5, 4, 0, 0, 39, 40, 5, 5, 0, 0, 40, 41, 3, 12, 6, 0, 41, 3, 1, 0, 0, 0, 42, 43, 3, 8, 4, 0, 43, 44, 3, 16, 8, 0, 44, 45, 5, 5, 0, 0, 45, 46, 3, 10, 5, 0, 46, 5, 1, 0, 0, 0, 47, 48, 5, 15, 0, 0, 48, 49, 3, 18, 9, 0, 49, 50, 5, 18, 0, 0, 50, 7, 1, 0, 0, 0, 51, 52, 7, 0, 0, 0, 52, 9, 1, 0, 0, 0, 53, 54, 5, 16, 0, 0, 54, 55, 5, 8, 0, 0, 55, 56, 3, 18, 9, 0, 56, 57, 5, 18, 0, 0, 57, 58, 5, 9, 0, 0, 58, 59, 3, 18, 9, 0, 59, 60, 5, 18, 0, 0, 60, 61, 5, 17, 0, 0, 61, 11, 1, 0, 0, 0, 62, 63, 5, 16, 0, 0, 63, 64, 5, 8, 0, 0, 64, 65, 3, 18, 9, 0, 65, 73, 5, 18, 0, 0, 66, 67, 5, 10, 0, 0, 67, 68, 3, 14, 7, 0, 68, 69, 5, 18, 0, 0, 69, 70, 5, 11, 0, 0, 70, 71, 3, 18, 9, 0, 71, 72, 5, 18, 0, 0, 72, 74, 1, 0, 0, 0, 73, 66, 1, 0, 0, 0, 73, 74, 1, 0, 0, 0, 74, 75, 1, 0, 0, 0, 75, 76, 5, 9, 0, 0, 76, 77, 3, 18, 9, 0, 77, 78, 5, 18, 0, 0, 78, 79, 5, 17, 0, 0, 79, 13, 1, 0, 0, 0, 80, 81, 7, 1, 0, 0, 81, 15, 1, 0, 0, 0, 82, 83, 5, 20, 0, 0, 83, 17, 1, 0, 0, 0, 84, 85, 5, 21, 0, 0, 85, 19, 1, 0, 0, 0, 4, 22, 30, 32, 73]
|
||||
@@ -0,0 +1,42 @@
|
||||
QUIXOS_LOCK=1
|
||||
FRAGMENT=2
|
||||
VERSION=3
|
||||
QUIXOS=4
|
||||
SOURCE=5
|
||||
INTERFACE=6
|
||||
PACKAGE=7
|
||||
REPOSITORY=8
|
||||
COMMIT=9
|
||||
POLICY=10
|
||||
REF=11
|
||||
PINNED=12
|
||||
TRACK_RELEASE=13
|
||||
TRACK_DEVELOPMENT=14
|
||||
IMPORT=15
|
||||
LBRACE=16
|
||||
RBRACE=17
|
||||
SEMI=18
|
||||
INTEGER=19
|
||||
IDENTIFIER=20
|
||||
STRING_LITERAL=21
|
||||
LINE_COMMENT=22
|
||||
BLOCK_COMMENT=23
|
||||
WS=24
|
||||
'quixos-lock'=1
|
||||
'fragment'=2
|
||||
'version'=3
|
||||
'quixos'=4
|
||||
'source'=5
|
||||
'interface'=6
|
||||
'package'=7
|
||||
'repository'=8
|
||||
'commit'=9
|
||||
'policy'=10
|
||||
'ref'=11
|
||||
'pinned'=12
|
||||
'track-release'=13
|
||||
'track-development'=14
|
||||
'import'=15
|
||||
'{'=16
|
||||
'}'=17
|
||||
';'=18
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
|
||||
QUIXOS_LOCK=1
|
||||
FRAGMENT=2
|
||||
VERSION=3
|
||||
QUIXOS=4
|
||||
SOURCE=5
|
||||
INTERFACE=6
|
||||
PACKAGE=7
|
||||
REPOSITORY=8
|
||||
COMMIT=9
|
||||
POLICY=10
|
||||
REF=11
|
||||
PINNED=12
|
||||
TRACK_RELEASE=13
|
||||
TRACK_DEVELOPMENT=14
|
||||
IMPORT=15
|
||||
LBRACE=16
|
||||
RBRACE=17
|
||||
SEMI=18
|
||||
INTEGER=19
|
||||
IDENTIFIER=20
|
||||
STRING_LITERAL=21
|
||||
LINE_COMMENT=22
|
||||
BLOCK_COMMENT=23
|
||||
WS=24
|
||||
'quixos-lock'=1
|
||||
'fragment'=2
|
||||
'version'=3
|
||||
'quixos'=4
|
||||
'source'=5
|
||||
'interface'=6
|
||||
'package'=7
|
||||
'repository'=8
|
||||
'commit'=9
|
||||
'policy'=10
|
||||
'ref'=11
|
||||
'pinned'=12
|
||||
'track-release'=13
|
||||
'track-development'=14
|
||||
'import'=15
|
||||
'{'=16
|
||||
'}'=17
|
||||
';'=18
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
import * as antlr from "antlr4ng";
|
||||
import { Token } from "antlr4ng";
|
||||
|
||||
|
||||
export class QuixosLockLexer extends antlr.Lexer {
|
||||
public static readonly QUIXOS_LOCK = 1;
|
||||
public static readonly FRAGMENT = 2;
|
||||
public static readonly VERSION = 3;
|
||||
public static readonly QUIXOS = 4;
|
||||
public static readonly SOURCE = 5;
|
||||
public static readonly INTERFACE = 6;
|
||||
public static readonly PACKAGE = 7;
|
||||
public static readonly REPOSITORY = 8;
|
||||
public static readonly COMMIT = 9;
|
||||
public static readonly POLICY = 10;
|
||||
public static readonly REF = 11;
|
||||
public static readonly PINNED = 12;
|
||||
public static readonly TRACK_RELEASE = 13;
|
||||
public static readonly TRACK_DEVELOPMENT = 14;
|
||||
public static readonly IMPORT = 15;
|
||||
public static readonly LBRACE = 16;
|
||||
public static readonly RBRACE = 17;
|
||||
public static readonly SEMI = 18;
|
||||
public static readonly INTEGER = 19;
|
||||
public static readonly IDENTIFIER = 20;
|
||||
public static readonly STRING_LITERAL = 21;
|
||||
public static readonly LINE_COMMENT = 22;
|
||||
public static readonly BLOCK_COMMENT = 23;
|
||||
public static readonly WS = 24;
|
||||
|
||||
public static readonly channelNames = [
|
||||
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
|
||||
];
|
||||
|
||||
public static readonly literalNames = [
|
||||
null, "'quixos-lock'", "'fragment'", "'version'", "'quixos'", "'source'",
|
||||
"'interface'", "'package'", "'repository'", "'commit'", "'policy'",
|
||||
"'ref'", "'pinned'", "'track-release'", "'track-development'", "'import'",
|
||||
"'{'", "'}'", "';'"
|
||||
];
|
||||
|
||||
public static readonly symbolicNames = [
|
||||
null, "QUIXOS_LOCK", "FRAGMENT", "VERSION", "QUIXOS", "SOURCE",
|
||||
"INTERFACE", "PACKAGE", "REPOSITORY", "COMMIT", "POLICY", "REF",
|
||||
"PINNED", "TRACK_RELEASE", "TRACK_DEVELOPMENT", "IMPORT", "LBRACE",
|
||||
"RBRACE", "SEMI", "INTEGER", "IDENTIFIER", "STRING_LITERAL", "LINE_COMMENT",
|
||||
"BLOCK_COMMENT", "WS"
|
||||
];
|
||||
|
||||
public static readonly modeNames = [
|
||||
"DEFAULT_MODE",
|
||||
];
|
||||
|
||||
public static readonly ruleNames = [
|
||||
"QUIXOS_LOCK", "FRAGMENT", "VERSION", "QUIXOS", "SOURCE", "INTERFACE",
|
||||
"PACKAGE", "REPOSITORY", "COMMIT", "POLICY", "REF", "PINNED", "TRACK_RELEASE",
|
||||
"TRACK_DEVELOPMENT", "IMPORT", "LBRACE", "RBRACE", "SEMI", "INTEGER",
|
||||
"IDENTIFIER", "STRING_LITERAL", "ESC", "HEX", "LINE_COMMENT", "BLOCK_COMMENT",
|
||||
"WS",
|
||||
];
|
||||
|
||||
|
||||
public constructor(input: antlr.CharStream) {
|
||||
super(input);
|
||||
this.interpreter = new antlr.LexerATNSimulator(this, QuixosLockLexer._ATN, QuixosLockLexer.decisionsToDFA, new antlr.PredictionContextCache());
|
||||
}
|
||||
|
||||
public get grammarFileName(): string { return "QuixosLock.g4"; }
|
||||
|
||||
public get literalNames(): (string | null)[] { return QuixosLockLexer.literalNames; }
|
||||
public get symbolicNames(): (string | null)[] { return QuixosLockLexer.symbolicNames; }
|
||||
public get ruleNames(): string[] { return QuixosLockLexer.ruleNames; }
|
||||
|
||||
public get serializedATN(): number[] { return QuixosLockLexer._serializedATN; }
|
||||
|
||||
public get channelNames(): string[] { return QuixosLockLexer.channelNames; }
|
||||
|
||||
public get modeNames(): string[] { return QuixosLockLexer.modeNames; }
|
||||
|
||||
public static readonly _serializedATN: number[] = [
|
||||
4,0,24,261,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,
|
||||
2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,
|
||||
13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,
|
||||
19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,1,
|
||||
0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,
|
||||
3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,
|
||||
5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,
|
||||
7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,9,1,
|
||||
9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,
|
||||
11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,
|
||||
12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,
|
||||
13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,
|
||||
14,1,14,1,14,1,15,1,15,1,16,1,16,1,17,1,17,1,18,4,18,197,8,18,11,
|
||||
18,12,18,198,1,19,1,19,5,19,203,8,19,10,19,12,19,206,9,19,1,20,1,
|
||||
20,1,20,5,20,211,8,20,10,20,12,20,214,9,20,1,20,1,20,1,21,1,21,1,
|
||||
21,1,21,1,21,1,21,1,21,1,21,3,21,226,8,21,1,22,1,22,1,23,1,23,1,
|
||||
23,1,23,5,23,234,8,23,10,23,12,23,237,9,23,1,23,1,23,1,24,1,24,1,
|
||||
24,1,24,5,24,245,8,24,10,24,12,24,248,9,24,1,24,1,24,1,24,1,24,1,
|
||||
24,1,25,4,25,256,8,25,11,25,12,25,257,1,25,1,25,1,246,0,26,1,1,3,
|
||||
2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,
|
||||
29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,0,45,0,47,22,49,23,
|
||||
51,24,1,0,8,1,0,48,57,3,0,65,90,95,95,97,122,4,0,48,57,65,90,95,
|
||||
95,97,122,4,0,10,10,13,13,34,34,92,92,8,0,34,34,47,47,92,92,98,98,
|
||||
102,102,110,110,114,114,116,116,3,0,48,57,65,70,97,102,2,0,10,10,
|
||||
13,13,3,0,9,10,13,13,32,32,266,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,
|
||||
0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,
|
||||
0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,
|
||||
0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,
|
||||
0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,
|
||||
0,51,1,0,0,0,1,53,1,0,0,0,3,65,1,0,0,0,5,74,1,0,0,0,7,82,1,0,0,0,
|
||||
9,89,1,0,0,0,11,96,1,0,0,0,13,106,1,0,0,0,15,114,1,0,0,0,17,125,
|
||||
1,0,0,0,19,132,1,0,0,0,21,139,1,0,0,0,23,143,1,0,0,0,25,150,1,0,
|
||||
0,0,27,164,1,0,0,0,29,182,1,0,0,0,31,189,1,0,0,0,33,191,1,0,0,0,
|
||||
35,193,1,0,0,0,37,196,1,0,0,0,39,200,1,0,0,0,41,207,1,0,0,0,43,217,
|
||||
1,0,0,0,45,227,1,0,0,0,47,229,1,0,0,0,49,240,1,0,0,0,51,255,1,0,
|
||||
0,0,53,54,5,113,0,0,54,55,5,117,0,0,55,56,5,105,0,0,56,57,5,120,
|
||||
0,0,57,58,5,111,0,0,58,59,5,115,0,0,59,60,5,45,0,0,60,61,5,108,0,
|
||||
0,61,62,5,111,0,0,62,63,5,99,0,0,63,64,5,107,0,0,64,2,1,0,0,0,65,
|
||||
66,5,102,0,0,66,67,5,114,0,0,67,68,5,97,0,0,68,69,5,103,0,0,69,70,
|
||||
5,109,0,0,70,71,5,101,0,0,71,72,5,110,0,0,72,73,5,116,0,0,73,4,1,
|
||||
0,0,0,74,75,5,118,0,0,75,76,5,101,0,0,76,77,5,114,0,0,77,78,5,115,
|
||||
0,0,78,79,5,105,0,0,79,80,5,111,0,0,80,81,5,110,0,0,81,6,1,0,0,0,
|
||||
82,83,5,113,0,0,83,84,5,117,0,0,84,85,5,105,0,0,85,86,5,120,0,0,
|
||||
86,87,5,111,0,0,87,88,5,115,0,0,88,8,1,0,0,0,89,90,5,115,0,0,90,
|
||||
91,5,111,0,0,91,92,5,117,0,0,92,93,5,114,0,0,93,94,5,99,0,0,94,95,
|
||||
5,101,0,0,95,10,1,0,0,0,96,97,5,105,0,0,97,98,5,110,0,0,98,99,5,
|
||||
116,0,0,99,100,5,101,0,0,100,101,5,114,0,0,101,102,5,102,0,0,102,
|
||||
103,5,97,0,0,103,104,5,99,0,0,104,105,5,101,0,0,105,12,1,0,0,0,106,
|
||||
107,5,112,0,0,107,108,5,97,0,0,108,109,5,99,0,0,109,110,5,107,0,
|
||||
0,110,111,5,97,0,0,111,112,5,103,0,0,112,113,5,101,0,0,113,14,1,
|
||||
0,0,0,114,115,5,114,0,0,115,116,5,101,0,0,116,117,5,112,0,0,117,
|
||||
118,5,111,0,0,118,119,5,115,0,0,119,120,5,105,0,0,120,121,5,116,
|
||||
0,0,121,122,5,111,0,0,122,123,5,114,0,0,123,124,5,121,0,0,124,16,
|
||||
1,0,0,0,125,126,5,99,0,0,126,127,5,111,0,0,127,128,5,109,0,0,128,
|
||||
129,5,109,0,0,129,130,5,105,0,0,130,131,5,116,0,0,131,18,1,0,0,0,
|
||||
132,133,5,112,0,0,133,134,5,111,0,0,134,135,5,108,0,0,135,136,5,
|
||||
105,0,0,136,137,5,99,0,0,137,138,5,121,0,0,138,20,1,0,0,0,139,140,
|
||||
5,114,0,0,140,141,5,101,0,0,141,142,5,102,0,0,142,22,1,0,0,0,143,
|
||||
144,5,112,0,0,144,145,5,105,0,0,145,146,5,110,0,0,146,147,5,110,
|
||||
0,0,147,148,5,101,0,0,148,149,5,100,0,0,149,24,1,0,0,0,150,151,5,
|
||||
116,0,0,151,152,5,114,0,0,152,153,5,97,0,0,153,154,5,99,0,0,154,
|
||||
155,5,107,0,0,155,156,5,45,0,0,156,157,5,114,0,0,157,158,5,101,0,
|
||||
0,158,159,5,108,0,0,159,160,5,101,0,0,160,161,5,97,0,0,161,162,5,
|
||||
115,0,0,162,163,5,101,0,0,163,26,1,0,0,0,164,165,5,116,0,0,165,166,
|
||||
5,114,0,0,166,167,5,97,0,0,167,168,5,99,0,0,168,169,5,107,0,0,169,
|
||||
170,5,45,0,0,170,171,5,100,0,0,171,172,5,101,0,0,172,173,5,118,0,
|
||||
0,173,174,5,101,0,0,174,175,5,108,0,0,175,176,5,111,0,0,176,177,
|
||||
5,112,0,0,177,178,5,109,0,0,178,179,5,101,0,0,179,180,5,110,0,0,
|
||||
180,181,5,116,0,0,181,28,1,0,0,0,182,183,5,105,0,0,183,184,5,109,
|
||||
0,0,184,185,5,112,0,0,185,186,5,111,0,0,186,187,5,114,0,0,187,188,
|
||||
5,116,0,0,188,30,1,0,0,0,189,190,5,123,0,0,190,32,1,0,0,0,191,192,
|
||||
5,125,0,0,192,34,1,0,0,0,193,194,5,59,0,0,194,36,1,0,0,0,195,197,
|
||||
7,0,0,0,196,195,1,0,0,0,197,198,1,0,0,0,198,196,1,0,0,0,198,199,
|
||||
1,0,0,0,199,38,1,0,0,0,200,204,7,1,0,0,201,203,7,2,0,0,202,201,1,
|
||||
0,0,0,203,206,1,0,0,0,204,202,1,0,0,0,204,205,1,0,0,0,205,40,1,0,
|
||||
0,0,206,204,1,0,0,0,207,212,5,34,0,0,208,211,3,43,21,0,209,211,8,
|
||||
3,0,0,210,208,1,0,0,0,210,209,1,0,0,0,211,214,1,0,0,0,212,210,1,
|
||||
0,0,0,212,213,1,0,0,0,213,215,1,0,0,0,214,212,1,0,0,0,215,216,5,
|
||||
34,0,0,216,42,1,0,0,0,217,225,5,92,0,0,218,226,7,4,0,0,219,220,5,
|
||||
117,0,0,220,221,3,45,22,0,221,222,3,45,22,0,222,223,3,45,22,0,223,
|
||||
224,3,45,22,0,224,226,1,0,0,0,225,218,1,0,0,0,225,219,1,0,0,0,226,
|
||||
44,1,0,0,0,227,228,7,5,0,0,228,46,1,0,0,0,229,230,5,47,0,0,230,231,
|
||||
5,47,0,0,231,235,1,0,0,0,232,234,8,6,0,0,233,232,1,0,0,0,234,237,
|
||||
1,0,0,0,235,233,1,0,0,0,235,236,1,0,0,0,236,238,1,0,0,0,237,235,
|
||||
1,0,0,0,238,239,6,23,0,0,239,48,1,0,0,0,240,241,5,47,0,0,241,242,
|
||||
5,42,0,0,242,246,1,0,0,0,243,245,9,0,0,0,244,243,1,0,0,0,245,248,
|
||||
1,0,0,0,246,247,1,0,0,0,246,244,1,0,0,0,247,249,1,0,0,0,248,246,
|
||||
1,0,0,0,249,250,5,42,0,0,250,251,5,47,0,0,251,252,1,0,0,0,252,253,
|
||||
6,24,0,0,253,50,1,0,0,0,254,256,7,7,0,0,255,254,1,0,0,0,256,257,
|
||||
1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,259,1,0,0,0,259,260,
|
||||
6,25,0,0,260,52,1,0,0,0,9,0,198,204,210,212,225,235,246,257,1,6,
|
||||
0,0
|
||||
];
|
||||
|
||||
private static __ATN: antlr.ATN;
|
||||
public static get _ATN(): antlr.ATN {
|
||||
if (!QuixosLockLexer.__ATN) {
|
||||
QuixosLockLexer.__ATN = new antlr.ATNDeserializer().deserialize(QuixosLockLexer._serializedATN);
|
||||
}
|
||||
|
||||
return QuixosLockLexer.__ATN;
|
||||
}
|
||||
|
||||
|
||||
private static readonly vocabulary = new antlr.Vocabulary(QuixosLockLexer.literalNames, QuixosLockLexer.symbolicNames, []);
|
||||
|
||||
public override get vocabulary(): antlr.Vocabulary {
|
||||
return QuixosLockLexer.vocabulary;
|
||||
}
|
||||
|
||||
private static readonly decisionsToDFA = QuixosLockLexer._ATN.decisionToState.map( (ds: antlr.DecisionState, index: number) => new antlr.DFA(ds, index) );
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
|
||||
import * as antlr from "antlr4ng";
|
||||
import { Token } from "antlr4ng";
|
||||
|
||||
import { QuixosLockVisitor } from "./QuixosLockVisitor.js";
|
||||
|
||||
// for running tests with parameters, TODO: discuss strategy for typed parameters in CI
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type int = number;
|
||||
|
||||
|
||||
export class QuixosLockParser extends antlr.Parser {
|
||||
public static readonly QUIXOS_LOCK = 1;
|
||||
public static readonly FRAGMENT = 2;
|
||||
public static readonly VERSION = 3;
|
||||
public static readonly QUIXOS = 4;
|
||||
public static readonly SOURCE = 5;
|
||||
public static readonly INTERFACE = 6;
|
||||
public static readonly PACKAGE = 7;
|
||||
public static readonly REPOSITORY = 8;
|
||||
public static readonly COMMIT = 9;
|
||||
public static readonly POLICY = 10;
|
||||
public static readonly REF = 11;
|
||||
public static readonly PINNED = 12;
|
||||
public static readonly TRACK_RELEASE = 13;
|
||||
public static readonly TRACK_DEVELOPMENT = 14;
|
||||
public static readonly IMPORT = 15;
|
||||
public static readonly LBRACE = 16;
|
||||
public static readonly RBRACE = 17;
|
||||
public static readonly SEMI = 18;
|
||||
public static readonly INTEGER = 19;
|
||||
public static readonly IDENTIFIER = 20;
|
||||
public static readonly STRING_LITERAL = 21;
|
||||
public static readonly LINE_COMMENT = 22;
|
||||
public static readonly BLOCK_COMMENT = 23;
|
||||
public static readonly WS = 24;
|
||||
public static readonly RULE_document = 0;
|
||||
public static readonly RULE_quixosEntry = 1;
|
||||
public static readonly RULE_resourceEntry = 2;
|
||||
public static readonly RULE_importEntry = 3;
|
||||
public static readonly RULE_resourceKind = 4;
|
||||
public static readonly RULE_sourceBlock = 5;
|
||||
public static readonly RULE_quixosSourceBlock = 6;
|
||||
public static readonly RULE_sourcePolicy = 7;
|
||||
public static readonly RULE_identifier = 8;
|
||||
public static readonly RULE_stringLiteral = 9;
|
||||
|
||||
public static readonly literalNames = [
|
||||
null, "'quixos-lock'", "'fragment'", "'version'", "'quixos'", "'source'",
|
||||
"'interface'", "'package'", "'repository'", "'commit'", "'policy'",
|
||||
"'ref'", "'pinned'", "'track-release'", "'track-development'", "'import'",
|
||||
"'{'", "'}'", "';'"
|
||||
];
|
||||
|
||||
public static readonly symbolicNames = [
|
||||
null, "QUIXOS_LOCK", "FRAGMENT", "VERSION", "QUIXOS", "SOURCE",
|
||||
"INTERFACE", "PACKAGE", "REPOSITORY", "COMMIT", "POLICY", "REF",
|
||||
"PINNED", "TRACK_RELEASE", "TRACK_DEVELOPMENT", "IMPORT", "LBRACE",
|
||||
"RBRACE", "SEMI", "INTEGER", "IDENTIFIER", "STRING_LITERAL", "LINE_COMMENT",
|
||||
"BLOCK_COMMENT", "WS"
|
||||
];
|
||||
public static readonly ruleNames = [
|
||||
"document", "quixosEntry", "resourceEntry", "importEntry", "resourceKind",
|
||||
"sourceBlock", "quixosSourceBlock", "sourcePolicy", "identifier",
|
||||
"stringLiteral",
|
||||
];
|
||||
|
||||
public get grammarFileName(): string { return "QuixosLock.g4"; }
|
||||
public get literalNames(): (string | null)[] { return QuixosLockParser.literalNames; }
|
||||
public get symbolicNames(): (string | null)[] { return QuixosLockParser.symbolicNames; }
|
||||
public get ruleNames(): string[] { return QuixosLockParser.ruleNames; }
|
||||
public get serializedATN(): number[] { return QuixosLockParser._serializedATN; }
|
||||
|
||||
protected createFailedPredicateException(predicate?: string, message?: string): antlr.FailedPredicateException {
|
||||
return new antlr.FailedPredicateException(this, predicate, message);
|
||||
}
|
||||
|
||||
public constructor(input: antlr.TokenStream) {
|
||||
super(input);
|
||||
this.interpreter = new antlr.ParserATNSimulator(this, QuixosLockParser._ATN, QuixosLockParser.decisionsToDFA, new antlr.PredictionContextCache());
|
||||
}
|
||||
public document(): DocumentContext {
|
||||
let localContext = new DocumentContext(this.context, this.state);
|
||||
this.enterRule(localContext, 0, QuixosLockParser.RULE_document);
|
||||
let _la: number;
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 20;
|
||||
this.match(QuixosLockParser.QUIXOS_LOCK);
|
||||
this.state = 22;
|
||||
this.errorHandler.sync(this);
|
||||
_la = this.tokenStream.LA(1);
|
||||
if (_la === 2) {
|
||||
{
|
||||
this.state = 21;
|
||||
this.match(QuixosLockParser.FRAGMENT);
|
||||
}
|
||||
}
|
||||
|
||||
this.state = 24;
|
||||
this.match(QuixosLockParser.VERSION);
|
||||
this.state = 25;
|
||||
this.match(QuixosLockParser.INTEGER);
|
||||
this.state = 26;
|
||||
this.match(QuixosLockParser.LBRACE);
|
||||
this.state = 32;
|
||||
this.errorHandler.sync(this);
|
||||
_la = this.tokenStream.LA(1);
|
||||
while ((((_la) & ~0x1F) === 0 && ((1 << _la) & 32976) !== 0)) {
|
||||
{
|
||||
this.state = 30;
|
||||
this.errorHandler.sync(this);
|
||||
switch (this.tokenStream.LA(1)) {
|
||||
case QuixosLockParser.QUIXOS:
|
||||
{
|
||||
this.state = 27;
|
||||
this.quixosEntry();
|
||||
}
|
||||
break;
|
||||
case QuixosLockParser.IMPORT:
|
||||
{
|
||||
this.state = 28;
|
||||
this.importEntry();
|
||||
}
|
||||
break;
|
||||
case QuixosLockParser.INTERFACE:
|
||||
case QuixosLockParser.PACKAGE:
|
||||
{
|
||||
this.state = 29;
|
||||
this.resourceEntry();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new antlr.NoViableAltException(this);
|
||||
}
|
||||
}
|
||||
this.state = 34;
|
||||
this.errorHandler.sync(this);
|
||||
_la = this.tokenStream.LA(1);
|
||||
}
|
||||
this.state = 35;
|
||||
this.match(QuixosLockParser.RBRACE);
|
||||
this.state = 36;
|
||||
this.match(QuixosLockParser.EOF);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public quixosEntry(): QuixosEntryContext {
|
||||
let localContext = new QuixosEntryContext(this.context, this.state);
|
||||
this.enterRule(localContext, 2, QuixosLockParser.RULE_quixosEntry);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 38;
|
||||
this.match(QuixosLockParser.QUIXOS);
|
||||
this.state = 39;
|
||||
this.match(QuixosLockParser.SOURCE);
|
||||
this.state = 40;
|
||||
this.quixosSourceBlock();
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public resourceEntry(): ResourceEntryContext {
|
||||
let localContext = new ResourceEntryContext(this.context, this.state);
|
||||
this.enterRule(localContext, 4, QuixosLockParser.RULE_resourceEntry);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 42;
|
||||
this.resourceKind();
|
||||
this.state = 43;
|
||||
this.identifier();
|
||||
this.state = 44;
|
||||
this.match(QuixosLockParser.SOURCE);
|
||||
this.state = 45;
|
||||
this.sourceBlock();
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public importEntry(): ImportEntryContext {
|
||||
let localContext = new ImportEntryContext(this.context, this.state);
|
||||
this.enterRule(localContext, 6, QuixosLockParser.RULE_importEntry);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 47;
|
||||
this.match(QuixosLockParser.IMPORT);
|
||||
this.state = 48;
|
||||
this.stringLiteral();
|
||||
this.state = 49;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public resourceKind(): ResourceKindContext {
|
||||
let localContext = new ResourceKindContext(this.context, this.state);
|
||||
this.enterRule(localContext, 8, QuixosLockParser.RULE_resourceKind);
|
||||
let _la: number;
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 51;
|
||||
_la = this.tokenStream.LA(1);
|
||||
if(!(_la === 6 || _la === 7)) {
|
||||
this.errorHandler.recoverInline(this);
|
||||
}
|
||||
else {
|
||||
this.errorHandler.reportMatch(this);
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public sourceBlock(): SourceBlockContext {
|
||||
let localContext = new SourceBlockContext(this.context, this.state);
|
||||
this.enterRule(localContext, 10, QuixosLockParser.RULE_sourceBlock);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 53;
|
||||
this.match(QuixosLockParser.LBRACE);
|
||||
this.state = 54;
|
||||
this.match(QuixosLockParser.REPOSITORY);
|
||||
this.state = 55;
|
||||
this.stringLiteral();
|
||||
this.state = 56;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
this.state = 57;
|
||||
this.match(QuixosLockParser.COMMIT);
|
||||
this.state = 58;
|
||||
this.stringLiteral();
|
||||
this.state = 59;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
this.state = 60;
|
||||
this.match(QuixosLockParser.RBRACE);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public quixosSourceBlock(): QuixosSourceBlockContext {
|
||||
let localContext = new QuixosSourceBlockContext(this.context, this.state);
|
||||
this.enterRule(localContext, 12, QuixosLockParser.RULE_quixosSourceBlock);
|
||||
let _la: number;
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 62;
|
||||
this.match(QuixosLockParser.LBRACE);
|
||||
this.state = 63;
|
||||
this.match(QuixosLockParser.REPOSITORY);
|
||||
this.state = 64;
|
||||
this.stringLiteral();
|
||||
this.state = 65;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
this.state = 73;
|
||||
this.errorHandler.sync(this);
|
||||
_la = this.tokenStream.LA(1);
|
||||
if (_la === 10) {
|
||||
{
|
||||
this.state = 66;
|
||||
this.match(QuixosLockParser.POLICY);
|
||||
this.state = 67;
|
||||
this.sourcePolicy();
|
||||
this.state = 68;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
this.state = 69;
|
||||
this.match(QuixosLockParser.REF);
|
||||
this.state = 70;
|
||||
this.stringLiteral();
|
||||
this.state = 71;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
}
|
||||
}
|
||||
|
||||
this.state = 75;
|
||||
this.match(QuixosLockParser.COMMIT);
|
||||
this.state = 76;
|
||||
this.stringLiteral();
|
||||
this.state = 77;
|
||||
this.match(QuixosLockParser.SEMI);
|
||||
this.state = 78;
|
||||
this.match(QuixosLockParser.RBRACE);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public sourcePolicy(): SourcePolicyContext {
|
||||
let localContext = new SourcePolicyContext(this.context, this.state);
|
||||
this.enterRule(localContext, 14, QuixosLockParser.RULE_sourcePolicy);
|
||||
let _la: number;
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 80;
|
||||
_la = this.tokenStream.LA(1);
|
||||
if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 28672) !== 0))) {
|
||||
this.errorHandler.recoverInline(this);
|
||||
}
|
||||
else {
|
||||
this.errorHandler.reportMatch(this);
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public identifier(): IdentifierContext {
|
||||
let localContext = new IdentifierContext(this.context, this.state);
|
||||
this.enterRule(localContext, 16, QuixosLockParser.RULE_identifier);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 82;
|
||||
this.match(QuixosLockParser.IDENTIFIER);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
public stringLiteral(): StringLiteralContext {
|
||||
let localContext = new StringLiteralContext(this.context, this.state);
|
||||
this.enterRule(localContext, 18, QuixosLockParser.RULE_stringLiteral);
|
||||
try {
|
||||
this.enterOuterAlt(localContext, 1);
|
||||
{
|
||||
this.state = 84;
|
||||
this.match(QuixosLockParser.STRING_LITERAL);
|
||||
}
|
||||
}
|
||||
catch (re) {
|
||||
if (re instanceof antlr.RecognitionException) {
|
||||
this.errorHandler.reportError(this, re);
|
||||
this.errorHandler.recover(this, re);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.exitRule();
|
||||
}
|
||||
return localContext;
|
||||
}
|
||||
|
||||
public static readonly _serializedATN: number[] = [
|
||||
4,1,24,87,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,
|
||||
6,2,7,7,7,2,8,7,8,2,9,7,9,1,0,1,0,3,0,23,8,0,1,0,1,0,1,0,1,0,1,0,
|
||||
1,0,5,0,31,8,0,10,0,12,0,34,9,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,2,
|
||||
1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,
|
||||
1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,3,6,74,8,
|
||||
6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,8,1,8,1,9,1,9,1,9,0,0,10,0,2,4,6,
|
||||
8,10,12,14,16,18,0,2,1,0,6,7,1,0,12,14,81,0,20,1,0,0,0,2,38,1,0,
|
||||
0,0,4,42,1,0,0,0,6,47,1,0,0,0,8,51,1,0,0,0,10,53,1,0,0,0,12,62,1,
|
||||
0,0,0,14,80,1,0,0,0,16,82,1,0,0,0,18,84,1,0,0,0,20,22,5,1,0,0,21,
|
||||
23,5,2,0,0,22,21,1,0,0,0,22,23,1,0,0,0,23,24,1,0,0,0,24,25,5,3,0,
|
||||
0,25,26,5,19,0,0,26,32,5,16,0,0,27,31,3,2,1,0,28,31,3,6,3,0,29,31,
|
||||
3,4,2,0,30,27,1,0,0,0,30,28,1,0,0,0,30,29,1,0,0,0,31,34,1,0,0,0,
|
||||
32,30,1,0,0,0,32,33,1,0,0,0,33,35,1,0,0,0,34,32,1,0,0,0,35,36,5,
|
||||
17,0,0,36,37,5,0,0,1,37,1,1,0,0,0,38,39,5,4,0,0,39,40,5,5,0,0,40,
|
||||
41,3,12,6,0,41,3,1,0,0,0,42,43,3,8,4,0,43,44,3,16,8,0,44,45,5,5,
|
||||
0,0,45,46,3,10,5,0,46,5,1,0,0,0,47,48,5,15,0,0,48,49,3,18,9,0,49,
|
||||
50,5,18,0,0,50,7,1,0,0,0,51,52,7,0,0,0,52,9,1,0,0,0,53,54,5,16,0,
|
||||
0,54,55,5,8,0,0,55,56,3,18,9,0,56,57,5,18,0,0,57,58,5,9,0,0,58,59,
|
||||
3,18,9,0,59,60,5,18,0,0,60,61,5,17,0,0,61,11,1,0,0,0,62,63,5,16,
|
||||
0,0,63,64,5,8,0,0,64,65,3,18,9,0,65,73,5,18,0,0,66,67,5,10,0,0,67,
|
||||
68,3,14,7,0,68,69,5,18,0,0,69,70,5,11,0,0,70,71,3,18,9,0,71,72,5,
|
||||
18,0,0,72,74,1,0,0,0,73,66,1,0,0,0,73,74,1,0,0,0,74,75,1,0,0,0,75,
|
||||
76,5,9,0,0,76,77,3,18,9,0,77,78,5,18,0,0,78,79,5,17,0,0,79,13,1,
|
||||
0,0,0,80,81,7,1,0,0,81,15,1,0,0,0,82,83,5,20,0,0,83,17,1,0,0,0,84,
|
||||
85,5,21,0,0,85,19,1,0,0,0,4,22,30,32,73
|
||||
];
|
||||
|
||||
private static __ATN: antlr.ATN;
|
||||
public static get _ATN(): antlr.ATN {
|
||||
if (!QuixosLockParser.__ATN) {
|
||||
QuixosLockParser.__ATN = new antlr.ATNDeserializer().deserialize(QuixosLockParser._serializedATN);
|
||||
}
|
||||
|
||||
return QuixosLockParser.__ATN;
|
||||
}
|
||||
|
||||
|
||||
private static readonly vocabulary = new antlr.Vocabulary(QuixosLockParser.literalNames, QuixosLockParser.symbolicNames, []);
|
||||
|
||||
public override get vocabulary(): antlr.Vocabulary {
|
||||
return QuixosLockParser.vocabulary;
|
||||
}
|
||||
|
||||
private static readonly decisionsToDFA = QuixosLockParser._ATN.decisionToState.map( (ds: antlr.DecisionState, index: number) => new antlr.DFA(ds, index) );
|
||||
}
|
||||
|
||||
export class DocumentContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public QUIXOS_LOCK(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.QUIXOS_LOCK, 0)!;
|
||||
}
|
||||
public VERSION(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.VERSION, 0)!;
|
||||
}
|
||||
public INTEGER(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.INTEGER, 0)!;
|
||||
}
|
||||
public LBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.LBRACE, 0)!;
|
||||
}
|
||||
public RBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.RBRACE, 0)!;
|
||||
}
|
||||
public EOF(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.EOF, 0)!;
|
||||
}
|
||||
public FRAGMENT(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.FRAGMENT, 0);
|
||||
}
|
||||
public quixosEntry(): QuixosEntryContext[];
|
||||
public quixosEntry(i: number): QuixosEntryContext | null;
|
||||
public quixosEntry(i?: number): QuixosEntryContext[] | QuixosEntryContext | null {
|
||||
if (i === undefined) {
|
||||
return this.getRuleContexts(QuixosEntryContext);
|
||||
}
|
||||
|
||||
return this.getRuleContext(i, QuixosEntryContext);
|
||||
}
|
||||
public importEntry(): ImportEntryContext[];
|
||||
public importEntry(i: number): ImportEntryContext | null;
|
||||
public importEntry(i?: number): ImportEntryContext[] | ImportEntryContext | null {
|
||||
if (i === undefined) {
|
||||
return this.getRuleContexts(ImportEntryContext);
|
||||
}
|
||||
|
||||
return this.getRuleContext(i, ImportEntryContext);
|
||||
}
|
||||
public resourceEntry(): ResourceEntryContext[];
|
||||
public resourceEntry(i: number): ResourceEntryContext | null;
|
||||
public resourceEntry(i?: number): ResourceEntryContext[] | ResourceEntryContext | null {
|
||||
if (i === undefined) {
|
||||
return this.getRuleContexts(ResourceEntryContext);
|
||||
}
|
||||
|
||||
return this.getRuleContext(i, ResourceEntryContext);
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_document;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitDocument) {
|
||||
return visitor.visitDocument(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class QuixosEntryContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public QUIXOS(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.QUIXOS, 0)!;
|
||||
}
|
||||
public SOURCE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.SOURCE, 0)!;
|
||||
}
|
||||
public quixosSourceBlock(): QuixosSourceBlockContext {
|
||||
return this.getRuleContext(0, QuixosSourceBlockContext)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_quixosEntry;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitQuixosEntry) {
|
||||
return visitor.visitQuixosEntry(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ResourceEntryContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public resourceKind(): ResourceKindContext {
|
||||
return this.getRuleContext(0, ResourceKindContext)!;
|
||||
}
|
||||
public identifier(): IdentifierContext {
|
||||
return this.getRuleContext(0, IdentifierContext)!;
|
||||
}
|
||||
public SOURCE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.SOURCE, 0)!;
|
||||
}
|
||||
public sourceBlock(): SourceBlockContext {
|
||||
return this.getRuleContext(0, SourceBlockContext)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_resourceEntry;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitResourceEntry) {
|
||||
return visitor.visitResourceEntry(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ImportEntryContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public IMPORT(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.IMPORT, 0)!;
|
||||
}
|
||||
public stringLiteral(): StringLiteralContext {
|
||||
return this.getRuleContext(0, StringLiteralContext)!;
|
||||
}
|
||||
public SEMI(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.SEMI, 0)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_importEntry;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitImportEntry) {
|
||||
return visitor.visitImportEntry(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ResourceKindContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public INTERFACE(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.INTERFACE, 0);
|
||||
}
|
||||
public PACKAGE(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.PACKAGE, 0);
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_resourceKind;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitResourceKind) {
|
||||
return visitor.visitResourceKind(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SourceBlockContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public LBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.LBRACE, 0)!;
|
||||
}
|
||||
public REPOSITORY(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.REPOSITORY, 0)!;
|
||||
}
|
||||
public stringLiteral(): StringLiteralContext[];
|
||||
public stringLiteral(i: number): StringLiteralContext | null;
|
||||
public stringLiteral(i?: number): StringLiteralContext[] | StringLiteralContext | null {
|
||||
if (i === undefined) {
|
||||
return this.getRuleContexts(StringLiteralContext);
|
||||
}
|
||||
|
||||
return this.getRuleContext(i, StringLiteralContext);
|
||||
}
|
||||
public SEMI(): antlr.TerminalNode[];
|
||||
public SEMI(i: number): antlr.TerminalNode | null;
|
||||
public SEMI(i?: number): antlr.TerminalNode | null | antlr.TerminalNode[] {
|
||||
if (i === undefined) {
|
||||
return this.getTokens(QuixosLockParser.SEMI);
|
||||
} else {
|
||||
return this.getToken(QuixosLockParser.SEMI, i);
|
||||
}
|
||||
}
|
||||
public COMMIT(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.COMMIT, 0)!;
|
||||
}
|
||||
public RBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.RBRACE, 0)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_sourceBlock;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitSourceBlock) {
|
||||
return visitor.visitSourceBlock(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class QuixosSourceBlockContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public LBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.LBRACE, 0)!;
|
||||
}
|
||||
public REPOSITORY(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.REPOSITORY, 0)!;
|
||||
}
|
||||
public stringLiteral(): StringLiteralContext[];
|
||||
public stringLiteral(i: number): StringLiteralContext | null;
|
||||
public stringLiteral(i?: number): StringLiteralContext[] | StringLiteralContext | null {
|
||||
if (i === undefined) {
|
||||
return this.getRuleContexts(StringLiteralContext);
|
||||
}
|
||||
|
||||
return this.getRuleContext(i, StringLiteralContext);
|
||||
}
|
||||
public SEMI(): antlr.TerminalNode[];
|
||||
public SEMI(i: number): antlr.TerminalNode | null;
|
||||
public SEMI(i?: number): antlr.TerminalNode | null | antlr.TerminalNode[] {
|
||||
if (i === undefined) {
|
||||
return this.getTokens(QuixosLockParser.SEMI);
|
||||
} else {
|
||||
return this.getToken(QuixosLockParser.SEMI, i);
|
||||
}
|
||||
}
|
||||
public COMMIT(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.COMMIT, 0)!;
|
||||
}
|
||||
public RBRACE(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.RBRACE, 0)!;
|
||||
}
|
||||
public POLICY(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.POLICY, 0);
|
||||
}
|
||||
public sourcePolicy(): SourcePolicyContext | null {
|
||||
return this.getRuleContext(0, SourcePolicyContext);
|
||||
}
|
||||
public REF(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.REF, 0);
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_quixosSourceBlock;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitQuixosSourceBlock) {
|
||||
return visitor.visitQuixosSourceBlock(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SourcePolicyContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public PINNED(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.PINNED, 0);
|
||||
}
|
||||
public TRACK_RELEASE(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.TRACK_RELEASE, 0);
|
||||
}
|
||||
public TRACK_DEVELOPMENT(): antlr.TerminalNode | null {
|
||||
return this.getToken(QuixosLockParser.TRACK_DEVELOPMENT, 0);
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_sourcePolicy;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitSourcePolicy) {
|
||||
return visitor.visitSourcePolicy(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class IdentifierContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public IDENTIFIER(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.IDENTIFIER, 0)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_identifier;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitIdentifier) {
|
||||
return visitor.visitIdentifier(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class StringLiteralContext extends antlr.ParserRuleContext {
|
||||
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
|
||||
super(parent, invokingState);
|
||||
}
|
||||
public STRING_LITERAL(): antlr.TerminalNode {
|
||||
return this.getToken(QuixosLockParser.STRING_LITERAL, 0)!;
|
||||
}
|
||||
public override get ruleIndex(): number {
|
||||
return QuixosLockParser.RULE_stringLiteral;
|
||||
}
|
||||
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
|
||||
if (visitor.visitStringLiteral) {
|
||||
return visitor.visitStringLiteral(this);
|
||||
} else {
|
||||
return visitor.visitChildren(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
import { AbstractParseTreeVisitor } from "antlr4ng";
|
||||
|
||||
|
||||
import { DocumentContext } from "./QuixosLockParser.js";
|
||||
import { QuixosEntryContext } from "./QuixosLockParser.js";
|
||||
import { ResourceEntryContext } from "./QuixosLockParser.js";
|
||||
import { ImportEntryContext } from "./QuixosLockParser.js";
|
||||
import { ResourceKindContext } from "./QuixosLockParser.js";
|
||||
import { SourceBlockContext } from "./QuixosLockParser.js";
|
||||
import { QuixosSourceBlockContext } from "./QuixosLockParser.js";
|
||||
import { SourcePolicyContext } from "./QuixosLockParser.js";
|
||||
import { IdentifierContext } from "./QuixosLockParser.js";
|
||||
import { StringLiteralContext } from "./QuixosLockParser.js";
|
||||
|
||||
|
||||
/**
|
||||
* This interface defines a complete generic visitor for a parse tree produced
|
||||
* by `QuixosLockParser`.
|
||||
*
|
||||
* @param <Result> The return type of the visit operation. Use `void` for
|
||||
* operations with no return type.
|
||||
*/
|
||||
export class QuixosLockVisitor<Result> extends AbstractParseTreeVisitor<Result> {
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.document`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitDocument?: (ctx: DocumentContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.quixosEntry`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitQuixosEntry?: (ctx: QuixosEntryContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.resourceEntry`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitResourceEntry?: (ctx: ResourceEntryContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.importEntry`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitImportEntry?: (ctx: ImportEntryContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.resourceKind`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitResourceKind?: (ctx: ResourceKindContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.sourceBlock`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSourceBlock?: (ctx: SourceBlockContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.quixosSourceBlock`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitQuixosSourceBlock?: (ctx: QuixosSourceBlockContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.sourcePolicy`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSourcePolicy?: (ctx: SourcePolicyContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.identifier`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitIdentifier?: (ctx: IdentifierContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosLockParser.stringLiteral`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitStringLiteral?: (ctx: StringLiteralContext) => Result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./loader.js";
|
||||
export * from "./parser.js";
|
||||
export * from "./types.js";
|
||||
@@ -0,0 +1,142 @@
|
||||
import { lstat, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parseQuixosLockDocument, type QuixosLockDiagnostic, type QuixosLockParseResult } from "./parser.js";
|
||||
import type { LockedResource, QuixosLockDocument, QuixosRepositoryLock } from "./types.js";
|
||||
|
||||
export type QuixosLockSourceReader = (relativePath: string) => Promise<string>;
|
||||
|
||||
const diagnostic = (code: string, message: string, fileName: string, path?: string): QuixosLockDiagnostic => ({
|
||||
phase: "resolution",
|
||||
code,
|
||||
message,
|
||||
fileName,
|
||||
line: 1,
|
||||
column: 0,
|
||||
path,
|
||||
});
|
||||
|
||||
export const resolveQuixosLock = async (
|
||||
rootSource: string,
|
||||
readSource: QuixosLockSourceReader,
|
||||
rootFileName = "quixos.lock",
|
||||
): Promise<QuixosLockParseResult> => {
|
||||
const root = parseQuixosLockDocument(rootSource, rootFileName);
|
||||
if (!root.ok) return root;
|
||||
if (root.document.kind !== "root") {
|
||||
return {
|
||||
ok: false,
|
||||
diagnostics: [
|
||||
diagnostic("expected-root-lock", "The entrypoint must be a root Quixos lock, not a fragment", rootFileName),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const diagnostics: QuixosLockDiagnostic[] = [];
|
||||
const resources: LockedResource[] = [];
|
||||
const sourceFiles = [rootFileName];
|
||||
const visited = new Set<string>();
|
||||
const active: string[] = [];
|
||||
|
||||
const addResources = (document: QuixosLockDocument, fileName: string) => {
|
||||
for (const resource of document.resources) {
|
||||
const duplicate = resources.find((entry) => entry.kind === resource.kind && entry.binding === resource.binding);
|
||||
if (duplicate) {
|
||||
diagnostics.push(
|
||||
diagnostic(
|
||||
"duplicate-resource-binding",
|
||||
`Duplicate ${resource.kind} binding ${resource.binding} across imported lock files`,
|
||||
fileName,
|
||||
`${resource.kind}.${resource.binding}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resources.push(resource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const visit = async (importPath: string) => {
|
||||
if (active.includes(importPath)) {
|
||||
diagnostics.push(
|
||||
diagnostic("import-cycle", `Lock import cycle: ${[...active, importPath].join(" -> ")}`, importPath),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (visited.has(importPath)) return;
|
||||
active.push(importPath);
|
||||
let source: string;
|
||||
try {
|
||||
source = await readSource(importPath);
|
||||
} catch (cause) {
|
||||
diagnostics.push(
|
||||
diagnostic(
|
||||
"import-read-failed",
|
||||
`Could not read lock import ${importPath}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
importPath,
|
||||
),
|
||||
);
|
||||
active.pop();
|
||||
return;
|
||||
}
|
||||
const parsed = parseQuixosLockDocument(source, importPath);
|
||||
if (!parsed.ok) {
|
||||
diagnostics.push(...parsed.diagnostics);
|
||||
active.pop();
|
||||
return;
|
||||
}
|
||||
if (parsed.document.kind !== "fragment") {
|
||||
diagnostics.push(
|
||||
diagnostic(
|
||||
"imported-root-lock",
|
||||
`Imported file ${importPath} must begin with "quixos-lock fragment"`,
|
||||
importPath,
|
||||
),
|
||||
);
|
||||
active.pop();
|
||||
return;
|
||||
}
|
||||
visited.add(importPath);
|
||||
sourceFiles.push(importPath);
|
||||
addResources(parsed.document, importPath);
|
||||
for (const nested of parsed.document.imports) await visit(nested);
|
||||
active.pop();
|
||||
};
|
||||
|
||||
addResources(root.document, rootFileName);
|
||||
for (const importPath of root.document.imports) await visit(importPath);
|
||||
if (diagnostics.length) return { ok: false, diagnostics };
|
||||
const lock: QuixosRepositoryLock = {
|
||||
formatVersion: 1,
|
||||
quixos: root.document.quixos,
|
||||
resources,
|
||||
sourceFiles,
|
||||
};
|
||||
return { ok: true, lock, diagnostics: [] };
|
||||
};
|
||||
|
||||
export const loadQuixosLock = async (fileName: string): Promise<QuixosLockParseResult> => {
|
||||
const absoluteRoot = path.resolve(fileName);
|
||||
const repositoryRoot = path.dirname(absoluteRoot);
|
||||
const rootName = path.basename(absoluteRoot);
|
||||
const rootSource = await readFile(absoluteRoot, "utf8");
|
||||
return await resolveQuixosLock(
|
||||
rootSource,
|
||||
async (relativePath) => {
|
||||
let absoluteImport = repositoryRoot;
|
||||
const segments = relativePath.split("/");
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
absoluteImport = path.join(absoluteImport, segment);
|
||||
const metadata = await lstat(absoluteImport);
|
||||
if (metadata.isSymbolicLink()) {
|
||||
throw new Error("imports must not traverse symbolic links");
|
||||
}
|
||||
const final = index === segments.length - 1;
|
||||
if ((!final && !metadata.isDirectory()) || (final && !metadata.isFile())) {
|
||||
throw new Error("imports must be ordinary files beneath ordinary directories");
|
||||
}
|
||||
}
|
||||
return await readFile(absoluteImport, "utf8");
|
||||
},
|
||||
rootName,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,410 @@
|
||||
import {
|
||||
BaseErrorListener,
|
||||
CharStream,
|
||||
CommonTokenStream,
|
||||
type ATNSimulator,
|
||||
type RecognitionException,
|
||||
type Recognizer,
|
||||
type Token,
|
||||
} from "antlr4ng";
|
||||
import { QuixosLockLexer } from "./generated/QuixosLockLexer.js";
|
||||
import {
|
||||
QuixosLockParser,
|
||||
type QuixosSourceBlockContext,
|
||||
type SourceBlockContext,
|
||||
} from "./generated/QuixosLockParser.js";
|
||||
import type { GitSource, LockedResource, QuixosLockDocument, QuixosRepositoryLock, QuixosSource } from "./types.js";
|
||||
|
||||
export type QuixosLockDiagnostic = {
|
||||
phase: "syntax" | "validation" | "resolution";
|
||||
code: string;
|
||||
message: string;
|
||||
fileName: string;
|
||||
line: number;
|
||||
column: number;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type QuixosLockParseResult =
|
||||
| { ok: true; lock: QuixosRepositoryLock; diagnostics: [] }
|
||||
| { ok: false; diagnostics: QuixosLockDiagnostic[] };
|
||||
|
||||
export type QuixosLockDocumentParseResult =
|
||||
| { ok: true; document: QuixosLockDocument; diagnostics: [] }
|
||||
| { ok: false; diagnostics: QuixosLockDiagnostic[] };
|
||||
|
||||
class SyntaxErrorListener extends BaseErrorListener {
|
||||
constructor(
|
||||
private readonly fileName: string,
|
||||
private readonly diagnostics: QuixosLockDiagnostic[],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override syntaxError<S extends Token, T extends ATNSimulator>(
|
||||
_recognizer: Recognizer<T>,
|
||||
_offendingSymbol: S | null,
|
||||
line: number,
|
||||
column: number,
|
||||
message: string,
|
||||
_error: RecognitionException | null,
|
||||
): void {
|
||||
this.diagnostics.push({
|
||||
phase: "syntax",
|
||||
code: "syntax-error",
|
||||
message,
|
||||
fileName: this.fileName,
|
||||
line,
|
||||
column,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stringValue = (context: { getText(): string }): string => JSON.parse(context.getText()) as string;
|
||||
|
||||
const lowerSource = (context: SourceBlockContext): GitSource => ({
|
||||
resolver: "git",
|
||||
repository: stringValue(context.stringLiteral(0)!),
|
||||
commit: stringValue(context.stringLiteral(1)!).toLowerCase(),
|
||||
});
|
||||
|
||||
const lowerQuixosSource = (context: QuixosSourceBlockContext): QuixosSource => {
|
||||
const source = {
|
||||
resolver: "git" as const,
|
||||
repository: stringValue(context.stringLiteral(0)!),
|
||||
commit: stringValue(context.stringLiteral(context.stringLiteral().length - 1)!).toLowerCase(),
|
||||
};
|
||||
const policyContext = context.sourcePolicy();
|
||||
if (!policyContext) return source;
|
||||
const policy = policyContext.PINNED()
|
||||
? "pinned"
|
||||
: policyContext.TRACK_RELEASE()
|
||||
? "track-release"
|
||||
: "track-development";
|
||||
return {
|
||||
...source,
|
||||
policy,
|
||||
ref: stringValue(context.stringLiteral(1)!),
|
||||
};
|
||||
};
|
||||
|
||||
const issue = (
|
||||
diagnostics: QuixosLockDiagnostic[],
|
||||
fileName: string,
|
||||
code: string,
|
||||
message: string,
|
||||
path?: string,
|
||||
line = 1,
|
||||
column = 0,
|
||||
) =>
|
||||
diagnostics.push({
|
||||
phase: "validation",
|
||||
code,
|
||||
message,
|
||||
fileName,
|
||||
line,
|
||||
column,
|
||||
path,
|
||||
});
|
||||
|
||||
const validateImport = (
|
||||
importPath: string,
|
||||
index: number,
|
||||
fileName: string,
|
||||
diagnostics: QuixosLockDiagnostic[],
|
||||
line: number,
|
||||
column: number,
|
||||
) => {
|
||||
const path = `imports[${index}]`;
|
||||
if (
|
||||
!importPath ||
|
||||
importPath.startsWith("/") ||
|
||||
importPath.includes("\\") ||
|
||||
importPath.split("/").some((segment) => !segment || segment === "." || segment === "..") ||
|
||||
/^[A-Za-z][A-Za-z0-9+.-]*:/.test(importPath)
|
||||
) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"invalid-import-path",
|
||||
`${path} must be a normalized relative path beneath the workspace repository root`,
|
||||
path,
|
||||
line,
|
||||
column,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validateSource = (source: GitSource, path: string, fileName: string, diagnostics: QuixosLockDiagnostic[]) => {
|
||||
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(source.commit)) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"invalid-git-commit",
|
||||
`${path}.commit must be a full 40- or 64-character Git object ID`,
|
||||
`${path}.commit`,
|
||||
);
|
||||
}
|
||||
let repository: URL;
|
||||
try {
|
||||
repository = new URL(source.repository);
|
||||
} catch {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"invalid-git-repository",
|
||||
`${path}.repository must be an absolute Git URL`,
|
||||
`${path}.repository`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!["https:", "ssh:"].includes(repository.protocol)) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"unsupported-git-transport",
|
||||
`${path}.repository must use https:// or ssh://`,
|
||||
`${path}.repository`,
|
||||
);
|
||||
}
|
||||
if (repository.search || repository.hash) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"decorated-git-repository",
|
||||
`${path}.repository must be a base repository URL without a query or fragment`,
|
||||
`${path}.repository`,
|
||||
);
|
||||
}
|
||||
if (repository.password || (repository.protocol === "https:" && repository.username)) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"embedded-git-credential",
|
||||
`${path}.repository must not embed credentials`,
|
||||
`${path}.repository`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validateQuixosSource = (source: QuixosSource, fileName: string, diagnostics: QuixosLockDiagnostic[]) => {
|
||||
validateSource(source, "quixos", fileName, diagnostics);
|
||||
if (!source.policy) return;
|
||||
const forbiddenRefCharacters = new Set("~^:?*[\\");
|
||||
const invalidRef =
|
||||
!source.ref ||
|
||||
[...source.ref].some((character) => {
|
||||
const code = character.charCodeAt(0);
|
||||
return code <= 0x20 || code === 0x7f || forbiddenRefCharacters.has(character);
|
||||
}) ||
|
||||
source.ref.startsWith("/") ||
|
||||
source.ref.endsWith("/") ||
|
||||
source.ref.endsWith(".") ||
|
||||
source.ref.includes("..") ||
|
||||
source.ref.includes("@{") ||
|
||||
source.ref.includes("//");
|
||||
if (invalidRef) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"invalid-quixos-ref",
|
||||
"quixos.ref must be a non-empty Git ref without whitespace or Git ref metacharacters",
|
||||
"quixos.ref",
|
||||
);
|
||||
}
|
||||
if (source.policy === "pinned") {
|
||||
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(source.ref)) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"pinned-quixos-ref-not-commit",
|
||||
"A pinned Quixos source must use an exact commit as its ref",
|
||||
"quixos.ref",
|
||||
);
|
||||
} else if (source.ref.toLowerCase() !== source.commit) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"pinned-quixos-commit-mismatch",
|
||||
"A pinned Quixos source ref and resolved commit must be identical",
|
||||
"quixos.commit",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const parseQuixosLockDocument = (source: string, fileName = "<memory>"): QuixosLockDocumentParseResult => {
|
||||
const diagnostics: QuixosLockDiagnostic[] = [];
|
||||
const listener = new SyntaxErrorListener(fileName, diagnostics);
|
||||
const lexer = new QuixosLockLexer(CharStream.fromString(source));
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(listener);
|
||||
const parser = new QuixosLockParser(new CommonTokenStream(lexer));
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(listener);
|
||||
const tree = parser.document();
|
||||
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
||||
|
||||
const formatVersion = Number.parseInt(tree.INTEGER().getText(), 10);
|
||||
if (formatVersion !== 1) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"unsupported-lock-version",
|
||||
`Unsupported Quixos lock format version ${formatVersion}`,
|
||||
"formatVersion",
|
||||
);
|
||||
}
|
||||
|
||||
const fragment = Boolean(tree.FRAGMENT());
|
||||
const quixosEntries = tree.quixosEntry();
|
||||
let quixos: QuixosSource | undefined;
|
||||
if (fragment && quixosEntries.length) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"fragment-has-quixos-source",
|
||||
"A lock fragment inherits the root document's Quixos source and must not redeclare it",
|
||||
"quixos",
|
||||
);
|
||||
} else if (!fragment && quixosEntries.length !== 1) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"root-quixos-source-count",
|
||||
"A root lock document must contain exactly one Quixos source",
|
||||
"quixos",
|
||||
);
|
||||
} else if (!fragment) {
|
||||
quixos = lowerQuixosSource(quixosEntries[0]!.quixosSourceBlock());
|
||||
validateQuixosSource(quixos, fileName, diagnostics);
|
||||
}
|
||||
|
||||
const imports = tree.importEntry().map((context, index) => {
|
||||
const importPath = stringValue(context.stringLiteral());
|
||||
validateImport(importPath, index, fileName, diagnostics, context.start?.line ?? 1, context.start?.column ?? 0);
|
||||
return importPath;
|
||||
});
|
||||
const repeatedImports = new Set<string>();
|
||||
imports.forEach((importPath, index) => {
|
||||
if (repeatedImports.has(importPath)) {
|
||||
issue(diagnostics, fileName, "duplicate-import", `Duplicate lock import ${importPath}`, `imports[${index}]`);
|
||||
}
|
||||
repeatedImports.add(importPath);
|
||||
});
|
||||
|
||||
const resources: LockedResource[] = [];
|
||||
const bindings = new Set<string>();
|
||||
for (const [index, context] of tree.resourceEntry().entries()) {
|
||||
const kind = context.resourceKind().INTERFACE() ? "interface" : "package";
|
||||
const binding = context.identifier().getText();
|
||||
const key = `${kind}\0${binding}`;
|
||||
if (bindings.has(key)) {
|
||||
issue(
|
||||
diagnostics,
|
||||
fileName,
|
||||
"duplicate-resource-binding",
|
||||
`Duplicate ${kind} binding ${binding}`,
|
||||
`resources[${index}].binding`,
|
||||
);
|
||||
}
|
||||
bindings.add(key);
|
||||
const resource = { kind, binding, source: lowerSource(context.sourceBlock()) } as LockedResource;
|
||||
validateSource(resource.source, `resources[${index}].source`, fileName, diagnostics);
|
||||
resources.push(resource);
|
||||
}
|
||||
|
||||
return diagnostics.length > 0
|
||||
? { ok: false, diagnostics }
|
||||
: {
|
||||
ok: true,
|
||||
document: fragment
|
||||
? { kind: "fragment", formatVersion: 1, imports, resources }
|
||||
: { kind: "root", formatVersion: 1, quixos: quixos!, imports, resources },
|
||||
diagnostics: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const parseQuixosLock = (source: string, fileName = "<memory>"): QuixosLockParseResult => {
|
||||
const parsed = parseQuixosLockDocument(source, fileName);
|
||||
if (!parsed.ok) return parsed;
|
||||
if (parsed.document.kind === "fragment") {
|
||||
return {
|
||||
ok: false,
|
||||
diagnostics: [
|
||||
{
|
||||
phase: "validation",
|
||||
code: "expected-root-lock",
|
||||
message: "Expected a root Quixos lock, found a lock fragment",
|
||||
fileName,
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (parsed.document.imports.length) {
|
||||
return {
|
||||
ok: false,
|
||||
diagnostics: [
|
||||
{
|
||||
phase: "resolution",
|
||||
code: "imports-require-file-resolution",
|
||||
message:
|
||||
"This lock has imports and must be loaded from its repository rather than parsed as an isolated string",
|
||||
fileName,
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
lock: {
|
||||
formatVersion: 1,
|
||||
quixos: parsed.document.quixos,
|
||||
resources: parsed.document.resources,
|
||||
},
|
||||
diagnostics: [],
|
||||
};
|
||||
};
|
||||
|
||||
const quoted = (value: string) => JSON.stringify(value);
|
||||
|
||||
const sourceLines = (source: GitSource, indentation: string): string[] => [
|
||||
`${indentation}repository ${quoted(source.repository)};`,
|
||||
`${indentation}commit ${quoted(source.commit.toLowerCase())};`,
|
||||
];
|
||||
|
||||
const quixosSourceLines = (source: QuixosSource, indentation: string): string[] => [
|
||||
`${indentation}repository ${quoted(source.repository)};`,
|
||||
...(source.policy ? [`${indentation}policy ${source.policy};`, `${indentation}ref ${quoted(source.ref)};`] : []),
|
||||
`${indentation}commit ${quoted(source.commit.toLowerCase())};`,
|
||||
];
|
||||
|
||||
export const formatQuixosLock = (lock: QuixosRepositoryLock): string => {
|
||||
const lines = ["quixos-lock version 1 {", " quixos source {", ...quixosSourceLines(lock.quixos, " "), " }"];
|
||||
for (const resource of lock.resources) {
|
||||
lines.push("", ` ${resource.kind} ${resource.binding} source {`, ...sourceLines(resource.source, " "), " }");
|
||||
}
|
||||
lines.push("}", "");
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
export const formatQuixosLockDocument = (document: QuixosLockDocument): string => {
|
||||
const lines = [`quixos-lock${document.kind === "fragment" ? " fragment" : ""} version 1 {`];
|
||||
if (document.kind === "root") {
|
||||
lines.push(" quixos source {", ...quixosSourceLines(document.quixos, " "), " }");
|
||||
}
|
||||
for (const importPath of document.imports) {
|
||||
if (lines.length > 1) lines.push("");
|
||||
lines.push(` import ${quoted(importPath)};`);
|
||||
}
|
||||
for (const resource of document.resources) {
|
||||
if (lines.length > 1) lines.push("");
|
||||
lines.push(` ${resource.kind} ${resource.binding} source {`, ...sourceLines(resource.source, " "), " }");
|
||||
}
|
||||
lines.push("}", "");
|
||||
return lines.join("\n");
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
export type GitSource = {
|
||||
resolver: "git";
|
||||
repository: string;
|
||||
commit: string;
|
||||
};
|
||||
|
||||
export type QuixosSourcePolicy = "pinned" | "track-release" | "track-development";
|
||||
|
||||
// Resource repositories only need the exact Quixos commit they were authored
|
||||
// against. A workspace root additionally declares how a runtime may advance
|
||||
// that exact baseline.
|
||||
export type QuixosSource = GitSource &
|
||||
(
|
||||
| {
|
||||
policy: QuixosSourcePolicy;
|
||||
ref: string;
|
||||
}
|
||||
| {
|
||||
policy?: undefined;
|
||||
ref?: undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export type LockedResourceKind = "interface" | "package";
|
||||
|
||||
export type LockedResource = {
|
||||
kind: LockedResourceKind;
|
||||
binding: string;
|
||||
source: GitSource;
|
||||
};
|
||||
|
||||
export type QuixosLockRootDocument = {
|
||||
kind: "root";
|
||||
formatVersion: 1;
|
||||
quixos: QuixosSource;
|
||||
imports: string[];
|
||||
resources: LockedResource[];
|
||||
};
|
||||
|
||||
export type QuixosLockFragmentDocument = {
|
||||
kind: "fragment";
|
||||
formatVersion: 1;
|
||||
imports: string[];
|
||||
resources: LockedResource[];
|
||||
};
|
||||
|
||||
export type QuixosLockDocument = QuixosLockRootDocument | QuixosLockFragmentDocument;
|
||||
|
||||
export type QuixosRepositoryLock = {
|
||||
formatVersion: 1;
|
||||
quixos: QuixosSource;
|
||||
resources: LockedResource[];
|
||||
sourceFiles?: string[];
|
||||
};
|
||||
|
||||
export type NixGitInput = {
|
||||
type: "git";
|
||||
url: string;
|
||||
ref: string;
|
||||
rev: string;
|
||||
};
|
||||
|
||||
export const RETENTION_TAG_PREFIX = "refs/tags/quixos-reachability/";
|
||||
|
||||
export const retentionTagForCommit = (commit: string): string => `${RETENTION_TAG_PREFIX}${commit.toLowerCase()}`;
|
||||
|
||||
export const nixGitInput = (source: GitSource): NixGitInput => ({
|
||||
type: "git",
|
||||
url: source.repository,
|
||||
ref: retentionTagForCommit(source.commit),
|
||||
rev: source.commit.toLowerCase(),
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { convergeAuthoring } from "../src/capability-language/authoring-converge.js";
|
||||
import { loadQuixosLock } from "../src/resource-lock/index.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("source convergence propagates nested edits and unchanged snapshots reach a fixed point", async (context) => {
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-converge-test-"));
|
||||
context.after(() => fs.rm(temporary, { recursive: true, force: true }));
|
||||
const workbench = path.join(temporary, "workbench"),
|
||||
remotes = path.join(temporary, "remotes");
|
||||
await fs.mkdir(path.join(workbench, ".quixos"), { recursive: true });
|
||||
await fs.mkdir(remotes);
|
||||
const origin = "https://convergence.example.test/";
|
||||
const previous = Object.fromEntries(
|
||||
["GIT_CONFIG_COUNT", "GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"].map((key) => [key, process.env[key]]),
|
||||
);
|
||||
process.env.GIT_CONFIG_COUNT = "1";
|
||||
process.env.GIT_CONFIG_KEY_0 = `url.file://${remotes}/.insteadOf`;
|
||||
process.env.GIT_CONFIG_VALUE_0 = origin;
|
||||
context.after(() => {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
const resources = [];
|
||||
let dependency = "";
|
||||
for (const [directory, kind, name] of [
|
||||
["resources/Base", "interface", "Base"],
|
||||
["resources/Consumer", "package", "Consumer"],
|
||||
["root", "workspace", "Root"],
|
||||
]) {
|
||||
const root = path.join(workbench, directory);
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await execFile("jj", ["git", "init", "--colocate", root]);
|
||||
await execFile("git", ["init", "--bare", path.join(remotes, name)]);
|
||||
const repository = `${origin}${name}`;
|
||||
await execFile("git", ["-C", root, "remote", "add", "origin", repository]);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } ${dependency} }`,
|
||||
);
|
||||
await fs.writeFile(path.join(root, `${kind}.qx`), "draft");
|
||||
await execFile("jj", ["status"], { cwd: root });
|
||||
const commit = (
|
||||
await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root })
|
||||
).stdout.trim();
|
||||
if (kind !== "workspace")
|
||||
resources.push({
|
||||
directory,
|
||||
kind,
|
||||
resourceId: `${kind}:${name}`,
|
||||
source: { resolver: "git", repository, commit },
|
||||
});
|
||||
dependency = `${kind} ${name} source { repository "${repository}"; commit "${commit}"; }`;
|
||||
}
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({ resources }));
|
||||
const first = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(first.worklist, []);
|
||||
assert.equal(first.converged, true);
|
||||
assert.equal(first.verificationEvidence, false, "source retention never approves even syntactically invalid code");
|
||||
await fs.writeFile(path.join(workbench, "resources/Base/interface.qx"), "edited draft");
|
||||
const second = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(second.worklist, []);
|
||||
assert.notEqual(second.candidate?.commit, first.candidate?.commit);
|
||||
const consumer = await loadQuixosLock(path.join(workbench, "resources/Consumer/quixos.lock"));
|
||||
assert.ok(consumer.ok);
|
||||
assert.equal(
|
||||
consumer.lock.resources[0].source.commit,
|
||||
second.retained.find((entry) => entry.directory === "resources/Base")?.source.commit,
|
||||
);
|
||||
const third = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(third, second);
|
||||
const base = path.join(workbench, "resources/Base");
|
||||
const consumerRoot = path.join(workbench, "resources/Consumer");
|
||||
const goodLock = await fs.readFile(path.join(consumerRoot, "quixos.lock"), "utf8");
|
||||
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), "broken draft");
|
||||
const scoped = await convergeAuthoring(workbench, "resources/Base");
|
||||
assert.deepEqual(scoped.worklist, [], "an unrelated malformed consumer cannot block a provider check");
|
||||
await fs.writeFile(path.join(base, "interface.qx"), "another edit");
|
||||
const partial = await convergeAuthoring(workbench);
|
||||
assert.equal(partial.converged, false);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
assert.equal(
|
||||
graph.resources[0].source.commit,
|
||||
partial.retained.find((entry) => entry.directory === "resources/Base")?.source.commit,
|
||||
);
|
||||
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), goodLock);
|
||||
const resumed = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(resumed.worklist, []);
|
||||
assert.deepEqual(await convergeAuthoring(workbench), resumed);
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Wrong"]);
|
||||
const mismatch = await convergeAuthoring(workbench);
|
||||
assert.ok(mismatch.worklist.some((entry) => entry.phase === "source" && /Origin differs/.test(entry.message)));
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Base"]);
|
||||
await fs.rename(path.join(remotes, "Base"), path.join(remotes, "Base-offline"));
|
||||
const offline = await convergeAuthoring(workbench);
|
||||
assert.equal(offline.candidate, null);
|
||||
assert.ok(offline.worklist.some((entry) => entry.phase === "publication"));
|
||||
await fs.rename(path.join(remotes, "Base-offline"), path.join(remotes, "Base"));
|
||||
assert.equal((await convergeAuthoring(workbench)).converged, true);
|
||||
const consumerSource = resumed.retained.find((entry) => entry.directory === "resources/Consumer")!.source;
|
||||
await fs.writeFile(
|
||||
path.join(base, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } package Consumer source { repository "${consumerSource.repository}"; commit "${consumerSource.commit}"; } }`,
|
||||
);
|
||||
const cycle = await convergeAuthoring(workbench);
|
||||
assert.equal(cycle.candidate, null);
|
||||
assert.ok(cycle.worklist.some((entry) => /Source dependency cycle/.test(entry.message)));
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, writeFile, rm, symlink } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { inspectAuthoringRepository } from "../src/capability-language/authoring-inspect.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
test("inspection keeps current files and labels historical recovery without granting verification", async (context) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-inspection-test-"));
|
||||
context.after(() => rm(root, { recursive: true, force: true }));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await writeFile(
|
||||
path.join(root, "interface.qx"),
|
||||
'interface Example id "interface:example" revision "interface:example@1" {}',
|
||||
);
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await writeFile(path.join(root, "interface.qx"), "interface broken {{{");
|
||||
await writeFile(path.join(root, "new.qx"), 'interface New id "interface:new" revision "interface:new@1" {}');
|
||||
const inspected = await inspectAuthoringRepository(root);
|
||||
assert.equal(inspected.verificationEvidence, false);
|
||||
assert.equal(inspected.resolutionChecked, false);
|
||||
assert.equal(inspected.files[0].status, "historical");
|
||||
assert.equal(inspected.files[0].revision, commit);
|
||||
assert.ok(inspected.files[0].currentErrors.length);
|
||||
assert.match(inspected.files[0].declarations[0].source, /Example/);
|
||||
assert.equal(inspected.files[1].status, "current");
|
||||
assert.match(inspected.files[1].declarations[0].source, /New/);
|
||||
assert.equal((await git("rev-parse", "HEAD")).stdout.trim(), commit);
|
||||
await symlink(path.join(root, "interface.qx"), path.join(root, "linked.qx"));
|
||||
const linked = (await inspectAuthoringRepository(root)).files.find((file) => file.file === "linked.qx")!;
|
||||
assert.equal(linked.status, "unavailable");
|
||||
assert.deepEqual(linked.declarations, []);
|
||||
assert.match(JSON.stringify(linked.currentErrors), /ordinary files/);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { authoringWorklist } from "../src/capability-language/authoring-worklist.js";
|
||||
import { checkRecordName } from "../src/capability-language/authoring-check.js";
|
||||
import { checkerIdentity, snapshotCommit } from "../src/capability-language/checked-build.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("worklist grows and clears from current source, dependency and checker observations", async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-worklist-test-"));
|
||||
context.after(() => fs.rm(workbench, { recursive: true, force: true }));
|
||||
const root = path.join(workbench, "root"),
|
||||
provider = path.join(workbench, "resources/Base");
|
||||
await fs.mkdir(root);
|
||||
await fs.mkdir(provider, { recursive: true });
|
||||
await fs.mkdir(path.join(workbench, ".quixos/checks"), { recursive: true });
|
||||
const header = `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; }`;
|
||||
for (const directory of [root, provider]) await execFile("jj", ["git", "init", "--colocate", directory]);
|
||||
await fs.writeFile(
|
||||
path.join(provider, "interface.qx"),
|
||||
'interface Base id "interface:base" revision "interface:base@1" {}',
|
||||
);
|
||||
await fs.writeFile(path.join(provider, "quixos.lock"), `${header} }`);
|
||||
const baseCommit = await snapshotCommit(provider);
|
||||
await fs.writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { import interface Base; }`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`${header} interface Base source { repository "https://example.test/base"; commit "${baseCommit}"; } }`,
|
||||
);
|
||||
const rootCommit = await snapshotCommit(root);
|
||||
await fs.writeFile(
|
||||
path.join(workbench, ".quixos/resource-graph.json"),
|
||||
JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
kind: "interface",
|
||||
directory: provider,
|
||||
source: { resolver: "git", repository: "https://example.test/base", commit: baseCommit },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.equal((await authoringWorklist(workbench)).worklist.filter((entry) => entry.phase === "unchecked").length, 2);
|
||||
const remember = (directory: string, commit: string, checker = checkerIdentity()) =>
|
||||
fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName(directory)),
|
||||
JSON.stringify({ commit, checker, phase: "checked", blockers: [] }),
|
||||
);
|
||||
await remember("root", rootCommit);
|
||||
await remember("resources/Base", baseCommit);
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), "interface broken {{{");
|
||||
const broken = await authoringWorklist(workbench);
|
||||
assert.ok(
|
||||
broken.worklist.some(
|
||||
(entry) => entry.directory === "resources/Base" && entry.phase === "syntax" && /historical/.test(entry.message),
|
||||
),
|
||||
);
|
||||
assert.ok(broken.worklist.some((entry) => entry.directory === "root" && entry.phase === "dependency"));
|
||||
await fs.writeFile(
|
||||
path.join(provider, "interface.qx"),
|
||||
'interface Base id "interface:base" revision "interface:base@1" {}',
|
||||
);
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await remember("resources/Base", baseCommit, "old-checker");
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some((entry) => /checker changed/.test(entry.message)));
|
||||
await fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName("resources/Base")),
|
||||
JSON.stringify({ phase: "publication", blockers: ["source retention unavailable"] }),
|
||||
);
|
||||
assert.ok(
|
||||
(await authoringWorklist(workbench)).worklist.some(
|
||||
(entry) => entry.phase === "publication" && /source retention/.test(entry.message),
|
||||
),
|
||||
);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user