Compare commits

...

20 Commits

Author SHA1 Message Date
Quixos Subtree Publisher e2747066ca Publish quixos-nix-helpers from 1de28b236de076d239752b77553f833dfbdb5b43 2026-09-16 23:49:09 +00:00
Timothy J. Aveni 0c3c9c6ad8 Revert generated-file renaming; retain formatter exclusions 2026-09-15 20:13:10 -07:00
Timothy J. Aveni b3d31b37b3 Mark first-party generated files with .gen; preserve third-party conventions 2026-09-15 17:57:46 -07:00
Timothy J. Aveni 6f3814587f Format authored monorepo code with pinned language formatters 2026-09-15 15:41:24 -07:00
Timothy J. Aveni 1c09bf43c2 Simplify workspace authoring and isolate managed component styles
Use imperative scaffolds without a registry/refresh lifecycle; preserve authored runtime wiring and generate checked descriptors. Consolidate binding options, embed checked TSX/CSS source, queue mutable convergence while allowing parallel immutable checks, and preload real CLI observations. Add Shadow DOM boundaries with scoped overlays and composed-event handling. Update template/toolchain pins and document VM-free authoring, browser, and stateful acceptance; no deployment or default-template selection.
2026-09-14 20:55:32 -07:00
Quixos Subtree Publisher 6657af7ca0 Publish quixos-nix-helpers from 8484d99fcf78a49341a17597bc62211e5f1807dd 2026-09-15 03:49:38 +00:00
Timothy J. Aveni e3080467c9 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:49:38 -07:00
Quixos Subtree Publisher e84b62f4d6 Publish quixos-nix-helpers from fc13e9b8411c2210b12db5e558e2b4fa7785505e 2026-09-15 03:42:37 +00:00
Timothy J. Aveni 06c668b587 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:42:37 -07:00
Timothy J. Aveni 56fa26acc8 Unify workspace authoring, verification and scaffolding workflows
Use exact jj snapshots and one candidate-bound Nix builder for incremental checks, template validation and activation. Keep provenance internal and separate recovery checkpoint failures from local command success.

Provision workspace-scoped managed package/interface repositories with recoverable Central effects. Add TypeScript/React presets, function and dependency commands, and scaffold enrollment for all TODO packages. Install authoring guides and controlled Codex sandbox rules.

Invalidate module resolutions across cutover, including in-flight races, and content-address host platform entries. Strengthen domain-model and verification instructions.

Validated real jj/Nix authoring, React/Slate dependency installation, bottom-up local Git publication, packaged CLI tests, PostgreSQL recovery/auth tests, Web Studio tests and host configuration. Public protocol/helpers and the validated 19-resource TODO template are published. Retained the approved exact private baseline and updated the installation's default template pin to 68d54f0d52be433ebf60bdc1faf7646c57f90307. Master and live deployments remain unchanged. See docs/WORKSPACE_AUTHORING_PROGRESS.md.
2026-09-13 23:06:38 -07:00
Quixos Subtree Publisher cf7945abb6 Publish quixos-nix-helpers from 7fd5e15105cef21d2b1c3da860c53c5033f66256 2026-09-14 05:46:08 +00:00
Timothy J. Aveni 74b6ff2ace Unify workspace authoring, verification and scaffolding workflows 2026-09-13 22:46:08 -07:00
Timothy J. Aveni 5861e91298 Migrate TODO implementations to generated candidate bindings 2026-09-13 20:28:50 -07:00
Quixos Subtree Publisher d109410617 Publish quixos-nix-helpers from 22bb3d02264980d74de65c34bbbcb81764dc65c0 2026-09-14 03:28:50 +00:00
Quixos Subtree Publisher 8b2a224698 Publish quixos-nix-helpers from e25eee6ce4f13702b2454a9354bc79a29eb2e4a1 2026-09-14 03:01:53 +00:00
Timothy J. Aveni 80b65b6f0c Migrate TODO implementations to generated candidate bindings 2026-09-13 20:01:53 -07:00
Timothy J. Aveni 32652d5279 Implement workspace evolution, migrations, and runtime continuity
Enable evolution by default for source-backed workspaces. Add stable
conformance ownership, semantic-major review, candidate typechecking,
and durable fenced cutover with explicit migrations and forward recovery.

Independently supervise package runtimes so unchanged resource owners keep
their processes and connections across cutover. Add scoped invocation
authority, resource sessions, and typed callback rebinding.

Wire opaque object references through generated bindings and RPCs. Add
canonical relationship sets, keyed maps, and ordered lists with scoped
transactional mutations, revision checks, and inverse consistency. Support
planned cascade deletion, protection, tombstones, and lifecycle foundations.

Add journaled structural edits, package/function/migration scaffolding,
managed repository creation, and resumable bottom-up dependency pin
publication. Document lifetime boundaries, revision pinning, prototype
compatibility policy, commands, and deferred work.

Validate with 210 tests, user-systemd process/connection continuity,
generated-package TypeScript checks, and Nix host/protocol checks.
TTL handoff, physical reclamation, general multi-step migrations, and
root-systemd migration isolation acceptance remain deferred.
2026-09-10 18:29:25 -07:00
Timothy J. Aveni 7e4fb60155 Generate typed QX bindings and add source editing tools 2026-09-08 15:42:20 -07:00
Quixos Subtree Publisher b6cd2f3cd7 Publish quixos-nix-helpers from b384206b9c01a9ac50030a583d028b71291bc8c5 2026-09-06 07:07:06 +00:00
timothy 2f66f25153 Build workspace agent, capability graph, and versioned cutovers 2026-09-06 00:07:06 -07:00
4 changed files with 613 additions and 334 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "fa363fa2f4e83d0996c1229ab1ecf9e0541d5d4f", "sourceCommit": "1de28b236de076d239752b77553f833dfbdb5b43",
"sourcePath": "quixos-instance/quixos-nix-helpers", "sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers", "exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
+69
View File
@@ -0,0 +1,69 @@
import { createRequire } from "node:module";
import path from "node:path";
// Resolve build dependencies from the package's locked node_modules, not from
// the helper's own Nix-store location. This program runs only during the build.
const require = createRequire(path.join(process.cwd(), "package.json"));
const { build } = require("esbuild");
const options = JSON.parse(process.argv[2]);
const platform = new Map([
["react", "/__quixos/platform/react/v18.mjs"],
["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],
["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],
["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],
]);
await build({
...options,
plugins: [
{
name: "quixos-browser-source",
setup(api) {
api.onResolve({ filter: /\?browser-source$/ }, async ({ path: specifier, resolveDir }) => {
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
throw new Error("Browser source imports must name a relative compiled module");
const resolved = await api.resolve(specifier.slice(0, -"?browser-source".length), {
resolveDir,
kind: "import-statement",
});
if (resolved.errors.length) return { errors: resolved.errors };
return { path: resolved.path, namespace: "browser-source" };
});
api.onLoad({ filter: /.*/, namespace: "browser-source" }, async ({ path: entry }) => {
const browser = await build({
entryPoints: [entry],
bundle: true,
write: false,
outfile: "component.mjs",
format: "esm",
platform: "browser",
target: "es2022",
metafile: true,
plugins: [
{
name: "quixos-platform",
setup(browserApi) {
browserApi.onResolve({ filter: /.*/ }, ({ path: name }) =>
platform.has(name) ? { path: platform.get(name), external: true } : undefined,
);
},
},
],
});
const js = browser.outputFiles.find((file) => file.path.endsWith(".mjs"));
if (!js) throw new Error("Browser source build produced no JavaScript");
const css = browser.outputFiles.find((file) => file.path.endsWith(".css"));
if (css && Object.values(browser.metafile.outputs).some((output) => output.exports?.includes("styles")))
throw new Error("Use either imported CSS or an explicit styles export, not both");
if (browser.outputFiles.some((file) => !file.path.endsWith(".mjs") && !file.path.endsWith(".css")))
throw new Error("Browser assets must be embedded, not emitted as unserved files");
const source = js.text + (css ? `\nexport const styles = ${JSON.stringify(css.text)};\n` : "");
return {
contents: `export default ${JSON.stringify(source)};`,
loader: "js",
watchFiles: Object.keys(browser.metafile.inputs),
};
});
},
},
],
});
+40
View File
@@ -0,0 +1,40 @@
import fs from "node:fs";
import crypto from "node:crypto";
const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2);
if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output)
throw new Error("Missing candidate check receipt inputs");
const canonical = (value) =>
Array.isArray(value)
? value.map(canonical)
: value && typeof value === "object"
? Object.fromEntries(
Object.entries(value)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, entry]) => [key, canonical(entry)]),
)
: value;
const hash = (value) =>
`sha256:${crypto
.createHash("sha256")
.update(JSON.stringify(canonical(value)))
.digest("hex")}`;
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId))
throw new Error("Checked binding schema lacks the package");
const generated = fs.readFileSync(bindingOutput, "utf8");
if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8"))
throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate");
const receipt = {
schemaVersion: 1,
packageRevisionId,
success: true,
bindingSchema: schema,
bindingSchemaDigest: hash(schema),
generatedDigest: hash(generated),
checkerDigest: hash({
generatorPath,
compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")),
lock: fs.readFileSync("yarn.lock", "utf8"),
}),
};
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
+502 -332
View File
@@ -19,23 +19,25 @@ let
schemaMainPathDefault = "dist/quixos-package-schema.js"; schemaMainPathDefault = "dist/quixos-package-schema.js";
schemaPackageName = schemaPackageJson.name or "schema"; schemaPackageName = schemaPackageJson.name or "schema";
selfSchemaName = selfSchemaName =
if pkgs.lib.hasInfix "/" schemaPackageName if pkgs.lib.hasInfix "/" schemaPackageName then
then pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName) pkgs.lib.last (pkgs.lib.splitString "/" schemaPackageName)
else schemaPackageName; else
schemaPackageName;
flakeText = flakeText = builtins.replaceStrings [ "\n" "\r" "\t" ] [ " " " " " " ] (
builtins.replaceStrings builtins.readFile "${flakeRoot}/flake.nix"
[ "\n" "\r" "\t" ] );
[ " " " " " " ]
(builtins.readFile "${flakeRoot}/flake.nix");
getInputUrl = inputName: getInputUrl =
inputName:
let let
pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*"; pattern1 = ".*" + inputName + "[[:space:]]*\\.url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
pattern2 = ".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*"; pattern2 =
".*" + inputName + "[[:space:]]*=[[:space:]]*\\{[^}]*url[[:space:]]*=[[:space:]]*\"([^\"]+)\".*";
match1 = builtins.match pattern1 flakeText; match1 = builtins.match pattern1 flakeText;
match2 = if match1 != null then match1 else builtins.match pattern2 flakeText; match2 = if match1 != null then match1 else builtins.match pattern2 flakeText;
in if match2 == null then null else builtins.elemAt match2 0; in
if match2 == null then null else builtins.elemAt match2 0;
schemaBase = schemaBase =
(pkgs.callPackage "${schemaDir}/yarn-project.nix" { (pkgs.callPackage "${schemaDir}/yarn-project.nix" {
@@ -65,68 +67,61 @@ let
''; '';
}); });
schemaInputs = schemaInputs = pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
pkgs.lib.filterAttrs (name: _: pkgs.lib.hasPrefix schemaPrefix name) inputs;
decodeSchemaInputName = encodedName: decodeSchemaInputName = encodedName: builtins.replaceStrings [ "__" ] [ "." ] encodedName;
builtins.replaceStrings [ "__" ] [ "." ] encodedName;
schemaPackagesFromInputs = schemaPackagesFromInputs = pkgs.lib.mapAttrs' (
pkgs.lib.mapAttrs' name: flake:
(name: flake: let
let schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
schemaName = in
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); {
in name = schemaName;
{ value = flake.packages.${system}.schema;
name = schemaName; }
value = flake.packages.${system}.schema; ) schemaInputs;
})
schemaInputs;
schemaPackages = schemaPackagesFromInputs // { "${selfSchemaName}" = schema; }; schemaPackages = schemaPackagesFromInputs // {
"${selfSchemaName}" = schema;
};
schemaMetaByName = schemaMetaByName = pkgs.lib.mapAttrs' (
pkgs.lib.mapAttrs' name: flake:
(name: flake: let
let schemaName = decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name);
schemaName = sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { };
decodeSchemaInputName (pkgs.lib.removePrefix schemaPrefix name); sourceRev = sourceInfo.rev or null;
sourceInfo = if flake ? sourceInfo then flake.sourceInfo else { }; inputUrl = getInputUrl name;
sourceRev = sourceInfo.rev or null; urlBase = if inputUrl == null then null else builtins.head (pkgs.lib.splitString "?" inputUrl);
inputUrl = getInputUrl name; isDisallowed =
urlBase = inputUrl == null
if inputUrl == null || pkgs.lib.hasPrefix "path:" inputUrl
then null || pkgs.lib.hasPrefix "file:" inputUrl
else builtins.head (pkgs.lib.splitString "?" inputUrl); || pkgs.lib.hasPrefix "git+file:" inputUrl;
isDisallowed = flakeRef =
inputUrl == null if isDisallowed || sourceRev == null || urlBase == null then
|| pkgs.lib.hasPrefix "path:" inputUrl null
|| pkgs.lib.hasPrefix "file:" inputUrl else
|| pkgs.lib.hasPrefix "git+file:" inputUrl; urlBase + "?rev=" + sourceRev;
flakeRef = meta = {
if isDisallowed || sourceRev == null || urlBase == null name = "@quixos-package-schemas/${schemaName}";
then null type = sourceInfo.type or null;
else urlBase + "?rev=" + sourceRev; url = inputUrl;
meta = rev = sourceRev;
{ flakeRef = flakeRef;
name = "@quixos-package-schemas/${schemaName}"; };
type = sourceInfo.type or null; in
url = inputUrl; if flakeRef == null then
rev = sourceRev; throw "Schema input ${schemaName} must be a git flake with url+rev"
flakeRef = flakeRef; else
}; {
in name = schemaName;
if flakeRef == null value = meta;
then throw "Schema input ${schemaName} must be a git flake with url+rev" }
else { ) schemaInputs;
name = schemaName;
value = meta;
})
schemaInputs;
schemaMetaJsonByName = schemaMetaJsonByName = pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
pkgs.lib.mapAttrs (_: meta: builtins.toJSON meta) schemaMetaByName;
schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" ( schemaExtensionsBlock = pkgs.lib.concatStringsSep "\n" (
[ [
@@ -151,12 +146,11 @@ let
); );
schemaInstallCommands = pkgs.lib.concatStringsSep "\n" ( schemaInstallCommands = pkgs.lib.concatStringsSep "\n" (
pkgs.lib.mapAttrsToList (name: drv: pkgs.lib.mapAttrsToList (
name: drv:
let let
metaJson = metaJson =
if pkgs.lib.hasAttr name schemaMetaJsonByName if pkgs.lib.hasAttr name schemaMetaJsonByName then schemaMetaJsonByName.${name} else null;
then schemaMetaJsonByName.${name}
else null;
in in
'' ''
schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)" schema_pkg="$(ls -d ${drv}/libexec/* | head -n1)"
@@ -166,46 +160,47 @@ let
chmod -R u+w "$dest" chmod -R u+w "$dest"
'' ''
+ ( + (
if metaJson != null if metaJson != null then
then '' ''
schema_main="${schemaMainPathDefault}" schema_main="${schemaMainPathDefault}"
if [ -f "$dest/$schema_main" ]; then if [ -f "$dest/$schema_main" ]; then
block_tmp="$(mktemp -p "$dest")" block_tmp="$(mktemp -p "$dest")"
tmp_out="$(mktemp -p "$dest")" tmp_out="$(mktemp -p "$dest")"
cat > "$block_tmp" <<'EOF' cat > "$block_tmp" <<'EOF'
packageSchema.__quixos = ${metaJson}; packageSchema.__quixos = ${metaJson};
EOF EOF
chmod u+w "$dest/$schema_main" chmod u+w "$dest/$schema_main"
if grep -q '^export default ' "$dest/$schema_main"; then if grep -q '^export default ' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" ' awk -v blockFile="$block_tmp" '
BEGIN { BEGIN {
while ((getline line < blockFile) > 0) { while ((getline line < blockFile) > 0) {
block = block line "\n" block = block line "\n"
} }
} }
!inserted && /^export default / { printf "%s", block; inserted=1 } !inserted && /^export default / { printf "%s", block; inserted=1 }
{ print } { print }
' "$dest/$schema_main" > "$tmp_out" ' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main" cat "$tmp_out" > "$dest/$schema_main"
elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then elif grep -q '^//# sourceMappingURL=' "$dest/$schema_main"; then
awk -v blockFile="$block_tmp" ' awk -v blockFile="$block_tmp" '
BEGIN { BEGIN {
while ((getline line < blockFile) > 0) { while ((getline line < blockFile) > 0) {
block = block line "\n" block = block line "\n"
} }
} }
/^\/\/# sourceMappingURL=/ { printf "%s", block; print; next } /^\/\/# sourceMappingURL=/ { printf "%s", block; print; next }
{ print } { print }
' "$dest/$schema_main" > "$tmp_out" ' "$dest/$schema_main" > "$tmp_out"
cat "$tmp_out" > "$dest/$schema_main" cat "$tmp_out" > "$dest/$schema_main"
else else
printf '\n' >> "$dest/$schema_main" printf '\n' >> "$dest/$schema_main"
cat "$block_tmp" >> "$dest/$schema_main" cat "$block_tmp" >> "$dest/$schema_main"
fi fi
rm -f "$block_tmp" "$tmp_out" rm -f "$block_tmp" "$tmp_out"
fi fi
'' ''
else "" else
""
) )
) schemaPackages ) schemaPackages
); );
@@ -240,7 +235,8 @@ EOF
schemaInstallCommands schemaInstallCommands
updateYarnrcScript updateYarnrcScript
schemaExtensionsBlockEscaped schemaExtensionsBlockEscaped
selfSchemaName; selfSchemaName
;
}; };
mkTsPackageServer = mkTsPackageServer =
@@ -265,15 +261,16 @@ EOF
nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24; nodejs' = if nodejs != null then nodejs else pkgs.nodejs_24;
git' = if git != null then git else pkgs.git; git' = if git != null then git else pkgs.git;
templatePreparePackages' = templatePreparePackages' =
if templatePreparePackages != null if templatePreparePackages != null then
then templatePreparePackages templatePreparePackages
else [ else
pkgs.findutils [
git' pkgs.findutils
pkgs.gnused git'
nodejs' pkgs.gnused
pkgs.yarn-berry_4 nodejs'
]; pkgs.yarn-berry_4
];
packageJson = builtins.fromJSON (builtins.readFile "${srcDir}/package.json"); packageJson = builtins.fromJSON (builtins.readFile "${srcDir}/package.json");
packageName = packageJson.name or "quixos-package"; packageName = packageJson.name or "quixos-package";
@@ -291,28 +288,33 @@ EOF
runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages; runtimeBinPath = pkgs.lib.makeBinPath extraRuntimePackages;
runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages; runtimeLibPath = pkgs.lib.makeLibraryPath extraRuntimePackages;
runtimeWrap = runtimeWrap =
if extraRuntimePackages == [ ] if extraRuntimePackages == [ ] then
then "" ""
else '' else
wrapProgram "$out/bin/${serverBin}" \ ''
--prefix PATH : ${runtimeBinPath} \ wrapProgram "$out/bin/${serverBin}" \
--prefix LD_LIBRARY_PATH : ${runtimeLibPath} --prefix PATH : ${runtimeBinPath} \
''; --prefix LD_LIBRARY_PATH : ${runtimeLibPath}
'';
server = base.overrideAttrs (old: server = base.overrideAttrs (
old:
let let
extraAttrs = serverOverrides old; extraAttrs = serverOverrides old;
in in
{ {
buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs; buildInputs = (old.buildInputs or [ ]) ++ extraBuildInputs;
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ nativeBuildInputs =
pkgs.python3 (old.nativeBuildInputs or [ ])
pkgs.gnumake ++ [
pkgs.gcc pkgs.python3
pkgs.pkg-config pkgs.gnumake
pkgs.makeWrapper pkgs.gcc
] ++ extraNativeBuildInputs; pkgs.pkg-config
pkgs.makeWrapper
]
++ extraNativeBuildInputs;
# node-gyp will look at these # node-gyp will look at these
PYTHON = "${pkgs.python3}/bin/python3"; PYTHON = "${pkgs.python3}/bin/python3";
@@ -326,13 +328,14 @@ EOF
buildPhase = buildPhase =
(old.buildPhase or "") (old.buildPhase or "")
+ ( + (
if buildCommand != null if buildCommand != null then
then '' ''
runHook preBuildQuixos runHook preBuildQuixos
${buildCommand} ${buildCommand}
runHook postBuildQuixos runHook postBuildQuixos
'' ''
else "" else
""
); );
postFixup = (old.postFixup or "") + runtimeWrap; postFixup = (old.postFixup or "") + runtimeWrap;
@@ -342,7 +345,8 @@ EOF
mkdir -p "$pkg_root/quixos-package-schemas" mkdir -p "$pkg_root/quixos-package-schemas"
(cd "$pkg_root" && ${schemaSupport.schemaInstallCommands}) (cd "$pkg_root" && ${schemaSupport.schemaInstallCommands})
''; '';
} // extraAttrs }
// extraAttrs
); );
check = server.overrideAttrs (old: { check = server.overrideAttrs (old: {
@@ -360,7 +364,12 @@ EOF
}; };
in in
{ {
inherit server check packageName serverBin; inherit
server
check
packageName
serverBin
;
devShells = { devShells = {
"quixos-prepare" = prepareShell; "quixos-prepare" = prepareShell;
@@ -392,13 +401,18 @@ EOF
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
schemaSupport = mkSchemaSupport { schemaSupport = mkSchemaSupport {
inherit pkgs inputs system schemaDir schemaPrefix self flakeRoot; inherit
pkgs
inputs
system
schemaDir
schemaPrefix
self
flakeRoot
;
}; };
normalizeList = value: normalizeList = value: if builtins.isFunction value then value pkgs else value;
if builtins.isFunction value
then value pkgs
else value;
serverResult = serverBuilder { serverResult = serverBuilder {
inherit pkgs schemaSupport; inherit pkgs schemaSupport;
@@ -408,68 +422,75 @@ EOF
}; };
combinedName = pkgs.lib.strings.sanitizeDerivationName ( combinedName = pkgs.lib.strings.sanitizeDerivationName (
if packageName != null if packageName != null then
then packageName packageName
else if serverResult ? packageName else if serverResult ? packageName then
then serverResult.packageName serverResult.packageName
else "quixos-package" else
"quixos-package"
); );
packageServer = serverResult.server; packageServer = serverResult.server;
packageServerCheck = if serverResult ? check then serverResult.check else null; packageServerCheck = if serverResult ? check then serverResult.check else null;
appProgramFinal = appProgramFinal =
if appProgram != null if appProgram != null then
then appProgram appProgram
else if serverResult ? appProgram else if serverResult ? appProgram then
then serverResult.appProgram serverResult.appProgram
else "${packageServer}/bin/${serverResult.serverBin or "server"}"; else
"${packageServer}/bin/${serverResult.serverBin or "server"}";
devShellHookBase = devShellHookBase =
if enableYarnrcHook if enableYarnrcHook then
then '' ''
find_schema_root() { find_schema_root() {
dir="$PWD" dir="$PWD"
while [ "$dir" != "/" ]; do while [ "$dir" != "/" ]; do
if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then if [ -f "$dir/src/.yarnrc.yml" ] && [ -d "$dir/schema" ]; then
printf "%s\n" "$dir" printf "%s\n" "$dir"
return 0 return 0
fi
dir="$(dirname "$dir")"
done
return 1
}
${schemaSupport.updateYarnrcScript}
link_schemas() {
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
}
base="$(find_schema_root || true)"
if [ -n "$base" ]; then
(cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc)
if [ -d "$base/schema" ]; then
ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}"
fi fi
dir="$(dirname "$dir")"
done
return 1
}
${schemaSupport.updateYarnrcScript}
link_schemas() {
mkdir -p quixos-package-schemas
${schemaSupport.schemaLinkCommands}
}
base="$(find_schema_root || true)"
if [ -n "$base" ]; then
(cd "$base/src" && link_schemas && quixos_package_schemas_update_yarnrc)
if [ -d "$base/schema" ]; then
ln -sfn "$base/schema" "$base/src/quixos-package-schemas/${schemaSupport.selfSchemaName}"
fi fi
fi ''
'' else
else ""; "";
devShellHook = devShellHookBase + extraDevShellHook; devShellHook = devShellHookBase + extraDevShellHook;
devShellPackages = normalizeList extraDevShellPackages; devShellPackages = normalizeList extraDevShellPackages;
checks = checks = {
{ schema = schemaSupport.schemaCheck; } schema = schemaSupport.schemaCheck;
// (if packageServerCheck != null then { default = packageServerCheck; } else { }); }
// (if packageServerCheck != null then { default = packageServerCheck; } else { });
extraDevShells = if serverResult ? devShells then serverResult.devShells else { }; extraDevShells = if serverResult ? devShells then serverResult.devShells else { };
in in
{ {
packages.default = pkgs.symlinkJoin { packages.default = pkgs.symlinkJoin {
name = combinedName; name = combinedName;
paths = [ packageServer schemaSupport.schema ]; paths = [
packageServer
schemaSupport.schema
];
}; };
packages.server = packageServer; packages.server = packageServer;
packages.schema = schemaSupport.schema; packages.schema = schemaSupport.schema;
@@ -510,6 +531,29 @@ EOF
''; '';
}; };
# Language-neutral, offline schema compilation. Snapshot directories must be
# fixed Nix inputs matching the resource lock's complete dependency closure.
mkQxBindingSchema =
{
pkgs,
protocol,
src,
repository,
commit,
resources ? [ ],
}:
let
snapshots = pkgs.writeText "qx-binding-snapshots.json" (builtins.toJSON { inherit resources; });
in
pkgs.runCommand "qx-binding-schema.json" { } ''
${protocol}/bin/quixos-resource-compile \
--root ${src} --kind package \
--repository ${pkgs.lib.escapeShellArg repository} \
--commit ${pkgs.lib.escapeShellArg commit} \
--checkout-root "$TMPDIR/checkouts" \
--snapshot-map ${snapshots} --snapshot-only true --schema-out "$out" > /dev/null
'';
mkCaminoTsYarnNixifyFlake = mkCaminoTsYarnNixifyFlake =
{ {
inputs, inputs,
@@ -522,6 +566,12 @@ EOF
nodejsAttr ? "nodejs_24", nodejsAttr ? "nodejs_24",
buildCommand ? "yarn build", buildCommand ? "yarn build",
buildEnv ? { }, buildEnv ? { },
# An exact, compiler-produced BindingSchema JSON artifact and a backend.
# Other language helpers can consume the same schema with their own generator/runtime.
bindings ? null,
# A dedicated entrypoint calling SDK serveMigration; never start the
# normal package server in the isolated migration execution boundary.
migrationEntrypoint ? null,
nativeBuildInputs ? [ ], nativeBuildInputs ? [ ],
devShellPackages ? [ ], devShellPackages ? [ ],
devShellHook ? "", devShellHook ? "",
@@ -531,150 +581,268 @@ EOF
flake-utils.lib.eachDefaultSystem ( flake-utils.lib.eachDefaultSystem (
system: system:
let let
pkgs = import nixpkgs { inherit system; }; outputsFor =
lib = pkgs.lib; candidateBindings:
nodejs = pkgs.${nodejsAttr};
callOption = value:
if builtins.isFunction value
then value { inherit inputs pkgs system; }
else value;
packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
packageNameFinal = if packageName != null then packageName else packageJson.name or "quixos-package";
promptNameFinal =
if promptName != null
then promptName
else lib.strings.sanitizeDerivationName packageNameFinal;
sourcePackage = mkCaminoSourcePackage {
inherit pkgs;
src = packageRoot;
name = sourceName;
};
exportsFor = attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList
(name: value: "export ${name}=${lib.escapeShellArg (toString value)}")
attrs
);
bundleConfig = if bundle == null then { } else bundle;
bundleOutfile = bundleConfig.outfile or "server.mjs";
bundlePlatform = bundleConfig.platform or "node";
bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner;
bundleAliases = bundleConfig.aliases or {
"@automerge/automerge" = "./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js";
};
bundleAliasArgs = lib.concatStringsSep " " (
lib.mapAttrsToList
(from: to: "--alias:${from}=${lib.escapeShellArg to}")
bundleAliases
);
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand =
if bundle == null
then ""
else ''
esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \
--platform=${bundlePlatform} \
--target=${bundleTarget} \
--format=${bundleFormat} \
${bundleAliasArgs} \
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
--banner:js=${lib.escapeShellArg bundleBanner} \
--outfile=${lib.escapeShellArg bundleOutfile}
'';
installConfig = if installServer == null then { } else installServer;
serverBin = installConfig.binName or "server";
serverLibexecName =
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
extraFiles = installConfig.extraFiles or [ ];
installExtraFile = file:
let let
source = toString file.source; pkgs = import nixpkgs { inherit system; };
target = file.target or (baseNameOf source); lib = pkgs.lib;
mode = file.mode or "0644"; nodejs = pkgs.${nodejsAttr};
in ''
install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}"
'';
installServerPhase =
if installServer == null
then null
else ''
runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
${lib.concatMapStringsSep "\n" installExtraFile extraFiles}
mkdir -p "$out/bin"
cat > "$out/bin/${serverBin}" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
EOF
chmod +x "$out/bin/${serverBin}"
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
''}
runHook postInstall
'';
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) { callOption =
src = packageRoot; value: if builtins.isFunction value then value { inherit inputs pkgs system; } else value;
overrideAttrs = old: {
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null) pkgs.esbuild
++ callOption nativeBuildInputs;
buildPhase = ''
runHook preBuild
${exportsFor (callOption buildEnv)}
${buildCommand}
${bundleCommand}
runHook postBuild
'';
} // lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase;
};
};
devShellHookBase = '' packageJson = builtins.fromJSON (builtins.readFile "${packageRoot}/package.json");
if [ -n "''${PS1-}" ]; then packageNameFinal =
if [ -n "''${QX_DEV_SHELL_PROMPT-}" ]; then if packageName != null then packageName else packageJson.name or "quixos-package";
PS1="''${PS1#\[qx:''${QX_DEV_SHELL_PROMPT}\] }" promptNameFinal =
fi if promptName != null then promptName else lib.strings.sanitizeDerivationName packageNameFinal;
export QX_DEV_SHELL_PROMPT=${lib.escapeShellArg promptNameFinal}
PS1="[qx:${promptNameFinal}] $PS1"
fi
'';
maybeServerOutputs = sourcePackage = mkCaminoSourcePackage {
if installServer == null inherit pkgs;
then { } src = packageRoot;
else { name = sourceName;
packages.server = project;
apps.default = {
type = "app";
program = "${project}/bin/${serverBin}";
}; };
};
exportsFor =
attrs:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg (toString value)}") attrs
);
bindingConfig =
if candidateBindings != null then
candidateBindings
else if bindings == null then
null
else
callOption bindings;
bindingSchema =
if bindingConfig == null then
null
else
bindingConfig.schema or (mkQxBindingSchema {
inherit pkgs;
protocol = bindingConfig.generator;
src = packageRoot;
inherit (bindingConfig) repository commit;
resources = bindingConfig.resources or [ ];
});
bindingOutput =
if bindingConfig == null then "src/gen/qx.ts" else bindingConfig.output or "src/gen/qx.ts";
authoringCheck = builtins.fromJSON (builtins.readFile "${packageRoot}/quixos.check.json");
bindingOptionsFile =
if bindingConfig == null then
null
else
pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (authoringCheck.options or { }));
bindingCommand =
if bindingConfig == null then
""
else
''
mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)}
${bindingConfig.generator}/bin/quixos-codegen-ts \
${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} \
${lib.escapeShellArg bindingOutput} ${bindingOptionsFile}
${lib.optionalString (installServer != null && descriptorPath != null) ''
${bindingConfig.generator}/bin/quixos-qx package-descriptor ${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} > ${lib.escapeShellArg descriptorPath}
''}
'';
generateBindings = pkgs.writeShellApplication {
name = "qx-generate-bindings";
text = bindingCommand;
};
bundleConfig = if bundle == null then { } else bundle;
bundleOutfile = bundleConfig.outfile or "server.mjs";
bundlePlatform = bundleConfig.platform or "node";
bundleTarget = bundleConfig.target or "node24";
bundleFormat = bundleConfig.format or "esm";
bundleBanner = bundleConfig.banner or nodeRequireBanner;
bundleAliases =
bundleConfig.aliases or {
"@automerge/automerge" =
"./node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js";
};
bundleAliasArgs = lib.concatStringsSep " " (
lib.mapAttrsToList (from: to: "--alias:${from}=${lib.escapeShellArg to}") bundleAliases
);
nodeRequireBanner = "import { createRequire } from 'module';const require = createRequire(import.meta.url);";
bundleCommand =
if bundle == null then
""
else if bundleConfig.browserSources or false then
''
${nodejs}/bin/node ${./browser-source.mjs} ${
lib.escapeShellArg (
builtins.toJSON {
entryPoints = [ bundleConfig.entry ];
bundle = true;
platform = bundlePlatform;
target = bundleTarget;
format = bundleFormat;
alias = bundleAliases;
preserveSymlinks = bundleConfig.preserveSymlinks or true;
banner.js = bundleBanner;
outfile = bundleOutfile;
}
)
}
''
else
''
esbuild ${lib.escapeShellArg bundleConfig.entry} \
--bundle \
--platform=${bundlePlatform} \
--target=${bundleTarget} \
--format=${bundleFormat} \
${bundleAliasArgs} \
${lib.optionalString (bundleConfig.preserveSymlinks or true) "--preserve-symlinks \\"}
--banner:js=${lib.escapeShellArg bundleBanner} \
--outfile=${lib.escapeShellArg bundleOutfile}
'';
installConfig = if installServer == null then { } else installServer;
serverBin = installConfig.binName or "server";
serverLibexecName =
installConfig.libexecName or (lib.strings.sanitizeDerivationName packageNameFinal);
serverFile = installConfig.serverFile or bundleOutfile;
descriptorPath = installConfig.descriptorPath or "descriptor.quixos-package.txtpb";
extraFiles = installConfig.extraFiles or [ ];
installExtraFile =
file:
let
source = toString file.source;
target = file.target or (baseNameOf source);
mode = file.mode or "0644";
in
''
install -Dm${toString mode} ${lib.escapeShellArg source} "$out/libexec/${serverLibexecName}/${target}"
'';
installServerPhase =
if installServer == null then
null
else
''
runHook preInstall
install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}"
${lib.concatMapStringsSep "\n" installExtraFile extraFiles}
mkdir -p "$out/bin"
cat > "$out/bin/${serverBin}" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node "$out/libexec/${serverLibexecName}/${serverFile}" "\$@"
EOF
chmod +x "$out/bin/${serverBin}"
${lib.optionalString (installConfig ? descriptorPath && descriptorPath != null) ''
cp ${lib.escapeShellArg descriptorPath} "$out/${descriptorPath}"
''}
${lib.optionalString (bindingConfig != null) ''
install -m 0444 quixos-check.json "$out/quixos-check.json"
''}
${lib.optionalString (migrationEntrypoint != null) ''
install -Dm444 migration.mjs "$out/libexec/${serverLibexecName}/migration.mjs"
cat > "$out/bin/migrate" <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node --max-old-space-size=256 "$out/libexec/${serverLibexecName}/migration.mjs" "\$@"
EOF
chmod +x "$out/bin/migrate"
''}
runHook postInstall
'';
project = (pkgs.callPackage "${packageRoot}/yarn-project.nix" { inherit nodejs; }) {
src = packageRoot;
overrideAttrs =
old:
{
nativeBuildInputs =
(old.nativeBuildInputs or [ ])
++ lib.optional (bundle != null || migrationEntrypoint != null) pkgs.esbuild
++ callOption nativeBuildInputs;
buildPhase = ''
runHook preBuild
${exportsFor (callOption buildEnv)}
${bindingCommand}
${lib.optionalString (
bindingConfig != null && bundle != null
) "${bindingConfig.generator}/bin/quixos-qx bundle-policy src"}
${lib.optionalString (bindingConfig != null) "yarn exec tsc --noEmit"}
${lib.optionalString (
bindingConfig != null
) "cp ${lib.escapeShellArg bindingOutput} .qx-checked-bindings"}
${buildCommand}
${lib.optionalString (bindingConfig != null) ''
yarn exec tsc --noEmit
node ${./check-receipt.mjs} ${lib.escapeShellArg (toString bindingSchema)} \
${lib.escapeShellArg bindingConfig.packageRevisionId} ${lib.escapeShellArg bindingOutput} \
${lib.escapeShellArg (toString bindingConfig.generator)} quixos-check.json
''}
${bundleCommand}
${lib.optionalString (migrationEntrypoint != null) ''
esbuild ${lib.escapeShellArg migrationEntrypoint} --bundle --platform=node --target=node24 --format=esm \
${bundleAliasArgs} --preserve-symlinks --banner:js=${lib.escapeShellArg nodeRequireBanner} --outfile=migration.mjs
''}
runHook postBuild
'';
}
// lib.optionalAttrs (installServerPhase != null) {
installPhase = installServerPhase;
};
};
devShellHookBase = ''
if [ -n "''${PS1-}" ]; then
if [ -n "''${QX_DEV_SHELL_PROMPT-}" ]; then
PS1="''${PS1#\[qx:''${QX_DEV_SHELL_PROMPT}\] }"
fi
export QX_DEV_SHELL_PROMPT=${lib.escapeShellArg promptNameFinal}
PS1="[qx:${promptNameFinal}] $PS1"
fi
'';
maybeServerOutputs =
if installServer == null then
{ }
else
{
packages.server = project;
apps.default = {
type = "app";
program = "${project}/bin/${serverBin}";
};
};
in
{
packages.default = project;
packages.source = sourcePackage;
devShells.default = pkgs.mkShell {
packages = [
nodejs
project.yarn-freestanding
]
++ lib.optional (bindingConfig != null) generateBindings
++ callOption devShellPackages;
# Explicit command keeps shell entry free of source mutations.
shellHook = devShellHookBase + callOption devShellHook;
};
}
// maybeServerOutputs;
in in
{ (outputsFor null)
packages.default = project; // {
packages.source = sourcePackage; # The workspace supplies a compiler-produced schema for the exact
devShells.default = pkgs.mkShell { # candidate graph. Standalone builds may use checked-in authoring types,
packages = [ # but only this build path regenerates and witnesses candidate contracts.
nodejs quixosPackages.checkedServer =
project.yarn-freestanding {
] ++ callOption devShellPackages; schema,
shellHook = devShellHookBase + callOption devShellHook; generator,
}; packageRevisionId,
} // maybeServerOutputs }:
(outputsFor { inherit schema generator packageRevisionId; }).packages.server;
}
); );
in in
{ {
@@ -683,5 +851,7 @@ in
mkTsPackageServer mkTsPackageServer
mkSchemaSupport mkSchemaSupport
mkCaminoSourcePackage mkCaminoSourcePackage
mkCaminoTsYarnNixifyFlake; mkQxBindingSchema
mkCaminoTsYarnNixifyFlake
;
} }