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

209 lines
12 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";
import { javascriptTarget } from "../execution-world/native-backends.js";
2026-09-21 00:44:47 -07:00
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",
`symbol JavaScript id ${q(javascriptTarget)};\npackage ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`,
);
2026-09-21 00:44:47 -07:00
create(
"quixos.lock",
formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }),
);
create("quixos.check.json", json({ backend: "execution" }));
create("components.nix", "{\n}\n");
create(
"package.json",
json({
name: `@quixos/${spec.name.toLowerCase()}`,
private: true,
type: "module",
packageManager: "yarn@4.18.0",
scripts: { typecheck: "qx-workspace check" },
}),
);
create(
".gitignore",
"node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n.yarn/cache/\n.yarn/install-state.gz\n.yarn/unplugged/\n",
);
create(
".yarnrc.yml",
'enableScripts: true\n\ngenerateDefaultNix: false\n\nindividualNixPackaging: true\n\nnodeLinker: node-modules\n\nsupportedArchitectures:\n os:\n - current\n - linux\n cpu:\n - current\n - x64\n - arm64\n libc:\n - current\n - glibc\n\nplugins:\n - checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b\n path: .yarn/plugins/yarn-plugin-nixify.cjs\n spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"\n',
);
2026-09-21 00:44:47 -07:00
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; };
camino = import (helpers + "/quixos-package-helpers.nix");
2026-09-21 00:44:47 -07:00
in {
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
quixosPackages.checkedArtifacts = context:
let
pkgs = camino.packageSet { inherit nixpkgs system context; };
contract = builtins.fromJSON (builtins.readFile context.buildContract);
package = contract.definition.package;
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
modules = import ./components.nix;
dependencies = if builtins.pathExists ./yarn-project.nix
then camino.mkCaminoYarnDependencies { inherit pkgs; src = ./.; } else null;
in camino.assembleCaminoPackage {
inherit pkgs context;
artifacts =
pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage {
inherit pkgs context dependencies; src = ./.;
entry = "src/server.ts"; entries.browser = "src/browser.ts";
targets = pkgs.lib.unique (map (target: if target == "target:org.quixos.execution.javascript@1" then "browser" else if target == "server" then "server" else throw "Unsupported TypeScript backend target") (pkgs.lib.concatMap (entry: entry.execution.targets) portable));
}) ++ pkgs.lib.optional (remote != []) (camino.mkCaminoTypeScriptService {
inherit pkgs context dependencies; src = ./.; entry = "src/remote.ts";
}) ++ pkgs.lib.optional (modules != {}) (camino.mkCaminoReactPackage {
inherit pkgs context dependencies modules; src = ./.;
2026-09-21 00:44:47 -07:00
});
};
});
2026-09-21 00:44:47 -07:00
}
`,
);
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 Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. 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 declaredTargets = remote
2026-09-21 00:44:47 -07:00
? ["remote"]
: [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text);
const targets = declaredTargets.length ? declaredTargets : ["server"];
2026-09-21 00:44:47 -07:00
if (
!targets.length ||
new Set(targets).size !== targets.length ||
targets.some((target) => !(remote ? ["remote"] : ["JavaScript", "server"]).includes(target))
2026-09-21 00:44:47 -07:00
)
throw new Error("This TypeScript scaffold supports the JavaScript target, server, and remote service handlers");
2026-09-21 00:44:47 -07:00
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 === "JavaScript" ? "browser" : target}.ts`;
2026-09-21 00:44:47 -07:00
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|(?:package|components|flake|yarn-project)\.nix|package\.json|yarn\.lock|\.yarnrc\.yml|\.gitignore|\.yarn\/plugins\/[A-Za-z0-9_.-]+\.cjs|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 };
}