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:
@@ -20,13 +20,37 @@ const semantic = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) return value.map(semantic);
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
|
||||
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
|
||||
.map(([key, entry]) => [key, semantic(entry)]));
|
||||
// These are authored data/maps, not schema nodes. A user field literally
|
||||
// named displayName or documentation is semantic and must stay in the hash.
|
||||
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]));
|
||||
return value;
|
||||
};
|
||||
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) => [...entries].sort((a, b) => compareText(key(a), key(b)));
|
||||
export const conformanceIdentity = (entry: Conformance): string => entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
|
||||
|
||||
export type StorageContract = { id: string; ownerId: string; kind: "state" | "edge"; digest: string; definition: unknown };
|
||||
/** Automatic evolution preserves values; it never interprets migration code or
|
||||
* guesses that a new nominal message descriptor means the same representation. */
|
||||
export const storageChangeRequiresMigration = (previous: StorageContract | undefined, next: StorageContract | undefined,
|
||||
oldAtomIds: ReadonlySet<string>): boolean => {
|
||||
if (!next) return true;
|
||||
const after = next.definition as PersistentAttachment;
|
||||
if (!previous) {
|
||||
if (after.kind === "state") return oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional";
|
||||
return after.endpoints.some(endpoint => endpoint.cardinality === "exactly-one" &&
|
||||
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)));
|
||||
}
|
||||
if (previous.ownerId !== next.ownerId || previous.kind !== next.kind) return true;
|
||||
const before = previous.definition as PersistentAttachment;
|
||||
if (before.kind === "state" && after.kind === "state") {
|
||||
// Capture materializes old defaults, so changing a default affects only
|
||||
// newly constructed objects, not existing sparse state.
|
||||
const {defaultValue: _beforeDefault, ...beforeStorage} = before;
|
||||
const {defaultValue: _afterDefault, ...afterStorage} = after;
|
||||
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
|
||||
}
|
||||
return canonicalJson(before) !== canonicalJson(after);
|
||||
};
|
||||
export const storageContracts = (workspace: WorkspaceRevision): StorageContract[] => {
|
||||
const result: StorageContract[] = [];
|
||||
const add = (attachment: PersistentAttachment, ownerId: string) => {
|
||||
@@ -141,7 +165,8 @@ export type RuntimeAction = { groupId: string; action: "keep" | "start" | "repla
|
||||
export type EvolutionReport = {
|
||||
schemaVersion: 1; baselineDigest: string | null; candidateDigest: string; checkerVersion: string;
|
||||
runtimeActions: RuntimeAction[];
|
||||
storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; previous?: StorageContract; candidate?: StorageContract }>;
|
||||
storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; requiresMigration: boolean; previous?: StorageContract; candidate?: StorageContract }>;
|
||||
migrationRequired: string[];
|
||||
reviews: Array<ReviewRequirement & { accepted: boolean }>;
|
||||
packageChecks: Array<{ groupId: string; contractDigest: string }>;
|
||||
blockers: string[];
|
||||
@@ -175,6 +200,7 @@ export const planEvolution = (baseline: WorkspaceRevision | null, candidate: Wor
|
||||
for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) {
|
||||
const previous = beforeStorage.get(id), next = afterStorage.get(id);
|
||||
if (previous?.digest !== next?.digest) storageChanges.push({ id, kind: !previous ? "add" : !next ? "remove" : "change",
|
||||
requiresMigration: storageChangeRequiresMigration(previous, next, new Set(baseline?.atoms.map(atom => atom.id) ?? [])),
|
||||
...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}) });
|
||||
}
|
||||
const providers = (workspace: WorkspaceRevision) => [
|
||||
@@ -201,6 +227,6 @@ export const planEvolution = (baseline: WorkspaceRevision | null, candidate: Wor
|
||||
}
|
||||
}
|
||||
return { schemaVersion: 1, baselineDigest: baseline ? contentDigest(baseline) : null, candidateDigest, checkerVersion,
|
||||
runtimeActions, storageChanges, reviews, packageChecks: runtimeActions.filter((entry) => entry.candidate && entry.action !== "keep")
|
||||
runtimeActions, storageChanges, migrationRequired: storageChanges.filter(entry => entry.requiresMigration).map(entry => entry.id), reviews, packageChecks: runtimeActions.filter((entry) => entry.candidate && entry.action !== "keep")
|
||||
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })), blockers };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user