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.
This commit is contained in:
Timothy J. Aveni
2026-09-19 10:18:47 -07:00
parent 0810775c07
commit 5634d3ee78
27 changed files with 1371 additions and 945 deletions
+4
View File
@@ -44,6 +44,9 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
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 Extent id "extent" root workspace TaskFacts document "extent.graphql" operation "Extent" {
max rows 30; watch;
}
query Upcoming id "upcoming" root Collection<interface TaskFacts> document "upcoming.graphql" operation "Upcoming" {
fragments "row.graphql"; max rows 30; watch;
}
@@ -73,6 +76,7 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
const pkg: PackageRevision = pkgResult.resource.revision;
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
const documents: Record<string, string> = {
"extent.graphql": `query Extent {root {objects(first: 30, orderBy: [{rank: ASC}]) {entries {node {_qx {ref} title rank}}}}}`,
"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}}}}}`,
+52 -4
View File
@@ -45,11 +45,11 @@ test("generic query templates check declared bounds before closed specialization
}`,
[facts],
);
const build = (bound: string) =>
const build = (bound: string, workspace = false) =>
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>
query Rows<object Item ${bound}> id "rows-template" root ${workspace ? "workspace " : ""}Collection<Item>
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
}`,
@@ -93,12 +93,33 @@ test("generic query templates check declared bounds before closed specialization
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
/bound|guarantee|implement/i,
);
const extent = build("implements TaskFacts", true);
assert.ok(extent.ok, JSON.stringify(extent.diagnostics));
if (extent.resource.kind !== "package") throw new Error("package expected");
const extentPackage = extent.resource.revision;
const readExtent = async () =>
`query Rows {root {objects(first: 2) {entries {node {items(first: 3) {entries {node {title}}}}}}}}`;
await checkQueryTemplate(extentPackage.queryTemplates![0]!, [generic, facts], readExtent);
const closed = await compileQuery(
extentPackage.queries![0]!,
[generic, facts, ...(extent.resource.specializations ?? [])],
readExtent,
);
assert.equal(closed.declaration.root.kind, "workspace-interface");
assert.equal(closed.declaration.root.interface, specialized.declaration.root.interface);
const invalidExtent = build("", true);
assert.ok(invalidExtent.ok, JSON.stringify(invalidExtent.diagnostics));
if (invalidExtent.resource.kind !== "package") throw new Error("package expected");
await assert.rejects(
checkQueryTemplate(invalidExtent.resource.revision.queryTemplates![0]!, [generic, facts], readExtent),
/bound|guarantee|implement/i,
);
});
function fixture(clauses = "") {
function fixture(clauses = "", root = "Tasks") {
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" {
query Upcoming id "upcoming" root ${root} document "queries/upcoming.graphql" operation "Upcoming" {
fragments "queries/row.graphql"; max rows 30; ${clauses}
}
}`,
@@ -119,6 +140,33 @@ package Queries id "queries" revision "queries@1" {
assert.equal(result.resource.revision.exports.length, 0);
return result.resource.revision.queries![0]!;
}
test("workspace roots expose a bounded interface extent, not a root object or writable relationship", async () => {
const declaration = { ...fixture("watch;", "workspace TaskFacts"), fragments: [] };
assert.deepEqual(declaration.root, { kind: "workspace-interface", interface: "facts@1" });
const checked = await compileQuery(
declaration,
[facts, collection],
async () => `query Upcoming {
root {
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
entries {key node {_qx {ref} title @live}} pageInfo {hasNextPage endCursor}
}
_qx {relations {objects {aggregate {count sum {due}}}}}
}
}`,
);
assert.deepEqual(new Set(checked.effects.map((effect) => effect.memberId)), new Set(["done", "title", "due"]));
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
await assert.rejects(
compileQuery(declaration, [facts], async () => `query Upcoming {root {_qx {ref}}}`),
/QUERY_VALIDATION/,
);
await assert.rejects(
compileQuery(declaration, [facts], async () => `query Upcoming {root {objects {entries {key}}}}`),
/QUERY_ROW_LIMIT/,
);
});
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 }
+25
View File
@@ -2,6 +2,7 @@ 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";
import { compileQuery } from "../src/query/compile.js";
test("query fixture links generic collections, both implementations, and native getter/RPC setter", async () => {
const { workspace } = await queryWorkspaceFixture();
@@ -27,3 +28,27 @@ test("query fixture links generic collections, both implementations, and native
const totals = workspace.linkedQueries.find((q) => q.id.endsWith(":totals"))!;
assert.equal(totals.fields.filter((f) => f.memberId === "rank").length, 2);
});
test("reference-only workspace queries hash every eligible conformance, including empty atoms", async () => {
const { workspace } = await queryWorkspaceFixture();
const pkg = workspace.packageImports.find((pkg) => pkg.queries?.some((q) => q.id === "upcoming"))!;
const query = pkg.queries!.find((q) => q.id === "upcoming")!;
const contract = workspace.interfaceImports.find((iface) => iface.displayName === "TaskFacts")!;
query.root = { kind: "workspace-interface", interface: contract.revisionId };
query.fragments = [];
const checked = await compileQuery(
query,
workspace.interfaceImports,
async () => `query ${query.operation} {root {objects(first: 10) {entries {node {_qx {ref}}}}}}`,
);
pkg.checkedQueries = pkg.checkedQueries!.map((q) => (q.declaration.id === query.id ? checked : q));
const linked = linkQueries(workspace).find((q) => q.id.endsWith(":upcoming"))!;
assert.equal(linked.fields.length, 0);
assert.equal(linked.rootAtoms.length, 2);
workspace.conformances = workspace.conformances.filter(
(c) => !(c.atomId === linked.rootAtoms[0] && c.interfaceRevisionId === contract.revisionId),
);
const changed = linkQueries(workspace).find((q) => q.id === linked.id)!;
assert.equal(changed.rootAtoms.length, 1);
assert.notEqual(linked.bindingDigest, changed.bindingDigest);
});