diff --git a/src/execution-world/client-contracts.ts b/src/execution-world/client-contracts.ts index d96161f..5f7cae6 100644 --- a/src/execution-world/client-contracts.ts +++ b/src/execution-world/client-contracts.ts @@ -213,17 +213,28 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi `ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`; // Preserve closed type arguments for the artifact consumer. Interpretation of // a particular module contract belongs to that consumer, not this compiler. - const moduleArguments = (iface: InterfaceRevision) => `[${(iface.application?.arguments ?? []).map(argument => { - if (argument.kind === "value") return tsType(argument.type); - const target = argument.target; - const reference = target.kind === "atom" ? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>` : `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`; - const fields = target.kind === "interface" ? find(target.interfaceRevisionId).members.flatMap(member => { - if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return []; - const getter = member.operations.find(operation => operation.displayName === (member.kind === "value" ? "get" : "resolve")); - return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : []; - }) : []; - return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`; - }).join(",")}]`; + const moduleArguments = (iface: InterfaceRevision) => + `[${(iface.application?.arguments ?? []) + .map((argument) => { + if (argument.kind === "value") return tsType(argument.type); + const target = argument.target; + const reference = + target.kind === "atom" + ? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>` + : `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`; + const fields = + target.kind === "interface" + ? find(target.interfaceRevisionId).members.flatMap((member) => { + if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return []; + const getter = member.operations.find( + (operation) => operation.displayName === (member.kind === "value" ? "get" : "resolve"), + ); + return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : []; + }) + : []; + return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`; + }) + .join(",")}]`; const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props); const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props) .split("\n") diff --git a/src/execution-world/inspection.ts b/src/execution-world/inspection.ts index e75e84d..88a28b2 100644 --- a/src/execution-world/inspection.ts +++ b/src/execution-world/inspection.ts @@ -18,7 +18,15 @@ const meta = () => selection("_camino", { selection: [selection("ref")] }); export const inspectionPackage = "org.quixos.inspection/1"; export interface InspectionTemplate { interface: string; - members: { id: string; name: string; key: string; flag: string; lane: "local" | "server"; many: boolean; relationship: boolean }[]; + members: { + id: string; + name: string; + key: string; + flag: string; + lane: "local" | "server"; + many: boolean; + relationship: boolean; + }[]; inputs?: PublicQueryContract; view?: PublicQueryContract; server?: PublicQueryContract; @@ -81,7 +89,15 @@ export function compileInspectionTemplates(plan: ExecutionPlan) { // Only native edges promise the relationship traversal used by the interpreter. if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue; if (!complete && (lane === "local" || !portable)) - template.members.push({ id: member.id, name: member.displayName, key, flag, lane, many, relationship: member.kind === "relationship" }); + template.members.push({ + id: member.id, + name: member.displayName, + key, + flag, + lane, + many, + relationship: member.kind === "relationship", + }); inputFields[flag] = scalar("bool"); const field = selection(member.displayName, { key, diff --git a/src/execution-world/kernel.ts b/src/execution-world/kernel.ts index 52f3082..5e12355 100644 --- a/src/execution-world/kernel.ts +++ b/src/execution-world/kernel.ts @@ -22,11 +22,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { if (hexDigest(plan.definition) !== plan.digest) throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch"); const workspace = plan.definition.workspace; - if (workspace.sharedAttachments.length) + if (workspace.sharedAttachments.some((attachment) => attachment.kind !== "edge")) throw new ExecutionPlanError( "UNSUPPORTED_SHARED_STORAGE", "kernel", - "Stage 0 requires explicit conformance-owned storage", + "Shared scalar storage is not implemented; use conformance-owned state", ); const schema = (type: ValueType): unknown | null => { try { @@ -36,7 +36,30 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { } }; const ownerBySlot = new Map(); - const edges: { id: string; owner: string; a: unknown; b: unknown }[] = []; + const edges: { id: string; owner: string | null; a: unknown; b: unknown }[] = []; + const lowerEdge = (storage: Extract, owner: string | null) => { + const endpoints = storage.endpoints.map((endpoint) => { + if (endpoint.ordered || (endpoint.keyType && endpoint.keyType !== "string") || endpoint.retainOther) + throw new ExecutionPlanError( + "UNSUPPORTED_GRAPH_FEATURE", + storage.id, + "Ordered relations, non-string keys and automatic retention are not implemented", + ); + return { + projection: endpoint.projectionId, + constraint: { + kind: endpoint.constraint.kind, + id: + endpoint.constraint.kind === "atom" ? endpoint.constraint.atomId : endpoint.constraint.interfaceRevisionId, + }, + cardinality: endpoint.cardinality, + ...(endpoint.keyType ? { keyType: endpoint.keyType } : {}), + ...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}), + }; + }); + edges.push({ id: storage.id, owner, a: endpoints[0], b: endpoints[1] }); + }; + for (const storage of workspace.sharedAttachments) if (storage.kind === "edge") lowerEdge(storage, null); const owners = plan.definition.owners.map((owner) => { const slots: unknown[] = []; for (const storage of owner.storage as PersistentAttachment[]) { @@ -51,30 +74,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { policy: storage.storagePolicy.kind === "register" ? "register" : "crdt", ...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}), }); - } else { - const endpoints = storage.endpoints.map((endpoint) => { - if (endpoint.ordered || (endpoint.keyType && endpoint.keyType !== "string") || endpoint.retainOther) - throw new ExecutionPlanError( - "UNSUPPORTED_GRAPH_FEATURE", - storage.id, - "Ordered relations, non-string keys and automatic retention are not implemented", - ); - return { - projection: endpoint.projectionId, - constraint: { - kind: endpoint.constraint.kind, - id: - endpoint.constraint.kind === "atom" - ? endpoint.constraint.atomId - : endpoint.constraint.interfaceRevisionId, - }, - cardinality: endpoint.cardinality, - ...(endpoint.keyType ? { keyType: endpoint.keyType } : {}), - ...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}), - }; - }); - edges.push({ id: storage.id, owner: owner.id, a: endpoints[0], b: endpoints[1] }); - } + } else lowerEdge(storage, owner.id); } return { id: owner.id, diff --git a/src/execution-world/plan.ts b/src/execution-world/plan.ts index 0e155d3..9f23f7e 100644 --- a/src/execution-world/plan.ts +++ b/src/execution-world/plan.ts @@ -82,7 +82,9 @@ function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevis ...(pkg.modules ?? []).flatMap((module) => { const contract = moduleContract(workspace.interfaceImports, module)!; if ( - !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(contract.contract) || + !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes( + contract.contract, + ) || contract.subject?.kind !== "interface" ) return []; diff --git a/test/keyed-relationships.test.ts b/test/keyed-relationships.test.ts index 6827552..079bf67 100644 --- a/test/keyed-relationships.test.ts +++ b/test/keyed-relationships.test.ts @@ -76,3 +76,35 @@ test("key contracts cannot silently bind to unkeyed storage or the inverse proje assert.match(JSON.stringify(result.diagnostics), /key type|keyed projection|signature/i); } }); + +test("checked shared edges lower without granting conformance initialization ownership", () => { + const edge = declarations.match(/ private edge Entries[\s\S]*?\n }/)![0]; + const shared = declarations + .replace(edge, "") + .replace(" conform Node as", edge.replace("private edge", "shared edge") + "\n conform Node as"); + const result = compile(shared); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + const program = compileKernelProgram(compileExecutionPlan(result.workspace)) as { + model: { edges: { id: string; owner: string | null }[] }; + }; + assert.deepEqual( + program.model.edges.map((edge) => ({ id: edge.id, owner: edge.owner })), + [{ id: "entries", owner: null }], + ); + const seeded = compile( + shared.replace( + " bind entries.resolve", + " initialize { initial to edge Entries.entries; };\n bind entries.resolve", + ), + ); + assert.ok(!seeded.ok); + assert.ok(seeded.diagnostics.some((issue) => issue.code === "invalid-initialization")); + const scalar = compile( + shared.replace( + ' atom Node id "node";', + ' atom Node id "node"; shared state Shared id "shared" on Node : string policy register default "";', + ), + ); + assert.ok(scalar.ok, JSON.stringify(scalar.diagnostics)); + assert.throws(() => compileKernelProgram(compileExecutionPlan(scalar.workspace)), /UNSUPPORTED_SHARED_STORAGE/); +}); diff --git a/test/presentations.test.ts b/test/presentations.test.ts index 19b26ec..ab69e70 100644 --- a/test/presentations.test.ts +++ b/test/presentations.test.ts @@ -148,7 +148,9 @@ test("standalone presentation rejects a general component contract that may requ assert.equal(member.kind, "module"); if (member.kind !== "module") throw Error("Expected module"); member.contract = "org.quixos.react.component/1"; - assert.ok(validateWorkspaceRevision(candidate).some((issue) => /subject-only/.test(issue.message))); + assert.ok( + validateWorkspaceRevision(candidate).some((issue) => /supported React presentation contract/.test(issue.message)), + ); }); test("checked component-only artifacts install without a service and reject missing or substituted entries", async () => {