Add explicit CRDT edit contracts and structured query values
This commit is contained in:
@@ -154,6 +154,8 @@ const typeLabel = (type: ValueType): string => {
|
||||
return type.expectation.kind === "atom"
|
||||
? `object:atom:${type.expectation.atomId}`
|
||||
: `object:interface:${type.expectation.interfaceRevisionId}`;
|
||||
case "change":
|
||||
return `change<${typeLabel(type.value)}>`;
|
||||
case "optional":
|
||||
return `optional<${typeLabel(type.value)}>`;
|
||||
case "list":
|
||||
@@ -189,6 +191,7 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
|
||||
? left.expectation.atomId === (other as typeof left.expectation).atomId
|
||||
: left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId;
|
||||
}
|
||||
case "change":
|
||||
case "optional":
|
||||
case "list":
|
||||
return valueTypesEqual(left.value, (right as typeof left).value);
|
||||
@@ -239,6 +242,7 @@ const validateValueType = (
|
||||
case "message":
|
||||
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
|
||||
return;
|
||||
case "change":
|
||||
case "optional":
|
||||
case "list":
|
||||
validateValueType(issues, type.value, `${path}.value`, indexes);
|
||||
@@ -341,6 +345,8 @@ export const statePrimitiveSignature = (slot: StateSlotDefinition, primitive: St
|
||||
inputType: valueType.unit,
|
||||
outputType: slot.valueType,
|
||||
};
|
||||
case "edit":
|
||||
return { mode: "call", inputType: { kind: "change", value: slot.valueType }, outputType: valueType.unit };
|
||||
case "write":
|
||||
return {
|
||||
mode: "call",
|
||||
@@ -532,9 +538,19 @@ const collectIdentityIndexes = (
|
||||
if (member.kind === "relationship" && contract === "rpc-permitted")
|
||||
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
|
||||
if (member.kind === "value") {
|
||||
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
||||
if (type.kind !== "scalar")
|
||||
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar");
|
||||
const queryValue = (type: ValueType): boolean =>
|
||||
type.kind === "scalar" ||
|
||||
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
|
||||
(type.kind === "record" &&
|
||||
Object.keys(type.fields).length > 0 &&
|
||||
Object.values(type.fields).every(queryValue));
|
||||
if (!queryValue(member.valueType))
|
||||
issue(
|
||||
issues,
|
||||
"invalid-query-contract",
|
||||
memberPath,
|
||||
"Queryable values require scalar leaves in optional, list, or nonempty record shapes",
|
||||
);
|
||||
}
|
||||
}
|
||||
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
|
||||
@@ -837,7 +853,8 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
|
||||
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
|
||||
const containsRpcType = (type: ValueType): boolean =>
|
||||
type.kind === "object-ref" ||
|
||||
type.kind === "record" ||
|
||||
type.kind === "change" ||
|
||||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
|
||||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
|
||||
if (
|
||||
containsRpcType(attachment.valueType) ||
|
||||
@@ -847,7 +864,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
|
||||
issues,
|
||||
"invalid-attachment",
|
||||
`${path}.valueType`,
|
||||
"Managed object references belong in graph relationships, not ordinary state; record types are RPC-only",
|
||||
"Managed object references belong in graph relationships; captured changes are transport-only",
|
||||
);
|
||||
}
|
||||
if (attachment.storagePolicy.kind === "crdt-document") {
|
||||
@@ -1287,6 +1304,12 @@ const validateBoundDependencies = (
|
||||
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
requirement.primitives.includes("edit") &&
|
||||
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
|
||||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
|
||||
)
|
||||
issue(issues, "invalid-dependency-binding", path, "Edit ports require a matching CRDT slot");
|
||||
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
|
||||
issue(
|
||||
issues,
|
||||
@@ -1579,6 +1602,17 @@ const validateConformances = (
|
||||
}
|
||||
} else binding = entry.binding;
|
||||
const member = operationEntry.member;
|
||||
if (
|
||||
member.kind === "value" &&
|
||||
operation.displayName === "edit" &&
|
||||
(binding.kind !== "state" || binding.primitive !== "edit")
|
||||
)
|
||||
issue(
|
||||
issues,
|
||||
"invalid-state-binding",
|
||||
bindingPath,
|
||||
"CRDT edit operations must resolve to a native state edit capability",
|
||||
);
|
||||
if (
|
||||
member.kind !== "operation" &&
|
||||
member.queryRead &&
|
||||
@@ -1652,6 +1686,17 @@ const validateConformances = (
|
||||
"State primitives may only implement value members",
|
||||
);
|
||||
}
|
||||
if (
|
||||
binding.primitive === "edit" &&
|
||||
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
|
||||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
|
||||
)
|
||||
issue(
|
||||
issues,
|
||||
"invalid-state-binding",
|
||||
bindingPath,
|
||||
"Edit requires a CRDT slot with a matching materialized profile type",
|
||||
);
|
||||
const expected = statePrimitiveSignature(attachment.attachment, binding.primitive);
|
||||
if (!signaturesMatch(operation, expected)) {
|
||||
issue(
|
||||
|
||||
Reference in New Issue
Block a user