Fix Studio query contracts for authored workspaces

Keep query definition identity scoped to interfaces present in the generated
query schema. Exclude retention provenance from both interface revisions and
generic applications while preserving nominal identities, closed arguments,
operation contracts, documents, budgets and fragment dependencies. Advance the
query semantics version so prior compiler outputs cannot share the new identity.

Add a generic compiler regression covering unrelated imports, retained generic
sources and invalidation when an observed getter changes. This fixes Studio's
CLIENT_QUERY_MISMATCH when template contracts become editable local repositories.

Validation: protocol tests (296 total, 293 passed, 3 existing skips); nix fmt,
monorepo lint, final formatting check and diff check; camino-dev/qx-workspace
builds. All 17 Studio query descriptors match a rebuilt copy of the user's
workspace. Headless fresh-workspace canvas/library/Reader/reload check passes.

Repair the existing local qx-reader workbench through its ordinary check and
activation path, updating only its pinned protocol/checker CLI. Retain the
original toolchain GC root and record checker provenance. Source commit, runtime
and template pins, and database are preserved. Verify the selected candidate is
ready at epoch 2 and its canvas loads through the corrected Studio bundle without
creating application objects. Stop the independent smoke-test services.
This commit is contained in:
Timothy J. Aveni
2026-10-01 09:49:40 -07:00
parent 7c1830bcd5
commit bd42306059
2 changed files with 59 additions and 4 deletions
+44
View File
@@ -36,6 +36,50 @@ interface Tasks id "tasks" revision "tasks@1" {
[facts],
);
test("query identity ignores unrelated imports and generic retention provenance", async () => {
const generic = iface(`interface Box<object Item> id "box" revision "box@1" {
queryable relation item id "box:item" : optional-one object Item {resolve id "box:item:resolve";}
}`);
const compiled = compileCapabilityResourceSource(
`import interface Box; import interface TaskFacts;
package Queries id "queries" revision "queries@1" {
query Row id "row" root Box<interface TaskFacts> document "row.graphql" operation "Row" {replica;max rows 10;}
}`,
{
source,
environment: {
interfaces: new Map([
["Box", generic],
["TaskFacts", facts],
]),
interfaceClosure: [generic, facts],
},
},
);
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
if (compiled.resource.kind !== "package") throw new Error("package required");
const declaration = compiled.resource.revision.queries![0]!;
const closed = compiled.resource.specializations![0]!;
const read = async () => "query Row {root {item {title}}}";
const before = await compileQuery(declaration, [facts, closed], read);
const retained = structuredClone(closed);
retained.source = { repository: "https://local.test/box.git", commit: "b".repeat(40) };
retained.application!.source = retained.source;
const unrelated = iface(`interface Unrelated id "unrelated" revision "unrelated@1" {
queryable value label id "label" : string {get id "label:get";}
}`);
assert.equal(
(await compileQuery(declaration, [unrelated, generic, retained, facts], read)).definitionDigest,
before.definitionDigest,
);
const changed = structuredClone(facts);
changed.members[0]!.operations[0]!.id = "changed:title:get" as (typeof changed.members)[0]["operations"][0]["id"];
assert.notEqual(
(await compileQuery(declaration, [changed, retained], read)).definitionDigest,
before.definitionDigest,
);
});
test("generic query templates check declared bounds before closed specialization", async () => {
const generic = iface(
`import interface TaskFacts;