Files
quixos-protocol/src/capability-language/execution-scaffold.ts
T

179 lines
9.6 KiB
TypeScript
Raw Normal View History

2026-09-21 00:44:47 -07:00
import fs from "node:fs/promises";
import path from "node:path";
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
import type { StructuralRequest } from "./structural-plan.js";
import { parseQx, walkSyntax } from "./source.js";
import { addImplementation } from "./implementation-edit.js";
import { formatQuixosLock } from "../resource-lock/index.js";
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
const q = (value: string) => JSON.stringify(value);
const nixString = (value: string) => q(value).replaceAll("${", "\\${");
export async function isExecutionScaffold(root: string, prefix: string): Promise<boolean> {
try {
return JSON.parse(await fs.readFile(path.join(root, prefix, "quixos.check.json"), "utf8")).backend === "execution";
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
throw error;
}
}
/** Insert ordinary authored code; declarations remain the only export inventory. */
export async function executionScaffold(
root: string,
command: string,
spec: ScaffoldRecipe,
prefix: string,
nixSource: (source: NonNullable<ScaffoldRecipe["tools"]>["helpers"]) => string,
): Promise<StructuralRequest> {
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id)
throw new Error("Execution scaffold requires a simple name and an ID");
if (spec.initialFiles && command !== "package") throw new Error("Initial files are only for new packages");
2026-09-21 00:44:47 -07:00
const files: StructuralRequest["files"] = [];
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
if (command === "package") {
if (!spec.tools || !spec.revision) throw new Error("Execution package requires exact tool sources and a revision");
create("package.qx", `package ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`);
create(
"quixos.lock",
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
);
create("quixos.check.json", json({ backend: "execution" }));
create("quixos.components.json", "{}\n");
2026-09-21 00:44:47 -07:00
create("package.json", json({ name: `@quixos/${spec.name.toLowerCase()}`, private: true, type: "module" }));
create(".gitignore", "node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n");
for (const lane of ["browser", "server", "remote"])
create(
`src/${lane}.ts`,
`import { createRegistry } from "./gen/${lane === "remote" ? "remote" : "qx"}.js";\nexport const registry = createRegistry({});\n`,
);
create(
"flake.nix",
`{
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; };
outputs = { nixpkgs, flake-utils, helpers, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; };
in {
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId, react ? null }:
(import (helpers + "/artifacts.nix")) {
2026-09-21 00:44:47 -07:00
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
protocol = generator;
src = ./.;
components = let entries = builtins.fromJSON (builtins.readFile ./quixos.components.json); in
if entries == {} then null else
if react == null then throw "Component entries require the workspace-selected React runtime" else
{ inherit (react) sdk runtime; inherit entries; };
2026-09-21 00:44:47 -07:00
portable = {
entry = "src/server.ts";
entries.browser = "src/browser.ts";
bindingOutput = "src/gen/qx.ts";
};
remote = { entry = "src/remote.ts"; bindingOutput = "src/gen/remote.ts"; };
};
});
}
`,
);
create(
"README.md",
`# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The coordinator supplies the checked workspace plan\nand runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from the workspace root to compile exact source and build\nchecked artifacts. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`,
2026-09-21 00:44:47 -07:00
);
} else if (command === "function") {
if (!spec.declaration)
throw new Error("Execution functions require a declaration with explicit execution effect and targets");
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${spec.declaration} }`;
const parsed = parseQx(wrapped);
const pkg = parsed.root.children.find((node) => node.kind === "packageResourceDecl");
const exports = [...walkSyntax(parsed.root)].filter((node) =>
["packageFunctionExport", "packageOperationExport"].includes(node.kind),
);
const declaration = exports[0];
if (
parsed.diagnostics.length ||
exports.length !== 1 ||
!declaration ||
parsed.root.children.length !== 1 ||
pkg?.children.filter((node) => node.kind === "packageExport").length !== 1 ||
pkg?.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
)
throw new Error("Expected one function or operation export with an execution contract");
const text = (node: { start: number; end: number }) => wrapped.slice(node.start, node.end);
const name = declaration.children.find((node) => node.kind === "identifier");
const id = declaration.children.find((node) => node.kind === "stringLiteral");
const execution = declaration.children.find((node) => node.kind === "executionClause");
if (!name || text(name) !== spec.name || !id || JSON.parse(text(id)) !== spec.id || !execution)
throw new Error("Declaration name/ID must match; an explicit execution contract is required");
const tokens = parsed.tokens
.filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end)
.map((token) => token.text);
const remote = tokens[1] === "remote";
const targets = remote
? ["remote"]
: [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text);
if (
!targets.length ||
new Set(targets).size !== targets.length ||
targets.some((target) => !(remote ? ["remote"] : ["browser", "server"]).includes(target))
)
throw new Error("Execution scaffold supports browser/server portable targets and remote service handlers");
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
const syntax = parseQx(authored);
const packageNode = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
if (syntax.diagnostics.length || !packageNode) throw new Error("Fix package.qx before adding a handler");
for (const node of [...walkSyntax(packageNode)].filter((node) =>
["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind),
)) {
const identifier = node.children.find((child) => child.kind === "identifier");
const literal = node.children.find((child) => child.kind === "stringLiteral");
if (
(identifier && authored.slice(identifier.start, identifier.end) === spec.name) ||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
)
throw new Error("New export requires a unique name and ID");
}
const packageID = packageNode.children.find((node) => node.kind === "stringLiteral")!;
files.push({
file: prefix + "package.qx",
edits: [
{
operation: "append",
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(packageID.start, packageID.end)) },
source: spec.declaration,
},
],
});
create(
`src/impl/${spec.name}.ts`,
`import type { ${remote ? "Remote" : "Portable"}Implementation } from "../gen/${remote ? "remote" : "qx"}.js";\nexport const handler: ${remote ? "Remote" : "Portable"}Implementation[${q(spec.name)}] = async (_context) => { throw new Error(${q("Implement " + spec.name)}); };\n`,
);
for (const target of targets) {
const file = prefix + `src/${target}.ts`;
const before = await fs.readFile(path.join(root, file), "utf8");
files.push({
file,
expected: before,
replace: addImplementation(before, "createRegistry", spec.name, `./impl/${spec.name}.js`),
});
}
} else
throw new Error("Execution packages use ordinary factory transactions; migration scaffolding is not supported");
if (spec.initialFiles) {
for (const [file, content] of Object.entries(spec.initialFiles)) {
if (
typeof content !== "string" ||
!/^(?:package\.qx|quixos\.components\.json|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test(
file,
) ||
file.split("/").some((part) => part === ".." || part === "." || part === "gen")
)
throw new Error("Not an initial authored execution file: " + file);
const previous = files.findIndex((entry) => entry.file === prefix + file);
if (previous >= 0) files.splice(previous, 1);
create(file, content);
}
}
2026-09-21 00:44:47 -07:00
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
}