Lower checked shared edges into the portable graph model
This commit is contained in:
@@ -213,17 +213,28 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
|||||||
`ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`;
|
`ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`;
|
||||||
// Preserve closed type arguments for the artifact consumer. Interpretation of
|
// Preserve closed type arguments for the artifact consumer. Interpretation of
|
||||||
// a particular module contract belongs to that consumer, not this compiler.
|
// a particular module contract belongs to that consumer, not this compiler.
|
||||||
const moduleArguments = (iface: InterfaceRevision) => `[${(iface.application?.arguments ?? []).map(argument => {
|
const moduleArguments = (iface: InterfaceRevision) =>
|
||||||
if (argument.kind === "value") return tsType(argument.type);
|
`[${(iface.application?.arguments ?? [])
|
||||||
const target = argument.target;
|
.map((argument) => {
|
||||||
const reference = target.kind === "atom" ? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>` : `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`;
|
if (argument.kind === "value") return tsType(argument.type);
|
||||||
const fields = target.kind === "interface" ? find(target.interfaceRevisionId).members.flatMap(member => {
|
const target = argument.target;
|
||||||
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
|
const reference =
|
||||||
const getter = member.operations.find(operation => operation.displayName === (member.kind === "value" ? "get" : "resolve"));
|
target.kind === "atom"
|
||||||
return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : [];
|
? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>`
|
||||||
}) : [];
|
: `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`;
|
||||||
return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`;
|
const fields =
|
||||||
}).join(",")}]`;
|
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 interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props);
|
||||||
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props)
|
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
|
|||||||
@@ -18,7 +18,15 @@ const meta = () => selection("_camino", { selection: [selection("ref")] });
|
|||||||
export const inspectionPackage = "org.quixos.inspection/1";
|
export const inspectionPackage = "org.quixos.inspection/1";
|
||||||
export interface InspectionTemplate {
|
export interface InspectionTemplate {
|
||||||
interface: string;
|
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;
|
inputs?: PublicQueryContract;
|
||||||
view?: PublicQueryContract;
|
view?: PublicQueryContract;
|
||||||
server?: PublicQueryContract;
|
server?: PublicQueryContract;
|
||||||
@@ -81,7 +89,15 @@ export function compileInspectionTemplates(plan: ExecutionPlan) {
|
|||||||
// Only native edges promise the relationship traversal used by the interpreter.
|
// Only native edges promise the relationship traversal used by the interpreter.
|
||||||
if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue;
|
if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue;
|
||||||
if (!complete && (lane === "local" || !portable))
|
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");
|
inputFields[flag] = scalar("bool");
|
||||||
const field = selection(member.displayName, {
|
const field = selection(member.displayName, {
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (hexDigest(plan.definition) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch");
|
throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch");
|
||||||
const workspace = plan.definition.workspace;
|
const workspace = plan.definition.workspace;
|
||||||
if (workspace.sharedAttachments.length)
|
if (workspace.sharedAttachments.some((attachment) => attachment.kind !== "edge"))
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"UNSUPPORTED_SHARED_STORAGE",
|
"UNSUPPORTED_SHARED_STORAGE",
|
||||||
"kernel",
|
"kernel",
|
||||||
"Stage 0 requires explicit conformance-owned storage",
|
"Shared scalar storage is not implemented; use conformance-owned state",
|
||||||
);
|
);
|
||||||
const schema = (type: ValueType): unknown | null => {
|
const schema = (type: ValueType): unknown | null => {
|
||||||
try {
|
try {
|
||||||
@@ -36,7 +36,30 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const ownerBySlot = new Map<string, string>();
|
const ownerBySlot = new Map<string, string>();
|
||||||
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<PersistentAttachment, { kind: "edge" }>, 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 owners = plan.definition.owners.map((owner) => {
|
||||||
const slots: unknown[] = [];
|
const slots: unknown[] = [];
|
||||||
for (const storage of owner.storage as PersistentAttachment[]) {
|
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",
|
policy: storage.storagePolicy.kind === "register" ? "register" : "crdt",
|
||||||
...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}),
|
...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}),
|
||||||
});
|
});
|
||||||
} else {
|
} else lowerEdge(storage, owner.id);
|
||||||
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] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: owner.id,
|
id: owner.id,
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevis
|
|||||||
...(pkg.modules ?? []).flatMap((module) => {
|
...(pkg.modules ?? []).flatMap((module) => {
|
||||||
const contract = moduleContract(workspace.interfaceImports, module)!;
|
const contract = moduleContract(workspace.interfaceImports, module)!;
|
||||||
if (
|
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"
|
contract.subject?.kind !== "interface"
|
||||||
)
|
)
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -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);
|
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/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ test("standalone presentation rejects a general component contract that may requ
|
|||||||
assert.equal(member.kind, "module");
|
assert.equal(member.kind, "module");
|
||||||
if (member.kind !== "module") throw Error("Expected module");
|
if (member.kind !== "module") throw Error("Expected module");
|
||||||
member.contract = "org.quixos.react.component/1";
|
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 () => {
|
test("checked component-only artifacts install without a service and reject missing or substituted entries", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user