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
@@ -91,6 +91,36 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
} else {
registry = await ownedJson<Registry>(root, prefix + "quixos.scaffold.json");
catalog = await ownedJson<typeof catalog>(root, prefix + "quixos.migrations.json");
// package.qx is authoritative. The registry remembers implementation paths,
// not a second declaration list that can erase an author's new exports.
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const syntax = parseQx(authored);
if (syntax.diagnostics.length) throw new Error("Cannot refresh an invalid package.qx; fix the reported syntax first");
const declaration = [...walkSyntax(syntax.root)].find(node => node.kind === "packageResourceDecl");
if (!declaration) throw new Error("Expected a package declaration");
const text = (node: typeof declaration) => authored.slice(node.start, node.end);
const literals = declaration.children.filter(node => node.kind === "stringLiteral");
registry.name = text(declaration.children.find(node => node.kind === "identifier")!);
registry.id = JSON.parse(text(literals[0]));
registry.revision = JSON.parse(text(literals[1]));
const previousExports = registry.exports;
registry.exports = [];
for (const node of walkSyntax(declaration)) {
if (!["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind)) continue;
const name = safeName(text(node.children.find(child => child.kind === "identifier")!));
const id = JSON.parse(text(node.children.find(child => child.kind === "stringLiteral")!));
if (registry.exports.some(entry => entry.id === id || entry.name === name)) throw new Error("Duplicate package export name or ID");
const old = previousExports.find(entry => entry.id === id);
const file = old?.file ?? `src/impl/${name}.ts`;
if (!old) {
const exists = await fs.access(path.join(root, prefix, file)).then(() => true, () => false);
if (!exists) {
const derived = [...walkSyntax(node)].some(child => child.kind === "eventClause");
create(file, `import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation[${JSON.stringify(name)}] = ${derived ? '{kind: "derived", get: ' : ""}async (_context) => { throw new Error(${JSON.stringify(`Implement ${name}`)}); }${derived ? "}" : ""};\n`);
}
}
registry.exports.push({...old, name, id, file});
}
if (command !== "refresh") {
const name = safeName(spec.name);
if (!spec.id || registry.exports.some((entry) => entry.id === spec.id || entry.name === name)) throw new Error("New export requires a unique name and ID");