From 15c132a38b45df04b78db98eec961eb384928eb1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sat, 19 Sep 2026 22:33:20 -0700 Subject: [PATCH] Compile complete local-first reader and scope fixtures --- .../local-first-reader/CommentData.qx | 27 ++++++ .../local-first-reader/DocumentActions.qx | 25 +++++ .../local-first-reader/DocumentData.qx | 36 +++++++ .../local-first-reader/DocumentFactory.qx | 25 +++++ .../local-first-reader/HighlightData.qx | 39 ++++++++ .../local-first-reader/ReaderRuntime.qx | 83 ++++++++++++++++ .../queries/editor-inputs.graphql | 1 + .../queries/editor-view.graphql | 1 + .../queries/highlight-list.graphql | 1 + .../queries/highlight-scope.graphql | 1 + .../queries/reader-screen.graphql | 21 ++++ .../queries/replica-inputs.graphql | 20 ++++ test/fixtures/local-first-reader/workspace.qx | 92 ++++++++++++++++++ test/local-first-reader.test.ts | 97 +++++++++++++++++++ 14 files changed, 469 insertions(+) create mode 100644 test/fixtures/local-first-reader/CommentData.qx create mode 100644 test/fixtures/local-first-reader/DocumentActions.qx create mode 100644 test/fixtures/local-first-reader/DocumentData.qx create mode 100644 test/fixtures/local-first-reader/DocumentFactory.qx create mode 100644 test/fixtures/local-first-reader/HighlightData.qx create mode 100644 test/fixtures/local-first-reader/ReaderRuntime.qx create mode 100644 test/fixtures/local-first-reader/queries/editor-inputs.graphql create mode 100644 test/fixtures/local-first-reader/queries/editor-view.graphql create mode 100644 test/fixtures/local-first-reader/queries/highlight-list.graphql create mode 100644 test/fixtures/local-first-reader/queries/highlight-scope.graphql create mode 100644 test/fixtures/local-first-reader/queries/reader-screen.graphql create mode 100644 test/fixtures/local-first-reader/queries/replica-inputs.graphql create mode 100644 test/fixtures/local-first-reader/workspace.qx create mode 100644 test/local-first-reader.test.ts diff --git a/test/fixtures/local-first-reader/CommentData.qx b/test/fixtures/local-first-reader/CommentData.qx new file mode 100644 index 0000000..8bfebb8 --- /dev/null +++ b/test/fixtures/local-first-reader/CommentData.qx @@ -0,0 +1,27 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +interface CommentData id "interface:reader:comment-data" +revision "interface:reader:comment-data@1" { + queryable value content id "comment:content" : CommentContent { + get id "comment:content:get" + execution snapshot targets [browser, server]; + edit id "comment:content:edit" + execution transaction targets [browser, server]; + } +} diff --git a/test/fixtures/local-first-reader/DocumentActions.qx b/test/fixtures/local-first-reader/DocumentActions.qx new file mode 100644 index 0000000..ae18ac1 --- /dev/null +++ b/test/fixtures/local-first-reader/DocumentActions.qx @@ -0,0 +1,25 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +interface DocumentActions id "interface:reader:document-actions" +revision "interface:reader:document-actions@1" { + operation annotate id "document:annotate" : SourceRange -> atom-ref { + call id "document:annotate:call" + execution transaction targets [browser, server]; + } +} diff --git a/test/fixtures/local-first-reader/DocumentData.qx b/test/fixtures/local-first-reader/DocumentData.qx new file mode 100644 index 0000000..7526ba5 --- /dev/null +++ b/test/fixtures/local-first-reader/DocumentData.qx @@ -0,0 +1,36 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +interface DocumentData id "interface:reader:document-data" +revision "interface:reader:document-data@1" { + queryable value sourceRevision id "document:source-revision" : string { + get id "document:source-revision:get" + execution snapshot targets [browser, server]; + } + queryable value content id "document:content" : string { + get id "document:content:get" + execution snapshot targets [browser, server]; + } + queryable relation highlights id "document:highlights" + : many-unique atom Highlight { + resolve id "document:highlights:resolve" + execution snapshot targets [browser, server]; + connect id "document:highlights:connect" + execution transaction targets [browser, server]; + } +} diff --git a/test/fixtures/local-first-reader/DocumentFactory.qx b/test/fixtures/local-first-reader/DocumentFactory.qx new file mode 100644 index 0000000..e8bdbd3 --- /dev/null +++ b/test/fixtures/local-first-reader/DocumentFactory.qx @@ -0,0 +1,25 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +interface DocumentFactory id "interface:reader:document-factory" +revision "interface:reader:document-factory@1" { + static operation create id "document:create" : DocumentSeed -> atom-ref { + call id "document:create:call" + execution transaction targets [browser, server]; + } +} diff --git a/test/fixtures/local-first-reader/HighlightData.qx b/test/fixtures/local-first-reader/HighlightData.qx new file mode 100644 index 0000000..035f501 --- /dev/null +++ b/test/fixtures/local-first-reader/HighlightData.qx @@ -0,0 +1,39 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +interface HighlightData id "interface:reader:highlight-data" +revision "interface:reader:highlight-data@1" { + queryable value range id "highlight:range" : SourceRange { + get id "highlight:range:get" + execution snapshot targets [browser, server]; + } + queryable value color id "highlight:color" : string { + get id "highlight:color:get" + execution snapshot targets [browser, server]; + set id "highlight:color:set" + execution transaction targets [browser, server]; + } + queryable portable value excerpt id "highlight:excerpt" : string { + get id "highlight:excerpt:get" + execution snapshot targets [browser, server]; + } + queryable relation comments id "highlight:comments" : many-unique atom Comment { + resolve id "highlight:comments:resolve" + execution snapshot targets [browser, server]; + } +} diff --git a/test/fixtures/local-first-reader/ReaderRuntime.qx b/test/fixtures/local-first-reader/ReaderRuntime.qx new file mode 100644 index 0000000..629a3ef --- /dev/null +++ b/test/fixtures/local-first-reader/ReaderRuntime.qx @@ -0,0 +1,83 @@ +// Shared record definitions, repeated explicitly in independently locked resources. +external atom Document id "atom:reader:document"; +external atom Highlight id "atom:reader:highlight"; +external atom Comment id "atom:reader:comment"; + +type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; +}; + +type CommentContent = record { + text: string; + tags: list; +}; + +type DocumentSeed = record { sourceRevision: string; content: string; }; + +import interface DocumentData; +import interface HighlightData; +import interface CommentData; +type CommentSeed = record { content: CommentContent; }; +type HighlightSeed = record { + range: SourceRange; + comments: list; }>; +}; + +package ReaderRuntime id "package:reader-runtime" +revision "package:reader-runtime@1" { + operation addHighlight id "reader:add-highlight" + : SourceRange -> atom-ref + mode call execution transaction targets [browser, server] + receiver atom Document requires { + interface document id "reader:add:document" : DocumentData + using [sourceRevision.get, content.get, highlights.connect]; + creation highlight id "reader:add:highlight" : Highlight input HighlightSeed; + creation comment id "reader:add:comment" : Comment input CommentSeed; + }; + + operation excerpt id "reader:excerpt" : unit -> string + mode call execution snapshot targets [browser, server] + receiver atom Highlight requires { + state range id "reader:excerpt:range" : SourceRange [read]; + interface document id "reader:excerpt:document" : DocumentData + using [sourceRevision.get, content.get]; + }; + + // A receiverless function needs no synthetic service object. + function validateRange id "reader:validate-range" + : record { range: SourceRange; content: string; } -> bool + execution snapshot targets [browser, server]; + + function createDocument id "reader:create-document" + : DocumentSeed -> atom-ref + execution transaction targets [browser, server] requires { + creation document id "reader:create:document" : Document input DocumentSeed; + }; + query ReplicaInputs id "reader:replica-inputs" root DocumentData + document "queries/replica-inputs.graphql" operation "ReplicaInputs" { + replica; + view Highlight as HighlightData; + view Comment as CommentData; + max rows 5000; + max depth 8; + max result-bytes 8388608; + } + + query ReaderScreen id "reader:screen" root DocumentData + document "queries/reader-screen.graphql" operation "ReaderScreen" { + local from ReplicaInputs; + view Highlight as HighlightData; + view Comment as CommentData; + max rows 5000; + max depth 8; + max result-bytes 8388608; + watch; + } + + query HighlightScope id "reader:highlight-scope" root workspace HighlightData document "queries/highlight-scope.graphql" operation "HighlightScope" { view Comment as CommentData; replica; max rows 200; } + query HighlightList id "reader:highlight-list" root workspace HighlightData document "queries/highlight-list.graphql" operation "HighlightList" { view Comment as CommentData; local from HighlightScope; max rows 200; watch; } + query HighlightEditorInputs id "reader:editor-inputs" root HighlightData document "queries/editor-inputs.graphql" operation "HighlightEditorInputs" { view Comment as CommentData; replica; max rows 1; } + query HighlightEditorView id "reader:editor-view" root HighlightData document "queries/editor-view.graphql" operation "HighlightEditorView" { view Comment as CommentData; local from HighlightEditorInputs; max rows 1; watch; } +} diff --git a/test/fixtures/local-first-reader/queries/editor-inputs.graphql b/test/fixtures/local-first-reader/queries/editor-inputs.graphql new file mode 100644 index 0000000..b69323d --- /dev/null +++ b/test/fixtures/local-first-reader/queries/editor-inputs.graphql @@ -0,0 +1 @@ +query HighlightEditorInputs { root { _qx {ref} color range {sourceRevision start end} } } diff --git a/test/fixtures/local-first-reader/queries/editor-view.graphql b/test/fixtures/local-first-reader/queries/editor-view.graphql new file mode 100644 index 0000000..97a1cc4 --- /dev/null +++ b/test/fixtures/local-first-reader/queries/editor-view.graphql @@ -0,0 +1 @@ +query HighlightEditorView { root { _qx {ref} color @live range {sourceRevision start end} } } diff --git a/test/fixtures/local-first-reader/queries/highlight-list.graphql b/test/fixtures/local-first-reader/queries/highlight-list.graphql new file mode 100644 index 0000000..13e0094 --- /dev/null +++ b/test/fixtures/local-first-reader/queries/highlight-list.graphql @@ -0,0 +1 @@ +query HighlightList($color: String!) { root { availableObjects(all: 200, where: {color: {eq: $color}}, orderBy: {range: {start: ASC}}) { entries { key node { _qx {ref} color @live range {sourceRevision start end} } } } } } diff --git a/test/fixtures/local-first-reader/queries/highlight-scope.graphql b/test/fixtures/local-first-reader/queries/highlight-scope.graphql new file mode 100644 index 0000000..eb35067 --- /dev/null +++ b/test/fixtures/local-first-reader/queries/highlight-scope.graphql @@ -0,0 +1 @@ +query HighlightScope($color: String!) { root { objects(all: 200, where: {color: {eq: $color}}) { entries { key node { _qx {ref} color range {sourceRevision start end} } } } } } diff --git a/test/fixtures/local-first-reader/queries/reader-screen.graphql b/test/fixtures/local-first-reader/queries/reader-screen.graphql new file mode 100644 index 0000000..1f50d8c --- /dev/null +++ b/test/fixtures/local-first-reader/queries/reader-screen.graphql @@ -0,0 +1,21 @@ +query ReaderScreen($color: String!) { + root { + sourceRevision + content + highlights(all: 1000, where: {color: {eq: $color}}, orderBy: [{range: {start: ASC}}]) { + entries { + key + node { + _qx { ref } + range { sourceRevision start end } + color @live + excerpt + comments(all: 200) { + entries { key node { _qx { ref } content @live { text tags } } } + } + } + } + } + _qx { relations { highlights { aggregate { count } } } } + } +} diff --git a/test/fixtures/local-first-reader/queries/replica-inputs.graphql b/test/fixtures/local-first-reader/queries/replica-inputs.graphql new file mode 100644 index 0000000..b69387b --- /dev/null +++ b/test/fixtures/local-first-reader/queries/replica-inputs.graphql @@ -0,0 +1,20 @@ +query ReplicaInputs { + root { + sourceRevision + content + highlights(all: 1000) { + entries { + key + node { + _qx { ref } + range { sourceRevision start end } + color + excerpt + comments(all: 200) { + entries { key node { _qx { ref } content { text tags } } } + } + } + } + } + } +} diff --git a/test/fixtures/local-first-reader/workspace.qx b/test/fixtures/local-first-reader/workspace.qx new file mode 100644 index 0000000..ad0b81d --- /dev/null +++ b/test/fixtures/local-first-reader/workspace.qx @@ -0,0 +1,92 @@ +workspace Reader id "workspace:reader" revision "workspace:reader@1" commit "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" { + import interface DocumentData; + import interface HighlightData; + import interface CommentData; + import interface DocumentActions; + import interface DocumentFactory; + import package ReaderRuntime; + // Shared record definitions, repeated explicitly in independently locked resources. + + type SourceRange = record { + sourceRevision: string; + start: int32; + end: int32; + }; + + type CommentContent = record { + text: string; + tags: list; + }; + + type DocumentSeed = record { sourceRevision: string; content: string; }; + + atom Document id "atom:reader:document"; + atom Highlight id "atom:reader:highlight"; + atom Comment id "atom:reader:comment"; + + conform Document as DocumentData id "conformance:reader:document-data" { + private state Revision id "slot:document:revision" + on Document : string policy register; + private state Content id "slot:document:content" + on Document : string policy register; + private edge DocumentHighlights id "edge:document-highlights" { + atom Document projection highlights id "projection:highlights" many-unique; + atom Highlight projection document id "projection:document" + exactly-one public-traversal; + } + initialize { + sourceRevision to state Revision; + content to state Content; + }; + bind sourceRevision.get to state Revision.read; + bind content.get to state Content.read; + bind highlights.resolve to edge DocumentHighlights.highlights.resolve; + bind highlights.connect to edge DocumentHighlights.highlights.connect; + } + + conform Document as DocumentActions id "conformance:reader:document-actions" { + bind annotate.call to package ReaderRuntime.addHighlight with { + document to interface DocumentData; + highlight to creation Highlight with { HighlightData from input; }; + comment to creation Comment with { CommentData from input; }; + }; + } + + conform Document as DocumentFactory id "conformance:reader:document-factory" { + bind create.call to package ReaderRuntime.createDocument with { + document to creation Document with { DocumentData from input; }; + }; + } + + conform Highlight as HighlightData id "conformance:reader:highlight-data" { + private state Range id "slot:highlight:range" + on Highlight : SourceRange policy register; + private state Color id "slot:highlight:color" + on Highlight : string policy register default "yellow"; + private edge HighlightComments id "edge:highlight-comments" { + atom Highlight projection comments id "projection:comments" many-unique; + atom Comment projection highlight id "projection:highlight" + exactly-one public-traversal; + } + initialize { + range to state Range; + comments to edge HighlightComments.comments; + }; + bind range.get to state Range.read; + bind color.get to state Color.read; + bind color.set to state Color.write; + bind comments.resolve to edge HighlightComments.comments.resolve; + bind excerpt.get to package ReaderRuntime.excerpt with { + range to state Range; + document to interface DocumentData via edge DocumentHighlights.document; + }; + } + + conform Comment as CommentData id "conformance:reader:comment-data" { + private state CommentBody id "slot:comment:content" + on Comment : CommentContent policy crdt(CommentContent); + initialize { content to state CommentBody; }; + bind content.get to state CommentBody.read; + bind content.edit to state CommentBody.edit; + } +} diff --git a/test/local-first-reader.test.ts b/test/local-first-reader.test.ts new file mode 100644 index 0000000..cc6ffcd --- /dev/null +++ b/test/local-first-reader.test.ts @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { + compileCapabilityResourceSource, + compileCapabilitySource, + formatQx, +} from "../src/capability-language/index.js"; +import type { InterfaceRevision } from "../src/capability-model/types.js"; +import { compileQuery } from "../src/query/compile.js"; +import { linkQueries } from "../src/query/link.js"; +const directory = resolve("test/fixtures/local-first-reader"); +const read = (name: string) => readFile(resolve(directory, name), "utf8"); +const source = { repository: "https://example.test/reader.git", commit: "a".repeat(40) }; +const load = async (transform = (name: string, text: string) => text) => { + const interfaces = new Map(); + for (const name of ["DocumentData", "HighlightData", "CommentData", "DocumentActions", "DocumentFactory"]) { + const text = transform(`${name}.qx`, await read(`${name}.qx`)); + const result = compileCapabilityResourceSource(formatQx(text), { source }); + assert.ok(result.ok && result.resource.kind === "interface", `${name}: ${JSON.stringify(result.diagnostics)}`); + interfaces.set(name, result.resource.revision); + } + const environment = { interfaces, interfaceClosure: [...interfaces.values()] }; + const pkg = compileCapabilityResourceSource(formatQx(transform("ReaderRuntime.qx", await read("ReaderRuntime.qx"))), { + source, + environment, + }); + assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); + const revision = pkg.resource.revision; + revision.checkedQueries = await Promise.all( + revision.queries!.map((query) => compileQuery(query, [...interfaces.values()], read)), + ); + const result = compileCapabilitySource(formatQx(await read("workspace.qx")), "workspace.qx", { + ...environment, + packages: new Map([["ReaderRuntime", revision]]), + packageClosure: [revision], + }); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + return result.workspace; +}; +test("complete reader contracts compose factories, portable selectors, CRDT edits, and six query scopes", async () => { + const workspace = await load(); + assert.equal(workspace.constructors.length, 0); + assert.equal(workspace.atoms.length, 3); + const linked = linkQueries(workspace); + assert.equal(linked.length, 6); + assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 3); + const screen = linked.find((q) => q.checked.declaration.displayName === "ReaderScreen")!; + assert.ok(screen.fields.some((f) => f.memberId === "highlight:excerpt" && f.binding.kind === "package")); + assert.ok(screen.fields.some((f) => f.memberId === "comment:content" && f.binding.kind === "state")); + assert.equal( + linked.find((q) => q.checked.declaration.displayName === "HighlightEditorInputs")!.checked.declaration.root.kind, + "object", + ); + assert.equal( + linked.find((q) => q.checked.declaration.displayName === "HighlightScope")!.checked.declaration.root.kind, + "workspace-interface", + ); +}); + +test("reader composition reports the dependency that weakens a portable promise", async () => { + await assert.rejects( + load((name, text) => + name === "ReaderRuntime.qx" + ? text.replace(/(operation excerpt[\s\S]*?targets )\[browser, server\]/, "$1[server]") + : text, + ), + /does not provide execution target browser/, + ); + await assert.rejects( + load((name, text) => + name === "ReaderRuntime.qx" + ? text.replace( + "using [sourceRevision.get, content.get];", + "using [sourceRevision.get, content.get, highlights.connect];", + ) + : text, + ), + /Snapshot execution cannot perform a transaction/, + ); +}); + +test("a field can remain server-queryable while failing local query placement", async () => { + const workspace = await load(); + const pkg = workspace.packageImports[0]!; + const highlight = workspace.interfaceImports.find((i) => i.displayName === "HighlightData")!; + const field = highlight.members.find((m) => m.displayName === "color")!; + delete field.operations[0]!.execution; + const admission = pkg.queries!.find((q) => q.displayName === "HighlightScope")!; + const local = pkg.queries!.find((q) => q.displayName === "HighlightList")!; + await compileQuery(admission, workspace.interfaceImports, read); + await assert.rejects( + compileQuery(local, workspace.interfaceImports, read), + /browser snapshot getter for HighlightData.color/, + ); +});