Introduce persisted Web Studio component capabilities

This commit is contained in:
2026-09-04 21:03:09 -07:00
parent f4987093f6
commit fd039f095b
17 changed files with 2459 additions and 1609 deletions
+290 -8
View File
@@ -51,6 +51,7 @@ export type CapabilityValidationIssueCode =
| "duplicate-dependency-port"
| "duplicate-conformance"
| "duplicate-operation-binding"
| "duplicate-relationship-materialization"
| "duplicate-constructor"
| "unresolved-reference"
| "invalid-attachment"
@@ -64,7 +65,8 @@ export type CapabilityValidationIssueCode =
| "cyclic-conformance-requirement"
| "missing-operation-binding"
| "unknown-operation-binding"
| "invalid-constructor";
| "invalid-constructor"
| "invalid-relationship-materialization";
export interface CapabilityValidationIssue {
code: CapabilityValidationIssueCode;
@@ -460,6 +462,16 @@ const atomSatisfiesConstraint = (
? constraint.atomId === atomId
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId));
const constraintSatisfiesConstraint = (
actual: EdgeEndpointConstraint,
required: EdgeEndpointConstraint,
conformances: ReadonlyMap<string, Conformance>,
) => constraintsEqual(actual, required) || (
actual.kind === "atom" &&
required.kind === "interface" &&
atomSatisfiesConstraint(actual.atomId, required, conformances)
);
const canAccessAttachment = (
owner: AttachmentOwner,
conformance: Pick<Conformance, "atomId" | "interfaceRevisionId"> | undefined,
@@ -958,6 +970,52 @@ const validateAttachmentAccess = (
return true;
};
const validateTraversal = (
issues: CapabilityValidationIssue[],
indexes: ValidationIndexes,
atomId: AtomId,
traversal: { edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId },
path: string,
) => {
const attachment = findAttachment(indexes, "edge", traversal.edgeTypeId);
if (!attachment || attachment.attachment.kind !== "edge") {
issue(
issues,
"invalid-dependency-binding",
`${path}.edgeTypeId`,
`Unknown traversal edge ${traversal.edgeTypeId}`,
);
return undefined;
}
const projection = edgeProjection(attachment.attachment, traversal.projectionId);
if (!projection) {
issue(
issues,
"invalid-dependency-binding",
`${path}.projectionId`,
`Traversal projection ${traversal.projectionId} is not on edge ${traversal.edgeTypeId}`,
);
return undefined;
}
if (!atomSatisfiesConstraint(atomId, projection.endpoint.constraint, indexes.conformances)) {
issue(
issues,
"invalid-dependency-binding",
`${path}.projectionId`,
`Traversal projection ${traversal.projectionId} cannot originate at receiver atom ${atomId}`,
);
}
if (projection.endpoint.cardinality !== "exactly-one") {
issue(
issues,
"invalid-dependency-binding",
`${path}.projectionId`,
"Dependency traversal must select exactly one object",
);
}
return projection;
};
const validateBoundDependencies = (
issues: CapabilityValidationIssue[],
params: {
@@ -1020,12 +1078,31 @@ const validateBoundDependencies = (
break;
}
validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`);
if (attachment.attachment.attachedTo !== params.atomId) {
const traversal = stateBinding.via
? validateTraversal(
issues,
params.indexes,
params.atomId,
stateBinding.via,
`${path}.binding.via`,
)
: undefined;
if (
stateBinding.via
? traversal && !atomSatisfiesConstraint(
attachment.attachment.attachedTo,
traversal.target.constraint,
params.indexes.conformances,
)
: attachment.attachment.attachedTo !== params.atomId
) {
issue(
issues,
"invalid-dependency-binding",
`${path}.binding.slotId`,
`State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
stateBinding.via
? `State ${stateBinding.slotId} is not attached to the traversal target`
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
);
}
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
@@ -1061,16 +1138,46 @@ const validateBoundDependencies = (
);
break;
}
if (!atomSatisfiesConstraint(params.atomId, projection.endpoint.constraint, params.indexes.conformances)) {
const traversal = edgeBinding.via
? validateTraversal(
issues,
params.indexes,
params.atomId,
edgeBinding.via,
`${path}.binding.via`,
)
: undefined;
const originMatches = edgeBinding.via
? traversal && (
projection.endpoint.constraint.kind === "atom"
? atomSatisfiesConstraint(
projection.endpoint.constraint.atomId,
traversal.target.constraint,
params.indexes.conformances,
)
: constraintsEqual(projection.endpoint.constraint, traversal.target.constraint)
)
: atomSatisfiesConstraint(
params.atomId,
projection.endpoint.constraint,
params.indexes.conformances,
);
if (!originMatches) {
issue(
issues,
"invalid-dependency-binding",
`${path}.binding.projectionId`,
`Projection ${edgeBinding.projectionId} cannot originate at receiver atom ${params.atomId}`,
edgeBinding.via
? `Projection ${edgeBinding.projectionId} cannot originate at the traversal target`
: `Projection ${edgeBinding.projectionId} cannot originate at receiver atom ${params.atomId}`,
);
}
if (
!constraintsEqual(requirement.target, projection.target.constraint) ||
!constraintSatisfiesConstraint(
projection.target.constraint,
requirement.target,
params.indexes.conformances,
) ||
requirement.cardinality !== projection.endpoint.cardinality
) {
issue(
@@ -1290,8 +1397,21 @@ const validateConformances = (
}
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`);
const projection = edgeProjection(attachment.attachment, binding.projectionId);
const relationshipMember = operationEntry.member.kind === "relationship"
? operationEntry.member
: undefined;
const operationEdge = relationshipMember
? {
...attachment.attachment,
endpoints: attachment.attachment.endpoints.map((endpoint, index) =>
index === (projection?.endpointIndex === 0 ? 1 : 0)
? { ...endpoint, constraint: relationshipMember.target }
: endpoint,
) as [EdgeEndpoint, EdgeEndpoint],
}
: attachment.attachment;
const expected = edgePrimitiveSignature(
attachment.attachment,
operationEdge,
binding.projectionId,
binding.primitive,
);
@@ -1320,7 +1440,11 @@ const validateConformances = (
"Edge primitives may only implement relationship members",
);
} else if (
!constraintsEqual(operationEntry.member.target, projection.target.constraint) ||
!constraintSatisfiesConstraint(
projection.target.constraint,
operationEntry.member.target,
indexes.conformances,
) ||
operationEntry.member.cardinality !== projection.endpoint.cardinality
) {
issue(
@@ -1398,6 +1522,148 @@ const validateConformances = (
});
}
const materializedMembers = new Set<string>();
for (const [materializationIndex, materialization] of
(conformance.relationshipMaterializations ?? []).entries()) {
const materializationPath =
`${path}.relationshipMaterializations[${materializationIndex}]`;
if (materializedMembers.has(materialization.memberId)) {
issue(
issues,
"duplicate-relationship-materialization",
materializationPath,
`Relationship ${materialization.memberId} has more than one materialization recipe`,
);
continue;
}
materializedMembers.add(materialization.memberId);
const member = interfaceEntry.revision.members.find((entry) =>
entry.id === materialization.memberId);
if (!member || member.kind !== "relationship") {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.memberId`,
`Member ${materialization.memberId} is not a relationship on ${conformance.interfaceRevisionId}`,
);
continue;
}
if (member.cardinality !== "optional-one") {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.memberId`,
"A lazily constructed relationship must be optional-one before construction",
);
}
const resolveOperation = member.operations.find((entry) =>
entry.displayName === "resolve");
const resolveBinding = resolveOperation
? bindings.get(resolveOperation.id)
: undefined;
if (!resolveBinding || resolveBinding.kind !== "edge") {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.memberId`,
"A materialized relationship must bind its resolve operation to an edge",
);
continue;
}
if (resolveBinding.edgeTypeId !== materialization.edgeTypeId) {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.edgeTypeId`,
"Materialization and relationship resolution must use the same edge",
);
}
const attachment = findAttachment(indexes, "edge", materialization.edgeTypeId);
if (!attachment || attachment.attachment.kind !== "edge") {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.edgeTypeId`,
`Unknown materialization edge ${materialization.edgeTypeId}`,
);
continue;
}
validateAttachmentAccess(
issues,
attachment,
conformance,
`${materializationPath}.edgeTypeId`,
);
const hostProjection = edgeProjection(
attachment.attachment,
resolveBinding.projectionId,
);
const constructedProjection = edgeProjection(
attachment.attachment,
materialization.constructedProjectionId,
);
if (!hostProjection || !constructedProjection ||
hostProjection.endpointIndex === constructedProjection.endpointIndex) {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.constructedProjectionId`,
"Materialization must connect through the opposite projection of the resolved edge",
);
continue;
}
if (!atomSatisfiesConstraint(
materialization.constructorAtomId,
member.target,
indexes.conformances,
)) {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.constructorAtomId`,
`Constructed atom ${materialization.constructorAtomId} does not satisfy the relationship target`,
);
}
if (!atomSatisfiesConstraint(
materialization.constructorAtomId,
constructedProjection.endpoint.constraint,
indexes.conformances,
) || !atomSatisfiesConstraint(
conformance.atomId,
constructedProjection.target.constraint,
indexes.conformances,
)) {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.constructedProjectionId`,
"Constructed projection does not connect the constructed atom back to the host atom",
);
}
const constructor = indexes.constructors.get(materialization.constructorAtomId);
const constructorExport = constructor
? indexes.packages.get(constructor.packageRevisionId)?.exports.get(constructor.exportId)
: undefined;
if (!constructor || !constructorExport || constructorExport.kind !== "constructor") {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.constructorAtomId`,
`Constructed atom ${materialization.constructorAtomId} has no valid constructor`,
);
} else if (!valueTypesEqual(
constructorExport.inputType,
valueType.atomRef(conformance.atomId),
)) {
issue(
issues,
"invalid-relationship-materialization",
`${materializationPath}.constructorAtomId`,
`Component constructor must accept ${typeLabel(valueType.atomRef(conformance.atomId))}`,
);
}
}
for (const operation of interfaceEntry.operations.values()) {
if (!bindings.has(operation.operation.id)) {
issue(
@@ -1714,9 +1980,15 @@ export const computeCapabilityClosure = (
switch (dependency.binding.kind) {
case "state":
attachments.add(dependency.binding.slotId);
if (dependency.binding.via) {
attachments.add(dependency.binding.via.edgeTypeId);
}
break;
case "edge":
attachments.add(dependency.binding.edgeTypeId);
if (dependency.binding.via) {
attachments.add(dependency.binding.via.edgeTypeId);
}
break;
case "receiver-interface":
queued.push({
@@ -1772,6 +2044,16 @@ export const computeCapabilityClosure = (
}
}
}
for (const materialization of
conformance.source.relationshipMaterializations ?? []) {
attachments.add(materialization.edgeTypeId);
constructors.add(materialization.constructorAtomId);
const constructor = plan.constructors.get(materialization.constructorAtomId);
if (constructor) {
packages.add(constructor.packageRevisionId);
includeDependencies(materialization.constructorAtomId, constructor.dependencies);
}
}
}
return {