Implement checked workspace integration contracts (WI-01)

This commit is contained in:
Timothy J. Aveni
2026-09-21 11:53:38 -07:00
parent 3c0378358f
commit 884b12f759
22 changed files with 4349 additions and 3089 deletions
+20
View File
@@ -27,6 +27,7 @@ workspaceItem
| sharedAttachmentDecl | sharedAttachmentDecl
| conformanceDecl | conformanceDecl
| constructorBindingDecl | constructorBindingDecl
| presentationDecl
| typeAliasDecl | typeAliasDecl
; ;
@@ -145,6 +146,15 @@ packageExport
| packageConstructorExport | packageConstructorExport
| packageQuery | packageQuery
| packageQuerySpecialization | packageQuerySpecialization
| componentDecl
;
componentDecl
: COMPONENT identifier ID stringLiteral SUBJECT interfaceType RUNTIME identifier SEMI
;
presentationDecl
: PRESENTATION identifier ID stringLiteral FOR interfaceType USING identifier DOT identifier SEMI
; ;
packageQuery packageQuery
@@ -471,6 +481,11 @@ identifier
| RESULT_BYTES | RESULT_BYTES
| RPC_CALLS | RPC_CALLS
| DEADLINE_MS | DEADLINE_MS
| COMPONENT
| PRESENTATION
| SUBJECT
| RUNTIME
| FOR
; ;
stringLiteral stringLiteral
@@ -478,6 +493,11 @@ stringLiteral
; ;
EDIT: 'edit'; EDIT: 'edit';
COMPONENT: 'component';
PRESENTATION: 'presentation';
SUBJECT: 'subject';
RUNTIME: 'runtime';
FOR: 'for';
CREATION: 'creation'; CREATION: 'creation';
INITIALIZE: 'initialize'; INITIALIZE: 'initialize';
WORKSPACE: 'workspace'; WORKSPACE: 'workspace';
+9
View File
@@ -4,6 +4,7 @@ import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileRepositoryQuery } from "../query/compile.js"; import { compileRepositoryQuery } from "../query/compile.js";
import { checkQueryTemplate } from "../query/templates.js"; import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js"; import { readRepositorySource } from "./source-loader.js";
import { parseControllerCatalog } from "../capability-model/controllers.js";
import { linkQueries } from "../query/link.js"; import { linkQueries } from "../query/link.js";
import { import {
capabilityId, capabilityId,
@@ -249,6 +250,14 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
compileRepositoryQuery(snapshot.directory, query, queryInterfaces), compileRepositoryQuery(snapshot.directory, query, queryInterfaces),
), ),
); );
let controllerText: string | undefined;
try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (controllerText !== undefined)
compiled.resource.revision.controllers = parseControllerCatalog(controllerText, queryInterfaces);
let catalogText: string | undefined; let catalogText: string | undefined;
try { try {
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8"); catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
+3 -1
View File
@@ -69,7 +69,9 @@ export async function inspectAuthoringRepository(root: string, historyLimit = 10
declarations: syntax declarations: syntax
? [...walkSyntax(syntax.root)] ? [...walkSyntax(syntax.root)]
.filter((node) => .filter((node) =>
/^(?:interface|package|atom|state|edge|method|function|event|conformance)\w*Decl$/.test(node.kind), /^(?:interface|package|atom|state|edge|method|function|event|conformance|component|presentation)\w*Decl$/.test(
node.kind,
),
) )
.map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) })) .map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) }))
: [], : [],
File diff suppressed because one or more lines are too long
@@ -1,289 +1,299 @@
EDIT=1 EDIT=1
CREATION=2 COMPONENT=2
INITIALIZE=3 PRESENTATION=3
WORKSPACE=4 SUBJECT=4
EXECUTION=5 RUNTIME=5
SNAPSHOT=6 FOR=6
TRANSACTION=7 CREATION=7
REMOTE=8 INITIALIZE=8
TARGETS=9 WORKSPACE=9
PORTABLE=10 EXECUTION=10
REPLICA=11 SNAPSHOT=11
LOCAL=12 TRANSACTION=12
FROM=13 REMOTE=13
RESULT_BYTES=14 TARGETS=14
RPC_CALLS=15 PORTABLE=15
DEADLINE_MS=16 REPLICA=16
QUERY=17 LOCAL=17
REFLECTION=18 FROM=18
SPECIALIZE=19 RESULT_BYTES=19
ROOT=20 RPC_CALLS=20
DOCUMENT=21 DEADLINE_MS=21
FRAGMENTS=22 QUERY=22
VIEW=23 REFLECTION=23
MAX=24 SPECIALIZE=24
ALLOW=25 ROOT=25
POLL=26 DOCUMENT=26
QUERYABLE=27 FRAGMENTS=27
RPC=28 VIEW=28
QUERY_REASON=29 MAX=29
TYPE=30 ALLOW=30
OBJECT=31 POLL=31
STORABLE=32 QUERYABLE=32
IMPLEMENTS=33 RPC=33
REF=34 QUERY_REASON=34
FRAGMENT=35 TYPE=35
IMPORT=36 OBJECT=36
EXTERNAL=37 STORABLE=37
ATOM=38 IMPLEMENTS=38
INTERFACE=39 REF=39
INTERFACES=40 FRAGMENT=40
PACKAGE=41 IMPORT=41
VALUE=42 EXTERNAL=42
RELATION=43 ATOM=43
OPERATION=44 INTERFACE=44
FUNCTION=45 INTERFACES=45
CONSTRUCTOR=46 PACKAGE=46
CONSTRUCTS=47 VALUE=47
INPUT=48 RELATION=48
CONFORM=49 OPERATION=49
AS=50 FUNCTION=50
BIND=51 CONSTRUCTOR=51
STATIC=52 CONSTRUCTS=52
TO=53 INPUT=53
PRIVATE=54 CONFORM=54
SHARED=55 AS=55
STATE=56 BIND=56
EDGE=57 STATIC=57
PROJECTION=58 TO=58
WITH=59 PRIVATE=59
USING=60 SHARED=60
ACCEPT=61 STATE=61
VIA=62 EDGE=62
MATERIALIZE=63 PROJECTION=63
IF=64 WITH=64
ABSENT=65 USING=65
ON=66 ACCEPT=66
POLICY=67 VIA=67
DEFAULT=68 MATERIALIZE=68
SOURCE=69 IF=69
REPOSITORY=70 ABSENT=70
COMMIT=71 ON=71
REVISION=72 POLICY=72
SEMANTIC_MAJOR=73 DEFAULT=73
ON_DELETE=74 SOURCE=74
LIFECYCLE=75 REPOSITORY=75
RETAIN_OTHER=76 COMMIT=76
KEYED=77 REVISION=77
PUBLIC_TRAVERSAL=78 SEMANTIC_MAJOR=78
ID=79 ON_DELETE=79
DOC=80 LIFECYCLE=80
MODE=81 RETAIN_OTHER=81
EMITS=82 KEYED=82
RECEIVER=83 PUBLIC_TRAVERSAL=83
REQUIRES=84 ID=84
ANY=85 DOC=85
GET=86 MODE=86
SET=87 EMITS=87
WATCH=88 RECEIVER=88
START=89 REQUIRES=89
STOP=90 ANY=90
READ=91 GET=91
WRITE=92 SET=92
RESOLVE=93 WATCH=93
CONNECT=94 START=94
DISCONNECT=95 STOP=95
CALL=96 READ=96
WATCH_START=97 WRITE=97
WATCH_STOP=98 RESOLVE=98
SUBSCRIBE=99 CONNECT=99
UNSUBSCRIBE=100 DISCONNECT=100
REGISTER=101 CALL=101
CRDT=102 WATCH_START=102
OPTIONAL_ONE=103 WATCH_STOP=103
EXACTLY_ONE=104 SUBSCRIBE=104
MANY_UNIQUE=105 UNSUBSCRIBE=105
MANY=106 REGISTER=106
ORDERED=107 CRDT=107
UNIT=108 OPTIONAL_ONE=108
WATCH_HANDLE=109 EXACTLY_ONE=109
MESSAGE=110 MANY_UNIQUE=110
ATOM_REF=111 MANY=111
INTERFACE_REF=112 ORDERED=112
OPTIONAL=113 UNIT=113
LIST=114 WATCH_HANDLE=114
RECORD=115 MESSAGE=115
BOOL=116 ATOM_REF=116
BYTES=117 INTERFACE_REF=117
DOUBLE=118 OPTIONAL=118
INT32=119 LIST=119
INT64=120 RECORD=120
STRING=121 BOOL=121
UINT32=122 BYTES=122
UINT64=123 DOUBLE=123
TRUE=124 INT32=124
FALSE=125 INT64=125
NULL=126 STRING=126
ARROW=127 UINT32=127
COLON=128 UINT64=128
SEMI=129 TRUE=129
COMMA=130 FALSE=130
DOT=131 NULL=131
LBRACE=132 ARROW=132
RBRACE=133 COLON=133
LBRACK=134 SEMI=134
RBRACK=135 COMMA=135
LPAREN=136 DOT=136
RPAREN=137 LBRACE=137
LT=138 RBRACE=138
GT=139 LBRACK=139
AMP=140 RBRACK=140
EQUAL=141 LPAREN=141
INTEGER=142 RPAREN=142
JSON_NUMBER=143 LT=143
IDENTIFIER=144 GT=144
STRING_LITERAL=145 AMP=145
LINE_COMMENT=146 EQUAL=146
BLOCK_COMMENT=147 INTEGER=147
WS=148 JSON_NUMBER=148
IDENTIFIER=149
STRING_LITERAL=150
LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
'edit'=1 'edit'=1
'creation'=2 'component'=2
'initialize'=3 'presentation'=3
'workspace'=4 'subject'=4
'execution'=5 'runtime'=5
'snapshot'=6 'for'=6
'transaction'=7 'creation'=7
'remote'=8 'initialize'=8
'targets'=9 'workspace'=9
'portable'=10 'execution'=10
'replica'=11 'snapshot'=11
'local'=12 'transaction'=12
'from'=13 'remote'=13
'result-bytes'=14 'targets'=14
'rpc-calls'=15 'portable'=15
'deadline-ms'=16 'replica'=16
'query'=17 'local'=17
'reflection'=18 'from'=18
'specialize'=19 'result-bytes'=19
'root'=20 'rpc-calls'=20
'document'=21 'deadline-ms'=21
'fragments'=22 'query'=22
'view'=23 'reflection'=23
'max'=24 'specialize'=24
'allow'=25 'root'=25
'poll'=26 'document'=26
'queryable'=27 'fragments'=27
'rpc'=28 'view'=28
'query-reason'=29 'max'=29
'type'=30 'allow'=30
'object'=31 'poll'=31
'storable'=32 'queryable'=32
'implements'=33 'rpc'=33
'ref'=34 'query-reason'=34
'fragment'=35 'type'=35
'import'=36 'object'=36
'external'=37 'storable'=37
'atom'=38 'implements'=38
'interface'=39 'ref'=39
'interfaces'=40 'fragment'=40
'package'=41 'import'=41
'value'=42 'external'=42
'relation'=43 'atom'=43
'operation'=44 'interface'=44
'function'=45 'interfaces'=45
'constructor'=46 'package'=46
'constructs'=47 'value'=47
'input'=48 'relation'=48
'conform'=49 'operation'=49
'as'=50 'function'=50
'bind'=51 'constructor'=51
'static'=52 'constructs'=52
'to'=53 'input'=53
'private'=54 'conform'=54
'shared'=55 'as'=55
'state'=56 'bind'=56
'edge'=57 'static'=57
'projection'=58 'to'=58
'with'=59 'private'=59
'using'=60 'shared'=60
'accept'=61 'state'=61
'via'=62 'edge'=62
'materialize'=63 'projection'=63
'if'=64 'with'=64
'absent'=65 'using'=65
'on'=66 'accept'=66
'policy'=67 'via'=67
'default'=68 'materialize'=68
'source'=69 'if'=69
'repository'=70 'absent'=70
'commit'=71 'on'=71
'revision'=72 'policy'=72
'semantic-major'=73 'default'=73
'on-delete'=74 'source'=74
'lifecycle'=75 'repository'=75
'retain-other'=76 'commit'=76
'keyed'=77 'revision'=77
'public-traversal'=78 'semantic-major'=78
'id'=79 'on-delete'=79
'doc'=80 'lifecycle'=80
'mode'=81 'retain-other'=81
'emits'=82 'keyed'=82
'receiver'=83 'public-traversal'=83
'requires'=84 'id'=84
'any'=85 'doc'=85
'get'=86 'mode'=86
'set'=87 'emits'=87
'watch'=88 'receiver'=88
'start'=89 'requires'=89
'stop'=90 'any'=90
'read'=91 'get'=91
'write'=92 'set'=92
'resolve'=93 'watch'=93
'connect'=94 'start'=94
'disconnect'=95 'stop'=95
'call'=96 'read'=96
'watch-start'=97 'write'=97
'watch-stop'=98 'resolve'=98
'subscribe'=99 'connect'=99
'unsubscribe'=100 'disconnect'=100
'register'=101 'call'=101
'crdt'=102 'watch-start'=102
'optional-one'=103 'watch-stop'=103
'exactly-one'=104 'subscribe'=104
'many-unique'=105 'unsubscribe'=105
'many'=106 'register'=106
'ordered'=107 'crdt'=107
'unit'=108 'optional-one'=108
'watch-handle'=109 'exactly-one'=109
'message'=110 'many-unique'=110
'atom-ref'=111 'many'=111
'interface-ref'=112 'ordered'=112
'optional'=113 'unit'=113
'list'=114 'watch-handle'=114
'record'=115 'message'=115
'bool'=116 'atom-ref'=116
'bytes'=117 'interface-ref'=117
'double'=118 'optional'=118
'int32'=119 'list'=119
'int64'=120 'record'=120
'string'=121 'bool'=121
'uint32'=122 'bytes'=122
'uint64'=123 'double'=123
'true'=124 'int32'=124
'false'=125 'int64'=125
'null'=126 'string'=126
'->'=127 'uint32'=127
':'=128 'uint64'=128
';'=129 'true'=129
','=130 'false'=130
'.'=131 'null'=131
'{'=132 '->'=132
'}'=133 ':'=133
'['=134 ';'=134
']'=135 ','=135
'('=136 '.'=136
')'=137 '{'=137
'<'=138 '}'=138
'>'=139 '['=139
'&'=140 ']'=140
'='=141 '('=141
')'=142
'<'=143
'>'=144
'&'=145
'='=146
File diff suppressed because one or more lines are too long
@@ -1,289 +1,299 @@
EDIT=1 EDIT=1
CREATION=2 COMPONENT=2
INITIALIZE=3 PRESENTATION=3
WORKSPACE=4 SUBJECT=4
EXECUTION=5 RUNTIME=5
SNAPSHOT=6 FOR=6
TRANSACTION=7 CREATION=7
REMOTE=8 INITIALIZE=8
TARGETS=9 WORKSPACE=9
PORTABLE=10 EXECUTION=10
REPLICA=11 SNAPSHOT=11
LOCAL=12 TRANSACTION=12
FROM=13 REMOTE=13
RESULT_BYTES=14 TARGETS=14
RPC_CALLS=15 PORTABLE=15
DEADLINE_MS=16 REPLICA=16
QUERY=17 LOCAL=17
REFLECTION=18 FROM=18
SPECIALIZE=19 RESULT_BYTES=19
ROOT=20 RPC_CALLS=20
DOCUMENT=21 DEADLINE_MS=21
FRAGMENTS=22 QUERY=22
VIEW=23 REFLECTION=23
MAX=24 SPECIALIZE=24
ALLOW=25 ROOT=25
POLL=26 DOCUMENT=26
QUERYABLE=27 FRAGMENTS=27
RPC=28 VIEW=28
QUERY_REASON=29 MAX=29
TYPE=30 ALLOW=30
OBJECT=31 POLL=31
STORABLE=32 QUERYABLE=32
IMPLEMENTS=33 RPC=33
REF=34 QUERY_REASON=34
FRAGMENT=35 TYPE=35
IMPORT=36 OBJECT=36
EXTERNAL=37 STORABLE=37
ATOM=38 IMPLEMENTS=38
INTERFACE=39 REF=39
INTERFACES=40 FRAGMENT=40
PACKAGE=41 IMPORT=41
VALUE=42 EXTERNAL=42
RELATION=43 ATOM=43
OPERATION=44 INTERFACE=44
FUNCTION=45 INTERFACES=45
CONSTRUCTOR=46 PACKAGE=46
CONSTRUCTS=47 VALUE=47
INPUT=48 RELATION=48
CONFORM=49 OPERATION=49
AS=50 FUNCTION=50
BIND=51 CONSTRUCTOR=51
STATIC=52 CONSTRUCTS=52
TO=53 INPUT=53
PRIVATE=54 CONFORM=54
SHARED=55 AS=55
STATE=56 BIND=56
EDGE=57 STATIC=57
PROJECTION=58 TO=58
WITH=59 PRIVATE=59
USING=60 SHARED=60
ACCEPT=61 STATE=61
VIA=62 EDGE=62
MATERIALIZE=63 PROJECTION=63
IF=64 WITH=64
ABSENT=65 USING=65
ON=66 ACCEPT=66
POLICY=67 VIA=67
DEFAULT=68 MATERIALIZE=68
SOURCE=69 IF=69
REPOSITORY=70 ABSENT=70
COMMIT=71 ON=71
REVISION=72 POLICY=72
SEMANTIC_MAJOR=73 DEFAULT=73
ON_DELETE=74 SOURCE=74
LIFECYCLE=75 REPOSITORY=75
RETAIN_OTHER=76 COMMIT=76
KEYED=77 REVISION=77
PUBLIC_TRAVERSAL=78 SEMANTIC_MAJOR=78
ID=79 ON_DELETE=79
DOC=80 LIFECYCLE=80
MODE=81 RETAIN_OTHER=81
EMITS=82 KEYED=82
RECEIVER=83 PUBLIC_TRAVERSAL=83
REQUIRES=84 ID=84
ANY=85 DOC=85
GET=86 MODE=86
SET=87 EMITS=87
WATCH=88 RECEIVER=88
START=89 REQUIRES=89
STOP=90 ANY=90
READ=91 GET=91
WRITE=92 SET=92
RESOLVE=93 WATCH=93
CONNECT=94 START=94
DISCONNECT=95 STOP=95
CALL=96 READ=96
WATCH_START=97 WRITE=97
WATCH_STOP=98 RESOLVE=98
SUBSCRIBE=99 CONNECT=99
UNSUBSCRIBE=100 DISCONNECT=100
REGISTER=101 CALL=101
CRDT=102 WATCH_START=102
OPTIONAL_ONE=103 WATCH_STOP=103
EXACTLY_ONE=104 SUBSCRIBE=104
MANY_UNIQUE=105 UNSUBSCRIBE=105
MANY=106 REGISTER=106
ORDERED=107 CRDT=107
UNIT=108 OPTIONAL_ONE=108
WATCH_HANDLE=109 EXACTLY_ONE=109
MESSAGE=110 MANY_UNIQUE=110
ATOM_REF=111 MANY=111
INTERFACE_REF=112 ORDERED=112
OPTIONAL=113 UNIT=113
LIST=114 WATCH_HANDLE=114
RECORD=115 MESSAGE=115
BOOL=116 ATOM_REF=116
BYTES=117 INTERFACE_REF=117
DOUBLE=118 OPTIONAL=118
INT32=119 LIST=119
INT64=120 RECORD=120
STRING=121 BOOL=121
UINT32=122 BYTES=122
UINT64=123 DOUBLE=123
TRUE=124 INT32=124
FALSE=125 INT64=125
NULL=126 STRING=126
ARROW=127 UINT32=127
COLON=128 UINT64=128
SEMI=129 TRUE=129
COMMA=130 FALSE=130
DOT=131 NULL=131
LBRACE=132 ARROW=132
RBRACE=133 COLON=133
LBRACK=134 SEMI=134
RBRACK=135 COMMA=135
LPAREN=136 DOT=136
RPAREN=137 LBRACE=137
LT=138 RBRACE=138
GT=139 LBRACK=139
AMP=140 RBRACK=140
EQUAL=141 LPAREN=141
INTEGER=142 RPAREN=142
JSON_NUMBER=143 LT=143
IDENTIFIER=144 GT=144
STRING_LITERAL=145 AMP=145
LINE_COMMENT=146 EQUAL=146
BLOCK_COMMENT=147 INTEGER=147
WS=148 JSON_NUMBER=148
IDENTIFIER=149
STRING_LITERAL=150
LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
'edit'=1 'edit'=1
'creation'=2 'component'=2
'initialize'=3 'presentation'=3
'workspace'=4 'subject'=4
'execution'=5 'runtime'=5
'snapshot'=6 'for'=6
'transaction'=7 'creation'=7
'remote'=8 'initialize'=8
'targets'=9 'workspace'=9
'portable'=10 'execution'=10
'replica'=11 'snapshot'=11
'local'=12 'transaction'=12
'from'=13 'remote'=13
'result-bytes'=14 'targets'=14
'rpc-calls'=15 'portable'=15
'deadline-ms'=16 'replica'=16
'query'=17 'local'=17
'reflection'=18 'from'=18
'specialize'=19 'result-bytes'=19
'root'=20 'rpc-calls'=20
'document'=21 'deadline-ms'=21
'fragments'=22 'query'=22
'view'=23 'reflection'=23
'max'=24 'specialize'=24
'allow'=25 'root'=25
'poll'=26 'document'=26
'queryable'=27 'fragments'=27
'rpc'=28 'view'=28
'query-reason'=29 'max'=29
'type'=30 'allow'=30
'object'=31 'poll'=31
'storable'=32 'queryable'=32
'implements'=33 'rpc'=33
'ref'=34 'query-reason'=34
'fragment'=35 'type'=35
'import'=36 'object'=36
'external'=37 'storable'=37
'atom'=38 'implements'=38
'interface'=39 'ref'=39
'interfaces'=40 'fragment'=40
'package'=41 'import'=41
'value'=42 'external'=42
'relation'=43 'atom'=43
'operation'=44 'interface'=44
'function'=45 'interfaces'=45
'constructor'=46 'package'=46
'constructs'=47 'value'=47
'input'=48 'relation'=48
'conform'=49 'operation'=49
'as'=50 'function'=50
'bind'=51 'constructor'=51
'static'=52 'constructs'=52
'to'=53 'input'=53
'private'=54 'conform'=54
'shared'=55 'as'=55
'state'=56 'bind'=56
'edge'=57 'static'=57
'projection'=58 'to'=58
'with'=59 'private'=59
'using'=60 'shared'=60
'accept'=61 'state'=61
'via'=62 'edge'=62
'materialize'=63 'projection'=63
'if'=64 'with'=64
'absent'=65 'using'=65
'on'=66 'accept'=66
'policy'=67 'via'=67
'default'=68 'materialize'=68
'source'=69 'if'=69
'repository'=70 'absent'=70
'commit'=71 'on'=71
'revision'=72 'policy'=72
'semantic-major'=73 'default'=73
'on-delete'=74 'source'=74
'lifecycle'=75 'repository'=75
'retain-other'=76 'commit'=76
'keyed'=77 'revision'=77
'public-traversal'=78 'semantic-major'=78
'id'=79 'on-delete'=79
'doc'=80 'lifecycle'=80
'mode'=81 'retain-other'=81
'emits'=82 'keyed'=82
'receiver'=83 'public-traversal'=83
'requires'=84 'id'=84
'any'=85 'doc'=85
'get'=86 'mode'=86
'set'=87 'emits'=87
'watch'=88 'receiver'=88
'start'=89 'requires'=89
'stop'=90 'any'=90
'read'=91 'get'=91
'write'=92 'set'=92
'resolve'=93 'watch'=93
'connect'=94 'start'=94
'disconnect'=95 'stop'=95
'call'=96 'read'=96
'watch-start'=97 'write'=97
'watch-stop'=98 'resolve'=98
'subscribe'=99 'connect'=99
'unsubscribe'=100 'disconnect'=100
'register'=101 'call'=101
'crdt'=102 'watch-start'=102
'optional-one'=103 'watch-stop'=103
'exactly-one'=104 'subscribe'=104
'many-unique'=105 'unsubscribe'=105
'many'=106 'register'=106
'ordered'=107 'crdt'=107
'unit'=108 'optional-one'=108
'watch-handle'=109 'exactly-one'=109
'message'=110 'many-unique'=110
'atom-ref'=111 'many'=111
'interface-ref'=112 'ordered'=112
'optional'=113 'unit'=113
'list'=114 'watch-handle'=114
'record'=115 'message'=115
'bool'=116 'atom-ref'=116
'bytes'=117 'interface-ref'=117
'double'=118 'optional'=118
'int32'=119 'list'=119
'int64'=120 'record'=120
'string'=121 'bool'=121
'uint32'=122 'bytes'=122
'uint64'=123 'double'=123
'true'=124 'int32'=124
'false'=125 'int64'=125
'null'=126 'string'=126
'->'=127 'uint32'=127
':'=128 'uint64'=128
';'=129 'true'=129
','=130 'false'=130
'.'=131 'null'=131
'{'=132 '->'=132
'}'=133 ':'=133
'['=134 ';'=134
']'=135 ','=135
'('=136 '.'=136
')'=137 '{'=137
'<'=138 '}'=138
'>'=139 '['=139
'&'=140 ']'=140
'='=141 '('=141
')'=142
'<'=143
'>'=144
'&'=145
'='=146
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -29,6 +29,8 @@ import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
import { TargetConstraintContext } from "./QuixosCapabilityParser.js"; import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js"; import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js"; import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { ComponentDeclContext } from "./QuixosCapabilityParser.js";
import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js"; import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js"; import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js"; import { QueryClauseContext } from "./QuixosCapabilityParser.js";
@@ -251,6 +253,18 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitPackageExport?: (ctx: PackageExportContext) => Result; visitPackageExport?: (ctx: PackageExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.componentDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitComponentDecl?: (ctx: ComponentDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.presentationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPresentationDecl?: (ctx: PresentationDeclContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`. * Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`.
* @param ctx the parse tree * @param ctx the parse tree
+42
View File
@@ -1333,9 +1333,25 @@ const lowerPackage = (
}), }),
); );
const genericExports: GenericPackageExport[] = []; const genericExports: GenericPackageExport[] = [];
const components: NonNullable<PackageRevision["components"]> = [];
const queries: QueryDeclaration[] = []; const queries: QueryDeclaration[] = [];
const queryTemplates: QueryTemplate[] = []; const queryTemplates: QueryTemplate[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => { const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
const component = exportContext.componentDecl();
if (component) {
const runtime = identifier(component.identifier(1));
if (runtime !== "react")
loweringIssue(state, component, "unsupported-component-runtime", `Unsupported component runtime ${runtime}`);
components.push({
id: stringValue(component.stringLiteral()),
displayName: identifier(component.identifier(0)),
subject: new TypeSubstitution(state.types.environment()).application(
state.types.interface(component.interfaceType()),
),
runtime: "react",
});
return [];
}
if (exportContext.packageQuerySpecialization()) return []; if (exportContext.packageQuerySpecialization()) return [];
const query = exportContext.packageQuery(); const query = exportContext.packageQuery();
if (query) { if (query) {
@@ -1595,6 +1611,7 @@ const lowerPackage = (
displayName: alias, displayName: alias,
source, source,
exports, exports,
...(components.length ? { components } : {}),
...(queries.length ? { queries } : {}), ...(queries.length ? { queries } : {}),
...(queryTemplates.length ? { queryTemplates } : {}), ...(queryTemplates.length ? { queryTemplates } : {}),
...(queries.some((query) => query.argumentRequirements?.length) ...(queries.some((query) => query.argumentRequirements?.length)
@@ -2397,6 +2414,30 @@ const lowerWorkspace = (
: []; : [];
}); });
const presentations: NonNullable<WorkspaceRevision["presentations"]> = items.flatMap((item) => {
const presentation = item.presentationDecl();
if (!presentation) return [];
const packageName = identifier(presentation.identifier(1));
const componentName = identifier(presentation.identifier(2));
const pkg = requireSymbol(state, state.packages, packageName, presentation, "package");
const component = pkg?.definition?.components?.find((entry) => entry.displayName === componentName);
if (!component) {
loweringIssue(state, presentation, "unknown-component", `Unknown component ${packageName}.${componentName}`);
return [];
}
return [
{
id: stringValue(presentation.stringLiteral()),
displayName: identifier(presentation.identifier(0)),
subject: new TypeSubstitution(state.types.environment()).application(
state.types.interface(presentation.interfaceType()),
),
packageRevisionId: pkg!.revisionId,
componentId: component.id,
},
];
});
return { return {
imports, imports,
workspace: { workspace: {
@@ -2417,6 +2458,7 @@ const lowerWorkspace = (
), ),
conformances, conformances,
constructors, constructors,
...(presentations.length ? { presentations } : {}),
}, },
}; };
}; };
+86
View File
@@ -0,0 +1,86 @@
import type { InterfaceRevision, PackageController, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { canonicalJson } from "./evolution.js";
export function validateControllers(
controllers: readonly PackageController[],
interfaces: readonly InterfaceRevision[],
prefix: string,
): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [],
ids = new Set<string>();
const fail = (path: string, message: string) => issues.push({ code: "invalid-controller" as const, path, message });
for (const [index, controller] of controllers.entries()) {
const path = `${prefix}[${index}]`;
if (!controller.id.trim() || ids.has(controller.id))
fail(`${path}.id`, "Controller identities must be nonempty and unique within the package");
ids.add(controller.id);
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
if (!iface) {
fail(`${path}.subject`, "Controller subject must be an imported closed interface");
continue;
}
const operations = iface.members.flatMap((m) => m.operations);
const snapshot = operations.find((o) => o.id === controller.snapshot),
remote = operations.find((o) => o.id === controller.remote);
const unit = (value: unknown) => canonicalJson(value) === canonicalJson({ kind: "builtin", name: "unit" });
if (
!snapshot ||
snapshot.scope === "class" ||
snapshot.mode !== "call" ||
snapshot.execution?.kind !== "snapshot" ||
!snapshot.execution.targets.includes("server") ||
!unit(snapshot.inputType)
)
fail(`${path}.snapshot`, "Controller snapshot must be a server snapshot object call with unit input");
if (
!remote ||
remote.scope === "class" ||
remote.mode !== "call" ||
(remote.execution?.kind ?? "remote") !== "remote"
)
fail(`${path}.remote`, "Controller target must be a remote object call");
if (controller.input !== "unit" && controller.input !== "value")
fail(`${path}.input`, "Controller input must be unit or value");
else if (
remote &&
(controller.input === "unit"
? !unit(remote.inputType)
: snapshot && canonicalJson(snapshot.outputType) !== canonicalJson(remote.inputType))
)
fail(`${path}.input`, "Controller input mapping does not match the remote operation input");
}
return issues;
}
export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
return workspace.packageImports.flatMap((pkg, index) =>
validateControllers(pkg.controllers ?? [], workspace.interfaceImports, `packageImports[${index}].controllers`),
);
}
/** The file is declarative metadata, not an executable registration callback. */
export function parseControllerCatalog(text: string, interfaces: readonly InterfaceRevision[]): PackageController[] {
if (Buffer.byteLength(text) > 1048576) throw new Error("Controller catalog exceeds 1 MiB");
const value: unknown = JSON.parse(text);
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value);
if (
!record(value) ||
value.schemaVersion !== 1 ||
Object.keys(value).some((k) => !["schemaVersion", "controllers"].includes(k)) ||
!Array.isArray(value.controllers) ||
value.controllers.length > 1000
)
throw new Error("Invalid controller catalog: expected schemaVersion 1 and at most 1000 controllers");
const fields = ["id", "subject", "snapshot", "remote", "input"];
for (const entry of value.controllers)
if (
!record(entry) ||
Object.keys(entry).length !== fields.length ||
fields.some((key) => typeof entry[key] !== "string")
)
throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings");
const controllers = value.controllers as PackageController[];
const issues = validateControllers(controllers, interfaces, "controllers");
if (issues.length) throw new Error(issues.map((i) => `${i.path}: ${i.message}`).join("\n"));
return controllers;
}
+56
View File
@@ -0,0 +1,56 @@
import type { WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
/** Presentation metadata has no graph authority and cannot name arbitrary module URLs. */
export function validatePresentations(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-presentation", path, message });
const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry]));
const packages = new Map(workspace.packageImports.map((entry) => [entry.revisionId, entry]));
const text = (value: string, path: string) => {
if (!value.trim()) fail(path, "Presentation identities and names must be nonempty");
};
const subject = (id: string, path: string) => {
const contract = interfaces.get(id as WorkspaceRevision["interfaceImports"][number]["revisionId"]);
if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`);
};
for (const [index, pkg] of workspace.packageImports.entries()) {
const ids = new Set(pkg.exports.map((entry) => entry.id as string));
const names = new Set([
...pkg.exports.map((entry) => entry.displayName),
...(pkg.queries ?? []).map((entry) => entry.displayName),
...(pkg.queryTemplates ?? []).map((entry) => entry.declaration.displayName),
...(pkg.genericExports ?? []).map((entry) => entry.displayName),
]);
for (const [componentIndex, component] of (pkg.components ?? []).entries()) {
const path = `packageImports[${index}].components[${componentIndex}]`;
text(component.id, `${path}.id`);
text(component.displayName, `${path}.displayName`);
if (ids.has(component.id) || names.has(component.displayName))
fail(path, "Duplicate component identity or package member name");
ids.add(component.id);
names.add(component.displayName);
subject(component.subject, `${path}.subject`);
if (component.runtime !== "react") fail(`${path}.runtime`, "Unsupported component runtime");
}
}
const ids = new Set<string>(),
names = new Set<string>();
for (const [index, presentation] of (workspace.presentations ?? []).entries()) {
const path = `presentations[${index}]`;
text(presentation.id, `${path}.id`);
text(presentation.displayName, `${path}.displayName`);
if (ids.has(presentation.id) || names.has(presentation.displayName))
fail(path, "Duplicate presentation identity or name");
ids.add(presentation.id);
names.add(presentation.displayName);
subject(presentation.subject, `${path}.subject`);
const component = packages
.get(presentation.packageRevisionId)
?.components?.find((entry) => entry.id === presentation.componentId);
if (!component) fail(`${path}.componentId`, "Presentation names no installed package component");
else if (component.subject !== presentation.subject)
fail(`${path}.subject`, `Component requires ${component.subject}, not ${presentation.subject}`);
}
return issues;
}
+28
View File
@@ -288,7 +288,25 @@ export interface PackageConstructorExport extends PackageExportBase {
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport; export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
/** A window presentation entry, never a graph operation or service export. */
export interface PackageComponent {
id: string;
displayName: string;
subject: InterfaceRevisionId;
runtime: "react";
}
export interface WorkspacePresentation {
id: string;
displayName: string;
subject: InterfaceRevisionId;
packageRevisionId: PackageRevisionId;
componentId: string;
}
export interface PackageRevision { export interface PackageRevision {
controllers?: PackageController[];
components?: PackageComponent[];
queries?: import("../query/types.js").QueryDeclaration[]; queries?: import("../query/types.js").QueryDeclaration[];
checkedQueries?: import("../query/types.js").CheckedQuery[]; checkedQueries?: import("../query/types.js").CheckedQuery[];
queryTemplates?: import("../query/types.js").QueryTemplate[]; queryTemplates?: import("../query/types.js").QueryTemplate[];
@@ -304,6 +322,15 @@ export interface PackageRevision {
semanticMajor?: number; semanticMajor?: number;
} }
/** Checked adapter registration. Implementations remain ordinary object capabilities. */
export interface PackageController {
id: string;
subject: InterfaceRevisionId;
snapshot: OperationId;
remote: OperationId;
input: "unit" | "value";
}
export type DependencyBinding = export type DependencyBinding =
| { kind: "reflection"; selectors: string[]; via?: never } | { kind: "reflection"; selectors: string[]; via?: never }
| { | {
@@ -415,6 +442,7 @@ export interface AtomConstructorBinding {
} }
export interface WorkspaceRevision { export interface WorkspaceRevision {
presentations?: WorkspacePresentation[];
linkedQueries?: import("../query/link.js").LinkedQuery[]; linkedQueries?: import("../query/link.js").LinkedQuery[];
id: WorkspaceRevisionId; id: WorkspaceRevisionId;
workspaceId: WorkspaceId; workspaceId: WorkspaceId;
+10 -1
View File
@@ -1,5 +1,7 @@
import { isStorableType } from "./generics.js"; import { isStorableType } from "./generics.js";
import { validateInitialization } from "./initialization.js"; import { validateInitialization } from "./initialization.js";
import { validatePresentations } from "./presentations.js";
import { validateWorkspaceControllers } from "./controllers.js";
import { validateExecutionContracts } from "./execution.js"; import { validateExecutionContracts } from "./execution.js";
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js"; import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
import type { import type {
@@ -44,6 +46,8 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode = export type CapabilityValidationIssueCode =
| "invalid-controller"
| "invalid-presentation"
| "invalid-initialization" | "invalid-initialization"
| "invalid-execution-contract" | "invalid-execution-contract"
| "invalid-query-contract" | "invalid-query-contract"
@@ -2213,7 +2217,12 @@ export const validateWorkspaceRevision = (workspace: WorkspaceRevision): Capabil
validatePackages(workspace, issues, indexes); validatePackages(workspace, issues, indexes);
validateConformances(workspace, issues, indexes); validateConformances(workspace, issues, indexes);
validateConstructors(workspace, issues, indexes); validateConstructors(workspace, issues, indexes);
issues.push(...validateExecutionContracts(workspace), ...validateInitialization(workspace)); issues.push(
...validateExecutionContracts(workspace),
...validateInitialization(workspace),
...validatePresentations(workspace),
...validateWorkspaceControllers(workspace),
);
return issues; return issues;
}; };
+110 -21
View File
@@ -27,6 +27,24 @@ export interface PackageArtifacts {
sourceDigest: string; sourceDigest: string;
versions: ExecutionPlan["definition"]["versions"]; versions: ExecutionPlan["definition"]["versions"];
implementations: ArtifactImplementation[]; implementations: ArtifactImplementation[];
components?: ComponentArtifact[];
}
export const componentSharedModules = [
"react",
"react-dom",
"react-dom/client",
"react/jsx-runtime",
"@quixos/camino-react",
] as const;
export interface ComponentArtifact {
id: string;
subject: string;
compatibility: "quixos-react-component-v1";
path: string;
exportName: string;
sharedModules: Record<(typeof componentSharedModules)[number], string>;
files: ArtifactFile[];
contentDigest: string;
} }
export interface ImplementationSelection { export interface ImplementationSelection {
target: string; target: string;
@@ -119,12 +137,73 @@ export function implementationDigest(implementation: Omit<ArtifactImplementation
files: [...implementation.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)), files: [...implementation.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
}); });
} }
export function componentArtifactDigest(component: Omit<ComponentArtifact, "contentDigest">): string {
return hexDigest({
...component,
files: [...component.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
});
}
function parseArtifactFiles(value: unknown, path: string): ArtifactFile[] {
const files = array(value, path).map((value): ArtifactFile => {
const file = object(value, path);
keys(file, ["path", "digest", "bytes", "mediaType"], [], path);
if (!Number.isSafeInteger(file.bytes) || (file.bytes as number) < 0 || (file.bytes as number) > 256 * 1024 * 1024)
fail(path, "Artifact file exceeds size bound");
return {
path: artifactPath(file.path),
digest: digest(file.digest, path),
bytes: file.bytes as number,
mediaType: text(file.mediaType, path),
};
});
unique(
files.map((file) => file.path),
path,
);
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
}
function parseComponent(value: unknown, index: number): ComponentArtifact {
const path = `components[${index}]`,
entry = object(value, path);
keys(
entry,
["id", "subject", "compatibility", "path", "exportName", "sharedModules", "files", "contentDigest"],
[],
path,
);
if (entry.compatibility !== "quixos-react-component-v1") fail(path, "Unsupported component ABI");
const files = parseArtifactFiles(entry.files, `${path}.files`);
const shared = object(entry.sharedModules, `${path}.sharedModules`);
keys(shared, [...componentSharedModules], [], `${path}.sharedModules`);
const sharedModules = Object.fromEntries(
componentSharedModules.map((name) => [name, artifactPath(shared[name])]),
) as ComponentArtifact["sharedModules"];
const entryPath = artifactPath(entry.path),
exportName = text(entry.exportName, path);
if (!/^[$A-Z_a-z][$\w]*$/.test(exportName)) fail(path, "Expected a JavaScript component export name");
for (const modulePath of [entryPath, ...Object.values(sharedModules)])
if (!files.some((file) => file.path === modulePath && file.mediaType === "text/javascript"))
fail(path, "Component entry and shared modules require declared JavaScript files");
const component: ComponentArtifact = {
id: text(entry.id, path),
subject: text(entry.subject, path),
compatibility: "quixos-react-component-v1",
path: entryPath,
exportName,
sharedModules,
files,
contentDigest: digest(entry.contentDigest, path),
};
const { contentDigest, ...body } = component;
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
return component;
}
export function parsePackageArtifacts(value: unknown): PackageArtifacts { export function parsePackageArtifacts(value: unknown): PackageArtifacts {
const record = object(value, "manifest"); const record = object(value, "manifest");
keys( keys(
record, record,
["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"], ["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"],
[], ["components"],
"manifest", "manifest",
); );
if (record.schemaVersion !== 1) fail("manifest", "Unsupported artifact manifest version"); if (record.schemaVersion !== 1) fail("manifest", "Unsupported artifact manifest version");
@@ -152,26 +231,7 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
if (entry.compatibility === "quixos-js-module-v1") text(entry.registryExport, `${path}.registryExport`); if (entry.compatibility === "quixos-js-module-v1") text(entry.registryExport, `${path}.registryExport`);
const exports = array(entry.exports, `${path}.exports`).map((value) => text(value, `${path}.exports`)); const exports = array(entry.exports, `${path}.exports`).map((value) => text(value, `${path}.exports`));
unique(exports, `${path}.exports`); unique(exports, `${path}.exports`);
const files = array(entry.files, `${path}.files`).map((value): ArtifactFile => { const files = parseArtifactFiles(entry.files, `${path}.files`);
const file = object(value, `${path}.file`);
keys(file, ["path", "digest", "bytes", "mediaType"], [], `${path}.file`);
if (
!Number.isSafeInteger(file.bytes) ||
(file.bytes as number) < 0 ||
(file.bytes as number) > 256 * 1024 * 1024
)
fail(path, "Artifact file exceeds size bound");
return {
path: artifactPath(file.path),
digest: digest(file.digest, path),
bytes: file.bytes as number,
mediaType: text(file.mediaType, path),
};
});
unique(
files.map((file) => file.path),
`${path}.files`,
);
const entryPath = artifactPath(entry.path); const entryPath = artifactPath(entry.path);
if (!files.some((file) => file.path === entryPath)) fail(path, "Entrypoint is not covered by the file manifest"); if (!files.some((file) => file.path === entryPath)) fail(path, "Entrypoint is not covered by the file manifest");
const result: ArtifactImplementation = { const result: ArtifactImplementation = {
@@ -194,6 +254,13 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
implementations.map((implementation) => implementation.id), implementations.map((implementation) => implementation.id),
"implementations", "implementations",
); );
const components =
record.components === undefined ? undefined : array(record.components, "components").map(parseComponent);
if (components)
unique(
components.map((component) => component.id),
"components",
);
return { return {
schemaVersion: 1, schemaVersion: 1,
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"), packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
@@ -201,11 +268,20 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
sourceDigest: digest(record.sourceDigest, "sourceDigest"), sourceDigest: digest(record.sourceDigest, "sourceDigest"),
versions: versions as unknown as PackageArtifacts["versions"], versions: versions as unknown as PackageArtifacts["versions"],
implementations, implementations,
...(components ? { components } : {}),
}; };
} }
function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void { function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void {
if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest) if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest)
fail(expected.revision, "Substituted binding schema or source revision"); fail(expected.revision, "Substituted binding schema or source revision");
for (const component of manifest.components ?? []) {
const declaration = expected.components?.find((entry) => entry.id === component.id);
if (!declaration || declaration.subject !== component.subject || declaration.runtime !== "react")
fail(component.id, "Component differs from its declared subject/runtime contract");
}
for (const component of expected.components ?? [])
if (!manifest.components?.some((entry) => entry.id === component.id))
fail(component.id, "Missing component artifact");
for (const implementation of manifest.implementations) for (const implementation of manifest.implementations)
for (const id of implementation.exports) { for (const id of implementation.exports) {
const entry = expected.exports.find((e) => e.id === id); const entry = expected.exports.find((e) => e.id === id);
@@ -272,6 +348,19 @@ export async function checkExecutionWorld(
digest: implementation.contentDigest, digest: implementation.contentDigest,
}); });
} }
for (const component of manifest.components ?? []) {
for (const file of component.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file))
fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
artifacts.push({
binding: JSON.stringify([expected.revision, "component", component.id]),
target: "browser-renderer",
digest: component.contentDigest,
});
}
for (const file of files.values()) { for (const file of files.values()) {
const bytes = await read(expected.revision, file.path, file.bytes); const bytes = await read(expected.revision, file.path, file.bytes);
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest) if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
+122
View File
@@ -0,0 +1,122 @@
import type {
InterfaceRevision,
InterfaceOperation,
ExecutionContract,
PackageRevision,
} from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { portableValueSchema, tsType } from "../bindings/portable.js";
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
export interface PublicOperationContract {
interface: string;
operation: string;
scope: "object" | "class";
execution: ExecutionContract;
input: unknown;
output: unknown;
}
export function publicOperationContract(
iface: InterfaceRevision,
operation: InterfaceOperation,
): PublicOperationContract {
if (iface.template || operation.mode !== "call" || !iface.members.some((m) => m.operations.includes(operation)))
throw new ExecutionPlanError(
"UNSUPPORTED_CLIENT_CONTRACT",
iface.revisionId,
"Expected a closed interface call operation",
);
const execution = operation.execution ?? { kind: "remote" };
return {
interface: iface.revisionId,
operation: operation.id,
scope: operation.scope ?? "object",
execution: execution.kind === "remote" ? execution : { ...execution, targets: [...execution.targets].sort() },
input: portableValueSchema(operation.inputType),
output: portableValueSchema(operation.outputType),
};
}
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
if (!iface || !operation || canonicalJson(publicOperationContract(iface, operation)) !== canonicalJson(contract))
throw new ExecutionPlanError(
"CLIENT_CONTRACT_MISMATCH",
`${contract.interface}/${contract.operation}`,
"Installed interface differs from the imported operation contract",
);
return resolvePlannedOperation(plan, atom, contract.interface, contract.operation);
}
export interface PublicQueryContract {
package: string;
query: string;
definitionDigest: string;
root: { kind: "object" | "workspace-interface"; interface: string };
placement: "server" | "replica" | "local";
sourceQuery?: string;
input: unknown;
output: unknown;
}
export function publicQueryContract(pkg: PackageRevision, queryId: string): PublicQueryContract {
const query = pkg.checkedQueries?.find((q) => q.declaration.id === queryId);
if (!query)
throw new ExecutionPlanError("UNCHECKED_QUERY", `${pkg.revisionId}/${queryId}`, "Expected a checked package query");
const placement = query.declaration.placement;
return {
package: pkg.revisionId,
query: queryId,
definitionDigest: query.definitionDigest,
root: query.declaration.root,
placement: placement?.kind ?? "server",
...(placement?.kind === "local" ? { sourceQuery: placement.sourceQueryId } : {}),
input: portableValueSchema(query.variables),
output: portableValueSchema(query.output),
};
}
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
throw new ExecutionPlanError(
"CLIENT_QUERY_MISMATCH",
`${contract.package}/${contract.query}`,
"Installed query differs from the imported query contract",
);
const query = plan.definition.queries.find(
(q) => q.query.packageRevisionId === contract.package && q.query.checked.declaration.id === contract.query,
);
if (!query) throw new ExecutionPlanError("UNINSTALLED_QUERY", contract.query, "No installed query binding");
return query;
}
/** Generate only the explicitly imported interfaces. No concrete atom inventory is needed. */
export function generateInterfaceClientBindings(
imports: Readonly<Record<string, InterfaceRevision>>,
runtimeModule = "@quixos/camino-replica-core",
): string {
const entries = Object.entries(imports).map(([alias, iface]) => {
if (iface.template)
throw new ExecutionPlanError(
"OPEN_CLIENT_CONTRACT",
alias,
"Specialize the interface before generating a client",
);
const members = iface.members.map((member) => {
const operations = member.operations
.filter((op) => op.mode === "call")
.map(
(op) =>
`${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}>(${JSON.stringify(publicOperationContract(iface, op))})`,
);
return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`;
});
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
});
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
}
+1
View File
@@ -3,6 +3,7 @@ export * from "./artifacts.js";
export * from "./ports.js"; export * from "./ports.js";
export * from "./files.js"; export * from "./files.js";
export * from "./installation.js"; export * from "./installation.js";
export * from "./client-contracts.js";
export * from "./receipts.js"; export * from "./receipts.js";
+119 -11
View File
@@ -1,8 +1,73 @@
import path from "node:path"; import path from "node:path";
import { loadCheckedArtifactWorld } from "./receipts.js"; import { loadCheckedArtifactWorld } from "./receipts.js";
import { contentArtifactUrl } from "./files.js"; import { contentArtifactUrl } from "./files.js";
import { selectArtifactImplementation, type ArtifactImplementation } from "./artifacts.js"; import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
import { ExecutionPlanError, type ExecutionPlan } from "./plan.js"; import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
import {
publicOperationContract,
publicQueryContract,
type PublicOperationContract,
type PublicQueryContract,
} from "./client-contracts.js";
/** Activation is assigned by graph authority, never by a package artifact. */
export interface ActiveExecutionIdentity {
readonly workspace: string;
readonly world: string;
readonly plan: string;
readonly epoch: string;
}
/** Host-owned execution selection. The lifecycle manager fills its checked inputs. */
export interface ExecutionGroupIdentity {
readonly logicalId: string;
readonly executionDigest: string;
}
export interface ClientContractCatalog {
readonly operations: readonly PublicOperationContract[];
readonly queries: readonly PublicQueryContract[];
readonly components: readonly InstalledComponent[];
readonly presentations: readonly { id: string; subject: string; packageRevision: string; component: string }[];
}
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
export function clientContractCatalog(
plan: ExecutionPlan,
installation: ExecutionInstallation,
requestedPackages: readonly string[],
): ClientContractCatalog {
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError(
"CATALOG_PLAN_MISMATCH",
plan.digest,
"Installation and public catalog must use the same checked plan",
);
const packages = requestedPackages.map((revision) => {
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === revision);
if (!pkg)
throw new ExecutionPlanError("UNINSTALLED_PACKAGE", revision, "Requested catalog package is not installed");
return pkg;
});
// Interface metadata contains no authority or private implementation configuration.
// A later admission optimization can narrow this set without changing operation identities.
const operations = plan.definition.workspace.interfaceImports
.filter((i) => !i.template)
.flatMap((iface) =>
iface.members.flatMap((member) =>
member.operations
.filter((o) => o.mode === "call")
.map((operation) => publicOperationContract(iface, operation)),
),
);
return {
operations,
queries: packages.flatMap((pkg) =>
(pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)),
),
components: installation.components.filter((c) => requestedPackages.includes(c.packageRevision)),
presentations: (plan.definition.presentations ?? [])
.filter((p) => requestedPackages.includes(p.packageRevisionId))
.map((p) => ({ id: p.id, subject: p.subject, packageRevision: p.packageRevisionId, component: p.componentId })),
};
}
export interface PackageInstallationSelection { export interface PackageInstallationSelection {
readonly browser?: { readonly id?: string }; readonly browser?: { readonly id?: string };
@@ -17,7 +82,7 @@ export interface ExecutionInstallation {
readonly packages: readonly { readonly packages: readonly {
readonly revision: string; readonly revision: string;
readonly root: string; readonly root: string;
readonly service: ArtifactImplementation; readonly service?: ArtifactImplementation;
readonly module?: ArtifactImplementation; readonly module?: ArtifactImplementation;
readonly browserURL?: string; readonly browserURL?: string;
readonly browserRegistry?: string; readonly browserRegistry?: string;
@@ -25,6 +90,17 @@ export interface ExecutionInstallation {
readonly routes: Readonly<Record<string, { readonly path: string; readonly mediaType: string }>>; readonly routes: Readonly<Record<string, { readonly path: string; readonly mediaType: string }>>;
readonly coreURL?: string; readonly coreURL?: string;
readonly engineURL?: string; readonly engineURL?: string;
readonly components: readonly InstalledComponent[];
}
/** Safe to expose in a presentation catalog: no source paths, state slots, or host configuration. */
export interface InstalledComponent {
readonly packageRevision: string;
readonly id: string;
readonly subject: string;
readonly url: string;
readonly exportName: string;
readonly compatibility: ComponentArtifact["compatibility"];
readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>;
} }
/** Verify candidate receipts before assembling process entrypoints and browser assets. /** Verify candidate receipts before assembling process entrypoints and browser assets.
* This grants no authority to activate a world or run optional modules in process. */ * This grants no authority to activate a world or run optional modules in process. */
@@ -43,18 +119,49 @@ export async function loadExecutionInstallation(
fail(revision, "Selection names a package outside this checked world"); fail(revision, "Selection names a package outside this checked world");
const routes: Record<string, { path: string; mediaType: string }> = Object.create(null); const routes: Record<string, { path: string; mediaType: string }> = Object.create(null);
const packages: ExecutionInstallation["packages"][number][] = []; const packages: ExecutionInstallation["packages"][number][] = [];
const components: InstalledComponent[] = [];
const sharedComponentModules = new Map<string, string>();
let coreURL: string | undefined, engineURL: string | undefined; let coreURL: string | undefined, engineURL: string | undefined;
for (const manifest of loaded.manifests) { for (const manifest of loaded.manifests) {
const revision = manifest.packageRevisionId; const revision = manifest.packageRevisionId;
const root = roots[revision]!; const root = roots[revision]!;
const selection = selections[revision] ?? {}; const selection = selections[revision] ?? {};
const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports; const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports;
const service = selectArtifactImplementation(manifest, { const service =
...selection.service, exports.length || selection.service
target: "server", ? selectArtifactImplementation(manifest, {
compatibility: "quixos-service-v1", ...selection.service,
exports: exports.map((entry) => entry.id), target: "server",
}); compatibility: "quixos-service-v1",
exports: exports.map((entry) => entry.id),
})
: undefined;
for (const component of manifest.components ?? []) {
const sharedModules = Object.fromEntries(
Object.entries(component.sharedModules).map(([name, filePath]) => {
const file = component.files.find((file) => file.path === filePath)!;
const url = contentArtifactUrl(file),
previous = sharedComponentModules.get(name);
if (previous && previous !== url) fail(component.id, `Components disagree on shared module ${name}`);
sharedComponentModules.set(name, url);
return [name, url];
}),
) as InstalledComponent["sharedModules"];
for (const file of component.files) {
const url = contentArtifactUrl(file);
if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types");
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
}
components.push({
packageRevision: revision,
id: component.id,
subject: component.subject,
compatibility: component.compatibility,
exportName: component.exportName,
sharedModules,
url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!),
});
}
const module = selection.module const module = selection.module
? selectArtifactImplementation(manifest, { ? selectArtifactImplementation(manifest, {
...selection.module, ...selection.module,
@@ -68,7 +175,7 @@ export async function loadExecutionInstallation(
); );
if (!browserExports.length) { if (!browserExports.length) {
if (selection.browser) fail(revision, "Package has no browser exports to select"); if (selection.browser) fail(revision, "Package has no browser exports to select");
packages.push({ revision, root, service, ...(module ? { module } : {}) }); packages.push({ revision, root, ...(service ? { service } : {}), ...(module ? { module } : {}) });
continue; continue;
} }
const browser = selectArtifactImplementation(manifest, { const browser = selectArtifactImplementation(manifest, {
@@ -97,7 +204,7 @@ export async function loadExecutionInstallation(
packages.push({ packages.push({
revision, revision,
root, root,
service, ...(service ? { service } : {}),
...(module ? { module } : {}), ...(module ? { module } : {}),
browserURL: contentArtifactUrl(entry), browserURL: contentArtifactUrl(entry),
browserRegistry: browser.registryExport, browserRegistry: browser.registryExport,
@@ -108,6 +215,7 @@ export async function loadExecutionInstallation(
plan: plan.digest, plan: plan.digest,
world: loaded.digest, world: loaded.digest,
packages, packages,
components,
routes, routes,
...(coreURL ? { coreURL, engineURL } : {}), ...(coreURL ? { coreURL, engineURL } : {}),
}; };
+6
View File
@@ -58,6 +58,8 @@ export interface PlannedQuery {
admittedOperations: string[]; admittedOperations: string[];
} }
export interface PlanPackage { export interface PlanPackage {
controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>;
components?: NonNullable<WorkspaceRevision["packageImports"][number]["components"]>;
revision: string; revision: string;
source: WorkspaceRevision["packageImports"][number]["source"]; source: WorkspaceRevision["packageImports"][number]["source"];
sourceDigest: string; sourceDigest: string;
@@ -65,6 +67,7 @@ export interface PlanPackage {
exports: { id: string; execution: ExecutionContract }[]; exports: { id: string; execution: ExecutionContract }[];
} }
export interface ExecutionPlanDefinition { export interface ExecutionPlanDefinition {
presentations?: NonNullable<WorkspaceRevision["presentations"]>;
codec: "camino-execution-plan-v1"; codec: "camino-execution-plan-v1";
workspace: WorkspaceRevision; workspace: WorkspaceRevision;
bindingSchema: BindingSchema; bindingSchema: BindingSchema;
@@ -421,11 +424,14 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
codec: "camino-execution-plan-v1", codec: "camino-execution-plan-v1",
workspace, workspace,
bindingSchema: normalizedSchema, bindingSchema: normalizedSchema,
...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}),
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => ({ packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => ({
revision: p.revisionId, revision: p.revisionId,
source: p.source, source: p.source,
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }), sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
bindingDigest: hexDigest(normalizedSchema), bindingDigest: hexDigest(normalizedSchema),
...(p.controllers?.length ? { controllers: sorted(p.controllers, (controller) => controller.id) } : {}),
...(p.components?.length ? { components: sorted(p.components, (component) => component.id) } : {}),
exports: sorted(p.exports, (e) => e.id).map((e) => ({ id: e.id, execution: e.execution ?? { kind: "remote" } })), exports: sorted(p.exports, (e) => e.id).map((e) => ({ id: e.id, execution: e.execution ?? { kind: "remote" } })),
})), })),
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => { owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {
+121
View File
@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
import {
compileExecutionPlan,
generateInterfaceClientBindings,
publicOperationContract,
linkInterfaceOperation,
publicQueryContract,
linkPackageQuery,
} from "../src/execution-world/index.js";
import { compileQuery } from "../src/query/compile.js";
import { parseControllerCatalog, validateControllers } from "../src/capability-model/controllers.js";
const source = { repository: "https://example.test/contracts.git", commit: "a".repeat(40) };
const parsed = compileCapabilityResourceSource(
`interface Data id "data" revision "data@1" {
queryable portable value label id "label" : string { get id "label:get" execution snapshot targets [browser, server]; }
operation publish id "publish" : string -> unit { call id "publish:call"; }
}`,
{ source },
);
assert.ok(parsed.ok && parsed.resource.kind === "interface", JSON.stringify(parsed.diagnostics));
const iface = parsed.resource.revision;
const environment = { interfaces: new Map([["Data", iface]]), interfaceClosure: [iface] };
const compile = async () => {
const pkg = compileCapabilityResourceSource(
`import interface Data; package Views id "views" revision "views@1" {
query Labels id "labels" root workspace Data document "labels.graphql" operation "Labels" { replica; }
operation publish id "export:publish" : string -> unit mode call receiver any;
}`,
{ source, environment },
);
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
const revision = pkg.resource.revision;
revision.checkedQueries = [
await compileQuery(
revision.queries![0]!,
[iface],
async () => "query Labels { root { objects(first: 10) { entries { node { label } } } } }",
),
];
const result = compileCapabilitySource(
`workspace Test id "test" revision "test@1" commit "${source.commit}" {
import interface Data; import package Views;
atom One id "one"; atom Two id "two";
${["One", "Two"]
.map(
(atom) => `conform ${atom} as Data id "${atom}:data" {
private state ${atom}Label id "${atom}:label" on ${atom} : string policy register default "";
bind label.get to state ${atom}Label.read;
bind publish.call to package Views.publish;
}`,
)
.join("\n")}
}`,
"workspace.qx",
{ ...environment, packages: new Map([["Views", revision]]) },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
return compileExecutionPlan(result.workspace);
};
test("one imported operation contract links to two atom implementations without containing either", async () => {
const plan = await compile();
const contract = publicOperationContract(iface, iface.members[0]!.operations[0]!);
assert.equal(linkInterfaceOperation(plan, "one", contract).atomId, "one");
assert.equal(linkInterfaceOperation(plan, "two", contract).atomId, "two");
const generated = generateInterfaceClientBindings({ Data: iface });
assert.ok(generated.includes("defineInterfaceOperation<null, string>"));
for (const privateValue of [plan.digest, "One:label", '"atomId"', '"binding"'])
assert.equal(generated.includes(privateValue), false);
assert.throws(() => linkInterfaceOperation(plan, "absent", contract), /UNINSTALLED_OPERATION/);
for (const change of [{ scope: "class" }, { input: { kind: "string" } }, { execution: { kind: "remote" } }])
assert.throws(
() => linkInterfaceOperation(plan, "one", { ...contract, ...change } as typeof contract),
/CLIENT_CONTRACT_MISMATCH/,
);
assert.throws(() => linkInterfaceOperation({ ...plan, digest: "bad" }, "one", contract), /INVALID_PLAN/);
});
test("package query handles bind the checked definition, placement and codecs", async () => {
const plan = await compile();
const contract = publicQueryContract(plan.definition.workspace.packageImports[0]!, "labels");
assert.equal(linkPackageQuery(plan, contract).query.checked.declaration.id, "labels");
for (const change of [{ placement: "local" }, { definitionDigest: "bad" }, { output: null }])
assert.throws(() => linkPackageQuery(plan, { ...contract, ...change } as typeof contract), /CLIENT_QUERY_MISMATCH/);
});
test("controller templates check object capability effects and exact input mapping", () => {
const catalog = {
schemaVersion: 1,
controllers: [
{ id: "publish-label", subject: "data@1", snapshot: "label:get", remote: "publish:call", input: "value" },
],
};
assert.equal(parseControllerCatalog(JSON.stringify(catalog), [iface]).length, 1);
for (const change of [
{ input: "unit" },
{ input: "callback" },
{ subject: "missing" },
{ snapshot: "publish:call" },
{ remote: "label:get" },
{ url: "https://example.test" },
]) {
const invalid = { ...catalog, controllers: [{ ...catalog.controllers[0], ...change }] };
assert.throws(() => parseControllerCatalog(JSON.stringify(invalid), [iface]), /Controller|controller/);
}
assert.throws(
() =>
parseControllerCatalog(
JSON.stringify({ ...catalog, controllers: [...catalog.controllers, ...catalog.controllers] }),
[iface],
),
/unique/,
);
const classIface = structuredClone(iface);
classIface.members[1]!.operations[0]!.scope = "class";
assert.ok(
validateControllers(parseControllerCatalog(JSON.stringify(catalog), [iface]), [classIface], "controllers").length,
);
});
+230
View File
@@ -0,0 +1,230 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
compileCapabilityResourceSource,
compileCapabilitySource,
formatQx,
parseQx,
walkSyntax,
} from "../src/capability-language/index.js";
import {
compileExecutionPlan,
checkExecutionWorld,
componentArtifactDigest,
componentSharedModules,
expectedArtifactReceipt,
loadExecutionInstallation,
clientContractCatalog,
type ComponentArtifact,
type PackageArtifacts,
} from "../src/execution-world/index.js";
import { validateWorkspaceRevision } from "../src/capability-model/index.js";
const source = { repository: "https://example.test/components.git", commit: "a".repeat(40) };
const contract = (name: string) => {
const result = compileCapabilityResourceSource(
`interface ${name} id "interface:${name}" revision "interface:${name}@1" { }`,
{ source },
);
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
return result.resource.revision;
};
const data = contract("Data"),
other = contract("Other");
const interfaces = new Map([
["Data", data],
["Other", other],
]);
const componentSource = (body = 'component Card id "component:card" subject Data runtime react;') =>
`import interface Data; package UI id "package:ui" revision "package:ui@1" { ${body} }`;
const compilePackage = (body?: string) =>
compileCapabilityResourceSource(componentSource(body), {
source,
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
});
const workspace = (presentation = 'presentation Card id "presentation:card" for Data using UI.Card;') => {
const pkg = compilePackage();
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
return compileCapabilitySource(
`workspace Example id "workspace:example" revision "workspace:example@1" commit "${source.commit}" {
import interface Data; import interface Other; import package UI;
atom One id "atom:one"; atom Two id "atom:two";
conform One as Data id "conformance:one:data" { }
conform Two as Data id "conformance:two:data" { }
${presentation}
}`,
"workspace.qx",
{
interfaces,
interfaceClosure: [...interfaces.values()],
packages: new Map([["UI", pkg.resource.revision]]),
},
);
};
test("interface-only component declarations serve multiple conforming atoms without capability exports", () => {
const result = workspace();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const plan = compileExecutionPlan(result.workspace);
assert.equal(plan.definition.packages[0]!.exports.length, 0);
assert.deepEqual(plan.definition.packages[0]!.components, [
{
id: "component:card",
displayName: "Card",
subject: "interface:Data@1",
runtime: "react",
},
]);
assert.equal(plan.definition.presentations?.[0]?.componentId, "component:card");
assert.equal(plan.definition.workspace.conformances.length, 2);
assert.equal(plan.definition.operations.length, 0);
});
test("component declarations survive formatting and syntax inspection", () => {
const text = formatQx(componentSource());
const parsed = parseQx(text);
assert.deepEqual(parsed.diagnostics, []);
assert.ok([...walkSyntax(parsed.root)].some((node) => node.kind === "componentDecl"));
assert.equal(formatQx(text), text);
});
test("component and presentation contracts reject wrong subjects, duplicates and unavailable runtimes", () => {
for (const [input, message] of [
['component Card id "component:card" subject Missing runtime react;', /Missing/],
['component Card id "component:card" subject Data runtime arduino;', /Unsupported component runtime/],
[
'component Card id "component:card" subject Data runtime react; component Other id "component:card" subject Data runtime react;',
/Duplicate component/,
],
[
'component Card id "component:card" subject Data runtime react; component Card id "component:other" subject Data runtime react;',
/Duplicate component/,
],
] as const) {
const result = compilePackage(input);
assert.equal(result.ok, false);
assert.match(JSON.stringify(result.diagnostics), message);
}
for (const [input, message] of [
['presentation Card id "presentation:card" for Other using UI.Card;', /Component requires/],
['presentation Card id "presentation:card" for Data using UI.Missing;', /Unknown component/],
[
'presentation Card id "presentation:card" for Data using UI.Card; presentation Duplicate id "presentation:card" for Data using UI.Card;',
/Duplicate presentation/,
],
] as const) {
const result = workspace(input);
assert.equal(result.ok, false);
assert.match(JSON.stringify(result.diagnostics), message);
}
});
test("model validation rechecks presentation metadata instead of trusting the source parser", () => {
const result = workspace();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
result.workspace.presentations![0]!.componentId = "missing";
assert.ok(validateWorkspaceRevision(result.workspace).some((issue) => issue.code === "invalid-presentation"));
assert.throws(() => compileExecutionPlan(result.workspace), /no installed package component/);
});
test("checked component-only artifacts install without a service and reject missing or substituted entries", async () => {
const result = workspace();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const plan = compileExecutionPlan(result.workspace),
pkg = plan.definition.packages[0]!;
const bytes = new TextEncoder().encode("export const Card = () => null;\n");
const paths = [
"components/card.mjs",
...componentSharedModules.map((_name, i) => `share/quixos/shared/runtime-${i}.mjs`),
];
const files = paths.map((file) => ({
path: file,
digest: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
mediaType: "text/javascript",
}));
const body: Omit<ComponentArtifact, "contentDigest"> = {
id: "component:card",
subject: "interface:Data@1",
compatibility: "quixos-react-component-v1",
path: paths[0]!,
exportName: "Card",
files,
sharedModules: Object.fromEntries(
componentSharedModules.map((name, i) => [name, paths[i + 1]!]),
) as ComponentArtifact["sharedModules"],
};
const manifest: PackageArtifacts = {
schemaVersion: 1,
packageRevisionId: pkg.revision,
bindingDigest: pkg.bindingDigest,
sourceDigest: pkg.sourceDigest,
versions: plan.definition.versions,
implementations: [],
components: [{ ...body, contentDigest: componentArtifactDigest(body) }],
};
const world = await checkExecutionWorld(plan, [manifest], async () => bytes);
assert.equal(world.artifacts.length, 1);
assert.equal(world.artifacts[0]!.target, "browser-renderer");
await assert.rejects(
checkExecutionWorld(plan, [{ ...manifest, components: [] }], async () => bytes),
/Missing component artifact/,
);
const substitute = structuredClone(manifest);
substitute.components![0]!.subject = "interface:Other@1";
const { contentDigest: _digest, ...changed } = substitute.components![0]!;
substitute.components![0]!.contentDigest = componentArtifactDigest(changed);
await assert.rejects(
checkExecutionWorld(plan, [substitute], async () => bytes),
/declared subject/,
);
await assert.rejects(
checkExecutionWorld(plan, [manifest], async () => new Uint8Array(bytes.length)),
/Artifact bytes/,
);
const missingShared = structuredClone(manifest);
delete (missingShared.components![0]!.sharedModules as Partial<ComponentArtifact["sharedModules"]>).react;
await assert.rejects(
checkExecutionWorld(plan, [missingShared], async () => bytes),
/missing fields/,
);
const root = await mkdtemp(path.join(os.tmpdir(), "camino-component-contract-"));
try {
for (const file of files) {
await mkdir(path.dirname(path.join(root, file.path)), { recursive: true });
await writeFile(path.join(root, file.path), bytes);
}
await mkdir(path.join(root, "share/quixos"), { recursive: true });
await writeFile(path.join(root, "share/quixos/package-artifacts.json"), JSON.stringify(manifest));
const generator = "a".repeat(64);
const { receipt, sources } = expectedArtifactReceipt(plan, manifest, generator);
for (const [file, content] of sources) {
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
await writeFile(path.join(root, file), content);
}
await writeFile(path.join(root, "share/quixos/build-receipt.json"), JSON.stringify(receipt));
const installation = await loadExecutionInstallation(plan, { [pkg.revision]: root }, generator);
assert.equal(installation.packages[0]!.service, undefined);
assert.equal(installation.components[0]!.id, "component:card");
assert.ok(installation.routes[installation.components[0]!.url]);
assert.equal(JSON.stringify(installation.components).includes(root), false);
assert.equal(JSON.stringify(installation.components).includes("slot:"), false);
const catalog = clientContractCatalog(plan, installation, [pkg.revision]);
assert.equal(catalog.presentations[0]!.component, "component:card");
assert.equal(catalog.components[0]!.subject, "interface:Data@1");
assert.equal(JSON.stringify(catalog).includes(root), false);
assert.equal(JSON.stringify(catalog).includes("bindingSchema"), false);
assert.throws(() => clientContractCatalog(plan, installation, ["absent"]), /UNINSTALLED_PACKAGE/);
assert.throws(
() => clientContractCatalog(plan, { ...installation, plan: "bad" }, [pkg.revision]),
/CATALOG_PLAN_MISMATCH/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});