Add local-first execution authoring support

This commit is contained in:
Timothy J. Aveni
2026-09-21 00:57:50 -07:00
parent b4ac9700c6
commit 7652b4c0b9
6 changed files with 269 additions and 2 deletions
@@ -0,0 +1,158 @@
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) throw new Error("Edit execution source after scaffolding; initialFiles is not supported");
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("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 }:
(import (helpers + "/portable-artifacts.nix")) {
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
protocol = generator;
src = ./.;
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\nThe execution scaffold currently requires the execution artifact build path;\nlegacy checkedServer verification and activation do not install these artifacts.\nA source-only structural check is not runtime qualification.\n`,
);
} 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");
return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files };
}
@@ -7,7 +7,7 @@ const node = (value: unknown): value is Node =>
* Ambiguous/custom wiring is left alone with an actionable error. */ * Ambiguous/custom wiring is left alone with an actionable error. */
export function addImplementation( export function addImplementation(
text: string, text: string,
factory: "createRuntime" | "serveMigration", factory: "createRuntime" | "serveMigration" | "createRegistry",
key: string, key: string,
importPath: string, importPath: string,
migrationOnly = false, migrationOnly = false,
+9 -1
View File
@@ -11,6 +11,7 @@ import { parseQx, walkSyntax } from "./source.js";
import type { StructuralRequest } from "./structural-plan.js"; import type { StructuralRequest } from "./structural-plan.js";
import { addImplementation } from "./implementation-edit.js"; import { addImplementation } from "./implementation-edit.js";
import { reactPlatformTypes } from "../bindings/react-platform.js"; import { reactPlatformTypes } from "../bindings/react-platform.js";
import { executionScaffold, isExecutionScaffold } from "./execution-scaffold.js";
type Source = { repository: string; commit: string }; type Source = { repository: string; commit: string };
type PackageEditModel = { type PackageEditModel = {
@@ -21,7 +22,7 @@ type PackageEditModel = {
exports: { name: string; id: string; file: string; migration?: boolean }[]; exports: { name: string; id: string; file: string; migration?: boolean }[];
}; };
export type ScaffoldRecipe = { export type ScaffoldRecipe = {
template?: "typescript" | "typescript-react"; template?: "typescript" | "typescript-react" | "typescript-execution";
source: Source; source: Source;
directory?: string; directory?: string;
name?: string; name?: string;
@@ -81,6 +82,13 @@ export const scaffoldRecipe = async (
if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory)) if (spec.directory && !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(spec.directory))
throw new Error("Scaffold directory must be contained"); throw new Error("Scaffold directory must be contained");
const prefix = spec.directory ? `${spec.directory}/` : ""; const prefix = spec.directory ? `${spec.directory}/` : "";
if (
spec.template === "typescript-execution" ||
(command !== "package" && (await isExecutionScaffold(root, prefix)))
) {
if (spec.tools) Object.values(spec.tools).forEach(source);
return executionScaffold(root, command, spec, prefix, nixSource);
}
const files: StructuralRequest["files"] = []; const files: StructuralRequest["files"] = [];
const create = (file: string, content: string) => files.push({ file: prefix + file, create: content }); const create = (file: string, content: string) => files.push({ file: prefix + file, create: content });
const generated = (file: string, content: string) => files.push({ file: prefix + file, generated: content }); const generated = (file: string, content: string) => files.push({ file: prefix + file, generated: content });
+4
View File
@@ -131,3 +131,7 @@ const compoundCrdt = await compoundCrdtPlan();
await mkdir("dist/portable-reader/compound-crdt", { recursive: true }); await mkdir("dist/portable-reader/compound-crdt", { recursive: true });
await writeFile("dist/portable-reader/compound-crdt/plan.json", JSON.stringify(compoundCrdt)); await writeFile("dist/portable-reader/compound-crdt/plan.json", JSON.stringify(compoundCrdt));
await writeFile("dist/portable-reader/compound-crdt/kernel.json", JSON.stringify(compileKernelProgram(compoundCrdt))); await writeFile("dist/portable-reader/compound-crdt/kernel.json", JSON.stringify(compileKernelProgram(compoundCrdt)));
const { emitExecutionScaffold } = await import("./execution-scaffold-fixture.js");
const executionScaffold = await emitExecutionScaffold("dist/portable-reader/execution-scaffold/source");
await writeFile("dist/portable-reader/execution-scaffold/plan.json", JSON.stringify(executionScaffold));
+53
View File
@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js";
import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
import { compileExecutionPlan } from "../src/execution-world/plan.js";
/** Exercise the authoring insertion path, then let the Nix gate build its actual flake output. */
export async function emitExecutionScaffold(root: string) {
await fs.mkdir(root, { recursive: true });
execFileSync("git", ["-C", root, "init"], { stdio: "ignore" });
const source = { repository: "https://example.test/scaffold.git", commit: "a".repeat(40) };
const apply = async (
command: Parameters<typeof scaffoldRecipe>[1],
spec: Partial<Parameters<typeof scaffoldRecipe>[2]>,
) => applyStructure(await planStructure(root, await scaffoldRecipe(root, command, { source, ...spec })));
await apply("package", {
name: "Scaffold",
id: "package:scaffold",
revision: "package:scaffold@1",
template: "typescript-execution",
tools: { quixos: source, protocol: source, helpers: source, sdk: source },
});
for (const [name, execution] of [
["echo", "snapshot targets [browser, server]"],
["serverEcho", "snapshot targets [server]"],
["remoteEcho", "remote"],
]) {
await apply("function", {
name,
id: "scaffold:" + name,
declaration: `function ${name} id "scaffold:${name}" : string -> string execution ${execution};`,
});
const file = path.join(root, `src/impl/${name}.ts`);
const placeholder = await fs.readFile(file, "utf8");
await fs.writeFile(file, placeholder.replace(/async \(_context\) => \{.*\};/, "async ({ input }) => input;"));
}
const pkg = compileCapabilityResourceSource(await fs.readFile(path.join(root, "package.qx"), "utf8"), { source });
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
const workspace = compileCapabilitySource(
`workspace W id "scaffold-workspace" revision "scaffold-workspace@1" commit "${source.commit}" { import package Scaffold; }`,
"workspace.qx",
{
packages: new Map([["Scaffold", pkg.resource.revision]]),
},
);
assert.ok(workspace.ok, JSON.stringify(workspace.diagnostics));
await fs.rm(path.join(root, ".quixos"), { recursive: true, force: true });
await fs.rm(path.join(root, ".git"), { recursive: true, force: true });
return compileExecutionPlan(workspace.workspace);
}
+44
View File
@@ -122,3 +122,47 @@ test("imperative scaffolds preserve authored wiring; migration sealing is explic
], ],
}); });
}); });
test("execution scaffolds route typed handlers by declared effect and target without legacy lifecycle files", async (context) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-execution-recipe-"));
context.after(() => fs.rm(root, { recursive: true, force: true }));
await execFile("git", ["-C", root, "init"]);
const source = { repository: "https://example.test/execution.git", commit: "a".repeat(40) };
const apply = async (
command: Parameters<typeof scaffoldRecipe>[1],
spec: Partial<Parameters<typeof scaffoldRecipe>[2]>,
) => applyStructure(await planStructure(root, await scaffoldRecipe(root, command, { source, ...spec })));
await apply("package", {
name: "Execution",
id: "package:execution",
revision: "package:execution@1",
template: "typescript-execution",
tools: { quixos: source, protocol: source, helpers: source, sdk: source },
});
for (const file of ["quixos.migrations.json", "src/migrate.ts", "descriptor.quixos-package.txtpb"])
await assert.rejects(fs.access(path.join(root, file)));
await fs.appendFile(path.join(root, "src/server.ts"), "\n// Preserve authored code\n");
const add = (name: string, execution: string) =>
apply("function", {
name,
id: "export:" + name,
declaration: `function ${name} id "export:${name}" : unit -> unit execution ${execution};`,
});
await add("both", "transaction targets [browser, server]");
await add("serverOnly", "snapshot targets [server]");
await add("asyncWork", "remote");
const read = (file: string) => fs.readFile(path.join(root, file), "utf8");
assert.match(await read("src/browser.ts"), /"both":/);
assert.doesNotMatch(await read("src/browser.ts"), /serverOnly|asyncWork/);
assert.match(await read("src/server.ts"), /"serverOnly":/);
assert.match(await read("src/server.ts"), /Preserve authored code/);
assert.match(await read("src/remote.ts"), /"asyncWork":/);
assert.match(await read("src/impl/asyncWork.ts"), /RemoteImplementation/);
assert.match(await read("src/impl/both.ts"), /PortableImplementation/);
assert.match(await read("flake.nix"), /checkedArtifacts/);
assert.match(await read("flake.nix"), /entries.browser/);
await assert.rejects(add("both", "transaction targets [browser, server]"), /unique/);
await assert.rejects(add("invalid", "snapshot targets [remote]"), /browser\/server/);
await assert.rejects(apply("function", { name: "missing", id: "export:missing" }), /explicit execution/);
await assert.rejects(apply("migration", { name: "migration", id: "export:migration" }), /not supported/);
});