Add checked React field bindings and Web Studio factories

Replace opaque props with checked generic presentation contracts and lazy typed
interface references. Generate readonly/writable field APIs and component checks.

Preserve CRDT editing through explicit resolved getter/setter contracts, binding-
fenced delta RPCs, native watches and replica-aware field adapters. Custom setters
retain semantic writes; storage snapshots never grant write authority. Cover
concurrent edits, lost acknowledgements, readonly contracts and authorization.

Add receiver-free static factory dispatch, state-field binding shorthand, and
conformance-based creation. Migrate TODO, editable scaffolds and authoring guides.
Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production
browser bundling and CRDT persistence with temporary PostgreSQL.
This commit is contained in:
Timothy J. Aveni
2026-09-16 11:25:20 -07:00
parent 52803dda05
commit 59735c5e38
22 changed files with 3034 additions and 2300 deletions
+52 -14
View File
@@ -23,6 +23,7 @@ import type {
OwnedAttachment,
PackageExport,
PackageOperationExport,
PackageFunctionExport,
PackageRevision,
PackageRevisionId,
PersistentAttachment,
@@ -1423,6 +1424,24 @@ const validateConformances = (
}
const operation = operationEntry.operation;
const binding = entry.binding;
if (operation.scope === "class" && (!conformance.id || operation.mode !== "call")) {
issue(
issues,
"invalid-package-binding",
bindingPath,
"Class capabilities require an explicit conformance ID and call mode",
);
continue;
}
if (operation.scope === "class" && binding.kind !== "package") {
issue(
issues,
"invalid-package-binding",
bindingPath,
"Class capabilities must bind a free package function, not instance storage",
);
continue;
}
if (binding.kind === "state") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (!attachment || attachment.attachment.kind !== "state") {
@@ -1562,31 +1581,49 @@ const validateConformances = (
);
continue;
}
if (packageExport.kind !== "operation") {
if (operation.scope === "class" ? packageExport.kind !== "function" : packageExport.kind !== "operation") {
issue(
issues,
"invalid-package-binding",
`${bindingPath}.binding.exportId`,
`Package export ${binding.exportId} is ${packageExport.kind}, not an operation`,
`Package export ${binding.exportId} must be ${operation.scope === "class" ? "a free function" : "an instance operation"}`,
);
continue;
}
if (!signaturesMatch(operation, packageExport)) {
if (
!signaturesMatch(operation, {
...packageExport,
mode: packageExport.kind === "operation" ? packageExport.mode : "call",
})
) {
issue(
issues,
"invalid-package-binding",
`${bindingPath}.binding.exportId`,
`Package export provides ${describeSignature(packageExport)}, but operation requires ${describeSignature(operation)}`,
`Package export signature does not match ${describeSignature(operation)}`,
);
}
if (packageExport.kind === "operation")
validatePackageReceiver(issues, {
entry: packageExport,
atomId: conformance.atomId,
path: `${bindingPath}.binding.exportId`,
indexes,
requirementGraph,
graphSourceKey: key,
});
if (
operation.scope === "class" &&
(binding.dependencies.some((entry) => entry.binding.kind !== "constructor") ||
packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor"))
) {
issue(
issues,
"invalid-package-binding",
bindingPath,
"Class functions may inject constructors, not instance-dependent ports",
);
}
validatePackageReceiver(issues, {
entry: packageExport,
atomId: conformance.atomId,
path: `${bindingPath}.binding.exportId`,
indexes,
requirementGraph,
graphSourceKey: key,
});
validateBoundDependencies(issues, {
dependencies: binding.dependencies,
dependencyPorts: packageExport.dependencyPorts,
@@ -1919,7 +1956,7 @@ export type ResolvedOperationPlan =
kind: "package";
binding: Extract<Binding, { kind: "package" }>;
packageRevision: PackageRevision;
packageExport: PackageOperationExport;
packageExport: PackageOperationExport | PackageFunctionExport;
dependencies: Array<{
port: DependencyPort;
binding: DependencyBinding;
@@ -1962,7 +1999,8 @@ export const resolveOperationPlan = (
}
const packageRevision = plan.packages.get(binding.packageRevisionId);
const packageExport = packageRevision?.exports.find(
(entry): entry is PackageOperationExport => entry.id === binding.exportId && entry.kind === "operation",
(entry): entry is PackageOperationExport | PackageFunctionExport =>
entry.id === binding.exportId && (entry.kind === "operation" || entry.kind === "function"),
);
if (!packageRevision || !packageExport) {
return undefined;