Build workspace agent, capability graph, and versioned cutovers

This commit is contained in:
2026-09-05 12:33:52 -07:00
parent c4d42a0ac5
commit f5ced64533
53 changed files with 5527 additions and 2520 deletions
+245 -9
View File
@@ -10,16 +10,19 @@ import {
import { QuixosLockLexer } from "./generated/QuixosLockLexer.js";
import {
QuixosLockParser,
type QuixosSourceBlockContext,
type SourceBlockContext,
} from "./generated/QuixosLockParser.js";
import type {
GitSource,
LockedResource,
QuixosLockDocument,
QuixosRepositoryLock,
QuixosSource,
} from "./types.js";
export type QuixosLockDiagnostic = {
phase: "syntax" | "validation";
phase: "syntax" | "validation" | "resolution";
code: string;
message: string;
fileName: string;
@@ -32,6 +35,10 @@ export type QuixosLockParseResult =
| { ok: true; lock: QuixosRepositoryLock; diagnostics: [] }
| { ok: false; diagnostics: QuixosLockDiagnostic[] };
export type QuixosLockDocumentParseResult =
| { ok: true; document: QuixosLockDocument; diagnostics: [] }
| { ok: false; diagnostics: QuixosLockDiagnostic[] };
class SyntaxErrorListener extends BaseErrorListener {
constructor(
private readonly fileName: string,
@@ -68,22 +75,72 @@ const lowerSource = (context: SourceBlockContext): GitSource => ({
commit: stringValue(context.stringLiteral(1)!).toLowerCase(),
});
const lowerQuixosSource = (context: QuixosSourceBlockContext): QuixosSource => {
const source = {
resolver: "git" as const,
repository: stringValue(context.stringLiteral(0)!),
commit: stringValue(context.stringLiteral(context.stringLiteral().length - 1)!).toLowerCase(),
};
const policyContext = context.sourcePolicy();
if (!policyContext) return source;
const policy = policyContext.PINNED()
? "pinned"
: policyContext.TRACK_RELEASE()
? "track-release"
: "track-development";
return {
...source,
policy,
ref: stringValue(context.stringLiteral(1)!),
};
};
const issue = (
diagnostics: QuixosLockDiagnostic[],
fileName: string,
code: string,
message: string,
path?: string,
line = 1,
column = 0,
) => diagnostics.push({
phase: "validation",
code,
message,
fileName,
line: 1,
column: 0,
line,
column,
path,
});
const validateImport = (
importPath: string,
index: number,
fileName: string,
diagnostics: QuixosLockDiagnostic[],
line: number,
column: number,
) => {
const path = `imports[${index}]`;
if (
!importPath
|| importPath.startsWith("/")
|| importPath.includes("\\")
|| importPath.split("/").some((segment) => !segment || segment === "." || segment === "..")
|| /^[A-Za-z][A-Za-z0-9+.-]*:/.test(importPath)
) {
issue(
diagnostics,
fileName,
"invalid-import-path",
`${path} must be a normalized relative path beneath the workspace repository root`,
path,
line,
column,
);
}
};
const validateSource = (
source: GitSource,
path: string,
@@ -141,10 +198,59 @@ const validateSource = (
}
};
export const parseQuixosLock = (
const validateQuixosSource = (
source: QuixosSource,
fileName: string,
diagnostics: QuixosLockDiagnostic[],
) => {
validateSource(source, "quixos", fileName, diagnostics);
if (!source.policy) return;
const forbiddenRefCharacters = new Set("~^:?*[\\");
const invalidRef = !source.ref
|| [...source.ref].some((character) => {
const code = character.charCodeAt(0);
return code <= 0x20 || code === 0x7f || forbiddenRefCharacters.has(character);
})
|| source.ref.startsWith("/")
|| source.ref.endsWith("/")
|| source.ref.endsWith(".")
|| source.ref.includes("..")
|| source.ref.includes("@{")
|| source.ref.includes("//");
if (invalidRef) {
issue(
diagnostics,
fileName,
"invalid-quixos-ref",
"quixos.ref must be a non-empty Git ref without whitespace or Git ref metacharacters",
"quixos.ref",
);
}
if (source.policy === "pinned") {
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(source.ref)) {
issue(
diagnostics,
fileName,
"pinned-quixos-ref-not-commit",
"A pinned Quixos source must use an exact commit as its ref",
"quixos.ref",
);
} else if (source.ref.toLowerCase() !== source.commit) {
issue(
diagnostics,
fileName,
"pinned-quixos-commit-mismatch",
"A pinned Quixos source ref and resolved commit must be identical",
"quixos.commit",
);
}
}
};
export const parseQuixosLockDocument = (
source: string,
fileName = "<memory>",
): QuixosLockParseResult => {
): QuixosLockDocumentParseResult => {
const diagnostics: QuixosLockDiagnostic[] = [];
const listener = new SyntaxErrorListener(fileName, diagnostics);
const lexer = new QuixosLockLexer(CharStream.fromString(source));
@@ -167,8 +273,55 @@ export const parseQuixosLock = (
);
}
const quixos = lowerSource(tree.quixosEntry().sourceBlock());
validateSource(quixos, "quixos", fileName, diagnostics);
const fragment = Boolean(tree.FRAGMENT());
const quixosEntries = tree.quixosEntry();
let quixos: QuixosSource | undefined;
if (fragment && quixosEntries.length) {
issue(
diagnostics,
fileName,
"fragment-has-quixos-source",
"A lock fragment inherits the root document's Quixos source and must not redeclare it",
"quixos",
);
} else if (!fragment && quixosEntries.length !== 1) {
issue(
diagnostics,
fileName,
"root-quixos-source-count",
"A root lock document must contain exactly one Quixos source",
"quixos",
);
} else if (!fragment) {
quixos = lowerQuixosSource(quixosEntries[0]!.quixosSourceBlock());
validateQuixosSource(quixos, fileName, diagnostics);
}
const imports = tree.importEntry().map((context, index) => {
const importPath = stringValue(context.stringLiteral());
validateImport(
importPath,
index,
fileName,
diagnostics,
context.start?.line ?? 1,
context.start?.column ?? 0,
);
return importPath;
});
const repeatedImports = new Set<string>();
imports.forEach((importPath, index) => {
if (repeatedImports.has(importPath)) {
issue(
diagnostics,
fileName,
"duplicate-import",
`Duplicate lock import ${importPath}`,
`imports[${index}]`,
);
}
repeatedImports.add(importPath);
});
const resources: LockedResource[] = [];
const bindings = new Set<string>();
@@ -195,11 +348,56 @@ export const parseQuixosLock = (
? { ok: false, diagnostics }
: {
ok: true,
lock: { formatVersion: 1, quixos, resources },
document: fragment
? { kind: "fragment", formatVersion: 1, imports, resources }
: { kind: "root", formatVersion: 1, quixos: quixos!, imports, resources },
diagnostics: [],
};
};
export const parseQuixosLock = (
source: string,
fileName = "<memory>",
): QuixosLockParseResult => {
const parsed = parseQuixosLockDocument(source, fileName);
if (!parsed.ok) return parsed;
if (parsed.document.kind === "fragment") {
return {
ok: false,
diagnostics: [{
phase: "validation",
code: "expected-root-lock",
message: "Expected a root Quixos lock, found a lock fragment",
fileName,
line: 1,
column: 0,
}],
};
}
if (parsed.document.imports.length) {
return {
ok: false,
diagnostics: [{
phase: "resolution",
code: "imports-require-file-resolution",
message: "This lock has imports and must be loaded from its repository rather than parsed as an isolated string",
fileName,
line: 1,
column: 0,
}],
};
}
return {
ok: true,
lock: {
formatVersion: 1,
quixos: parsed.document.quixos,
resources: parsed.document.resources,
},
diagnostics: [],
};
};
const quoted = (value: string) => JSON.stringify(value);
const sourceLines = (source: GitSource, indentation: string): string[] => [
@@ -207,11 +405,22 @@ const sourceLines = (source: GitSource, indentation: string): string[] => [
`${indentation}commit ${quoted(source.commit.toLowerCase())};`,
];
const quixosSourceLines = (source: QuixosSource, indentation: string): string[] => [
`${indentation}repository ${quoted(source.repository)};`,
...(source.policy
? [
`${indentation}policy ${source.policy};`,
`${indentation}ref ${quoted(source.ref)};`,
]
: []),
`${indentation}commit ${quoted(source.commit.toLowerCase())};`,
];
export const formatQuixosLock = (lock: QuixosRepositoryLock): string => {
const lines = [
"quixos-lock version 1 {",
" quixos source {",
...sourceLines(lock.quixos, " "),
...quixosSourceLines(lock.quixos, " "),
" }",
];
for (const resource of lock.resources) {
@@ -225,3 +434,30 @@ export const formatQuixosLock = (lock: QuixosRepositoryLock): string => {
lines.push("}", "");
return lines.join("\n");
};
export const formatQuixosLockDocument = (document: QuixosLockDocument): string => {
const lines = [
`quixos-lock${document.kind === "fragment" ? " fragment" : ""} version 1 {`,
];
if (document.kind === "root") {
lines.push(
" quixos source {",
...quixosSourceLines(document.quixos, " "),
" }",
);
}
for (const importPath of document.imports) {
if (lines.length > 1) lines.push("");
lines.push(` import ${quoted(importPath)};`);
}
for (const resource of document.resources) {
if (lines.length > 1) lines.push("");
lines.push(
` ${resource.kind} ${resource.binding} source {`,
...sourceLines(resource.source, " "),
" }",
);
}
lines.push("}", "");
return lines.join("\n");
};