Compare commits

..

6 Commits

Author SHA1 Message Date
Quixos Subtree Publisher ae0a5064a4 Publish quixos-protocol from 53b9a56e8cb120c34d6b5c85b52b18080dd1d9a3 2026-09-17 23:49:44 +00:00
Timothy J. Aveni cd13120937 Implement query execution, scoped RPC enrichment, live collections, and scaffold integration 2026-09-17 16:49:44 -07:00
Timothy J. Aveni 61a410f98f Add queryable capability contracts and checked GraphQL artifacts 2026-09-17 14:55:10 -07:00
Timothy J. Aveni e3501772c5 Add exact runtime conformance lookup and agent-visible language limits
Add authorized exact-contract lookup, revision-fenced views, generated typed package/browser descriptors, and lifecycle-safe React discovery. Preserve closed generic contracts without competing-conformance policies or new authority grants. Document supported workarounds and privacy boundaries for agent feedback. Publish matching protocol/SDK exports and update scaffold/starter pins; leave the deployed template unchanged.
2026-09-17 12:04:06 -07:00
Quixos Subtree Publisher 7863ce3e12 Publish quixos-protocol from db6d7990a4ffec273258ac313b7d2aeac57b261a 2026-09-17 18:54:46 +00:00
Timothy J. Aveni ea51ca89e7 Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:54:46 -07:00
44 changed files with 6680 additions and 2405 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "a45fd9a85b541ce6b449b28e8b47954bf04de96b",
"sourceCommit": "53b9a56e8cb120c34d6b5c85b52b18080dd1d9a3",
"sourcePath": "quixos-protocol",
"exportName": "quixos-protocol",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
+52 -3
View File
@@ -100,7 +100,7 @@ operationMember
;
valueMember
: VALUE identifier ID stringLiteral COLON valueType
: queryReadContract? VALUE identifier ID stringLiteral COLON valueType
LBRACE valueMemberOperation* RBRACE
;
@@ -111,10 +111,14 @@ valueMemberOperation
;
relationshipMember
: RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED?
: 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
@@ -138,6 +142,26 @@ 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
@@ -187,6 +211,7 @@ dependencyPort
| 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
@@ -252,7 +277,7 @@ relationshipMaterializationDecl
;
operationBindingDecl
: BIND memberOperationRef TO operationProvider SEMI
: BIND memberOperationRef TO operationProvider (QUERY_REASON stringLiteral)? SEMI
;
memberOperationRef
@@ -303,6 +328,7 @@ dependencyBinding
| 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
@@ -371,6 +397,17 @@ jsonArray
identifier
: IDENTIFIER
| SOURCE
| QUERY
| SPECIALIZE
| ROOT
| DOCUMENT
| FRAGMENTS
| VIEW
| MAX
| ALLOW
| POLL
| RPC
| QUERYABLE
;
stringLiteral
@@ -378,6 +415,18 @@ stringLiteral
;
WORKSPACE: 'workspace';
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';
+2 -1
View File
@@ -32,7 +32,8 @@
"@babel/parser": "^7.28.0",
"@bufbuild/protobuf": "^2.12.1",
"@bufbuild/protoc-gen-es": "^2.12.1",
"antlr4ng": "^3.0.16"
"antlr4ng": "^3.0.16",
"graphql": "16.11.0"
},
"devDependencies": {
"@types/node": "^24",
+83
View File
@@ -5,6 +5,8 @@ package camino;
import "camino/schema.proto";
service CaminoService {
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);
@@ -214,3 +216,84 @@ message CaminoOp {
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;
}
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;
}
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
}
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;
}
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;
}
+72
View File
@@ -70,4 +70,76 @@ message PersistencePlan {
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;
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;
}
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;
}
+26
View File
@@ -3,11 +3,14 @@ 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 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);
@@ -23,6 +26,16 @@ service OrchestratorRuntime {
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
}
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;
}
// Exact closed interface lookup; no policy selection or competing conformances.
message TryConformRequest {
string object_id = 1;
@@ -110,6 +123,7 @@ message WatchCapabilityEvent {
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;
@@ -123,6 +137,18 @@ message GetWorkspaceResponse {
// 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;
+1
View File
@@ -29,6 +29,7 @@ message InjectedDependency {
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.
+5
View File
@@ -13,6 +13,9 @@ 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[] };
@@ -108,6 +111,8 @@ export const genericImplementationType = (
]);
return object(methods);
}
case "query":
return queryPort(requirement);
case "constructor":
return object([
[
+67 -3
View File
@@ -144,6 +144,33 @@ export const generateTypeScriptBindings = (
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)}>`];
@@ -282,10 +309,31 @@ export const generateTypeScriptBindings = (
for (const definition of pkg.genericExports ?? []) {
handlers.push([
definition.displayName,
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
genericImplementationType(
definition,
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])],
type,
(requirement) => port({ id: capabilityId.dependencyPort("query"), displayName: "query", requirement }).type,
),
]);
}
const contracts = schema.interfaces.map((iface) => {
// 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,
@@ -304,7 +352,19 @@ export const generateTypeScriptBindings = (
throw new Error(`Invalid message binding export ${binding.export}`);
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
});
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${contracts.map((entry) => entry.type).join(" ")}`;
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",
@@ -317,6 +377,8 @@ export const generateTypeScriptBindings = (
"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` +
@@ -325,6 +387,8 @@ export const generateTypeScriptBindings = (
`\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)
+9
View File
@@ -20,6 +20,15 @@ declare module "@quixos/web-studio-react-runtime" {
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;
+56 -2
View File
@@ -78,7 +78,30 @@ export function generateReactBindings(
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"]>;`;
});
const contracts = schema.interfaces.map((iface) => {
// 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 },
@@ -93,8 +116,39 @@ export function generateReactBindings(
);
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} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, 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
+23
View File
@@ -1,6 +1,10 @@
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,
@@ -231,6 +235,20 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
}
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");
@@ -389,6 +407,11 @@ export const compileWorkspaceRepository = async (options: {
.join("\n")}`,
);
}
const linkedQueries = linkQueries(workspace);
if (linkedQueries.length) {
workspace.linkedQueries = linkedQueries;
checked.plan.source.linkedQueries = structuredClone(linkedQueries);
}
return {
workspace,
plan: checked.plan,
File diff suppressed because one or more lines are too long
@@ -1,229 +1,253 @@
WORKSPACE=1
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
QUERY=2
SPECIALIZE=3
ROOT=4
DOCUMENT=5
FRAGMENTS=6
VIEW=7
MAX=8
ALLOW=9
POLL=10
QUERYABLE=11
RPC=12
QUERY_REASON=13
TYPE=14
OBJECT=15
STORABLE=16
IMPLEMENTS=17
REF=18
FRAGMENT=19
IMPORT=20
EXTERNAL=21
ATOM=22
INTERFACE=23
INTERFACES=24
PACKAGE=25
VALUE=26
RELATION=27
OPERATION=28
FUNCTION=29
CONSTRUCTOR=30
CONSTRUCTS=31
INPUT=32
CONFORM=33
AS=34
BIND=35
STATIC=36
TO=37
PRIVATE=38
SHARED=39
STATE=40
EDGE=41
PROJECTION=42
WITH=43
USING=44
VIA=45
MATERIALIZE=46
IF=47
ABSENT=48
ON=49
POLICY=50
DEFAULT=51
SOURCE=52
REPOSITORY=53
COMMIT=54
REVISION=55
SEMANTIC_MAJOR=56
ON_DELETE=57
RETAIN_OTHER=58
KEYED=59
PUBLIC_TRAVERSAL=60
ID=61
DOC=62
MODE=63
EMITS=64
RECEIVER=65
REQUIRES=66
ANY=67
GET=68
SET=69
WATCH=70
START=71
STOP=72
READ=73
WRITE=74
RESOLVE=75
CONNECT=76
DISCONNECT=77
CALL=78
WATCH_START=79
WATCH_STOP=80
SUBSCRIBE=81
UNSUBSCRIBE=82
OPTIMISTIC_REGISTER=83
CRDT=84
OPTIONAL_ONE=85
EXACTLY_ONE=86
MANY_UNIQUE=87
MANY=88
ORDERED=89
UNIT=90
WATCH_HANDLE=91
MESSAGE=92
ATOM_REF=93
INTERFACE_REF=94
OPTIONAL=95
LIST=96
RECORD=97
BOOL=98
BYTES=99
DOUBLE=100
INT32=101
INT64=102
STRING=103
UINT32=104
UINT64=105
TRUE=106
FALSE=107
NULL=108
ARROW=109
COLON=110
SEMI=111
COMMA=112
DOT=113
LBRACE=114
RBRACE=115
LBRACK=116
RBRACK=117
LPAREN=118
RPAREN=119
LT=120
GT=121
AMP=122
EQUAL=123
INTEGER=124
JSON_NUMBER=125
IDENTIFIER=126
STRING_LITERAL=127
LINE_COMMENT=128
BLOCK_COMMENT=129
WS=130
'workspace'=1
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
'query'=2
'specialize'=3
'root'=4
'document'=5
'fragments'=6
'view'=7
'max'=8
'allow'=9
'poll'=10
'queryable'=11
'rpc'=12
'query-reason'=13
'type'=14
'object'=15
'storable'=16
'implements'=17
'ref'=18
'fragment'=19
'import'=20
'external'=21
'atom'=22
'interface'=23
'interfaces'=24
'package'=25
'value'=26
'relation'=27
'operation'=28
'function'=29
'constructor'=30
'constructs'=31
'input'=32
'conform'=33
'as'=34
'bind'=35
'static'=36
'to'=37
'private'=38
'shared'=39
'state'=40
'edge'=41
'projection'=42
'with'=43
'using'=44
'via'=45
'materialize'=46
'if'=47
'absent'=48
'on'=49
'policy'=50
'default'=51
'source'=52
'repository'=53
'commit'=54
'revision'=55
'semantic-major'=56
'on-delete'=57
'retain-other'=58
'keyed'=59
'public-traversal'=60
'id'=61
'doc'=62
'mode'=63
'emits'=64
'receiver'=65
'requires'=66
'any'=67
'get'=68
'set'=69
'watch'=70
'start'=71
'stop'=72
'read'=73
'write'=74
'resolve'=75
'connect'=76
'disconnect'=77
'call'=78
'watch-start'=79
'watch-stop'=80
'subscribe'=81
'unsubscribe'=82
'optimistic-register'=83
'crdt'=84
'optional-one'=85
'exactly-one'=86
'many-unique'=87
'many'=88
'ordered'=89
'unit'=90
'watch-handle'=91
'message'=92
'atom-ref'=93
'interface-ref'=94
'optional'=95
'list'=96
'record'=97
'bool'=98
'bytes'=99
'double'=100
'int32'=101
'int64'=102
'string'=103
'uint32'=104
'uint64'=105
'true'=106
'false'=107
'null'=108
'->'=109
':'=110
';'=111
','=112
'.'=113
'{'=114
'}'=115
'['=116
']'=117
'('=118
')'=119
'<'=120
'>'=121
'&'=122
'='=123
File diff suppressed because one or more lines are too long
@@ -1,229 +1,253 @@
WORKSPACE=1
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
QUERY=2
SPECIALIZE=3
ROOT=4
DOCUMENT=5
FRAGMENTS=6
VIEW=7
MAX=8
ALLOW=9
POLL=10
QUERYABLE=11
RPC=12
QUERY_REASON=13
TYPE=14
OBJECT=15
STORABLE=16
IMPLEMENTS=17
REF=18
FRAGMENT=19
IMPORT=20
EXTERNAL=21
ATOM=22
INTERFACE=23
INTERFACES=24
PACKAGE=25
VALUE=26
RELATION=27
OPERATION=28
FUNCTION=29
CONSTRUCTOR=30
CONSTRUCTS=31
INPUT=32
CONFORM=33
AS=34
BIND=35
STATIC=36
TO=37
PRIVATE=38
SHARED=39
STATE=40
EDGE=41
PROJECTION=42
WITH=43
USING=44
VIA=45
MATERIALIZE=46
IF=47
ABSENT=48
ON=49
POLICY=50
DEFAULT=51
SOURCE=52
REPOSITORY=53
COMMIT=54
REVISION=55
SEMANTIC_MAJOR=56
ON_DELETE=57
RETAIN_OTHER=58
KEYED=59
PUBLIC_TRAVERSAL=60
ID=61
DOC=62
MODE=63
EMITS=64
RECEIVER=65
REQUIRES=66
ANY=67
GET=68
SET=69
WATCH=70
START=71
STOP=72
READ=73
WRITE=74
RESOLVE=75
CONNECT=76
DISCONNECT=77
CALL=78
WATCH_START=79
WATCH_STOP=80
SUBSCRIBE=81
UNSUBSCRIBE=82
OPTIMISTIC_REGISTER=83
CRDT=84
OPTIONAL_ONE=85
EXACTLY_ONE=86
MANY_UNIQUE=87
MANY=88
ORDERED=89
UNIT=90
WATCH_HANDLE=91
MESSAGE=92
ATOM_REF=93
INTERFACE_REF=94
OPTIONAL=95
LIST=96
RECORD=97
BOOL=98
BYTES=99
DOUBLE=100
INT32=101
INT64=102
STRING=103
UINT32=104
UINT64=105
TRUE=106
FALSE=107
NULL=108
ARROW=109
COLON=110
SEMI=111
COMMA=112
DOT=113
LBRACE=114
RBRACE=115
LBRACK=116
RBRACK=117
LPAREN=118
RPAREN=119
LT=120
GT=121
AMP=122
EQUAL=123
INTEGER=124
JSON_NUMBER=125
IDENTIFIER=126
STRING_LITERAL=127
LINE_COMMENT=128
BLOCK_COMMENT=129
WS=130
'workspace'=1
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
'query'=2
'specialize'=3
'root'=4
'document'=5
'fragments'=6
'view'=7
'max'=8
'allow'=9
'poll'=10
'queryable'=11
'rpc'=12
'query-reason'=13
'type'=14
'object'=15
'storable'=16
'implements'=17
'ref'=18
'fragment'=19
'import'=20
'external'=21
'atom'=22
'interface'=23
'interfaces'=24
'package'=25
'value'=26
'relation'=27
'operation'=28
'function'=29
'constructor'=30
'constructs'=31
'input'=32
'conform'=33
'as'=34
'bind'=35
'static'=36
'to'=37
'private'=38
'shared'=39
'state'=40
'edge'=41
'projection'=42
'with'=43
'using'=44
'via'=45
'materialize'=46
'if'=47
'absent'=48
'on'=49
'policy'=50
'default'=51
'source'=52
'repository'=53
'commit'=54
'revision'=55
'semantic-major'=56
'on-delete'=57
'retain-other'=58
'keyed'=59
'public-traversal'=60
'id'=61
'doc'=62
'mode'=63
'emits'=64
'receiver'=65
'requires'=66
'any'=67
'get'=68
'set'=69
'watch'=70
'start'=71
'stop'=72
'read'=73
'write'=74
'resolve'=75
'connect'=76
'disconnect'=77
'call'=78
'watch-start'=79
'watch-stop'=80
'subscribe'=81
'unsubscribe'=82
'optimistic-register'=83
'crdt'=84
'optional-one'=85
'exactly-one'=86
'many-unique'=87
'many'=88
'ordered'=89
'unit'=90
'watch-handle'=91
'message'=92
'atom-ref'=93
'interface-ref'=94
'optional'=95
'list'=96
'record'=97
'bool'=98
'bytes'=99
'double'=100
'int32'=101
'int64'=102
'string'=103
'uint32'=104
'uint64'=105
'true'=106
'false'=107
'null'=108
'->'=109
':'=110
';'=111
','=112
'.'=113
'{'=114
'}'=115
'['=116
']'=117
'('=118
')'=119
'<'=120
'>'=121
'&'=122
'='=123
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,10 +24,14 @@ 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";
@@ -210,6 +214,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @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
@@ -234,6 +244,24 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @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
+290 -2
View File
@@ -46,6 +46,14 @@ import {
specializePackageExport,
} from "../capability-model/index.js";
import { GenericSourceTypes } from "./generic-types.js";
import {
defaultQueryBudgets,
type QueryDeclaration,
type QueryBudgets,
type QueryUse,
type QueryTemplate,
} from "../query/types.js";
import { specializeQueryTemplate } from "../query/templates.js";
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
import {
QuixosCapabilityParser,
@@ -176,6 +184,7 @@ interface PackageExportSymbol {
}
interface PackageSymbol {
queries?: Map<string, string>;
definition?: PackageRevision;
revisionId: PackageRevision["revisionId"];
exports: Map<string, PackageExportSymbol>;
@@ -463,6 +472,13 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
}
return {
kind: "value",
...(context.queryReadContract()
? {
queryRead: {
execution: context.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(context.stringLiteral())),
displayName: memberName,
valueType: memberValueType,
@@ -527,7 +543,15 @@ const lowerRelationshipMember = (
}
return {
kind: "relationship",
id: capabilityId.member(stringValue(context.stringLiteral())),
...(context.KEYED() ? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
...(context.queryReadContract()
? {
queryRead: {
execution: context.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(context.stringLiteral(0))),
displayName: identifier(context.identifier()),
target,
cardinality,
@@ -672,6 +696,13 @@ const lowerInterfaceTemplate = (
const type = types.expression(value.valueType());
return {
kind: "value",
...(value.queryReadContract()
? {
queryRead: {
execution: value.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(value.stringLiteral())),
displayName: identifier(value.identifier()),
valueType: type,
@@ -725,7 +756,17 @@ const lowerInterfaceTemplate = (
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
return {
kind: "relationship",
id: capabilityId.member(stringValue(relationship.stringLiteral())),
...(relationship.KEYED()
? { keyType: stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64" }
: {}),
...(relationship.queryReadContract()
? {
queryRead: {
execution: relationship.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(relationship.stringLiteral(0))),
displayName: identifier(relationship.identifier()),
target,
cardinality,
@@ -789,9 +830,34 @@ const lowerInterfaceTemplate = (
};
};
const lowerQueryReference = (
state: LoweringState,
packageName: string,
queryName: string,
context: ParserRuleContext,
) => {
const pkg = requireSymbol(state, state.packages, packageName, context, "package");
const queryId =
pkg?.queries?.get(queryName) ?? pkg?.definition?.queries?.find((query) => query.displayName === queryName)?.id;
if (!pkg || !queryId) {
loweringIssue(state, context, "unknown-query", `Unknown query export ${packageName}.${queryName}`);
return undefined;
}
return { packageRevisionId: pkg.revisionId, queryId };
};
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
const name = identifier(context.identifier(0));
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
if (context.QUERY()) {
const query = lowerQueryReference(
state,
identifier(context.identifier(1)),
identifier(context.identifier(2)),
context,
);
return query ? { id, displayName: name, requirement: { kind: "query", ...query } } : undefined;
}
if (context.STATE()) {
const primitives = context
.primitiveList()!
@@ -1031,6 +1097,16 @@ const lowerGenericPackageExport = (
.primitiveList()
?.primitive()
.map((item) => text(item)) ?? [];
if (port.QUERY()) {
const query = lowerQueryReference(state, identifier(port.identifier(1)), identifier(port.identifier(2)), port);
if (!query)
throw new GenericTypeError(
"unknown-query",
base.displayName,
"Query port requires an existing closed query export",
);
return { ...base, requirement: { kind: "query", ...query } };
}
if (port.STATE())
return {
...base,
@@ -1129,8 +1205,179 @@ const lowerPackage = (
source: SourceRevision,
): PackageRevision => {
const alias = identifier(context.identifier());
state.packages.get(alias)!.queries = new Map(
context.packageExport().flatMap((entry) => {
const query = entry.packageQuery();
const specialization = entry.packageQuerySpecialization();
return query && !query.typeParameters()
? [[identifier(query.identifier()), stringValue(query.stringLiteral(0))] as const]
: specialization
? [[identifier(specialization.identifier(0)), stringValue(specialization.stringLiteral())] as const]
: [];
}),
);
const genericExports: GenericPackageExport[] = [];
const queries: QueryDeclaration[] = [];
const queryTemplates: QueryTemplate[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
if (exportContext.packageQuerySpecialization()) return [];
const query = exportContext.packageQuery();
if (query) {
const previousParameters = state.types.parameters;
state.types.parameters = new Map();
try {
const parameters = state.types.declareParameters(
query.typeParameters(),
`query:${stringValue(query.stringLiteral(0))}`,
);
const expressions = new Map<string, ReturnType<GenericSourceTypes["interface"]>>();
const templateViews: QueryTemplate["views"] = [];
const templateAllowances: QueryTemplate["allowances"] = [];
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
parameters.length
? (() => {
const id = capabilityId.interfaceRevision(`query-expression:${expressions.size}`);
expressions.set(id, state.types.interface(ctx));
return id;
})()
: new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
const declaration: QueryDeclaration = {
id: stringValue(query.stringLiteral(0)),
displayName: identifier(query.identifier()),
root: close(query.interfaceType()),
document: stringValue(query.stringLiteral(1)),
operation: stringValue(query.stringLiteral(2)),
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: false,
};
const budgetNames: Record<string, keyof QueryBudgets> = {
rows: "rows",
depth: "depth",
"result-bytes": "resultBytes",
candidates: "candidates",
"rpc-calls": "rpcCalls",
concurrency: "concurrency",
"deadline-ms": "deadlineMs",
};
const assigned = new Set<string>();
for (const clause of query.queryClause()) {
if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else if (clause.VIEW()) {
const name = identifier(clause.identifier(0));
if (parameters.length) {
const parameter = state.types.parameters.get(name);
const atomId = state.atoms.get(name);
if (clause.OBJECT() && parameter?.kind === "object")
templateViews.push({
target: { kind: "parameter", parameterId: parameter.id },
interface: state.types.interface(clause.interfaceType()!),
});
else if (!clause.OBJECT() && atomId)
templateViews.push({
target: { kind: "atom", atomId },
interface: state.types.interface(clause.interfaceType()!),
});
else
loweringIssue(state, clause, "invalid-query-view", "Expected declared object parameter or atom view");
continue;
}
if (clause.OBJECT())
loweringIssue(state, clause, "invalid-query-view", "Object parameter views require a query template");
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
} else if (clause.MAX()) {
const name = identifier(clause.identifier(0));
const key = budgetNames[name],
n = Number(clause.INTEGER()!.getText());
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
loweringIssue(
state,
clause,
"invalid-query-budget",
`Invalid, duplicate, or excessive query budget ${name}`,
);
else {
declaration.budgets[key] = n;
assigned.add(name);
}
} else if (clause.ALLOW()) {
if (parameters.length) {
const use = identifier(clause.identifier(1));
const reason = stringValue(clause.stringLiteral()!);
if (!["select", "predicate", "order"].includes(use) || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-allowance",
"Expected select/predicate/order and a nonempty reason",
);
templateAllowances.push({
interface: state.types.interface(clause.interfaceType()!),
memberName: identifier(clause.identifier(0)),
uses: [use as QueryUse],
reason,
});
continue;
}
const interfaceRevisionId = close(clause.interfaceType()!);
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
(entry) => entry.revisionId === interfaceRevisionId,
);
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
const use = identifier(clause.identifier(1));
const reason = stringValue(clause.stringLiteral()!);
if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-allowance",
"Query allowances name an exact member, use (select/predicate/order), and nonempty reason",
);
else
declaration.allowances.push({
interfaceRevisionId,
memberId: member.id,
uses: [use as QueryUse],
reason,
});
} else if (clause.WATCH()) declaration.watch = true;
else if (clause.POLL()) {
const intervalMs = Number(clause.INTEGER()!.getText()),
reason = stringValue(clause.stringLiteral()!);
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-polling",
"Polling needs one interval of at least 1000ms and a reason",
);
else declaration.polling = { intervalMs, reason };
}
}
if (
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
)
)
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
if (parameters.length) {
const { root, views: _views, allowances: _allowances, ...common } = declaration;
queryTemplates.push({
declaration: common,
parameters,
root: expressions.get(root)!,
views: templateViews,
allowances: templateAllowances,
});
} else queries.push(declaration);
return [];
} finally {
state.types.parameters = previousParameters;
}
}
const operation = exportContext.packageOperationExport();
const generic = operation ?? exportContext.packageFunctionExport();
if (generic?.typeParameters()) {
@@ -1155,6 +1402,34 @@ const lowerPackage = (
state.types.self = previousSelf;
}
});
for (const entry of context.packageExport()) {
const specialized = entry.packageQuerySpecialization();
if (!specialized) continue;
const templateName = identifier(specialized.identifier(1));
const template = queryTemplates.find((entry) => entry.declaration.displayName === templateName);
if (!template) {
loweringIssue(state, specialized, "unknown-query-template", `Unknown local query template ${templateName}`);
continue;
}
const substitution = new TypeSubstitution(state.types.environment());
const arguments_ = state.types
.arguments(specialized.typeArguments())
.map((argument) => substitution.argument(argument));
const declaration = specializeQueryTemplate(
template,
arguments_,
state.types.environment(),
() => [...state.types.definitions.values()],
{ id: stringValue(specialized.stringLiteral()), displayName: identifier(specialized.identifier(0)) },
);
if (
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
)
)
loweringIssue(state, specialized, "duplicate-query", "Duplicate query name or ID");
queries.push(declaration);
}
return {
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
revisionId: state.packages.get(alias)!.revisionId,
@@ -1162,6 +1437,11 @@ const lowerPackage = (
displayName: alias,
source,
exports,
...(queries.length ? { queries } : {}),
...(queryTemplates.length ? { queryTemplates } : {}),
...(queries.some((query) => query.argumentRequirements?.length)
? { argumentRequirements: queries.flatMap((query) => query.argumentRequirements ?? []) }
: {}),
...(genericExports.length ? { genericExports } : {}),
};
};
@@ -1291,6 +1571,13 @@ const lowerBoundDependencies = (
);
return projectionId ? { edgeTypeId: attachment.attachment.id, projectionId } : undefined;
};
if (entry.QUERY()) {
const query = lowerQueryReference(state, identifier(entry.identifier(1)), identifier(entry.identifier(2)), entry);
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
return query && (!entry.VIA() || via)
? [{ portId, binding: { kind: "query", ...query, ...(via ? { via } : {}) } }]
: [];
}
if (entry.STATE()) {
const attachmentName = identifier(entry.identifier(1));
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
@@ -1568,6 +1855,7 @@ const lowerConformance = (
? [
{
operationId,
...(bindingContext.stringLiteral() ? { queryReason: stringValue(bindingContext.stringLiteral()!) } : {}),
binding: {
kind: "package" as const,
packageRevisionId: packageSymbol.revisionId,
+8 -1
View File
@@ -4,13 +4,20 @@ 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(`QX imports must be ordinary files beneath ordinary directories: ${name}`);
throw new Error(`Sources must be ordinary files beneath ordinary directories: ${name}`);
}
return readFile(current, "utf8");
};
+1 -1
View File
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
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|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
!/^(?:[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))
+15
View File
@@ -132,6 +132,21 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
})),
});
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") {
+3
View File
@@ -22,6 +22,7 @@ import {
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
requirement:
| Extract<DependencyPort["requirement"], { kind: "query" }>
| {
kind: "state";
valueType: ValueTypeExpression;
@@ -79,6 +80,8 @@ export const specializePackageExport = (
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":
+8 -1
View File
@@ -87,14 +87,21 @@ export const instantiateInterface = (
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
switch (member.kind) {
case "value":
return { ...common, kind: "value", valueType: substitution.value(member.valueType, member.displayName) };
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 {
+13
View File
@@ -116,6 +116,8 @@ export interface InterfaceOperation<Type = ValueType> {
eventType?: Type;
}
export type QueryReadContract = { execution: "native" | "rpc-permitted" };
interface InterfaceMemberBase<Type = ValueType> {
id: MemberId;
displayName: string;
@@ -124,6 +126,7 @@ interface InterfaceMemberBase<Type = ValueType> {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value";
queryRead?: QueryReadContract;
valueType: Type;
}
@@ -141,9 +144,11 @@ export interface RelationshipInterfaceMember<
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. */
@@ -218,6 +223,7 @@ export type PackageReceiverRequirement =
};
export type DependencyPortRequirement =
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string }
| {
kind: "state";
valueType: ValueType;
@@ -270,6 +276,9 @@ export interface PackageConstructorExport extends PackageExportBase {
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;
@@ -283,6 +292,7 @@ export interface PackageRevision {
}
export type DependencyBinding =
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string; via?: EdgeTraversal }
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
| {
kind: "edge";
@@ -342,6 +352,8 @@ export type Binding =
export interface OperationBinding {
operationId: OperationId;
binding: Binding;
/** Required for package-backed reads of an RPC-permitted queryable field. */
queryReason?: string;
}
export interface Conformance {
@@ -363,6 +375,7 @@ export interface AtomConstructorBinding {
}
export interface WorkspaceRevision {
linkedQueries?: import("../query/link.js").LinkedQuery[];
id: WorkspaceRevisionId;
workspaceId: WorkspaceId;
parentRevisionIds: WorkspaceRevisionId[];
+121 -1
View File
@@ -40,6 +40,8 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode =
| "invalid-query-contract"
| "query-provider-reason-required"
| "invalid-semantic-major"
| "duplicate-conformance-id"
| "required-value"
@@ -502,6 +504,33 @@ const collectIdentityIndexes = (
const operations = new Map<OperationId, InterfaceOperationEntry>();
for (const [memberIndex, member] of revision.members.entries()) {
const memberPath = `${path}.members[${memberIndex}]`;
if (
member.kind === "relationship" &&
member.keyType !== undefined &&
(!["string", "boolean", "int64"].includes(member.keyType) ||
!["many", "many-unique"].includes(member.cardinality))
)
issue(
issues,
"invalid-query-contract",
memberPath,
"Keyed relationships require many cardinality and string, boolean or int64 keys",
);
if (member.kind !== "operation" && member.queryRead) {
const contract = member.queryRead.execution;
const getter = member.kind === "value" ? "get" : "resolve";
if (contract !== "native" && contract !== "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract");
if (member.operations.filter((operation) => operation.displayName === getter).length !== 1)
issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`);
if (member.kind === "relationship" && contract === "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
if (member.kind === "value") {
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
if (type.kind !== "scalar")
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar");
}
}
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name");
if (memberIds.has(member.id)) {
@@ -967,6 +996,21 @@ const validatePackages = (
for (const [portIndex, port] of entry.dependencyPorts.entries()) {
const portPath = `${exportPath}.dependencyPorts[${portIndex}]`;
switch (port.requirement.kind) {
case "query":
if (
!indexes.packages
.get(port.requirement.packageRevisionId)
?.revision.queries?.some(
(query) => port.requirement.kind === "query" && query.id === port.requirement.queryId,
)
)
issue(
issues,
"unresolved-reference",
portPath,
`Unknown query ${port.requirement.packageRevisionId}:${port.requirement.queryId}`,
);
break;
case "state":
validateValueType(issues, port.requirement.valueType, `${portPath}.requirement.valueType`, indexes);
uniquePrimitiveList(issues, port.requirement.primitives, `${portPath}.requirement.primitives`);
@@ -1116,6 +1160,41 @@ const validateBoundDependencies = (
continue;
}
switch (requirement.kind) {
case "query": {
const selected = binding as Extract<DependencyBinding, { kind: "query" }>;
const query = params.indexes.packages
.get(requirement.packageRevisionId)
?.revision.queries?.find((query) => query.id === requirement.queryId);
if (
!query ||
selected.queryId !== requirement.queryId ||
selected.packageRevisionId !== requirement.packageRevisionId
) {
issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export");
break;
}
const traversal = selected.via
? validateTraversal(
issues,
params.indexes,
params.atomId,
selected.via,
`${path}.binding.via`,
params.conformance,
)
: undefined;
const targets = selected.via
? traversal
? [...params.indexes.atoms.keys()].filter((atom) =>
atomSatisfiesConstraint(atom as AtomId, traversal.target.constraint, params.indexes.conformances),
)
: []
: [params.atomId];
for (const atom of targets)
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root)))
issue(issues, "unsatisfied-interface", path, `Query root ${atom} does not conform to ${query.root}`);
break;
}
case "state": {
const stateBinding = binding as Extract<DependencyBinding, { kind: "state" }>;
const attachment = findAttachment(params.indexes, "state", stateBinding.slotId);
@@ -1424,6 +1503,31 @@ const validateConformances = (
}
const operation = operationEntry.operation;
const binding = entry.binding;
const member = operationEntry.member;
if (
member.kind !== "operation" &&
member.queryRead &&
operation.displayName === (member.kind === "value" ? "get" : "resolve")
) {
const native =
member.kind === "value"
? binding.kind === "state" && binding.primitive === "read"
: binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue(
issues,
"invalid-query-contract",
bindingPath,
"This queryable contract requires a native read binding; a justification cannot weaken it",
);
else if (!native && !entry.queryReason?.trim())
issue(
issues,
"query-provider-reason-required",
bindingPath,
"Package-backed query reads require a concrete query-reason justification",
);
}
if (operation.scope === "class" && (!conformance.id || operation.mode !== "call")) {
issue(
issues,
@@ -1541,7 +1645,8 @@ const validateConformances = (
operationEntry.member.target,
indexes.conformances,
) ||
operationEntry.member.cardinality !== projection.endpoint.cardinality
operationEntry.member.cardinality !== projection.endpoint.cardinality ||
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
) {
issue(
issues,
@@ -2049,6 +2154,21 @@ export const computeCapabilityClosure = (
const includeDependencies = (atomId: AtomId, dependencies: readonly BoundDependency[]) => {
for (const dependency of dependencies) {
switch (dependency.binding.kind) {
case "query": {
packages.add(dependency.binding.packageRevisionId);
const query = plan.packages
.get(dependency.binding.packageRevisionId)
?.checkedQueries?.find(
(query) => dependency.binding.kind === "query" && query.declaration.id === dependency.binding.queryId,
);
if (dependency.binding.via) attachments.add(dependency.binding.via.edgeTypeId);
for (const effect of query?.effects ?? []) {
for (const conformance of sourceConformances.values())
if (conformance.interfaceRevisionId === effect.interfaceRevisionId)
queued.push({ atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId });
}
break;
}
case "state":
attachments.add(dependency.binding.slotId);
if (dependency.binding.via) {
File diff suppressed because one or more lines are too long
+364 -1
View File
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file camino/schema.proto.
*/
export const file_camino_schema: GenFile = /*@__PURE__*/
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAki7AEKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudCpoCgtDYXJkaW5hbGl0eRIbChdDQVJESU5BTElUWV9VTlNQRUNJRklFRBAAEhAKDE9QVElPTkFMX09ORRABEg8KC0VYQUNUTFlfT05FEAISCAoETUFOWRADEg8KC01BTllfVU5JUVVFEARiBnByb3RvMw");
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLdAgoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhpHCg5Bcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEi6QIKEFF1ZXJ5UmVhZEJpbmRpbmcSDwoHYXRvbV9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSEQoJbWVtYmVyX2lkGAMgASgJEhsKE2dldHRlcl9vcGVyYXRpb25faWQYBCABKAkSDwoHc2xvdF9pZBgFIAEoCRIUCgxlZGdlX3R5cGVfaWQYBiABKAkSFQoNcHJvamVjdGlvbl9pZBgHIAEoCRILCgNycGMYCCABKAgSIAoYd2F0Y2hfc3RhcnRfb3BlcmF0aW9uX2lkGAkgASgJEh8KF3dhdGNoX3N0b3Bfb3BlcmF0aW9uX2lkGAogASgJEhIKCmZpZWxkX25hbWUYCyABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAwgASgJEigKC2NhcmRpbmFsaXR5GA0gASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5EhAKCGtleV90eXBlGA4gASgJIpIBCgxRdWVyeUJ1ZGdldHMSDAoEcm93cxgBIAEoDRINCgVkZXB0aBgCIAEoDRIUCgxyZXN1bHRfYnl0ZXMYAyABKA0SEgoKY2FuZGlkYXRlcxgEIAEoDRIRCglycGNfY2FsbHMYBSABKA0SEwoLY29uY3VycmVuY3kYBiABKA0SEwoLZGVhZGxpbmVfbXMYByABKA0ijQQKDkluc3RhbGxlZFF1ZXJ5EgoKAmlkGAEgASgJEhkKEWRlZmluaXRpb25fZGlnZXN0GAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEiIKGnJvb3RfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAQgASgJEikKCXNlbGVjdGlvbhgFIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIqCghiaW5kaW5ncxgGIAMoCzIYLmNhbWluby5RdWVyeVJlYWRCaW5kaW5nEiUKB2J1ZGdldHMYByABKAsyFC5jYW1pbm8uUXVlcnlCdWRnZXRzEhsKE3ZhcmlhYmxlc190eXBlX2pzb24YCCABKAkSGAoQb3V0cHV0X3R5cGVfanNvbhgJIAEoCRJHChF2YXJpYWJsZV9kZWZhdWx0cxgKIAMoCzIsLmNhbWluby5JbnN0YWxsZWRRdWVyeS5WYXJpYWJsZURlZmF1bHRzRW50cnkSDQoFd2F0Y2gYCyABKAgSGwoTcG9sbGluZ19pbnRlcnZhbF9tcxgMIAEoDRIeChZycGNfcHJlZGljYXRlX29yX29yZGVyGA0gASgIGk4KFVZhcmlhYmxlRGVmYXVsdHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEqaAoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEYgZwcm90bzM");
/**
* @generated from message camino.AtomDefinition
@@ -273,6 +273,11 @@ export type PersistencePlan = Message<"camino.PersistencePlan"> & {
* @generated from field: repeated camino.EdgeAttachment edges = 6;
*/
edges: EdgeAttachment[];
/**
* @generated from field: repeated camino.InstalledQuery queries = 7;
*/
queries: InstalledQuery[];
};
/**
@@ -282,6 +287,364 @@ export type PersistencePlan = Message<"camino.PersistencePlan"> & {
export const PersistencePlanSchema: GenMessage<PersistencePlan> = /*@__PURE__*/
messageDesc(file_camino_schema, 6);
/**
* Immutable checked query IR. Installed only with the checked persistence plan;
* query callers select its ID, never send or modify these definitions.
*
* @generated from message camino.QueryArgument
*/
export type QueryArgument = Message<"camino.QueryArgument"> & {
/**
* @generated from oneof camino.QueryArgument.value
*/
value: {
/**
* @generated from field: string variable = 1;
*/
value: string;
case: "variable";
} | {
/**
* @generated from field: string literal_json = 2;
*/
value: string;
case: "literalJson";
} | {
/**
* @generated from field: camino.QueryArgumentList list = 3;
*/
value: QueryArgumentList;
case: "list";
} | {
/**
* @generated from field: camino.QueryArgumentObject object = 4;
*/
value: QueryArgumentObject;
case: "object";
} | { case: undefined; value?: undefined };
};
/**
* Describes the message camino.QueryArgument.
* Use `create(QueryArgumentSchema)` to create a new message.
*/
export const QueryArgumentSchema: GenMessage<QueryArgument> = /*@__PURE__*/
messageDesc(file_camino_schema, 7);
/**
* @generated from message camino.QueryArgumentList
*/
export type QueryArgumentList = Message<"camino.QueryArgumentList"> & {
/**
* @generated from field: repeated camino.QueryArgument values = 1;
*/
values: QueryArgument[];
};
/**
* Describes the message camino.QueryArgumentList.
* Use `create(QueryArgumentListSchema)` to create a new message.
*/
export const QueryArgumentListSchema: GenMessage<QueryArgumentList> = /*@__PURE__*/
messageDesc(file_camino_schema, 8);
/**
* @generated from message camino.QueryArgumentObject
*/
export type QueryArgumentObject = Message<"camino.QueryArgumentObject"> & {
/**
* @generated from field: map<string, camino.QueryArgument> fields = 1;
*/
fields: { [key: string]: QueryArgument };
};
/**
* Describes the message camino.QueryArgumentObject.
* Use `create(QueryArgumentObjectSchema)` to create a new message.
*/
export const QueryArgumentObjectSchema: GenMessage<QueryArgumentObject> = /*@__PURE__*/
messageDesc(file_camino_schema, 9);
/**
* @generated from message camino.QueryCondition
*/
export type QueryCondition = Message<"camino.QueryCondition"> & {
/**
* @generated from field: bool include = 1;
*/
include: boolean;
/**
* @generated from field: camino.QueryArgument value = 2;
*/
value?: QueryArgument | undefined;
};
/**
* Describes the message camino.QueryCondition.
* Use `create(QueryConditionSchema)` to create a new message.
*/
export const QueryConditionSchema: GenMessage<QueryCondition> = /*@__PURE__*/
messageDesc(file_camino_schema, 10);
/**
* @generated from message camino.QuerySelection
*/
export type QuerySelection = Message<"camino.QuerySelection"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* @generated from field: string key = 2;
*/
key: string;
/**
* @generated from field: string interface_revision_id = 3;
*/
interfaceRevisionId: string;
/**
* @generated from field: string member_id = 4;
*/
memberId: string;
/**
* @generated from field: string target_interface_revision_id = 5;
*/
targetInterfaceRevisionId: string;
/**
* @generated from field: repeated camino.QueryCondition conditions = 6;
*/
conditions: QueryCondition[];
/**
* @generated from field: map<string, camino.QueryArgument> arguments = 7;
*/
arguments: { [key: string]: QueryArgument };
/**
* @generated from field: repeated camino.QuerySelection selection = 8;
*/
selection: QuerySelection[];
};
/**
* Describes the message camino.QuerySelection.
* Use `create(QuerySelectionSchema)` to create a new message.
*/
export const QuerySelectionSchema: GenMessage<QuerySelection> = /*@__PURE__*/
messageDesc(file_camino_schema, 11);
/**
* @generated from message camino.QueryReadBinding
*/
export type QueryReadBinding = Message<"camino.QueryReadBinding"> & {
/**
* @generated from field: string atom_id = 1;
*/
atomId: string;
/**
* @generated from field: string interface_revision_id = 2;
*/
interfaceRevisionId: string;
/**
* @generated from field: string member_id = 3;
*/
memberId: string;
/**
* @generated from field: string getter_operation_id = 4;
*/
getterOperationId: string;
/**
* @generated from field: string slot_id = 5;
*/
slotId: string;
/**
* @generated from field: string edge_type_id = 6;
*/
edgeTypeId: string;
/**
* @generated from field: string projection_id = 7;
*/
projectionId: string;
/**
* @generated from field: bool rpc = 8;
*/
rpc: boolean;
/**
* @generated from field: string watch_start_operation_id = 9;
*/
watchStartOperationId: string;
/**
* @generated from field: string watch_stop_operation_id = 10;
*/
watchStopOperationId: string;
/**
* @generated from field: string field_name = 11;
*/
fieldName: string;
/**
* @generated from field: string value_type_json = 12;
*/
valueTypeJson: string;
/**
* @generated from field: camino.Cardinality cardinality = 13;
*/
cardinality: Cardinality;
/**
* @generated from field: string key_type = 14;
*/
keyType: string;
};
/**
* Describes the message camino.QueryReadBinding.
* Use `create(QueryReadBindingSchema)` to create a new message.
*/
export const QueryReadBindingSchema: GenMessage<QueryReadBinding> = /*@__PURE__*/
messageDesc(file_camino_schema, 12);
/**
* @generated from message camino.QueryBudgets
*/
export type QueryBudgets = Message<"camino.QueryBudgets"> & {
/**
* @generated from field: uint32 rows = 1;
*/
rows: number;
/**
* @generated from field: uint32 depth = 2;
*/
depth: number;
/**
* @generated from field: uint32 result_bytes = 3;
*/
resultBytes: number;
/**
* @generated from field: uint32 candidates = 4;
*/
candidates: number;
/**
* @generated from field: uint32 rpc_calls = 5;
*/
rpcCalls: number;
/**
* @generated from field: uint32 concurrency = 6;
*/
concurrency: number;
/**
* @generated from field: uint32 deadline_ms = 7;
*/
deadlineMs: number;
};
/**
* Describes the message camino.QueryBudgets.
* Use `create(QueryBudgetsSchema)` to create a new message.
*/
export const QueryBudgetsSchema: GenMessage<QueryBudgets> = /*@__PURE__*/
messageDesc(file_camino_schema, 13);
/**
* @generated from message camino.InstalledQuery
*/
export type InstalledQuery = Message<"camino.InstalledQuery"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string definition_digest = 2;
*/
definitionDigest: string;
/**
* @generated from field: string binding_digest = 3;
*/
bindingDigest: string;
/**
* @generated from field: string root_interface_revision_id = 4;
*/
rootInterfaceRevisionId: string;
/**
* @generated from field: repeated camino.QuerySelection selection = 5;
*/
selection: QuerySelection[];
/**
* @generated from field: repeated camino.QueryReadBinding bindings = 6;
*/
bindings: QueryReadBinding[];
/**
* @generated from field: camino.QueryBudgets budgets = 7;
*/
budgets?: QueryBudgets | undefined;
/**
* @generated from field: string variables_type_json = 8;
*/
variablesTypeJson: string;
/**
* @generated from field: string output_type_json = 9;
*/
outputTypeJson: string;
/**
* @generated from field: map<string, camino.QueryArgument> variable_defaults = 10;
*/
variableDefaults: { [key: string]: QueryArgument };
/**
* @generated from field: bool watch = 11;
*/
watch: boolean;
/**
* @generated from field: uint32 polling_interval_ms = 12;
*/
pollingIntervalMs: number;
/**
* @generated from field: bool rpc_predicate_or_order = 13;
*/
rpcPredicateOrOrder: boolean;
};
/**
* Describes the message camino.InstalledQuery.
* Use `create(InstalledQuerySchema)` to create a new message.
*/
export const InstalledQuerySchema: GenMessage<InstalledQuery> = /*@__PURE__*/
messageDesc(file_camino_schema, 14);
/**
* @generated from enum camino.Cardinality
*/
+162 -30
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file quixos/refs.proto.
*/
export const file_quixos_refs: GenFile = /*@__PURE__*/
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLEAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEQoJb2JqZWN0X2lkGAYgASgJQgkKB2JpbmRpbmciPQoORWRnZURlcGVuZGVuY3kSFAoMZWRnZV90eXBlX2lkGAEgASgJEhUKDXByb2plY3Rpb25faWQYAiABKAliBnByb3RvMw");
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLYAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEgoIcXVlcnlfaWQYByABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
/**
* @generated from message quixos.CapabilityRef
@@ -136,6 +136,12 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
*/
value: string;
case: "constructorAtomId";
} | {
/**
* @generated from field: string query_id = 7;
*/
value: string;
case: "queryId";
} | { case: undefined; value?: undefined };
/**
+433
View File
@@ -0,0 +1,433 @@
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 {
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,
);
};
const shapeScalar = (name: string): ValueType => {
switch (name) {
case "Boolean":
return valueType.bool;
case "Int":
return valueType.int32;
case "Int64":
return valueType.int64;
case "UInt32":
return valueType.uint32;
case "UInt64":
return valueType.uint64;
case "Float":
return valueType.double;
case "Bytes":
return valueType.bytes;
default:
return valueType.string;
}
};
/** 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);
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.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 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 inputEffects = (id: InterfaceRevisionId, value: ValueNode, use: "predicate" | "order") => {
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) {
if (use === "predicate" && ["and", "or", "not"].includes(field.name.value)) inputEffects(id, field.value, use);
else mark(id, field.name.value, use, field);
}
};
const info = new TypeInfo(generated.schema);
let hasContinuation = false;
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") inputEffects(target, argument.value, "predicate");
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
if (argument.name.value === "after") hasContinuation = true;
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,
);
}
},
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);
},
}),
);
if (
hasContinuation &&
[...effects.values()].some(
(effect) => effect.execution === "rpc-permitted" && effect.uses.some((use) => use !== "select"),
)
)
fail("QUERY_UNSUPPORTED_FEATURE", "RPC predicates/order support bounded first windows, not continuation");
const fragments = new Map(
definitions
.filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition")
.map((entry) => [entry.name.value, entry]),
);
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 (selection.name.value === "_qx") {
const contract = generated.byName.get(type.name)!;
const metadataFields: Record<string, ValueType> = {};
for (const metadata of selection.selectionSet?.selections ?? []) {
if (metadata.kind !== "Field" || metadata.name.value !== "ref")
fail("QUERY_UNSUPPORTED_FEATURE", "Select _qx.ref directly", metadata);
else metadataFields[metadata.alias?.value ?? "ref"] = valueType.interfaceRef(contract.revisionId);
}
fields[key] = { kind: "record", fields: metadataFields };
}
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 shapeScalar(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 (isScalarType(type) || isEnumType(type)) return shapeScalar(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);
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) : [],
},
];
});
const normalized = print(document),
schema = printSchema(generated.schema);
const definitionDigest = createHash("sha256")
.update(
canonicalJson({
semantics: 1,
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: selection(operations[0]!.selectionSet, generated.schema.getQueryType()!),
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`,
);
}
};
+156
View File
@@ -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;
}
+89
View File
@@ -0,0 +1,89 @@
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";
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),
});
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,
});
}),
}),
);
+253
View File
@@ -0,0 +1,253 @@
import {
GraphQLBoolean,
GraphQLString,
GraphQLInt,
GraphQLFloat,
GraphQLScalarType,
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLEnumType,
GraphQLList,
GraphQLNonNull,
GraphQLSchema,
type GraphQLOutputType,
type GraphQLInputType,
type GraphQLFieldConfigMap,
type GraphQLInputFieldConfigMap,
} 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";
// 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 const referenceScalar = new GraphQLScalarType({ name: "ManagedReference" });
export function querySchema(declaration: QueryDeclaration, interfaces: readonly InterfaceRevision[]) {
const contracts = new Map(interfaces.map((entry) => [entry.revisionId, entry]));
const objects = new Map<InterfaceRevisionId, GraphQLObjectType>();
const filters = new Map<InterfaceRevisionId, GraphQLInputObjectType>();
const orders = new Map<InterfaceRevisionId, GraphQLInputObjectType>();
const byName = new Map<string, InterfaceRevision>();
const comparisons = new Map<string, GraphQLInputObjectType>();
const metadata = new GraphQLObjectType({
name: "QxMetadata",
fields: { ref: { type: new GraphQLNonNull(referenceScalar) } },
});
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 !== "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 filter = (id: InterfaceRevisionId): GraphQLInputObjectType => {
const existing = filters.get(id);
if (existing) return existing;
const result = new GraphQLInputObjectType({
name: `${name(id)}Where`,
fields: () => {
const fields: GraphQLInputFieldConfigMap = {
and: { type: new GraphQLList(new GraphQLNonNull(result)) },
or: { type: new GraphQLList(new GraphQLNonNull(result)) },
not: { type: result },
};
for (const member of contract(id).members)
if (member.kind === "value" && member.queryRead) {
if (member.displayName in fields)
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${member.displayName}`);
fields[member.displayName] = { type: comparison(member.valueType) };
}
return fields;
},
});
filters.set(id, result);
return result;
};
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 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 };
}
+136
View File
@@ -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);
}
+109
View File
@@ -0,0 +1,109 @@
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";
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[];
}
+117
View File
@@ -0,0 +1,117 @@
import assert from "node:assert/strict";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../../src/capability-language/index.js";
import { compileQuery } from "../../src/query/compile.js";
import { linkQueries } from "../../src/query/link.js";
import type { InterfaceRevision, PackageRevision } from "../../src/capability-model/types.js";
export const queryFixtureSource = {
repository: "https://query-fixture.example.test/source.git",
commit: "1".repeat(40),
};
export async function queryWorkspaceFixture() {
const interfaces = new Map<string, InterfaceRevision>();
const sources: Record<string, string> = {};
function iface(name: string, members: string, parameters = "") {
const text = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
interface ${name}${parameters} id "${name}" revision "${name}@1" {${members}}`;
sources[name] = text;
const result = compileCapabilityResourceSource(text, {
source: queryFixtureSource,
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
});
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "interface") throw new Error("interface expected");
interfaces.set(name, result.resource.revision);
}
iface("PersonFacts", `queryable value name id "name" : string {get id "name:get";}`);
iface(
"TaskFacts",
`
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
queryable value done id "done" : bool {get id "done:get";}
queryable value rank id "rank" : int64 {get id "rank:get";}
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
queryable rpc value score id "score" : int32 {get id "score:get";}
`,
);
iface(
"Collection",
`queryable relation items id "items" : many object Item ordered {resolve id "items:resolve";}`,
"<object Item implements TaskFacts>",
);
const packageSource = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
package Queries id "queries" revision "queries@1" {
operation titleSet id "title-set" : string -> unit mode call receiver any
requires {state title id "title-port" : string [write];};
operation score id "score" : unit -> int32 mode call receiver any;
query Upcoming id "upcoming" root Collection<interface TaskFacts> document "upcoming.graphql" operation "Upcoming" {
fragments "row.graphql"; max rows 30; watch;
}
query Enriched id "enriched" root Collection<interface TaskFacts> document "enriched.graphql" operation "Enriched" {
max rows 30; allow TaskFacts.score select "Small visible page only";
}
query Ranked id "ranked" root Collection<interface TaskFacts> document "ranked.graphql" operation "Ranked" {
max rows 30; max candidates 60;
allow TaskFacts.score predicate "Bounded local collection";
allow TaskFacts.score order "Bounded local collection";
}
}`;
sources.Queries = packageSource;
const pkgResult = compileCapabilityResourceSource(packageSource, {
source: queryFixtureSource,
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
});
assert.ok(pkgResult.ok, JSON.stringify(pkgResult.diagnostics));
if (pkgResult.resource.kind !== "package") throw new Error("package expected");
const pkg: PackageRevision = pkgResult.resource.revision;
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
const documents: Record<string, string> = {
"upcoming.graphql": `query Upcoming($first: Int!, $after: Cursor) {root {items(first: $first, after: $after, where: {done: {eq: false}}, orderBy: [{rank: ASC}]) {entries {key cursor node {_qx {ref} ...TaskRow}} pageInfo {hasNextPage endCursor}}}}`,
"row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`,
"enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_qx {ref} title score}}}}}`,
"ranked.graphql": `query Ranked {root {items(first: 3, where: {score: {gt: 0}}, orderBy: [{score: DESC}]) {entries {key node {_qx {ref} title}}}}}`,
};
pkg.checkedQueries = await Promise.all(
pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)),
);
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
private state Done${atom} id "${atom}:done" on ${atom} : bool policy optimistic-register default false;
private state Rank${atom} id "${atom}:rank" on ${atom} : int64 policy optimistic-register default 0;
private edge Assignee${atom} id "${atom}:assignee" {
atom ${atom} projection assignee id "${atom}:assignee:forward" optional-one;
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
}
bind title.get to state Title${atom}.read;
bind title.set to package Queries.titleSet with {title to state Title${atom};};
bind done to state Done${atom}; bind rank to state Rank${atom};
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
}`;
const workspaceSource = `workspace QueryFixture id "query-fixture" revision "query-fixture@1" commit "${queryFixtureSource.commit}" {
import interface PersonFacts; import interface TaskFacts; import interface Collection; import package Queries;
atom Task id "Task"; atom Reminder id "Reminder"; atom Person id "Person"; atom Tasks id "Tasks";
conform Person as PersonFacts id "person-facts" {
private state Name id "person:name" on Person : string policy optimistic-register default "Nobody";
bind name to state Name;
}
${implementation("Task")} ${implementation("Reminder")}
conform Tasks as Collection<interface TaskFacts> id "tasks-collection" {
private edge Items id "items" {
atom Tasks projection items id "items:forward" many ordered;
interface TaskFacts projection collections id "items:inverse" many;
}
bind items.resolve to edge Items.items.resolve;
}
}`;
sources.workspace = workspaceSource;
const result = compileCapabilitySource(workspaceSource, "workspace.qx", {
interfaces,
interfaceClosure: allInterfaces,
packages: new Map([["Queries", pkg]]),
packageClosure: [pkg],
});
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const workspace = { ...result.workspace, linkedQueries: linkQueries(result.workspace) };
return { workspace, pkg, interfaces: allInterfaces, sources, documents };
}
+206
View File
@@ -0,0 +1,206 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { QueryCompileError } from "../src/query/types.js";
import { checkQueryTemplate } from "../src/query/templates.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
const compiled = compileCapabilityResourceSource(text, {
source,
environment: {
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
interfaceClosure: dependencies,
},
});
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
assert.equal(compiled.resource.kind, "interface");
if (compiled.resource.kind !== "interface") throw new Error("interface required");
return compiled.resource.revision;
};
const facts = iface(`interface TaskFacts id "facts" revision "facts@1" {
queryable value title id "title" : string { get id "title:get"; }
queryable value done id "done" : bool { get id "done:get"; }
queryable value due id "due" : optional<int64> { get id "due:get"; }
queryable rpc value score id "score" : int32 { get id "score:get"; }
value hidden id "hidden" : string { get id "hidden:get"; }
}`);
const collection = iface(
`import interface TaskFacts;
interface Tasks id "tasks" revision "tasks@1" {
queryable relation items id "items" : many interface TaskFacts ordered { resolve id "items:resolve"; }
}`,
[facts],
);
test("generic query templates check declared bounds before closed specialization", async () => {
const generic = iface(
`import interface TaskFacts;
interface Collection<object Item implements TaskFacts> id "generic-collection" revision "generic-collection@1" {
queryable relation items id "items" : many object Item { resolve id "items:resolve"; }
}`,
[facts],
);
const build = (bound: string) =>
compileCapabilityResourceSource(
`import interface Collection; import interface TaskFacts;
package GenericQueries id "generic-queries" revision "generic-queries@1" {
query Rows<object Item ${bound}> id "rows-template" root Collection<Item>
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
}`,
{
source,
environment: {
interfaces: new Map([
["Collection", generic],
["TaskFacts", facts],
]),
interfaceClosure: [generic, facts],
},
},
);
const result = build("implements TaskFacts");
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "package") throw new Error("package expected");
const pkg = result.resource.revision;
assert.equal(pkg.queries?.length, 1);
assert.equal(pkg.queryTemplates?.length, 1);
assert.equal(pkg.queries![0]!.application!.templateId, "rows-template");
const read = async () => `query Rows { root { items(first: 3) { entries { node { title } } } } }`;
const universal = await checkQueryTemplate(pkg.queryTemplates![0]!, [generic, facts], read);
assert.equal(
universal.effects.some((effect) => effect.memberId === "title"),
true,
);
const specialized = await compileQuery(
pkg.queries![0]!,
[generic, facts, ...(result.resource.specializations ?? [])],
read,
);
assert.equal(
specialized.effects.some((effect) => effect.memberId === "title"),
true,
);
const invalid = build("");
assert.ok(invalid.ok, JSON.stringify(invalid.diagnostics));
if (invalid.resource.kind !== "package") throw new Error("package expected");
await assert.rejects(
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
/bound|guarantee|implement/i,
);
});
function fixture(clauses = "") {
const result = compileCapabilityResourceSource(
`import interface Tasks; import interface TaskFacts;
package Queries id "queries" revision "queries@1" {
query Upcoming id "upcoming" root Tasks document "queries/upcoming.graphql" operation "Upcoming" {
fragments "queries/row.graphql"; max rows 30; ${clauses}
}
}`,
{
source,
environment: {
interfaces: new Map([
["Tasks", collection],
["TaskFacts", facts],
]),
interfaceClosure: [collection, facts],
},
},
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.equal(result.resource.kind, "package");
if (result.resource.kind !== "package") throw new Error("package required");
assert.equal(result.resource.revision.exports.length, 0);
return result.resource.revision.queries![0]!;
}
const document = `query Upcoming($first: Int!, $before: Int64!) {
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
entries { key node { _qx { ref } ...Row } } pageInfo { hasNextPage endCursor }
} }
}`;
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
const result = compileCapabilityResourceSource(
`import interface Tasks;
package Queries id "queries" revision "queries@1" {
operation load id "read" : unit -> unit mode call receiver interfaces [Tasks]
requires { query upcoming id "upcoming-port" : Queries.Upcoming; };
query Upcoming id "upcoming" root Tasks document "upcoming.graphql" operation "Upcoming" { max rows 30; }
}`,
{ source, environment: { interfaces: new Map([["Tasks", collection]]), interfaceClosure: [collection, facts] } },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.equal(result.resource.kind, "package");
if (result.resource.kind !== "package") throw new Error("package expected");
assert.deepEqual(result.resource.revision.exports[0]!.dependencyPorts[0]!.requirement, {
kind: "query",
packageRevisionId: "queries@1",
queryId: "upcoming",
});
});
test("result types merge repeated selections and preserve conditional fragment fields", async () => {
const checked = await compile(
`query Upcoming($show: Boolean!) {
root { items(first: 3) { entries { node { title } } } }
root { items(first: 3) { entries { node { ...Row @include(if: $show) } } } }
}`,
"fragment Row on TaskFacts { due done }",
);
const output = JSON.stringify(checked.output);
assert.match(output, /\"title\":\{\"kind\":\"scalar\",\"name\":\"string\"\}/);
assert.match(output, /\"done\":\{\"kind\":\"optional\",\"value\":\{\"kind\":\"scalar\",\"name\":\"bool\"\}\}/);
assert.match(output, /\"due\"/);
});
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
const checked = await compile();
assert.deepEqual(checked.variables, {
kind: "record",
fields: {
first: { kind: "scalar", name: "int32" },
before: { kind: "scalar", name: "int64" },
},
});
assert.match(JSON.stringify(checked.output), /"kind":"object-ref"/);
assert.ok(
checked.effects.some(
(effect) => effect.memberId === "due" && effect.uses.includes("order") && effect.uses.includes("predicate"),
),
);
assert.equal(checked.definitionDigest, (await compile()).definitionDigest);
assert.notEqual(
checked.definitionDigest,
(await compile(document, "fragment Row on TaskFacts { title }")).definitionDigest,
);
});
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
const rejects = (promise: Promise<unknown>, code: string) =>
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
await compile(
document,
"fragment Row on TaskFacts { score }",
'allow TaskFacts.score select "Only the visible page is enriched";',
);
await rejects(
compile(document, "fragment Row on TaskFacts { score }", 'watch; allow TaskFacts.score select "Visible rows";'),
"QUERY_WATCH_UNSUPPORTED",
);
await compile(
document,
"fragment Row on TaskFacts { score }",
'watch; poll 5000 "External score has no push API"; allow TaskFacts.score select "Visible rows";',
);
await rejects(compile(document.replace("first: $first", "first: 31")), "QUERY_VALIDATION");
await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE");
await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION");
});
+41
View File
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import {
capabilityFixtureSource,
capabilityResourceSources,
compileCapabilityFixture,
} from "./fixtures/capability-model.js";
test("query contracts retain native reads and explicitly acknowledged package reads", () => {
const named = capabilityResourceSources.named.replace("value name", "queryable value name");
const native = compileCapabilityFixture({ named });
assert.equal(native.ok, true, JSON.stringify(native.diagnostics));
const summary = capabilityResourceSources.summary.replace("value summary", "queryable rpc value summary");
const missing = compileCapabilityFixture({ summary });
assert.equal(missing.ok, false);
assert.ok(missing.diagnostics.some((issue) => issue.code === "query-provider-reason-required"));
const workspace = capabilityFixtureSource.replace(
"person to constructor Person;\n };",
'person to constructor Person;\n } query-reason "Summary needs package execution; bounded query callers acknowledge this cost";',
);
const allowed = compileCapabilityFixture({ named, summary, workspace });
assert.equal(allowed.ok, true, JSON.stringify(allowed.diagnostics));
const denied = compileCapabilityFixture({ summary: summary.replace("queryable rpc", "queryable"), workspace });
assert.equal(denied.ok, false);
assert.ok(denied.diagnostics.some((issue) => issue.code === "invalid-query-contract"));
});
test("queryable fields need one authoritative getter and supported values", () => {
const compile = (member: string) =>
compileCapabilityResourceSource(`interface Facts id "facts" revision "facts@1" { ${member} }`, {
source: { repository: "https://example.test/facts.git", commit: "a".repeat(40) },
});
assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true);
for (const member of [
'queryable value title id "title" : string { set id "title:set"; }',
'queryable value title id "title" : list<string> { get id "title:get"; }',
'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }',
])
assert.equal(compile(member).ok, false, member);
});
+20
View File
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import test from "node:test";
import { queryWorkspaceFixture } from "./fixtures/query-workspace.js";
import { linkQueries } from "../src/query/link.js";
test("query fixture links generic collections, both implementations, and native getter/RPC setter", async () => {
const { workspace } = await queryWorkspaceFixture();
const query = workspace.linkedQueries.find((query) => query.id.endsWith(":upcoming"))!;
assert.ok(query);
assert.equal(query.fields.filter((field) => field.memberId === "title").length, 2);
assert.ok(
query.fields.filter((field) => field.memberId === "title").every((field) => field.binding.kind === "state"),
);
const broken = structuredClone(workspace);
const facts = broken.interfaceImports.find((iface) => iface.displayName === "TaskFacts")!;
const title = facts.members.find((member) => member.id === "title")!;
assert.equal(title.kind, "value");
delete title.queryRead;
assert.throws(() => linkQueries(broken), /changed/);
});
+12
View File
@@ -33,6 +33,18 @@ test("React bindings preserve read-only, writable and nested reference contracts
interfaces: [iface.resource.revision],
packages: [pkg.resource.revision],
} as const;
const opaque = structuredClone(iface.resource.revision);
opaque.revisionId = "opaque@1" as typeof opaque.revisionId;
opaque.displayName = "Opaque";
for (const member of opaque.members)
for (const operation of member.operations)
operation.outputType = { kind: "message", descriptorId: "UnsupportedBrowserMessage" };
const withUnusedOpaque = generateReactBindings(
{ ...schema, interfaces: [...schema.interfaces, opaque], packages: [...schema.packages] },
"p@1",
["props"],
);
assert.doesNotMatch(withUnusedOpaque, /"opaque@1": defineReactInterfaceContract/);
const generated = generateReactBindings(
{ ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] },
"p@1",
+1
View File
@@ -174,6 +174,7 @@ cacheEntries = {
"commander@npm:13.1.0" = { filename = "commander-npm-13.1.0-bdbbfaaf9d-7b8c5544bb.zip"; hash = "sha512-e4xVRLunBPvoS3yrLgQ9+FhtXBFKTFtgf4OuUGBwiUDtC1vVg4z4zidTnN4mXBy9Wc48jGsBftPuyJQ+OkFRZA=="; };
"debug@npm:4.4.3" = { filename = "debug-npm-4.4.3-0105c6123a-d79136ec6c.zip"; hash = "sha512-15E27GyD7L79D2pVk9pqnJHsTX3cS1TIg9bnHsmsy19noaXpbQCjKBlrW1yG02XpjYo6cIVqrxa057GYXmf1pg=="; };
"fast-printf@npm:1.6.10" = { filename = "fast-printf-npm-1.6.10-c05cca9b81-630cccbef8.zip"; hash = "sha512-YwzMvvg0mm7q2plYyTkenRS1vZUn3SjZ5+nylbAGxkDdyo/CRwz/VepcbJLKUwSIu62/fCT0hSrYw02RVdqddQ=="; };
"graphql@npm:16.11.0" = { filename = "graphql-npm-16.11.0-836e6ade28-124da7860a.zip"; hash = "sha512-Ek2nhgoikums8v7Qxx/A9qm5yoZdOQ0RK91WPB9HQ1cUFQHBKJH0Fk/phDFXZHNq1n9wUhnGL3WAaB1DGoXbiA=="; };
"he@npm:1.2.0" = { filename = "he-npm-1.2.0-3b73a2ff07-a27d478bef.zip"; hash = "sha512-on1Hi+/jyBkvAGzdBjmmZ5iXnfpuISXGrFgqGaXr/sYq2D6DguYDYXDYc/RuRTan55W/i5W/fCR/TMCCXMyMFw=="; };
"luxon@npm:3.5.0" = { filename = "luxon-npm-3.5.0-92bb977f7f-335789bba9.zip"; hash = "sha512-M1eJu6lQd9uDHvmYlO2t6yMCOz6yE3obVqzQ0pAIK2kc95MUPWnjC8Bp7JXwtJ82QZ9I6VHGgBTxn/4SBF40lA=="; };
"ms@npm:2.1.3" = { filename = "ms-npm-2.1.3-81ff3cfac1-d924b57e73.zip"; hash = "sha512-2SS1fnMSs7Y60h/Fs9wK9eeNYaH8fPtUV+2vJjJr9ivlMHzIf/toYu8cKzOwIzzbXU8BxMlYzA1mCUi2Wih6SA=="; };
+8
View File
@@ -85,6 +85,7 @@ __metadata:
"@types/node": "npm:^24"
antlr-ng: "npm:^1.0.10"
antlr4ng: "npm:^3.0.16"
graphql: "npm:16.11.0"
typescript: "npm:^7.0.2"
bin:
quixos-capability-compile: dist/src/capability-language/cli.js
@@ -321,6 +322,13 @@ __metadata:
languageName: node
linkType: hard
"graphql@npm:16.11.0":
version: 16.11.0
resolution: "graphql@npm:16.11.0"
checksum: 10c0/124da7860a2292e9acf2fed0c71fc0f6a9b9ca865d390d112bdd563c1f474357141501c12891f4164fe984315764736ad67f705219c62f7580681d431a85db88
languageName: node
linkType: hard
"he@npm:1.2.0":
version: 1.2.0
resolution: "he@npm:1.2.0"