Format authored monorepo code with pinned language formatters
This commit is contained in:
@@ -4,12 +4,23 @@ import type { PersistentAttachment } from "./types.js";
|
||||
/** Portable storage shape: local owner/slot/projection identities are supplied
|
||||
* by the consuming workspace's explicit bindings, never baked into this hash. */
|
||||
export const migrationPortContract = (attachment: PersistentAttachment): unknown => {
|
||||
if (attachment.kind === "state") return {kind: "state", valueType: attachment.valueType, storagePolicy: attachment.storagePolicy,
|
||||
...(attachment.defaultValue === undefined ? {} : {defaultValue: attachment.defaultValue})};
|
||||
const endpoint = (value: typeof attachment.endpoints[number]) => ({constraint: value.constraint, cardinality: value.cardinality,
|
||||
ordered: value.ordered, onDelete: value.onDelete ?? "restrict", retainOther: value.retainOther ?? false,
|
||||
...(value.keyType ? {keyType: value.keyType} : {}), ...(value.publicTraversal ? {publicTraversal: true} : {})});
|
||||
return {kind: "edge", first: endpoint(attachment.endpoints[0]), second: endpoint(attachment.endpoints[1])};
|
||||
if (attachment.kind === "state")
|
||||
return {
|
||||
kind: "state",
|
||||
valueType: attachment.valueType,
|
||||
storagePolicy: attachment.storagePolicy,
|
||||
...(attachment.defaultValue === undefined ? {} : { defaultValue: attachment.defaultValue }),
|
||||
};
|
||||
const endpoint = (value: (typeof attachment.endpoints)[number]) => ({
|
||||
constraint: value.constraint,
|
||||
cardinality: value.cardinality,
|
||||
ordered: value.ordered,
|
||||
onDelete: value.onDelete ?? "restrict",
|
||||
retainOther: value.retainOther ?? false,
|
||||
...(value.keyType ? { keyType: value.keyType } : {}),
|
||||
...(value.publicTraversal ? { publicTraversal: true } : {}),
|
||||
});
|
||||
return { kind: "edge", first: endpoint(attachment.endpoints[0]), second: endpoint(attachment.endpoints[1]) };
|
||||
};
|
||||
|
||||
export type MigrationDeclaration = {
|
||||
@@ -19,7 +30,12 @@ export type MigrationDeclaration = {
|
||||
to: string;
|
||||
implementation: { exportId: string; file: string; digest: string };
|
||||
predecessors: string[];
|
||||
ports: { name: string; view: "old" | "new"; access: ("read" | "write" | "create" | "edge")[]; contractDigest: string }[];
|
||||
ports: {
|
||||
name: string;
|
||||
view: "old" | "new";
|
||||
access: ("read" | "write" | "create" | "edge")[];
|
||||
contractDigest: string;
|
||||
}[];
|
||||
preservesOldReaders?: boolean;
|
||||
preservesOldWriters?: boolean;
|
||||
};
|
||||
@@ -29,56 +45,99 @@ export type MigrationCatalog = {
|
||||
migrations: MigrationDeclaration[];
|
||||
};
|
||||
export const validateMigrationCatalog = (value: unknown, exportIds?: ReadonlySet<string>): MigrationCatalog => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Migration catalog must be an object");
|
||||
if (!value || typeof value !== "object" || Array.isArray(value))
|
||||
throw new Error("Migration catalog must be an object");
|
||||
const catalog = value as MigrationCatalog;
|
||||
if (catalog.schemaVersion !== 1 || !catalog.contracts || Array.isArray(catalog.contracts) || !Array.isArray(catalog.migrations)) throw new Error("Unsupported migration catalog");
|
||||
for (const [digest, contract] of Object.entries(catalog.contracts)) if (contentDigest(contract) !== digest) throw new Error(`Migration contract digest mismatch: ${digest}`);
|
||||
if (
|
||||
catalog.schemaVersion !== 1 ||
|
||||
!catalog.contracts ||
|
||||
Array.isArray(catalog.contracts) ||
|
||||
!Array.isArray(catalog.migrations)
|
||||
)
|
||||
throw new Error("Unsupported migration catalog");
|
||||
for (const [digest, contract] of Object.entries(catalog.contracts))
|
||||
if (contentDigest(contract) !== digest) throw new Error(`Migration contract digest mismatch: ${digest}`);
|
||||
const ids = new Set<string>();
|
||||
for (const migration of catalog.migrations) {
|
||||
if (!migration.id || !migration.scopeId || ids.has(migration.id)) throw new Error("Migration IDs must be stable and unique");
|
||||
if (!migration.id || !migration.scopeId || ids.has(migration.id))
|
||||
throw new Error("Migration IDs must be stable and unique");
|
||||
ids.add(migration.id);
|
||||
if (!catalog.contracts[migration.from] || !catalog.contracts[migration.to] || migration.from === migration.to) throw new Error(`Migration ${migration.id} requires distinct retained source and target contracts`);
|
||||
if (!migration.implementation?.exportId || !/^sha256:[0-9a-f]{64}$/.test(migration.implementation.digest)) throw new Error(`Migration ${migration.id} requires an exact implementation digest`);
|
||||
if (!migration.implementation.file || migration.implementation.file.startsWith("/") || migration.implementation.file.split(/[\\/]/).some((part) => !part || part === "." || part === "..")) throw new Error("Migration implementation must be a relative package file");
|
||||
if (exportIds && !exportIds.has(migration.implementation.exportId)) throw new Error(`Migration ${migration.id} refers to an undeclared package export`);
|
||||
if (!Array.isArray(migration.predecessors) || !Array.isArray(migration.ports)) throw new Error(`Migration ${migration.id} requires predecessors and ports`);
|
||||
if (new Set(migration.predecessors).size !== migration.predecessors.length) throw new Error(`Duplicate predecessor in ${migration.id}`);
|
||||
for (const promise of [migration.preservesOldReaders, migration.preservesOldWriters]) if (promise !== undefined && typeof promise !== "boolean") throw new Error("Migration compatibility promises must be booleans");
|
||||
if (!catalog.contracts[migration.from] || !catalog.contracts[migration.to] || migration.from === migration.to)
|
||||
throw new Error(`Migration ${migration.id} requires distinct retained source and target contracts`);
|
||||
if (!migration.implementation?.exportId || !/^sha256:[0-9a-f]{64}$/.test(migration.implementation.digest))
|
||||
throw new Error(`Migration ${migration.id} requires an exact implementation digest`);
|
||||
if (
|
||||
!migration.implementation.file ||
|
||||
migration.implementation.file.startsWith("/") ||
|
||||
migration.implementation.file.split(/[\\/]/).some((part) => !part || part === "." || part === "..")
|
||||
)
|
||||
throw new Error("Migration implementation must be a relative package file");
|
||||
if (exportIds && !exportIds.has(migration.implementation.exportId))
|
||||
throw new Error(`Migration ${migration.id} refers to an undeclared package export`);
|
||||
if (!Array.isArray(migration.predecessors) || !Array.isArray(migration.ports))
|
||||
throw new Error(`Migration ${migration.id} requires predecessors and ports`);
|
||||
if (new Set(migration.predecessors).size !== migration.predecessors.length)
|
||||
throw new Error(`Duplicate predecessor in ${migration.id}`);
|
||||
for (const promise of [migration.preservesOldReaders, migration.preservesOldWriters])
|
||||
if (promise !== undefined && typeof promise !== "boolean")
|
||||
throw new Error("Migration compatibility promises must be booleans");
|
||||
const ports = new Set<string>();
|
||||
for (const port of migration.ports) {
|
||||
if (!port.name || ports.has(port.name) || !["old", "new"].includes(port.view) || !Array.isArray(port.access) || !port.access.length
|
||||
|| port.access.some((access) => !["read", "write", "create", "edge"].includes(access)) || !catalog.contracts[port.contractDigest]) throw new Error(`Invalid migration port in ${migration.id}`);
|
||||
if (port.view === "old" && port.access.some((access) => access !== "read")) throw new Error("Old migration views are read-only");
|
||||
if (
|
||||
!port.name ||
|
||||
ports.has(port.name) ||
|
||||
!["old", "new"].includes(port.view) ||
|
||||
!Array.isArray(port.access) ||
|
||||
!port.access.length ||
|
||||
port.access.some((access) => !["read", "write", "create", "edge"].includes(access)) ||
|
||||
!catalog.contracts[port.contractDigest]
|
||||
)
|
||||
throw new Error(`Invalid migration port in ${migration.id}`);
|
||||
if (port.view === "old" && port.access.some((access) => access !== "read"))
|
||||
throw new Error("Old migration views are read-only");
|
||||
ports.add(port.name);
|
||||
}
|
||||
}
|
||||
for (const migration of catalog.migrations) for (const predecessor of migration.predecessors) if (!ids.has(predecessor)) throw new Error(`Missing retained predecessor ${predecessor}`);
|
||||
for (const migration of catalog.migrations)
|
||||
for (const predecessor of migration.predecessors)
|
||||
if (!ids.has(predecessor)) throw new Error(`Missing retained predecessor ${predecessor}`);
|
||||
// Catalogs are retained across releases. Reject impossible histories at
|
||||
// publication/check time, not only when someone tries to select a path.
|
||||
const remaining = new Map(catalog.migrations.map((entry) => [entry.id, new Set(entry.predecessors)]));
|
||||
const ready = [...remaining].filter(([, dependencies]) => dependencies.size === 0).map(([id]) => id);
|
||||
for (let index = 0; index < ready.length; index++) {
|
||||
remaining.delete(ready[index]);
|
||||
for (const [id, dependencies] of remaining) if (dependencies.delete(ready[index]) && dependencies.size === 0) ready.push(id);
|
||||
for (const [id, dependencies] of remaining)
|
||||
if (dependencies.delete(ready[index]) && dependencies.size === 0) ready.push(id);
|
||||
}
|
||||
if (remaining.size) throw new Error(`Cyclic migration predecessors: ${[...remaining.keys()].join(", ")}`);
|
||||
return catalog;
|
||||
};
|
||||
|
||||
export type MigrationSelection = {
|
||||
scopeId: string; from: string; to: string; path: string[];
|
||||
scopeId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
path: string[];
|
||||
bindings: Record<string, string>;
|
||||
};
|
||||
/** Explicit paths, not shortest-path guesses. Receipts identify code plus local scope mapping. */
|
||||
export const selectMigrationPath = (catalog: MigrationCatalog, selection: MigrationSelection, previousReceipts: ReadonlyMap<string, string> = new Map()) => {
|
||||
export const selectMigrationPath = (
|
||||
catalog: MigrationCatalog,
|
||||
selection: MigrationSelection,
|
||||
previousReceipts: ReadonlyMap<string, string> = new Map(),
|
||||
) => {
|
||||
validateMigrationCatalog(catalog);
|
||||
let current = selection.from;
|
||||
const seen = new Set<string>();
|
||||
const transitions = [];
|
||||
for (const id of selection.path) {
|
||||
const declaration = catalog.migrations.find((entry) => entry.id === id);
|
||||
if (!declaration || declaration.scopeId !== selection.scopeId || declaration.from !== current || seen.has(id)) throw new Error(`Invalid selected migration transition ${id}`);
|
||||
for (const predecessor of declaration.predecessors) if (!seen.has(predecessor) && !previousReceipts.has(predecessor)) throw new Error(`Unsatisfied predecessor ${predecessor}`);
|
||||
if (!declaration || declaration.scopeId !== selection.scopeId || declaration.from !== current || seen.has(id))
|
||||
throw new Error(`Invalid selected migration transition ${id}`);
|
||||
for (const predecessor of declaration.predecessors)
|
||||
if (!seen.has(predecessor) && !previousReceipts.has(predecessor))
|
||||
throw new Error(`Unsatisfied predecessor ${predecessor}`);
|
||||
const usedBindings: Record<string, string> = {};
|
||||
for (const port of declaration.ports) {
|
||||
if (!selection.bindings[port.name]) throw new Error(`Missing local migration binding ${port.name}`);
|
||||
@@ -86,7 +145,8 @@ export const selectMigrationPath = (catalog: MigrationCatalog, selection: Migrat
|
||||
}
|
||||
const digest = contentDigest({ declaration, bindings: usedBindings });
|
||||
const previous = previousReceipts.get(id);
|
||||
if (previous && previous !== digest) throw new Error(`Migration identity ${id} was previously used with different code or scope`);
|
||||
if (previous && previous !== digest)
|
||||
throw new Error(`Migration identity ${id} was previously used with different code or scope`);
|
||||
transitions.push({ declaration, bindings: usedBindings, digest, alreadyApplied: Boolean(previous) });
|
||||
seen.add(id);
|
||||
current = declaration.to;
|
||||
|
||||
Reference in New Issue
Block a user