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:
@@ -1,3 +1,4 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
BaseErrorListener,
|
BaseErrorListener,
|
||||||
CharStream,
|
CharStream,
|
||||||
@@ -1712,7 +1713,8 @@ const lowerPackage = (
|
|||||||
const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotDefinition => {
|
const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotDefinition => {
|
||||||
const atomName = identifier(context.identifier(1));
|
const atomName = identifier(context.identifier(1));
|
||||||
const atomId =
|
const atomId =
|
||||||
requireSymbol(state, state.atoms, atomName, context, "atom") ?? capabilityId.atom(`unresolved:${atomName}`);
|
(atomName === "Self" ? state.types.self : requireSymbol(state, state.atoms, atomName, context, "atom")) ??
|
||||||
|
capabilityId.atom(`unresolved:${atomName}`);
|
||||||
const policy = context.storagePolicy();
|
const policy = context.storagePolicy();
|
||||||
const defaultContext = context.jsonLiteral();
|
const defaultContext = context.jsonLiteral();
|
||||||
let defaultValue: unknown;
|
let defaultValue: unknown;
|
||||||
@@ -1932,7 +1934,18 @@ const lowerBoundDependencies = (
|
|||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
const atomName = identifier(entry.identifier(1));
|
const atomName = identifier(entry.identifier(1));
|
||||||
const atomId = requireSymbol(state, state.atoms, atomName, entry, "atom");
|
const parameter = state.types.parameters.get(atomName);
|
||||||
|
const argument = parameter && state.types.bindings.get(parameter.id);
|
||||||
|
const atomId =
|
||||||
|
atomName === "Self"
|
||||||
|
? state.types.self
|
||||||
|
: argument?.kind === "object" && argument.target.kind === "atom"
|
||||||
|
? argument.target.atomId
|
||||||
|
: parameter
|
||||||
|
? undefined
|
||||||
|
: requireSymbol(state, state.atoms, atomName, entry, "atom");
|
||||||
|
if (!atomId && (parameter || atomName === "Self"))
|
||||||
|
loweringIssue(state, entry, "invalid-creation-target", "Creation requires a concrete atom argument");
|
||||||
if (entry.CREATION())
|
if (entry.CREATION())
|
||||||
return atomId
|
return atomId
|
||||||
? [
|
? [
|
||||||
@@ -2031,19 +2044,90 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
|||||||
const targetDefinition = state.types.definitions.get(target.definitionId)!;
|
const targetDefinition = state.types.definitions.get(target.definitionId)!;
|
||||||
const applications = [source, target];
|
const applications = [source, target];
|
||||||
const members = targetDefinition.template?.members ?? targetDefinition.members;
|
const members = targetDefinition.template?.members ?? targetDefinition.members;
|
||||||
|
const privateStorage = new Map<string, "state" | "edge">();
|
||||||
|
const privateIds = new Set<string>();
|
||||||
for (const item of context.conformanceItem()) {
|
for (const item of context.conformanceItem()) {
|
||||||
|
const declaration = item.attachmentDecl();
|
||||||
|
if (!declaration) continue;
|
||||||
|
const stateDeclaration = declaration.stateDecl();
|
||||||
|
const edgeDeclaration = declaration.edgeDecl();
|
||||||
|
const name = identifier(stateDeclaration ? stateDeclaration.identifier(0) : edgeDeclaration!.identifier());
|
||||||
|
const authoredId = stringValue((stateDeclaration ?? edgeDeclaration)!.stringLiteral());
|
||||||
|
if (privateStorage.has(name))
|
||||||
|
loweringIssue(state, declaration, "duplicate-symbol", `Duplicate private attachment ${name}`);
|
||||||
|
if (privateIds.has(authoredId))
|
||||||
|
loweringIssue(state, declaration, "duplicate-attachment", `Duplicate private attachment ID ${authoredId}`);
|
||||||
|
privateStorage.set(name, stateDeclaration ? "state" : "edge");
|
||||||
|
privateIds.add(authoredId);
|
||||||
|
if (stateDeclaration && identifier(stateDeclaration.identifier(1)) !== "Self")
|
||||||
|
loweringIssue(state, stateDeclaration, "invalid-rule-storage", "Rule private state must attach to Self");
|
||||||
|
if (stateDeclaration) {
|
||||||
|
try {
|
||||||
|
state.types.expression(stateDeclaration.valueType());
|
||||||
|
if (stateDeclaration.storagePolicy().valueType())
|
||||||
|
state.types.expression(stateDeclaration.storagePolicy().valueType()!);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof GenericTypeError) loweringIssue(state, stateDeclaration, error.code, error.message);
|
||||||
|
else throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
item.attachmentDecl() ||
|
edgeDeclaration &&
|
||||||
item.initializationDecl() ||
|
!edgeDeclaration
|
||||||
item.relationshipMaterializationDecl() ||
|
.edgeEndpoint()
|
||||||
item.stateFieldBindingDecl()
|
.some((endpoint) => identifier(endpoint.targetConstraint().identifier()) === "Self")
|
||||||
) {
|
)
|
||||||
|
loweringIssue(state, edgeDeclaration, "invalid-rule-storage", "Rule private edge must have a Self endpoint");
|
||||||
|
for (const endpoint of edgeDeclaration?.edgeEndpoint() ?? []) {
|
||||||
|
try {
|
||||||
|
state.types.targetExpression(endpoint.targetConstraint());
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof GenericTypeError) loweringIssue(state, endpoint, error.code, error.message);
|
||||||
|
else throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (context.conformanceItem().filter((item) => item.initializationDecl()).length > 1)
|
||||||
|
loweringIssue(state, context, "duplicate-initialization", "Declare one initialization mapping per rule owner");
|
||||||
|
for (const item of context.conformanceItem()) {
|
||||||
|
const field = item.stateFieldBindingDecl();
|
||||||
|
if (field) {
|
||||||
|
const member = members.find((entry) => entry.displayName === identifier(field.identifier(0)));
|
||||||
|
if (member?.kind !== "value" || privateStorage.get(identifier(field.identifier(1))) !== "state")
|
||||||
loweringIssue(
|
loweringIssue(
|
||||||
state,
|
state,
|
||||||
item,
|
field,
|
||||||
"unsupported-rule-storage",
|
"invalid-state-field-binding",
|
||||||
"Rules support operation and module bindings, not private storage, initialization or materialization",
|
"Field shorthand requires a value member and rule-private state",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
const initialization = item.initializationDecl();
|
||||||
|
for (const entry of initialization?.initializationField() ?? []) {
|
||||||
|
const kind = privateStorage.get(identifier(entry.identifier(1)));
|
||||||
|
if (!kind || (entry.STATE() && kind !== "state") || (entry.EDGE() && kind !== "edge"))
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
entry,
|
||||||
|
"invalid-initialization",
|
||||||
|
"Initialization must name matching rule-private storage",
|
||||||
|
);
|
||||||
|
if (kind === "edge" && entry.EDGE()) {
|
||||||
|
const edge = context
|
||||||
|
.conformanceItem()
|
||||||
|
.flatMap((candidate) => candidate.attachmentDecl()?.edgeDecl() ?? [])
|
||||||
|
.find((candidate) => identifier(candidate.identifier()) === identifier(entry.identifier(1)));
|
||||||
|
if (
|
||||||
|
!edge
|
||||||
|
?.edgeEndpoint()
|
||||||
|
.some((endpoint) => identifier(endpoint.identifier()) === identifier(entry.identifier(2)))
|
||||||
|
)
|
||||||
|
loweringIssue(state, entry, "unknown-symbol", "Unknown rule-private edge projection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of context.conformanceItem()) {
|
||||||
|
if (item.relationshipMaterializationDecl()) {
|
||||||
|
loweringIssue(state, item, "unsupported-rule-storage", "Rule relationship materialization is not supported");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const module = item.moduleBindingDecl();
|
const module = item.moduleBindingDecl();
|
||||||
@@ -2095,6 +2179,28 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (provider.STATE() || provider.EDGE()) {
|
||||||
|
const name = identifier(provider.identifier(0));
|
||||||
|
const expected = provider.STATE() ? "state" : "edge";
|
||||||
|
if (privateStorage.get(name) !== expected)
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
provider,
|
||||||
|
"invalid-rule-storage-binding",
|
||||||
|
`Rule provider must name private ${expected} ${name}`,
|
||||||
|
);
|
||||||
|
if (provider.EDGE()) {
|
||||||
|
const edge = context
|
||||||
|
.conformanceItem()
|
||||||
|
.flatMap((candidate) => candidate.attachmentDecl()?.edgeDecl() ?? [])
|
||||||
|
.find((candidate) => identifier(candidate.identifier()) === name);
|
||||||
|
if (
|
||||||
|
!edge
|
||||||
|
?.edgeEndpoint()
|
||||||
|
.some((endpoint) => identifier(endpoint.identifier()) === identifier(provider.identifier(1)))
|
||||||
|
)
|
||||||
|
loweringIssue(state, provider, "unknown-symbol", "Unknown rule-private edge projection");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
loweringIssue(
|
loweringIssue(
|
||||||
state,
|
state,
|
||||||
@@ -2123,8 +2229,13 @@ const lowerConformanceBody = (
|
|||||||
context: ConformanceDeclContext,
|
context: ConformanceDeclContext,
|
||||||
privateAttachments: PersistentAttachment[],
|
privateAttachments: PersistentAttachment[],
|
||||||
implementingAtom?: AtomId,
|
implementingAtom?: AtomId,
|
||||||
|
ruleApplicationId?: string,
|
||||||
): WorkspaceRevision["conformances"][number] | undefined => {
|
): WorkspaceRevision["conformances"][number] | undefined => {
|
||||||
const state = { ...parent, interfaces: new Map(parent.interfaces) };
|
const state = {
|
||||||
|
...parent,
|
||||||
|
interfaces: new Map(parent.interfaces),
|
||||||
|
attachments: ruleApplicationId ? new Map(parent.attachments) : parent.attachments,
|
||||||
|
};
|
||||||
const atomName = identifier(context.identifier(0));
|
const atomName = identifier(context.identifier(0));
|
||||||
const interfaceName = identifier(context.identifier(1));
|
const interfaceName = identifier(context.identifier(1));
|
||||||
const atomId = implementingAtom ?? requireSymbol(state, state.atoms, atomName, context, "atom");
|
const atomId = implementingAtom ?? requireSymbol(state, state.atoms, atomName, context, "atom");
|
||||||
@@ -2132,6 +2243,36 @@ const lowerConformanceBody = (
|
|||||||
if (!atomId || !interfaceSymbol) {
|
if (!atomId || !interfaceSymbol) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (ruleApplicationId) {
|
||||||
|
privateAttachments = [];
|
||||||
|
for (const item of context.conformanceItem()) {
|
||||||
|
const declaration = item.attachmentDecl();
|
||||||
|
if (!declaration) continue;
|
||||||
|
const attachment = lowerAttachment(state, declaration);
|
||||||
|
if (!attachment) continue;
|
||||||
|
const authoredId = attachment.id;
|
||||||
|
const scopedId = createHash("sha256")
|
||||||
|
.update(JSON.stringify(["quixos-rule-attachment-v1", ruleApplicationId, authoredId]))
|
||||||
|
.digest("hex");
|
||||||
|
if (attachment.kind === "state") {
|
||||||
|
attachment.id = capabilityId.slot(`rule:sha256:${scopedId}`);
|
||||||
|
} else {
|
||||||
|
attachment.id = capabilityId.edgeType(`rule:sha256:${scopedId}`);
|
||||||
|
for (const endpoint of attachment.endpoints) {
|
||||||
|
const projectionHash = createHash("sha256")
|
||||||
|
.update(JSON.stringify(["quixos-rule-projection-v1", ruleApplicationId, authoredId, endpoint.projectionId]))
|
||||||
|
.digest("hex");
|
||||||
|
endpoint.projectionId = capabilityId.edgeProjection(`rule:sha256:${projectionHash}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
privateAttachments.push(attachment);
|
||||||
|
// A rule application owns a lexical scope. A same-named attachment in
|
||||||
|
// another owner must neither prevent this declaration nor receive bindings.
|
||||||
|
const name = identifier(declaration.stateDecl()?.identifier(0) ?? declaration.edgeDecl()!.identifier());
|
||||||
|
state.attachments.delete(name);
|
||||||
|
registerAttachment(state, declaration, attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
const originalSymbol = interfaceSymbol;
|
const originalSymbol = interfaceSymbol;
|
||||||
if (context.typeArguments() || interfaceSymbol.definition?.template) {
|
if (context.typeArguments() || interfaceSymbol.definition?.template) {
|
||||||
const previousSelf = state.types.self;
|
const previousSelf = state.types.self;
|
||||||
@@ -2654,7 +2795,7 @@ const lowerWorkspace = (
|
|||||||
const declaration = rules.get(application.rule)!;
|
const declaration = rules.get(application.rule)!;
|
||||||
const result = atGenericSource(declaration, () =>
|
const result = atGenericSource(declaration, () =>
|
||||||
withConformanceTypes(state, application.atomId, application.rule.parameters, application.bindings, () =>
|
withConformanceTypes(state, application.atomId, application.rule.parameters, application.bindings, () =>
|
||||||
lowerConformanceBody(state, declaration, [], application.atomId),
|
lowerConformanceBody(state, declaration, [], application.atomId, application.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -75,12 +75,17 @@ test("rule conflicts identify competing implementations", () => {
|
|||||||
assert.match(JSON.stringify(result.diagnostics), /duplicate-conformance/);
|
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);
|
const compile = fixture(nameResources);
|
||||||
for (const [body, code] of [
|
for (const [body, code] of [
|
||||||
["conform Person as HasName { }", "missing-rule-id"],
|
["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" {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"],
|
['atom Person id "p";' + namedRule, "ambiguous-conformance-source"],
|
||||||
[namedRule + namedRule, "duplicate-rule-id"],
|
[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 = [
|
const genericResources = [
|
||||||
'interface Placeable id "placeable" revision "placeable@1" {}',
|
'interface Placeable id "placeable" revision "placeable@1" {}',
|
||||||
'interface Eligible id "eligible" revision "eligible@1" {}',
|
'interface Eligible id "eligible" revision "eligible@1" {}',
|
||||||
|
|||||||
@@ -87,6 +87,27 @@ test("two factories initialize two independent state owners without constructors
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rule-owned state uses the same checked initialization and creation obligations", () => {
|
||||||
|
const source = text
|
||||||
|
.replace('conform Note as Data id "owner:data"', 'conform Tagged as Data id "rule:data"')
|
||||||
|
.replace("on Note : string", "on Self : string");
|
||||||
|
const result = compile(source);
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const owner = result.workspace.conformances.find((c) => c.ruleOrigin?.ruleId === "rule:data")!;
|
||||||
|
assert.equal(owner.initialization?.[0]?.binding.kind, "state");
|
||||||
|
assert(owner.privateAttachments[0]!.id.startsWith("rule:sha256:"));
|
||||||
|
for (const [invalid, diagnostic] of [
|
||||||
|
[source.replaceAll("Data from input.data;", ""), "Required state"],
|
||||||
|
[source.replace("initialize { title to state Title; };", ""), "no declared initialization mapping"],
|
||||||
|
[source.replace("title to state Title;", "title to state Tags;"), "rule-private storage"],
|
||||||
|
[source.replaceAll("Data from input.data;", "Data from input.tags;"), "does not match"],
|
||||||
|
]) {
|
||||||
|
const failure = compile(invalid!);
|
||||||
|
assert.equal(failure.ok, false);
|
||||||
|
assert.match(JSON.stringify(failure.diagnostics), new RegExp(diagnostic!));
|
||||||
|
}
|
||||||
|
});
|
||||||
test("creation rejects missing seeds, wrong input shapes, and private initialization laundering", () => {
|
test("creation rejects missing seeds, wrong input shapes, and private initialization laundering", () => {
|
||||||
for (const [edit, message] of [
|
for (const [edit, message] of [
|
||||||
[text.replaceAll("Tagged from input.tags;", ""), "Required state tags-state"],
|
[text.replaceAll("Tagged from input.tags;", ""), "Required state tags-state"],
|
||||||
|
|||||||
Reference in New Issue
Block a user