From a83239892c7000421c8f1c7c15eae1e2129c91de Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Fri, 25 Sep 2026 11:04:23 -0700 Subject: [PATCH] Prove PresentationMount with authored generic graph operations --- src/capability-language/parser.ts | 4 + test/conformance-rules.test.ts | 26 ++++++ test/emit-portable-reader.ts | 15 ++++ .../presentation-mount/DocumentNavigation.qx | 7 ++ .../presentation-mount/Presentation.qx | 2 + .../presentation-mount/PresentationMount.qx | 15 ++++ .../presentation-mount/PresentationRuntime.qx | 23 ++++++ .../presentation-mount/PresentationState.qx | 12 +++ .../presentation-mount/PresentationStates.qx | 10 +++ .../presentation-mount/ReaderNavigation.qx | 7 ++ .../presentation-mount/StateFactory.qx | 6 ++ .../presentation-mount/StateRecord.qx | 1 + test/fixtures/presentation-mount/workspace.qx | 81 +++++++++++++++++++ test/presentation-mount-fixture.ts | 35 ++++++++ 14 files changed, 244 insertions(+) create mode 100644 test/fixtures/presentation-mount/DocumentNavigation.qx create mode 100644 test/fixtures/presentation-mount/Presentation.qx create mode 100644 test/fixtures/presentation-mount/PresentationMount.qx create mode 100644 test/fixtures/presentation-mount/PresentationRuntime.qx create mode 100644 test/fixtures/presentation-mount/PresentationState.qx create mode 100644 test/fixtures/presentation-mount/PresentationStates.qx create mode 100644 test/fixtures/presentation-mount/ReaderNavigation.qx create mode 100644 test/fixtures/presentation-mount/StateFactory.qx create mode 100644 test/fixtures/presentation-mount/StateRecord.qx create mode 100644 test/fixtures/presentation-mount/workspace.qx create mode 100644 test/presentation-mount-fixture.ts diff --git a/src/capability-language/parser.ts b/src/capability-language/parser.ts index 102b950..dd8a39b 100644 --- a/src/capability-language/parser.ts +++ b/src/capability-language/parser.ts @@ -2182,6 +2182,10 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont } else if (provider.STATE() || provider.EDGE()) { const name = identifier(provider.identifier(0)); const expected = provider.STATE() ? "state" : "edge"; + // A rule may traverse another owner's public inverse projection, just as + // a concrete conformance may. Resolve external edges after specialization; + // ordinary ownership validation still permits only public resolve access. + if (provider.EDGE() && !privateStorage.has(name)) continue; if (privateStorage.get(name) !== expected) loweringIssue( state, diff --git a/test/conformance-rules.test.ts b/test/conformance-rules.test.ts index dfda342..b274720 100644 --- a/test/conformance-rules.test.ts +++ b/test/conformance-rules.test.ts @@ -319,3 +319,29 @@ test("chains do not duplicate an application and explicit targets conflict", () assert.match(JSON.stringify(conflict.diagnostics), /explicit-c/); assert.match(JSON.stringify(conflict.diagnostics), /rule bc/); }); + +test("rule inverse reads retain the concrete conformance ownership checks", () => { + const compile = fixture([ + 'interface Parent id "parent" revision "parent@1" {}', + 'interface Child id "child" revision "child@1" {}', + 'import interface Parent; interface Owned id "owned" revision "owned@1" {queryable relation parent id "parent-ref" : optional-one interface Parent {resolve id "parent-read" execution snapshot targets [browser,server];}}', + ]); + const source = ` + atom P id "p"; atom C id "c"; + conform P as Parent id "parent-impl" { + private edge Membership id "membership" { + atom P projection children id "children" many-unique; + interface Child projection parent id "parent" optional-one public-traversal; + } + } + conform C as Child id "child-impl" {} + conform Child as Owned id "owned-rule" {bind parent.resolve to edge Membership.parent.resolve;} + `; + ok(compile(source)); + const hidden = compile(source.replace(" public-traversal", "")); + assert.equal(hidden.ok, false); + assert.match(JSON.stringify(hidden.diagnostics), /private|public|own/i); + const missing = compile(source.replace("Membership.parent.resolve", "Missing.parent.resolve")); + assert.equal(missing.ok, false); + assert.match(JSON.stringify(missing.diagnostics), /Unknown attachment/); +}); diff --git a/test/emit-portable-reader.ts b/test/emit-portable-reader.ts index 5b99aec..92d90d9 100644 --- a/test/emit-portable-reader.ts +++ b/test/emit-portable-reader.ts @@ -126,6 +126,21 @@ await mkdir("dist/portable-reader/collections", { recursive: true }); await writeFile("dist/portable-reader/collections/plan.json", JSON.stringify(collections)); await writeFile("dist/portable-reader/collections/kernel.json", JSON.stringify(compileKernelProgram(collections))); +const { presentationMountPlan } = await import("./presentation-mount-fixture.js"); +const presentationMount = await presentationMountPlan(); +await mkdir("dist/portable-reader/presentation-mount", { recursive: true }); +await writeFile("dist/portable-reader/presentation-mount/plan.json", JSON.stringify(presentationMount)); +await writeFile( + "dist/portable-reader/presentation-mount/kernel.json", + JSON.stringify(compileKernelProgram(presentationMount)), +); +await writeFile( + "dist/portable-reader/presentation-mount/browser.ts", + generatePortableTypeScriptBindings(presentationMount.definition.bindingSchema, "presentation-runtime@1", { + target: "browser", + }), +); + const { compoundCrdtPlan } = await import("./compound-crdt-fixture.js"); const compoundCrdt = await compoundCrdtPlan(); await mkdir("dist/portable-reader/compound-crdt", { recursive: true }); diff --git a/test/fixtures/presentation-mount/DocumentNavigation.qx b/test/fixtures/presentation-mount/DocumentNavigation.qx new file mode 100644 index 0000000..a0e7da7 --- /dev/null +++ b/test/fixtures/presentation-mount/DocumentNavigation.qx @@ -0,0 +1,7 @@ +import interface PresentationState; +interface DocumentNavigation id "document-navigation" revision "document-navigation@1" requires PresentationState { + queryable value compact id "document-navigation:compact" : bool { + get id "document-navigation:compact:get" execution snapshot targets [browser,server]; + set id "document-navigation:compact:set" execution transaction targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/Presentation.qx b/test/fixtures/presentation-mount/Presentation.qx new file mode 100644 index 0000000..7bae9c6 --- /dev/null +++ b/test/fixtures/presentation-mount/Presentation.qx @@ -0,0 +1,2 @@ +import interface PresentationState; +interface Presentation id "presentation" revision "presentation@1" {} diff --git a/test/fixtures/presentation-mount/PresentationMount.qx b/test/fixtures/presentation-mount/PresentationMount.qx new file mode 100644 index 0000000..a74a683 --- /dev/null +++ b/test/fixtures/presentation-mount/PresentationMount.qx @@ -0,0 +1,15 @@ +import interface PresentationState; +interface PresentationMount id "presentation-mount" revision "presentation-mount@1" { + static operation create id "presentation-mount:create" : unit -> interface-ref { + call id "presentation-mount:create:call" execution transaction targets [browser,server]; + } + queryable relation members id "presentation-mount:members" : many-unique interface PresentationState { + resolve id "presentation-mount:members:resolve" execution snapshot targets [browser,server]; + } + queryable relation root id "presentation-mount:root" : optional-one interface PresentationState { + resolve id "presentation-mount:root:resolve" execution snapshot targets [browser,server]; + } + operation attach id "presentation-mount:attach" : interface-ref -> unit { + call id "presentation-mount:attach:call" execution transaction targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/PresentationRuntime.qx b/test/fixtures/presentation-mount/PresentationRuntime.qx new file mode 100644 index 0000000..d7f9788 --- /dev/null +++ b/test/fixtures/presentation-mount/PresentationRuntime.qx @@ -0,0 +1,23 @@ +import interface PresentationState; +import interface PresentationMount; +import interface StateFactory; +external atom PresentationMountRecord id "presentation-mount-record"; +package PresentationRuntime id "presentation-runtime" revision "presentation-runtime@1" { + function createPresentationMount id "presentation-runtime:create" : unit -> interface-ref execution transaction targets [browser,server] requires { + creation presentationMount id "presentationMount" : PresentationMountRecord input unit; + }; + operation attach id "presentation-runtime:attach" : interface-ref -> unit mode call execution transaction targets [browser,server] receiver any requires { + edge members id "members" : many-unique interface PresentationState [resolve,connect]; + edge root id "root" : optional-one interface PresentationState [resolve,connect]; + interface metadata id "state" : PresentationState using [presentationMount.resolve,parent.resolve] from input; + }; + operation ensure id "presentation-runtime:ensure" : record {presentationMount:interface-ref;occurrence:string;parent:optional>;} -> ref mode call execution transaction targets [browser,server] receiver any requires { + edge states id "states" : many-unique object Navigation [resolve,connect]; + interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve] from input; + interface presentationMount id "presentationMount" : PresentationMount using [attach.call] from input; + interface factory id "factory" : StateFactory using [create.call]; + }; + function createState id "presentation-runtime:create-state" : record {occurrence:string;parent:optional>;} -> ref execution transaction targets [browser,server] requires { + creation navigation id "navigation" : Navigation input record {occurrence:string;parent:optional>;}; + }; +} diff --git a/test/fixtures/presentation-mount/PresentationState.qx b/test/fixtures/presentation-mount/PresentationState.qx new file mode 100644 index 0000000..ac2f30f --- /dev/null +++ b/test/fixtures/presentation-mount/PresentationState.qx @@ -0,0 +1,12 @@ +external interface PresentationMount revision "presentation-mount@1"; +interface PresentationState id "presentation-state" revision "presentation-state@1" { + queryable value occurrence id "state:occurrence" : string { + get id "state:occurrence:get" execution snapshot targets [browser,server]; + } + queryable relation presentationMount id "state:presentation-mount" : optional-one interface PresentationMount { + resolve id "state:presentation-mount:resolve" execution snapshot targets [browser,server]; + } + queryable relation parent id "state:parent" : optional-one interface PresentationState { + resolve id "state:parent:resolve" execution snapshot targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/PresentationStates.qx b/test/fixtures/presentation-mount/PresentationStates.qx new file mode 100644 index 0000000..55eac33 --- /dev/null +++ b/test/fixtures/presentation-mount/PresentationStates.qx @@ -0,0 +1,10 @@ +import interface PresentationState; +import interface PresentationMount; +interface PresentationStates id "presentation-states" revision "presentation-states@1" { + queryable relation states id "presentation-states:states" : many-unique object Navigation { + resolve id "presentation-states:states:resolve" execution snapshot targets [browser,server]; + } + operation ensure id "presentation-states:ensure" : record {presentationMount:interface-ref;occurrence:string;parent:optional>;} -> ref { + call id "presentation-states:ensure:call" execution transaction targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/ReaderNavigation.qx b/test/fixtures/presentation-mount/ReaderNavigation.qx new file mode 100644 index 0000000..e255880 --- /dev/null +++ b/test/fixtures/presentation-mount/ReaderNavigation.qx @@ -0,0 +1,7 @@ +import interface PresentationState; +interface ReaderNavigation id "reader-navigation" revision "reader-navigation@1" requires PresentationState { + queryable value page id "reader-navigation:mode" : string { + get id "reader-navigation:mode:get" execution snapshot targets [browser,server]; + set id "reader-navigation:mode:set" execution transaction targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/StateFactory.qx b/test/fixtures/presentation-mount/StateFactory.qx new file mode 100644 index 0000000..c764532 --- /dev/null +++ b/test/fixtures/presentation-mount/StateFactory.qx @@ -0,0 +1,6 @@ +import interface PresentationState; +interface StateFactory id "state-factory" revision "state-factory@1" { + static operation create id "state-factory:create" : record {occurrence:string;parent:optional>;} -> ref { + call id "state-factory:create:call" execution transaction targets [browser,server]; + } +} diff --git a/test/fixtures/presentation-mount/StateRecord.qx b/test/fixtures/presentation-mount/StateRecord.qx new file mode 100644 index 0000000..3088af6 --- /dev/null +++ b/test/fixtures/presentation-mount/StateRecord.qx @@ -0,0 +1 @@ +interface StateRecord id "state-record" revision "state-record@1" {} diff --git a/test/fixtures/presentation-mount/workspace.qx b/test/fixtures/presentation-mount/workspace.qx new file mode 100644 index 0000000..debd2a9 --- /dev/null +++ b/test/fixtures/presentation-mount/workspace.qx @@ -0,0 +1,81 @@ +workspace Example id "presentation-example" revision "presentation-example@1" commit "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" { + import interface PresentationState; + import interface StateRecord; + import interface PresentationMount; + import interface Presentation; + import interface PresentationStates; + import interface StateFactory; + import interface ReaderNavigation; + import interface DocumentNavigation; + import package PresentationRuntime; + atom PresentationMountRecord id "presentation-mount-record"; + atom Library id "library"; + atom Document id "document"; + atom ReaderNavigationRecord id "reader-navigation-record"; + atom DocumentNavigationRecord id "document-navigation-record"; + + conform PresentationMountRecord as PresentationMount id "presentation-mount-record:implementation" { + private edge Membership id "presentation-mount:membership" { + atom PresentationMountRecord projection members id "presentation-mount:members-projection" many-unique public-traversal; + interface PresentationState projection presentationMount id "presentation-mount:owner-projection" optional-one public-traversal; + } + private edge Root id "presentation-mount:root-edge" { + atom PresentationMountRecord projection root id "presentation-mount:root-projection" optional-one; + interface PresentationState projection rootOf id "presentation-mount:root-of-projection" optional-one; + } + bind members.resolve to edge Membership.members.resolve; + bind root.resolve to edge Root.root.resolve; + bind create.call to package PresentationRuntime.createPresentationMount with {presentationMount to creation PresentationMountRecord with {};}; + bind attach.call to package PresentationRuntime.attach with { + members to edge Membership.members; + root to edge Root.root; + metadata to interface PresentationState from input; + }; + } + conform Presentation as PresentationStates id "presentation:state-association" { + private edge States id "presentation:states-edge" { + atom Self projection states id "presentation:states-projection" many-unique; + object Navigation projection subject id "presentation:subject-projection" exactly-one; + } + bind states.resolve to edge States.states.resolve; + bind ensure.call to package PresentationRuntime.ensure with { + states to edge States.states; + metadata to interface PresentationState from input; + presentationMount to interface PresentationMount from input; + factory to interface StateFactory; + }; + } + conform StateRecord as PresentationState id "state-record:metadata" { + private state Occurrence id "metadata:occurrence" on Self : string policy register; + private edge Parent id "metadata:parent" { + atom Self projection parent id "metadata:parent-projection" optional-one; + interface PresentationState projection children id "metadata:children-projection" many-unique; + } + initialize {occurrence to state Occurrence;parent to edge Parent.parent;}; + bind occurrence.get to state Occurrence.read; + bind parent.resolve to edge Parent.parent.resolve; + bind presentationMount.resolve to edge Membership.presentationMount.resolve; + } + conform ReaderNavigationRecord as StateRecord id "reader:state-record" {} + conform DocumentNavigationRecord as StateRecord id "document:state-record" {} + conform ReaderNavigationRecord as ReaderNavigation id "reader:navigation" { + private state Mode id "reader:mode" on Self : string policy register default "library"; + bind page to state Mode; + } + conform DocumentNavigationRecord as DocumentNavigation id "document:navigation" { + private state Compact id "document:compact" on Self : bool policy register default false; + bind compact to state Compact; + } + conform Library as StateFactory id "reader:factory" { + bind create.call to package PresentationRuntime.createState with { + navigation to creation ReaderNavigationRecord with {PresentationState from input;}; + }; + } + conform Document as StateFactory id "document:factory" { + bind create.call to package PresentationRuntime.createState with { + navigation to creation DocumentNavigationRecord with {PresentationState from input;}; + }; + } + conform Library as Presentation id "library:presentation" {} + conform Document as Presentation id "document:presentation" {} +} diff --git a/test/presentation-mount-fixture.ts b/test/presentation-mount-fixture.ts new file mode 100644 index 0000000..16a5c06 --- /dev/null +++ b/test/presentation-mount-fixture.ts @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js"; +import { compileExecutionPlan } from "../src/execution-world/plan.js"; +import type { InterfaceRevision } from "../src/capability-model/types.js"; +export async function presentationMountPlan() { + const read = (name: string) => readFile(`test/fixtures/presentation-mount/${name}.qx`, "utf8"); + const source = { repository: "https://example.test/presentation-mount.git", commit: "a".repeat(40) }; + const interfaces = new Map(); + for (const name of [ + "StateRecord", + "PresentationState", + "PresentationMount", + "StateFactory", + "Presentation", + "PresentationStates", + "ReaderNavigation", + "DocumentNavigation", + ]) { + const result = compileCapabilityResourceSource(await read(name), { source, environment: { interfaces } }); + assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics)); + interfaces.set(name, result.resource.revision); + } + const pkg = compileCapabilityResourceSource(await read("PresentationRuntime"), { + source, + environment: { interfaces }, + }); + assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); + const result = compileCapabilitySource(await read("workspace"), "workspace.qx", { + interfaces, + packages: new Map([["PresentationRuntime", pkg.resource.revision]]), + }); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + return compileExecutionPlan(result.workspace); +}