Files
quixos-protocol/src/query/templates.ts
T
Timothy J. Aveni 5634d3ee78 Add checked workspace-interface query roots
Implement scoped interface extents across QX, generated bindings, SDK,
Orch, Camino and browser queries. Preserve native projection, aggregation,
pagination and watch invalidation without synthetic persisted objects.
Validate exact query grants and scope freshness on execution and replay.
Add local PostgreSQL, transport, browser and code-generation coverage.

C1 implementation slice only: full-text indexes and Studio search adoption
(C2/C3) remain tracked in the implementation handoff. No publication or deployment.
2026-09-19 10:18:47 -07:00

137 lines
5.9 KiB
TypeScript

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: { kind: template.root.kind, interface: substitution.application(template.root.interface) },
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);
}