From bd423060590b1424f8d81c0f04863a40c784a301 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Thu, 1 Oct 2026 09:21:57 -0700 Subject: [PATCH] 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. --- src/query/compile.ts | 19 ++++++++++++---- test/query-compiler.test.ts | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/query/compile.ts b/src/query/compile.ts index 68a9728..d9e0d49 100644 --- a/src/query/compile.ts +++ b/src/query/compile.ts @@ -634,15 +634,26 @@ export async function compileQuery( const definitionDigest = createHash("sha256") .update( canonicalJson({ - semantics: 5, + semantics: 6, declaration, normalized, fragmentContracts: Object.fromEntries(fragmentContracts), // Source provenance belongs to the checked installation. Identical public // contracts keep their query identity when retained in another repository. - // Closed generic identity remains explicit in revisionId/application. - interfaces: [...generated.byName.values()] - .map(({ source: _source, ...contract }) => contract) + // Closed generic identity remains explicit in revisionId/application; + // its template's retention source is provenance too. byName also holds + // names probed while discovering nominal projections, so only include + // interfaces actually present in the generated query schema. + interfaces: Object.values(generated.schema.getTypeMap()) + .filter(isObjectType) + .flatMap((type) => { + const revision = generated.byName.get(type.name); + if (!revision) return []; + const { source: _source, application, ...contract } = revision; + if (!application) return [contract]; + const { source: _templateSource, ...identity } = application; + return [{ ...contract, application: identity }]; + }) .sort((a, b) => (a.revisionId < b.revisionId ? -1 : a.revisionId > b.revisionId ? 1 : 0)), }), ) diff --git a/test/query-compiler.test.ts b/test/query-compiler.test.ts index f4e1cb5..6ccac88 100644 --- a/test/query-compiler.test.ts +++ b/test/query-compiler.test.ts @@ -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 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 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;