Introduce repository-backed capability workspaces

- define and validate the capability and resource-lock languages
- provision workspace source repositories through Central Gitea
- build package runtimes from pinned standalone sources
- replace legacy schema compilation with workspace persistence plans
- add stable optimistic registers and Automerge CRDT documents
- modernize TypeScript/Nix package builds and runtime activation readiness
This commit is contained in:
2026-09-03 15:46:52 -07:00
parent 3c2e4a7e85
commit f4987093f6
57 changed files with 14878 additions and 2788 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { parseQuixosLock } from "./parser.js";
const fileName = process.argv[2];
if (!fileName || process.argv.length !== 3) {
console.error("Usage: quixos-lock-check <quixos.lock>");
process.exit(2);
}
const result = parseQuixosLock(await readFile(fileName, "utf8"), fileName);
if (!result.ok) {
for (const diagnostic of result.diagnostics) {
console.error(
`${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column + 1}: ${diagnostic.phase}/${diagnostic.code}: ${diagnostic.message}`,
);
}
process.exit(1);
}
console.log(JSON.stringify(result.lock, null, 2));
@@ -0,0 +1,52 @@
token literal names:
null
'quixos-lock'
'version'
'quixos'
'source'
'interface'
'package'
'repository'
'commit'
'{'
'}'
';'
null
null
null
null
null
null
token symbolic names:
null
QUIXOS_LOCK
VERSION
QUIXOS
SOURCE
INTERFACE
PACKAGE
REPOSITORY
COMMIT
LBRACE
RBRACE
SEMI
INTEGER
IDENTIFIER
STRING_LITERAL
LINE_COMMENT
BLOCK_COMMENT
WS
rule names:
document
quixosEntry
resourceEntry
resourceKind
sourceBlock
identifier
stringLiteral
atn:
[4, 1, 17, 53, 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, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 5, 0, 21, 8, 0, 10, 0, 12, 0, 24, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 0, 0, 7, 0, 2, 4, 6, 8, 10, 12, 0, 1, 1, 0, 5, 6, 46, 0, 14, 1, 0, 0, 0, 2, 28, 1, 0, 0, 0, 4, 32, 1, 0, 0, 0, 6, 37, 1, 0, 0, 0, 8, 39, 1, 0, 0, 0, 10, 48, 1, 0, 0, 0, 12, 50, 1, 0, 0, 0, 14, 15, 5, 1, 0, 0, 15, 16, 5, 2, 0, 0, 16, 17, 5, 12, 0, 0, 17, 18, 5, 9, 0, 0, 18, 22, 3, 2, 1, 0, 19, 21, 3, 4, 2, 0, 20, 19, 1, 0, 0, 0, 21, 24, 1, 0, 0, 0, 22, 20, 1, 0, 0, 0, 22, 23, 1, 0, 0, 0, 23, 25, 1, 0, 0, 0, 24, 22, 1, 0, 0, 0, 25, 26, 5, 10, 0, 0, 26, 27, 5, 0, 0, 1, 27, 1, 1, 0, 0, 0, 28, 29, 5, 3, 0, 0, 29, 30, 5, 4, 0, 0, 30, 31, 3, 8, 4, 0, 31, 3, 1, 0, 0, 0, 32, 33, 3, 6, 3, 0, 33, 34, 3, 10, 5, 0, 34, 35, 5, 4, 0, 0, 35, 36, 3, 8, 4, 0, 36, 5, 1, 0, 0, 0, 37, 38, 7, 0, 0, 0, 38, 7, 1, 0, 0, 0, 39, 40, 5, 9, 0, 0, 40, 41, 5, 7, 0, 0, 41, 42, 3, 12, 6, 0, 42, 43, 5, 11, 0, 0, 43, 44, 5, 8, 0, 0, 44, 45, 3, 12, 6, 0, 45, 46, 5, 11, 0, 0, 46, 47, 5, 10, 0, 0, 47, 9, 1, 0, 0, 0, 48, 49, 5, 13, 0, 0, 49, 11, 1, 0, 0, 0, 50, 51, 5, 14, 0, 0, 51, 13, 1, 0, 0, 0, 1, 22]
@@ -0,0 +1,28 @@
QUIXOS_LOCK=1
VERSION=2
QUIXOS=3
SOURCE=4
INTERFACE=5
PACKAGE=6
REPOSITORY=7
COMMIT=8
LBRACE=9
RBRACE=10
SEMI=11
INTEGER=12
IDENTIFIER=13
STRING_LITERAL=14
LINE_COMMENT=15
BLOCK_COMMENT=16
WS=17
'quixos-lock'=1
'version'=2
'quixos'=3
'source'=4
'interface'=5
'package'=6
'repository'=7
'commit'=8
'{'=9
'}'=10
';'=11
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
QUIXOS_LOCK=1
VERSION=2
QUIXOS=3
SOURCE=4
INTERFACE=5
PACKAGE=6
REPOSITORY=7
COMMIT=8
LBRACE=9
RBRACE=10
SEMI=11
INTEGER=12
IDENTIFIER=13
STRING_LITERAL=14
LINE_COMMENT=15
BLOCK_COMMENT=16
WS=17
'quixos-lock'=1
'version'=2
'quixos'=3
'source'=4
'interface'=5
'package'=6
'repository'=7
'commit'=8
'{'=9
'}'=10
';'=11
@@ -0,0 +1,155 @@
import * as antlr from "antlr4ng";
import { Token } from "antlr4ng";
export class QuixosLockLexer extends antlr.Lexer {
public static readonly QUIXOS_LOCK = 1;
public static readonly VERSION = 2;
public static readonly QUIXOS = 3;
public static readonly SOURCE = 4;
public static readonly INTERFACE = 5;
public static readonly PACKAGE = 6;
public static readonly REPOSITORY = 7;
public static readonly COMMIT = 8;
public static readonly LBRACE = 9;
public static readonly RBRACE = 10;
public static readonly SEMI = 11;
public static readonly INTEGER = 12;
public static readonly IDENTIFIER = 13;
public static readonly STRING_LITERAL = 14;
public static readonly LINE_COMMENT = 15;
public static readonly BLOCK_COMMENT = 16;
public static readonly WS = 17;
public static readonly channelNames = [
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
];
public static readonly literalNames = [
null, "'quixos-lock'", "'version'", "'quixos'", "'source'", "'interface'",
"'package'", "'repository'", "'commit'", "'{'", "'}'", "';'"
];
public static readonly symbolicNames = [
null, "QUIXOS_LOCK", "VERSION", "QUIXOS", "SOURCE", "INTERFACE",
"PACKAGE", "REPOSITORY", "COMMIT", "LBRACE", "RBRACE", "SEMI", "INTEGER",
"IDENTIFIER", "STRING_LITERAL", "LINE_COMMENT", "BLOCK_COMMENT",
"WS"
];
public static readonly modeNames = [
"DEFAULT_MODE",
];
public static readonly ruleNames = [
"QUIXOS_LOCK", "VERSION", "QUIXOS", "SOURCE", "INTERFACE", "PACKAGE",
"REPOSITORY", "COMMIT", "LBRACE", "RBRACE", "SEMI", "INTEGER", "IDENTIFIER",
"STRING_LITERAL", "ESC", "HEX", "LINE_COMMENT", "BLOCK_COMMENT",
"WS",
];
public constructor(input: antlr.CharStream) {
super(input);
this.interpreter = new antlr.LexerATNSimulator(this, QuixosLockLexer._ATN, QuixosLockLexer.decisionsToDFA, new antlr.PredictionContextCache());
}
public get grammarFileName(): string { return "QuixosLock.g4"; }
public get literalNames(): (string | null)[] { return QuixosLockLexer.literalNames; }
public get symbolicNames(): (string | null)[] { return QuixosLockLexer.symbolicNames; }
public get ruleNames(): string[] { return QuixosLockLexer.ruleNames; }
public get serializedATN(): number[] { return QuixosLockLexer._serializedATN; }
public get channelNames(): string[] { return QuixosLockLexer.channelNames; }
public get modeNames(): string[] { return QuixosLockLexer.modeNames; }
public static readonly _serializedATN: number[] = [
4,0,17,181,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,1,0,1,
0,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,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,4,1,4,1,4,1,4,1,4,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,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,8,1,8,1,9,1,9,1,10,1,10,1,11,4,11,117,8,11,11,
11,12,11,118,1,12,1,12,5,12,123,8,12,10,12,12,12,126,9,12,1,13,1,
13,1,13,5,13,131,8,13,10,13,12,13,134,9,13,1,13,1,13,1,14,1,14,1,
14,1,14,1,14,1,14,1,14,1,14,3,14,146,8,14,1,15,1,15,1,16,1,16,1,
16,1,16,5,16,154,8,16,10,16,12,16,157,9,16,1,16,1,16,1,17,1,17,1,
17,1,17,5,17,165,8,17,10,17,12,17,168,9,17,1,17,1,17,1,17,1,17,1,
17,1,18,4,18,176,8,18,11,18,12,18,177,1,18,1,18,1,166,0,19,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,0,31,0,33,15,35,16,37,17,1,0,8,1,0,48,57,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,186,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,33,1,0,0,0,0,35,1,0,0,0,0,37,
1,0,0,0,1,39,1,0,0,0,3,51,1,0,0,0,5,59,1,0,0,0,7,66,1,0,0,0,9,73,
1,0,0,0,11,83,1,0,0,0,13,91,1,0,0,0,15,102,1,0,0,0,17,109,1,0,0,
0,19,111,1,0,0,0,21,113,1,0,0,0,23,116,1,0,0,0,25,120,1,0,0,0,27,
127,1,0,0,0,29,137,1,0,0,0,31,147,1,0,0,0,33,149,1,0,0,0,35,160,
1,0,0,0,37,175,1,0,0,0,39,40,5,113,0,0,40,41,5,117,0,0,41,42,5,105,
0,0,42,43,5,120,0,0,43,44,5,111,0,0,44,45,5,115,0,0,45,46,5,45,0,
0,46,47,5,108,0,0,47,48,5,111,0,0,48,49,5,99,0,0,49,50,5,107,0,0,
50,2,1,0,0,0,51,52,5,118,0,0,52,53,5,101,0,0,53,54,5,114,0,0,54,
55,5,115,0,0,55,56,5,105,0,0,56,57,5,111,0,0,57,58,5,110,0,0,58,
4,1,0,0,0,59,60,5,113,0,0,60,61,5,117,0,0,61,62,5,105,0,0,62,63,
5,120,0,0,63,64,5,111,0,0,64,65,5,115,0,0,65,6,1,0,0,0,66,67,5,115,
0,0,67,68,5,111,0,0,68,69,5,117,0,0,69,70,5,114,0,0,70,71,5,99,0,
0,71,72,5,101,0,0,72,8,1,0,0,0,73,74,5,105,0,0,74,75,5,110,0,0,75,
76,5,116,0,0,76,77,5,101,0,0,77,78,5,114,0,0,78,79,5,102,0,0,79,
80,5,97,0,0,80,81,5,99,0,0,81,82,5,101,0,0,82,10,1,0,0,0,83,84,5,
112,0,0,84,85,5,97,0,0,85,86,5,99,0,0,86,87,5,107,0,0,87,88,5,97,
0,0,88,89,5,103,0,0,89,90,5,101,0,0,90,12,1,0,0,0,91,92,5,114,0,
0,92,93,5,101,0,0,93,94,5,112,0,0,94,95,5,111,0,0,95,96,5,115,0,
0,96,97,5,105,0,0,97,98,5,116,0,0,98,99,5,111,0,0,99,100,5,114,0,
0,100,101,5,121,0,0,101,14,1,0,0,0,102,103,5,99,0,0,103,104,5,111,
0,0,104,105,5,109,0,0,105,106,5,109,0,0,106,107,5,105,0,0,107,108,
5,116,0,0,108,16,1,0,0,0,109,110,5,123,0,0,110,18,1,0,0,0,111,112,
5,125,0,0,112,20,1,0,0,0,113,114,5,59,0,0,114,22,1,0,0,0,115,117,
7,0,0,0,116,115,1,0,0,0,117,118,1,0,0,0,118,116,1,0,0,0,118,119,
1,0,0,0,119,24,1,0,0,0,120,124,7,1,0,0,121,123,7,2,0,0,122,121,1,
0,0,0,123,126,1,0,0,0,124,122,1,0,0,0,124,125,1,0,0,0,125,26,1,0,
0,0,126,124,1,0,0,0,127,132,5,34,0,0,128,131,3,29,14,0,129,131,8,
3,0,0,130,128,1,0,0,0,130,129,1,0,0,0,131,134,1,0,0,0,132,130,1,
0,0,0,132,133,1,0,0,0,133,135,1,0,0,0,134,132,1,0,0,0,135,136,5,
34,0,0,136,28,1,0,0,0,137,145,5,92,0,0,138,146,7,4,0,0,139,140,5,
117,0,0,140,141,3,31,15,0,141,142,3,31,15,0,142,143,3,31,15,0,143,
144,3,31,15,0,144,146,1,0,0,0,145,138,1,0,0,0,145,139,1,0,0,0,146,
30,1,0,0,0,147,148,7,5,0,0,148,32,1,0,0,0,149,150,5,47,0,0,150,151,
5,47,0,0,151,155,1,0,0,0,152,154,8,6,0,0,153,152,1,0,0,0,154,157,
1,0,0,0,155,153,1,0,0,0,155,156,1,0,0,0,156,158,1,0,0,0,157,155,
1,0,0,0,158,159,6,16,0,0,159,34,1,0,0,0,160,161,5,47,0,0,161,162,
5,42,0,0,162,166,1,0,0,0,163,165,9,0,0,0,164,163,1,0,0,0,165,168,
1,0,0,0,166,167,1,0,0,0,166,164,1,0,0,0,167,169,1,0,0,0,168,166,
1,0,0,0,169,170,5,42,0,0,170,171,5,47,0,0,171,172,1,0,0,0,172,173,
6,17,0,0,173,36,1,0,0,0,174,176,7,7,0,0,175,174,1,0,0,0,176,177,
1,0,0,0,177,175,1,0,0,0,177,178,1,0,0,0,178,179,1,0,0,0,179,180,
6,18,0,0,180,38,1,0,0,0,9,0,118,124,130,132,145,155,166,177,1,6,
0,0
];
private static __ATN: antlr.ATN;
public static get _ATN(): antlr.ATN {
if (!QuixosLockLexer.__ATN) {
QuixosLockLexer.__ATN = new antlr.ATNDeserializer().deserialize(QuixosLockLexer._serializedATN);
}
return QuixosLockLexer.__ATN;
}
private static readonly vocabulary = new antlr.Vocabulary(QuixosLockLexer.literalNames, QuixosLockLexer.symbolicNames, []);
public override get vocabulary(): antlr.Vocabulary {
return QuixosLockLexer.vocabulary;
}
private static readonly decisionsToDFA = QuixosLockLexer._ATN.decisionToState.map( (ds: antlr.DecisionState, index: number) => new antlr.DFA(ds, index) );
}
@@ -0,0 +1,535 @@
import * as antlr from "antlr4ng";
import { Token } from "antlr4ng";
import { QuixosLockVisitor } from "./QuixosLockVisitor.js";
// for running tests with parameters, TODO: discuss strategy for typed parameters in CI
// eslint-disable-next-line no-unused-vars
type int = number;
export class QuixosLockParser extends antlr.Parser {
public static readonly QUIXOS_LOCK = 1;
public static readonly VERSION = 2;
public static readonly QUIXOS = 3;
public static readonly SOURCE = 4;
public static readonly INTERFACE = 5;
public static readonly PACKAGE = 6;
public static readonly REPOSITORY = 7;
public static readonly COMMIT = 8;
public static readonly LBRACE = 9;
public static readonly RBRACE = 10;
public static readonly SEMI = 11;
public static readonly INTEGER = 12;
public static readonly IDENTIFIER = 13;
public static readonly STRING_LITERAL = 14;
public static readonly LINE_COMMENT = 15;
public static readonly BLOCK_COMMENT = 16;
public static readonly WS = 17;
public static readonly RULE_document = 0;
public static readonly RULE_quixosEntry = 1;
public static readonly RULE_resourceEntry = 2;
public static readonly RULE_resourceKind = 3;
public static readonly RULE_sourceBlock = 4;
public static readonly RULE_identifier = 5;
public static readonly RULE_stringLiteral = 6;
public static readonly literalNames = [
null, "'quixos-lock'", "'version'", "'quixos'", "'source'", "'interface'",
"'package'", "'repository'", "'commit'", "'{'", "'}'", "';'"
];
public static readonly symbolicNames = [
null, "QUIXOS_LOCK", "VERSION", "QUIXOS", "SOURCE", "INTERFACE",
"PACKAGE", "REPOSITORY", "COMMIT", "LBRACE", "RBRACE", "SEMI", "INTEGER",
"IDENTIFIER", "STRING_LITERAL", "LINE_COMMENT", "BLOCK_COMMENT",
"WS"
];
public static readonly ruleNames = [
"document", "quixosEntry", "resourceEntry", "resourceKind", "sourceBlock",
"identifier", "stringLiteral",
];
public get grammarFileName(): string { return "QuixosLock.g4"; }
public get literalNames(): (string | null)[] { return QuixosLockParser.literalNames; }
public get symbolicNames(): (string | null)[] { return QuixosLockParser.symbolicNames; }
public get ruleNames(): string[] { return QuixosLockParser.ruleNames; }
public get serializedATN(): number[] { return QuixosLockParser._serializedATN; }
protected createFailedPredicateException(predicate?: string, message?: string): antlr.FailedPredicateException {
return new antlr.FailedPredicateException(this, predicate, message);
}
public constructor(input: antlr.TokenStream) {
super(input);
this.interpreter = new antlr.ParserATNSimulator(this, QuixosLockParser._ATN, QuixosLockParser.decisionsToDFA, new antlr.PredictionContextCache());
}
public document(): DocumentContext {
let localContext = new DocumentContext(this.context, this.state);
this.enterRule(localContext, 0, QuixosLockParser.RULE_document);
let _la: number;
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 14;
this.match(QuixosLockParser.QUIXOS_LOCK);
this.state = 15;
this.match(QuixosLockParser.VERSION);
this.state = 16;
this.match(QuixosLockParser.INTEGER);
this.state = 17;
this.match(QuixosLockParser.LBRACE);
this.state = 18;
this.quixosEntry();
this.state = 22;
this.errorHandler.sync(this);
_la = this.tokenStream.LA(1);
while (_la === 5 || _la === 6) {
{
{
this.state = 19;
this.resourceEntry();
}
}
this.state = 24;
this.errorHandler.sync(this);
_la = this.tokenStream.LA(1);
}
this.state = 25;
this.match(QuixosLockParser.RBRACE);
this.state = 26;
this.match(QuixosLockParser.EOF);
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public quixosEntry(): QuixosEntryContext {
let localContext = new QuixosEntryContext(this.context, this.state);
this.enterRule(localContext, 2, QuixosLockParser.RULE_quixosEntry);
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 28;
this.match(QuixosLockParser.QUIXOS);
this.state = 29;
this.match(QuixosLockParser.SOURCE);
this.state = 30;
this.sourceBlock();
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public resourceEntry(): ResourceEntryContext {
let localContext = new ResourceEntryContext(this.context, this.state);
this.enterRule(localContext, 4, QuixosLockParser.RULE_resourceEntry);
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 32;
this.resourceKind();
this.state = 33;
this.identifier();
this.state = 34;
this.match(QuixosLockParser.SOURCE);
this.state = 35;
this.sourceBlock();
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public resourceKind(): ResourceKindContext {
let localContext = new ResourceKindContext(this.context, this.state);
this.enterRule(localContext, 6, QuixosLockParser.RULE_resourceKind);
let _la: number;
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 37;
_la = this.tokenStream.LA(1);
if(!(_la === 5 || _la === 6)) {
this.errorHandler.recoverInline(this);
}
else {
this.errorHandler.reportMatch(this);
this.consume();
}
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public sourceBlock(): SourceBlockContext {
let localContext = new SourceBlockContext(this.context, this.state);
this.enterRule(localContext, 8, QuixosLockParser.RULE_sourceBlock);
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 39;
this.match(QuixosLockParser.LBRACE);
this.state = 40;
this.match(QuixosLockParser.REPOSITORY);
this.state = 41;
this.stringLiteral();
this.state = 42;
this.match(QuixosLockParser.SEMI);
this.state = 43;
this.match(QuixosLockParser.COMMIT);
this.state = 44;
this.stringLiteral();
this.state = 45;
this.match(QuixosLockParser.SEMI);
this.state = 46;
this.match(QuixosLockParser.RBRACE);
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public identifier(): IdentifierContext {
let localContext = new IdentifierContext(this.context, this.state);
this.enterRule(localContext, 10, QuixosLockParser.RULE_identifier);
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 48;
this.match(QuixosLockParser.IDENTIFIER);
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public stringLiteral(): StringLiteralContext {
let localContext = new StringLiteralContext(this.context, this.state);
this.enterRule(localContext, 12, QuixosLockParser.RULE_stringLiteral);
try {
this.enterOuterAlt(localContext, 1);
{
this.state = 50;
this.match(QuixosLockParser.STRING_LITERAL);
}
}
catch (re) {
if (re instanceof antlr.RecognitionException) {
this.errorHandler.reportError(this, re);
this.errorHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localContext;
}
public static readonly _serializedATN: number[] = [
4,1,17,53,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,1,0,1,0,1,0,1,0,1,0,1,0,5,0,21,8,0,10,0,12,0,24,9,0,1,0,1,0,1,
0,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,4,1,4,1,4,1,4,1,
4,1,4,1,4,1,4,1,4,1,5,1,5,1,6,1,6,1,6,0,0,7,0,2,4,6,8,10,12,0,1,
1,0,5,6,46,0,14,1,0,0,0,2,28,1,0,0,0,4,32,1,0,0,0,6,37,1,0,0,0,8,
39,1,0,0,0,10,48,1,0,0,0,12,50,1,0,0,0,14,15,5,1,0,0,15,16,5,2,0,
0,16,17,5,12,0,0,17,18,5,9,0,0,18,22,3,2,1,0,19,21,3,4,2,0,20,19,
1,0,0,0,21,24,1,0,0,0,22,20,1,0,0,0,22,23,1,0,0,0,23,25,1,0,0,0,
24,22,1,0,0,0,25,26,5,10,0,0,26,27,5,0,0,1,27,1,1,0,0,0,28,29,5,
3,0,0,29,30,5,4,0,0,30,31,3,8,4,0,31,3,1,0,0,0,32,33,3,6,3,0,33,
34,3,10,5,0,34,35,5,4,0,0,35,36,3,8,4,0,36,5,1,0,0,0,37,38,7,0,0,
0,38,7,1,0,0,0,39,40,5,9,0,0,40,41,5,7,0,0,41,42,3,12,6,0,42,43,
5,11,0,0,43,44,5,8,0,0,44,45,3,12,6,0,45,46,5,11,0,0,46,47,5,10,
0,0,47,9,1,0,0,0,48,49,5,13,0,0,49,11,1,0,0,0,50,51,5,14,0,0,51,
13,1,0,0,0,1,22
];
private static __ATN: antlr.ATN;
public static get _ATN(): antlr.ATN {
if (!QuixosLockParser.__ATN) {
QuixosLockParser.__ATN = new antlr.ATNDeserializer().deserialize(QuixosLockParser._serializedATN);
}
return QuixosLockParser.__ATN;
}
private static readonly vocabulary = new antlr.Vocabulary(QuixosLockParser.literalNames, QuixosLockParser.symbolicNames, []);
public override get vocabulary(): antlr.Vocabulary {
return QuixosLockParser.vocabulary;
}
private static readonly decisionsToDFA = QuixosLockParser._ATN.decisionToState.map( (ds: antlr.DecisionState, index: number) => new antlr.DFA(ds, index) );
}
export class DocumentContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public QUIXOS_LOCK(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.QUIXOS_LOCK, 0)!;
}
public VERSION(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.VERSION, 0)!;
}
public INTEGER(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.INTEGER, 0)!;
}
public LBRACE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.LBRACE, 0)!;
}
public quixosEntry(): QuixosEntryContext {
return this.getRuleContext(0, QuixosEntryContext)!;
}
public RBRACE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.RBRACE, 0)!;
}
public EOF(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.EOF, 0)!;
}
public resourceEntry(): ResourceEntryContext[];
public resourceEntry(i: number): ResourceEntryContext | null;
public resourceEntry(i?: number): ResourceEntryContext[] | ResourceEntryContext | null {
if (i === undefined) {
return this.getRuleContexts(ResourceEntryContext);
}
return this.getRuleContext(i, ResourceEntryContext);
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_document;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitDocument) {
return visitor.visitDocument(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class QuixosEntryContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public QUIXOS(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.QUIXOS, 0)!;
}
public SOURCE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.SOURCE, 0)!;
}
public sourceBlock(): SourceBlockContext {
return this.getRuleContext(0, SourceBlockContext)!;
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_quixosEntry;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitQuixosEntry) {
return visitor.visitQuixosEntry(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class ResourceEntryContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public resourceKind(): ResourceKindContext {
return this.getRuleContext(0, ResourceKindContext)!;
}
public identifier(): IdentifierContext {
return this.getRuleContext(0, IdentifierContext)!;
}
public SOURCE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.SOURCE, 0)!;
}
public sourceBlock(): SourceBlockContext {
return this.getRuleContext(0, SourceBlockContext)!;
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_resourceEntry;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitResourceEntry) {
return visitor.visitResourceEntry(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class ResourceKindContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public INTERFACE(): antlr.TerminalNode | null {
return this.getToken(QuixosLockParser.INTERFACE, 0);
}
public PACKAGE(): antlr.TerminalNode | null {
return this.getToken(QuixosLockParser.PACKAGE, 0);
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_resourceKind;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitResourceKind) {
return visitor.visitResourceKind(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class SourceBlockContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public LBRACE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.LBRACE, 0)!;
}
public REPOSITORY(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.REPOSITORY, 0)!;
}
public stringLiteral(): StringLiteralContext[];
public stringLiteral(i: number): StringLiteralContext | null;
public stringLiteral(i?: number): StringLiteralContext[] | StringLiteralContext | null {
if (i === undefined) {
return this.getRuleContexts(StringLiteralContext);
}
return this.getRuleContext(i, StringLiteralContext);
}
public SEMI(): antlr.TerminalNode[];
public SEMI(i: number): antlr.TerminalNode | null;
public SEMI(i?: number): antlr.TerminalNode | null | antlr.TerminalNode[] {
if (i === undefined) {
return this.getTokens(QuixosLockParser.SEMI);
} else {
return this.getToken(QuixosLockParser.SEMI, i);
}
}
public COMMIT(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.COMMIT, 0)!;
}
public RBRACE(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.RBRACE, 0)!;
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_sourceBlock;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitSourceBlock) {
return visitor.visitSourceBlock(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class IdentifierContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public IDENTIFIER(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.IDENTIFIER, 0)!;
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_identifier;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitIdentifier) {
return visitor.visitIdentifier(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class StringLiteralContext extends antlr.ParserRuleContext {
public constructor(parent: antlr.ParserRuleContext | null, invokingState: number) {
super(parent, invokingState);
}
public STRING_LITERAL(): antlr.TerminalNode {
return this.getToken(QuixosLockParser.STRING_LITERAL, 0)!;
}
public override get ruleIndex(): number {
return QuixosLockParser.RULE_stringLiteral;
}
public override accept<Result>(visitor: QuixosLockVisitor<Result>): Result | null {
if (visitor.visitStringLiteral) {
return visitor.visitStringLiteral(this);
} else {
return visitor.visitChildren(this);
}
}
}
@@ -0,0 +1,65 @@
import { AbstractParseTreeVisitor } from "antlr4ng";
import { DocumentContext } from "./QuixosLockParser.js";
import { QuixosEntryContext } from "./QuixosLockParser.js";
import { ResourceEntryContext } from "./QuixosLockParser.js";
import { ResourceKindContext } from "./QuixosLockParser.js";
import { SourceBlockContext } from "./QuixosLockParser.js";
import { IdentifierContext } from "./QuixosLockParser.js";
import { StringLiteralContext } from "./QuixosLockParser.js";
/**
* This interface defines a complete generic visitor for a parse tree produced
* by `QuixosLockParser`.
*
* @param <Result> The return type of the visit operation. Use `void` for
* operations with no return type.
*/
export class QuixosLockVisitor<Result> extends AbstractParseTreeVisitor<Result> {
/**
* Visit a parse tree produced by `QuixosLockParser.document`.
* @param ctx the parse tree
* @return the visitor result
*/
visitDocument?: (ctx: DocumentContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.quixosEntry`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQuixosEntry?: (ctx: QuixosEntryContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.resourceEntry`.
* @param ctx the parse tree
* @return the visitor result
*/
visitResourceEntry?: (ctx: ResourceEntryContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.resourceKind`.
* @param ctx the parse tree
* @return the visitor result
*/
visitResourceKind?: (ctx: ResourceKindContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.sourceBlock`.
* @param ctx the parse tree
* @return the visitor result
*/
visitSourceBlock?: (ctx: SourceBlockContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.identifier`.
* @param ctx the parse tree
* @return the visitor result
*/
visitIdentifier?: (ctx: IdentifierContext) => Result;
/**
* Visit a parse tree produced by `QuixosLockParser.stringLiteral`.
* @param ctx the parse tree
* @return the visitor result
*/
visitStringLiteral?: (ctx: StringLiteralContext) => Result;
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./parser.js";
export * from "./types.js";
+227
View File
@@ -0,0 +1,227 @@
import {
BaseErrorListener,
CharStream,
CommonTokenStream,
type ATNSimulator,
type RecognitionException,
type Recognizer,
type Token,
} from "antlr4ng";
import { QuixosLockLexer } from "./generated/QuixosLockLexer.js";
import {
QuixosLockParser,
type SourceBlockContext,
} from "./generated/QuixosLockParser.js";
import type {
GitSource,
LockedResource,
QuixosRepositoryLock,
} from "./types.js";
export type QuixosLockDiagnostic = {
phase: "syntax" | "validation";
code: string;
message: string;
fileName: string;
line: number;
column: number;
path?: string;
};
export type QuixosLockParseResult =
| { ok: true; lock: QuixosRepositoryLock; diagnostics: [] }
| { ok: false; diagnostics: QuixosLockDiagnostic[] };
class SyntaxErrorListener extends BaseErrorListener {
constructor(
private readonly fileName: string,
private readonly diagnostics: QuixosLockDiagnostic[],
) {
super();
}
override syntaxError<S extends Token, T extends ATNSimulator>(
_recognizer: Recognizer<T>,
_offendingSymbol: S | null,
line: number,
column: number,
message: string,
_error: RecognitionException | null,
): void {
this.diagnostics.push({
phase: "syntax",
code: "syntax-error",
message,
fileName: this.fileName,
line,
column,
});
}
}
const stringValue = (context: { getText(): string }): string =>
JSON.parse(context.getText()) as string;
const lowerSource = (context: SourceBlockContext): GitSource => ({
resolver: "git",
repository: stringValue(context.stringLiteral(0)!),
commit: stringValue(context.stringLiteral(1)!).toLowerCase(),
});
const issue = (
diagnostics: QuixosLockDiagnostic[],
fileName: string,
code: string,
message: string,
path?: string,
) => diagnostics.push({
phase: "validation",
code,
message,
fileName,
line: 1,
column: 0,
path,
});
const validateSource = (
source: GitSource,
path: string,
fileName: string,
diagnostics: QuixosLockDiagnostic[],
) => {
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(source.commit)) {
issue(
diagnostics,
fileName,
"invalid-git-commit",
`${path}.commit must be a full 40- or 64-character Git object ID`,
`${path}.commit`,
);
}
let repository: URL;
try {
repository = new URL(source.repository);
} catch {
issue(
diagnostics,
fileName,
"invalid-git-repository",
`${path}.repository must be an absolute Git URL`,
`${path}.repository`,
);
return;
}
if (!["https:", "ssh:"].includes(repository.protocol)) {
issue(
diagnostics,
fileName,
"unsupported-git-transport",
`${path}.repository must use https:// or ssh://`,
`${path}.repository`,
);
}
if (repository.search || repository.hash) {
issue(
diagnostics,
fileName,
"decorated-git-repository",
`${path}.repository must be a base repository URL without a query or fragment`,
`${path}.repository`,
);
}
if (repository.password || (repository.protocol === "https:" && repository.username)) {
issue(
diagnostics,
fileName,
"embedded-git-credential",
`${path}.repository must not embed credentials`,
`${path}.repository`,
);
}
};
export const parseQuixosLock = (
source: string,
fileName = "<memory>",
): QuixosLockParseResult => {
const diagnostics: QuixosLockDiagnostic[] = [];
const listener = new SyntaxErrorListener(fileName, diagnostics);
const lexer = new QuixosLockLexer(CharStream.fromString(source));
lexer.removeErrorListeners();
lexer.addErrorListener(listener);
const parser = new QuixosLockParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.addErrorListener(listener);
const tree = parser.document();
if (diagnostics.length > 0) return { ok: false, diagnostics };
const formatVersion = Number.parseInt(tree.INTEGER().getText(), 10);
if (formatVersion !== 1) {
issue(
diagnostics,
fileName,
"unsupported-lock-version",
`Unsupported Quixos lock format version ${formatVersion}`,
"formatVersion",
);
}
const quixos = lowerSource(tree.quixosEntry().sourceBlock());
validateSource(quixos, "quixos", fileName, diagnostics);
const resources: LockedResource[] = [];
const bindings = new Set<string>();
for (const [index, context] of tree.resourceEntry().entries()) {
const kind = context.resourceKind().INTERFACE() ? "interface" : "package";
const binding = context.identifier().getText();
const key = `${kind}\0${binding}`;
if (bindings.has(key)) {
issue(
diagnostics,
fileName,
"duplicate-resource-binding",
`Duplicate ${kind} binding ${binding}`,
`resources[${index}].binding`,
);
}
bindings.add(key);
const resource = { kind, binding, source: lowerSource(context.sourceBlock()) } as LockedResource;
validateSource(resource.source, `resources[${index}].source`, fileName, diagnostics);
resources.push(resource);
}
return diagnostics.length > 0
? { ok: false, diagnostics }
: {
ok: true,
lock: { formatVersion: 1, quixos, resources },
diagnostics: [],
};
};
const quoted = (value: string) => JSON.stringify(value);
const sourceLines = (source: GitSource, indentation: string): string[] => [
`${indentation}repository ${quoted(source.repository)};`,
`${indentation}commit ${quoted(source.commit.toLowerCase())};`,
];
export const formatQuixosLock = (lock: QuixosRepositoryLock): string => {
const lines = [
"quixos-lock version 1 {",
" quixos source {",
...sourceLines(lock.quixos, " "),
" }",
];
for (const resource of lock.resources) {
lines.push(
"",
` ${resource.kind} ${resource.binding} source {`,
...sourceLines(resource.source, " "),
" }",
);
}
lines.push("}", "");
return lines.join("\n");
};
+38
View File
@@ -0,0 +1,38 @@
export type GitSource = {
resolver: "git";
repository: string;
commit: string;
};
export type LockedResourceKind = "interface" | "package";
export type LockedResource = {
kind: LockedResourceKind;
binding: string;
source: GitSource;
};
export type QuixosRepositoryLock = {
formatVersion: 1;
quixos: GitSource;
resources: LockedResource[];
};
export type NixGitInput = {
type: "git";
url: string;
ref: string;
rev: string;
};
export const RETENTION_TAG_PREFIX = "refs/tags/quixos-reachability/";
export const retentionTagForCommit = (commit: string): string =>
`${RETENTION_TAG_PREFIX}${commit.toLowerCase()}`;
export const nixGitInput = (source: GitSource): NixGitInput => ({
type: "git",
url: source.repository,
ref: retentionTagForCommit(source.commit),
rev: source.commit.toLowerCase(),
});