Files
quixos-protocol/src/query/link.ts
T

180 lines
7.7 KiB
TypeScript
Raw Normal View History

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";
import { resolveForwardedBinding } from "../capability-model/forwarding.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[];
2026-09-18 14:55:12 -07:00
rootAtoms: AtomId[];
}
/** 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.kind === "module" ||
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 = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
getter.id,
).binding;
if (binding.kind === "package") {
if (effect.execution === "native")
throw new QueryCompileError(
"QUERY_NATIVE_BINDING_REQUIRED",
`Native query field ${effect.memberId} binds package code`,
);
if (effect.execution === "rpc-permitted" && !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.some((entry) => entry.operationId === start.id)
? resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id)
: undefined;
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));
2026-09-18 14:55:12 -07:00
const rootConformances = workspace.conformances.filter(
(entry) => entry.interfaceRevisionId === declaration.root.interface,
);
const rootAtoms = [...new Set(rootConformances.map((entry) => entry.atomId))].sort();
const bindingDigest = createHash("sha256")
.update(
canonicalJson({
definition: checked.definitionDigest,
2026-09-18 14:55:12 -07:00
rootConformances: rootConformances
.map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId }))
.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
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,
2026-09-18 14:55:12 -07:00
rootAtoms,
});
}
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;
}