Add explicit CRDT edit contracts and structured query values

This commit is contained in:
Timothy J. Aveni
2026-09-19 22:33:20 -07:00
parent 0555c929b1
commit 4c0c404a39
27 changed files with 3272 additions and 2745 deletions
+20
View File
@@ -107,6 +107,7 @@ valueMember
valueMemberOperation valueMemberOperation
: GET ID stringLiteral executionClause? SEMI : GET ID stringLiteral executionClause? SEMI
| SET ID stringLiteral executionClause? SEMI | SET ID stringLiteral executionClause? SEMI
| EDIT ID stringLiteral executionClause? SEMI
| WATCH START ID stringLiteral STOP ID stringLiteral SEMI | WATCH START ID stringLiteral STOP ID stringLiteral SEMI
; ;
@@ -234,6 +235,7 @@ primitiveList
primitive primitive
: READ : READ
| WRITE | WRITE
| EDIT
| RESOLVE | RESOLVE
| CONNECT | CONNECT
| DISCONNECT | DISCONNECT
@@ -316,6 +318,7 @@ operationName
| CALL | CALL
| GET | GET
| SET | SET
| EDIT
| RESOLVE | RESOLVE
| CONNECT | CONNECT
| DISCONNECT | DISCONNECT
@@ -335,6 +338,7 @@ operationProvider
statePrimitive statePrimitive
: READ : READ
| WRITE | WRITE
| EDIT
| WATCH_START | WATCH_START
| WATCH_STOP | WATCH_STOP
; ;
@@ -426,6 +430,21 @@ jsonArray
identifier identifier
: IDENTIFIER : IDENTIFIER
| START
| STOP
| EDIT
| CREATION
| INITIALIZE
| EXECUTION
| SNAPSHOT
| TRANSACTION
| REMOTE
| TARGETS
| PORTABLE
| REPLICA
| LOCAL
| FROM
| REGISTER
| INTERFACES | INTERFACES
| REFLECTION | REFLECTION
| SOURCE | SOURCE
@@ -449,6 +468,7 @@ stringLiteral
: STRING_LITERAL : STRING_LITERAL
; ;
EDIT: 'edit';
CREATION: 'creation'; CREATION: 'creation';
INITIALIZE: 'initialize'; INITIALIZE: 'initialize';
WORKSPACE: 'workspace'; WORKSPACE: 'workspace';
+1
View File
@@ -125,6 +125,7 @@ message QueryRelationPath {
message QueryFieldOperand { message QueryFieldOperand {
string interface_revision_id = 1; string interface_revision_id = 1;
string member_id = 2; string member_id = 2;
repeated string value_path = 3;
} }
message QueryExpression { message QueryExpression {
repeated QueryPathStep path = 1; repeated QueryPathStep path = 1;
+2
View File
@@ -28,6 +28,8 @@ export const generateClientContracts = (
return qualified return qualified
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>` ? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
: `{readonly $quixosRef: string}`; : `{readonly $quixosRef: string}`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
+2
View File
@@ -60,6 +60,8 @@ export const genericImplementationType = (
return `QxObjectRef<${target(type.expectation, scope)}>`; return `QxObjectRef<${target(type.expectation, scope)}>`;
case "record": case "record":
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)])); return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${value(type.value, scope, depth + 1)} | null)`; return `(${value(type.value, scope, depth + 1)} | null)`;
case "list": case "list":
+2
View File
@@ -122,6 +122,8 @@ export const generateTypeScriptBindings = (
}[value.name]; }[value.name];
case "object-ref": case "object-ref":
return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`; return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
+2
View File
@@ -30,6 +30,8 @@ export function generateReactBindings(
uint32: "number", uint32: "number",
double: "number", double: "number",
}[value.name]; }[value.name];
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional": case "optional":
return `(${type(value.value)} | null)`; return `(${type(value.value)} | null)`;
case "list": case "list":
File diff suppressed because one or more lines are too long
@@ -1,283 +1,285 @@
CREATION=1 EDIT=1
INITIALIZE=2 CREATION=2
WORKSPACE=3 INITIALIZE=3
EXECUTION=4 WORKSPACE=4
SNAPSHOT=5 EXECUTION=5
TRANSACTION=6 SNAPSHOT=6
REMOTE=7 TRANSACTION=7
TARGETS=8 REMOTE=8
PORTABLE=9 TARGETS=9
REPLICA=10 PORTABLE=10
LOCAL=11 REPLICA=11
FROM=12 LOCAL=12
RESULT_BYTES=13 FROM=13
RPC_CALLS=14 RESULT_BYTES=14
DEADLINE_MS=15 RPC_CALLS=15
QUERY=16 DEADLINE_MS=16
REFLECTION=17 QUERY=17
SPECIALIZE=18 REFLECTION=18
ROOT=19 SPECIALIZE=19
DOCUMENT=20 ROOT=20
FRAGMENTS=21 DOCUMENT=21
VIEW=22 FRAGMENTS=22
MAX=23 VIEW=23
ALLOW=24 MAX=24
POLL=25 ALLOW=25
QUERYABLE=26 POLL=26
RPC=27 QUERYABLE=27
QUERY_REASON=28 RPC=28
TYPE=29 QUERY_REASON=29
OBJECT=30 TYPE=30
STORABLE=31 OBJECT=31
IMPLEMENTS=32 STORABLE=32
REF=33 IMPLEMENTS=33
FRAGMENT=34 REF=34
IMPORT=35 FRAGMENT=35
EXTERNAL=36 IMPORT=36
ATOM=37 EXTERNAL=37
INTERFACE=38 ATOM=38
INTERFACES=39 INTERFACE=39
PACKAGE=40 INTERFACES=40
VALUE=41 PACKAGE=41
RELATION=42 VALUE=42
OPERATION=43 RELATION=43
FUNCTION=44 OPERATION=44
CONSTRUCTOR=45 FUNCTION=45
CONSTRUCTS=46 CONSTRUCTOR=46
INPUT=47 CONSTRUCTS=47
CONFORM=48 INPUT=48
AS=49 CONFORM=49
BIND=50 AS=50
STATIC=51 BIND=51
TO=52 STATIC=52
PRIVATE=53 TO=53
SHARED=54 PRIVATE=54
STATE=55 SHARED=55
EDGE=56 STATE=56
PROJECTION=57 EDGE=57
WITH=58 PROJECTION=58
USING=59 WITH=59
VIA=60 USING=60
MATERIALIZE=61 VIA=61
IF=62 MATERIALIZE=62
ABSENT=63 IF=63
ON=64 ABSENT=64
POLICY=65 ON=65
DEFAULT=66 POLICY=66
SOURCE=67 DEFAULT=67
REPOSITORY=68 SOURCE=68
COMMIT=69 REPOSITORY=69
REVISION=70 COMMIT=70
SEMANTIC_MAJOR=71 REVISION=71
ON_DELETE=72 SEMANTIC_MAJOR=72
RETAIN_OTHER=73 ON_DELETE=73
KEYED=74 RETAIN_OTHER=74
PUBLIC_TRAVERSAL=75 KEYED=75
ID=76 PUBLIC_TRAVERSAL=76
DOC=77 ID=77
MODE=78 DOC=78
EMITS=79 MODE=79
RECEIVER=80 EMITS=80
REQUIRES=81 RECEIVER=81
ANY=82 REQUIRES=82
GET=83 ANY=83
SET=84 GET=84
WATCH=85 SET=85
START=86 WATCH=86
STOP=87 START=87
READ=88 STOP=88
WRITE=89 READ=89
RESOLVE=90 WRITE=90
CONNECT=91 RESOLVE=91
DISCONNECT=92 CONNECT=92
CALL=93 DISCONNECT=93
WATCH_START=94 CALL=94
WATCH_STOP=95 WATCH_START=95
SUBSCRIBE=96 WATCH_STOP=96
UNSUBSCRIBE=97 SUBSCRIBE=97
REGISTER=98 UNSUBSCRIBE=98
CRDT=99 REGISTER=99
OPTIONAL_ONE=100 CRDT=100
EXACTLY_ONE=101 OPTIONAL_ONE=101
MANY_UNIQUE=102 EXACTLY_ONE=102
MANY=103 MANY_UNIQUE=103
ORDERED=104 MANY=104
UNIT=105 ORDERED=105
WATCH_HANDLE=106 UNIT=106
MESSAGE=107 WATCH_HANDLE=107
ATOM_REF=108 MESSAGE=108
INTERFACE_REF=109 ATOM_REF=109
OPTIONAL=110 INTERFACE_REF=110
LIST=111 OPTIONAL=111
RECORD=112 LIST=112
BOOL=113 RECORD=113
BYTES=114 BOOL=114
DOUBLE=115 BYTES=115
INT32=116 DOUBLE=116
INT64=117 INT32=117
STRING=118 INT64=118
UINT32=119 STRING=119
UINT64=120 UINT32=120
TRUE=121 UINT64=121
FALSE=122 TRUE=122
NULL=123 FALSE=123
ARROW=124 NULL=124
COLON=125 ARROW=125
SEMI=126 COLON=126
COMMA=127 SEMI=127
DOT=128 COMMA=128
LBRACE=129 DOT=129
RBRACE=130 LBRACE=130
LBRACK=131 RBRACE=131
RBRACK=132 LBRACK=132
LPAREN=133 RBRACK=133
RPAREN=134 LPAREN=134
LT=135 RPAREN=135
GT=136 LT=136
AMP=137 GT=137
EQUAL=138 AMP=138
INTEGER=139 EQUAL=139
JSON_NUMBER=140 INTEGER=140
IDENTIFIER=141 JSON_NUMBER=141
STRING_LITERAL=142 IDENTIFIER=142
LINE_COMMENT=143 STRING_LITERAL=143
BLOCK_COMMENT=144 LINE_COMMENT=144
WS=145 BLOCK_COMMENT=145
'creation'=1 WS=146
'initialize'=2 'edit'=1
'workspace'=3 'creation'=2
'execution'=4 'initialize'=3
'snapshot'=5 'workspace'=4
'transaction'=6 'execution'=5
'remote'=7 'snapshot'=6
'targets'=8 'transaction'=7
'portable'=9 'remote'=8
'replica'=10 'targets'=9
'local'=11 'portable'=10
'from'=12 'replica'=11
'result-bytes'=13 'local'=12
'rpc-calls'=14 'from'=13
'deadline-ms'=15 'result-bytes'=14
'query'=16 'rpc-calls'=15
'reflection'=17 'deadline-ms'=16
'specialize'=18 'query'=17
'root'=19 'reflection'=18
'document'=20 'specialize'=19
'fragments'=21 'root'=20
'view'=22 'document'=21
'max'=23 'fragments'=22
'allow'=24 'view'=23
'poll'=25 'max'=24
'queryable'=26 'allow'=25
'rpc'=27 'poll'=26
'query-reason'=28 'queryable'=27
'type'=29 'rpc'=28
'object'=30 'query-reason'=29
'storable'=31 'type'=30
'implements'=32 'object'=31
'ref'=33 'storable'=32
'fragment'=34 'implements'=33
'import'=35 'ref'=34
'external'=36 'fragment'=35
'atom'=37 'import'=36
'interface'=38 'external'=37
'interfaces'=39 'atom'=38
'package'=40 'interface'=39
'value'=41 'interfaces'=40
'relation'=42 'package'=41
'operation'=43 'value'=42
'function'=44 'relation'=43
'constructor'=45 'operation'=44
'constructs'=46 'function'=45
'input'=47 'constructor'=46
'conform'=48 'constructs'=47
'as'=49 'input'=48
'bind'=50 'conform'=49
'static'=51 'as'=50
'to'=52 'bind'=51
'private'=53 'static'=52
'shared'=54 'to'=53
'state'=55 'private'=54
'edge'=56 'shared'=55
'projection'=57 'state'=56
'with'=58 'edge'=57
'using'=59 'projection'=58
'via'=60 'with'=59
'materialize'=61 'using'=60
'if'=62 'via'=61
'absent'=63 'materialize'=62
'on'=64 'if'=63
'policy'=65 'absent'=64
'default'=66 'on'=65
'source'=67 'policy'=66
'repository'=68 'default'=67
'commit'=69 'source'=68
'revision'=70 'repository'=69
'semantic-major'=71 'commit'=70
'on-delete'=72 'revision'=71
'retain-other'=73 'semantic-major'=72
'keyed'=74 'on-delete'=73
'public-traversal'=75 'retain-other'=74
'id'=76 'keyed'=75
'doc'=77 'public-traversal'=76
'mode'=78 'id'=77
'emits'=79 'doc'=78
'receiver'=80 'mode'=79
'requires'=81 'emits'=80
'any'=82 'receiver'=81
'get'=83 'requires'=82
'set'=84 'any'=83
'watch'=85 'get'=84
'start'=86 'set'=85
'stop'=87 'watch'=86
'read'=88 'start'=87
'write'=89 'stop'=88
'resolve'=90 'read'=89
'connect'=91 'write'=90
'disconnect'=92 'resolve'=91
'call'=93 'connect'=92
'watch-start'=94 'disconnect'=93
'watch-stop'=95 'call'=94
'subscribe'=96 'watch-start'=95
'unsubscribe'=97 'watch-stop'=96
'register'=98 'subscribe'=97
'crdt'=99 'unsubscribe'=98
'optional-one'=100 'register'=99
'exactly-one'=101 'crdt'=100
'many-unique'=102 'optional-one'=101
'many'=103 'exactly-one'=102
'ordered'=104 'many-unique'=103
'unit'=105 'many'=104
'watch-handle'=106 'ordered'=105
'message'=107 'unit'=106
'atom-ref'=108 'watch-handle'=107
'interface-ref'=109 'message'=108
'optional'=110 'atom-ref'=109
'list'=111 'interface-ref'=110
'record'=112 'optional'=111
'bool'=113 'list'=112
'bytes'=114 'record'=113
'double'=115 'bool'=114
'int32'=116 'bytes'=115
'int64'=117 'double'=116
'string'=118 'int32'=117
'uint32'=119 'int64'=118
'uint64'=120 'string'=119
'true'=121 'uint32'=120
'false'=122 'uint64'=121
'null'=123 'true'=122
'->'=124 'false'=123
':'=125 'null'=124
';'=126 '->'=125
','=127 ':'=126
'.'=128 ';'=127
'{'=129 ','=128
'}'=130 '.'=129
'['=131 '{'=130
']'=132 '}'=131
'('=133 '['=132
')'=134 ']'=133
'<'=135 '('=134
'>'=136 ')'=135
'&'=137 '<'=136
'='=138 '>'=137
'&'=138
'='=139
File diff suppressed because one or more lines are too long
@@ -1,283 +1,285 @@
CREATION=1 EDIT=1
INITIALIZE=2 CREATION=2
WORKSPACE=3 INITIALIZE=3
EXECUTION=4 WORKSPACE=4
SNAPSHOT=5 EXECUTION=5
TRANSACTION=6 SNAPSHOT=6
REMOTE=7 TRANSACTION=7
TARGETS=8 REMOTE=8
PORTABLE=9 TARGETS=9
REPLICA=10 PORTABLE=10
LOCAL=11 REPLICA=11
FROM=12 LOCAL=12
RESULT_BYTES=13 FROM=13
RPC_CALLS=14 RESULT_BYTES=14
DEADLINE_MS=15 RPC_CALLS=15
QUERY=16 DEADLINE_MS=16
REFLECTION=17 QUERY=17
SPECIALIZE=18 REFLECTION=18
ROOT=19 SPECIALIZE=19
DOCUMENT=20 ROOT=20
FRAGMENTS=21 DOCUMENT=21
VIEW=22 FRAGMENTS=22
MAX=23 VIEW=23
ALLOW=24 MAX=24
POLL=25 ALLOW=25
QUERYABLE=26 POLL=26
RPC=27 QUERYABLE=27
QUERY_REASON=28 RPC=28
TYPE=29 QUERY_REASON=29
OBJECT=30 TYPE=30
STORABLE=31 OBJECT=31
IMPLEMENTS=32 STORABLE=32
REF=33 IMPLEMENTS=33
FRAGMENT=34 REF=34
IMPORT=35 FRAGMENT=35
EXTERNAL=36 IMPORT=36
ATOM=37 EXTERNAL=37
INTERFACE=38 ATOM=38
INTERFACES=39 INTERFACE=39
PACKAGE=40 INTERFACES=40
VALUE=41 PACKAGE=41
RELATION=42 VALUE=42
OPERATION=43 RELATION=43
FUNCTION=44 OPERATION=44
CONSTRUCTOR=45 FUNCTION=45
CONSTRUCTS=46 CONSTRUCTOR=46
INPUT=47 CONSTRUCTS=47
CONFORM=48 INPUT=48
AS=49 CONFORM=49
BIND=50 AS=50
STATIC=51 BIND=51
TO=52 STATIC=52
PRIVATE=53 TO=53
SHARED=54 PRIVATE=54
STATE=55 SHARED=55
EDGE=56 STATE=56
PROJECTION=57 EDGE=57
WITH=58 PROJECTION=58
USING=59 WITH=59
VIA=60 USING=60
MATERIALIZE=61 VIA=61
IF=62 MATERIALIZE=62
ABSENT=63 IF=63
ON=64 ABSENT=64
POLICY=65 ON=65
DEFAULT=66 POLICY=66
SOURCE=67 DEFAULT=67
REPOSITORY=68 SOURCE=68
COMMIT=69 REPOSITORY=69
REVISION=70 COMMIT=70
SEMANTIC_MAJOR=71 REVISION=71
ON_DELETE=72 SEMANTIC_MAJOR=72
RETAIN_OTHER=73 ON_DELETE=73
KEYED=74 RETAIN_OTHER=74
PUBLIC_TRAVERSAL=75 KEYED=75
ID=76 PUBLIC_TRAVERSAL=76
DOC=77 ID=77
MODE=78 DOC=78
EMITS=79 MODE=79
RECEIVER=80 EMITS=80
REQUIRES=81 RECEIVER=81
ANY=82 REQUIRES=82
GET=83 ANY=83
SET=84 GET=84
WATCH=85 SET=85
START=86 WATCH=86
STOP=87 START=87
READ=88 STOP=88
WRITE=89 READ=89
RESOLVE=90 WRITE=90
CONNECT=91 RESOLVE=91
DISCONNECT=92 CONNECT=92
CALL=93 DISCONNECT=93
WATCH_START=94 CALL=94
WATCH_STOP=95 WATCH_START=95
SUBSCRIBE=96 WATCH_STOP=96
UNSUBSCRIBE=97 SUBSCRIBE=97
REGISTER=98 UNSUBSCRIBE=98
CRDT=99 REGISTER=99
OPTIONAL_ONE=100 CRDT=100
EXACTLY_ONE=101 OPTIONAL_ONE=101
MANY_UNIQUE=102 EXACTLY_ONE=102
MANY=103 MANY_UNIQUE=103
ORDERED=104 MANY=104
UNIT=105 ORDERED=105
WATCH_HANDLE=106 UNIT=106
MESSAGE=107 WATCH_HANDLE=107
ATOM_REF=108 MESSAGE=108
INTERFACE_REF=109 ATOM_REF=109
OPTIONAL=110 INTERFACE_REF=110
LIST=111 OPTIONAL=111
RECORD=112 LIST=112
BOOL=113 RECORD=113
BYTES=114 BOOL=114
DOUBLE=115 BYTES=115
INT32=116 DOUBLE=116
INT64=117 INT32=117
STRING=118 INT64=118
UINT32=119 STRING=119
UINT64=120 UINT32=120
TRUE=121 UINT64=121
FALSE=122 TRUE=122
NULL=123 FALSE=123
ARROW=124 NULL=124
COLON=125 ARROW=125
SEMI=126 COLON=126
COMMA=127 SEMI=127
DOT=128 COMMA=128
LBRACE=129 DOT=129
RBRACE=130 LBRACE=130
LBRACK=131 RBRACE=131
RBRACK=132 LBRACK=132
LPAREN=133 RBRACK=133
RPAREN=134 LPAREN=134
LT=135 RPAREN=135
GT=136 LT=136
AMP=137 GT=137
EQUAL=138 AMP=138
INTEGER=139 EQUAL=139
JSON_NUMBER=140 INTEGER=140
IDENTIFIER=141 JSON_NUMBER=141
STRING_LITERAL=142 IDENTIFIER=142
LINE_COMMENT=143 STRING_LITERAL=143
BLOCK_COMMENT=144 LINE_COMMENT=144
WS=145 BLOCK_COMMENT=145
'creation'=1 WS=146
'initialize'=2 'edit'=1
'workspace'=3 'creation'=2
'execution'=4 'initialize'=3
'snapshot'=5 'workspace'=4
'transaction'=6 'execution'=5
'remote'=7 'snapshot'=6
'targets'=8 'transaction'=7
'portable'=9 'remote'=8
'replica'=10 'targets'=9
'local'=11 'portable'=10
'from'=12 'replica'=11
'result-bytes'=13 'local'=12
'rpc-calls'=14 'from'=13
'deadline-ms'=15 'result-bytes'=14
'query'=16 'rpc-calls'=15
'reflection'=17 'deadline-ms'=16
'specialize'=18 'query'=17
'root'=19 'reflection'=18
'document'=20 'specialize'=19
'fragments'=21 'root'=20
'view'=22 'document'=21
'max'=23 'fragments'=22
'allow'=24 'view'=23
'poll'=25 'max'=24
'queryable'=26 'allow'=25
'rpc'=27 'poll'=26
'query-reason'=28 'queryable'=27
'type'=29 'rpc'=28
'object'=30 'query-reason'=29
'storable'=31 'type'=30
'implements'=32 'object'=31
'ref'=33 'storable'=32
'fragment'=34 'implements'=33
'import'=35 'ref'=34
'external'=36 'fragment'=35
'atom'=37 'import'=36
'interface'=38 'external'=37
'interfaces'=39 'atom'=38
'package'=40 'interface'=39
'value'=41 'interfaces'=40
'relation'=42 'package'=41
'operation'=43 'value'=42
'function'=44 'relation'=43
'constructor'=45 'operation'=44
'constructs'=46 'function'=45
'input'=47 'constructor'=46
'conform'=48 'constructs'=47
'as'=49 'input'=48
'bind'=50 'conform'=49
'static'=51 'as'=50
'to'=52 'bind'=51
'private'=53 'static'=52
'shared'=54 'to'=53
'state'=55 'private'=54
'edge'=56 'shared'=55
'projection'=57 'state'=56
'with'=58 'edge'=57
'using'=59 'projection'=58
'via'=60 'with'=59
'materialize'=61 'using'=60
'if'=62 'via'=61
'absent'=63 'materialize'=62
'on'=64 'if'=63
'policy'=65 'absent'=64
'default'=66 'on'=65
'source'=67 'policy'=66
'repository'=68 'default'=67
'commit'=69 'source'=68
'revision'=70 'repository'=69
'semantic-major'=71 'commit'=70
'on-delete'=72 'revision'=71
'retain-other'=73 'semantic-major'=72
'keyed'=74 'on-delete'=73
'public-traversal'=75 'retain-other'=74
'id'=76 'keyed'=75
'doc'=77 'public-traversal'=76
'mode'=78 'id'=77
'emits'=79 'doc'=78
'receiver'=80 'mode'=79
'requires'=81 'emits'=80
'any'=82 'receiver'=81
'get'=83 'requires'=82
'set'=84 'any'=83
'watch'=85 'get'=84
'start'=86 'set'=85
'stop'=87 'watch'=86
'read'=88 'start'=87
'write'=89 'stop'=88
'resolve'=90 'read'=89
'connect'=91 'write'=90
'disconnect'=92 'resolve'=91
'call'=93 'connect'=92
'watch-start'=94 'disconnect'=93
'watch-stop'=95 'call'=94
'subscribe'=96 'watch-start'=95
'unsubscribe'=97 'watch-stop'=96
'register'=98 'subscribe'=97
'crdt'=99 'unsubscribe'=98
'optional-one'=100 'register'=99
'exactly-one'=101 'crdt'=100
'many-unique'=102 'optional-one'=101
'many'=103 'exactly-one'=102
'ordered'=104 'many-unique'=103
'unit'=105 'many'=104
'watch-handle'=106 'ordered'=105
'message'=107 'unit'=106
'atom-ref'=108 'watch-handle'=107
'interface-ref'=109 'message'=108
'optional'=110 'atom-ref'=109
'list'=111 'interface-ref'=110
'record'=112 'optional'=111
'bool'=113 'list'=112
'bytes'=114 'record'=113
'double'=115 'bool'=114
'int32'=116 'bytes'=115
'int64'=117 'double'=116
'string'=118 'int32'=117
'uint32'=119 'int64'=118
'uint64'=120 'string'=119
'true'=121 'uint32'=120
'false'=122 'uint64'=121
'null'=123 'true'=122
'->'=124 'false'=123
':'=125 'null'=124
';'=126 '->'=125
','=127 ':'=126
'.'=128 ';'=127
'{'=129 ','=128
'}'=130 '.'=129
'['=131 '{'=130
']'=132 '}'=131
'('=133 '['=132
')'=134 ']'=133
'<'=135 '('=134
'>'=136 ')'=135
'&'=137 '<'=136
'='=138 '>'=137
'&'=138
'='=139
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -108,6 +108,7 @@ export class GenericSourceTypes {
return parameter?.kind === "value" && Boolean(parameter.storable); return parameter?.kind === "value" && Boolean(parameter.storable);
} }
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1); if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
if (type.kind === "alias") { if (type.kind === "alias") {
const alias = aliases.get(type.definitionId); const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false; if (!alias || alias.parameters.length !== type.arguments.length) return false;
@@ -318,6 +319,12 @@ export class GenericSourceTypes {
return { kind: "record", fields: Object.fromEntries(fields) }; return { kind: "record", fields: Object.fromEntries(fields) };
} }
const name = context.identifier()!.getText(); const name = context.identifier()!.getText();
if (name === "change" && context.typeArguments())
throw new GenericTypeError(
"unsupported-prepared-change",
name,
"Authored change<T> transport values require LF-19; native edit contracts are available separately",
);
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) }; if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
if (context.INTERFACE_REF()) if (context.INTERFACE_REF())
return { return {
+21 -4
View File
@@ -405,6 +405,8 @@ const valueOperation = (displayName: string, id: InterfaceOperation["id"], value
outputType: value, outputType: value,
mode: "call", mode: "call",
}; };
case "edit":
return { id, displayName, inputType: { kind: "change", value }, outputType: valueType.unit, mode: "call" };
case "set": case "set":
return { return {
id, id,
@@ -494,6 +496,11 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType), ...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
...lowerExecution(operation.executionClause()), ...lowerExecution(operation.executionClause()),
}); });
} else if (operation.EDIT()) {
operations.push({
...valueOperation("edit", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
...lowerExecution(operation.executionClause()),
});
} else if (operation.SET()) { } else if (operation.SET()) {
operations.push({ operations.push({
...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType), ...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
@@ -763,6 +770,13 @@ const lowerInterfaceTemplate = (
return [ return [
{ ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) }, { ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) },
]; ];
if (operation.EDIT())
return [
{
...signature("edit", id, { kind: "change", value: type }, valueType.unit),
...lowerExecution(operation.executionClause()),
},
];
if (operation.SET()) if (operation.SET())
return [ return [
{ ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) }, { ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) },
@@ -939,7 +953,9 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
.primitiveList()! .primitiveList()!
.primitive() .primitive()
.map((entry) => text(entry)); .map((entry) => text(entry));
const invalid = primitives.filter((entry) => !["read", "write", "watch-start", "watch-stop"].includes(entry)); const invalid = primitives.filter(
(entry) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(entry),
);
if (invalid.length > 0) { if (invalid.length > 0) {
loweringIssue(state, context, "invalid-port-primitive", `State port cannot request ${invalid.join(", ")}`); loweringIssue(state, context, "invalid-port-primitive", `State port cannot request ${invalid.join(", ")}`);
} }
@@ -1405,12 +1421,12 @@ const lowerPackage = (
const name = identifier(clause.identifier(0)); const name = identifier(clause.identifier(0));
const key = budgetNames[name], const key = budgetNames[name],
n = Number(clause.INTEGER()!.getText()); n = Number(clause.INTEGER()!.getText());
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key]) if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > 0xffffffff)
loweringIssue( loweringIssue(
state, state,
clause, clause,
"invalid-query-budget", "invalid-query-budget",
`Invalid, duplicate, or excessive query budget ${name}`, `Invalid, duplicate, or out-of-range query budget ${name}`,
); );
else { else {
declaration.budgets[key] = n; declaration.budgets[key] = n;
@@ -1898,6 +1914,7 @@ const lowerConformance = (
const primitives = { const primitives = {
get: "read", get: "read",
set: "write", set: "write",
edit: "edit",
"watch-start": "watch-start", "watch-start": "watch-start",
"watch-stop": "watch-stop", "watch-stop": "watch-stop",
} as const; } as const;
@@ -2622,7 +2639,7 @@ const compileCapabilityResourceSourceInternal = (
portNames.add(port.displayName); portNames.add(port.displayName);
const requirement = port.requirement; const requirement = port.requirement;
if (requirement.kind === "state") { if (requirement.kind === "state") {
if (requirement.primitives.some((p) => !["read", "write", "watch-start", "watch-stop"].includes(p))) if (requirement.primitives.some((p) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(p)))
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive"); throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
types.push(requirement.valueType); types.push(requirement.valueType);
} }
+6 -4
View File
@@ -16,7 +16,7 @@ export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }> | Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string } | { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> } | { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "list" | "optional"; value: ValueTypeExpression } | { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression } | { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] }; | { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
@@ -184,10 +184,10 @@ export const isStorableType = (
case "list": case "list":
case "optional": case "optional":
return visit(value.value, depth + 1); return visit(value.value, depth + 1);
// Records currently have RPC codecs, not ordinary-state persistence codecs. case "change":
// A generic bound must not promise storage that the installed model rejects.
case "record":
return false; return false;
case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1));
} }
}; };
return visit(type, 0); return visit(type, 0);
@@ -216,6 +216,7 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
return ["message", value.descriptorId]; return ["message", value.descriptorId];
case "object-ref": case "object-ref":
return ["object-ref", target(value.expectation)]; return ["object-ref", target(value.expectation)];
case "change":
case "list": case "list":
case "optional": case "optional":
return [value.kind, type(value.value, depth + 1)]; return [value.kind, type(value.value, depth + 1)];
@@ -323,6 +324,7 @@ export class TypeSubstitution {
return { kind: "scalar", name: expression.name }; return { kind: "scalar", name: expression.name };
case "message": case "message":
return { kind: "message", descriptorId: expression.descriptorId }; return { kind: "message", descriptorId: expression.descriptorId };
case "change":
case "list": case "list":
case "optional": case "optional":
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) }; return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
+3 -2
View File
@@ -67,7 +67,8 @@ export type ValueType =
| { kind: "record"; fields: Record<string, ValueType> } | { kind: "record"; fields: Record<string, ValueType> }
| { kind: "object-ref"; expectation: ObjectExpectation } | { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType } | { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType }; | { kind: "list"; value: ValueType }
| { kind: "change"; value: ValueType };
export const valueType = { export const valueType = {
unit: { kind: "builtin", name: "unit" } as const, unit: { kind: "builtin", name: "unit" } as const,
@@ -214,7 +215,7 @@ export interface EdgeDefinition {
export type PersistentAttachment = StateSlotDefinition | EdgeDefinition; export type PersistentAttachment = StateSlotDefinition | EdgeDefinition;
export type StatePrimitive = "read" | "write" | "watch-start" | "watch-stop"; export type StatePrimitive = "read" | "write" | "edit" | "watch-start" | "watch-stop";
export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop"; export type EdgePrimitive = "resolve" | "connect" | "disconnect" | "watch-start" | "watch-stop";
+50 -5
View File
@@ -154,6 +154,8 @@ const typeLabel = (type: ValueType): string => {
return type.expectation.kind === "atom" return type.expectation.kind === "atom"
? `object:atom:${type.expectation.atomId}` ? `object:atom:${type.expectation.atomId}`
: `object:interface:${type.expectation.interfaceRevisionId}`; : `object:interface:${type.expectation.interfaceRevisionId}`;
case "change":
return `change<${typeLabel(type.value)}>`;
case "optional": case "optional":
return `optional<${typeLabel(type.value)}>`; return `optional<${typeLabel(type.value)}>`;
case "list": case "list":
@@ -189,6 +191,7 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
? left.expectation.atomId === (other as typeof left.expectation).atomId ? left.expectation.atomId === (other as typeof left.expectation).atomId
: left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId; : left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId;
} }
case "change":
case "optional": case "optional":
case "list": case "list":
return valueTypesEqual(left.value, (right as typeof left).value); return valueTypesEqual(left.value, (right as typeof left).value);
@@ -239,6 +242,7 @@ const validateValueType = (
case "message": case "message":
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity"); requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
return; return;
case "change":
case "optional": case "optional":
case "list": case "list":
validateValueType(issues, type.value, `${path}.value`, indexes); validateValueType(issues, type.value, `${path}.value`, indexes);
@@ -341,6 +345,8 @@ export const statePrimitiveSignature = (slot: StateSlotDefinition, primitive: St
inputType: valueType.unit, inputType: valueType.unit,
outputType: slot.valueType, outputType: slot.valueType,
}; };
case "edit":
return { mode: "call", inputType: { kind: "change", value: slot.valueType }, outputType: valueType.unit };
case "write": case "write":
return { return {
mode: "call", mode: "call",
@@ -532,9 +538,19 @@ const collectIdentityIndexes = (
if (member.kind === "relationship" && contract === "rpc-permitted") if (member.kind === "relationship" && contract === "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported"); issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
if (member.kind === "value") { if (member.kind === "value") {
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType; const queryValue = (type: ValueType): boolean =>
if (type.kind !== "scalar") type.kind === "scalar" ||
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar"); ((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
(type.kind === "record" &&
Object.keys(type.fields).length > 0 &&
Object.values(type.fields).every(queryValue));
if (!queryValue(member.valueType))
issue(
issues,
"invalid-query-contract",
memberPath,
"Queryable values require scalar leaves in optional, list, or nonempty record shapes",
);
} }
} }
requireText(issues, member.id, `${memberPath}.id`, "Member ID"); requireText(issues, member.id, `${memberPath}.id`, "Member ID");
@@ -837,7 +853,8 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes); validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes);
const containsRpcType = (type: ValueType): boolean => const containsRpcType = (type: ValueType): boolean =>
type.kind === "object-ref" || type.kind === "object-ref" ||
type.kind === "record" || type.kind === "change" ||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value)); ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if ( if (
containsRpcType(attachment.valueType) || containsRpcType(attachment.valueType) ||
@@ -847,7 +864,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
issues, issues,
"invalid-attachment", "invalid-attachment",
`${path}.valueType`, `${path}.valueType`,
"Managed object references belong in graph relationships, not ordinary state; record types are RPC-only", "Managed object references belong in graph relationships; captured changes are transport-only",
); );
} }
if (attachment.storagePolicy.kind === "crdt-document") { if (attachment.storagePolicy.kind === "crdt-document") {
@@ -1287,6 +1304,12 @@ const validateBoundDependencies = (
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`, : `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
); );
} }
if (
requirement.primitives.includes("edit") &&
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
)
issue(issues, "invalid-dependency-binding", path, "Edit ports require a matching CRDT slot");
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) { if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
issue( issue(
issues, issues,
@@ -1579,6 +1602,17 @@ const validateConformances = (
} }
} else binding = entry.binding; } else binding = entry.binding;
const member = operationEntry.member; const member = operationEntry.member;
if (
member.kind === "value" &&
operation.displayName === "edit" &&
(binding.kind !== "state" || binding.primitive !== "edit")
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"CRDT edit operations must resolve to a native state edit capability",
);
if ( if (
member.kind !== "operation" && member.kind !== "operation" &&
member.queryRead && member.queryRead &&
@@ -1652,6 +1686,17 @@ const validateConformances = (
"State primitives may only implement value members", "State primitives may only implement value members",
); );
} }
if (
binding.primitive === "edit" &&
(attachment.attachment.storagePolicy.kind !== "crdt-document" ||
!valueTypesEqual(attachment.attachment.valueType, attachment.attachment.storagePolicy.updateType))
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Edit requires a CRDT slot with a matching materialized profile type",
);
const expected = statePrimitiveSignature(attachment.attachment, binding.primitive); const expected = statePrimitiveSignature(attachment.attachment, binding.primitive);
if (!signaturesMatch(operation, expected)) { if (!signaturesMatch(operation, expected)) {
issue( issue(
File diff suppressed because one or more lines are too long
+47 -12
View File
@@ -154,8 +154,19 @@ export function relationalCompiler(
): QueryExpression => { ): QueryExpression => {
requireAvailable(names, available, node); requireAvailable(names, available, node);
const meta = names.at(-2) === "_qx"; const meta = names.at(-2) === "_qx";
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : -1), uses, node); let fieldIndex = names.length - 1;
const name = names.at(-1)!; if (!meta) {
let cursor = root;
for (let index = 0; index < names.length; index++) {
if (cursor.kind === "object" && rowFields(cursor).get(names[index]!)?.kind === "leaf") {
fieldIndex = index;
break;
}
if (index < names.length - 1) cursor = walk(root, names.slice(0, index + 1), uses, node).row;
}
}
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : fieldIndex), uses, node);
const name = names[meta ? names.length - 1 : fieldIndex]!;
let type: ValueType, leaf: QueryExpression["leaf"]; let type: ValueType, leaf: QueryExpression["leaf"];
if (meta) { if (meta) {
if (name === "ref" && row.kind === "object") { if (name === "ref" && row.kind === "object") {
@@ -185,7 +196,24 @@ export function relationalCompiler(
for (const use of uses) mark(row.interfaceRevisionId, name, use, node); for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!; const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!;
type = field.type; type = field.type;
leaf = { kind: "field", interfaceRevisionId: row.interfaceRevisionId, memberId: member.id }; const valuePath = names.slice(fieldIndex + 1);
let valueNullable = false;
for (const component of valuePath) {
if (type.kind === "optional") {
valueNullable = true;
type = type.value;
}
if (type.kind !== "record" || !Object.hasOwn(type.fields, component))
return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown scalar value path ${names.join(".")}`, node);
type = type.fields[component]!;
}
if (valueNullable && type.kind !== "optional") type = valueType.optional(type);
leaf = {
kind: "field",
interfaceRevisionId: row.interfaceRevisionId,
memberId: member.id,
...(valuePath.length ? { valuePath } : {}),
};
} }
return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type }; return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type };
}; };
@@ -356,15 +384,22 @@ export function relationalCompiler(
} }
} }
} else { } else {
const expr = expression(row, [...base, name], ["predicate"], field, available); const valuePredicate = (names: string[], value: ValueNode) => {
children.push( const expr = expression(row, names, ["predicate"], field, available);
...(comparisons(field.value, (operator, value) => ({ const type = expr.type.kind === "optional" ? expr.type.value : expr.type;
kind: "compare", if (type.kind === "record") {
expression: expr, for (const child of pairs(value)) valuePredicate([...names, child.name.value], child.value);
operator, } else
value, children.push(
})) as QueryPredicate[]), ...(comparisons(value, (operator, value) => ({
); kind: "compare",
expression: expr,
operator,
value,
})) as QueryPredicate[]),
);
};
valuePredicate([...base, name], field.value);
} }
} }
}; };
+5 -1
View File
@@ -1,3 +1,4 @@
import { comparableValue } from "./structured.js";
import { import {
GraphQLBoolean, GraphQLBoolean,
GraphQLString, GraphQLString,
@@ -45,6 +46,7 @@ export interface RelationalSchemaEnvironment {
name(id: InterfaceRevisionId): string; name(id: InterfaceRevisionId): string;
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType; scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
comparison(type: ValueType): GraphQLInputObjectType; comparison(type: ValueType): GraphQLInputObjectType;
valueOrder(type: ValueType): GraphQLInputType;
scalarTypes: Map<string, ValueType>; scalarTypes: Map<string, ValueType>;
direction: GraphQLEnumType; direction: GraphQLEnumType;
cursor: GraphQLScalarType; cursor: GraphQLScalarType;
@@ -255,7 +257,8 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
continue; continue;
} }
} }
result[name] = { type: ordering ? env.direction : compare(type) }; if (!comparableValue(type, ordering)) continue;
result[name] = { type: ordering ? env.valueOrder(type) : compare(type) };
} }
} }
const meta: GraphQLInputFieldConfigMap = {}; const meta: GraphQLInputFieldConfigMap = {};
@@ -300,6 +303,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
for (const [name, field] of fields(row)) { for (const [name, field] of fields(row)) {
if (row.kind === "object" && field.kind === "row") continue; if (row.kind === "object" && field.kind === "row") continue;
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`); if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
if (field.kind === "leaf" && !comparableValue(field.type)) continue;
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) }; result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
} }
result._qx = { result._qx = {
+1 -1
View File
@@ -49,7 +49,7 @@ export type QueryPathStep =
export interface QueryExpression { export interface QueryExpression {
path: QueryPathStep[]; path: QueryPathStep[];
leaf: leaf:
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId } | { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId; valuePath?: string[] }
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId } | { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
| { kind: "entry" | "mapKey" }; | { kind: "entry" | "mapKey" };
type: ValueType; type: ValueType;
+18 -7
View File
@@ -1,3 +1,4 @@
import { structuredQueryTypes, comparableValue } from "./structured.js";
import { import {
GraphQLBoolean, GraphQLBoolean,
GraphQLString, GraphQLString,
@@ -132,7 +133,7 @@ export function querySchema(
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported"); throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
return queryScalars[type.name]; return queryScalars[type.name];
}; };
const comparison = (type: ValueType) => { const scalarComparison = (type: ValueType) => {
const base = type.kind === "optional" ? type.value : type; const base = type.kind === "optional" ? type.value : type;
const value = scalar(base); const value = scalar(base);
const key = String(value); const key = String(value);
@@ -149,8 +150,21 @@ export function querySchema(
} }
return result; return result;
}; };
const structured = structuredQueryTypes(scalar, scalarComparison, direction);
const comparison = (type: ValueType) => structured.input(type) as GraphQLInputObjectType;
const relational = relationalSchema( const relational = relationalSchema(
{ contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo }, {
contracts,
target,
name,
scalar,
comparison,
valueOrder: (type) => structured.input(type, true),
scalarTypes,
direction,
cursor: cursorScalar,
pageInfo,
},
document, document,
); );
relational.discover(root.interface); relational.discover(root.interface);
@@ -162,7 +176,7 @@ export function querySchema(
for (const member of contract(id).members) { for (const member of contract(id).members) {
if (member.kind !== "value" || !member.queryRead) continue; if (member.kind !== "value" || !member.queryRead) continue;
const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType; const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
if (t.kind === "scalar" && t.name !== "bytes") fields[member.displayName] = { type: direction }; if (comparableValue(t, true)) fields[member.displayName] = { type: structured.input(t, true) };
} }
if (Object.keys(fields).length === 0) return undefined; if (Object.keys(fields).length === 0) return undefined;
result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields }); result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields });
@@ -190,10 +204,7 @@ export function querySchema(
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_")) if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`); throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`);
if (member.kind === "value") { if (member.kind === "value") {
const type = scalar(member.valueType); fields[member.displayName] = { type: structured.output(member.valueType) };
fields[member.displayName] = {
type: member.valueType.kind === "optional" ? type : new GraphQLNonNull(type),
};
} else { } else {
const targetId = target(member), const targetId = target(member),
node = object(targetId); node = object(targetId);
+76
View File
@@ -0,0 +1,76 @@
import { createHash } from "node:crypto";
import {
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
type GraphQLNullableType,
type GraphQLOutputType,
type GraphQLInputType,
type GraphQLEnumType,
} from "graphql";
import type { ValueType } from "../capability-model/types.js";
import { QueryCompileError } from "./types.js";
export function comparableValue(type: ValueType, ordering = false): boolean {
if (type.kind === "optional") return comparableValue(type.value, ordering);
if (type.kind === "record") return Object.values(type.fields).some((t) => comparableValue(t, ordering));
return type.kind === "scalar" && (!ordering || type.name !== "bytes");
}
/** Structured values retain one field identity; their children are value projections. */
export function structuredQueryTypes(
scalar: (type: ValueType) => GraphQLInputType & GraphQLOutputType,
scalarComparison: (type: ValueType) => GraphQLInputObjectType,
direction: GraphQLEnumType,
) {
const objects = new Map<string, GraphQLObjectType>();
const inputs = new Map<string, GraphQLInputObjectType>();
const name = (role: string, type: ValueType) =>
`QxValue${role}_${createHash("sha256").update(JSON.stringify(type)).digest("hex").slice(0, 16)}`;
const output = (type: ValueType, depth = 0): GraphQLOutputType => {
if (depth > 128) throw new QueryCompileError("QUERY_TYPE", "Structured value exceeds type depth limit");
if (type.kind === "optional") return required(type.value, depth + 1);
return new GraphQLNonNull(required(type, depth));
};
const required = (type: ValueType, depth: number): GraphQLNullableType & GraphQLOutputType => {
if (type.kind === "optional") return required(type.value, depth + 1);
if (type.kind === "list") return new GraphQLList(output(type.value, depth + 1));
if (type.kind !== "record") return scalar(type) as GraphQLNullableType & GraphQLOutputType;
const key = name("Record", type);
let result = objects.get(key);
if (!result) {
if (!Object.keys(type.fields).length)
throw new QueryCompileError("QUERY_TYPE", "Queryable records must have at least one field");
result = new GraphQLObjectType({
name: key,
fields: () =>
Object.fromEntries(
Object.entries(type.fields).map(([key, value]) => [key, { type: output(value, depth + 1) }]),
),
});
objects.set(key, result);
}
return result;
};
const input = (type: ValueType, ordering = false): GraphQLInputType => {
if (type.kind === "optional") return input(type.value, ordering);
if (type.kind !== "record") return ordering ? direction : scalarComparison(type);
const key = name(ordering ? "Order" : "Compare", type);
let result = inputs.get(key);
if (!result) {
result = new GraphQLInputObjectType({
name: key,
fields: () =>
Object.fromEntries(
Object.entries(type.fields)
.filter(([, value]) => comparableValue(value, ordering))
.map(([key, value]) => [key, { type: input(value, ordering) }]),
),
});
inputs.set(key, result);
}
return result;
};
return { output, input };
}
+91
View File
@@ -0,0 +1,91 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
const source = { repository: "https://example.test/edit.git", commit: "a".repeat(40) };
const content = "record { text:string; tags:list<string>; }";
const iface = compileCapabilityResourceSource(
`interface Comment id "comment" revision "comment@1" {
value content id "content" : ${content} {
get id "content:get" execution snapshot targets [browser, server];
edit id "content:edit" execution transaction targets [browser, server];
}
}`,
{ source },
);
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
const contract = iface.resource.revision;
const compile = (policy = `crdt(${content})`, binding = "edit") =>
compileCapabilitySource(
`
workspace Comments id "w" revision "w@1" commit "${"a".repeat(40)}" {
import interface Comment;
atom Comment id "comment";
conform Comment as Comment id "owner:comment" {
private state Content id "content" on Comment : ${content} policy ${policy};
initialize { content to state Content; };
bind content.get to state Content.read;
bind content.edit to state Content.${binding};
}
}`,
"comments.qx",
{ interfaces: new Map([["Comment", contract]]), interfaceClosure: [contract] },
);
test("native edit is an explicit typed change capability over structured content", () => {
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const member = contract.members[0]!;
assert.equal(member.kind, "value");
if (member.kind !== "value") return;
assert.deepEqual(member.operations[1]!.inputType, { kind: "change", value: member.valueType });
assert.equal(result.workspace.conformances[0]!.operationBindings[1]!.binding.kind, "state");
});
test("edit cannot target a register, mismatched profile, or ordinary replacement", () => {
for (const [policy, binding] of [
["register", "edit"],
["crdt(bytes)", "edit"],
[`crdt(${content})`, "write"],
]) {
const result = compile(policy, binding);
assert.equal(result.ok, false);
assert.ok(
result.diagnostics.some((d) => d.code === "invalid-state-binding"),
JSON.stringify(result.diagnostics),
);
}
});
test("authored prepared change transport values fail explicitly until their runtime is implemented", () => {
const result = compileCapabilityResourceSource(
'package P id "p" revision "p@1" { function applyChange id "apply" : change<string> -> unit; }',
{ source },
);
assert.equal(result.ok, false);
assert.ok(
result.diagnostics.some((d) => d.code === "unsupported-prepared-change"),
JSON.stringify(result.diagnostics),
);
});
test("generic edit retains its materialized type through specialization", () => {
const generic = compileCapabilityResourceSource(
`interface Cell<value T : storable> id "cell" revision "cell@1" {
value content id "content" : T { get id "get"; edit id "edit" execution transaction targets [browser, server]; }
}`,
{ source },
);
assert.ok(generic.ok && generic.resource.kind === "interface", JSON.stringify(generic.diagnostics));
const result = compileCapabilitySource(
`workspace W id "w" revision "w@1" commit "${"a".repeat(40)}" {
import interface Cell;
atom Note id "note";
conform Note as Cell<${content}> {
private state Content id "content" on Note : ${content} policy crdt(${content});
bind content to state Content;
}
}`,
"cell.qx",
{ interfaces: new Map([["Cell", generic.resource.revision]]), interfaceClosure: [generic.resource.revision] },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const applied = result.workspace.interfaceImports.find((i) => i.application)!;
assert.equal(applied.members[0]!.operations[1]!.inputType.kind, "change");
});
+2 -2
View File
@@ -455,8 +455,8 @@ test("expanding recursive generic interfaces fail with a bounded diagnostic", ()
assert.equal(result.diagnostics[0].code, "recursive-application"); assert.equal(result.diagnostics[0].code, "recursive-application");
}); });
test("storable parameters do not imply support for RPC-only records", () => { test("storable records recursively require storable leaves", () => {
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), false); assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), true);
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true); assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
}); });
+1 -1
View File
@@ -34,7 +34,7 @@ test("queryable fields need one authoritative getter and supported values", () =
assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true); assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true);
for (const member of [ for (const member of [
'queryable value title id "title" : string { set id "title:set"; }', 'queryable value title id "title" : string { set id "title:set"; }',
'queryable value title id "title" : list<string> { get id "title:get"; }', 'queryable value title id "title" : list<unit> { get id "title:get"; }',
'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }', 'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }',
]) ])
assert.equal(compile(member).ok, false, member); assert.equal(compile(member).ok, false, member);
+86
View File
@@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { querySelectionToWire } from "../src/query/proto.js";
import { defaultQueryBudgets, type QueryDeclaration } from "../src/query/types.js";
const source = { repository: "https://example.test/structured.git", commit: "a".repeat(40) };
const result = compileCapabilityResourceSource(
`interface Highlight id "highlight" revision "highlight@1" {
queryable value range id "range" : record { sourceRevision:string; start:int32; end:int32; } { get id "range:get" execution snapshot targets [browser, server]; }
queryable value content id "content" : record { text:string; tags:list<string>; metadata:optional<record { score:int32; }>; } { get id "content:get" execution snapshot targets [browser, server]; }
}`,
{ source },
);
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
const iface = result.resource.revision;
const declaration: QueryDeclaration = {
id: "rows",
displayName: "Rows",
root: { kind: "workspace-interface", interface: iface.revisionId },
document: "rows.graphql",
operation: "Rows",
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: false,
};
const compile = (selection: string) =>
compileQuery(
declaration,
[iface],
async () =>
`query Rows {root {objects(first:10, ${selection}) {entries {node {position:range {begin:start end} content {text tags metadata {score}}}}}}}`,
);
test("record/list query values preserve nested projections and leaf predicates", async () => {
const checked = await compile(
"where:{range:{start:{gte:4}},content:{metadata:{score:{gt:2}}}}, orderBy:{range:{start:ASC}}",
);
const extent = checked.selection[0]!.selection[0]!;
assert.equal(extent.predicate?.kind, "and");
if (extent.predicate?.kind !== "and") return;
const expressions = extent.predicate.children.map((p) => (p.kind === "compare" ? p.expression : undefined));
assert.deepEqual(
expressions.map((e) => e?.leaf),
[
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "range", valuePath: ["start"] },
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "content", valuePath: ["metadata", "score"] },
],
);
assert.deepEqual(expressions[1]?.type, { kind: "optional", value: { kind: "scalar", name: "int32" } });
const wire = querySelectionToWire(extent);
assert.equal(wire.predicate?.predicate.case, "and");
if (wire.predicate?.predicate.case === "and") {
const child = wire.predicate.predicate.value.children[0]!;
assert.equal(child.predicate.case, "compare");
if (child.predicate.case === "compare") {
const operand = child.predicate.value.operand?.operand;
assert.ok(operand?.case === "expression" && operand.value.leaf.case === "field");
assert.deepEqual(operand.value.leaf.value.valuePath, ["start"]);
}
}
const raw = JSON.stringify(checked.output);
assert.match(raw, /"begin"/);
assert.match(raw, /"tags":\{"kind":"list"/);
assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1);
});
test("structured predicates reject whole-record scalar comparisons and implicit list expansion", async () => {
await assert.rejects(compile("where:{range:{eq:4}}"), /not defined/);
await assert.rejects(compile('where:{content:{tags:{eq:"x"}}}'), /not defined/);
});
test("explicit budgets can exceed defaults without exceeding wire bounds", () => {
const compile = (rows: string) =>
compileCapabilityResourceSource(
`import interface Highlight;
package P id "p" revision "p@1" {
query Rows id "rows" root Highlight document "rows.graphql" operation "Rows" { replica; max rows ${rows}; max result-bytes 8388608; }
}`,
{ source, environment: { interfaces: new Map([["Highlight", iface]]), interfaceClosure: [iface] } },
);
const result = compile("5000");
assert.ok(result.ok && result.resource.kind === "package", JSON.stringify(result.diagnostics));
assert.equal(result.resource.revision.queries![0]!.budgets.rows, 5000);
assert.equal(compile("4294967296").ok, false);
});