Add composable interface operation forwarding

Support bind member.operation to interface Target<Args>.member.operation.
Check exact signatures and scopes, same-receiver conformances, cycles and
watch pairing. Resolve chains for native queries, editing and dispatch while
retaining the terminal conformance owner and injected package dependencies.
Track forwarded providers in dependency and semantic-major review machinery.

Cover native get/set/watch chains, generic targets, invalid bindings, package
forwarding, runtime owner authority and watch cancellation. Document the
language feature and keep Studio search adoption explicitly pending.
No push or deployment.
This commit is contained in:
Timothy J. Aveni
2026-09-19 10:18:47 -07:00
parent 2594403503
commit 1aa55f7a35
10 changed files with 821 additions and 467 deletions
+80 -2
View File
@@ -1,3 +1,4 @@
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
import type {
AtomConstructorBinding,
AtomId,
@@ -66,6 +67,7 @@ export type CapabilityValidationIssueCode =
| "invalid-state-binding"
| "invalid-edge-binding"
| "invalid-package-binding"
| "invalid-forwarding-binding"
| "invalid-dependency-binding"
| "private-attachment-access"
| "incompatible-package-receiver"
@@ -1521,7 +1523,32 @@ const validateConformances = (
continue;
}
const operation = operationEntry.operation;
const binding = entry.binding;
let binding: ConcreteBinding;
if (entry.binding.kind === "interface") {
const target = indexes.interfaces
.get(entry.binding.interfaceRevisionId)
?.operations.get(entry.binding.operationId)?.operation;
if (!target || !signaturesMatch(operation, target) || operation.scope !== target.scope) {
issue(
issues,
"invalid-forwarding-binding",
bindingPath,
"Forwarding requires matching signatures, modes and receiver scopes",
);
continue;
}
try {
binding = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
entry.operationId,
).binding;
} catch (error) {
issue(issues, "invalid-forwarding-binding", bindingPath, String(error));
continue;
}
} else binding = entry.binding;
const member = operationEntry.member;
if (
member.kind !== "operation" &&
@@ -1556,6 +1583,9 @@ const validateConformances = (
);
continue;
}
// The target conformance validates the concrete provider and its private
// authority. Forwarding exposes that operation, not its attachment ports.
if (entry.binding.kind === "interface") continue;
if (operation.scope === "class" && binding.kind !== "package") {
issue(
issues,
@@ -1760,6 +1790,49 @@ const validateConformances = (
});
}
for (const member of interfaceEntry.revision.members) {
const start = member.operations.find((op) => op.mode === "watch-start");
const stop = member.operations.find((op) => op.mode === "watch-stop");
if (!start || !stop || ![start, stop].some((op) => bindings.get(op.id)?.kind === "interface")) continue;
try {
const a = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id);
const b = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, stop.id);
const target = indexes.interfaces
.get(a.conformance.interfaceRevisionId)
?.revision.members.find((m) => m.operations.some((op) => op.id === a.entry.operationId));
if (a.conformance !== b.conformance || !target?.operations.some((op) => op.id === b.entry.operationId))
issue(
issues,
"invalid-forwarding-binding",
path,
"Forwarded watch start/stop must resolve to the same target member",
);
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
if (get && (a.binding.kind === "state" || a.binding.kind === "edge")) {
const read = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
get.id,
).binding;
const matches =
a.binding.kind === "state"
? read.kind === "state" && read.slotId === a.binding.slotId
: read.kind === "edge" &&
read.edgeTypeId === a.binding.edgeTypeId &&
read.projectionId === a.binding.projectionId;
if (!matches)
issue(
issues,
"invalid-forwarding-binding",
path,
"Native forwarded watch must observe its getter's storage",
);
}
} catch (error) {
issue(issues, "invalid-forwarding-binding", path, String(error));
}
}
const materializedMembers = new Set<string>();
for (const [materializationIndex, materialization] of (conformance.relationshipMaterializations ?? []).entries()) {
const materializationPath = `${path}.relationshipMaterializations[${materializationIndex}]`;
@@ -2063,7 +2136,10 @@ export const resolveOperationBinding = (
atomId: AtomId,
interfaceRevisionId: InterfaceRevisionId,
operationId: OperationId,
): Binding | undefined => resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.get(operationId);
): ConcreteBinding | undefined => {
if (!resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.has(operationId)) return undefined;
return resolveForwardedBinding(plan.source, atomId, interfaceRevisionId, operationId).binding;
};
export type ResolvedOperationPlan =
| {
@@ -2259,6 +2335,8 @@ export const computeCapabilityClosure = (
attachments.add(binding.slotId);
} else if (binding.kind === "edge") {
attachments.add(binding.edgeTypeId);
} else if (binding.kind === "interface") {
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
} else {
packages.add(binding.packageRevisionId);
includeDependencies(root.atomId, binding.dependencies);