Compare commits

..

12 Commits

Author SHA1 Message Date
Quixos Subtree Publisher c16271510a Publish quixos-protocol from 0023a184c839088480702512751ebb05bf170162 2026-09-14 22:54:30 +00:00
Timothy J. Aveni 8aac091f6c Make workspace authoring transitions and typed invocation coherent 2026-09-14 15:54:30 -07:00
Timothy J. Aveni 01ca965c7f Make workspace authoring converge through immutable Nix candidates
Coordinate registered resource edits bottom-up into retained exact remote sources.
Use one Nix-owned source graph for provisional checking, template publication,
explicit baseline upgrades and host activation; retain independent runtime pins.

Add scoped contract inspection, historical recovery, derived worklists, crash-safe
locks, named dependency adoption and plain-QX structural editing. Repair TODO
ownership and template instantiation, and document the supported agent workflow.

Validated with protocol and command suites, real jj/Nix convergence and cache
checks, TS/React installed-command acceptance, and fresh TODO first-edit acceptance.
No live deployment or public publication performed. Props projection generation
and a one-command rich feature generator remain explicitly outside this delivery.
2026-09-14 12:25:47 -07:00
Timothy J. Aveni fae4e48f72 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 6ecf565549 Publish quixos-protocol from 7fd5e15105cef21d2b1c3da860c53c5033f66256 2026-09-14 05:46:08 +00:00
Timothy J. Aveni 73cdeadc2c Unify workspace authoring, verification and scaffolding workflows 2026-09-13 22:46:08 -07:00
Timothy J. Aveni 16b28f1bc4 Migrate TODO implementations to generated candidate bindings 2026-09-13 20:28:50 -07:00
Quixos Subtree Publisher dcef58d6c5 Publish quixos-protocol from 22bb3d02264980d74de65c34bbbcb81764dc65c0 2026-09-14 03:28:50 +00:00
Quixos Subtree Publisher f7fcc7d08b Publish quixos-protocol from e25eee6ce4f13702b2454a9354bc79a29eb2e4a1 2026-09-14 03:01:53 +00:00
Timothy J. Aveni 295181244f Migrate TODO implementations to generated candidate bindings 2026-09-13 20:01:53 -07:00
Timothy J. Aveni 42af87f8bd Fix supervised credentials and declare typed RPC records for canvas placement 2026-09-13 16:38:55 -07:00
Quixos Subtree Publisher e9fb42dce4 Publish quixos-protocol from 61cb9aa5a580c2846d426464228c7d1082b7021a 2026-09-13 23:38:55 +00:00
50 changed files with 3207 additions and 1650 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "0e6bb95d903004916bd93670497ab310a076a28b",
"sourceCommit": "0023a184c839088480702512751ebb05bf170162",
"sourcePath": "quixos-protocol",
"exportName": "quixos-protocol",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
+8
View File
@@ -19,6 +19,9 @@
pkgs.esbuild
pkgs.protobuf
pkgs.git
pkgs.jujutsu
pkgs.gnutar
pkgs.util-linux
];
buildPhase = ''
runHook preBuild
@@ -29,6 +32,7 @@
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:$PWD/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
patchShebangs node_modules/.bin node_modules/@bufbuild/protoc-gen-es/bin
yarn build
esbuild dist/src/bindings/client.js --bundle --platform=node --target=node24 --format=esm --outfile=client-codegen.mjs
diff --recursive --unified "$TMPDIR/generated-before/proto" src/gen
diff --recursive --unified "$TMPDIR/generated-before/capability" src/capability-language/generated
diff --recursive --unified "$TMPDIR/generated-before/lock" src/resource-lock/generated
@@ -60,6 +64,9 @@
installPhase = ''
runHook preInstall
mkdir -p "$out"
install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix"
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
--replace-fail '@nixpkgs@' '${pkgs.path}'
cp --reflink=auto --recursive grammar "$out/grammar"
cp --reflink=auto --recursive proto "$out/proto"
cp --reflink=auto --recursive dist "$out/dist"
@@ -97,6 +104,7 @@ exec ${pkgs.nodejs_24}/bin/node "$out/libexec/quixos-protocol/quixos-lock-check.
EOF
chmod +x "$out/bin/quixos-lock-check"
mkdir -p "$out/libexec/quixos-protocol"
install -m644 client-codegen.mjs "$out/libexec/quixos-protocol/client-codegen.mjs"
install -m644 quixos-codegen-ts.mjs quixos-qx.mjs "$out/libexec/quixos-protocol/"
install -m644 quixos-descriptor-check.mjs "$out/libexec/quixos-protocol/quixos-descriptor-check.mjs"
install -m644 quixos-capability-compile.mjs "$out/libexec/quixos-protocol/quixos-capability-compile.mjs"
+6
View File
@@ -280,6 +280,11 @@ valueType
| INTERFACE_REF LT identifier GT
| OPTIONAL LT valueType GT
| LIST LT valueType GT
| RECORD LBRACE recordField* RBRACE
;
recordField
: identifier COLON valueType SEMI
;
scalarType
@@ -410,6 +415,7 @@ ATOM_REF: 'atom-ref';
INTERFACE_REF: 'interface-ref';
OPTIONAL: 'optional';
LIST: 'list';
RECORD: 'record';
BOOL: 'bool';
BYTES: 'bytes';
DOUBLE: 'double';
+77
View File
@@ -0,0 +1,77 @@
# All source trees and recursive locks are fetched by Nix at exact retained
# revisions. No authoring-directory overlay or externally assembled schema.
{ repository, commit, kind, generator, contractOnly ? false, system ? builtins.currentSystem }:
let
pkgs = import (/. + "@nixpkgs@") { inherit system; };
protocol = builtins.storePath generator;
sourceKey = source: "${source.kind}:${source.repository}@${source.commit}";
fetch = source: builtins.fetchGit {
url = source.repository;
rev = source.commit;
ref = "refs/tags/quixos-reachability/${source.commit}";
shallow = true;
};
load = ancestors: source:
if builtins.elem (sourceKey source) ancestors then
throw "Source dependency cycle at ${sourceKey source}"
else if builtins.length ancestors >= 100 then
throw "Source dependency depth exceeds 100"
else let
directory = fetch source;
lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out"
'';
lock = builtins.fromJSON (builtins.readFile lockFile);
children = map (entry: load (ancestors ++ [ (sourceKey source) ]) (entry.source // { inherit (entry) kind; })) lock.resources;
in source // { inherit directory children; };
root = load [ ] { inherit kind repository commit; };
flatten = node: [ node ] ++ pkgs.lib.concatMap flatten node.children;
nodes = builtins.attrValues (builtins.listToAttrs (map (node: {
name = sourceKey node; value = node;
}) (flatten root)));
snapshots = pkgs.writeText "qx-nix-source-graph.json" (builtins.toJSON {
resources = map (node: { inherit (node) kind repository commit directory; })
(builtins.filter (node: node.kind != "workspace") nodes);
});
compile = node: pkgs.runCommand "qx-${node.kind}-contract" { } ''
mkdir -p "$out"
${if node.kind == "workspace" then ''
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} \
--graph-out "$out/graph.json" > "$out/candidate.json"
'' else ''
${protocol}/bin/quixos-resource-compile --root ${node.directory} \
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
--commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} --snapshot-only true \
--graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json"
''}
'';
contract = compile root;
checkPackage = node: let
compiled = compile node;
candidate = builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
package = builtins.getFlake ("git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}");
checked = package.quixosPackages.${system}.checkedServer or
(throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
artifact = checked {
schema = "${compiled}/bindings.json";
generator = protocol;
packageRevisionId = candidate.revision.revisionId;
};
in { packageRevisionId = candidate.revision.revisionId; artifactPath = artifact; };
checks = if contractOnly then [ ] else map checkPackage (builtins.filter (node: node.kind == "package") nodes);
manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks);
in pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } ''
mkdir -p "$out"
cp ${contract}/candidate.json "$out/candidate.json"
cp ${contract}/graph.json "$out/graph.json"
cp ${manifest} "$out/checks.json"
${pkgs.lib.optionalString (kind != "workspace") ''cp ${contract}/bindings.json "$out/bindings.json"''}
${pkgs.lib.optionalString (kind == "package") ''
${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
"$out/bindings.ts" ${pkgs.lib.optionalString (builtins.pathExists (root.directory + "/bindings.json")) (toString root.directory + "/bindings.json")}
''}
''
+17
View File
@@ -0,0 +1,17 @@
# One derivation path for provisional checking and activation. The source and
# schema come from exact committed inputs resolved by the Quixos compiler.
{ source, schema, generator, packageRevisionId, system ? builtins.currentSystem }:
let
packageSource = builtins.path {
path = /. + source;
name = "quixos-package-source";
filter = path: _: let name = baseNameOf path; in name != ".git" && name != ".jj";
};
package = builtins.getFlake ("path:" + builtins.unsafeDiscardStringContext (toString packageSource));
checked = package.quixosPackages.${system}.checkedServer or
(throw "Package ${packageRevisionId} lacks checkedServer; use the supported package scaffold.");
in checked {
inherit packageRevisionId;
schema = builtins.path { path = /. + schema; name = "candidate-package-bindings.json"; };
generator = builtins.storePath generator;
}
+14
View File
@@ -72,6 +72,20 @@ message GetWorkspaceResponse {
string workspace_id = 1;
string workspace_revision_id = 2;
string source_root_commit = 3;
// Checked constructors whose wire input can be empty. Web Studio intersects
// this with its temporary Createable marker; the marker is not a factory.
repeated string empty_input_constructible_atom_ids = 4;
repeated CapabilityInputContract capability_inputs = 5;
repeated ConstructorInputContract constructor_inputs = 6;
}
message CapabilityInputContract {
string interface_revision_id = 1;
string operation_id = 2;
string type_json = 3;
}
message ConstructorInputContract {
string atom_id = 1;
string type_json = 2;
}
message ListActivationsRequest {}
message ListPackageDescriptorsRequest {}
+29
View File
@@ -0,0 +1,29 @@
import type {InterfaceRevision, ValueType} from "../capability-model/types.js";
/** Host clients have no package receiver, but must use the same checked
* interface signatures and argument framing as generated package ports. */
export const generateClientContracts = (interfaces: InterfaceRevision[], messages: Record<string, string>) => {
const type = (value: ValueType): string => {
switch (value.kind) {
case "builtin": return value.name === "unit" ? "undefined" : "string";
case "scalar": return ({bool: "boolean", string: "string", bytes: "Uint8Array", int32: "number", uint32: "number", double: "number", int64: "bigint", uint64: "bigint"})[value.name];
case "object-ref": return `{readonly $quixosRef: string}`;
case "optional": return `(${type(value.value)} | null)`;
case "list": return `Array<${type(value.value)}>`;
case "record": return `{${Object.entries(value.fields).map(([name, field]) => `${JSON.stringify(name)}${field.kind === "optional" ? "?" : ""}: ${type(field)}`).join("; ")}}`;
case "message": {
const binding = messages[value.descriptorId];
if (!binding) throw new Error(`Missing host message type ${value.descriptorId}`);
return binding;
}
}
};
const operations = interfaces.flatMap(iface => iface.members.flatMap(member => member.operations
.filter(operation => operation.mode === "call").map(operation => ({...operation, interfaceRevisionId: iface.revisionId}))));
return `// Generated from checked QX interfaces. Regenerate with scripts/generate-platform-contracts.mjs.\n` +
`export type PlatformInputs = {\n${operations.map(operation => ` ${JSON.stringify(operation.id)}: ${type(operation.inputType)};`).join("\n")}\n};\n` +
`export const platformOperations = ${JSON.stringify(Object.fromEntries(operations.map(operation => [operation.id, {
interfaceRevisionId: operation.interfaceRevisionId,
input: operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "unit" : ["record", "message"].includes(operation.inputType.kind) ? "fields" : "value",
}])), null, 2)} as const;\n`;
};
+10 -3
View File
@@ -37,6 +37,7 @@ export const generateTypeScriptBindings = (
case "object-ref": return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
case "optional": return `(${type(value.value)} | null)`;
case "list": return `Array<${type(value.value)}>`;
case "record": return `{ ${Object.entries(value.fields).map(([name, field]) => `${q(name)}: ${type(field)}`).join("; ")} }`;
case "message": {
if (!options.messages?.[value.descriptorId]) throw new Error(`Missing TypeScript message binding for ${value.descriptorId}`);
if (!messages.has(value.descriptorId)) messages.set(value.descriptorId, `message${messages.size}`);
@@ -56,6 +57,7 @@ export const generateTypeScriptBindings = (
if (primitive === "write") return ["set", `(value: ${type(requirement.valueType)}) => Promise<void>`];
throw new Error(`State primitive ${primitive} is not supported by the TypeScript runtime binding yet`);
});
if (requirement.primitives.includes("read")) methods.push(["live", "() => Promise<QxLiveValue>"]);
return { type: object(methods), spec: { ...requirement, id: entry.id } };
}
case "edge": {
@@ -74,8 +76,13 @@ export const generateTypeScriptBindings = (
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
const operations = contract.members.flatMap((member) => member.operations.filter((operation) => operation.mode === "call")
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })));
return { type: object(operations.map((operation) => [operation.name, `(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`])),
spec: { kind: "interface", id: entry.id, operations: Object.fromEntries(operations.map((operation) => [operation.name, operation])) } };
return { type: object([
["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })],
["live", object(operations.map(operation => [operation.name, `(${params(operation.inputType)}) => Promise<QxLiveValue>`]))],
...operations.map((operation): [string, string] => [operation.name, `(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`]),
]),
spec: { kind: "interface", id: entry.id, operations: Object.fromEntries(operations.map(({name, id, inputType, outputType}) =>
[name, {id, inputType, outputType}])) } };
}
case "constructor": {
const input = requirement.inputType;
@@ -118,7 +125,7 @@ export const generateTypeScriptBindings = (
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
});
const signatures = `${object(contexts)} ${object(handlers)}`;
const typeImports = ["BindingValue", "QxObjectRef", "QxWatchHandle", "QxHandler", "QxDerived", "QxContextLifecycle", "RelationshipCollection", "RelationshipEntry"].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
const typeImports = ["BindingValue", "QxObjectRef", "QxWatchHandle", "QxHandler", "QxDerived", "QxContextLifecycle", "QxLiveValue", "RelationshipCollection", "RelationshipEntry"].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
return `// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
`import { ${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
imports.join("\n") + `\nexport const packageRevisionId = ${q(pkg.revisionId)};\n` +
+1 -1
View File
@@ -361,7 +361,7 @@ export const compileWorkspaceRepository = async (options: {
: {}),
...(options.workspaceRevisionId
? { id: capabilityId.workspaceRevision(options.workspaceRevisionId) }
: {}),
: options.sourceRootCommit ? { id: capabilityId.workspaceRevision(`workspace-revision:${options.workspaceId ?? compiled.workspace.workspaceId}:${options.sourceRootCommit}`) } : {}),
...(options.sourceRootCommit
? { sourceRootCommit: options.sourceRootCommit }
: {}),
@@ -0,0 +1,80 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { authoringContext } from "./authoring-context.js";
import type { convergeAuthoring } from "./authoring-converge.js";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js";
import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../capability-model/index.js";
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
export async function checkAuthoring(start: string, output: string, options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {}) {
const context = await authoringContext(start);
const location = await fs.realpath(start);
const directory = location === context.workbench ? "root" : path.relative(context.workbench, location);
const resource = context.resources.find(entry => entry.directory === directory);
if (!resource) throw new Error("Run check from a registered repository root or the workbench");
await fs.mkdir(output, { mode: 0o700 });
const report: { directory: string; checker: string; candidateOnly: true; activationEvidence: false; commit?: string; artifactPath?: string; blockers: string[]; phase: string; output: string; compilation?: "passed"; activationReadiness?: "preserve" | "migration-required" | "blocked"; migrationRequired?: string[] } = {
directory, checker: checkerIdentity(), candidateOnly: true, activationEvidence: false, blockers: [], phase: "convergence", output,
};
try {
// Serialize only source capture, not the potentially slow Nix build.
// Repository-scoped agents can check separate immutable candidates in parallel.
const captured = await promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
maxBuffer: 4 * 1024 * 1024, env: {...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1"},
}).catch(error => {
if (typeof error.stdout === "string" && error.stdout.trim().startsWith("{")) return {stdout: error.stdout};
throw error;
});
const converged = JSON.parse(captured.stdout) as Awaited<ReturnType<typeof convergeAuthoring>>;
if (!converged.candidate) {
report.phase = converged.worklist.find(entry => entry.phase !== "dependency")?.phase ?? "convergence";
throw new Error(converged.worklist.map(entry => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"));
}
report.commit = converged.candidate.commit;
report.phase = "verification";
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
report.compilation = "passed";
if (resource.kind === "workspace" && !options.contractOnly) {
report.phase = "evolution";
let baseline = options.baseline;
if (!baseline) {
try {
const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8"));
if (await fs.realpath(host.workbenchRoot) === context.workbench) baseline = JSON.parse(await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8")).workspacePlanPath;
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
}
const before = baseline ? JSON.parse(await fs.readFile(baseline, "utf8")) as WorkspaceRevision : null;
const reviews = options.reviews ? JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[] : [];
const evolution = planEvolution(before, JSON.parse(candidateText), { reviews });
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
report.blockers.push(...evolution.blockers);
report.migrationRequired = evolution.migrationRequired;
report.activationReadiness = evolution.blockers.length ? "blocked" : evolution.migrationRequired.length ? "migration-required" : "preserve";
if (evolution.migrationRequired.length) report.blockers.push(`Explicit migration required for: ${evolution.migrationRequired.join(", ")}. Compilation passed; supply a migration path before cutover.`);
}
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
} catch (error) { report.blockers.push(String(error instanceof Error ? error.message : error)); }
await fs.writeFile(path.join(output, "report.json"), JSON.stringify(report, null, 2));
if (options.contractOnly) return report;
const records = path.join(context.workbench, ".quixos/checks");
await fs.mkdir(records, { recursive: true });
const remember = async (value: typeof report) => {
const filename = path.join(records, checkRecordName(value.directory)), temporary = `${filename}.${randomUUID()}.tmp`;
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
await fs.rename(temporary, filename);
};
if (report.artifactPath) {
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
for (const checked of graph.resources) {
const managed = context.resources.find(entry => entry.kind === checked.kind && entry.source?.repository === checked.source.repository);
if (managed && managed.directory !== directory) await remember({...report, directory: managed.directory, commit: checked.source.commit, blockers: [], phase: "checked"});
}
}
await remember(report);
return report;
}
@@ -0,0 +1,49 @@
import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
export type AuthoringResource = {
kind: "workspace" | "interface" | "package";
directory: string;
resourceId?: string;
source?: GitSource;
};
/** Registration, not directory co-location, defines the editable selection. */
export async function authoringContext(start: string) {
let workbench = await realpath(start);
for (;;) {
let text: string | undefined;
try { text = await readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
if (text !== undefined) {
const graph = JSON.parse(text) as { resources: AuthoringResource[] };
if (!Array.isArray(graph.resources)) throw new Error("Managed resource inventory is malformed");
const resources: AuthoringResource[] = [{ kind: "workspace", directory: "root" }];
const identities = new Set<string>(), directories = new Set<string>(["root"]);
for (const entry of graph.resources) {
// Compiler graphs carry resolved paths; the authoring API presents
// stable workbench-relative names and validates containment here.
if (typeof entry.directory === "string" && path.isAbsolute(entry.directory)) entry.directory = path.relative(workbench, entry.directory);
if (!["interface", "package"].includes(entry.kind) || !entry.source ||
!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory)) {
throw new Error("Invalid managed resource registration");
}
const identity = `${entry.kind}\0${entry.resourceId ?? entry.source.repository}`;
if (identities.has(identity) || directories.has(entry.directory)) {
throw new Error(`Multiple editable selections for ${entry.resourceId ?? entry.source.repository}`);
}
identities.add(identity); directories.add(entry.directory);
resources.push({kind: entry.kind, directory: entry.directory, resourceId: entry.resourceId, source: entry.source});
}
return { workbench, resources, async baseline() {
const result = await loadQuixosLock(path.join(workbench, "root/quixos.lock"));
if (!result.ok) throw new Error(`Workspace source baseline is invalid: ${result.diagnostics.map(d => d.message).join("; ")}`);
return result.lock.quixos;
} };
}
const parent = path.dirname(workbench);
if (parent === workbench) throw new Error("Not in a managed workbench; select one with --workbench DIRECTORY");
workbench = parent;
}
}
@@ -0,0 +1,166 @@
import { readFile, writeFile, rename, rm, realpath } from "node:fs/promises";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";
import { authoringContext } from "./authoring-context.js";
import { snapshotCommit } from "./checked-build.js";
import { loadQuixosLock, parseQuixosLockDocument, formatQuixosLockDocument, retentionTagForCommit, type GitSource } from "../resource-lock/index.js";
const execFile = promisify(callback);
const command = async (cwd: string, executable: string, args: string[]) => (await execFile(executable, args, {
cwd, maxBuffer: 4 * 1024 * 1024,
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0", GIT_TERMINAL_PROMPT: "0" },
})).stdout.trim();
const identity = (kind: string, repository: string) => `${kind}\0${repository}`;
export type AuthoringBlocker = { directory: string; phase: "resolution" | "dependency" | "source" | "publication" | "concurrent-edit"; message: string };
/** Source retention only. Neither successful convergence nor an empty source
* worklist grants typechecking, semantic review or activation approval.
* Caller serializes coordinators; package authors may still be editing. */
export async function convergeAuthoring(start: string, target = "root") {
const context = await authoringContext(start);
const graphFile = path.join(context.workbench, ".quixos/resource-graph.json");
const graphBefore = await readFile(graphFile, "utf8");
const blockers: AuthoringBlocker[] = [];
const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>();
const selected = new Map<string, string>();
for (const entry of context.resources) {
const root = path.join(context.workbench, entry.directory);
let repository = entry.source?.repository;
try {
if (await realpath(root) !== root) throw new Error(`Managed checkout crosses a symlink: ${entry.directory}`);
// Transport rewrites must not become committed source identities.
const origin = await command(root, "git", ["config", "--get", "remote.origin.url"]);
if (repository && origin !== repository) throw new Error(`Origin differs from registered source for ${entry.directory}`);
repository ??= origin;
} catch (error) {
blockers.push({directory: entry.directory, phase: "source", message: String(error).slice(0, 2000)});
if (!repository) throw error; // The root has no separate registered source.
}
const key = identity(entry.kind, repository);
if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`);
selected.set(key, entry.directory);
nodes.set(entry.directory, { ...entry, source: { resolver: "git", repository, commit: entry.source?.commit ?? "" }, dependencies: [] });
}
for (const node of nodes.values()) {
try {
const lock = await loadQuixosLock(path.join(context.workbench, node.directory, "quixos.lock"));
if (!lock.ok) throw new Error(lock.diagnostics.map(d => `${d.fileName}: ${d.message}`).join("\n"));
node.dependencies = [...new Set(lock.lock.resources.flatMap(entry => {
const directory = selected.get(identity(entry.kind, entry.source.repository));
return directory ? [directory] : [];
}))];
} catch (error) { blockers.push({ directory: node.directory, phase: "resolution", message: String(error) }); }
}
const complete = new Map<string, GitSource>(), active = new Set<string>();
const visited = new Set<string>();
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
const visit = async (directory: string): Promise<boolean> => {
visited.add(directory);
if (complete.has(directory)) return true;
if (blockers.some(entry => entry.directory === directory)) return false;
if (active.has(directory)) { blockers.push({ directory, phase: "dependency", message: `Source dependency cycle: ${[...active, directory].join(" -> ")}` }); return false; }
active.add(directory);
const node = nodes.get(directory)!;
for (const dependency of node.dependencies) if (!await visit(dependency)) {
blockers.push({ directory, phase: "dependency", message: `Waiting for ${dependency}` }); active.delete(directory); return false;
}
const root = path.join(context.workbench, directory);
let phase: AuthoringBlocker["phase"] = "source";
try {
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) throw new Error("Lock changed during convergence; retry after joining writers");
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
const filename = path.join(root, file), before = await readFile(filename, "utf8");
const parsed = parseQuixosLockDocument(before, file);
if (!parsed.ok) throw new Error(`Invalid lock ${file}`);
let changed = false;
for (const dependency of parsed.document.resources) {
const target = selected.get(identity(dependency.kind, dependency.source.repository));
const source = target ? complete.get(target) : undefined;
if (target && !source) throw new Error(`Dependencies changed during convergence (${dependency.binding}); join writers and retry`);
if (source && source.commit !== dependency.source.commit) { dependency.source = source; changed = true; }
}
if (changed) {
const temporary = `${filename}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, formatQuixosLockDocument(parsed.document), { flag: "wx" });
if (await readFile(filename, "utf8") !== before) throw new Error(`Concurrent edit to ${file}; retry after joining writers`);
await rename(temporary, filename);
} finally { await rm(temporary, { force: true }); }
}
}
const commit = await snapshotCommit(root);
phase = "publication";
const ref = retentionTagForCommit(commit);
const remote = await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref]);
if (remote && remote.split(/\s+/)[0] !== commit) throw new Error(`Conflicting immutable retention ref ${ref}`);
if (!remote) await command(root, "git", ["push", node.source.repository, `${commit}:${ref}`]);
if ((await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !== commit) throw new Error("Published source retention was not observed");
complete.set(directory, { ...node.source, commit });
} catch (error) { blockers.push({ directory, phase, message: String(error).slice(0, 4000) }); }
active.delete(directory);
return complete.has(directory);
};
// Include newly created, not-yet-imported resources, then the root.
if (target === "root") for (const directory of [...nodes.keys()].filter(d => d !== "root")) await visit(directory);
await visit(target);
for (let index = blockers.length - 1; index >= 0; index--) if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
for (const [directory, source] of complete) {
try { if (await snapshotCommit(path.join(context.workbench, directory)) !== source.commit) throw new Error("Source advanced while converging; join writers and retry"); }
catch (error) { blockers.push({ directory, phase: "concurrent-edit", message: String(error) }); }
}
// Persist successful selections even if another repository is still broken.
// Recovery must not depend on all parents succeeding in the same invocation.
const graph = JSON.parse(graphBefore);
for (const resource of graph.resources) resource.directory = path.relative(context.workbench, path.resolve(context.workbench, resource.directory));
const replacements = new Map<string, string>();
for (const resource of graph.resources) {
const source = complete.get(resource.directory);
if (!source) continue;
const key = `${resource.kind}\0${source.repository}\0${source.commit}`;
replacements.set(resource.key, key);
if (resource.source.commit !== source.commit) delete resource.revisionId;
resource.source = source; resource.key = key;
}
for (const resource of graph.resources) for (const dependency of resource.dependencies ?? []) {
dependency.resourceKey = replacements.get(dependency.resourceKey) ?? dependency.resourceKey;
}
for (const direct of graph.directResources ?? []) direct.resourceKey = replacements.get(direct.resourceKey) ?? direct.resourceKey;
// Inventory is a projection of actual locks, including newly added/removed
// imports. Never require a successful parent compilation to repair it.
for (const [directory] of complete) {
const lock = await loadQuixosLock(path.join(context.workbench, directory, "quixos.lock"));
if (!lock.ok) continue;
const dependencies = lock.lock.resources.map(dependency => ({
binding: `${dependency.kind}\0${dependency.binding}`,
resourceKey: `${dependency.kind}\0${dependency.source.repository}\0${dependency.source.commit}`,
}));
if (directory === "root") {
graph.quixos = lock.lock.quixos;
graph.directResources = lock.lock.resources.map((dependency, index) => ({
kind: dependency.kind, binding: dependency.binding, resourceKey: dependencies[index].resourceKey,
...(selected.has(identity(dependency.kind, dependency.source.repository))
? {directory: selected.get(identity(dependency.kind, dependency.source.repository))} : {}),
}));
} else {
const resource = graph.resources.find((entry: {directory: string}) => entry.directory === directory);
if (resource) resource.dependencies = dependencies;
}
}
const graphAfter = JSON.stringify(graph, null, 2) + "\n";
if (graphBefore !== graphAfter) {
const temporary = `${graphFile}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, graphAfter, { flag: "wx", mode: 0o600 });
if (await readFile(graphFile, "utf8") !== graphBefore) throw new Error("Managed inventory changed during convergence; source is retained, retry after joining writers");
await rename(temporary, graphFile);
} finally { await rm(temporary, { force: true }); }
}
return { workbench: context.workbench, converged: blockers.length === 0,
candidate: blockers.length ? null : complete.get(target) ?? null,
retained: [...complete].map(([directory, source]) => ({ directory, source })),
worklist: blockers, verificationEvidence: false, activated: false };
}
@@ -0,0 +1,76 @@
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { realpath } from "node:fs/promises";
import path from "node:path";
import { parseQx, walkSyntax } from "./source.js";
import { authoringContext } from "./authoring-context.js";
import { readQxSource } from "./source-loader.js";
import {loadQuixosLock} from "../resource-lock/index.js";
const execFile = promisify(callback);
const git = async (root: string, args: string[]) => (await execFile("git", ["-C", root, ...args], {
maxBuffer: 8 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
})).stdout;
const message = (error: unknown) => String(error instanceof Error ? error.message : error).slice(0, 2000);
/** Syntax-only contract inspection is deliberately NOT verification evidence.
* Each file can recover independently; current valid files always win. */
export async function inspectAuthoringRepository(root: string, historyLimit = 100) {
const names = (await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]))
.split("\0").filter(name => name.endsWith(".qx"));
if (names.length > 128) throw new Error("Repository inspection exceeds 128 QX files; split the resource into smaller repositories");
const files = [];
for (const name of [...new Set(names)].sort()) {
let source = "", errors: unknown[] = [], revision: string | null = null;
try {
source = await readQxSource(root, name);
if (source.length > 262144) throw new Error(`Inspection file exceeds 256 KiB: ${name}`);
errors = parseQx(source, name).diagnostics;
} catch (error) { errors = [{ message: message(error) }]; }
const currentErrors = errors;
if (errors.length) {
// Git can traverse jj's immutable commit DAG without mutating/snapshotting @.
const head = await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"],
{ cwd: root, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" } }).then(r => r.stdout.trim(), () => "HEAD");
const commits = await git(root, ["rev-list", `--max-count=${historyLimit}`, head, "--", name]).catch(() => "");
for (const commit of commits.trim().split("\n").filter(Boolean)) {
const historical = await git(root, ["show", `${commit}:${name}`]).catch(() => null);
if (historical === null || historical.length > 262144) continue;
if (!parseQx(historical, name).diagnostics.length) {
source = historical; revision = commit; errors = []; break;
}
}
}
const syntax = errors.length ? null : parseQx(source, name);
files.push({ file: name, status: errors.length ? "unavailable" : revision ? "historical" : "current",
revision, currentErrors: currentErrors.slice(0, 20), omittedErrors: Math.max(0, currentErrors.length - 20),
declarations: syntax ? [...walkSyntax(syntax.root)]
.filter(node => /^(?:interface|package|atom|state|edge|method|function|event|conformance)\w*Decl$/.test(node.kind))
.map(node => ({ kind: node.kind, source: source.slice(node.start, node.end) })) : [],
});
}
return { verificationEvidence: false as const, resolutionChecked: false as const, files };
}
export async function inspectWorkbench(start: string, selector?: string) {
const context = await authoringContext(start);
if (!selector || selector === ".") {
const relative = path.relative(context.workbench, await realpath(start));
selector = context.resources.find(entry => relative === entry.directory || relative.startsWith(entry.directory + path.sep))?.directory ?? "root";
}
const lock = await loadQuixosLock(path.join(context.workbench, "root/quixos.lock"));
const aliases = lock.ok ? lock.lock.resources.filter(entry => entry.binding === selector) : [];
const selected = context.resources.filter(entry => !selector || selector === entry.directory ||
selector === entry.resourceId || selector === path.basename(entry.directory) || aliases.some(alias => alias.kind === entry.kind && alias.source.repository === entry.source?.repository));
if (!selected.length) throw new Error(`No registered resource matches ${selector}`);
if (selector && selected.length > 1) throw new Error(`Ambiguous resource ${selector}; use its resource ID or directory`);
const resources = [];
for (const entry of selected) {
try {
const root = path.join(context.workbench, entry.directory);
if (await realpath(root) !== root) throw new Error("Managed checkout crosses a symlink");
resources.push({ ...entry, ...await inspectAuthoringRepository(root) });
} catch (error) { resources.push({ ...entry, error: message(error) }); }
}
return { workbench: context.workbench, verificationEvidence: false, resources };
}
@@ -0,0 +1,60 @@
import fs from "node:fs/promises";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { authoringContext } from "./authoring-context.js";
import { inspectAuthoringRepository } from "./authoring-inspect.js";
import { checkRecordName } from "./authoring-check.js";
import { loadQuixosLock } from "../resource-lock/index.js";
import { checkerIdentity } from "./checked-build.js";
const execFile = promisify(callback);
export async function authoringWorklist(start: string) {
const context = await authoringContext(start);
const entries: {directory: string; resourceId?: string; phase: string; message: string; next: string}[] = [];
const dependencies = new Map<string, string[]>();
for (const resource of context.resources) {
const root = path.join(context.workbench, resource.directory);
const add = (phase: string, message: string) => entries.push({directory: resource.directory, resourceId: resource.resourceId, phase, message,
next: phase === "syntax" ? `qx-workspace inspect ${resource.directory}` : phase === "evolution" ? "Inspect evolution.json in the check output; resolve its named migration/review requirements before cutover" : `cd ${resource.directory} && qx-workspace check`});
try {
if (await fs.realpath(root) !== root) throw new Error("Registered checkout crosses a symlink");
const inspected = await inspectAuthoringRepository(root);
for (const file of inspected.files) if (file.currentErrors.length) add("syntax", `${file.file}: ${JSON.stringify(file.currentErrors)}${file.status === "historical" ? `; historical contract available at ${file.revision}` : ""}`);
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) add("resolution", lock.diagnostics.map(entry => `${entry.fileName}: ${entry.message}`).join("\n"));
else dependencies.set(resource.directory, lock.lock.resources.flatMap(dependency => {
const selected = context.resources.find(entry => entry.kind === dependency.kind && entry.source?.repository === dependency.source.repository);
if (selected?.source && selected.source.commit !== dependency.source.commit) add("propagation", `Dependency ${dependency.binding} has advanced; check will repin it automatically`);
return selected ? [selected.directory] : [];
}));
let record;
try { record = JSON.parse(await fs.readFile(path.join(context.workbench, ".quixos/checks", checkRecordName(resource.directory)), "utf8")); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
if (!record?.commit) {
if (record?.blockers?.length) add(record.phase, record.blockers.join("\n"));
else add("unchecked", "No immutable candidate check recorded yet");
continue;
}
if (record.checker !== checkerIdentity()) add("unchecked", "The installed checker changed since the last check");
const env = {...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1"};
const commit = (await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {cwd: root, env})).stdout.trim();
const dirty = await execFile("git", ["diff", "--quiet", "--no-ext-diff", record.commit, "--"], {cwd: root}).then(() => false, () => true);
const untracked = (await execFile("git", ["ls-files", "--others", "--exclude-standard"], {cwd: root})).stdout;
if (commit !== record.commit || dirty || untracked) add("unchecked", `Edits are newer than the last check (${record.commit.slice(0, 12)})`);
else if (record.blockers?.length) add(record.phase, record.blockers.join("\n"));
} catch (error) { add("inspection", String(error).slice(0, 3000)); }
}
// Fixed-point propagation, independent of registration order.
const blocked = new Set(entries.map(entry => entry.directory));
let changed = true;
while (changed) {
changed = false;
for (const [directory, required] of dependencies) if (!blocked.has(directory)) {
const waiting = required.filter(dependency => blocked.has(dependency));
if (waiting.length) { blocked.add(directory); changed = true; entries.push({directory, phase: "dependency", message: `Waiting for ${waiting.join(", ")}`, next: "Resolve the named repositories, then rerun check"}); }
}
}
return { workbench: context.workbench, verificationEvidence: false, worklist: entries,
note: "Derived authoring guidance, not activation approval. Independent repositories can be delegated separately; join writers before a root check." };
}
+58 -86
View File
@@ -7,7 +7,8 @@ import { createHash } from "node:crypto";
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { contentDigest, planEvolution, type EvolutionReview, type WorkspaceRevision } from "../capability-model/index.js";
import { bindingSchema, generateTypeScriptBindings, type BindingSchema, type TypeScriptBindingOptions } from "../bindings/index.js";
import { bindingSchema } from "../bindings/index.js";
import {snapshotCommit, checkoutCommit, buildCheckedPackage} from "./checked-build.js";
const execFile = promisify(execFileCallback);
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
@@ -64,116 +65,87 @@ export const snapshotRepository = async (source: string, destination: string) =>
return { source: root, directory: destination, treeDigest: contentDigest(contents), files: contents };
};
// Local mirrors accelerate resolution, but only their committed locked trees
// may stand in for published dependencies. Never relabel dirty files as a pin.
async function committedResolver(root: string, temporary: string, filename?: string, publishedOnly = false) {
const map = publishedOnly ? {resources: []} : await localResourceSnapshots(root, filename);
const resources = [];
for (const [index, entry] of map.resources.entries()) {
const directory = path.join(temporary, `dependency-${index}`);
await checkoutCommit(entry.directory, entry.commit, directory);
resources.push({...entry, directory});
}
const snapshotMap = path.join(temporary, "snapshots.json");
await fs.writeFile(snapshotMap, JSON.stringify({resources}));
return createGitCapabilityResolver({checkoutRoot: path.join(temporary, "resolved"), snapshotMap});
}
export const checkResourceCandidate = async (options: {root: string; output: string; kind: "package" | "interface"; source: {repository: string; commit: string}; snapshotMap?: string; publishedOnly?: boolean}) => {
await fs.mkdir(options.output, {mode: 0o700});
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-resource-check-"));
const blockers: string[] = [];
let diagnostics = "";
let treeDigest: string | undefined;
let treeDigest: string | undefined, commit: string | undefined, artifactPath: string | undefined;
try {
const root = await snapshotRepository(options.root, path.join(temporary, "root"));
treeDigest = root.treeDigest;
const map = options.publishedOnly ? {resources: []} : await localResourceSnapshots(options.root, options.snapshotMap);
const resources = [];
for (const [index, entry] of map.resources.entries()) {
const snapshot = await snapshotRepository(entry.directory, path.join(temporary, `dependency-${index}`));
resources.push({...entry, directory: snapshot.directory});
}
const snapshotMap = path.join(temporary, "snapshots.json");
await fs.writeFile(snapshotMap, JSON.stringify({resources}));
const resolveResource = await createGitCapabilityResolver({checkoutRoot: path.join(temporary, "resolved"), snapshotMap});
const compiled = await compileCapabilityResourceRepository({rootDirectory: root.directory, kind: options.kind, source: {resolver: "git", ...options.source}, resolveResource});
commit = await snapshotCommit(options.root);
treeDigest = (await snapshotRepository(options.root, path.join(temporary, "observed"))).treeDigest;
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(options.root, temporary, options.snapshotMap, options.publishedOnly);
const compiled = await compileCapabilityResourceRepository({rootDirectory: root, kind: options.kind, source: {resolver: "git", repository: options.source.repository, commit}, resolveResource});
if (compiled.resource.kind === "package") {
const configuration = JSON.parse(await fs.readFile(path.join(root.directory, "quixos.check.json"), "utf8"));
const output = configuration.bindingOutput as string;
if (configuration.backend !== "typescript" || !/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.ts$/.test(output)) throw new Error("Unsupported candidate checker configuration");
const modules = path.join(root.source, "node_modules");
await fs.access(path.join(modules, ".bin/tsc"));
await fs.symlink(modules, path.join(root.directory, "node_modules"), "dir");
const destination = path.join(root.directory, output);
await fs.mkdir(path.dirname(destination), {recursive: true});
await fs.writeFile(destination, generateTypeScriptBindings(bindingSchema(compiled), compiled.resource.revision.revisionId, configuration.options));
diagnostics = (await execFile(path.join(modules, ".bin/tsc"), ["--noEmit", "--pretty", "false"], {cwd: root.directory, maxBuffer: 16 * 1024 * 1024})).stdout;
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(schema, JSON.stringify(bindingSchema(compiled)));
artifactPath = await buildCheckedPackage(root, schema, compiled.resource.revision.revisionId);
}
if (await snapshotCommit(options.root) !== commit) throw new Error("Source changed during verification; run the check again");
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.resource, null, 2));
} catch (error) {
blockers.push(error instanceof Error ? error.message : String(error));
diagnostics += (error as {stdout?: string; stderr?: string}).stdout ?? "";
diagnostics += (error as {stderr?: string}).stderr ?? "";
} finally {await fs.rm(temporary, {recursive: true, force: true});}
const result = {candidateOnly: true, activationEvidence: false, treeDigest, blockers, diagnostics};
const result = {candidateOnly: true, activationEvidence: false, commit, treeDigest, artifactPath, blockers,
note: "Checked immutable candidate; cutover independently checks current migration/review requirements. No publication or activation performed."};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
};
export const checkWorkspaceCandidate = async (options: { root: string; output: string; snapshotMap?: string; baseline?: string; reviews?: string }) => {
// A new output directory is the whole artifact boundary; never overwrite a prior check.
await fs.mkdir(options.output, { mode: 0o700 });
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "quixos-candidate-"));
const blockers: string[] = [];
const checks: unknown[] = [];
const snapshots = [];
export const checkWorkspaceCandidate = async (options: {root: string; output: string; snapshotMap?: string; baseline?: string; reviews?: string}) => {
await fs.mkdir(options.output, {mode: 0o700});
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-workspace-check-"));
const blockers: string[] = [], checks: {packageRevisionId: string; artifactPath: string}[] = [];
let commit: string | undefined;
try {
const root = await snapshotRepository(options.root, path.join(temporary, "root"));
snapshots.push(root);
const map = await localResourceSnapshots(options.root, options.snapshotMap);
const resources = [];
for (const [index, entry] of map.resources.entries()) {
const source = entry.directory;
const snapshot = await snapshotRepository(source, path.join(temporary, `resource-${index}`));
snapshots.push(snapshot);
resources.push({ ...entry, directory: snapshot.directory });
commit = await snapshotCommit(options.root);
for (const entry of (await localResourceSnapshots(options.root, options.snapshotMap)).resources) {
const current = await snapshotCommit(entry.directory);
const tree = async (revision: string) => (await execFile("git", ["rev-parse", `${revision}^{tree}`], {cwd: entry.directory})).stdout.trim();
if (await tree(current) !== await tree(entry.commit)) throw new Error(`Edited resource is not in the root's locked candidate: ${entry.directory}. Check that resource, then run qx-workspace resource upgrade --publish to propagate its revision.`);
}
const mapFile = path.join(temporary, "snapshots.json");
await fs.writeFile(mapFile, JSON.stringify({ resources }));
const resolveResource = await createGitCapabilityResolver({ checkoutRoot: path.join(temporary, "resolved"), snapshotMap: mapFile });
const compiled = await compileWorkspaceRepository({ rootDirectory: root.directory, resolveResource });
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(options.root, temporary, options.snapshotMap);
const compiled = await compileWorkspaceRepository({rootDirectory: root, sourceRootCommit: commit, resolveResource});
const baseline = options.baseline ? JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision : null;
const reviews = options.reviews ? JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[] : [];
const evolution = planEvolution(baseline, compiled.workspace, { reviews });
const evolution = planEvolution(baseline, compiled.workspace, {reviews});
blockers.push(...evolution.blockers);
const schema: BindingSchema = { format: "quixos-bindings", version: 1, interfaces: compiled.workspace.interfaceImports, packages: compiled.workspace.packageImports };
for (const resource of compiled.resources.filter((entry) => entry.kind === "package")) {
if (resource.resource.kind !== "package") continue;
const revision = resource.resource.revision;
let config: { backend: string; bindingOutput: string; options?: TypeScriptBindingOptions };
try { config = JSON.parse(await fs.readFile(path.join(resource.directory, "quixos.check.json"), "utf8")); }
catch { blockers.push(`No candidate checker configured for ${revision.revisionId} (quixos.check.json)`); continue; }
if (config.backend !== "typescript" || !/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.ts$/.test(config.bindingOutput)
|| config.bindingOutput.split("/").includes("..")) { blockers.push(`Unsupported checker or binding path for ${revision.revisionId}`); continue; }
const sourceSnapshot = snapshots.find((entry) => entry.directory === resource.directory);
const dependencyRoot = sourceSnapshot?.source ?? resource.directory;
const modules = path.join(dependencyRoot, "node_modules");
try { await fs.access(path.join(modules, ".bin", "tsc")); }
catch { blockers.push(`Missing installed TypeScript checker/dependencies for ${revision.revisionId}; install its locked development dependencies first`); continue; }
if (sourceSnapshot) await fs.symlink(modules, path.join(resource.directory, "node_modules"), "dir");
const generated = generateTypeScriptBindings(schema, revision.revisionId, config.options);
const destination = path.join(resource.directory, config.bindingOutput);
await fs.mkdir(path.dirname(destination), { recursive: true });
await fs.writeFile(destination, generated);
let success = false, diagnostics = "";
try { diagnostics = (await execFile(path.join(modules, ".bin", "tsc"), ["--noEmit", "--pretty", "false", "--listFiles"], { cwd: resource.directory, maxBuffer: 16 * 1024 * 1024 })).stdout; success = true; }
catch (error) { const result = error as Error & {stdout?: string; stderr?: string}; diagnostics = `${result.stdout ?? ""}\n${result.stderr ?? result.message}`; }
const typeInputs = [];
for (const line of diagnostics.split(/\r?\n/)) if (path.isAbsolute(line) && /\.[cm]?tsx?$/.test(line)) {
try { typeInputs.push({file: line, digest: bytesDigest(await fs.readFile(line))}); } catch { success = false; }
for (const resource of compiled.resources.filter(entry => entry.kind === "package")) {
// Per-package recursive schema, identical to host activation, not unrelated
// workspace declarations that would unnecessarily invalidate build caches.
const candidate = await compileCapabilityResourceRepository({rootDirectory: resource.directory, kind: "package", source: resource.source, resolveResource});
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(schema, JSON.stringify(bindingSchema(candidate)));
const artifactPath = await buildCheckedPackage(resource.directory, schema, candidate.resource.revision.revisionId);
checks.push({packageRevisionId: candidate.resource.revision.revisionId, artifactPath});
}
const checker = await fs.realpath(path.join(modules, ".bin", "tsc"));
const check = { packageRevisionId: revision.revisionId, success, bindingSchemaDigest: contentDigest(schema), generatedDigest: contentDigest(generated),
checkerDigest: contentDigest({ executable: bytesDigest(await fs.readFile(checker)), typeInputs }), diagnostics };
checks.push(check);
if (!success) blockers.push(`Typecheck failed for ${revision.revisionId}`);
}
const result = { schemaVersion: 1, candidateOnly: true, activationEvidence: false,
sourceDigest: contentDigest(snapshots.map(({source, treeDigest}) => ({source, treeDigest}))),
snapshots: snapshots.map(({source, treeDigest}) => ({source, treeDigest})), evolution, checks, blockers,
note: "Local source-tree checks do not certify old Git revisions. Publication must repin the DAG and recheck final immutable artifacts." };
if (await snapshotCommit(options.root) !== commit) throw new Error("Source changed during verification; run the check again");
const result = {schemaVersion: 1, candidateOnly: true, activationEvidence: false, commit, evolution, checks, blockers,
note: "Checks the committed root and its exact locked dependencies. Resource edits must be verified and repinned before they enter this candidate. No publication or activation performed."};
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.workspace, null, 2));
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} catch (error) {
const result = { schemaVersion: 1, candidateOnly: true, activationEvidence: false, checks, blockers: [...blockers, error instanceof Error ? error.message : String(error)] };
const result = {schemaVersion: 1, candidateOnly: true, activationEvidence: false, commit, checks, blockers: [...blockers, error instanceof Error ? error.message : String(error)]};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} finally { await fs.rm(temporary, { recursive: true, force: true }); }
} finally {await fs.rm(temporary, {recursive: true, force: true});}
};
+95
View File
@@ -0,0 +1,95 @@
import fs from "node:fs/promises";
import path from "node:path";
import {execFile as callback, spawn} from "node:child_process";
import { createWriteStream } from "node:fs";
import {promisify} from "node:util";
import {fileURLToPath} from "node:url";
const execFile = promisify(callback);
const environment = () => ({...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", GIT_TERMINAL_PROMPT: "0"});
export const checkerIdentity = () => process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
/** Checking snapshots jj, but never publishes or activates the working copy. */
export async function snapshotCommit(root: string): Promise<string> {
const run = async (...args: string[]) => (await execFile("jj", args, {cwd: root, env: environment()})).stdout.trim();
await run("status");
// jj resolve --list exits 1 on a clean revision. Query structured revision
// metadata instead of depending on diagnostic wording or swallowing errors.
if (await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "conflict") !== "false") throw new Error("Resolve source conflicts before verification");
const commit = await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id");
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Verification requires an exact jj commit");
await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], {cwd: root, env: environment()});
const {stdout} = await execFile("git", ["ls-files", "--others", "--exclude-standard", "-z"], {cwd: root});
if (stdout) throw new Error("Source contains files not captured by jj; inspect jj tracking before verification");
return commit;
}
/** Use Git's actual committed tree, never dirty overlays labelled as old pins. */
export async function checkoutCommit(root: string, commit: string, destination: string) {
await fs.mkdir(destination, {recursive: true});
const archive = await execFile("git", ["archive", "--format=tar", commit], {cwd: root, encoding: "buffer", maxBuffer: 128 * 1024 * 1024});
await new Promise<void>((resolve, reject) => {
const child = spawn("tar", ["-xf", "-", "-C", destination], {stdio: ["pipe", "ignore", "pipe"]});
let error = "";
child.stderr.on("data", chunk => {error += chunk;});
child.on("error", reject);
child.on("close", code => code === 0 ? resolve() : reject(new Error(`Cannot extract committed source: ${error}`)));
child.stdin.on("error", reject);
child.stdin.end(archive.stdout);
});
}
/** Shared by provisional checks, template publication and host activation.
* The Nix derivation is the cached check; its metadata is internal provenance,
* not a certificate authored or approved by the workspace agent.
*/
export async function buildCheckedPackage(source: string, schema: string, packageRevisionId: string): Promise<string> {
const generator = process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
if (!/^\/nix\/store\/[^/]+$/.test(generator)) throw new Error("Run verification with the installed Quixos tooling (its exact Nix checker is required)");
const builder = path.join(generator, "share/checked-package.nix");
await fs.access(builder);
return await new Promise((resolve, reject) => {
const child = spawn("nix", ["build", "--impure", "--file", builder,
"--argstr", "source", source, "--argstr", "schema", schema,
"--argstr", "generator", generator, "--argstr", "packageRevisionId", packageRevisionId,
"--no-link", "--print-out-paths", "-L"], {env: environment(), stdio: ["ignore", "pipe", "inherit"]});
let output = "";
child.stdout.on("data", chunk => {output += chunk;});
child.on("error", reject);
child.on("close", code => {
const artifact = output.trim();
if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact)) reject(new Error(`Checked Nix build failed (${code}); see build diagnostics above`));
else resolve(artifact);
});
});
}
/** Exact remote source DAG; the Nix checker owns schema construction and all
* nested fetches. Keep build noise in a named log, with a bounded failure tail. */
export async function buildImmutableCandidate(source: { repository: string; commit: string }, kind: "workspace" | "interface" | "package", logFile: string, contractOnly = false): Promise<string> {
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit)) throw new Error("An immutable candidate requires an exact commit");
const url = new URL(source.repository);
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Candidate origin must be credential-free HTTPS");
const generator = process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
if (!/^\/nix\/store\/[^/]+$/.test(generator)) throw new Error("Use the installed Quixos checker");
const builder = path.join(generator, "share/checked-candidate.nix");
await fs.access(builder);
const log = createWriteStream(logFile, { flags: "wx", mode: 0o600 });
await new Promise<void>((resolve, reject) => { log.once("open", () => resolve()); log.once("error", reject); });
return await new Promise((resolve, reject) => {
let output = "", tail = "", failure: Error | undefined;
const child = spawn("nix", ["build", "--impure", "--file", builder,
"--argstr", "repository", source.repository, "--argstr", "commit", source.commit,
"--argstr", "kind", kind, "--argstr", "generator", generator,
"--arg", "contractOnly", contractOnly ? "true" : "false",
"--no-link", "--print-out-paths", "-L"], { env: environment(), stdio: ["ignore", "pipe", "pipe"] });
log.on("error", error => { failure = error; child.kill(); });
child.stdout.on("data", chunk => { output += chunk; });
child.stderr.on("data", chunk => { log.write(chunk); tail = (tail + String(chunk)).slice(-6000); });
child.on("error", error => { failure = error; });
child.on("close", code => log.end(() => {
if (failure) reject(failure);
else if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(output.trim())) reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
else resolve(output.trim());
}));
});
}
+21
View File
@@ -0,0 +1,21 @@
import { spawn } from "node:child_process";
/** Kernel-owned lock: a crashed coordinator cannot leave a stale ownership file.
* The persistent file is just an inode; EOF releases the helper's lock. */
export async function withFileLock<T>(filename: string, work: () => Promise<T>): Promise<T> {
const child = spawn("flock", ["--exclusive", "--timeout", "120", "--conflict-exit-code", "75", filename,
process.execPath, "-e", 'process.stdout.write("locked\\n"); process.stdin.resume();'], {stdio: ["pipe", "pipe", "pipe"]});
let diagnostics = "";
child.stdin.on("error", () => { /* acquisition/exit handling reports helper failure */ });
child.stderr.on("data", chunk => { diagnostics = (diagnostics + String(chunk)).slice(-2000); });
const closed = new Promise<void>((resolve) => { child.once("close", () => resolve()); });
try {
await new Promise<void>((resolve, reject) => {
let output = "";
child.once("error", reject);
child.once("exit", code => reject(new Error(code === 75 ? "Timed out after 120 seconds waiting for another authoring command; inspect that command before retrying" : `Cannot acquire authoring lock: ${diagnostics}`)));
child.stdout.on("data", chunk => { output += chunk; if (output.includes("locked\n")) resolve(); });
});
return await work();
} finally { child.stdin.end(); await closed; }
}
File diff suppressed because one or more lines are too long
@@ -76,37 +76,38 @@ ATOM_REF=75
INTERFACE_REF=76
OPTIONAL=77
LIST=78
BOOL=79
BYTES=80
DOUBLE=81
INT32=82
INT64=83
STRING=84
UINT32=85
UINT64=86
TRUE=87
FALSE=88
NULL=89
ARROW=90
COLON=91
SEMI=92
COMMA=93
DOT=94
LBRACE=95
RBRACE=96
LBRACK=97
RBRACK=98
LPAREN=99
RPAREN=100
LT=101
GT=102
INTEGER=103
JSON_NUMBER=104
IDENTIFIER=105
STRING_LITERAL=106
LINE_COMMENT=107
BLOCK_COMMENT=108
WS=109
RECORD=79
BOOL=80
BYTES=81
DOUBLE=82
INT32=83
INT64=84
STRING=85
UINT32=86
UINT64=87
TRUE=88
FALSE=89
NULL=90
ARROW=91
COLON=92
SEMI=93
COMMA=94
DOT=95
LBRACE=96
RBRACE=97
LBRACK=98
RBRACK=99
LPAREN=100
RPAREN=101
LT=102
GT=103
INTEGER=104
JSON_NUMBER=105
IDENTIFIER=106
STRING_LITERAL=107
LINE_COMMENT=108
BLOCK_COMMENT=109
WS=110
'workspace'=1
'fragment'=2
'import'=3
@@ -185,27 +186,28 @@ WS=109
'interface-ref'=76
'optional'=77
'list'=78
'bool'=79
'bytes'=80
'double'=81
'int32'=82
'int64'=83
'string'=84
'uint32'=85
'uint64'=86
'true'=87
'false'=88
'null'=89
'->'=90
':'=91
';'=92
','=93
'.'=94
'{'=95
'}'=96
'['=97
']'=98
'('=99
')'=100
'<'=101
'>'=102
'record'=79
'bool'=80
'bytes'=81
'double'=82
'int32'=83
'int64'=84
'string'=85
'uint32'=86
'uint64'=87
'true'=88
'false'=89
'null'=90
'->'=91
':'=92
';'=93
','=94
'.'=95
'{'=96
'}'=97
'['=98
']'=99
'('=100
')'=101
'<'=102
'>'=103
File diff suppressed because one or more lines are too long
@@ -76,37 +76,38 @@ ATOM_REF=75
INTERFACE_REF=76
OPTIONAL=77
LIST=78
BOOL=79
BYTES=80
DOUBLE=81
INT32=82
INT64=83
STRING=84
UINT32=85
UINT64=86
TRUE=87
FALSE=88
NULL=89
ARROW=90
COLON=91
SEMI=92
COMMA=93
DOT=94
LBRACE=95
RBRACE=96
LBRACK=97
RBRACK=98
LPAREN=99
RPAREN=100
LT=101
GT=102
INTEGER=103
JSON_NUMBER=104
IDENTIFIER=105
STRING_LITERAL=106
LINE_COMMENT=107
BLOCK_COMMENT=108
WS=109
RECORD=79
BOOL=80
BYTES=81
DOUBLE=82
INT32=83
INT64=84
STRING=85
UINT32=86
UINT64=87
TRUE=88
FALSE=89
NULL=90
ARROW=91
COLON=92
SEMI=93
COMMA=94
DOT=95
LBRACE=96
RBRACE=97
LBRACK=98
RBRACK=99
LPAREN=100
RPAREN=101
LT=102
GT=103
INTEGER=104
JSON_NUMBER=105
IDENTIFIER=106
STRING_LITERAL=107
LINE_COMMENT=108
BLOCK_COMMENT=109
WS=110
'workspace'=1
'fragment'=2
'import'=3
@@ -185,27 +186,28 @@ WS=109
'interface-ref'=76
'optional'=77
'list'=78
'bool'=79
'bytes'=80
'double'=81
'int32'=82
'int64'=83
'string'=84
'uint32'=85
'uint64'=86
'true'=87
'false'=88
'null'=89
'->'=90
':'=91
';'=92
','=93
'.'=94
'{'=95
'}'=96
'['=97
']'=98
'('=99
')'=100
'<'=101
'>'=102
'record'=79
'bool'=80
'bytes'=81
'double'=82
'int32'=83
'int64'=84
'string'=85
'uint32'=86
'uint64'=87
'true'=88
'false'=89
'null'=90
'->'=91
':'=92
';'=93
','=94
'.'=95
'{'=96
'}'=97
'['=98
']'=99
'('=100
')'=101
'<'=102
'>'=103
@@ -82,37 +82,38 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
public static readonly INTERFACE_REF = 76;
public static readonly OPTIONAL = 77;
public static readonly LIST = 78;
public static readonly BOOL = 79;
public static readonly BYTES = 80;
public static readonly DOUBLE = 81;
public static readonly INT32 = 82;
public static readonly INT64 = 83;
public static readonly STRING = 84;
public static readonly UINT32 = 85;
public static readonly UINT64 = 86;
public static readonly TRUE = 87;
public static readonly FALSE = 88;
public static readonly NULL = 89;
public static readonly ARROW = 90;
public static readonly COLON = 91;
public static readonly SEMI = 92;
public static readonly COMMA = 93;
public static readonly DOT = 94;
public static readonly LBRACE = 95;
public static readonly RBRACE = 96;
public static readonly LBRACK = 97;
public static readonly RBRACK = 98;
public static readonly LPAREN = 99;
public static readonly RPAREN = 100;
public static readonly LT = 101;
public static readonly GT = 102;
public static readonly INTEGER = 103;
public static readonly JSON_NUMBER = 104;
public static readonly IDENTIFIER = 105;
public static readonly STRING_LITERAL = 106;
public static readonly LINE_COMMENT = 107;
public static readonly BLOCK_COMMENT = 108;
public static readonly WS = 109;
public static readonly RECORD = 79;
public static readonly BOOL = 80;
public static readonly BYTES = 81;
public static readonly DOUBLE = 82;
public static readonly INT32 = 83;
public static readonly INT64 = 84;
public static readonly STRING = 85;
public static readonly UINT32 = 86;
public static readonly UINT64 = 87;
public static readonly TRUE = 88;
public static readonly FALSE = 89;
public static readonly NULL = 90;
public static readonly ARROW = 91;
public static readonly COLON = 92;
public static readonly SEMI = 93;
public static readonly COMMA = 94;
public static readonly DOT = 95;
public static readonly LBRACE = 96;
public static readonly RBRACE = 97;
public static readonly LBRACK = 98;
public static readonly RBRACK = 99;
public static readonly LPAREN = 100;
public static readonly RPAREN = 101;
public static readonly LT = 102;
public static readonly GT = 103;
public static readonly INTEGER = 104;
public static readonly JSON_NUMBER = 105;
public static readonly IDENTIFIER = 106;
public static readonly STRING_LITERAL = 107;
public static readonly LINE_COMMENT = 108;
public static readonly BLOCK_COMMENT = 109;
public static readonly WS = 110;
public static readonly channelNames = [
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
@@ -133,10 +134,10 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
"'watch-stop'", "'subscribe'", "'unsubscribe'", "'optimistic-register'",
"'crdt'", "'optional-one'", "'exactly-one'", "'many-unique'", "'many'",
"'ordered'", "'unit'", "'watch-handle'", "'message'", "'atom-ref'",
"'interface-ref'", "'optional'", "'list'", "'bool'", "'bytes'",
"'double'", "'int32'", "'int64'", "'string'", "'uint32'", "'uint64'",
"'true'", "'false'", "'null'", "'->'", "':'", "';'", "','", "'.'",
"'{'", "'}'", "'['", "']'", "'('", "')'", "'<'", "'>'"
"'interface-ref'", "'optional'", "'list'", "'record'", "'bool'",
"'bytes'", "'double'", "'int32'", "'int64'", "'string'", "'uint32'",
"'uint64'", "'true'", "'false'", "'null'", "'->'", "':'", "';'",
"','", "'.'", "'{'", "'}'", "'['", "']'", "'('", "')'", "'<'", "'>'"
];
public static readonly symbolicNames = [
@@ -152,12 +153,12 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
"DISCONNECT", "CALL", "WATCH_START", "WATCH_STOP", "SUBSCRIBE",
"UNSUBSCRIBE", "OPTIMISTIC_REGISTER", "CRDT", "OPTIONAL_ONE", "EXACTLY_ONE",
"MANY_UNIQUE", "MANY", "ORDERED", "UNIT", "WATCH_HANDLE", "MESSAGE",
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "BOOL", "BYTES",
"DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64", "TRUE",
"FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT", "LBRACE",
"RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT", "GT", "INTEGER",
"JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL", "LINE_COMMENT", "BLOCK_COMMENT",
"WS"
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "RECORD", "BOOL",
"BYTES", "DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64",
"TRUE", "FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT",
"LBRACE", "RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT",
"GT", "INTEGER", "JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL",
"LINE_COMMENT", "BLOCK_COMMENT", "WS"
];
public static readonly modeNames = [
@@ -177,12 +178,12 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
"DISCONNECT", "CALL", "WATCH_START", "WATCH_STOP", "SUBSCRIBE",
"UNSUBSCRIBE", "OPTIMISTIC_REGISTER", "CRDT", "OPTIONAL_ONE", "EXACTLY_ONE",
"MANY_UNIQUE", "MANY", "ORDERED", "UNIT", "WATCH_HANDLE", "MESSAGE",
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "BOOL", "BYTES",
"DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64", "TRUE",
"FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT", "LBRACE",
"RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT", "GT", "INTEGER",
"JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL", "ESC", "HEX", "LINE_COMMENT",
"BLOCK_COMMENT", "WS",
"ATOM_REF", "INTERFACE_REF", "OPTIONAL", "LIST", "RECORD", "BOOL",
"BYTES", "DOUBLE", "INT32", "INT64", "STRING", "UINT32", "UINT64",
"TRUE", "FALSE", "NULL", "ARROW", "COLON", "SEMI", "COMMA", "DOT",
"LBRACE", "RBRACE", "LBRACK", "RBRACK", "LPAREN", "RPAREN", "LT",
"GT", "INTEGER", "JSON_NUMBER", "IDENTIFIER", "STRING_LITERAL",
"ESC", "HEX", "LINE_COMMENT", "BLOCK_COMMENT", "WS",
];
@@ -204,7 +205,7 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
public get modeNames(): string[] { return QuixosCapabilityLexer.modeNames; }
public static readonly _serializedATN: number[] = [
4,0,109,1047,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,
4,0,110,1056,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,
5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,
2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,
7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,
@@ -221,364 +222,368 @@ export class QuixosCapabilityLexer extends antlr.Lexer {
2,91,7,91,2,92,7,92,2,93,7,93,2,94,7,94,2,95,7,95,2,96,7,96,2,97,
7,97,2,98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103,
7,103,2,104,7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108,
2,109,7,109,2,110,7,110,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,
1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,5,1,5,
1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,
1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,
1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,
1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,
1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,
1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,
1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,15,1,16,
1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,19,1,19,1,19,
1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,21,
1,21,1,21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,
1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,
1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,27,1,27,1,27,
1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,29,
1,29,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,31,1,31,1,31,1,31,
1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,
1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,
1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,
1,36,1,36,1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,1,37,1,37,
1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,38,
1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,
1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,40,1,41,1,41,
1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,
1,41,1,41,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,44,
1,44,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,46,1,46,
1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,48,
1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1,50,1,51,1,51,
1,51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,53,1,53,1,53,
1,53,1,53,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,
1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1,57,1,57,
1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,
1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,60,1,60,
1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,
1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,
1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,64,
1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,
1,64,1,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,65,1,66,1,66,
1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,
1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,68,1,68,1,68,
1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,69,1,69,1,69,1,69,
1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,71,1,71,1,71,1,71,
1,71,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,
1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,
1,74,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,
1,75,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,76,1,76,
1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1,79,
1,79,1,79,1,79,1,79,1,79,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,81,
1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,82,1,83,1,83,
1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,85,
1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1,86,1,87,1,87,
1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1,89,1,89,1,89,1,90,
1,90,1,91,1,91,1,92,1,92,1,93,1,93,1,94,1,94,1,95,1,95,1,96,1,96,
1,97,1,97,1,98,1,98,1,99,1,99,1,100,1,100,1,101,1,101,1,102,3,102,
948,8,102,1,102,4,102,951,8,102,11,102,12,102,952,1,103,3,103,956,
8,103,1,103,1,103,1,103,5,103,961,8,103,10,103,12,103,964,9,103,
3,103,966,8,103,1,103,1,103,4,103,970,8,103,11,103,12,103,971,3,
103,974,8,103,1,103,1,103,3,103,978,8,103,1,103,4,103,981,8,103,
11,103,12,103,982,3,103,985,8,103,1,104,1,104,5,104,989,8,104,10,
104,12,104,992,9,104,1,105,1,105,1,105,5,105,997,8,105,10,105,12,
105,1000,9,105,1,105,1,105,1,106,1,106,1,106,1,106,1,106,1,106,1,
106,1,106,3,106,1012,8,106,1,107,1,107,1,108,1,108,1,108,1,108,5,
108,1020,8,108,10,108,12,108,1023,9,108,1,108,1,108,1,109,1,109,
1,109,1,109,5,109,1031,8,109,10,109,12,109,1034,9,109,1,109,1,109,
1,109,1,109,1,109,1,110,4,110,1042,8,110,11,110,12,110,1043,1,110,
1,110,1,1032,0,111,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,
21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,
43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,29,59,30,61,31,63,32,
65,33,67,34,69,35,71,36,73,37,75,38,77,39,79,40,81,41,83,42,85,43,
87,44,89,45,91,46,93,47,95,48,97,49,99,50,101,51,103,52,105,53,107,
54,109,55,111,56,113,57,115,58,117,59,119,60,121,61,123,62,125,63,
127,64,129,65,131,66,133,67,135,68,137,69,139,70,141,71,143,72,145,
73,147,74,149,75,151,76,153,77,155,78,157,79,159,80,161,81,163,82,
165,83,167,84,169,85,171,86,173,87,175,88,177,89,179,90,181,91,183,
92,185,93,187,94,189,95,191,96,193,97,195,98,197,99,199,100,201,
101,203,102,205,103,207,104,209,105,211,106,213,0,215,0,217,107,
219,108,221,109,1,0,11,1,0,48,57,1,0,49,57,2,0,69,69,101,101,2,0,
43,43,45,45,3,0,65,90,95,95,97,122,4,0,48,57,65,90,95,95,97,122,
4,0,10,10,13,13,34,34,92,92,8,0,34,34,47,47,92,92,98,98,102,102,
110,110,114,114,116,116,3,0,48,57,65,70,97,102,2,0,10,10,13,13,3,
0,9,10,13,13,32,32,1061,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,
1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,
1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,
1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,
1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,
1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,
1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,
1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,
1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,
1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,
1,0,0,0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,
107,1,0,0,0,0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,
0,0,0,117,1,0,0,0,0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,
1,0,0,0,0,127,1,0,0,0,0,129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,
0,135,1,0,0,0,0,137,1,0,0,0,0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,
0,0,0,0,145,1,0,0,0,0,147,1,0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,
153,1,0,0,0,0,155,1,0,0,0,0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,
0,0,0,163,1,0,0,0,0,165,1,0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,
1,0,0,0,0,173,1,0,0,0,0,175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,
0,181,1,0,0,0,0,183,1,0,0,0,0,185,1,0,0,0,0,187,1,0,0,0,0,189,1,
0,0,0,0,191,1,0,0,0,0,193,1,0,0,0,0,195,1,0,0,0,0,197,1,0,0,0,0,
199,1,0,0,0,0,201,1,0,0,0,0,203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,
0,0,0,209,1,0,0,0,0,211,1,0,0,0,0,217,1,0,0,0,0,219,1,0,0,0,0,221,
1,0,0,0,1,223,1,0,0,0,3,233,1,0,0,0,5,242,1,0,0,0,7,249,1,0,0,0,
9,258,1,0,0,0,11,263,1,0,0,0,13,273,1,0,0,0,15,284,1,0,0,0,17,292,
1,0,0,0,19,298,1,0,0,0,21,307,1,0,0,0,23,317,1,0,0,0,25,326,1,0,
0,0,27,338,1,0,0,0,29,349,1,0,0,0,31,355,1,0,0,0,33,363,1,0,0,0,
35,366,1,0,0,0,37,371,1,0,0,0,39,374,1,0,0,0,41,382,1,0,0,0,43,389,
1,0,0,0,45,395,1,0,0,0,47,400,1,0,0,0,49,411,1,0,0,0,51,416,1,0,
0,0,53,422,1,0,0,0,55,426,1,0,0,0,57,438,1,0,0,0,59,441,1,0,0,0,
61,448,1,0,0,0,63,451,1,0,0,0,65,458,1,0,0,0,67,466,1,0,0,0,69,473,
1,0,0,0,71,484,1,0,0,0,73,491,1,0,0,0,75,500,1,0,0,0,77,515,1,0,
0,0,79,525,1,0,0,0,81,538,1,0,0,0,83,544,1,0,0,0,85,561,1,0,0,0,
87,564,1,0,0,0,89,568,1,0,0,0,91,573,1,0,0,0,93,579,1,0,0,0,95,588,
1,0,0,0,97,597,1,0,0,0,99,601,1,0,0,0,101,605,1,0,0,0,103,609,1,
0,0,0,105,615,1,0,0,0,107,621,1,0,0,0,109,626,1,0,0,0,111,631,1,
0,0,0,113,637,1,0,0,0,115,645,1,0,0,0,117,653,1,0,0,0,119,664,1,
0,0,0,121,669,1,0,0,0,123,681,1,0,0,0,125,692,1,0,0,0,127,702,1,
0,0,0,129,714,1,0,0,0,131,734,1,0,0,0,133,739,1,0,0,0,135,752,1,
0,0,0,137,764,1,0,0,0,139,776,1,0,0,0,141,781,1,0,0,0,143,789,1,
0,0,0,145,794,1,0,0,0,147,807,1,0,0,0,149,815,1,0,0,0,151,824,1,
0,0,0,153,838,1,0,0,0,155,847,1,0,0,0,157,852,1,0,0,0,159,857,1,
0,0,0,161,863,1,0,0,0,163,870,1,0,0,0,165,876,1,0,0,0,167,882,1,
0,0,0,169,889,1,0,0,0,171,896,1,0,0,0,173,903,1,0,0,0,175,908,1,
0,0,0,177,914,1,0,0,0,179,919,1,0,0,0,181,922,1,0,0,0,183,924,1,
0,0,0,185,926,1,0,0,0,187,928,1,0,0,0,189,930,1,0,0,0,191,932,1,
0,0,0,193,934,1,0,0,0,195,936,1,0,0,0,197,938,1,0,0,0,199,940,1,
0,0,0,201,942,1,0,0,0,203,944,1,0,0,0,205,947,1,0,0,0,207,955,1,
0,0,0,209,986,1,0,0,0,211,993,1,0,0,0,213,1003,1,0,0,0,215,1013,
1,0,0,0,217,1015,1,0,0,0,219,1026,1,0,0,0,221,1041,1,0,0,0,223,224,
5,119,0,0,224,225,5,111,0,0,225,226,5,114,0,0,226,227,5,107,0,0,
227,228,5,115,0,0,228,229,5,112,0,0,229,230,5,97,0,0,230,231,5,99,
0,0,231,232,5,101,0,0,232,2,1,0,0,0,233,234,5,102,0,0,234,235,5,
114,0,0,235,236,5,97,0,0,236,237,5,103,0,0,237,238,5,109,0,0,238,
239,5,101,0,0,239,240,5,110,0,0,240,241,5,116,0,0,241,4,1,0,0,0,
242,243,5,105,0,0,243,244,5,109,0,0,244,245,5,112,0,0,245,246,5,
111,0,0,246,247,5,114,0,0,247,248,5,116,0,0,248,6,1,0,0,0,249,250,
5,101,0,0,250,251,5,120,0,0,251,252,5,116,0,0,252,253,5,101,0,0,
253,254,5,114,0,0,254,255,5,110,0,0,255,256,5,97,0,0,256,257,5,108,
0,0,257,8,1,0,0,0,258,259,5,97,0,0,259,260,5,116,0,0,260,261,5,111,
0,0,261,262,5,109,0,0,262,10,1,0,0,0,263,264,5,105,0,0,264,265,5,
110,0,0,265,266,5,116,0,0,266,267,5,101,0,0,267,268,5,114,0,0,268,
269,5,102,0,0,269,270,5,97,0,0,270,271,5,99,0,0,271,272,5,101,0,
0,272,12,1,0,0,0,273,274,5,105,0,0,274,275,5,110,0,0,275,276,5,116,
0,0,276,277,5,101,0,0,277,278,5,114,0,0,278,279,5,102,0,0,279,280,
5,97,0,0,280,281,5,99,0,0,281,282,5,101,0,0,282,283,5,115,0,0,283,
14,1,0,0,0,284,285,5,112,0,0,285,286,5,97,0,0,286,287,5,99,0,0,287,
288,5,107,0,0,288,289,5,97,0,0,289,290,5,103,0,0,290,291,5,101,0,
0,291,16,1,0,0,0,292,293,5,118,0,0,293,294,5,97,0,0,294,295,5,108,
0,0,295,296,5,117,0,0,296,297,5,101,0,0,297,18,1,0,0,0,298,299,5,
114,0,0,299,300,5,101,0,0,300,301,5,108,0,0,301,302,5,97,0,0,302,
303,5,116,0,0,303,304,5,105,0,0,304,305,5,111,0,0,305,306,5,110,
0,0,306,20,1,0,0,0,307,308,5,111,0,0,308,309,5,112,0,0,309,310,5,
101,0,0,310,311,5,114,0,0,311,312,5,97,0,0,312,313,5,116,0,0,313,
314,5,105,0,0,314,315,5,111,0,0,315,316,5,110,0,0,316,22,1,0,0,0,
317,318,5,102,0,0,318,319,5,117,0,0,319,320,5,110,0,0,320,321,5,
99,0,0,321,322,5,116,0,0,322,323,5,105,0,0,323,324,5,111,0,0,324,
325,5,110,0,0,325,24,1,0,0,0,326,327,5,99,0,0,327,328,5,111,0,0,
328,329,5,110,0,0,329,330,5,115,0,0,330,331,5,116,0,0,331,332,5,
114,0,0,332,333,5,117,0,0,333,334,5,99,0,0,334,335,5,116,0,0,335,
336,5,111,0,0,336,337,5,114,0,0,337,26,1,0,0,0,338,339,5,99,0,0,
339,340,5,111,0,0,340,341,5,110,0,0,341,342,5,115,0,0,342,343,5,
116,0,0,343,344,5,114,0,0,344,345,5,117,0,0,345,346,5,99,0,0,346,
347,5,116,0,0,347,348,5,115,0,0,348,28,1,0,0,0,349,350,5,105,0,0,
350,351,5,110,0,0,351,352,5,112,0,0,352,353,5,117,0,0,353,354,5,
116,0,0,354,30,1,0,0,0,355,356,5,99,0,0,356,357,5,111,0,0,357,358,
5,110,0,0,358,359,5,102,0,0,359,360,5,111,0,0,360,361,5,114,0,0,
361,362,5,109,0,0,362,32,1,0,0,0,363,364,5,97,0,0,364,365,5,115,
0,0,365,34,1,0,0,0,366,367,5,98,0,0,367,368,5,105,0,0,368,369,5,
110,0,0,369,370,5,100,0,0,370,36,1,0,0,0,371,372,5,116,0,0,372,373,
5,111,0,0,373,38,1,0,0,0,374,375,5,112,0,0,375,376,5,114,0,0,376,
377,5,105,0,0,377,378,5,118,0,0,378,379,5,97,0,0,379,380,5,116,0,
0,380,381,5,101,0,0,381,40,1,0,0,0,382,383,5,115,0,0,383,384,5,104,
0,0,384,385,5,97,0,0,385,386,5,114,0,0,386,387,5,101,0,0,387,388,
5,100,0,0,388,42,1,0,0,0,389,390,5,115,0,0,390,391,5,116,0,0,391,
392,5,97,0,0,392,393,5,116,0,0,393,394,5,101,0,0,394,44,1,0,0,0,
395,396,5,101,0,0,396,397,5,100,0,0,397,398,5,103,0,0,398,399,5,
101,0,0,399,46,1,0,0,0,400,401,5,112,0,0,401,402,5,114,0,0,402,403,
5,111,0,0,403,404,5,106,0,0,404,405,5,101,0,0,405,406,5,99,0,0,406,
407,5,116,0,0,407,408,5,105,0,0,408,409,5,111,0,0,409,410,5,110,
0,0,410,48,1,0,0,0,411,412,5,119,0,0,412,413,5,105,0,0,413,414,5,
116,0,0,414,415,5,104,0,0,415,50,1,0,0,0,416,417,5,117,0,0,417,418,
5,115,0,0,418,419,5,105,0,0,419,420,5,110,0,0,420,421,5,103,0,0,
421,52,1,0,0,0,422,423,5,118,0,0,423,424,5,105,0,0,424,425,5,97,
0,0,425,54,1,0,0,0,426,427,5,109,0,0,427,428,5,97,0,0,428,429,5,
116,0,0,429,430,5,101,0,0,430,431,5,114,0,0,431,432,5,105,0,0,432,
433,5,97,0,0,433,434,5,108,0,0,434,435,5,105,0,0,435,436,5,122,0,
0,436,437,5,101,0,0,437,56,1,0,0,0,438,439,5,105,0,0,439,440,5,102,
0,0,440,58,1,0,0,0,441,442,5,97,0,0,442,443,5,98,0,0,443,444,5,115,
0,0,444,445,5,101,0,0,445,446,5,110,0,0,446,447,5,116,0,0,447,60,
1,0,0,0,448,449,5,111,0,0,449,450,5,110,0,0,450,62,1,0,0,0,451,452,
5,112,0,0,452,453,5,111,0,0,453,454,5,108,0,0,454,455,5,105,0,0,
455,456,5,99,0,0,456,457,5,121,0,0,457,64,1,0,0,0,458,459,5,100,
0,0,459,460,5,101,0,0,460,461,5,102,0,0,461,462,5,97,0,0,462,463,
5,117,0,0,463,464,5,108,0,0,464,465,5,116,0,0,465,66,1,0,0,0,466,
467,5,115,0,0,467,468,5,111,0,0,468,469,5,117,0,0,469,470,5,114,
0,0,470,471,5,99,0,0,471,472,5,101,0,0,472,68,1,0,0,0,473,474,5,
114,0,0,474,475,5,101,0,0,475,476,5,112,0,0,476,477,5,111,0,0,477,
478,5,115,0,0,478,479,5,105,0,0,479,480,5,116,0,0,480,481,5,111,
0,0,481,482,5,114,0,0,482,483,5,121,0,0,483,70,1,0,0,0,484,485,5,
99,0,0,485,486,5,111,0,0,486,487,5,109,0,0,487,488,5,109,0,0,488,
489,5,105,0,0,489,490,5,116,0,0,490,72,1,0,0,0,491,492,5,114,0,0,
492,493,5,101,0,0,493,494,5,118,0,0,494,495,5,105,0,0,495,496,5,
115,0,0,496,497,5,105,0,0,497,498,5,111,0,0,498,499,5,110,0,0,499,
74,1,0,0,0,500,501,5,115,0,0,501,502,5,101,0,0,502,503,5,109,0,0,
503,504,5,97,0,0,504,505,5,110,0,0,505,506,5,116,0,0,506,507,5,105,
0,0,507,508,5,99,0,0,508,509,5,45,0,0,509,510,5,109,0,0,510,511,
5,97,0,0,511,512,5,106,0,0,512,513,5,111,0,0,513,514,5,114,0,0,514,
76,1,0,0,0,515,516,5,111,0,0,516,517,5,110,0,0,517,518,5,45,0,0,
518,519,5,100,0,0,519,520,5,101,0,0,520,521,5,108,0,0,521,522,5,
101,0,0,522,523,5,116,0,0,523,524,5,101,0,0,524,78,1,0,0,0,525,526,
5,114,0,0,526,527,5,101,0,0,527,528,5,116,0,0,528,529,5,97,0,0,529,
530,5,105,0,0,530,531,5,110,0,0,531,532,5,45,0,0,532,533,5,111,0,
0,533,534,5,116,0,0,534,535,5,104,0,0,535,536,5,101,0,0,536,537,
5,114,0,0,537,80,1,0,0,0,538,539,5,107,0,0,539,540,5,101,0,0,540,
541,5,121,0,0,541,542,5,101,0,0,542,543,5,100,0,0,543,82,1,0,0,0,
544,545,5,112,0,0,545,546,5,117,0,0,546,547,5,98,0,0,547,548,5,108,
0,0,548,549,5,105,0,0,549,550,5,99,0,0,550,551,5,45,0,0,551,552,
5,116,0,0,552,553,5,114,0,0,553,554,5,97,0,0,554,555,5,118,0,0,555,
556,5,101,0,0,556,557,5,114,0,0,557,558,5,115,0,0,558,559,5,97,0,
0,559,560,5,108,0,0,560,84,1,0,0,0,561,562,5,105,0,0,562,563,5,100,
0,0,563,86,1,0,0,0,564,565,5,100,0,0,565,566,5,111,0,0,566,567,5,
99,0,0,567,88,1,0,0,0,568,569,5,109,0,0,569,570,5,111,0,0,570,571,
5,100,0,0,571,572,5,101,0,0,572,90,1,0,0,0,573,574,5,101,0,0,574,
575,5,109,0,0,575,576,5,105,0,0,576,577,5,116,0,0,577,578,5,115,
0,0,578,92,1,0,0,0,579,580,5,114,0,0,580,581,5,101,0,0,581,582,5,
99,0,0,582,583,5,101,0,0,583,584,5,105,0,0,584,585,5,118,0,0,585,
586,5,101,0,0,586,587,5,114,0,0,587,94,1,0,0,0,588,589,5,114,0,0,
589,590,5,101,0,0,590,591,5,113,0,0,591,592,5,117,0,0,592,593,5,
105,0,0,593,594,5,114,0,0,594,595,5,101,0,0,595,596,5,115,0,0,596,
96,1,0,0,0,597,598,5,97,0,0,598,599,5,110,0,0,599,600,5,121,0,0,
600,98,1,0,0,0,601,602,5,103,0,0,602,603,5,101,0,0,603,604,5,116,
0,0,604,100,1,0,0,0,605,606,5,115,0,0,606,607,5,101,0,0,607,608,
5,116,0,0,608,102,1,0,0,0,609,610,5,119,0,0,610,611,5,97,0,0,611,
612,5,116,0,0,612,613,5,99,0,0,613,614,5,104,0,0,614,104,1,0,0,0,
615,616,5,115,0,0,616,617,5,116,0,0,617,618,5,97,0,0,618,619,5,114,
0,0,619,620,5,116,0,0,620,106,1,0,0,0,621,622,5,115,0,0,622,623,
5,116,0,0,623,624,5,111,0,0,624,625,5,112,0,0,625,108,1,0,0,0,626,
627,5,114,0,0,627,628,5,101,0,0,628,629,5,97,0,0,629,630,5,100,0,
0,630,110,1,0,0,0,631,632,5,119,0,0,632,633,5,114,0,0,633,634,5,
105,0,0,634,635,5,116,0,0,635,636,5,101,0,0,636,112,1,0,0,0,637,
638,5,114,0,0,638,639,5,101,0,0,639,640,5,115,0,0,640,641,5,111,
0,0,641,642,5,108,0,0,642,643,5,118,0,0,643,644,5,101,0,0,644,114,
1,0,0,0,645,646,5,99,0,0,646,647,5,111,0,0,647,648,5,110,0,0,648,
649,5,110,0,0,649,650,5,101,0,0,650,651,5,99,0,0,651,652,5,116,0,
0,652,116,1,0,0,0,653,654,5,100,0,0,654,655,5,105,0,0,655,656,5,
115,0,0,656,657,5,99,0,0,657,658,5,111,0,0,658,659,5,110,0,0,659,
660,5,110,0,0,660,661,5,101,0,0,661,662,5,99,0,0,662,663,5,116,0,
0,663,118,1,0,0,0,664,665,5,99,0,0,665,666,5,97,0,0,666,667,5,108,
0,0,667,668,5,108,0,0,668,120,1,0,0,0,669,670,5,119,0,0,670,671,
5,97,0,0,671,672,5,116,0,0,672,673,5,99,0,0,673,674,5,104,0,0,674,
675,5,45,0,0,675,676,5,115,0,0,676,677,5,116,0,0,677,678,5,97,0,
0,678,679,5,114,0,0,679,680,5,116,0,0,680,122,1,0,0,0,681,682,5,
119,0,0,682,683,5,97,0,0,683,684,5,116,0,0,684,685,5,99,0,0,685,
686,5,104,0,0,686,687,5,45,0,0,687,688,5,115,0,0,688,689,5,116,0,
0,689,690,5,111,0,0,690,691,5,112,0,0,691,124,1,0,0,0,692,693,5,
115,0,0,693,694,5,117,0,0,694,695,5,98,0,0,695,696,5,115,0,0,696,
697,5,99,0,0,697,698,5,114,0,0,698,699,5,105,0,0,699,700,5,98,0,
0,700,701,5,101,0,0,701,126,1,0,0,0,702,703,5,117,0,0,703,704,5,
110,0,0,704,705,5,115,0,0,705,706,5,117,0,0,706,707,5,98,0,0,707,
708,5,115,0,0,708,709,5,99,0,0,709,710,5,114,0,0,710,711,5,105,0,
0,711,712,5,98,0,0,712,713,5,101,0,0,713,128,1,0,0,0,714,715,5,111,
0,0,715,716,5,112,0,0,716,717,5,116,0,0,717,718,5,105,0,0,718,719,
5,109,0,0,719,720,5,105,0,0,720,721,5,115,0,0,721,722,5,116,0,0,
722,723,5,105,0,0,723,724,5,99,0,0,724,725,5,45,0,0,725,726,5,114,
0,0,726,727,5,101,0,0,727,728,5,103,0,0,728,729,5,105,0,0,729,730,
5,115,0,0,730,731,5,116,0,0,731,732,5,101,0,0,732,733,5,114,0,0,
733,130,1,0,0,0,734,735,5,99,0,0,735,736,5,114,0,0,736,737,5,100,
0,0,737,738,5,116,0,0,738,132,1,0,0,0,739,740,5,111,0,0,740,741,
5,112,0,0,741,742,5,116,0,0,742,743,5,105,0,0,743,744,5,111,0,0,
744,745,5,110,0,0,745,746,5,97,0,0,746,747,5,108,0,0,747,748,5,45,
0,0,748,749,5,111,0,0,749,750,5,110,0,0,750,751,5,101,0,0,751,134,
1,0,0,0,752,753,5,101,0,0,753,754,5,120,0,0,754,755,5,97,0,0,755,
756,5,99,0,0,756,757,5,116,0,0,757,758,5,108,0,0,758,759,5,121,0,
0,759,760,5,45,0,0,760,761,5,111,0,0,761,762,5,110,0,0,762,763,5,
101,0,0,763,136,1,0,0,0,764,765,5,109,0,0,765,766,5,97,0,0,766,767,
5,110,0,0,767,768,5,121,0,0,768,769,5,45,0,0,769,770,5,117,0,0,770,
771,5,110,0,0,771,772,5,105,0,0,772,773,5,113,0,0,773,774,5,117,
0,0,774,775,5,101,0,0,775,138,1,0,0,0,776,777,5,109,0,0,777,778,
5,97,0,0,778,779,5,110,0,0,779,780,5,121,0,0,780,140,1,0,0,0,781,
782,5,111,0,0,782,783,5,114,0,0,783,784,5,100,0,0,784,785,5,101,
0,0,785,786,5,114,0,0,786,787,5,101,0,0,787,788,5,100,0,0,788,142,
1,0,0,0,789,790,5,117,0,0,790,791,5,110,0,0,791,792,5,105,0,0,792,
793,5,116,0,0,793,144,1,0,0,0,794,795,5,119,0,0,795,796,5,97,0,0,
796,797,5,116,0,0,797,798,5,99,0,0,798,799,5,104,0,0,799,800,5,45,
0,0,800,801,5,104,0,0,801,802,5,97,0,0,802,803,5,110,0,0,803,804,
5,100,0,0,804,805,5,108,0,0,805,806,5,101,0,0,806,146,1,0,0,0,807,
808,5,109,0,0,808,809,5,101,0,0,809,810,5,115,0,0,810,811,5,115,
0,0,811,812,5,97,0,0,812,813,5,103,0,0,813,814,5,101,0,0,814,148,
1,0,0,0,815,816,5,97,0,0,816,817,5,116,0,0,817,818,5,111,0,0,818,
819,5,109,0,0,819,820,5,45,0,0,820,821,5,114,0,0,821,822,5,101,0,
0,822,823,5,102,0,0,823,150,1,0,0,0,824,825,5,105,0,0,825,826,5,
110,0,0,826,827,5,116,0,0,827,828,5,101,0,0,828,829,5,114,0,0,829,
830,5,102,0,0,830,831,5,97,0,0,831,832,5,99,0,0,832,833,5,101,0,
0,833,834,5,45,0,0,834,835,5,114,0,0,835,836,5,101,0,0,836,837,5,
102,0,0,837,152,1,0,0,0,838,839,5,111,0,0,839,840,5,112,0,0,840,
841,5,116,0,0,841,842,5,105,0,0,842,843,5,111,0,0,843,844,5,110,
0,0,844,845,5,97,0,0,845,846,5,108,0,0,846,154,1,0,0,0,847,848,5,
108,0,0,848,849,5,105,0,0,849,850,5,115,0,0,850,851,5,116,0,0,851,
156,1,0,0,0,852,853,5,98,0,0,853,854,5,111,0,0,854,855,5,111,0,0,
855,856,5,108,0,0,856,158,1,0,0,0,857,858,5,98,0,0,858,859,5,121,
0,0,859,860,5,116,0,0,860,861,5,101,0,0,861,862,5,115,0,0,862,160,
1,0,0,0,863,864,5,100,0,0,864,865,5,111,0,0,865,866,5,117,0,0,866,
867,5,98,0,0,867,868,5,108,0,0,868,869,5,101,0,0,869,162,1,0,0,0,
870,871,5,105,0,0,871,872,5,110,0,0,872,873,5,116,0,0,873,874,5,
51,0,0,874,875,5,50,0,0,875,164,1,0,0,0,876,877,5,105,0,0,877,878,
5,110,0,0,878,879,5,116,0,0,879,880,5,54,0,0,880,881,5,52,0,0,881,
166,1,0,0,0,882,883,5,115,0,0,883,884,5,116,0,0,884,885,5,114,0,
0,885,886,5,105,0,0,886,887,5,110,0,0,887,888,5,103,0,0,888,168,
1,0,0,0,889,890,5,117,0,0,890,891,5,105,0,0,891,892,5,110,0,0,892,
893,5,116,0,0,893,894,5,51,0,0,894,895,5,50,0,0,895,170,1,0,0,0,
896,897,5,117,0,0,897,898,5,105,0,0,898,899,5,110,0,0,899,900,5,
116,0,0,900,901,5,54,0,0,901,902,5,52,0,0,902,172,1,0,0,0,903,904,
5,116,0,0,904,905,5,114,0,0,905,906,5,117,0,0,906,907,5,101,0,0,
907,174,1,0,0,0,908,909,5,102,0,0,909,910,5,97,0,0,910,911,5,108,
0,0,911,912,5,115,0,0,912,913,5,101,0,0,913,176,1,0,0,0,914,915,
5,110,0,0,915,916,5,117,0,0,916,917,5,108,0,0,917,918,5,108,0,0,
918,178,1,0,0,0,919,920,5,45,0,0,920,921,5,62,0,0,921,180,1,0,0,
0,922,923,5,58,0,0,923,182,1,0,0,0,924,925,5,59,0,0,925,184,1,0,
0,0,926,927,5,44,0,0,927,186,1,0,0,0,928,929,5,46,0,0,929,188,1,
0,0,0,930,931,5,123,0,0,931,190,1,0,0,0,932,933,5,125,0,0,933,192,
1,0,0,0,934,935,5,91,0,0,935,194,1,0,0,0,936,937,5,93,0,0,937,196,
1,0,0,0,938,939,5,40,0,0,939,198,1,0,0,0,940,941,5,41,0,0,941,200,
1,0,0,0,942,943,5,60,0,0,943,202,1,0,0,0,944,945,5,62,0,0,945,204,
1,0,0,0,946,948,5,45,0,0,947,946,1,0,0,0,947,948,1,0,0,0,948,950,
1,0,0,0,949,951,7,0,0,0,950,949,1,0,0,0,951,952,1,0,0,0,952,950,
1,0,0,0,952,953,1,0,0,0,953,206,1,0,0,0,954,956,5,45,0,0,955,954,
1,0,0,0,955,956,1,0,0,0,956,965,1,0,0,0,957,966,5,48,0,0,958,962,
7,1,0,0,959,961,7,0,0,0,960,959,1,0,0,0,961,964,1,0,0,0,962,960,
1,0,0,0,962,963,1,0,0,0,963,966,1,0,0,0,964,962,1,0,0,0,965,957,
1,0,0,0,965,958,1,0,0,0,966,973,1,0,0,0,967,969,5,46,0,0,968,970,
7,0,0,0,969,968,1,0,0,0,970,971,1,0,0,0,971,969,1,0,0,0,971,972,
1,0,0,0,972,974,1,0,0,0,973,967,1,0,0,0,973,974,1,0,0,0,974,984,
1,0,0,0,975,977,7,2,0,0,976,978,7,3,0,0,977,976,1,0,0,0,977,978,
1,0,0,0,978,980,1,0,0,0,979,981,7,0,0,0,980,979,1,0,0,0,981,982,
1,0,0,0,982,980,1,0,0,0,982,983,1,0,0,0,983,985,1,0,0,0,984,975,
1,0,0,0,984,985,1,0,0,0,985,208,1,0,0,0,986,990,7,4,0,0,987,989,
7,5,0,0,988,987,1,0,0,0,989,992,1,0,0,0,990,988,1,0,0,0,990,991,
1,0,0,0,991,210,1,0,0,0,992,990,1,0,0,0,993,998,5,34,0,0,994,997,
3,213,106,0,995,997,8,6,0,0,996,994,1,0,0,0,996,995,1,0,0,0,997,
1000,1,0,0,0,998,996,1,0,0,0,998,999,1,0,0,0,999,1001,1,0,0,0,1000,
998,1,0,0,0,1001,1002,5,34,0,0,1002,212,1,0,0,0,1003,1011,5,92,0,
0,1004,1012,7,7,0,0,1005,1006,5,117,0,0,1006,1007,3,215,107,0,1007,
1008,3,215,107,0,1008,1009,3,215,107,0,1009,1010,3,215,107,0,1010,
1012,1,0,0,0,1011,1004,1,0,0,0,1011,1005,1,0,0,0,1012,214,1,0,0,
0,1013,1014,7,8,0,0,1014,216,1,0,0,0,1015,1016,5,47,0,0,1016,1017,
5,47,0,0,1017,1021,1,0,0,0,1018,1020,8,9,0,0,1019,1018,1,0,0,0,1020,
1023,1,0,0,0,1021,1019,1,0,0,0,1021,1022,1,0,0,0,1022,1024,1,0,0,
0,1023,1021,1,0,0,0,1024,1025,6,108,0,0,1025,218,1,0,0,0,1026,1027,
5,47,0,0,1027,1028,5,42,0,0,1028,1032,1,0,0,0,1029,1031,9,0,0,0,
1030,1029,1,0,0,0,1031,1034,1,0,0,0,1032,1033,1,0,0,0,1032,1030,
1,0,0,0,1033,1035,1,0,0,0,1034,1032,1,0,0,0,1035,1036,5,42,0,0,1036,
1037,5,47,0,0,1037,1038,1,0,0,0,1038,1039,6,109,0,0,1039,220,1,0,
0,0,1040,1042,7,10,0,0,1041,1040,1,0,0,0,1042,1043,1,0,0,0,1043,
1041,1,0,0,0,1043,1044,1,0,0,0,1044,1045,1,0,0,0,1045,1046,6,110,
0,0,1046,222,1,0,0,0,18,0,947,952,955,962,965,971,973,977,982,984,
990,996,998,1011,1021,1032,1043,1,0,1,0
2,109,7,109,2,110,7,110,2,111,7,111,1,0,1,0,1,0,1,0,1,0,1,0,1,0,
1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,
1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,
1,4,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,
1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,8,1,8,
1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,
10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,
11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,
12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,
13,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,
15,1,15,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,
19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,20,1,
20,1,20,1,21,1,21,1,21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,
23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,
24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,
27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,
28,1,28,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,31,1,
31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,
32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,
34,1,34,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,
36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,
37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,
38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,
39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,
40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,
41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,1,
44,1,44,1,44,1,44,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,
46,1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,47,1,47,1,
47,1,47,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1,
50,1,51,1,51,1,51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,
53,1,53,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,
55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,
57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,
58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,
60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,
61,1,61,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,
62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,
63,1,63,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,
64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,
65,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,
66,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,
68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,69,1,
69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,71,1,
71,1,71,1,71,1,71,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,
72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,
74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,
75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,
76,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,
78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,79,1,80,1,80,1,80,1,80,1,
80,1,80,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,
82,1,82,1,83,1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,
84,1,84,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1,
86,1,86,1,86,1,87,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1,
88,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,91,1,91,1,92,1,92,1,
93,1,93,1,94,1,94,1,95,1,95,1,96,1,96,1,97,1,97,1,98,1,98,1,99,1,
99,1,100,1,100,1,101,1,101,1,102,1,102,1,103,3,103,957,8,103,1,103,
4,103,960,8,103,11,103,12,103,961,1,104,3,104,965,8,104,1,104,1,
104,1,104,5,104,970,8,104,10,104,12,104,973,9,104,3,104,975,8,104,
1,104,1,104,4,104,979,8,104,11,104,12,104,980,3,104,983,8,104,1,
104,1,104,3,104,987,8,104,1,104,4,104,990,8,104,11,104,12,104,991,
3,104,994,8,104,1,105,1,105,5,105,998,8,105,10,105,12,105,1001,9,
105,1,106,1,106,1,106,5,106,1006,8,106,10,106,12,106,1009,9,106,
1,106,1,106,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107,3,107,
1021,8,107,1,108,1,108,1,109,1,109,1,109,1,109,5,109,1029,8,109,
10,109,12,109,1032,9,109,1,109,1,109,1,110,1,110,1,110,1,110,5,110,
1040,8,110,10,110,12,110,1043,9,110,1,110,1,110,1,110,1,110,1,110,
1,111,4,111,1051,8,111,11,111,12,111,1052,1,111,1,111,1,1041,0,112,
1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,
27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,
49,25,51,26,53,27,55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35,
71,36,73,37,75,38,77,39,79,40,81,41,83,42,85,43,87,44,89,45,91,46,
93,47,95,48,97,49,99,50,101,51,103,52,105,53,107,54,109,55,111,56,
113,57,115,58,117,59,119,60,121,61,123,62,125,63,127,64,129,65,131,
66,133,67,135,68,137,69,139,70,141,71,143,72,145,73,147,74,149,75,
151,76,153,77,155,78,157,79,159,80,161,81,163,82,165,83,167,84,169,
85,171,86,173,87,175,88,177,89,179,90,181,91,183,92,185,93,187,94,
189,95,191,96,193,97,195,98,197,99,199,100,201,101,203,102,205,103,
207,104,209,105,211,106,213,107,215,0,217,0,219,108,221,109,223,
110,1,0,11,1,0,48,57,1,0,49,57,2,0,69,69,101,101,2,0,43,43,45,45,
3,0,65,90,95,95,97,122,4,0,48,57,65,90,95,95,97,122,4,0,10,10,13,
13,34,34,92,92,8,0,34,34,47,47,92,92,98,98,102,102,110,110,114,114,
116,116,3,0,48,57,65,70,97,102,2,0,10,10,13,13,3,0,9,10,13,13,32,
32,1070,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,
0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,
0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,
0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,
0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,
0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,
0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,1,0,
0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,1,0,
0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,1,0,
0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,
0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0,0,109,
1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,0,0,0,117,1,0,0,0,
0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0,127,1,
0,0,0,0,129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0,135,1,0,0,0,0,
137,1,0,0,0,0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145,1,0,
0,0,0,147,1,0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0,155,
1,0,0,0,0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,
0,165,1,0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,
0,0,0,0,175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1,0,0,0,0,
183,1,0,0,0,0,185,1,0,0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191,1,0,
0,0,0,193,1,0,0,0,0,195,1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0,0,201,
1,0,0,0,0,203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,0,0,0,209,1,0,0,0,
0,211,1,0,0,0,0,213,1,0,0,0,0,219,1,0,0,0,0,221,1,0,0,0,0,223,1,
0,0,0,1,225,1,0,0,0,3,235,1,0,0,0,5,244,1,0,0,0,7,251,1,0,0,0,9,
260,1,0,0,0,11,265,1,0,0,0,13,275,1,0,0,0,15,286,1,0,0,0,17,294,
1,0,0,0,19,300,1,0,0,0,21,309,1,0,0,0,23,319,1,0,0,0,25,328,1,0,
0,0,27,340,1,0,0,0,29,351,1,0,0,0,31,357,1,0,0,0,33,365,1,0,0,0,
35,368,1,0,0,0,37,373,1,0,0,0,39,376,1,0,0,0,41,384,1,0,0,0,43,391,
1,0,0,0,45,397,1,0,0,0,47,402,1,0,0,0,49,413,1,0,0,0,51,418,1,0,
0,0,53,424,1,0,0,0,55,428,1,0,0,0,57,440,1,0,0,0,59,443,1,0,0,0,
61,450,1,0,0,0,63,453,1,0,0,0,65,460,1,0,0,0,67,468,1,0,0,0,69,475,
1,0,0,0,71,486,1,0,0,0,73,493,1,0,0,0,75,502,1,0,0,0,77,517,1,0,
0,0,79,527,1,0,0,0,81,540,1,0,0,0,83,546,1,0,0,0,85,563,1,0,0,0,
87,566,1,0,0,0,89,570,1,0,0,0,91,575,1,0,0,0,93,581,1,0,0,0,95,590,
1,0,0,0,97,599,1,0,0,0,99,603,1,0,0,0,101,607,1,0,0,0,103,611,1,
0,0,0,105,617,1,0,0,0,107,623,1,0,0,0,109,628,1,0,0,0,111,633,1,
0,0,0,113,639,1,0,0,0,115,647,1,0,0,0,117,655,1,0,0,0,119,666,1,
0,0,0,121,671,1,0,0,0,123,683,1,0,0,0,125,694,1,0,0,0,127,704,1,
0,0,0,129,716,1,0,0,0,131,736,1,0,0,0,133,741,1,0,0,0,135,754,1,
0,0,0,137,766,1,0,0,0,139,778,1,0,0,0,141,783,1,0,0,0,143,791,1,
0,0,0,145,796,1,0,0,0,147,809,1,0,0,0,149,817,1,0,0,0,151,826,1,
0,0,0,153,840,1,0,0,0,155,849,1,0,0,0,157,854,1,0,0,0,159,861,1,
0,0,0,161,866,1,0,0,0,163,872,1,0,0,0,165,879,1,0,0,0,167,885,1,
0,0,0,169,891,1,0,0,0,171,898,1,0,0,0,173,905,1,0,0,0,175,912,1,
0,0,0,177,917,1,0,0,0,179,923,1,0,0,0,181,928,1,0,0,0,183,931,1,
0,0,0,185,933,1,0,0,0,187,935,1,0,0,0,189,937,1,0,0,0,191,939,1,
0,0,0,193,941,1,0,0,0,195,943,1,0,0,0,197,945,1,0,0,0,199,947,1,
0,0,0,201,949,1,0,0,0,203,951,1,0,0,0,205,953,1,0,0,0,207,956,1,
0,0,0,209,964,1,0,0,0,211,995,1,0,0,0,213,1002,1,0,0,0,215,1012,
1,0,0,0,217,1022,1,0,0,0,219,1024,1,0,0,0,221,1035,1,0,0,0,223,1050,
1,0,0,0,225,226,5,119,0,0,226,227,5,111,0,0,227,228,5,114,0,0,228,
229,5,107,0,0,229,230,5,115,0,0,230,231,5,112,0,0,231,232,5,97,0,
0,232,233,5,99,0,0,233,234,5,101,0,0,234,2,1,0,0,0,235,236,5,102,
0,0,236,237,5,114,0,0,237,238,5,97,0,0,238,239,5,103,0,0,239,240,
5,109,0,0,240,241,5,101,0,0,241,242,5,110,0,0,242,243,5,116,0,0,
243,4,1,0,0,0,244,245,5,105,0,0,245,246,5,109,0,0,246,247,5,112,
0,0,247,248,5,111,0,0,248,249,5,114,0,0,249,250,5,116,0,0,250,6,
1,0,0,0,251,252,5,101,0,0,252,253,5,120,0,0,253,254,5,116,0,0,254,
255,5,101,0,0,255,256,5,114,0,0,256,257,5,110,0,0,257,258,5,97,0,
0,258,259,5,108,0,0,259,8,1,0,0,0,260,261,5,97,0,0,261,262,5,116,
0,0,262,263,5,111,0,0,263,264,5,109,0,0,264,10,1,0,0,0,265,266,5,
105,0,0,266,267,5,110,0,0,267,268,5,116,0,0,268,269,5,101,0,0,269,
270,5,114,0,0,270,271,5,102,0,0,271,272,5,97,0,0,272,273,5,99,0,
0,273,274,5,101,0,0,274,12,1,0,0,0,275,276,5,105,0,0,276,277,5,110,
0,0,277,278,5,116,0,0,278,279,5,101,0,0,279,280,5,114,0,0,280,281,
5,102,0,0,281,282,5,97,0,0,282,283,5,99,0,0,283,284,5,101,0,0,284,
285,5,115,0,0,285,14,1,0,0,0,286,287,5,112,0,0,287,288,5,97,0,0,
288,289,5,99,0,0,289,290,5,107,0,0,290,291,5,97,0,0,291,292,5,103,
0,0,292,293,5,101,0,0,293,16,1,0,0,0,294,295,5,118,0,0,295,296,5,
97,0,0,296,297,5,108,0,0,297,298,5,117,0,0,298,299,5,101,0,0,299,
18,1,0,0,0,300,301,5,114,0,0,301,302,5,101,0,0,302,303,5,108,0,0,
303,304,5,97,0,0,304,305,5,116,0,0,305,306,5,105,0,0,306,307,5,111,
0,0,307,308,5,110,0,0,308,20,1,0,0,0,309,310,5,111,0,0,310,311,5,
112,0,0,311,312,5,101,0,0,312,313,5,114,0,0,313,314,5,97,0,0,314,
315,5,116,0,0,315,316,5,105,0,0,316,317,5,111,0,0,317,318,5,110,
0,0,318,22,1,0,0,0,319,320,5,102,0,0,320,321,5,117,0,0,321,322,5,
110,0,0,322,323,5,99,0,0,323,324,5,116,0,0,324,325,5,105,0,0,325,
326,5,111,0,0,326,327,5,110,0,0,327,24,1,0,0,0,328,329,5,99,0,0,
329,330,5,111,0,0,330,331,5,110,0,0,331,332,5,115,0,0,332,333,5,
116,0,0,333,334,5,114,0,0,334,335,5,117,0,0,335,336,5,99,0,0,336,
337,5,116,0,0,337,338,5,111,0,0,338,339,5,114,0,0,339,26,1,0,0,0,
340,341,5,99,0,0,341,342,5,111,0,0,342,343,5,110,0,0,343,344,5,115,
0,0,344,345,5,116,0,0,345,346,5,114,0,0,346,347,5,117,0,0,347,348,
5,99,0,0,348,349,5,116,0,0,349,350,5,115,0,0,350,28,1,0,0,0,351,
352,5,105,0,0,352,353,5,110,0,0,353,354,5,112,0,0,354,355,5,117,
0,0,355,356,5,116,0,0,356,30,1,0,0,0,357,358,5,99,0,0,358,359,5,
111,0,0,359,360,5,110,0,0,360,361,5,102,0,0,361,362,5,111,0,0,362,
363,5,114,0,0,363,364,5,109,0,0,364,32,1,0,0,0,365,366,5,97,0,0,
366,367,5,115,0,0,367,34,1,0,0,0,368,369,5,98,0,0,369,370,5,105,
0,0,370,371,5,110,0,0,371,372,5,100,0,0,372,36,1,0,0,0,373,374,5,
116,0,0,374,375,5,111,0,0,375,38,1,0,0,0,376,377,5,112,0,0,377,378,
5,114,0,0,378,379,5,105,0,0,379,380,5,118,0,0,380,381,5,97,0,0,381,
382,5,116,0,0,382,383,5,101,0,0,383,40,1,0,0,0,384,385,5,115,0,0,
385,386,5,104,0,0,386,387,5,97,0,0,387,388,5,114,0,0,388,389,5,101,
0,0,389,390,5,100,0,0,390,42,1,0,0,0,391,392,5,115,0,0,392,393,5,
116,0,0,393,394,5,97,0,0,394,395,5,116,0,0,395,396,5,101,0,0,396,
44,1,0,0,0,397,398,5,101,0,0,398,399,5,100,0,0,399,400,5,103,0,0,
400,401,5,101,0,0,401,46,1,0,0,0,402,403,5,112,0,0,403,404,5,114,
0,0,404,405,5,111,0,0,405,406,5,106,0,0,406,407,5,101,0,0,407,408,
5,99,0,0,408,409,5,116,0,0,409,410,5,105,0,0,410,411,5,111,0,0,411,
412,5,110,0,0,412,48,1,0,0,0,413,414,5,119,0,0,414,415,5,105,0,0,
415,416,5,116,0,0,416,417,5,104,0,0,417,50,1,0,0,0,418,419,5,117,
0,0,419,420,5,115,0,0,420,421,5,105,0,0,421,422,5,110,0,0,422,423,
5,103,0,0,423,52,1,0,0,0,424,425,5,118,0,0,425,426,5,105,0,0,426,
427,5,97,0,0,427,54,1,0,0,0,428,429,5,109,0,0,429,430,5,97,0,0,430,
431,5,116,0,0,431,432,5,101,0,0,432,433,5,114,0,0,433,434,5,105,
0,0,434,435,5,97,0,0,435,436,5,108,0,0,436,437,5,105,0,0,437,438,
5,122,0,0,438,439,5,101,0,0,439,56,1,0,0,0,440,441,5,105,0,0,441,
442,5,102,0,0,442,58,1,0,0,0,443,444,5,97,0,0,444,445,5,98,0,0,445,
446,5,115,0,0,446,447,5,101,0,0,447,448,5,110,0,0,448,449,5,116,
0,0,449,60,1,0,0,0,450,451,5,111,0,0,451,452,5,110,0,0,452,62,1,
0,0,0,453,454,5,112,0,0,454,455,5,111,0,0,455,456,5,108,0,0,456,
457,5,105,0,0,457,458,5,99,0,0,458,459,5,121,0,0,459,64,1,0,0,0,
460,461,5,100,0,0,461,462,5,101,0,0,462,463,5,102,0,0,463,464,5,
97,0,0,464,465,5,117,0,0,465,466,5,108,0,0,466,467,5,116,0,0,467,
66,1,0,0,0,468,469,5,115,0,0,469,470,5,111,0,0,470,471,5,117,0,0,
471,472,5,114,0,0,472,473,5,99,0,0,473,474,5,101,0,0,474,68,1,0,
0,0,475,476,5,114,0,0,476,477,5,101,0,0,477,478,5,112,0,0,478,479,
5,111,0,0,479,480,5,115,0,0,480,481,5,105,0,0,481,482,5,116,0,0,
482,483,5,111,0,0,483,484,5,114,0,0,484,485,5,121,0,0,485,70,1,0,
0,0,486,487,5,99,0,0,487,488,5,111,0,0,488,489,5,109,0,0,489,490,
5,109,0,0,490,491,5,105,0,0,491,492,5,116,0,0,492,72,1,0,0,0,493,
494,5,114,0,0,494,495,5,101,0,0,495,496,5,118,0,0,496,497,5,105,
0,0,497,498,5,115,0,0,498,499,5,105,0,0,499,500,5,111,0,0,500,501,
5,110,0,0,501,74,1,0,0,0,502,503,5,115,0,0,503,504,5,101,0,0,504,
505,5,109,0,0,505,506,5,97,0,0,506,507,5,110,0,0,507,508,5,116,0,
0,508,509,5,105,0,0,509,510,5,99,0,0,510,511,5,45,0,0,511,512,5,
109,0,0,512,513,5,97,0,0,513,514,5,106,0,0,514,515,5,111,0,0,515,
516,5,114,0,0,516,76,1,0,0,0,517,518,5,111,0,0,518,519,5,110,0,0,
519,520,5,45,0,0,520,521,5,100,0,0,521,522,5,101,0,0,522,523,5,108,
0,0,523,524,5,101,0,0,524,525,5,116,0,0,525,526,5,101,0,0,526,78,
1,0,0,0,527,528,5,114,0,0,528,529,5,101,0,0,529,530,5,116,0,0,530,
531,5,97,0,0,531,532,5,105,0,0,532,533,5,110,0,0,533,534,5,45,0,
0,534,535,5,111,0,0,535,536,5,116,0,0,536,537,5,104,0,0,537,538,
5,101,0,0,538,539,5,114,0,0,539,80,1,0,0,0,540,541,5,107,0,0,541,
542,5,101,0,0,542,543,5,121,0,0,543,544,5,101,0,0,544,545,5,100,
0,0,545,82,1,0,0,0,546,547,5,112,0,0,547,548,5,117,0,0,548,549,5,
98,0,0,549,550,5,108,0,0,550,551,5,105,0,0,551,552,5,99,0,0,552,
553,5,45,0,0,553,554,5,116,0,0,554,555,5,114,0,0,555,556,5,97,0,
0,556,557,5,118,0,0,557,558,5,101,0,0,558,559,5,114,0,0,559,560,
5,115,0,0,560,561,5,97,0,0,561,562,5,108,0,0,562,84,1,0,0,0,563,
564,5,105,0,0,564,565,5,100,0,0,565,86,1,0,0,0,566,567,5,100,0,0,
567,568,5,111,0,0,568,569,5,99,0,0,569,88,1,0,0,0,570,571,5,109,
0,0,571,572,5,111,0,0,572,573,5,100,0,0,573,574,5,101,0,0,574,90,
1,0,0,0,575,576,5,101,0,0,576,577,5,109,0,0,577,578,5,105,0,0,578,
579,5,116,0,0,579,580,5,115,0,0,580,92,1,0,0,0,581,582,5,114,0,0,
582,583,5,101,0,0,583,584,5,99,0,0,584,585,5,101,0,0,585,586,5,105,
0,0,586,587,5,118,0,0,587,588,5,101,0,0,588,589,5,114,0,0,589,94,
1,0,0,0,590,591,5,114,0,0,591,592,5,101,0,0,592,593,5,113,0,0,593,
594,5,117,0,0,594,595,5,105,0,0,595,596,5,114,0,0,596,597,5,101,
0,0,597,598,5,115,0,0,598,96,1,0,0,0,599,600,5,97,0,0,600,601,5,
110,0,0,601,602,5,121,0,0,602,98,1,0,0,0,603,604,5,103,0,0,604,605,
5,101,0,0,605,606,5,116,0,0,606,100,1,0,0,0,607,608,5,115,0,0,608,
609,5,101,0,0,609,610,5,116,0,0,610,102,1,0,0,0,611,612,5,119,0,
0,612,613,5,97,0,0,613,614,5,116,0,0,614,615,5,99,0,0,615,616,5,
104,0,0,616,104,1,0,0,0,617,618,5,115,0,0,618,619,5,116,0,0,619,
620,5,97,0,0,620,621,5,114,0,0,621,622,5,116,0,0,622,106,1,0,0,0,
623,624,5,115,0,0,624,625,5,116,0,0,625,626,5,111,0,0,626,627,5,
112,0,0,627,108,1,0,0,0,628,629,5,114,0,0,629,630,5,101,0,0,630,
631,5,97,0,0,631,632,5,100,0,0,632,110,1,0,0,0,633,634,5,119,0,0,
634,635,5,114,0,0,635,636,5,105,0,0,636,637,5,116,0,0,637,638,5,
101,0,0,638,112,1,0,0,0,639,640,5,114,0,0,640,641,5,101,0,0,641,
642,5,115,0,0,642,643,5,111,0,0,643,644,5,108,0,0,644,645,5,118,
0,0,645,646,5,101,0,0,646,114,1,0,0,0,647,648,5,99,0,0,648,649,5,
111,0,0,649,650,5,110,0,0,650,651,5,110,0,0,651,652,5,101,0,0,652,
653,5,99,0,0,653,654,5,116,0,0,654,116,1,0,0,0,655,656,5,100,0,0,
656,657,5,105,0,0,657,658,5,115,0,0,658,659,5,99,0,0,659,660,5,111,
0,0,660,661,5,110,0,0,661,662,5,110,0,0,662,663,5,101,0,0,663,664,
5,99,0,0,664,665,5,116,0,0,665,118,1,0,0,0,666,667,5,99,0,0,667,
668,5,97,0,0,668,669,5,108,0,0,669,670,5,108,0,0,670,120,1,0,0,0,
671,672,5,119,0,0,672,673,5,97,0,0,673,674,5,116,0,0,674,675,5,99,
0,0,675,676,5,104,0,0,676,677,5,45,0,0,677,678,5,115,0,0,678,679,
5,116,0,0,679,680,5,97,0,0,680,681,5,114,0,0,681,682,5,116,0,0,682,
122,1,0,0,0,683,684,5,119,0,0,684,685,5,97,0,0,685,686,5,116,0,0,
686,687,5,99,0,0,687,688,5,104,0,0,688,689,5,45,0,0,689,690,5,115,
0,0,690,691,5,116,0,0,691,692,5,111,0,0,692,693,5,112,0,0,693,124,
1,0,0,0,694,695,5,115,0,0,695,696,5,117,0,0,696,697,5,98,0,0,697,
698,5,115,0,0,698,699,5,99,0,0,699,700,5,114,0,0,700,701,5,105,0,
0,701,702,5,98,0,0,702,703,5,101,0,0,703,126,1,0,0,0,704,705,5,117,
0,0,705,706,5,110,0,0,706,707,5,115,0,0,707,708,5,117,0,0,708,709,
5,98,0,0,709,710,5,115,0,0,710,711,5,99,0,0,711,712,5,114,0,0,712,
713,5,105,0,0,713,714,5,98,0,0,714,715,5,101,0,0,715,128,1,0,0,0,
716,717,5,111,0,0,717,718,5,112,0,0,718,719,5,116,0,0,719,720,5,
105,0,0,720,721,5,109,0,0,721,722,5,105,0,0,722,723,5,115,0,0,723,
724,5,116,0,0,724,725,5,105,0,0,725,726,5,99,0,0,726,727,5,45,0,
0,727,728,5,114,0,0,728,729,5,101,0,0,729,730,5,103,0,0,730,731,
5,105,0,0,731,732,5,115,0,0,732,733,5,116,0,0,733,734,5,101,0,0,
734,735,5,114,0,0,735,130,1,0,0,0,736,737,5,99,0,0,737,738,5,114,
0,0,738,739,5,100,0,0,739,740,5,116,0,0,740,132,1,0,0,0,741,742,
5,111,0,0,742,743,5,112,0,0,743,744,5,116,0,0,744,745,5,105,0,0,
745,746,5,111,0,0,746,747,5,110,0,0,747,748,5,97,0,0,748,749,5,108,
0,0,749,750,5,45,0,0,750,751,5,111,0,0,751,752,5,110,0,0,752,753,
5,101,0,0,753,134,1,0,0,0,754,755,5,101,0,0,755,756,5,120,0,0,756,
757,5,97,0,0,757,758,5,99,0,0,758,759,5,116,0,0,759,760,5,108,0,
0,760,761,5,121,0,0,761,762,5,45,0,0,762,763,5,111,0,0,763,764,5,
110,0,0,764,765,5,101,0,0,765,136,1,0,0,0,766,767,5,109,0,0,767,
768,5,97,0,0,768,769,5,110,0,0,769,770,5,121,0,0,770,771,5,45,0,
0,771,772,5,117,0,0,772,773,5,110,0,0,773,774,5,105,0,0,774,775,
5,113,0,0,775,776,5,117,0,0,776,777,5,101,0,0,777,138,1,0,0,0,778,
779,5,109,0,0,779,780,5,97,0,0,780,781,5,110,0,0,781,782,5,121,0,
0,782,140,1,0,0,0,783,784,5,111,0,0,784,785,5,114,0,0,785,786,5,
100,0,0,786,787,5,101,0,0,787,788,5,114,0,0,788,789,5,101,0,0,789,
790,5,100,0,0,790,142,1,0,0,0,791,792,5,117,0,0,792,793,5,110,0,
0,793,794,5,105,0,0,794,795,5,116,0,0,795,144,1,0,0,0,796,797,5,
119,0,0,797,798,5,97,0,0,798,799,5,116,0,0,799,800,5,99,0,0,800,
801,5,104,0,0,801,802,5,45,0,0,802,803,5,104,0,0,803,804,5,97,0,
0,804,805,5,110,0,0,805,806,5,100,0,0,806,807,5,108,0,0,807,808,
5,101,0,0,808,146,1,0,0,0,809,810,5,109,0,0,810,811,5,101,0,0,811,
812,5,115,0,0,812,813,5,115,0,0,813,814,5,97,0,0,814,815,5,103,0,
0,815,816,5,101,0,0,816,148,1,0,0,0,817,818,5,97,0,0,818,819,5,116,
0,0,819,820,5,111,0,0,820,821,5,109,0,0,821,822,5,45,0,0,822,823,
5,114,0,0,823,824,5,101,0,0,824,825,5,102,0,0,825,150,1,0,0,0,826,
827,5,105,0,0,827,828,5,110,0,0,828,829,5,116,0,0,829,830,5,101,
0,0,830,831,5,114,0,0,831,832,5,102,0,0,832,833,5,97,0,0,833,834,
5,99,0,0,834,835,5,101,0,0,835,836,5,45,0,0,836,837,5,114,0,0,837,
838,5,101,0,0,838,839,5,102,0,0,839,152,1,0,0,0,840,841,5,111,0,
0,841,842,5,112,0,0,842,843,5,116,0,0,843,844,5,105,0,0,844,845,
5,111,0,0,845,846,5,110,0,0,846,847,5,97,0,0,847,848,5,108,0,0,848,
154,1,0,0,0,849,850,5,108,0,0,850,851,5,105,0,0,851,852,5,115,0,
0,852,853,5,116,0,0,853,156,1,0,0,0,854,855,5,114,0,0,855,856,5,
101,0,0,856,857,5,99,0,0,857,858,5,111,0,0,858,859,5,114,0,0,859,
860,5,100,0,0,860,158,1,0,0,0,861,862,5,98,0,0,862,863,5,111,0,0,
863,864,5,111,0,0,864,865,5,108,0,0,865,160,1,0,0,0,866,867,5,98,
0,0,867,868,5,121,0,0,868,869,5,116,0,0,869,870,5,101,0,0,870,871,
5,115,0,0,871,162,1,0,0,0,872,873,5,100,0,0,873,874,5,111,0,0,874,
875,5,117,0,0,875,876,5,98,0,0,876,877,5,108,0,0,877,878,5,101,0,
0,878,164,1,0,0,0,879,880,5,105,0,0,880,881,5,110,0,0,881,882,5,
116,0,0,882,883,5,51,0,0,883,884,5,50,0,0,884,166,1,0,0,0,885,886,
5,105,0,0,886,887,5,110,0,0,887,888,5,116,0,0,888,889,5,54,0,0,889,
890,5,52,0,0,890,168,1,0,0,0,891,892,5,115,0,0,892,893,5,116,0,0,
893,894,5,114,0,0,894,895,5,105,0,0,895,896,5,110,0,0,896,897,5,
103,0,0,897,170,1,0,0,0,898,899,5,117,0,0,899,900,5,105,0,0,900,
901,5,110,0,0,901,902,5,116,0,0,902,903,5,51,0,0,903,904,5,50,0,
0,904,172,1,0,0,0,905,906,5,117,0,0,906,907,5,105,0,0,907,908,5,
110,0,0,908,909,5,116,0,0,909,910,5,54,0,0,910,911,5,52,0,0,911,
174,1,0,0,0,912,913,5,116,0,0,913,914,5,114,0,0,914,915,5,117,0,
0,915,916,5,101,0,0,916,176,1,0,0,0,917,918,5,102,0,0,918,919,5,
97,0,0,919,920,5,108,0,0,920,921,5,115,0,0,921,922,5,101,0,0,922,
178,1,0,0,0,923,924,5,110,0,0,924,925,5,117,0,0,925,926,5,108,0,
0,926,927,5,108,0,0,927,180,1,0,0,0,928,929,5,45,0,0,929,930,5,62,
0,0,930,182,1,0,0,0,931,932,5,58,0,0,932,184,1,0,0,0,933,934,5,59,
0,0,934,186,1,0,0,0,935,936,5,44,0,0,936,188,1,0,0,0,937,938,5,46,
0,0,938,190,1,0,0,0,939,940,5,123,0,0,940,192,1,0,0,0,941,942,5,
125,0,0,942,194,1,0,0,0,943,944,5,91,0,0,944,196,1,0,0,0,945,946,
5,93,0,0,946,198,1,0,0,0,947,948,5,40,0,0,948,200,1,0,0,0,949,950,
5,41,0,0,950,202,1,0,0,0,951,952,5,60,0,0,952,204,1,0,0,0,953,954,
5,62,0,0,954,206,1,0,0,0,955,957,5,45,0,0,956,955,1,0,0,0,956,957,
1,0,0,0,957,959,1,0,0,0,958,960,7,0,0,0,959,958,1,0,0,0,960,961,
1,0,0,0,961,959,1,0,0,0,961,962,1,0,0,0,962,208,1,0,0,0,963,965,
5,45,0,0,964,963,1,0,0,0,964,965,1,0,0,0,965,974,1,0,0,0,966,975,
5,48,0,0,967,971,7,1,0,0,968,970,7,0,0,0,969,968,1,0,0,0,970,973,
1,0,0,0,971,969,1,0,0,0,971,972,1,0,0,0,972,975,1,0,0,0,973,971,
1,0,0,0,974,966,1,0,0,0,974,967,1,0,0,0,975,982,1,0,0,0,976,978,
5,46,0,0,977,979,7,0,0,0,978,977,1,0,0,0,979,980,1,0,0,0,980,978,
1,0,0,0,980,981,1,0,0,0,981,983,1,0,0,0,982,976,1,0,0,0,982,983,
1,0,0,0,983,993,1,0,0,0,984,986,7,2,0,0,985,987,7,3,0,0,986,985,
1,0,0,0,986,987,1,0,0,0,987,989,1,0,0,0,988,990,7,0,0,0,989,988,
1,0,0,0,990,991,1,0,0,0,991,989,1,0,0,0,991,992,1,0,0,0,992,994,
1,0,0,0,993,984,1,0,0,0,993,994,1,0,0,0,994,210,1,0,0,0,995,999,
7,4,0,0,996,998,7,5,0,0,997,996,1,0,0,0,998,1001,1,0,0,0,999,997,
1,0,0,0,999,1000,1,0,0,0,1000,212,1,0,0,0,1001,999,1,0,0,0,1002,
1007,5,34,0,0,1003,1006,3,215,107,0,1004,1006,8,6,0,0,1005,1003,
1,0,0,0,1005,1004,1,0,0,0,1006,1009,1,0,0,0,1007,1005,1,0,0,0,1007,
1008,1,0,0,0,1008,1010,1,0,0,0,1009,1007,1,0,0,0,1010,1011,5,34,
0,0,1011,214,1,0,0,0,1012,1020,5,92,0,0,1013,1021,7,7,0,0,1014,1015,
5,117,0,0,1015,1016,3,217,108,0,1016,1017,3,217,108,0,1017,1018,
3,217,108,0,1018,1019,3,217,108,0,1019,1021,1,0,0,0,1020,1013,1,
0,0,0,1020,1014,1,0,0,0,1021,216,1,0,0,0,1022,1023,7,8,0,0,1023,
218,1,0,0,0,1024,1025,5,47,0,0,1025,1026,5,47,0,0,1026,1030,1,0,
0,0,1027,1029,8,9,0,0,1028,1027,1,0,0,0,1029,1032,1,0,0,0,1030,1028,
1,0,0,0,1030,1031,1,0,0,0,1031,1033,1,0,0,0,1032,1030,1,0,0,0,1033,
1034,6,109,0,0,1034,220,1,0,0,0,1035,1036,5,47,0,0,1036,1037,5,42,
0,0,1037,1041,1,0,0,0,1038,1040,9,0,0,0,1039,1038,1,0,0,0,1040,1043,
1,0,0,0,1041,1042,1,0,0,0,1041,1039,1,0,0,0,1042,1044,1,0,0,0,1043,
1041,1,0,0,0,1044,1045,5,42,0,0,1045,1046,5,47,0,0,1046,1047,1,0,
0,0,1047,1048,6,110,0,0,1048,222,1,0,0,0,1049,1051,7,10,0,0,1050,
1049,1,0,0,0,1051,1052,1,0,0,0,1052,1050,1,0,0,0,1052,1053,1,0,0,
0,1053,1054,1,0,0,0,1054,1055,6,111,0,0,1055,224,1,0,0,0,18,0,956,
961,964,971,974,980,982,986,991,993,999,1005,1007,1020,1030,1041,
1052,1,0,1,0
];
private static __ATN: antlr.ATN;
File diff suppressed because it is too large Load Diff
@@ -52,6 +52,7 @@ import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
import { ValueTypeContext } from "./QuixosCapabilityParser.js";
import { RecordFieldContext } from "./QuixosCapabilityParser.js";
import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
import { CardinalityContext } from "./QuixosCapabilityParser.js";
import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
@@ -370,6 +371,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitValueType?: (ctx: ValueTypeContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.recordField`.
* @param ctx the parse tree
* @return the visitor result
*/
visitRecordField?: (ctx: RecordFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
* @param ctx the parse tree
+32 -9
View File
@@ -1,6 +1,6 @@
import childProcess from "node:child_process";
import crypto from "node:crypto";
import { mkdir, readFile, realpath } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import type { CapabilityRepositoryResolver } from "./assembly.js";
@@ -61,6 +61,26 @@ export const createGitCapabilityResolver = async (options: {
checkoutRoot,
checkoutName(kind, source.repository, source.commit),
);
const verify = async (checkout: string) => {
const { stdout } = await execFile("git", ["-C", checkout, "rev-parse", "HEAD"]);
if (stdout.trim().toLowerCase() !== source.commit.toLowerCase()) {
throw new Error(`Locked commit mismatch for ${source.repository}: wanted ${source.commit}, fetched ${stdout.trim()}`);
}
const { stdout: changes } = await execFile("git", ["-C", checkout, "status", "--porcelain", "--untracked-files=all"]);
if (changes.trim()) throw new Error(`Dependency checkout was modified: ${checkout}`);
};
// Only complete, checked clones become visible under the deterministic name.
// Concurrent resolvers may fetch independently, but cannot observe a partial clone.
if (await stat(directory).then(() => true, (error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return false;
throw error;
})) {
await verify(directory);
return { directory };
}
const staging = await mkdtemp(path.join(checkoutRoot, ".fetch-"));
const checkout = path.join(staging, "checkout");
try {
await execFile("git", [
"-c",
"advice.detachedHead=false",
@@ -71,18 +91,21 @@ export const createGitCapabilityResolver = async (options: {
"--branch",
`quixos-reachability/${source.commit.toLowerCase()}`,
source.repository,
directory,
checkout,
]);
const { stdout } = await execFile("git", ["-C", directory, "rev-parse", "HEAD"]);
if (stdout.trim().toLowerCase() !== source.commit.toLowerCase()) {
throw new Error(
`Locked commit mismatch for ${source.repository}: ` +
`wanted ${source.commit}, fetched ${stdout.trim()}`,
);
await verify(checkout);
try { await rename(checkout, directory); }
catch (error) {
if (!["EEXIST", "ENOTEMPTY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
await verify(directory);
}
} finally {
await rm(staging, { recursive: true, force: true });
}
return { directory };
})();
checkouts.set(key, pending);
return await pending;
try { return await pending; }
catch (error) { checkouts.delete(key); throw error; }
};
};
+8 -1
View File
@@ -2,12 +2,19 @@
// Data only: never load files, resolve repositories, or evaluate code.
import { parseQx } from "./source.js";
import { parseQuixosLockDocument } from "../resource-lock/parser.js";
import { instantiateWorkspaceIdentity } from "./structural-edits.js";
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
if (Buffer.byteLength(input) > 6 * 1024 * 1024) throw new Error("Inspection input exceeds limit");
}
const files: { path: string; source: string }[] = JSON.parse(input);
const document = JSON.parse(input);
if (!Array.isArray(document) && document?.operation === "instantiate-workspace") {
if (typeof document.source !== "string" || document.source.length > 262144 || typeof document.workspaceId !== "string") throw new Error("Invalid template identity request");
process.stdout.write(JSON.stringify({ source: instantiateWorkspaceIdentity(document.source, document.workspaceId) }));
process.exit(0);
}
const files: { path: string; source: string }[] = document;
if (!Array.isArray(files) || files.length > 128) throw new Error("Too many source files");
const result = files.map(({ path, source }) => {
if (typeof path !== "string" || typeof source !== "string" || source.length > 262144)
+5
View File
@@ -319,6 +319,11 @@ const lowerValueType = (
if (context.MESSAGE()) {
return valueType.message(stringValue(context.stringLiteral()));
}
if (context.RECORD()) {
const fields = context.recordField().map((field) => [identifier(field.identifier()), lowerValueType(state, field.valueType())] as const);
if (new Set(fields.map(([name]) => name)).size !== fields.length) loweringIssue(state, context, "invalid-type", "Duplicate record field");
return {kind: "record", fields: Object.fromEntries(fields)};
}
if (context.ATOM_REF()) {
const name = identifier(context.identifier());
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
+26 -21
View File
@@ -7,9 +7,12 @@ import {promisify} from "node:util";
import {loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
import {contentDigest} from "../capability-model/evolution.js";
import {planStructure, applyStructure, type StructuralRequest} from "./structural-plan.js";
import {snapshotRepository, checkResourceCandidate, checkWorkspaceCandidate} from "./candidate-check.js";
import {snapshotRepository} from "./candidate-check.js";
import {compileWorkspaceRepository, compileCapabilityResourceRepository, type ResolvedCapabilityResource} from "./assembly.js";
import {createGitCapabilityResolver} from "./git-resolver.js";
import {snapshotCommit, buildImmutableCandidate} from "./checked-build.js";
import {planEvolution, type WorkspaceRevision, type EvolutionReview} from "../capability-model/index.js";
import {withFileLock} from "./file-lock.js";
const execFile = promisify(callback);
type Source = {repository: string; commit: string};
export type UpgradeNode = {kind: "workspace" | "package" | "interface"; directory: string; source: Source};
@@ -47,7 +50,7 @@ export const discoverUpgradeSpec = async (workbench: string): Promise<UpgradeSpe
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
const root = await location(workbench, "root");
const nodes: UpgradeNode[] = [{kind: "workspace", directory: "root", source: {
repository: await command(root, "git", ["remote", "get-url", "origin"]),
repository: await command(root, "git", ["config", "--get", "remote.origin.url"]),
commit: await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]),
}}, ...graph.resources.map((entry: {kind: "interface" | "package"; directory: string; source: Source}) => ({kind: entry.kind, source: entry.source,
directory: path.relative(workbench, path.resolve(workbench, entry.directory))}))];
@@ -75,7 +78,7 @@ export const planPinUpgrades = async (workbenchPath: string, spec: UpgradeSpec):
const nodes: NodePlan[] = [];
for (const node of spec.nodes) {
const root = await location(workbench, node.directory);
if (await command(root, "git", ["remote", "get-url", "origin"]) !== node.source.repository) throw new Error(`Upgrade origin differs from selected source: ${node.directory}`);
if (await command(root, "git", ["config", "--get", "remote.origin.url"]) !== node.source.repository) throw new Error(`Upgrade origin differs from selected source: ${node.directory}`);
const loaded = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!loaded.ok) throw new Error(`Invalid lock in ${node.directory}: ${loaded.diagnostics.map((entry) => entry.message).join("; ")}`);
const dependencies = loaded.lock.resources.map((resource) => keys.get(sourceKey(resource))).filter((value): value is string => Boolean(value));
@@ -102,22 +105,24 @@ export type UpgradeEffects = {
const effects: UpgradeEffects = {
async check(node, root, output, spec) {
if (node.kind === "workspace" && !spec.baseline && !spec.bootstrap) throw new Error("Upgrading a workspace requires its checked active baseline for major-review checks (or explicit bootstrap:true for a new workspace)");
// Publication checks consume already-published dependency revisions, never
// workbench dirty overlays masquerading as those immutable identities.
const snapshotMap = `${output}-published-dependencies.json`;
await fs.writeFile(snapshotMap, JSON.stringify({resources: []}), {flag: "wx"});
const result = node.kind === "workspace" ? await checkWorkspaceCandidate({root, output, snapshotMap, baseline: spec.baseline, reviews: spec.reviews})
: await checkResourceCandidate({root, output, kind: node.kind, source: node.source, snapshotMap});
if (result.blockers.length) throw new Error(`Refactor required in ${node.directory}: ${result.blockers.join("; ")}`);
const evolution = (result as {evolution?: {reviews: {accepted: boolean}[]}}).evolution;
if (evolution?.reviews.some((review) => !review.accepted)) throw new Error("Explicit semantic-major review required before publishing the workspace");
// Explicit baseline upgrades use the same immutable Nix checker. Retaining
// an unverified source is safe and must precede a remote flake fetch.
await fs.mkdir(output);
const commit = await snapshotCommit(root);
await effects.publish(root, commit);
const artifact = await buildImmutableCandidate({...node.source, commit}, node.kind, path.join(output, "nix.log"));
const candidate = await fs.readFile(path.join(artifact, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidate);
if (node.kind === "workspace") {
const baseline = spec.baseline ? JSON.parse(await fs.readFile(spec.baseline, "utf8")) as WorkspaceRevision : null;
const reviews = spec.reviews ? JSON.parse(await fs.readFile(spec.reviews, "utf8")) as EvolutionReview[] : [];
const evolution = planEvolution(baseline, JSON.parse(candidate), {reviews});
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
if (evolution.blockers.length) throw new Error(`Refactor required in ${node.directory}: ${evolution.blockers.join("; ")}`);
}
},
async snapshot(root) {
// Unlike checking, publication deliberately captures the working copy.
await command(root, "jj", ["status"]);
const conflicts = await command(root, "jj", ["resolve", "--list"]);
if (conflicts) throw new Error("Resolve source conflicts before publication");
const commit = await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]);
const commit = await snapshotCommit(root);
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Publication did not resolve an exact commit");
await command(root, "git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"]);
const tracked = new Set((await command(root, "git", ["ls-tree", "-r", "--name-only", "-z", commit])).split("\0"));
@@ -142,17 +147,17 @@ export const applyPinUpgrades = async (plan: UpgradePlan, journalId?: string, im
const directory = path.join(plan.workbench, ".quixos", "upgrades");
await fs.mkdir(directory, {recursive: true, mode: 0o700});
if (await fs.realpath(directory) !== directory) throw new Error("Upgrade journals must not cross symlinks");
const lock = await fs.open(path.join(directory, "writer.lock"), "wx", 0o600);
const id = journalId ?? randomUUID();
if (!/^[a-f0-9-]{36}$/.test(id)) {await lock.close(); await fs.unlink(path.join(directory, "writer.lock")); throw new Error("Invalid upgrade journal ID");}
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid upgrade journal ID");
const filename = path.join(directory, `${id}.json`);
return withFileLock(path.join(directory, "writer.lock"), async () => {
try {
const journal: Journal = journalId ? JSON.parse(await fs.readFile(filename, "utf8")) : {schemaVersion: 1, plan, steps: []};
if (journal.plan.digest !== plan.digest) throw new Error("Upgrade journal belongs to another plan");
if (!journalId) await writeJournal(filename, journal);
for (const node of plan.nodes) {
const root = await location(plan.workbench, node.directory);
if (await command(root, "git", ["remote", "get-url", "origin"]) !== node.source.repository) throw new Error("Upgrade remote changed after planning");
if (await command(root, "git", ["config", "--get", "remote.origin.url"]) !== node.source.repository) throw new Error("Upgrade remote changed after planning");
let step = journal.steps.find((entry) => entry.directory === node.directory);
if (step?.phase === "published") continue;
if (!step) {
@@ -245,5 +250,5 @@ export const applyPinUpgrades = async (plan: UpgradePlan, journalId?: string, im
}
return {id, journal: filename, revisions: journal.steps.map((step) => ({directory: step.directory, commit: step.commit})), activated: false};
} catch (error) {throw new Error(`${error instanceof Error ? error.message : String(error)}; upgrade journal ${filename}`, {cause: error});}
finally {await lock.close(); await fs.unlink(path.join(directory, "writer.lock"));}
});
};
+47 -17
View File
@@ -9,6 +9,7 @@ import type {StructuralRequest} from "./structural-plan.js";
type Source = {repository: string; commit: string};
type Registry = {generatedBy: "qx-scaffold-v1"; name: string; id: string; revision: string; exports: {name: string; id: string; file: string; migration?: boolean}[]};
export type ScaffoldRecipe = {
template?: "typescript" | "typescript-react";
source: Source; directory?: string; name?: string; id?: string; revision?: string;
declaration?: string;
tools?: {quixos: Source; protocol: Source; helpers: Source; sdk: Source};
@@ -49,23 +50,31 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
let catalog: MigrationCatalog & {generatedBy: "qx-scaffold-v1"};
if (command === "package") {
const name = safeName(spec.name);
if (spec.template && !["typescript", "typescript-react"].includes(spec.template)) throw new Error("Unknown package template");
const react = spec.template === "typescript-react";
if (!spec.id || !spec.revision || !spec.tools) throw new Error("Package scaffold requires id, revision, and exact quixos/protocol/helpers/sdk tool sources");
Object.values(spec.tools).forEach(source);
registry = {generatedBy: "qx-scaffold-v1", name, id: spec.id, revision: spec.revision, exports: []};
catalog = {generatedBy: "qx-scaffold-v1", schemaVersion: 1, contracts: {}, migrations: []};
create("package.qx", `package ${name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`);
if (react) registry.exports.push({name: "sourceGet", id: `export:${name}:source`, file: "src/impl/sourceGet.ts"});
create("package.qx", `package ${name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n${react ? ` function sourceGet id ${JSON.stringify(`export:${name}:source`)} : unit -> string;\n` : ""}}\n`);
create("quixos.lock", formatQuixosLock({formatVersion: 1, quixos: source(spec.tools.quixos), resources: []}));
create("package.json", json({name: `@quixos/${name.toLowerCase()}`, version: "0.1.0", private: true, type: "module", packageManager: "yarn@4.18.0",
scripts: {build: "tsc -p tsconfig.json", typecheck: "tsc --noEmit"}, dependencies: {"@quixos/camino-package-runtime": `${spec.tools.sdk.repository}#commit=${spec.tools.sdk.commit}`},
devDependencies: {"@types/node": "^24", typescript: "^7.0.2"}}));
create("tsconfig.json", json({compilerOptions: {target: "ES2023", module: "NodeNext", moduleResolution: "NodeNext", strict: true, outDir: "dist", skipLibCheck: true}, include: ["src/**/*.ts"]}));
scripts: {build: `tsc -p tsconfig.json${react ? " && node scripts/build-component.mjs" : ""}`, typecheck: "tsc --noEmit"}, dependencies: {"@quixos/camino-package-runtime": `${spec.tools.sdk.repository}#commit=${spec.tools.sdk.commit}`},
devDependencies: {"@types/node": "^24", typescript: "^7.0.2", ...(react ? {react: "^18.3.1", "@types/react": "^18.3.12", esbuild: "^0.25.12"} : {})}}));
create("tsconfig.json", json({compilerOptions: {target: "ES2023", module: "NodeNext", moduleResolution: "NodeNext", strict: true, types: ["node", ...(react ? ["react"] : [])], outDir: "dist", rootDir: "src", skipLibCheck: true, ...(react ? {jsx: "react-jsx", esModuleInterop: true} : {})}, include: ["src/**/*.ts", "src/**/*.tsx"]}));
if (react) {
create("src/component.tsx", `// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`);
create("src/impl/sourceGet.ts", `import {readFile} from "node:fs/promises";\nimport type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["sourceGet"] = () => readFile(new URL("./component.mjs", import.meta.url), "utf8");\n`);
create("scripts/build-component.mjs", `import {build} from "esbuild";\nconst platform = new Map([\n ["react", "/__quixos/platform/react/v18.mjs"],\n ["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],\n ["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],\n ["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],\n]);\nawait build({entryPoints: ["dist/component.js"], outfile: "dist/component.mjs", bundle: true, format: "esm", platform: "browser", target: "es2022", plugins: [{name: "quixos-platform", setup(api) {api.onResolve({filter: /.*/}, ({path}) => platform.has(path) ? {path: platform.get(path), external: true} : undefined);}}]});\n`);
}
create(".gitignore", "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n");
create(".yarnrc.yml", `nodeLinker: node-modules\nenableScripts: true\nnpmMinimalAgeGate: 0\napprovedGitRepositories:\n - ${JSON.stringify(spec.tools.sdk.repository)}\nsupportedArchitectures:\n os: [current, linux]\n cpu: [current, x64, arm64]\n libc: [current, glibc]\n`);
const nixifyPluginUrl = spec.nixifyPluginUrl ?? "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js";
const plugin = new URL(nixifyPluginUrl);
if (plugin.protocol !== "https:" || plugin.username || plugin.password || plugin.search || plugin.hash || !/\/commit\/[a-f0-9]{40,64}\//.test(plugin.pathname)) throw new Error("Nixify plugin must have an exact credential-free HTTPS commit URL");
generated("quixos.toolchain.json", json({generatedBy: "qx-scaffold-v1", nixifyPluginUrl}));
create("quixos.check.json", json({backend: "typescript", bindingOutput: "src/generated-bindings.ts"}));
create("quixos.check.json", json({backend: "typescript", bindingOutput: "src/gen/qx.ts"}));
create("flake.nix", `{
inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))};
inputs.nixpkgs.follows = "protocol/nixpkgs";
@@ -74,23 +83,44 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
outputs = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
bindings = { system, ... }: {
generator = (builtins.getAttr system protocol.packages).default;
repository = ${nixString(spec.source.repository)};
commit = self.rev or (throw "Publish an exact package revision before building an activation artifact");
packageRevisionId = ${nixString(spec.revision)};
output = "src/generated-bindings.ts";
resources = map (entry: entry // { directory = builtins.fetchGit { url = entry.repository; rev = entry.commit; ref = "refs/tags/quixos-reachability/" + entry.commit; }; }) (builtins.fromJSON (builtins.readFile ./quixos.resources.json)).resources;
};
bundle = { entry = "dist/server.js"; };
migrationEntrypoint = "dist/migrate.js";
installServer = { libexecName = ${JSON.stringify(name.toLowerCase())}; descriptorPath = "descriptor.quixos-package.txtpb"; };
installServer = { libexecName = ${JSON.stringify(name.toLowerCase())}; descriptorPath = "descriptor.quixos-package.txtpb"; ${react ? 'extraFiles = [ { source = "dist/component.mjs"; target = "component.mjs"; } ];' : ""} };
};
}\n`);
generated("quixos.resources.json", json({generatedBy: "qx-scaffold-v1", resources: []}));
} 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");
@@ -107,7 +137,7 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
files.push({file: prefix + "package.qx", edits: [{operation: "append", parent: {kind: "packageResourceDecl", id: registry.id}, source: declaration}]});
const file = `src/${command === "migration" ? "migrations" : "impl"}/${name}.ts`;
const implementation = command === "migration" ? `import type {MigrationContext} from "@quixos/camino-package-runtime";\nexport const handler = async (_context: MigrationContext): Promise<void> => { throw new Error(${JSON.stringify(`Implement migration ${name}`)}); };\n`
: `import type {Implementation} from "../generated-bindings.js";\nexport const handler: Implementation[${JSON.stringify(name)}] = ${derived ? '{kind: "derived", get: ' : ""}async (_context) => { throw new Error(${JSON.stringify(`Implement ${name}`)}); }${derived ? "}" : ""};\n`;
: `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`;
create(file, implementation);
registry.exports.push({name, id: spec.id, file, ...(command === "migration" ? {migration: true} : {})});
if (command === "migration") {
@@ -129,7 +159,7 @@ export const scaffoldRecipe = async (root: string, command: "package" | "functio
validateMigrationCatalog(catalog, new Set(registry.exports.map((entry) => entry.id)));
generated("quixos.scaffold.json", json(registry));
generated("quixos.migrations.json", json(catalog));
generated("src/server.ts", marker + `import {servePackageRuntime} from "@quixos/camino-package-runtime";\nimport {createRuntime} from "./generated-bindings.js";\n` + registry.exports.filter((entry) => !entry.migration).map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") +
generated("src/server.ts", marker + `import {servePackageRuntime} from "@quixos/camino-package-runtime";\nimport {createRuntime} from "./gen/qx.js";\n` + registry.exports.filter((entry) => !entry.migration).map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") +
`servePackageRuntime(createRuntime({\n` + registry.exports.map((entry) => ` ${JSON.stringify(entry.name)}: ${entry.migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl${registry.exports.filter((value) => !value.migration).indexOf(entry)}`},`).join("\n") + `\n}));\n`);
const migrations = registry.exports.filter((entry) => entry.migration);
generated("src/migrate.ts", marker + `import {serveMigration} from "@quixos/camino-package-runtime";\n` + migrations.map((entry, index) => `import {handler as impl${index}} from ${JSON.stringify(`./${entry.file.slice(4, -3)}.js`)};\n`).join("") + `await serveMigration({${migrations.map((entry, index) => `${JSON.stringify(entry.id)}: impl${index}`).join(", ")}});\n`);
+22 -1
View File
@@ -21,6 +21,21 @@ const selectable = new Set(["workspaceDecl", "fragmentDecl", "interfaceResourceD
"valueMember", "relationshipMember", "operationMember", "packageOperationExport", "packageFunctionExport", "packageConstructorExport",
"conformanceDecl", "stateDecl", "edgeDecl", "constructorBindingDecl", "resourceImportDecl", "sourceImportDecl", "operationBindingDecl"]);
/** Bind only the template root identity; schema/atom identities are reusable.
* The revision's real identity is derived from its containing commit at compile time. */
export const instantiateWorkspaceIdentity = (source: string, workspaceId: string): string => {
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(workspaceId)) throw new Error("Workspace identity must be a UUID");
const syntax = parseQx(source);
if (syntax.diagnostics.length) throw new Error("Cannot instantiate a malformed template workspace");
const declaration = syntax.root.children.find(node => node.kind === "workspaceDecl");
const literals = declaration?.children.filter(node => node.kind === "stringLiteral");
if (!literals || literals.length !== 3) throw new Error("Template must contain a workspace declaration");
return applySourceEdits(source, [
{ ...literals[0], text: JSON.stringify(workspaceId) },
{ ...literals[1], text: JSON.stringify(`workspace-revision:${workspaceId}:source`) },
]);
};
const select = (source: string, selector: StructuralSelector): {node: SyntaxNode; syntax: ReturnType<typeof parseQx>} => {
if (!selectable.has(selector.kind)) throw new Error(`Unsupported structural selector ${selector.kind}`);
const syntax = parseQx(source);
@@ -117,10 +132,16 @@ export const editStructure = (source: string, edit: StructuralEdit): string => {
if (!closing) throw new Error("Append requires a declaration with a body");
result = applySourceEdits(source, [{start: closing.start, end: closing.start, text: `\n${edit.source}\n`}]);
} else {
// A resource parser node includes imports/external declarations preceding
// its header. Replacing the declaration must not delete that preamble.
const identifier = node.children.find(child => child.kind === "identifier");
const header = ["packageResourceDecl", "interfaceResourceDecl"].includes(node.kind) && identifier
? syntax.tokens.filter(token => token.start >= node.start && token.end <= identifier.start &&
token.kind === (node.kind === "packageResourceDecl" ? "PACKAGE" : "INTERFACE")).at(-1)?.start : undefined;
const wrapper = edit.operation === "remove" && ["stateDecl", "edgeDecl"].includes(node.kind)
? [...walkSyntax(syntax.root)].filter((entry) => ["conformanceItem", "sharedAttachmentDecl"].includes(entry.kind) && entry.start <= node.start && entry.end >= node.end).sort((a, b) => (a.end - a.start) - (b.end - b.start))[0]
: undefined;
result = applySourceEdits(source, [{start: wrapper?.start ?? node.start, end: wrapper?.end ?? node.end, text: edit.operation === "replace" ? edit.source : ""}]);
result = applySourceEdits(source, [{start: wrapper?.start ?? header ?? node.start, end: wrapper?.end ?? node.end, text: edit.operation === "replace" ? edit.source : ""}]);
}
const checked = parseQx(result);
if (checked.diagnostics.length) throw new Error(`Invalid structural change: ${checked.diagnostics.map((entry) => entry.message).join("; ")}`);
+26 -12
View File
@@ -8,17 +8,21 @@ import { snapshotRepository, localResourceSnapshots } from "./candidate-check.js
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import {bindingSchema, generateTypeScriptBindings} from "../bindings/index.js";
import { parseQx } from "./source.js";
import { parseQuixosLockDocument } from "../resource-lock/index.js";
import { withFileLock } from "./file-lock.js";
export type StructuralRequest = {
kind: "workspace" | "interface" | "package";
source?: {repository: string; commit: string};
resourceRoot?: string;
validation?: "syntax" | "resource-graph";
files: ({file: string; edits: StructuralEdit[]} | {file: string; create: string} | {file: string; generated: string})[];
};
type Change = {file: string; before: string | null; after: string; mode: number};
type Journal = {schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[]};
const safeFile = (file: string) => {
if (!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|json|nix|txtpb))$/.test(file)
if (!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|mjs|json|nix|txtpb))$/.test(file)
|| file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))) throw new Error(`Unsafe scaffold path ${file}`);
};
const read = async (root: string, file: string): Promise<string | null> => {
@@ -50,6 +54,7 @@ const durableJson = async (file: string, value: unknown) => {
/** Validate the entire edited resource graph in a private snapshot before writes. */
export const planStructure = async (rootPath: string, request: StructuralRequest, snapshotMap?: string) => {
if (request.validation && !["syntax", "resource-graph"].includes(request.validation)) throw new Error("Unknown structural validation mode");
const root = await fs.realpath(rootPath);
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-structure-"));
try {
@@ -74,11 +79,28 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
after = input.edits.reduce(editStructure, before);
}
if (Buffer.byteLength(after) > 1024 * 1024) throw new Error("Scaffold file exceeds 1 MiB");
if (input.file.endsWith("package.qx")) {
let registry;
try { registry = JSON.parse(await fs.readFile(path.join(root, path.dirname(input.file), "quixos.scaffold.json"), "utf8")); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
if (registry?.generatedBy === "qx-scaffold-v1") {
const declaration = parseQx(after).root.children.find(node => node.kind === "packageResourceDecl");
const literal = declaration?.children.find(node => node.kind === "stringLiteral");
if (literal && JSON.parse(after.slice(literal.start, literal.end)) !== registry.id)
throw new Error("Cannot change a scaffold-owned package identity independently of its registry; create a new managed package instead");
}
}
const mode = before === null ? 0o644 : (await fs.stat(path.join(root, input.file))).mode & 0o777;
changes.push({file: input.file, before, after, mode});
await containedParent(snapshot.directory, input.file);
await fs.writeFile(path.join(snapshot.directory, input.file), after);
}
if (request.validation === "syntax") {
for (const change of changes) {
if (change.file.endsWith(".qx") && parseQx(change.after, change.file).diagnostics.length) throw new Error(`Invalid QX syntax in ${change.file}`);
if (change.file.endsWith(".lock") && !parseQuixosLockDocument(change.after, change.file).ok) throw new Error(`Invalid lock syntax in ${change.file}`);
}
} else {
const localMap = path.join(temporary, "local-resources.json");
await fs.writeFile(localMap, JSON.stringify(await localResourceSnapshots(root, snapshotMap)));
const resolveResource = await createGitCapabilityResolver({checkoutRoot: path.join(temporary, "resources"), snapshotMap: localMap});
@@ -93,7 +115,6 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
const artifacts = [
{file: configuration.bindingOutput as string, after: generateTypeScriptBindings(bindingSchema(compiled), compiled.resource.revision.revisionId, configuration.options)},
{file: "quixos.resources.json", after: JSON.stringify({generatedBy: "qx-scaffold-v1", resources: compiled.resources.filter((entry) => entry.directory !== resourceRoot).map((entry) => ({kind: entry.kind, repository: entry.source.repository, commit: entry.source.commit}))}, null, 2) + "\n"},
];
for (const artifact of artifacts) {
const file = request.resourceRoot ? `${request.resourceRoot}/${artifact.file}` : artifact.file;
@@ -108,11 +129,12 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
}
}
else throw new Error("Resource plans require kind and exact authored source identity");
}
if (changes.length > 100) throw new Error("Structural plan including generated artifacts exceeds 100 files");
// Validation may fetch dependencies; reject edits made while it was running.
for (const entry of changes) if (await read(root, entry.file) !== entry.before) throw new Error(`Source changed while planning: ${entry.file}`);
for (const entry of observed) if (contentDigest(await fs.readFile(path.join(root, entry.file), "utf8")) !== entry.digest) throw new Error(`Validation input changed while planning: ${entry.file}`);
return {root, changes, observed, digest: contentDigest(changes), validation: "resource-graph" as const};
return {root, changes, observed, digest: contentDigest(changes), validation: request.validation ?? "resource-graph" as const};
} finally { await fs.rm(temporary, {recursive: true, force: true}); }
};
@@ -154,15 +176,7 @@ const replayStructure = async (rootPath: string, id: string) => {
const withStructureLock = async <T>(root: string, work: () => Promise<T>) => {
await containedParent(root, ".quixos/scaffolds/placeholder.json");
const lock = path.join(root, ".quixos", "scaffolds", "writer.lock");
// Never steal a possibly live writer's lock. A process crash requires the
// operator to verify that writer is gone, remove this lock, then resume its
// journal. This is deliberately fail-closed instead of guessing from a PID.
const handle = await fs.open(lock, "wx", 0o600).catch((error) => {
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Another scaffold writer or interrupted writer owns ${lock}; verify it has exited before removing its lock and resuming`);
throw error;
});
try { await handle.writeFile(JSON.stringify({pid: process.pid})); await handle.sync(); return await work(); }
finally { await handle.close(); await fs.unlink(lock); }
return withFileLock(lock, work);
};
export const resumeStructure = async (rootPath: string, id: string) => {
const root = await fs.realpath(rootPath);
+174 -28
View File
@@ -1,19 +1,162 @@
#!/usr/bin/env node
import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises";
import { parseQx, formatQx, lintQx } from "./source.js";
import { scaffoldAtom } from "./scaffold.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { planEvolution } from "../capability-model/index.js";
import { checkWorkspaceCandidate, checkResourceCandidate, snapshotRepository } from "./candidate-check.js";
import { snapshotRepository } from "./candidate-check.js";
import {planPinUpgrades, applyPinUpgrades, discoverUpgradeSpec, type UpgradeSpec} from "./pin-upgrades.js";
import path from "node:path";
import os from "node:os";
import {spawnSync} from "node:child_process";
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
import {scaffoldRecipe, type ScaffoldRecipe} from "./scaffold-recipes.js";
import {buildCheckedPackage, buildImmutableCandidate, snapshotCommit} from "./checked-build.js";
import {formatQuixosLock, loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
import { walkSyntax } from "./source.js";
import { inspectWorkbench } from "./authoring-inspect.js";
import { authoringContext } from "./authoring-context.js";
import { convergeAuthoring } from "./authoring-converge.js";
import { checkAuthoring } from "./authoring-check.js";
import { authoringWorklist } from "./authoring-worklist.js";
const authorSource = async (root: string) => {
const result = spawnSync("git", ["config", "--get", "remote.origin.url"], {cwd: root, encoding: "utf8"});
if (result.error || result.status !== 0) throw new Error("Managed resource has no origin");
return {repository: result.stdout.trim(), commit: await snapshotCommit(root)};
};
const readSpec = async (value: string) => {
if (value === "-") { let input = ""; for await (const chunk of process.stdin) { input += chunk; if (input.length > 1024 * 1024) throw new Error("Scaffold specification exceeds 1 MiB"); } return JSON.parse(input); }
return JSON.parse(value.trimStart().startsWith("{") ? value : await readFile(value, "utf8"));
};
const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
root: plan.root, validation: plan.validation,
changes: plan.changes.map(change => ({file: change.file, beforeBytes: change.before?.length ?? 0, afterBytes: change.after?.length ?? 0})),
note: "Structural plan only, not implementation verification. Run qx-workspace check while iterating.",
});
const main = async () => {
const [command, ...args] = process.argv.slice(2);
if (command === "worklist" && !args.includes("--help")) {
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
return;
}
if (["author-check", "author-contract"].includes(command) && !args.includes("--help")) {
const [root, output, ...flags] = args;
if (!root || !output) throw new Error("usage: quixos-qx author-check ROOT OUTPUT [--baseline FILE] [--reviews FILE]");
const options: {baseline?: string; reviews?: string} = {};
for (let index = 0; index < flags.length; index += 2) {
if (!flags[index + 1]) throw new Error("Missing check option value");
if (flags[index] === "--baseline") options.baseline = flags[index + 1];
else if (flags[index] === "--reviews") options.reviews = flags[index + 1];
else throw new Error(`Unknown check option ${flags[index]}`);
}
const result = await checkAuthoring(root, output, {...options, contractOnly: command === "author-contract"});
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (result.blockers.length) process.exitCode = 1;
return;
}
if (["converge", "_converge"].includes(command) && !args.includes("--help")) {
if (!args.length || args.length > 2) throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
const context = await authoringContext(args[0]);
if (command === "converge") {
const result = spawnSync("flock", ["--exclusive", "--nonblock", "--conflict-exit-code", "75", path.join(context.workbench, ".quixos/converge.lock"),
process.execPath, process.argv[1], "_converge", context.workbench, ...(args[1] ? [args[1]] : [])], {stdio: "inherit"});
if (result.error) throw result.error;
if (result.status === 75) process.stderr.write("Another source coordinator is running; retry when it finishes.\n");
process.exitCode = result.status ?? 1; return;
}
const result = await convergeAuthoring(context.workbench, args[1]);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (!result.converged) process.exitCode = 1;
return;
}
if (command === "check-committed" && !args.includes("--help")) {
const [kind, repository, commit, log, ...extra] = args;
if (!["workspace", "interface", "package"].includes(kind) || !log || extra.length) throw new Error("usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE");
process.stdout.write(`${await buildImmutableCandidate({repository, commit}, kind as "workspace" | "interface" | "package", log)}\n`);
return;
}
if (!command || command === "--help" || args.includes("--help")) {
process.stdout.write("quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-function, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n");
return;
}
if (command === "inspect" || command === "resources") {
if (!args[0] || args.length > (command === "inspect" ? 2 : 1)) throw new Error(`usage: quixos-qx ${command} WORKBENCH${command === "inspect" ? " [RESOURCE]" : ""}`);
const result = command === "inspect" ? await inspectWorkbench(args[0], args[1]) : await authoringContext(args[0]);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
if (command === "source-baseline") {
if (args.length !== 1) throw new Error("usage: quixos-qx source-baseline WORKBENCH");
process.stdout.write(`${JSON.stringify(await (await authoringContext(args[0])).baseline())}\n`);
return;
}
if (command === "scaffold-dependency") {
const [root, kind, name, ...remaining] = args;
let repository: string | undefined, commit: string | undefined;
const flags = [...remaining];
if (flags.length && !flags[0].startsWith("--")) { repository = flags.shift(); commit = flags.shift(); }
else if (root && ["interface", "package"].includes(kind) && name) {
const context = await authoringContext(root);
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
const matches = context.resources.filter(entry => entry.kind === kind && (entry.resourceId === name || path.basename(entry.directory) === name || path.join(context.workbench, entry.directory) === alias));
if (matches.length !== 1 || !matches[0].source) throw new Error(`Select exactly one registered ${kind} with qx-workspace resources; no match for ${name}`);
const selected = matches[0];
let source = selected.source!;
if (flags.includes("--write")) {
const retained = spawnSync("quixos-qx", ["converge", context.workbench, selected.directory], {encoding: "utf8", env: {...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1"}});
if (retained.error || retained.status !== 0) throw new Error(`Dependency source needs attention: ${retained.error?.message ?? retained.stdout ?? retained.stderr}`);
source = JSON.parse(retained.stdout).candidate;
}
repository = source.repository; commit = source.commit;
}
if (!root || !["interface", "package"].includes(kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") || !repository || !commit || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]");
const resourceKind = kind as "package" | "interface";
let entrypoint: "workspace" | "package" | "interface" | undefined;
for (const candidate of ["workspace", "package", "interface"] as const) {
try {await readFile(path.join(root, `${candidate}.qx`)); if (entrypoint) throw new Error("Ambiguous repository entrypoint"); entrypoint = candidate;}
catch (error) {if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;}
}
if (!entrypoint) throw new Error("No QX repository entrypoint");
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok) throw new Error("Invalid resource lock");
let target = "quixos.lock";
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
if (parsed.ok && parsed.document.resources.some(entry => entry.kind === kind && entry.binding === name)) target = file;
}
const request: StructuralRequest = {kind: entrypoint, source: await authorSource(root), files: [
{file: `${entrypoint}.qx`, edits: [{operation: "import", kind: resourceKind, name}]},
{file: target, edits: [{operation: "dependency", kind: resourceKind, name, source: {repository, commit}}]},
]};
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
return;
}
if (command === "scaffold-interface") {
const [root, specFile, ...flags] = args;
if (!root || !specFile || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]");
const spec = await readSpec(specFile) as ScaffoldRecipe;
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id || !spec.revision || !spec.tools?.quixos) throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
const request: StructuralRequest = {kind: "interface", source: spec.source, files: [
{file: "interface.qx", create: `interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`},
{file: "quixos.lock", create: formatQuixosLock({formatVersion: 1, quixos: {resolver: "git", ...spec.tools.quixos}, resources: []})},
{file: ".gitignore", create: ".quixos/\n"},
]};
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
return;
}
if (command === "build-package") {
if (args.length !== 3) throw new Error("usage: quixos-qx build-package COMMITTED_SOURCE SCHEMA PACKAGE_REVISION_ID");
process.stdout.write(`${await buildCheckedPackage(args[0], args[1], args[2])}\n`);
return;
}
if (command === "source-digest") {
if (!args[0] || args.length !== 1) throw new Error("usage: quixos-qx source-digest ROOT");
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-source-digest-"));
@@ -37,18 +180,27 @@ const main = async () => {
process.stdout.write(`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, {acceptEdits}) : plan, null, 2)}\n`);
return;
}
if (command === "check-resource") {
const [root, kind, repository, commit, output, ...extra] = args;
if (!root || !output || !["package", "interface"].includes(kind) || extra.length) throw new Error("usage: quixos-qx check-resource ROOT package|interface REPOSITORY COMMIT OUTPUT");
const result = await checkResourceCandidate({root, kind: kind as "package" | "interface", source: {repository, commit}, output, publishedOnly: true});
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (result.blockers.length) process.exitCode = 1;
return;
}
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-refresh"].includes(command)) {
const [root, specFile, ...flags] = args;
let spec: ScaffoldRecipe;
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
const registry = JSON.parse(await readFile(path.join(root, "quixos.scaffold.json"), "utf8"));
spec = {source: await authorSource(root), name: specFile, id: `export:${registry.name}:${specFile}`};
const declaration = flags.indexOf("--declaration");
if (declaration >= 0) {
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
spec.declaration = await readFile(flags[declaration + 1], "utf8");
const parsed = parseQx(`package Draft id "package:draft" revision "package:draft@1" { ${spec.declaration} }`);
if (parsed.diagnostics.length) throw new Error(parsed.diagnostics.map(d => d.message).join("\n"));
const exported = [...walkSyntax(parsed.root)].filter(node => ["packageOperationExport", "packageFunctionExport", "packageConstructorExport"].includes(node.kind));
if (exported.length !== 1) throw new Error("--declaration must contain exactly one function, operation or constructor export");
const literal = exported[0].children.find(node => node.kind === "stringLiteral");
if (!literal) throw new Error("Declaration requires an authored export ID");
spec.id = JSON.parse(parsed.source.slice(literal.start, literal.end));
flags.splice(declaration, 2);
}
} else spec = await readSpec(specFile) as ScaffoldRecipe;
if (!root || !specFile || flags.some((flag) => !["--write", "--install"].includes(flag)) || (flags.includes("--install") && !flags.includes("--write"))) throw new Error("usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]");
const spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
const request = await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration" | "refresh", spec);
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
@@ -56,21 +208,27 @@ const main = async () => {
const cwd = path.resolve(root, spec.directory ?? "");
const toolchain = JSON.parse(await readFile(path.join(cwd, "quixos.toolchain.json"), "utf8"));
if (toolchain.generatedBy !== "qx-scaffold-v1" || typeof toolchain.nixifyPluginUrl !== "string") throw new Error("Missing scaffold toolchain");
for (const [executable, args] of [["corepack", ["yarn", "plugin", "import", toolchain.nixifyPluginUrl]], ["corepack", ["yarn", "config", "set", "generateDefaultNix", "false"]], ["corepack", ["yarn", "config", "set", "individualNixPackaging", "true"]], ["corepack", ["yarn", "install"]], ["corepack", ["yarn", "typecheck"]], ["nix", ["flake", "lock"]]] as const) {
for (const [executable, args] of [["corepack", ["yarn", "plugin", "import", toolchain.nixifyPluginUrl]], ["corepack", ["yarn", "config", "set", "generateDefaultNix", "false"]], ["corepack", ["yarn", "config", "set", "individualNixPackaging", "true"]], ["corepack", ["yarn", "install"]]] as const) {
const result = spawnSync(executable, [...args], {cwd, stdio: ["inherit", 2, 2]});
if (result.error || result.status !== 0) throw new Error(`Scaffold files retained; ${executable} ${args.join(" ")} failed: ${result.error?.message ?? result.status}`);
}
try {await readFile(path.join(cwd, "yarn-project.nix"));}
catch {throw new Error("Nixify did not generate yarn-project.nix. It skips repositories under the OS temporary directory; use an ordinary workspace checkout and retry installation.");}
await snapshotCommit(cwd);
const locked = spawnSync("nix", ["flake", "lock"], {cwd, stdio: ["inherit", 2, 2]});
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
}
process.stdout.write(`${JSON.stringify({...plan, applied}, null, 2)}\n`);
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied}, null, 2)}\n`);
return;
}
if (command === "scaffold-structure") {
const [root, spec, ...flags] = args;
if (!root || !spec || flags.some((flag) => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-structure ROOT SPEC_JSON [--write]");
const request = JSON.parse(await readFile(spec, "utf8")) as StructuralRequest;
const request = await readSpec(spec) as StructuralRequest;
if (request.kind !== "workspace" && !request.source) request.source = await authorSource(root);
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
process.stdout.write(`${JSON.stringify({...plan, applied}, null, 2)}\n`);
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied}, null, 2)}\n`);
return;
}
if (command === "scaffold-resume") {
@@ -79,19 +237,7 @@ const main = async () => {
process.stdout.write(`${JSON.stringify(await resumeStructure(root, id), null, 2)}\n`);
return;
}
if (command === "check") {
const [root, output, ...flags] = args;
if (!root || !output || flags.length % 2) throw new Error("usage: quixos-qx check ROOT OUTPUT [--snapshot-map FILE] [--baseline FILE] [--reviews FILE]");
const values = new Map<string, string>();
for (let index = 0; index < flags.length; index += 2) {
if (!["--snapshot-map", "--baseline", "--reviews"].includes(flags[index])) throw new Error(`Unknown check option ${flags[index]}`);
values.set(flags[index], flags[index + 1]);
}
const result = await checkWorkspaceCandidate({ root, output, snapshotMap: values.get("--snapshot-map"), baseline: values.get("--baseline"), reviews: values.get("--reviews") });
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (result.blockers.length) process.exitCode = 1;
return;
}
if (["check", "check-resource"].includes(command)) throw new Error("Use qx-workspace check in the registered repository; handwritten source/snapshot-map candidates are no longer an authoring check path");
if (command === "evolution") {
const [baseline, candidate, reviews, ...extra] = args;
if (!baseline || !candidate || extra.length) throw new Error("usage: quixos-qx evolution BASELINE_JSON CANDIDATE_JSON [REVIEWS_JSON]");
+29 -3
View File
@@ -20,13 +20,37 @@ const semantic = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(semantic);
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
.map(([key, entry]) => [key, semantic(entry)]));
// These are authored data/maps, not schema nodes. A user field literally
// named displayName or documentation is semantic and must stay in the hash.
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]));
return value;
};
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) => [...entries].sort((a, b) => compareText(key(a), key(b)));
export const conformanceIdentity = (entry: Conformance): string => entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
export type StorageContract = { id: string; ownerId: string; kind: "state" | "edge"; digest: string; definition: unknown };
/** Automatic evolution preserves values; it never interprets migration code or
* guesses that a new nominal message descriptor means the same representation. */
export const storageChangeRequiresMigration = (previous: StorageContract | undefined, next: StorageContract | undefined,
oldAtomIds: ReadonlySet<string>): boolean => {
if (!next) return true;
const after = next.definition as PersistentAttachment;
if (!previous) {
if (after.kind === "state") return oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional";
return after.endpoints.some(endpoint => endpoint.cardinality === "exactly-one" &&
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)));
}
if (previous.ownerId !== next.ownerId || previous.kind !== next.kind) return true;
const before = previous.definition as PersistentAttachment;
if (before.kind === "state" && after.kind === "state") {
// Capture materializes old defaults, so changing a default affects only
// newly constructed objects, not existing sparse state.
const {defaultValue: _beforeDefault, ...beforeStorage} = before;
const {defaultValue: _afterDefault, ...afterStorage} = after;
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
}
return canonicalJson(before) !== canonicalJson(after);
};
export const storageContracts = (workspace: WorkspaceRevision): StorageContract[] => {
const result: StorageContract[] = [];
const add = (attachment: PersistentAttachment, ownerId: string) => {
@@ -141,7 +165,8 @@ export type RuntimeAction = { groupId: string; action: "keep" | "start" | "repla
export type EvolutionReport = {
schemaVersion: 1; baselineDigest: string | null; candidateDigest: string; checkerVersion: string;
runtimeActions: RuntimeAction[];
storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; previous?: StorageContract; candidate?: StorageContract }>;
storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; requiresMigration: boolean; previous?: StorageContract; candidate?: StorageContract }>;
migrationRequired: string[];
reviews: Array<ReviewRequirement & { accepted: boolean }>;
packageChecks: Array<{ groupId: string; contractDigest: string }>;
blockers: string[];
@@ -175,6 +200,7 @@ export const planEvolution = (baseline: WorkspaceRevision | null, candidate: Wor
for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) {
const previous = beforeStorage.get(id), next = afterStorage.get(id);
if (previous?.digest !== next?.digest) storageChanges.push({ id, kind: !previous ? "add" : !next ? "remove" : "change",
requiresMigration: storageChangeRequiresMigration(previous, next, new Set(baseline?.atoms.map(atom => atom.id) ?? [])),
...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}) });
}
const providers = (workspace: WorkspaceRevision) => [
@@ -201,6 +227,6 @@ export const planEvolution = (baseline: WorkspaceRevision | null, candidate: Wor
}
}
return { schemaVersion: 1, baselineDigest: baseline ? contentDigest(baseline) : null, candidateDigest, checkerVersion,
runtimeActions, storageChanges, reviews, packageChecks: runtimeActions.filter((entry) => entry.candidate && entry.action !== "keep")
runtimeActions, storageChanges, migrationRequired: storageChanges.filter(entry => entry.requiresMigration).map(entry => entry.id), reviews, packageChecks: runtimeActions.filter((entry) => entry.candidate && entry.action !== "keep")
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })), blockers };
};
+1
View File
@@ -74,6 +74,7 @@ export type ValueType =
| { kind: "builtin"; name: "unit" | "watch-handle" }
| { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> }
| { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType };
+20 -4
View File
@@ -144,6 +144,8 @@ const typeLabel = (type: ValueType): string => {
return type.name;
case "message":
return `message:${type.descriptorId}`;
case "record":
return `record{${Object.keys(type.fields).sort().map((key) => `${key}:${typeLabel(type.fields[key])}`).join(";")}}`;
case "object-ref":
return type.expectation.kind === "atom"
? `object:atom:${type.expectation.atomId}`
@@ -165,6 +167,10 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
return left.name === (right as typeof left).name;
case "message":
return left.descriptorId === (right as typeof left).descriptorId;
case "record": {
const other = (right as typeof left).fields;
return Object.keys(left.fields).length === Object.keys(other).length && Object.entries(left.fields).every(([key, value]) => Object.hasOwn(other, key) && valueTypesEqual(value, other[key]));
}
case "object-ref": {
const other = (right as typeof left).expectation;
if (left.expectation.kind !== other.kind) {
@@ -220,6 +226,12 @@ const validateValueType = (
indexes: Pick<ValidationIndexes, "atoms" | "interfaces">,
) => {
switch (type.kind) {
case "record":
for (const [name, field] of Object.entries(type.fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) issue(issues, "invalid-value-type", path, "Invalid record field name");
validateValueType(issues, field, `${path}.fields.${name}`, indexes);
}
return;
case "builtin":
case "scalar":
return;
@@ -777,9 +789,9 @@ const validateAttachments = (
);
}
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
const containsReference = (type: ValueType): boolean => type.kind === "object-ref" || ((type.kind === "optional" || type.kind === "list") && containsReference(type.value));
if (containsReference(attachment.valueType) || (attachment.storagePolicy.kind === "crdt-document" && containsReference(attachment.storagePolicy.updateType))) {
issue(issues, "invalid-attachment", `${path}.valueType`, "Managed object references belong in graph relationships, not ordinary state");
const containsRpcType = (type: ValueType): boolean => type.kind === "object-ref" || type.kind === "record" || ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if (containsRpcType(attachment.valueType) || (attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType))) {
issue(issues, "invalid-attachment", `${path}.valueType`, "Managed object references belong in graph relationships, not ordinary state; record types are RPC-only");
}
if (attachment.storagePolicy.kind === "crdt-document") {
validateValueType(
@@ -1455,8 +1467,12 @@ const validateConformances = (
);
continue;
}
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`);
const projection = edgeProjection(attachment.attachment, binding.projectionId);
// An owned endpoint may explicitly export read-only traversal, including
// a native inverse relationship view. This never exports mutation rights.
if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) {
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`);
}
const relationshipMember = operationEntry.member.kind === "relationship"
? operationEntry.member
: undefined;
File diff suppressed because one or more lines are too long
+84
View File
@@ -0,0 +1,84 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { convergeAuthoring } from "../src/capability-language/authoring-converge.js";
import { loadQuixosLock } from "../src/resource-lock/index.js";
const execFile = promisify(callback);
test("source convergence propagates nested edits and unchanged snapshots reach a fixed point", async context => {
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-converge-test-"));
context.after(() => fs.rm(temporary, { recursive: true, force: true }));
const workbench = path.join(temporary, "workbench"), remotes = path.join(temporary, "remotes");
await fs.mkdir(path.join(workbench, ".quixos"), { recursive: true });
await fs.mkdir(remotes);
const origin = "https://convergence.example.test/";
const previous = Object.fromEntries(["GIT_CONFIG_COUNT", "GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"].map(key => [key, process.env[key]]));
process.env.GIT_CONFIG_COUNT = "1";
process.env.GIT_CONFIG_KEY_0 = `url.file://${remotes}/.insteadOf`;
process.env.GIT_CONFIG_VALUE_0 = origin;
context.after(() => { for (const [key, value] of Object.entries(previous)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } });
const resources = [];
let dependency = "";
for (const [directory, kind, name] of [["resources/Base", "interface", "Base"], ["resources/Consumer", "package", "Consumer"], ["root", "workspace", "Root"]]) {
const root = path.join(workbench, directory);
await fs.mkdir(root, { recursive: true });
await execFile("jj", ["git", "init", "--colocate", root]);
await execFile("git", ["init", "--bare", path.join(remotes, name)]);
const repository = `${origin}${name}`;
await execFile("git", ["-C", root, "remote", "add", "origin", repository]);
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } ${dependency} }`);
await fs.writeFile(path.join(root, `${kind}.qx`), "draft");
await execFile("jj", ["status"], { cwd: root });
const commit = (await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root })).stdout.trim();
if (kind !== "workspace") resources.push({ directory, kind, resourceId: `${kind}:${name}`, source: { resolver: "git", repository, commit } });
dependency = `${kind} ${name} source { repository "${repository}"; commit "${commit}"; }`;
}
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({ resources }));
const first = await convergeAuthoring(workbench);
assert.deepEqual(first.worklist, []);
assert.equal(first.converged, true);
assert.equal(first.verificationEvidence, false, "source retention never approves even syntactically invalid code");
await fs.writeFile(path.join(workbench, "resources/Base/interface.qx"), "edited draft");
const second = await convergeAuthoring(workbench);
assert.deepEqual(second.worklist, []);
assert.notEqual(second.candidate?.commit, first.candidate?.commit);
const consumer = await loadQuixosLock(path.join(workbench, "resources/Consumer/quixos.lock"));
assert.ok(consumer.ok);
assert.equal(consumer.lock.resources[0].source.commit, second.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
const third = await convergeAuthoring(workbench);
assert.deepEqual(third, second);
const base = path.join(workbench, "resources/Base");
const consumerRoot = path.join(workbench, "resources/Consumer");
const goodLock = await fs.readFile(path.join(consumerRoot, "quixos.lock"), "utf8");
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), "broken draft");
const scoped = await convergeAuthoring(workbench, "resources/Base");
assert.deepEqual(scoped.worklist, [], "an unrelated malformed consumer cannot block a provider check");
await fs.writeFile(path.join(base, "interface.qx"), "another edit");
const partial = await convergeAuthoring(workbench);
assert.equal(partial.converged, false);
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
assert.equal(graph.resources[0].source.commit, partial.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), goodLock);
const resumed = await convergeAuthoring(workbench);
assert.deepEqual(resumed.worklist, []);
assert.deepEqual(await convergeAuthoring(workbench), resumed);
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Wrong"]);
const mismatch = await convergeAuthoring(workbench);
assert.ok(mismatch.worklist.some(entry => entry.phase === "source" && /Origin differs/.test(entry.message)));
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Base"]);
await fs.rename(path.join(remotes, "Base"), path.join(remotes, "Base-offline"));
const offline = await convergeAuthoring(workbench);
assert.equal(offline.candidate, null);
assert.ok(offline.worklist.some(entry => entry.phase === "publication"));
await fs.rename(path.join(remotes, "Base-offline"), path.join(remotes, "Base"));
assert.equal((await convergeAuthoring(workbench)).converged, true);
const consumerSource = resumed.retained.find(entry => entry.directory === "resources/Consumer")!.source;
await fs.writeFile(path.join(base, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } package Consumer source { repository "${consumerSource.repository}"; commit "${consumerSource.commit}"; } }`);
const cycle = await convergeAuthoring(workbench);
assert.equal(cycle.candidate, null);
assert.ok(cycle.worklist.some(entry => /Source dependency cycle/.test(entry.message)));
});
+37
View File
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, writeFile, rm, symlink } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { inspectAuthoringRepository } from "../src/capability-language/authoring-inspect.js";
const execFile = promisify(callback);
test("inspection keeps current files and labels historical recovery without granting verification", async context => {
const root = await mkdtemp(path.join(os.tmpdir(), "qx-inspection-test-"));
context.after(() => rm(root, { recursive: true, force: true }));
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
await git("init");
await writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
await git("add", ".");
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
await writeFile(path.join(root, "interface.qx"), "interface broken {{{");
await writeFile(path.join(root, "new.qx"), 'interface New id "interface:new" revision "interface:new@1" {}');
const inspected = await inspectAuthoringRepository(root);
assert.equal(inspected.verificationEvidence, false);
assert.equal(inspected.resolutionChecked, false);
assert.equal(inspected.files[0].status, "historical");
assert.equal(inspected.files[0].revision, commit);
assert.ok(inspected.files[0].currentErrors.length);
assert.match(inspected.files[0].declarations[0].source, /Example/);
assert.equal(inspected.files[1].status, "current");
assert.match(inspected.files[1].declarations[0].source, /New/);
assert.equal((await git("rev-parse", "HEAD")).stdout.trim(), commit);
await symlink(path.join(root, "interface.qx"), path.join(root, "linked.qx"));
const linked = (await inspectAuthoringRepository(root)).files.find(file => file.file === "linked.qx")!;
assert.equal(linked.status, "unavailable");
assert.deepEqual(linked.declarations, []);
assert.match(JSON.stringify(linked.currentErrors), /ordinary files/);
});
+45
View File
@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {execFile as callback} from "node:child_process";
import {promisify} from "node:util";
import {authoringWorklist} from "../src/capability-language/authoring-worklist.js";
import {checkRecordName} from "../src/capability-language/authoring-check.js";
import {checkerIdentity, snapshotCommit} from "../src/capability-language/checked-build.js";
const execFile = promisify(callback);
test("worklist grows and clears from current source, dependency and checker observations", async context => {
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-worklist-test-"));
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
const root = path.join(workbench, "root"), provider = path.join(workbench, "resources/Base");
await fs.mkdir(root); await fs.mkdir(provider, {recursive: true});
await fs.mkdir(path.join(workbench, ".quixos/checks"), {recursive: true});
const header = `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; }`;
for (const directory of [root, provider]) await execFile("jj", ["git", "init", "--colocate", directory]);
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
await fs.writeFile(path.join(provider, "quixos.lock"), `${header} }`);
const baseCommit = await snapshotCommit(provider);
await fs.writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { import interface Base; }`);
await fs.writeFile(path.join(root, "quixos.lock"), `${header} interface Base source { repository "https://example.test/base"; commit "${baseCommit}"; } }`);
const rootCommit = await snapshotCommit(root);
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [
{kind: "interface", directory: provider, source: {resolver: "git", repository: "https://example.test/base", commit: baseCommit}},
]}));
assert.equal((await authoringWorklist(workbench)).worklist.filter(entry => entry.phase === "unchecked").length, 2);
const remember = (directory: string, commit: string, checker = checkerIdentity()) => fs.writeFile(
path.join(workbench, ".quixos/checks", checkRecordName(directory)), JSON.stringify({commit, checker, phase: "checked", blockers: []}));
await remember("root", rootCommit); await remember("resources/Base", baseCommit);
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
await fs.writeFile(path.join(provider, "interface.qx"), "interface broken {{{");
const broken = await authoringWorklist(workbench);
assert.ok(broken.worklist.some(entry => entry.directory === "resources/Base" && entry.phase === "syntax" && /historical/.test(entry.message)));
assert.ok(broken.worklist.some(entry => entry.directory === "root" && entry.phase === "dependency"));
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
await remember("resources/Base", baseCommit, "old-checker");
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => /checker changed/.test(entry.message)));
await fs.writeFile(path.join(workbench, ".quixos/checks", checkRecordName("resources/Base")), JSON.stringify({phase: "publication", blockers: ["source retention unavailable"]}));
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => entry.phase === "publication" && /source retention/.test(entry.message)));
});
+1 -1
View File
@@ -37,7 +37,7 @@ test("candidate check produces explicitly non-activation evidence and never over
context.after(() => fs.rm(directory, { recursive: true, force: true }));
const root = path.join(directory, "source"), output = path.join(directory, "check");
await fs.mkdir(root);
await execFile("git", ["-C", root, "init"]);
await execFile("jj", ["git", "init", "--colocate", root]);
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; } }`);
await fs.writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { atom Subject id "atom:subject"; }`);
const result = await checkWorkspaceCandidate({root, output});
+11
View File
@@ -13,6 +13,17 @@ import {
fixtureId,
} from "./fixtures/capability-model.js";
test("RPC record fields retain declared reference targets and reject duplicate fields", () => {
const compile = (fields: string) => compileCapabilityResourceSource(`
external atom Board id "atom:board";
package Runtime id "package:runtime" revision "package:runtime@1" {
function move id "export:move" : record { ${fields} } -> unit;
}`, {source: {repository: "https://example.test/runtime.git", commit: "a".repeat(40)}});
const result = compile("board: atom-ref<Board>; position: record { x: double; y: double; }; note: optional<string>;");
assert.equal(result.ok, true, JSON.stringify(result.diagnostics));
assert.equal(compile("board: atom-ref<Board>; board: string;").ok, false);
});
const compileWebStudioFixture = (packageTransform = (source: string) => source) => {
const fixture = (name: string) => readFileSync(
resolve(process.cwd(), "test/fixtures", name),
+17
View File
@@ -208,6 +208,23 @@ test("related-object dependency views cannot traverse another conformance's priv
assert.equal(validateWorkspaceRevision(workspace).some((entry) => entry.code === "private-attachment-access"), false, "Only an explicitly exported read-only traversal crosses ownership");
});
test("public traversal permits a native inverse read without exporting mutation authority", () => {
const workspace = makeValidCapabilityWorkspace();
const owned = conformance(workspace, fixtureId.projectOwnedConformance);
const index = owned.privateAttachments.findIndex(entry => entry.kind === "edge" && entry.id === fixtureId.projectOwner);
const edge = owned.privateAttachments.splice(index, 1)[0];
assert.ok(edge?.kind === "edge");
conformance(workspace, fixtureId.projectNamedConformance).privateAttachments.push(edge);
expectIssue(workspace, "private-attachment-access");
edge.endpoints.find(endpoint => endpoint.projectionId === fixtureId.projectOwnerProjection)!.publicTraversal = true;
const readPath = `conformances[${workspace.conformances.indexOf(owned)}].operationBindings[0]`;
assert.equal(validateWorkspaceRevision(workspace).some(issue => issue.code === "private-attachment-access" && issue.path.startsWith(readPath)), false);
const binding = owned.operationBindings[0].binding;
assert.ok(binding.kind === "edge");
binding.primitive = "connect";
expectIssue(workspace, "private-attachment-access");
});
test("native state and edge providers must match operation shape", () => {
const stateWorkspace = makeValidCapabilityWorkspace();
const state = conformance(
+1
View File
@@ -68,6 +68,7 @@ test("storage defaults and ownership changes invalidate consumers without requir
if (slot.kind === "state") slot.defaultValue = "Different default";
const report = planEvolution(before, after, { allowLegacy: true });
assert.equal(report.storageChanges.length, 1);
assert.deepEqual(report.migrationRequired, []);
assert.equal(report.runtimeActions[0]!.action, "replace");
assert.deepEqual(report.reviews, []);
assert.notDeepEqual(storageContracts(before), storageContracts(after));
+34
View File
@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";
import {mkdtemp, rm} from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import {spawn} from "node:child_process";
import {once} from "node:events";
import {withFileLock} from "../src/capability-language/file-lock.js";
test("authoring lock excludes concurrent mutations and survives owner death", async context => {
const root = await mkdtemp(path.join(os.tmpdir(), "qx-lock-test-"));
context.after(() => rm(root, {recursive: true, force: true}));
const filename = path.join(root, "lock");
const events: string[] = [];
let queued: Promise<void>;
await withFileLock(filename, async () => {
queued = withFileLock(filename, async () => {events.push("second");});
events.push("first");
});
await queued!;
assert.deepEqual(events, ["first", "second"]);
const module = new URL("../src/capability-language/file-lock.js", import.meta.url).href;
const owner = spawn(process.execPath, ["--input-type=module", "-e",
`import {withFileLock} from ${JSON.stringify(module)}; await withFileLock(${JSON.stringify(filename)}, async () => {process.stdout.write('ready'); await new Promise(() => {});});`],
{stdio: ["ignore", "pipe", "pipe"]});
context.after(() => owner.kill("SIGKILL"));
await once(owner.stdout, "data");
const exited = once(owner, "exit");
owner.kill("SIGKILL");
await exited;
let acquired = false;
await withFileLock(filename, async () => {acquired = true;});
assert.equal(acquired, true);
});
+37
View File
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { createGitCapabilityResolver } from "../src/capability-language/git-resolver.js";
const execFile = promisify(callback);
test("dependency resolution is reusable across processes, concurrent and rejects modified checkouts", async context => {
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-resolver-test-"));
context.after(() => rm(temporary, { recursive: true, force: true }));
const origin = path.join(temporary, "origin");
await mkdir(origin);
const git = (...args: string[]) => execFile("git", ["-C", origin, ...args]);
await git("init");
await writeFile(path.join(origin, "interface.qx"), "contract");
await git("add", ".");
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
await git("tag", `quixos-reachability/${commit}`);
const source = { resolver: "git" as const, repository: origin, commit };
const options = { checkoutRoot: path.join(temporary, "cache") };
const a = await createGitCapabilityResolver(options);
const b = await createGitCapabilityResolver(options);
const [first, second] = await Promise.all([a(source, "interface"), b(source, "interface")]);
assert.equal(first.directory, second.directory);
const c = await createGitCapabilityResolver(options);
assert.equal((await c(source, "interface")).directory, first.directory);
await writeFile(path.join(first.directory, "interface.qx"), "changed");
const d = await createGitCapabilityResolver(options);
await assert.rejects(d(source, "interface"), /modified/);
// A failed request is not memoized forever; repair can be observed on retry.
await writeFile(path.join(first.directory, "interface.qx"), "contract");
assert.equal((await d(source, "interface")).directory, first.directory);
});
+34
View File
@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
const execFile = promisify(callback);
test("Nix checks a retained immutable source without a local overlay and reuses the result", {skip: !process.env.QX_CHECK_GENERATOR}, async context => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-nix-candidate-test-"));
context.after(() => fs.rm(root, {recursive: true, force: true}));
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
await git("init");
await fs.writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } }`);
await git("add", ".");
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
await git("tag", `quixos-reachability/${commit}`);
const generator = process.env.QX_CHECK_GENERATOR!;
const repository = "https://immutable-candidate.example.test/contract.git";
const env = {...process.env, GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${root}.insteadOf`, GIT_CONFIG_VALUE_0: repository};
const build = async () => (await execFile("nix", ["build", "--impure", "--file", path.join(generator, "share/checked-candidate.nix"),
"--argstr", "repository", repository, "--argstr", "commit", commit,
"--argstr", "kind", "interface", "--argstr", "generator", generator,
"--option", "substitute", "false", "--no-link", "--print-out-paths"], {env, maxBuffer: 4 * 1024 * 1024})).stdout.trim();
const output = await build();
const candidate = JSON.parse(await fs.readFile(path.join(output, "candidate.json"), "utf8"));
assert.equal(candidate.revision.source.commit, commit);
await fs.writeFile(path.join(root, "interface.qx"), "broken draft");
assert.equal(await build(), output);
assert.deepEqual(JSON.parse(await fs.readFile(path.join(output, "checks.json"), "utf8")), []);
});
+43
View File
@@ -8,6 +8,49 @@ import {execFile as callback} from "node:child_process";
import {planPinUpgrades, applyPinUpgrades, type UpgradeEffects} from "../src/capability-language/pin-upgrades.js";
const execFile = promisify(callback);
test("real jj snapshots and immutable Git publication propagate a changed interface into the root", {skip: !process.env.QX_CHECK_GENERATOR}, async (context) => {
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-real-upgrade-"));
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
// Exercise the actual effects with local bare remotes, without external writes.
const environment = {
GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${workbench}/remotes/.insteadOf`,
GIT_CONFIG_VALUE_0: "https://upgrade.test/", QUIXOS_JJ_NO_CHECKPOINT: "1",
QUIXOS_CHECK_GENERATOR: process.env.QX_CHECK_GENERATOR!,
};
const previous = Object.fromEntries(Object.keys(environment).map(key => [key, process.env[key]]));
Object.assign(process.env, environment);
context.after(() => {for (const [key, value] of Object.entries(previous)) if (value === undefined) delete process.env[key]; else process.env[key] = value;});
const run = async (cwd: string, command: string, args: string[]) => (await execFile(command, args, {cwd})).stdout.trim();
await fs.mkdir(path.join(workbench, "remotes"));
const nodes = [];
for (const [kind, directory, remote] of [["interface", "resources/Named", "named.git"], ["workspace", "root", "workspace.git"]] as const) {
const root = path.join(workbench, directory);
await fs.mkdir(root, {recursive: true});
await run(workbench, "git", ["init", "--bare", path.join(workbench, "remotes", remote)]);
await run(root, "jj", ["git", "init", "--colocate"]);
await run(root, "git", ["remote", "add", "origin", `https://upgrade.test/${remote}`]);
await fs.writeFile(path.join(root, ".gitignore"), ".quixos/\n");
const child = nodes[0];
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://upgrade.test/quixos.git"; commit "${"a".repeat(40)}"; } ${child ? `interface Named source { repository "${child.source.repository}"; commit "${child.source.commit}"; }` : ""} }`);
await fs.writeFile(path.join(root, `${kind}.qx`), kind === "interface" ? 'interface Named id "interface:named" revision "interface:named@1" {}' : `workspace W id "workspace:w" revision "workspace:w@1" commit "${"a".repeat(40)}" { import interface Named; atom A id "atom:a"; }`);
await run(root, "jj", ["describe", "-m", "Initial source"]);
const commit = await run(root, "jj", ["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
await run(root, "git", ["push", "origin", `${commit}:refs/tags/quixos-reachability/${commit}`]);
nodes.push({kind, directory, source: {repository: `https://upgrade.test/${remote}`, commit}});
}
await fs.mkdir(path.join(workbench, ".quixos"));
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [nodes[0]]}));
await fs.appendFile(path.join(workbench, nodes[0].directory, "interface.qx"), "\n// incremental author edit\n");
const plan = await planPinUpgrades(workbench, {nodes, bootstrap: true});
const result = await applyPinUpgrades(plan);
assert.equal(result.activated, false);
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
const commit = graph.resources[0].source.commit;
assert.notEqual(commit, nodes[0].source.commit);
assert.match(await fs.readFile(path.join(workbench, "root/quixos.lock"), "utf8"), new RegExp(commit));
assert.match(await run(workbench, "git", ["--git-dir", path.join(workbench, "remotes/named.git"), "show-ref"]), new RegExp(commit));
});
test("pin upgrades publish children before parent locks and resume without republishing completed nodes", async (context) => {
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-test-"));
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
+23 -2
View File
@@ -10,6 +10,17 @@ import {planStructure, applyStructure} from "../src/capability-language/structur
import {contentDigest} from "../src/capability-model/evolution.js";
const execFile = promisify(callback);
test("React preset applies its browser build script and shared-platform imports", async (context) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-react-recipe-"));
context.after(() => fs.rm(root, {recursive: true, force: true}));
await execFile("git", ["-C", root, "init"]);
const source = {repository: "https://example.test/react.git", commit: "a".repeat(40)};
const request = await scaffoldRecipe(root, "package", {source, name: "React", id: "package:react", revision: "package:react@1", template: "typescript-react", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
await applyStructure(await planStructure(root, request));
assert.match(await fs.readFile(path.join(root, "scripts/build-component.mjs"), "utf8"), /__quixos\/platform\/react/);
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
});
test("package/function/migration scaffolds register implementations and refresh code digests without overwriting code", async (context) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-recipes-"));
context.after(() => fs.rm(root, {recursive: true, force: true}));
@@ -21,7 +32,7 @@ test("package/function/migration scaffolds register implementations and refresh
await apply("package", {...base, name: "Chess", id: "package:chess", revision: "package:chess@1", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
await apply("function", {...base, name: "play", id: "export:play"});
const filename = path.join(root, base.directory, "src/impl/play.ts");
const edited = 'import type {Implementation} from "../generated-bindings.js";\nexport const handler: Implementation["play"] = async () => null;\n';
const edited = 'import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["play"] = async () => null;\n';
await fs.writeFile(filename, edited);
const old = {version: 1}, next = {version: 2}, from = contentDigest(old), to = contentDigest(next);
await apply("migration", {...base, name: "upgrade", id: "export:upgrade", migration: {id: "upgrade-v2", scopeId: "board", from, to, predecessors: [], ports: [], contracts: {[from]: old, [to]: next}}});
@@ -31,7 +42,7 @@ test("package/function/migration scaffolds register implementations and refresh
assert.equal(await fs.readFile(filename, "utf8"), edited);
const catalog = JSON.parse(await fs.readFile(path.join(root, base.directory, "quixos.migrations.json"), "utf8"));
assert.equal(catalog.migrations[0].implementation.digest, contentDigest(await fs.readFile(migrationFile, "utf8")));
const bindings = await fs.readFile(path.join(root, base.directory, "src/generated-bindings.ts"), "utf8");
const bindings = await fs.readFile(path.join(root, base.directory, "src/gen/qx.ts"), "utf8");
assert.match(bindings, /export:play/);
assert.match(await fs.readFile(path.join(root, base.directory, "src/migrate.ts"), "utf8"), /export:upgrade/);
if (process.env.QX_SCAFFOLD_TEST_SDK) {
@@ -44,4 +55,14 @@ test("package/function/migration scaffolds register implementations and refresh
await execFile("nix-instantiate", ["--parse", path.join(packageRoot, "flake.nix")]);
}
await assert.rejects(() => apply("function", {...base, name: "play", id: "export:play"}), /unique/);
const declarations = path.join(root, base.directory, "package.qx");
await fs.writeFile(declarations, (await fs.readFile(declarations, "utf8")).replace(/}\s*$/, ' function authored id "export:authored" : unit -> unit;\n}\n'));
await apply("refresh", base);
assert.match(await fs.readFile(path.join(root, base.directory, "src/server.ts"), "utf8"), /"authored":/);
assert.match(await fs.readFile(path.join(root, base.directory, "src/impl/authored.ts"), "utf8"), /Implement authored/);
assert.equal(await fs.readFile(filename, "utf8"), edited);
await assert.rejects(() => planStructure(root, {kind: "package", source, resourceRoot: base.directory, validation: "syntax", files: [{
file: `${base.directory}/package.qx`, edits: [{operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"},
source: 'package Other id "package:other" revision "package:other@1" {}'}],
}]}), /scaffold-owned package identity/);
});
+15 -1
View File
@@ -1,6 +1,17 @@
import test from "node:test";
import assert from "node:assert/strict";
import { editStructure, scaffoldResourceSource } from "../src/capability-language/structural-edits.js";
import { editStructure, scaffoldResourceSource, instantiateWorkspaceIdentity } from "../src/capability-language/structural-edits.js";
test("template identity binding changes only the workspace header and is idempotent", () => {
const source = '// workspace fake id "do-not-touch"\nworkspace Todo id "workspace:todo" revision "workspace:todo@1" commit "' + "a".repeat(40) + '" { atom Task id "atom:task"; }';
const id = "00000000-0000-0000-0000-000000000123";
const bound = instantiateWorkspaceIdentity(source, id);
assert.match(bound, /atom Task id "atom:task"/);
assert.ok(bound.startsWith('// workspace fake id "do-not-touch"'));
assert.ok(bound.includes(`workspace Todo id "${id}"`));
assert.equal(instantiateWorkspaceIdentity(bound, id), bound);
assert.throws(() => instantiateWorkspaceIdentity(source, "not-a-workspace"), /UUID/);
});
test("package scaffolding and function edits preserve surrounding source and use exact selectors", () => {
const source = `// 🧭 resource comment\n${scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@1")}`;
@@ -13,6 +24,9 @@ test("package scaffolding and function edits preserve surrounding source and use
assert.ok(!editStructure(replaced, {operation: "remove", target: {kind: "packageFunctionExport", id: "export:evaluate"}}).includes("evaluate"));
assert.throws(() => editStructure(source, {operation: "remove", target: {kind: "packageResourceDecl", id: "package:chess@1"}}), /exactly once/);
assert.throws(() => editStructure(source, {operation: "append", parent: {kind: "packageResourceDecl"}, source: "not valid QX"}), /Invalid structural/);
const prefixed = `import interface Board;\nexternal atom Game id "atom:game";\n${source}`;
const replacedPackage = editStructure(prefixed, {operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"}, source: scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@2")});
assert.ok(replacedPackage.startsWith('import interface Board;\nexternal atom Game id "atom:game";'));
});
test("dependency scaffolding validates exact sources and preserves unrelated lock comments", () => {
+9
View File
@@ -34,4 +34,13 @@ test("structural plans validate the graph, journal originals, and reject stale e
await fs.writeFile(applied.journalPath, JSON.stringify(journal));
assert.equal((await resumeStructure(root, applied.id)).phase, "complete");
await assert.rejects(() => planStructure(root, request), /Duplicate|duplicate/);
// Cross-repository edits may temporarily refer to an unfinished provider.
const provisional: StructuralRequest = {kind: "workspace", validation: "syntax", files: [{file: "workspace.qx", edits: [
{operation: "import", kind: "interface", name: "NotImplementedYet"},
]}]};
const draft = await planStructure(root, provisional);
assert.equal(draft.validation, "syntax");
await applyStructure(draft);
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
await assert.rejects(() => planStructure(root, {...provisional, validation: "resource-graph"}));
});