Add checked private storage to reusable conformance rules

Scope attachments and projections by stable rule-application identity; keep lexical names private to each application. Lower ordinary initialization mappings and concrete captured creation targets through existing checking. Preserve stateless forwarding, conflict detection, and the materialization fence. Add tests for multiple same-atom specializations, name isolation, generic edges, factory seeds, and storage evolution. Full protocol Nix build and tests pass.
This commit is contained in:
Timothy J. Aveni
2026-09-25 11:04:22 -07:00
parent 902739b284
commit 73148d76e1
3 changed files with 303 additions and 17 deletions
+126 -2
View File
@@ -75,12 +75,17 @@ test("rule conflicts identify competing implementations", () => {
assert.match(JSON.stringify(result.diagnostics), /duplicate-conformance/);
});
test("rules validate unused bodies and reject storage and ambiguous names", () => {
test("rules validate unused bodies and ambiguous names", () => {
const compile = fixture(nameResources);
for (const [body, code] of [
["conform Person as HasName { }", "missing-rule-id"],
['conform Person as HasName id "r" {bind name.get to interface Person.missing.get;}', "unknown-operation"],
['conform Person as HasName id "r" { initialize {}; }', "unsupported-rule-storage"],
['conform Person as HasName id "r" { initialize { content to state Missing; }; }', "invalid-initialization"],
['conform Person as HasName id "r" { bind name to state Missing; }', "invalid-state-field-binding"],
[
'conform Person as HasName id "r" {private state Bad id "bad" on Self : Missing policy register;}',
"unknown-type",
],
['atom Person id "p";' + namedRule, "ambiguous-conformance-source"],
[namedRule + namedRule, "duplicate-rule-id"],
]) {
@@ -90,6 +95,125 @@ test("rules validate unused bodies and reject storage and ambiguous names", () =
}
});
test("rule storage is scoped per application, including repeated specializations on one atom", async () => {
const compile = fixture([
'interface Source<value T : storable> id "source" revision "source@1" {}',
'interface Stored<value T : storable> id "stored" revision "stored@1" {value content id "value" : optional<T> {get id "get";set id "set";}}',
]);
const rule = `conform<value T : storable> Source<T> as Stored<T> id "store-rule" {
private state Value id "stored:value" on Self : optional<T> policy register default null;
bind content to state Value;
}`;
const atoms = `atom A id "a"; atom B id "b";
conform A as Source<string> id "a-string" {}
conform A as Source<int64> id "a-number" {}
conform B as Source<string> id "b-string" {}`;
const first = ok(compile(rule + atoms)).workspace;
const second = ok(compile(atoms + rule)).workspace;
const storage = (workspace: typeof first) =>
workspace.conformances
.filter((c) => c.ruleOrigin)
.map((c) => [c.id, c.privateAttachments, c.operationBindings])
.sort((a, b) => String(a[0]).localeCompare(String(b[0])));
assert.deepEqual(storage(first), storage(second));
const owners = first.conformances.filter((c) => c.ruleOrigin);
assert.equal(owners.length, 3);
assert.equal(new Set(owners.map((c) => c.privateAttachments[0]!.id)).size, 3);
for (const owner of owners) {
const state = owner.privateAttachments[0]!;
assert.equal(state.kind, "state");
if (state.kind !== "state") throw Error("state expected");
assert.equal(state.attachedTo, owner.atomId);
assert(owner.operationBindings.every((b) => b.binding.kind === "state" && b.binding.slotId === state.id));
}
const { storageContracts, storageChangeRequiresMigration } = await import("../src/capability-model/evolution.js");
const before = storageContracts(first);
const changed = ok(
compile(
atoms.replace('conform A as Source<int64> id "a-number" {}', "") +
rule.replace("default null", 'default "new default"'),
),
).workspace;
// Use the string specialization for default evolution; all nominal identities
// remain stable, and changing a default must not rewrite existing state.
const oldSlot = before.find((s) => s.id === owners.find((c) => c.atomId === "b")!.privateAttachments[0]!.id)!;
const newSlot = storageContracts(changed).find((s) => s.id === oldSlot.id)!;
assert.equal(storageChangeRequiresMigration(oldSlot, newSlot, new Set(["a", "b"])), false);
assert.equal(storageChangeRequiresMigration(oldSlot, undefined, new Set(["a", "b"])), true);
});
test("rule attachments shadow outside names without leaking into another owner", () => {
const rule = `conform Person as HasName id "r" {
private state NameLiving id "rule:name" on Self : string policy register default "rule";
bind name to state NameLiving;
}`;
const workspace = ok(fixture(nameResources)(rule + person("Living"))).workspace;
const original = workspace.conformances.find((c) => c.interfaceRevisionId === "person@1")!;
const derived = workspace.conformances.find((c) => c.interfaceRevisionId === "name@1")!;
assert.notEqual(original.privateAttachments[0]!.id, derived.privateAttachments[0]!.id);
assert(
derived.operationBindings.every(
(b) => b.binding.kind === "state" && b.binding.slotId === derived.privateAttachments[0]!.id,
),
);
assert(
original.operationBindings.every(
(b) => b.binding.kind === "state" && b.binding.slotId === original.privateAttachments[0]!.id,
),
);
});
test("rule private edges substitute Self and captured endpoints with stable projection IDs", () => {
const compile = fixture([
'interface Linkable<object T> id "linkable" revision "linkable@1" {}',
'interface Links<object T> id "links" revision "links@1" {relation items id "items" : many object T {resolve id "resolve";}}',
]);
const source = `atom A id "a"; atom B id "b"; atom Target id "target";
conform A as Linkable<atom Target> id "a-link" {} conform B as Linkable<atom Target> id "b-link" {}
conform<object T> Linkable<T> as Links<T> id "links" {
private edge Items id "items" {
object Self projection items id "forward" many;
object T projection owners id "inverse" many;
}
bind items.resolve to edge Items.items.resolve;
}`;
const workspace = ok(compile(source)).workspace;
const edges = workspace.conformances.filter((c) => c.ruleOrigin).map((c) => c.privateAttachments[0]!);
assert.equal(edges.length, 2);
assert.notEqual(edges[0]!.id, edges[1]!.id);
const projections = edges.flatMap((edge) => {
assert.equal(edge.kind, "edge");
if (edge.kind !== "edge") throw Error("edge expected");
assert.deepEqual(edge.endpoints[1].constraint, { kind: "atom", atomId: "target" });
return edge.endpoints.map((e) => e.projectionId);
});
assert.equal(new Set(projections).size, 4);
});
test("rule creation ports close captured atom parameters and retain initialization checking", () => {
const compile = fixture([
'interface Factory<object T> id "factory" revision "factory@1" {operation create id "create" : unit -> ref<T> {call id "call" execution transaction targets [browser,server];}}',
'interface Creates<object T> id "creates" revision "creates@1" {}',
'package Runtime id "runtime" revision "runtime@1" {operation create<object T> id "create" : unit -> ref<T> mode call execution transaction targets [browser,server] receiver any requires {creation result id "result" : T input unit;};}',
]);
const rule = `conform<object T> Creates<T> as Factory<T> id "factory-rule" {
bind create.call to package Runtime.create<T> with {result to creation T with {};};
}`;
const result = ok(
compile(`atom Subject id "subject"; atom Product id "product";
conform Subject as Creates<atom Product> id "creates-product" {} ${rule}`),
);
const owner = result.workspace.conformances.find((c) => c.ruleOrigin)!;
const binding = owner.operationBindings[0]!.binding;
assert.equal(binding.kind, "package");
if (binding.kind !== "package") throw Error("package expected");
assert.deepEqual(binding.dependencies[0]!.binding, { kind: "creation", atomId: "product", owners: [] });
const invalid = compile(`atom Subject id "subject";
conform Subject as Creates<interface Creates<atom Subject>> id "creates-interface" {} ${rule}`);
assert.equal(invalid.ok, false);
assert.match(JSON.stringify(invalid.diagnostics), /concrete atom/);
});
const genericResources = [
'interface Placeable id "placeable" revision "placeable@1" {}',
'interface Eligible id "eligible" revision "eligible@1" {}',