Repair workspace authoring: typed RPC inputs, automatic preservation, and template releases

Use checked input contracts and protobuf JSON for CLI roundtrips; generate Web Studio platform inputs; reject invalid constructors before allocation and filter Createable eligibility.

Preserve compatible storage without no-op migrations, report activation readiness, and validate migration coverage before maintenance. Follow authored package declarations during scaffold refresh and queue source capture.

Add a real local TODO check/activate/create/place/edit acceptance, publish updated protocol and SDK dependencies, and make verified template default selection explicit.
This commit is contained in:
Timothy J. Aveni
2026-09-14 15:24:37 -07:00
parent 01ca965c7f
commit 14b0ef25c3
13 changed files with 231 additions and 27 deletions
+23 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { capabilityId as id, valueType, validateWorkspaceRevision } from "../src/capability-model/index.js";
import { contentDigest, planEvolution, runtimeContracts, storageContracts } from "../src/capability-model/evolution.js";
import { contentDigest, planEvolution, runtimeContracts, storageContracts, storageChangeRequiresMigration } from "../src/capability-model/evolution.js";
import { capabilityFixtureSource, capabilityResourceSources, compileCapabilityFixture, makeValidCapabilityWorkspace } from "./fixtures/capability-model.js";
test("QX carries stable conformance IDs and implementation semantic majors", () => {
@@ -68,6 +68,7 @@ test("storage defaults and ownership changes invalidate consumers without requir
if (slot.kind === "state") slot.defaultValue = "Different default";
const report = planEvolution(before, after, { allowLegacy: true });
assert.equal(report.storageChanges.length, 1);
assert.deepEqual(report.migrationRequired, []);
assert.equal(report.runtimeActions[0]!.action, "replace");
assert.deepEqual(report.reviews, []);
assert.notDeepEqual(storageContracts(before), storageContracts(after));
@@ -96,3 +97,24 @@ test("evolution enrollment is explicit and never erases legacy ownership", () =>
assert.equal(runtimeContracts(workspace).length, 1);
assert.throws(() => planEvolution(workspace, { ...workspace, workspaceId: id.workspace("other") }), /different workspace/);
});
test("automatic preservation distinguishes additions and defaults from incompatible storage", () => {
const workspace = makeValidCapabilityWorkspace();
const before = storageContracts(workspace).find(entry => entry.kind === "state")!;
const next = structuredClone(before);
const value = next.definition as Record<string, unknown>;
value.defaultValue = {displayName: "important user data"};
assert.equal(storageChangeRequiresMigration(before, next, new Set()), false);
next.ownerId = "another-owner";
assert.equal(storageChangeRequiresMigration(before, next, new Set()), true);
assert.equal(storageChangeRequiresMigration(before, undefined, new Set()), true);
delete value.defaultValue;
assert.equal(storageChangeRequiresMigration(undefined, next, new Set([value.attachedTo as string])), true);
assert.equal(storageChangeRequiresMigration(undefined, next, new Set()), false);
const slot = workspace.sharedAttachments.find(entry => entry.kind === "state")!;
if (slot.kind !== "state") throw new Error("fixture slot");
slot.defaultValue = {displayName: "one"};
const first = storageContracts(workspace);
slot.defaultValue = {displayName: "two"};
assert.notDeepEqual(storageContracts(workspace), first, "user data must not be stripped as schema metadata");
});