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
+55
View File
@@ -1,4 +1,6 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { test } from "node:test";
import { compileCapabilitySource } from "../src/capability-language/index.js";
import {
@@ -146,3 +148,56 @@ test("well-formed but invalid programs report semantic paths", () => {
assert.equal(diagnostic.phase, "validation");
assert.ok(diagnostic.path?.includes("operationBindings"));
});
test("Web Studio sidecars declare lazy materialization and checked cross-object ports", () => {
const source = readFileSync(
resolve(process.cwd(), "test/fixtures/web-studio.capabilities.qx"),
"utf8",
);
const result = compileCapabilitySource(source, "web-studio.capabilities.qx");
assert.equal(result.ok, true);
if (!result.ok) return;
const host = result.workspace.conformances.find((entry) =>
entry.atomId === "atom:project" &&
entry.interfaceRevisionId === "interface:org.quixos.web-studio.has-react-component@1"
);
assert.deepEqual(host?.relationshipMaterializations, [{
memberId: "member:org.quixos.web-studio.has-react-component:component",
constructorAtomId: "atom:project-component",
edgeTypeId: "edge:project:component",
constructedProjectionId: "projection:component:subject",
}]);
const component = result.workspace.conformances.find((entry) =>
entry.atomId === "atom:project-component"
);
const binding = component?.operationBindings[0]?.binding;
assert.equal(binding?.kind, "package");
if (binding?.kind !== "package") return;
assert.deepEqual(binding.dependencies[1]?.binding, {
kind: "state",
slotId: "slot:project:name",
via: {
edgeTypeId: "edge:project:component",
projectionId: "projection:component:subject",
},
});
});
test("relationship materializers require a constructor from the host atom", () => {
const source = readFileSync(
resolve(process.cwd(), "test/fixtures/web-studio.capabilities.qx"),
"utf8",
).replace(
"constructs ProjectComponent : atom-ref<Project>;",
"constructs ProjectComponent : unit;",
);
const result = compileCapabilitySource(source, "invalid-materializer.qx");
assert.equal(result.ok, false);
if (result.ok) return;
assert.ok(result.diagnostics.some((entry) =>
entry.code === "invalid-relationship-materialization" &&
entry.message.includes("must accept")
));
});
+53
View File
@@ -0,0 +1,53 @@
workspace WebStudioFixture id "workspace:web-studio-fixture" revision "workspace:web-studio-fixture@1" commit "1111111111111111111111111111111111111111" {
atom Project id "atom:project";
atom ProjectComponent id "atom:project-component";
interface ReactComponent id "interface:org.quixos.web-studio.react-component" revision "interface:org.quixos.web-studio.react-component@1" source {
repository "https://repos.quixos.org/org-quixos-web-studio/interface-react-component.git";
commit "2222222222222222222222222222222222222222";
} {
value props id "member:org.quixos.web-studio.react-component:props" : string {
get id "operation:org.quixos.web-studio.react-component:props:get";
}
}
interface HasReactComponent id "interface:org.quixos.web-studio.has-react-component" revision "interface:org.quixos.web-studio.has-react-component@1" source {
repository "https://repos.quixos.org/org-quixos-web-studio/interface-has-react-component.git";
commit "3333333333333333333333333333333333333333";
} {
relation component id "member:org.quixos.web-studio.has-react-component:component" : optional-one interface ReactComponent {
resolve id "operation:org.quixos.web-studio.has-react-component:component:resolve";
}
}
package ComponentRuntime id "package:component-runtime" revision "package:component-runtime@1" source {
repository "https://repos.quixos.org/quixos-test/package-component-runtime.git";
commit "4444444444444444444444444444444444444444";
} {
constructor createComponent id "export:component-runtime:create-component" constructs ProjectComponent : atom-ref<Project>;
operation propsGet id "export:component-runtime:props-get" : unit -> string mode call receiver atom ProjectComponent requires {
edge subject id "port:component:subject" : exactly-one atom Project [resolve];
state projectName id "port:component:project-name" : string [read];
};
}
shared state ProjectName id "slot:project:name" on Project : string
policy optimistic-register default "Untitled project";
shared edge ProjectComponentEdge id "edge:project:component" {
atom ProjectComponent projection subject id "projection:component:subject" exactly-one;
atom Project projection component id "projection:project:component" optional-one;
}
conform Project as HasReactComponent {
bind component.resolve to edge ProjectComponentEdge.component.resolve;
materialize component if absent using constructor ProjectComponent via edge ProjectComponentEdge.subject;
}
conform ProjectComponent as ReactComponent {
bind props.get to package ComponentRuntime.propsGet with {
subject to edge ProjectComponentEdge.subject;
projectName to state ProjectName via edge ProjectComponentEdge.subject;
};
}
constructor ProjectComponent to ComponentRuntime.createComponent;
}