Add exact recursive source checkout, independent Git/jj developer workbenches and source-only backup/recovery commands, with Workspace instance binding outside source. Qualify two empty Reader Graphs, packaged source commands, Agent VM compilation, exact Gitea read grants and reconciled Reader settings/annotation stability. Publish checked Reader Package/root revisions to canonical review branches and retention tags, retain all 57 repositories independently in private versioned storage, verify downloaded-backup recovery and advance the example pin. Add bounded Central pin acquisition and selection-specific read-only identities, qualified against all 57 canonical repositories without write authority. Replace regex fork dependency discovery with compiler-parsed lock imports and rewrite proposals; preserve source identities and unrelated files, passing instance binding through packed and ordinary host checks. Hosted run-pin versus authoring preparation and maintained duplicate removal remain pending. Live source selections, Template defaults and default source branches are unchanged.
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { lstat, readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { parseQuixosLockDocument, type QuixosLockDiagnostic, type QuixosLockParseResult } from "./parser.js";
|
|
import type { LockedResource, QuixosLockDocument, QuixosRepositoryLock } from "./types.js";
|
|
|
|
export type QuixosLockSourceReader = (relativePath: string) => Promise<string>;
|
|
|
|
const diagnostic = (code: string, message: string, fileName: string, path?: string): QuixosLockDiagnostic => ({
|
|
phase: "resolution",
|
|
code,
|
|
message,
|
|
fileName,
|
|
line: 1,
|
|
column: 0,
|
|
path,
|
|
});
|
|
|
|
export const resolveQuixosLock = async (
|
|
rootSource: string,
|
|
readSource: QuixosLockSourceReader,
|
|
rootFileName = "quixos.lock",
|
|
): Promise<QuixosLockParseResult> => {
|
|
const root = parseQuixosLockDocument(rootSource, rootFileName);
|
|
if (!root.ok) return root;
|
|
if (root.document.kind !== "root") {
|
|
return {
|
|
ok: false,
|
|
diagnostics: [
|
|
diagnostic("expected-root-lock", "The entrypoint must be a root Quixos lock, not a fragment", rootFileName),
|
|
],
|
|
};
|
|
}
|
|
|
|
const diagnostics: QuixosLockDiagnostic[] = [];
|
|
const resources: LockedResource[] = [];
|
|
const sourceFiles = [rootFileName];
|
|
const visited = new Set<string>();
|
|
const active: string[] = [];
|
|
|
|
const addResources = (document: QuixosLockDocument, fileName: string) => {
|
|
for (const resource of document.resources) {
|
|
const duplicate = resources.find((entry) => entry.kind === resource.kind && entry.binding === resource.binding);
|
|
if (duplicate) {
|
|
diagnostics.push(
|
|
diagnostic(
|
|
"duplicate-resource-binding",
|
|
`Duplicate ${resource.kind} binding ${resource.binding} across imported lock files`,
|
|
fileName,
|
|
`${resource.kind}.${resource.binding}`,
|
|
),
|
|
);
|
|
} else {
|
|
resources.push(resource);
|
|
}
|
|
}
|
|
};
|
|
|
|
const visit = async (importPath: string) => {
|
|
if (active.includes(importPath)) {
|
|
diagnostics.push(
|
|
diagnostic("import-cycle", `Lock import cycle: ${[...active, importPath].join(" -> ")}`, importPath),
|
|
);
|
|
return;
|
|
}
|
|
if (visited.has(importPath)) return;
|
|
active.push(importPath);
|
|
let source: string;
|
|
try {
|
|
source = await readSource(importPath);
|
|
} catch (cause) {
|
|
diagnostics.push(
|
|
diagnostic(
|
|
"import-read-failed",
|
|
`Could not read lock import ${importPath}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
importPath,
|
|
),
|
|
);
|
|
active.pop();
|
|
return;
|
|
}
|
|
const parsed = parseQuixosLockDocument(source, importPath);
|
|
if (!parsed.ok) {
|
|
diagnostics.push(...parsed.diagnostics);
|
|
active.pop();
|
|
return;
|
|
}
|
|
if (parsed.document.kind !== "fragment") {
|
|
diagnostics.push(
|
|
diagnostic(
|
|
"imported-root-lock",
|
|
`Imported file ${importPath} must begin with "quixos-lock fragment"`,
|
|
importPath,
|
|
),
|
|
);
|
|
active.pop();
|
|
return;
|
|
}
|
|
visited.add(importPath);
|
|
sourceFiles.push(importPath);
|
|
addResources(parsed.document, importPath);
|
|
for (const nested of parsed.document.imports) await visit(nested);
|
|
active.pop();
|
|
};
|
|
|
|
addResources(root.document, rootFileName);
|
|
for (const importPath of root.document.imports) await visit(importPath);
|
|
if (diagnostics.length) return { ok: false, diagnostics };
|
|
const lock: QuixosRepositoryLock = {
|
|
formatVersion: 1,
|
|
quixos: root.document.quixos,
|
|
resources,
|
|
sourceFiles,
|
|
};
|
|
return { ok: true, lock, diagnostics: [] };
|
|
};
|
|
|
|
export const loadQuixosLock = async (fileName: string): Promise<QuixosLockParseResult> => {
|
|
const absoluteRoot = path.resolve(fileName);
|
|
const repositoryRoot = path.dirname(absoluteRoot);
|
|
const rootName = path.basename(absoluteRoot);
|
|
const metadata = await lstat(absoluteRoot);
|
|
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
throw new Error("root locks must be ordinary files, not symbolic links");
|
|
const rootSource = await readFile(absoluteRoot, "utf8");
|
|
return await resolveQuixosLock(
|
|
rootSource,
|
|
async (relativePath) => {
|
|
let absoluteImport = repositoryRoot;
|
|
const segments = relativePath.split("/");
|
|
for (const [index, segment] of segments.entries()) {
|
|
absoluteImport = path.join(absoluteImport, segment);
|
|
const metadata = await lstat(absoluteImport);
|
|
if (metadata.isSymbolicLink()) {
|
|
throw new Error("imports must not traverse symbolic links");
|
|
}
|
|
const final = index === segments.length - 1;
|
|
if ((!final && !metadata.isDirectory()) || (final && !metadata.isFile())) {
|
|
throw new Error("imports must be ordinary files beneath ordinary directories");
|
|
}
|
|
}
|
|
return await readFile(absoluteImport, "utf8");
|
|
},
|
|
rootName,
|
|
);
|
|
};
|