Add factory creation ports and owner initialization contracts

This commit is contained in:
Timothy J. Aveni
2026-09-19 22:20:55 -07:00
parent 03ac7f8557
commit 0555c929b1
25 changed files with 3899 additions and 3012 deletions
+20 -2
View File
@@ -222,6 +222,7 @@ dependencyPort
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI | STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI | EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? SEMI | INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? SEMI
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI | CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI | QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
; ;
@@ -255,7 +256,7 @@ stateDecl
; ;
storagePolicy storagePolicy
: OPTIMISTIC_REGISTER : REGISTER
| CRDT LPAREN valueType RPAREN | CRDT LPAREN valueType RPAREN
; ;
@@ -277,9 +278,23 @@ conformanceItem
: PRIVATE attachmentDecl : PRIVATE attachmentDecl
| operationBindingDecl | operationBindingDecl
| stateFieldBindingDecl | stateFieldBindingDecl
| initializationDecl
| relationshipMaterializationDecl | relationshipMaterializationDecl
; ;
initializationDecl
: INITIALIZE LBRACE initializationField* RBRACE SEMI
;
initializationField
: identifier TO STATE identifier SEMI
| identifier TO EDGE identifier DOT identifier SEMI
;
creationOwnerMapping
: interfaceType FROM INPUT (DOT identifier)* SEMI
;
stateFieldBindingDecl stateFieldBindingDecl
: BIND identifier TO STATE identifier SEMI : BIND identifier TO STATE identifier SEMI
; ;
@@ -341,6 +356,7 @@ dependencyBinding
| identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI | identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI | identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI | identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI
| identifier TO CREATION identifier WITH LBRACE creationOwnerMapping* RBRACE SEMI
| identifier TO CONSTRUCTOR identifier SEMI | identifier TO CONSTRUCTOR identifier SEMI
| identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI | identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
; ;
@@ -433,6 +449,8 @@ stringLiteral
: STRING_LITERAL : STRING_LITERAL
; ;
CREATION: 'creation';
INITIALIZE: 'initialize';
WORKSPACE: 'workspace'; WORKSPACE: 'workspace';
EXECUTION: 'execution'; EXECUTION: 'execution';
SNAPSHOT: 'snapshot'; SNAPSHOT: 'snapshot';
@@ -528,7 +546,7 @@ WATCH_START: 'watch-start';
WATCH_STOP: 'watch-stop'; WATCH_STOP: 'watch-stop';
SUBSCRIBE: 'subscribe'; SUBSCRIBE: 'subscribe';
UNSUBSCRIBE: 'unsubscribe'; UNSUBSCRIBE: 'unsubscribe';
OPTIMISTIC_REGISTER: 'optimistic-register'; REGISTER: 'register';
CRDT: 'crdt'; CRDT: 'crdt';
OPTIONAL_ONE: 'optional-one'; OPTIONAL_ONE: 'optional-one';
EXACTLY_ONE: 'exactly-one'; EXACTLY_ONE: 'exactly-one';
+2
View File
@@ -115,6 +115,8 @@ export const genericImplementationType = (
} }
case "query": case "query":
return queryPort(requirement); return queryPort(requirement);
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor": case "constructor":
return object([ return object([
[ [
+2
View File
@@ -249,6 +249,8 @@ export const generateTypeScriptBindings = (
}, },
}; };
} }
case "creation":
throw new Error("Creation ports require the portable transaction context generator (LF-09)");
case "constructor": { case "constructor": {
const input = requirement.inputType; const input = requirement.inputType;
if (!input) if (!input)
File diff suppressed because one or more lines are too long
@@ -1,279 +1,283 @@
WORKSPACE=1 CREATION=1
EXECUTION=2 INITIALIZE=2
SNAPSHOT=3 WORKSPACE=3
TRANSACTION=4 EXECUTION=4
REMOTE=5 SNAPSHOT=5
TARGETS=6 TRANSACTION=6
PORTABLE=7 REMOTE=7
REPLICA=8 TARGETS=8
LOCAL=9 PORTABLE=9
FROM=10 REPLICA=10
RESULT_BYTES=11 LOCAL=11
RPC_CALLS=12 FROM=12
DEADLINE_MS=13 RESULT_BYTES=13
QUERY=14 RPC_CALLS=14
REFLECTION=15 DEADLINE_MS=15
SPECIALIZE=16 QUERY=16
ROOT=17 REFLECTION=17
DOCUMENT=18 SPECIALIZE=18
FRAGMENTS=19 ROOT=19
VIEW=20 DOCUMENT=20
MAX=21 FRAGMENTS=21
ALLOW=22 VIEW=22
POLL=23 MAX=23
QUERYABLE=24 ALLOW=24
RPC=25 POLL=25
QUERY_REASON=26 QUERYABLE=26
TYPE=27 RPC=27
OBJECT=28 QUERY_REASON=28
STORABLE=29 TYPE=29
IMPLEMENTS=30 OBJECT=30
REF=31 STORABLE=31
FRAGMENT=32 IMPLEMENTS=32
IMPORT=33 REF=33
EXTERNAL=34 FRAGMENT=34
ATOM=35 IMPORT=35
INTERFACE=36 EXTERNAL=36
INTERFACES=37 ATOM=37
PACKAGE=38 INTERFACE=38
VALUE=39 INTERFACES=39
RELATION=40 PACKAGE=40
OPERATION=41 VALUE=41
FUNCTION=42 RELATION=42
CONSTRUCTOR=43 OPERATION=43
CONSTRUCTS=44 FUNCTION=44
INPUT=45 CONSTRUCTOR=45
CONFORM=46 CONSTRUCTS=46
AS=47 INPUT=47
BIND=48 CONFORM=48
STATIC=49 AS=49
TO=50 BIND=50
PRIVATE=51 STATIC=51
SHARED=52 TO=52
STATE=53 PRIVATE=53
EDGE=54 SHARED=54
PROJECTION=55 STATE=55
WITH=56 EDGE=56
USING=57 PROJECTION=57
VIA=58 WITH=58
MATERIALIZE=59 USING=59
IF=60 VIA=60
ABSENT=61 MATERIALIZE=61
ON=62 IF=62
POLICY=63 ABSENT=63
DEFAULT=64 ON=64
SOURCE=65 POLICY=65
REPOSITORY=66 DEFAULT=66
COMMIT=67 SOURCE=67
REVISION=68 REPOSITORY=68
SEMANTIC_MAJOR=69 COMMIT=69
ON_DELETE=70 REVISION=70
RETAIN_OTHER=71 SEMANTIC_MAJOR=71
KEYED=72 ON_DELETE=72
PUBLIC_TRAVERSAL=73 RETAIN_OTHER=73
ID=74 KEYED=74
DOC=75 PUBLIC_TRAVERSAL=75
MODE=76 ID=76
EMITS=77 DOC=77
RECEIVER=78 MODE=78
REQUIRES=79 EMITS=79
ANY=80 RECEIVER=80
GET=81 REQUIRES=81
SET=82 ANY=82
WATCH=83 GET=83
START=84 SET=84
STOP=85 WATCH=85
READ=86 START=86
WRITE=87 STOP=87
RESOLVE=88 READ=88
CONNECT=89 WRITE=89
DISCONNECT=90 RESOLVE=90
CALL=91 CONNECT=91
WATCH_START=92 DISCONNECT=92
WATCH_STOP=93 CALL=93
SUBSCRIBE=94 WATCH_START=94
UNSUBSCRIBE=95 WATCH_STOP=95
OPTIMISTIC_REGISTER=96 SUBSCRIBE=96
CRDT=97 UNSUBSCRIBE=97
OPTIONAL_ONE=98 REGISTER=98
EXACTLY_ONE=99 CRDT=99
MANY_UNIQUE=100 OPTIONAL_ONE=100
MANY=101 EXACTLY_ONE=101
ORDERED=102 MANY_UNIQUE=102
UNIT=103 MANY=103
WATCH_HANDLE=104 ORDERED=104
MESSAGE=105 UNIT=105
ATOM_REF=106 WATCH_HANDLE=106
INTERFACE_REF=107 MESSAGE=107
OPTIONAL=108 ATOM_REF=108
LIST=109 INTERFACE_REF=109
RECORD=110 OPTIONAL=110
BOOL=111 LIST=111
BYTES=112 RECORD=112
DOUBLE=113 BOOL=113
INT32=114 BYTES=114
INT64=115 DOUBLE=115
STRING=116 INT32=116
UINT32=117 INT64=117
UINT64=118 STRING=118
TRUE=119 UINT32=119
FALSE=120 UINT64=120
NULL=121 TRUE=121
ARROW=122 FALSE=122
COLON=123 NULL=123
SEMI=124 ARROW=124
COMMA=125 COLON=125
DOT=126 SEMI=126
LBRACE=127 COMMA=127
RBRACE=128 DOT=128
LBRACK=129 LBRACE=129
RBRACK=130 RBRACE=130
LPAREN=131 LBRACK=131
RPAREN=132 RBRACK=132
LT=133 LPAREN=133
GT=134 RPAREN=134
AMP=135 LT=135
EQUAL=136 GT=136
INTEGER=137 AMP=137
JSON_NUMBER=138 EQUAL=138
IDENTIFIER=139 INTEGER=139
STRING_LITERAL=140 JSON_NUMBER=140
LINE_COMMENT=141 IDENTIFIER=141
BLOCK_COMMENT=142 STRING_LITERAL=142
WS=143 LINE_COMMENT=143
'workspace'=1 BLOCK_COMMENT=144
'execution'=2 WS=145
'snapshot'=3 'creation'=1
'transaction'=4 'initialize'=2
'remote'=5 'workspace'=3
'targets'=6 'execution'=4
'portable'=7 'snapshot'=5
'replica'=8 'transaction'=6
'local'=9 'remote'=7
'from'=10 'targets'=8
'result-bytes'=11 'portable'=9
'rpc-calls'=12 'replica'=10
'deadline-ms'=13 'local'=11
'query'=14 'from'=12
'reflection'=15 'result-bytes'=13
'specialize'=16 'rpc-calls'=14
'root'=17 'deadline-ms'=15
'document'=18 'query'=16
'fragments'=19 'reflection'=17
'view'=20 'specialize'=18
'max'=21 'root'=19
'allow'=22 'document'=20
'poll'=23 'fragments'=21
'queryable'=24 'view'=22
'rpc'=25 'max'=23
'query-reason'=26 'allow'=24
'type'=27 'poll'=25
'object'=28 'queryable'=26
'storable'=29 'rpc'=27
'implements'=30 'query-reason'=28
'ref'=31 'type'=29
'fragment'=32 'object'=30
'import'=33 'storable'=31
'external'=34 'implements'=32
'atom'=35 'ref'=33
'interface'=36 'fragment'=34
'interfaces'=37 'import'=35
'package'=38 'external'=36
'value'=39 'atom'=37
'relation'=40 'interface'=38
'operation'=41 'interfaces'=39
'function'=42 'package'=40
'constructor'=43 'value'=41
'constructs'=44 'relation'=42
'input'=45 'operation'=43
'conform'=46 'function'=44
'as'=47 'constructor'=45
'bind'=48 'constructs'=46
'static'=49 'input'=47
'to'=50 'conform'=48
'private'=51 'as'=49
'shared'=52 'bind'=50
'state'=53 'static'=51
'edge'=54 'to'=52
'projection'=55 'private'=53
'with'=56 'shared'=54
'using'=57 'state'=55
'via'=58 'edge'=56
'materialize'=59 'projection'=57
'if'=60 'with'=58
'absent'=61 'using'=59
'on'=62 'via'=60
'policy'=63 'materialize'=61
'default'=64 'if'=62
'source'=65 'absent'=63
'repository'=66 'on'=64
'commit'=67 'policy'=65
'revision'=68 'default'=66
'semantic-major'=69 'source'=67
'on-delete'=70 'repository'=68
'retain-other'=71 'commit'=69
'keyed'=72 'revision'=70
'public-traversal'=73 'semantic-major'=71
'id'=74 'on-delete'=72
'doc'=75 'retain-other'=73
'mode'=76 'keyed'=74
'emits'=77 'public-traversal'=75
'receiver'=78 'id'=76
'requires'=79 'doc'=77
'any'=80 'mode'=78
'get'=81 'emits'=79
'set'=82 'receiver'=80
'watch'=83 'requires'=81
'start'=84 'any'=82
'stop'=85 'get'=83
'read'=86 'set'=84
'write'=87 'watch'=85
'resolve'=88 'start'=86
'connect'=89 'stop'=87
'disconnect'=90 'read'=88
'call'=91 'write'=89
'watch-start'=92 'resolve'=90
'watch-stop'=93 'connect'=91
'subscribe'=94 'disconnect'=92
'unsubscribe'=95 'call'=93
'optimistic-register'=96 'watch-start'=94
'crdt'=97 'watch-stop'=95
'optional-one'=98 'subscribe'=96
'exactly-one'=99 'unsubscribe'=97
'many-unique'=100 'register'=98
'many'=101 'crdt'=99
'ordered'=102 'optional-one'=100
'unit'=103 'exactly-one'=101
'watch-handle'=104 'many-unique'=102
'message'=105 'many'=103
'atom-ref'=106 'ordered'=104
'interface-ref'=107 'unit'=105
'optional'=108 'watch-handle'=106
'list'=109 'message'=107
'record'=110 'atom-ref'=108
'bool'=111 'interface-ref'=109
'bytes'=112 'optional'=110
'double'=113 'list'=111
'int32'=114 'record'=112
'int64'=115 'bool'=113
'string'=116 'bytes'=114
'uint32'=117 'double'=115
'uint64'=118 'int32'=116
'true'=119 'int64'=117
'false'=120 'string'=118
'null'=121 'uint32'=119
'->'=122 'uint64'=120
':'=123 'true'=121
';'=124 'false'=122
','=125 'null'=123
'.'=126 '->'=124
'{'=127 ':'=125
'}'=128 ';'=126
'['=129 ','=127
']'=130 '.'=128
'('=131 '{'=129
')'=132 '}'=130
'<'=133 '['=131
'>'=134 ']'=132
'&'=135 '('=133
'='=136 ')'=134
'<'=135
'>'=136
'&'=137
'='=138
File diff suppressed because one or more lines are too long
@@ -1,279 +1,283 @@
WORKSPACE=1 CREATION=1
EXECUTION=2 INITIALIZE=2
SNAPSHOT=3 WORKSPACE=3
TRANSACTION=4 EXECUTION=4
REMOTE=5 SNAPSHOT=5
TARGETS=6 TRANSACTION=6
PORTABLE=7 REMOTE=7
REPLICA=8 TARGETS=8
LOCAL=9 PORTABLE=9
FROM=10 REPLICA=10
RESULT_BYTES=11 LOCAL=11
RPC_CALLS=12 FROM=12
DEADLINE_MS=13 RESULT_BYTES=13
QUERY=14 RPC_CALLS=14
REFLECTION=15 DEADLINE_MS=15
SPECIALIZE=16 QUERY=16
ROOT=17 REFLECTION=17
DOCUMENT=18 SPECIALIZE=18
FRAGMENTS=19 ROOT=19
VIEW=20 DOCUMENT=20
MAX=21 FRAGMENTS=21
ALLOW=22 VIEW=22
POLL=23 MAX=23
QUERYABLE=24 ALLOW=24
RPC=25 POLL=25
QUERY_REASON=26 QUERYABLE=26
TYPE=27 RPC=27
OBJECT=28 QUERY_REASON=28
STORABLE=29 TYPE=29
IMPLEMENTS=30 OBJECT=30
REF=31 STORABLE=31
FRAGMENT=32 IMPLEMENTS=32
IMPORT=33 REF=33
EXTERNAL=34 FRAGMENT=34
ATOM=35 IMPORT=35
INTERFACE=36 EXTERNAL=36
INTERFACES=37 ATOM=37
PACKAGE=38 INTERFACE=38
VALUE=39 INTERFACES=39
RELATION=40 PACKAGE=40
OPERATION=41 VALUE=41
FUNCTION=42 RELATION=42
CONSTRUCTOR=43 OPERATION=43
CONSTRUCTS=44 FUNCTION=44
INPUT=45 CONSTRUCTOR=45
CONFORM=46 CONSTRUCTS=46
AS=47 INPUT=47
BIND=48 CONFORM=48
STATIC=49 AS=49
TO=50 BIND=50
PRIVATE=51 STATIC=51
SHARED=52 TO=52
STATE=53 PRIVATE=53
EDGE=54 SHARED=54
PROJECTION=55 STATE=55
WITH=56 EDGE=56
USING=57 PROJECTION=57
VIA=58 WITH=58
MATERIALIZE=59 USING=59
IF=60 VIA=60
ABSENT=61 MATERIALIZE=61
ON=62 IF=62
POLICY=63 ABSENT=63
DEFAULT=64 ON=64
SOURCE=65 POLICY=65
REPOSITORY=66 DEFAULT=66
COMMIT=67 SOURCE=67
REVISION=68 REPOSITORY=68
SEMANTIC_MAJOR=69 COMMIT=69
ON_DELETE=70 REVISION=70
RETAIN_OTHER=71 SEMANTIC_MAJOR=71
KEYED=72 ON_DELETE=72
PUBLIC_TRAVERSAL=73 RETAIN_OTHER=73
ID=74 KEYED=74
DOC=75 PUBLIC_TRAVERSAL=75
MODE=76 ID=76
EMITS=77 DOC=77
RECEIVER=78 MODE=78
REQUIRES=79 EMITS=79
ANY=80 RECEIVER=80
GET=81 REQUIRES=81
SET=82 ANY=82
WATCH=83 GET=83
START=84 SET=84
STOP=85 WATCH=85
READ=86 START=86
WRITE=87 STOP=87
RESOLVE=88 READ=88
CONNECT=89 WRITE=89
DISCONNECT=90 RESOLVE=90
CALL=91 CONNECT=91
WATCH_START=92 DISCONNECT=92
WATCH_STOP=93 CALL=93
SUBSCRIBE=94 WATCH_START=94
UNSUBSCRIBE=95 WATCH_STOP=95
OPTIMISTIC_REGISTER=96 SUBSCRIBE=96
CRDT=97 UNSUBSCRIBE=97
OPTIONAL_ONE=98 REGISTER=98
EXACTLY_ONE=99 CRDT=99
MANY_UNIQUE=100 OPTIONAL_ONE=100
MANY=101 EXACTLY_ONE=101
ORDERED=102 MANY_UNIQUE=102
UNIT=103 MANY=103
WATCH_HANDLE=104 ORDERED=104
MESSAGE=105 UNIT=105
ATOM_REF=106 WATCH_HANDLE=106
INTERFACE_REF=107 MESSAGE=107
OPTIONAL=108 ATOM_REF=108
LIST=109 INTERFACE_REF=109
RECORD=110 OPTIONAL=110
BOOL=111 LIST=111
BYTES=112 RECORD=112
DOUBLE=113 BOOL=113
INT32=114 BYTES=114
INT64=115 DOUBLE=115
STRING=116 INT32=116
UINT32=117 INT64=117
UINT64=118 STRING=118
TRUE=119 UINT32=119
FALSE=120 UINT64=120
NULL=121 TRUE=121
ARROW=122 FALSE=122
COLON=123 NULL=123
SEMI=124 ARROW=124
COMMA=125 COLON=125
DOT=126 SEMI=126
LBRACE=127 COMMA=127
RBRACE=128 DOT=128
LBRACK=129 LBRACE=129
RBRACK=130 RBRACE=130
LPAREN=131 LBRACK=131
RPAREN=132 RBRACK=132
LT=133 LPAREN=133
GT=134 RPAREN=134
AMP=135 LT=135
EQUAL=136 GT=136
INTEGER=137 AMP=137
JSON_NUMBER=138 EQUAL=138
IDENTIFIER=139 INTEGER=139
STRING_LITERAL=140 JSON_NUMBER=140
LINE_COMMENT=141 IDENTIFIER=141
BLOCK_COMMENT=142 STRING_LITERAL=142
WS=143 LINE_COMMENT=143
'workspace'=1 BLOCK_COMMENT=144
'execution'=2 WS=145
'snapshot'=3 'creation'=1
'transaction'=4 'initialize'=2
'remote'=5 'workspace'=3
'targets'=6 'execution'=4
'portable'=7 'snapshot'=5
'replica'=8 'transaction'=6
'local'=9 'remote'=7
'from'=10 'targets'=8
'result-bytes'=11 'portable'=9
'rpc-calls'=12 'replica'=10
'deadline-ms'=13 'local'=11
'query'=14 'from'=12
'reflection'=15 'result-bytes'=13
'specialize'=16 'rpc-calls'=14
'root'=17 'deadline-ms'=15
'document'=18 'query'=16
'fragments'=19 'reflection'=17
'view'=20 'specialize'=18
'max'=21 'root'=19
'allow'=22 'document'=20
'poll'=23 'fragments'=21
'queryable'=24 'view'=22
'rpc'=25 'max'=23
'query-reason'=26 'allow'=24
'type'=27 'poll'=25
'object'=28 'queryable'=26
'storable'=29 'rpc'=27
'implements'=30 'query-reason'=28
'ref'=31 'type'=29
'fragment'=32 'object'=30
'import'=33 'storable'=31
'external'=34 'implements'=32
'atom'=35 'ref'=33
'interface'=36 'fragment'=34
'interfaces'=37 'import'=35
'package'=38 'external'=36
'value'=39 'atom'=37
'relation'=40 'interface'=38
'operation'=41 'interfaces'=39
'function'=42 'package'=40
'constructor'=43 'value'=41
'constructs'=44 'relation'=42
'input'=45 'operation'=43
'conform'=46 'function'=44
'as'=47 'constructor'=45
'bind'=48 'constructs'=46
'static'=49 'input'=47
'to'=50 'conform'=48
'private'=51 'as'=49
'shared'=52 'bind'=50
'state'=53 'static'=51
'edge'=54 'to'=52
'projection'=55 'private'=53
'with'=56 'shared'=54
'using'=57 'state'=55
'via'=58 'edge'=56
'materialize'=59 'projection'=57
'if'=60 'with'=58
'absent'=61 'using'=59
'on'=62 'via'=60
'policy'=63 'materialize'=61
'default'=64 'if'=62
'source'=65 'absent'=63
'repository'=66 'on'=64
'commit'=67 'policy'=65
'revision'=68 'default'=66
'semantic-major'=69 'source'=67
'on-delete'=70 'repository'=68
'retain-other'=71 'commit'=69
'keyed'=72 'revision'=70
'public-traversal'=73 'semantic-major'=71
'id'=74 'on-delete'=72
'doc'=75 'retain-other'=73
'mode'=76 'keyed'=74
'emits'=77 'public-traversal'=75
'receiver'=78 'id'=76
'requires'=79 'doc'=77
'any'=80 'mode'=78
'get'=81 'emits'=79
'set'=82 'receiver'=80
'watch'=83 'requires'=81
'start'=84 'any'=82
'stop'=85 'get'=83
'read'=86 'set'=84
'write'=87 'watch'=85
'resolve'=88 'start'=86
'connect'=89 'stop'=87
'disconnect'=90 'read'=88
'call'=91 'write'=89
'watch-start'=92 'resolve'=90
'watch-stop'=93 'connect'=91
'subscribe'=94 'disconnect'=92
'unsubscribe'=95 'call'=93
'optimistic-register'=96 'watch-start'=94
'crdt'=97 'watch-stop'=95
'optional-one'=98 'subscribe'=96
'exactly-one'=99 'unsubscribe'=97
'many-unique'=100 'register'=98
'many'=101 'crdt'=99
'ordered'=102 'optional-one'=100
'unit'=103 'exactly-one'=101
'watch-handle'=104 'many-unique'=102
'message'=105 'many'=103
'atom-ref'=106 'ordered'=104
'interface-ref'=107 'unit'=105
'optional'=108 'watch-handle'=106
'list'=109 'message'=107
'record'=110 'atom-ref'=108
'bool'=111 'interface-ref'=109
'bytes'=112 'optional'=110
'double'=113 'list'=111
'int32'=114 'record'=112
'int64'=115 'bool'=113
'string'=116 'bytes'=114
'uint32'=117 'double'=115
'uint64'=118 'int32'=116
'true'=119 'int64'=117
'false'=120 'string'=118
'null'=121 'uint32'=119
'->'=122 'uint64'=120
':'=123 'true'=121
';'=124 'false'=122
','=125 'null'=123
'.'=126 '->'=124
'{'=127 ':'=125
'}'=128 ';'=126
'['=129 ','=127
']'=130 '.'=128
'('=131 '{'=129
')'=132 '}'=130
'<'=133 '['=131
'>'=134 ']'=132
'&'=135 '('=133
'='=136 ')'=134
'<'=135
'>'=136
'&'=137
'='=138
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -53,6 +53,9 @@ import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js"; import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js"; import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
import { ConformanceItemContext } from "./QuixosCapabilityParser.js"; import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
import { InitializationDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationFieldContext } from "./QuixosCapabilityParser.js";
import { CreationOwnerMappingContext } from "./QuixosCapabilityParser.js";
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js"; import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js"; import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js"; import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
@@ -390,6 +393,24 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitConformanceItem?: (ctx: ConformanceItemContext) => Result; visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationDecl?: (ctx: InitializationDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationField`.
* @param ctx the parse tree
* @return the visitor result
*/
visitInitializationField?: (ctx: InitializationFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.creationOwnerMapping`.
* @param ctx the parse tree
* @return the visitor result
*/
visitCreationOwnerMapping?: (ctx: CreationOwnerMappingContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`. * Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
* @param ctx the parse tree * @param ctx the parse tree
+69 -4
View File
@@ -1009,6 +1009,14 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
: undefined; : undefined;
} }
const atomId = requireSymbol(state, state.atoms, targetName, context, "atom"); const atomId = requireSymbol(state, state.atoms, targetName, context, "atom");
if (context.CREATION())
return atomId
? {
id,
displayName: name,
requirement: { kind: "creation", atomId, inputType: lowerValueType(state, context.valueType()) },
}
: undefined;
return atomId return atomId
? { ? {
id, id,
@@ -1229,7 +1237,11 @@ const lowerGenericPackageExport = (
throw new GenericTypeError("missing-constructor-input", id, "Constructor ports require an explicit input type"); throw new GenericTypeError("missing-constructor-input", id, "Constructor ports require an explicit input type");
return { return {
...base, ...base,
requirement: { kind: "constructor", target, inputType: state.types.expression(port.valueType()!) }, requirement: {
kind: port.CREATION() ? "creation" : "constructor",
target,
inputType: state.types.expression(port.valueType()!),
},
}; };
}); });
const operation = "receiverRequirement" in context ? context : undefined; const operation = "receiverRequirement" in context ? context : undefined;
@@ -1567,8 +1579,8 @@ const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotD
attachedTo: atomId, attachedTo: atomId,
displayName: identifier(context.identifier(0)), displayName: identifier(context.identifier(0)),
valueType: lowerValueType(state, context.valueType()), valueType: lowerValueType(state, context.valueType()),
storagePolicy: policy.OPTIMISTIC_REGISTER() storagePolicy: policy.REGISTER()
? { kind: "optimistic-register" } ? { kind: "register" }
: { : {
kind: "crdt-document", kind: "crdt-document",
updateType: lowerValueType(state, policy.valueType()), updateType: lowerValueType(state, policy.valueType()),
@@ -1774,6 +1786,24 @@ const lowerBoundDependencies = (
} }
const atomName = identifier(entry.identifier(1)); const atomName = identifier(entry.identifier(1));
const atomId = requireSymbol(state, state.atoms, atomName, entry, "atom"); const atomId = requireSymbol(state, state.atoms, atomName, entry, "atom");
if (entry.CREATION())
return atomId
? [
{
portId,
binding: {
kind: "creation",
atomId,
owners: entry.creationOwnerMapping().map((owner) => ({
interfaceRevisionId: new TypeSubstitution(state.types.environment()).application(
state.types.interface(owner.interfaceType()),
),
inputPath: owner.identifier().map(identifier),
})),
},
},
]
: [];
return atomId ? [{ portId, binding: { kind: "constructor", atomId } }] : []; return atomId ? [{ portId, binding: { kind: "constructor", atomId } }] : [];
}); });
}; };
@@ -2025,8 +2055,43 @@ const lowerConformance = (
const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization); const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization);
return lowered ? [lowered] : []; return lowered ? [lowered] : [];
}); });
const initializers = context
.conformanceItem()
.flatMap((item) => (item.initializationDecl() ? [item.initializationDecl()!] : []));
if (initializers.length > 1)
loweringIssue(state, context, "duplicate-initialization", "Declare one initialization mapping per owner");
const initialization = initializers.flatMap((initializer) =>
initializer.initializationField().flatMap((field): import("../capability-model/types.js").InitializationField[] => {
const input = identifier(field.identifier(0));
const attachment = requireSymbol(
state,
state.attachments,
identifier(field.identifier(1)),
field,
"initialization attachment",
);
if (!attachment) return [];
if (field.STATE() && attachment.attachment.kind === "state")
return [{ input, binding: { kind: "state", slotId: attachment.attachment.id } }];
if (field.EDGE() && attachment.attachment.kind === "edge") {
const projectionId = requireSymbol(
state,
attachment.projections,
identifier(field.identifier(2)),
field,
"initialization projection",
);
return projectionId
? [{ input, binding: { kind: "edge", edgeTypeId: attachment.attachment.id, projectionId } }]
: [];
}
loweringIssue(state, field, "invalid-initialization", "Initialization attachment kind does not match");
return [];
}),
);
state.interfaces.set(interfaceName, originalSymbol); state.interfaces.set(interfaceName, originalSymbol);
return { return {
...(initializers.length ? { initialization } : {}),
atomId, atomId,
interfaceRevisionId: interfaceSymbol.revisionId, interfaceRevisionId: interfaceSymbol.revisionId,
...(context.stringLiteral() ? { id: capabilityId.conformance(stringValue(context.stringLiteral()!)) } : {}), ...(context.stringLiteral() ? { id: capabilityId.conformance(stringValue(context.stringLiteral()!)) } : {}),
@@ -2562,7 +2627,7 @@ const compileCapabilityResourceSourceInternal = (
types.push(requirement.valueType); types.push(requirement.valueType);
} }
if (requirement.kind === "edge") types.push({ kind: "object-ref", expectation: requirement.target }); if (requirement.kind === "edge") types.push({ kind: "object-ref", expectation: requirement.target });
if (requirement.kind === "constructor") if (requirement.kind === "constructor" || requirement.kind === "creation")
types.push(requirement.inputType, { kind: "object-ref", expectation: requirement.target }); types.push(requirement.inputType, { kind: "object-ref", expectation: requirement.target });
if (requirement.kind === "interface") requires.push(requirement.application); if (requirement.kind === "interface") requires.push(requirement.application);
} }
+7
View File
@@ -149,6 +149,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
} }
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`); if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`); if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
if (binding.kind === "creation") {
for (const owner of binding.owners)
parent.dependencies.add(conformanceKey(binding.atomId, owner.interfaceRevisionId));
}
if (binding.kind === "constructor") { if (binding.kind === "constructor") {
parent.dependencies.add(`constructor:${binding.atomId}`); parent.dependencies.add(`constructor:${binding.atomId}`);
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId); const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
@@ -210,6 +214,9 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
atomId: conformance.atomId, atomId: conformance.atomId,
interfaceRevisionId: conformance.interfaceRevisionId, interfaceRevisionId: conformance.interfaceRevisionId,
operations: sorted(conformance.operationBindings, (entry) => entry.operationId), operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
...(conformance.initialization
? { initialization: sorted(conformance.initialization, (entry) => entry.input) }
: {}),
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId), materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
}); });
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`); parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
+2
View File
@@ -89,6 +89,8 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId); const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId);
if (query?.placement?.kind !== "local") if (query?.placement?.kind !== "local")
fail(portPath, "Portable operations require a local query dependency"); fail(portPath, "Portable operations require a local query dependency");
} else if (requirement.kind === "creation") {
if (entry.execution.kind !== "transaction") fail(portPath, "Object creation requires transaction execution");
} else if (requirement.kind !== "interface") } else if (requirement.kind !== "interface")
fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`); fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`);
} }
+3 -1
View File
@@ -39,6 +39,7 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
application: InterfaceApplicationExpression; application: InterfaceApplicationExpression;
operations?: import("./types.js").OperationId[]; operations?: import("./types.js").OperationId[];
} }
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression }; | { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
}; };
@@ -101,6 +102,7 @@ export const specializePackageExport = (
...(requirement.operations ? { operations: requirement.operations } : {}), ...(requirement.operations ? { operations: requirement.operations } : {}),
}, },
}; };
case "creation":
case "constructor": { case "constructor": {
const target = substitution.object(requirement.target); const target = substitution.object(requirement.target);
if (target.kind !== "atom") if (target.kind !== "atom")
@@ -112,7 +114,7 @@ export const specializePackageExport = (
return { return {
...port, ...port,
requirement: { requirement: {
kind: "constructor", kind: requirement.kind,
atomId: target.atomId, atomId: target.atomId,
inputType: substitution.value(requirement.inputType), inputType: substitution.value(requirement.inputType),
}, },
+121
View File
@@ -0,0 +1,121 @@
import type {
BoundDependency,
Conformance,
DependencyPort,
InitializationField,
ValueType,
WorkspaceRevision,
} from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { cardinalityValueType, valueTypesEqual } from "./validation.js";
/** Creation grants seed inputs on new objects, never general private-state authority. */
export function validateInitialization(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-initialization", path, message });
const ownerKey = (owner: Conformance) => `${owner.atomId}/${owner.interfaceRevisionId}`;
const shapes = new Map<string, ValueType>();
const fieldType = (owner: Conformance, field: InitializationField): ValueType | undefined => {
const binding = field.binding;
const attachment = owner.privateAttachments.find(
(a) => a.kind === binding.kind && a.id === (binding.kind === "state" ? binding.slotId : binding.edgeTypeId),
);
if (!attachment) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`);
return;
}
if (attachment.kind === "state") return attachment.valueType;
if (binding.kind !== "edge") return;
const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId);
const endpoint = attachment.endpoints[index];
const target = attachment.endpoints[1 - index];
if (!endpoint || !target || endpoint.constraint.kind !== "atom" || endpoint.constraint.atomId !== owner.atomId) {
fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`);
return;
}
if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") {
const fields: Record<string, ValueType> = {
key: { kind: "scalar", name: "string" },
target: cardinalityValueType(target.constraint, "exactly-one"),
};
if (endpoint.keyType)
fields.mapKey = { kind: "scalar", name: endpoint.keyType === "boolean" ? "bool" : endpoint.keyType };
return { kind: "list", value: { kind: "record", fields } };
}
return cardinalityValueType(target.constraint, endpoint.cardinality);
};
for (const owner of workspace.conformances) {
if (!owner.initialization) continue;
const fields: Record<string, ValueType> = Object.create(null);
const destinations = new Set<string>();
for (const field of owner.initialization) {
const path = `${ownerKey(owner)}/${field.input}`;
if (!field.input || Object.hasOwn(fields, field.input))
fail(path, "Initialization input names must be nonempty and distinct");
const destination = JSON.stringify(field.binding);
if (destinations.has(destination)) fail(path, "An attachment may have only one initialization input");
destinations.add(destination);
const type = fieldType(owner, field);
if (type) fields[field.input] = type;
}
shapes.set(ownerKey(owner), { kind: "record", fields });
}
const checkBindings = (ports: DependencyPort[], dependencies: BoundDependency[], path: string) => {
for (const dependency of dependencies) {
if (dependency.binding.kind !== "creation") continue;
const binding = dependency.binding;
const requirement = ports.find((p) => p.id === dependency.portId)?.requirement;
if (requirement?.kind !== "creation") continue; // General binding validation reports the kind mismatch.
const portPath = `${path}/${dependency.portId}`;
if (binding.atomId !== requirement.atomId) fail(portPath, `Creation port requires atom ${requirement.atomId}`);
const initialized = new Set<string>();
const supplied = new Set<string>();
for (const mapping of binding.owners) {
const key = `${binding.atomId}/${mapping.interfaceRevisionId}`;
if (supplied.has(key)) fail(portPath, `Duplicate initialization owner ${key}`);
supplied.add(key);
const owner = workspace.conformances.find((o) => ownerKey(o) === key);
const shape = shapes.get(key);
if (!owner || !shape) {
fail(portPath, `Owner ${key} has no declared initialization mapping`);
continue;
}
let seed: ValueType | undefined = requirement.inputType;
for (const component of mapping.inputPath) seed = seed?.kind === "record" ? seed.fields[component] : undefined;
if (!seed || !valueTypesEqual(seed, shape))
fail(
portPath,
`Seed input${mapping.inputPath.map((p) => `.${p}`).join("")} does not match initialization inputs of ${key}`,
);
for (const field of owner.initialization!)
if (field.binding.kind === "state") initialized.add(field.binding.slotId);
}
const states = [
...workspace.sharedAttachments,
...workspace.conformances.flatMap((o) => o.privateAttachments),
].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId);
for (const state of states)
if (state.kind === "state" && state.defaultValue === undefined && !initialized.has(state.id))
fail(portPath, `Required state ${state.id} has no default or declared seed mapping`);
}
};
const exports = new Map(
workspace.packageImports.flatMap((p) => p.exports.map((e) => [`${p.revisionId}/${e.id}`, e] as const)),
);
for (const owner of workspace.conformances)
for (const operation of owner.operationBindings) {
if (operation.binding.kind !== "package") continue;
const entry = exports.get(`${operation.binding.packageRevisionId}/${operation.binding.exportId}`);
if (entry)
checkBindings(
entry.dependencyPorts,
operation.binding.dependencies,
`${ownerKey(owner)}/${operation.operationId}`,
);
}
for (const constructor of workspace.constructors) {
const entry = exports.get(`${constructor.packageRevisionId}/${constructor.exportId}`);
if (entry) checkBindings(entry.dependencyPorts, constructor.dependencies, `constructor/${constructor.atomId}`);
}
return issues;
}
+14 -1
View File
@@ -181,7 +181,7 @@ export interface InterfaceRevision {
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[]; argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
} }
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType }; export type StoragePolicy = { kind: "register" } | { kind: "crdt-document"; updateType: ValueType };
export interface StateSlotDefinition { export interface StateSlotDefinition {
kind: "state"; kind: "state";
@@ -246,6 +246,7 @@ export type DependencyPortRequirement =
/** Exact public operation identities, not implementation export names. */ /** Exact public operation identities, not implementation export names. */
operations?: OperationId[]; operations?: OperationId[];
} }
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType }; | { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
export interface DependencyPort { export interface DependencyPort {
@@ -320,6 +321,12 @@ export type DependencyBinding =
interfaceRevisionId: InterfaceRevisionId; interfaceRevisionId: InterfaceRevisionId;
via?: EdgeTraversal; via?: EdgeTraversal;
} }
| {
kind: "creation";
atomId: AtomId;
owners: { interfaceRevisionId: InterfaceRevisionId; inputPath: string[] }[];
via?: never;
}
| { kind: "constructor"; atomId: AtomId }; | { kind: "constructor"; atomId: AtomId };
export interface BoundDependency { export interface BoundDependency {
@@ -376,7 +383,13 @@ export interface OperationBinding {
queryReason?: string; queryReason?: string;
} }
export interface InitializationField {
input: string;
binding: { kind: "state"; slotId: SlotId } | { kind: "edge"; edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId };
}
export interface Conformance { export interface Conformance {
initialization?: InitializationField[];
/** Absent only for legacy assemblies awaiting explicit ownership enrollment. */ /** Absent only for legacy assemblies awaiting explicit ownership enrollment. */
id?: ConformanceId; id?: ConformanceId;
semanticMajor?: number; semanticMajor?: number;
+12 -4
View File
@@ -1,3 +1,4 @@
import { validateInitialization } from "./initialization.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 {
@@ -42,6 +43,7 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode = export type CapabilityValidationIssueCode =
| "invalid-initialization"
| "invalid-execution-contract" | "invalid-execution-contract"
| "invalid-query-contract" | "invalid-query-contract"
| "query-provider-reason-required" | "query-provider-reason-required"
@@ -1043,6 +1045,7 @@ const validatePackages = (
); );
} }
break; break;
case "creation":
case "constructor": case "constructor":
if (port.requirement.inputType) if (port.requirement.inputType)
validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes); validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes);
@@ -1794,14 +1797,14 @@ const validateConformances = (
}); });
if ( if (
operation.scope === "class" && operation.scope === "class" &&
(binding.dependencies.some((entry) => entry.binding.kind !== "constructor") || (binding.dependencies.some((entry) => !["constructor", "creation"].includes(entry.binding.kind)) ||
packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor")) packageExport.dependencyPorts.some((entry) => !["constructor", "creation"].includes(entry.requirement.kind)))
) { ) {
issue( issue(
issues, issues,
"invalid-package-binding", "invalid-package-binding",
bindingPath, bindingPath,
"Class functions may inject constructors, not instance-dependent ports", "Class functions require receiverless dependencies such as creation ports",
); );
} }
validateBoundDependencies(issues, { validateBoundDependencies(issues, {
@@ -2084,7 +2087,7 @@ 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)); issues.push(...validateExecutionContracts(workspace), ...validateInitialization(workspace));
return issues; return issues;
}; };
@@ -2327,6 +2330,11 @@ export const computeCapabilityClosure = (
} }
break; break;
} }
case "creation": {
for (const owner of dependency.binding.owners)
queued.push({ atomId: dependency.binding.atomId, interfaceRevisionId: owner.interfaceRevisionId });
break;
}
case "constructor": { case "constructor": {
constructors.add(dependency.binding.atomId); constructors.add(dependency.binding.atomId);
const constructor = plan.constructors.get(dependency.binding.atomId); const constructor = plan.constructors.get(dependency.binding.atomId);
+3 -3
View File
@@ -148,10 +148,10 @@ test("interfaces can declare ordinary call operations", () => {
test("state defaults accept recursive JSON values", () => { test("state defaults accept recursive JSON values", () => {
const source = capabilityFixtureSource.replace( const source = capabilityFixtureSource.replace(
/policy optimistic-register default "Untitled project";/, /policy register default "Untitled project";/,
` policy optimistic-register default "Untitled project"; ` policy register default "Untitled project";
shared state ProjectMetadata id "slot:project:metadata" on Project : message "example.Metadata" shared state ProjectMetadata id "slot:project:metadata" on Project : message "example.Metadata"
policy optimistic-register default {"labels":["compiler","runtime"],"score":1.5,"enabled":true,"extra":null};`, policy register default {"labels":["compiler","runtime"],"score":1.5,"enabled":true,"extra":null};`,
); );
const result = compileCapabilityFixture({ workspace: source }); const result = compileCapabilityFixture({ workspace: source });
assert.equal(result.ok, true); assert.equal(result.ok, true);
+197
View File
@@ -0,0 +1,197 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
compileCapabilityResourceSource,
compileCapabilitySource,
formatQx,
} from "../src/capability-language/index.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
const source = { repository: "https://example.test/factories.git", commit: "a".repeat(40) };
const seed = "record { data: record { title: string; }; tags: record { tags: list<string>; }; }";
const iface = (text: string): InterfaceRevision => {
const result = compileCapabilityResourceSource(text, { source });
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
return result.resource.revision;
};
const data = iface(`interface Data id "data" revision "data@1" {
value title id "title" : string { get id "title:get" execution snapshot targets [browser, server]; }
}`);
const tagged = iface(`interface Tagged id "tagged" revision "tagged@1" {
value tags id "tags" : list<string> { get id "tags:get" execution snapshot targets [browser, server]; }
}`);
const factory = iface(`external atom Note id "note";
interface Factory id "factory" revision "factory@1" {
static operation create id "create" : ${seed} -> atom-ref<Note> { call id "create:call" execution transaction targets [browser, server]; }
static operation importNote id "import" : ${seed} -> atom-ref<Note> { call id "import:call" execution transaction targets [browser, server]; }
}`);
const runtime = compileCapabilityResourceSource(
`external atom Note id "note";
package Runtime id "runtime" revision "runtime@1" {
function createNote id "create-note" : ${seed} -> atom-ref<Note> execution transaction targets [browser, server] requires {
creation note id "new-note" : Note input ${seed};
};
function importNote id "import-note" : ${seed} -> atom-ref<Note> execution transaction targets [browser, server] requires {
creation note id "new-note" : Note input ${seed};
};
}`,
{ source },
);
assert.ok(runtime.ok && runtime.resource.kind === "package", JSON.stringify(runtime.diagnostics));
const packageRevision = runtime.resource.revision;
const text = `workspace Notes id "notes" revision "notes@1" commit "${"a".repeat(40)}" {
import interface Data; import interface Tagged; import interface Factory; import package Runtime;
atom Note id "note";
conform Note as Data id "owner:data" {
private state Title id "title-state" on Note : string policy register;
initialize { title to state Title; };
bind title.get to state Title.read;
}
conform Note as Tagged id "owner:tags" {
private state Tags id "tags-state" on Note : list<string> policy register;
initialize { tags to state Tags; };
bind tags.get to state Tags.read;
}
conform Note as Factory id "owner:factory" {
bind create.call to package Runtime.createNote with {
note to creation Note with { Data from input.data; Tagged from input.tags; };
};
bind importNote.call to package Runtime.importNote with {
note to creation Note with { Data from input.data; Tagged from input.tags; };
};
}
}`;
const compile = (textOverride = text) =>
compileCapabilitySource(formatQx(textOverride), "notes.qx", {
interfaces: new Map([
["Data", data],
["Tagged", tagged],
["Factory", factory],
]),
interfaceClosure: [data, tagged, factory],
packages: new Map([["Runtime", packageRevision]]),
packageClosure: [packageRevision],
});
test("two factories initialize two independent state owners without constructors", () => {
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.deepEqual(result.workspace.constructors, []);
assert.equal(result.workspace.conformances.filter((c) => c.initialization).length, 2);
const owners = result.workspace.conformances[2]!.operationBindings.map((b) =>
b.binding.kind === "package" ? b.binding.dependencies[0]!.binding : undefined,
);
for (const binding of owners) {
assert.ok(binding?.kind === "creation");
assert.deepEqual(binding.owners, [
{ interfaceRevisionId: "data@1", inputPath: ["data"] },
{ interfaceRevisionId: "tagged@1", inputPath: ["tags"] },
]);
}
});
test("creation rejects missing seeds, wrong input shapes, and private initialization laundering", () => {
for (const [edit, message] of [
[text.replaceAll("Tagged from input.tags;", ""), "Required state tags-state"],
[text.replaceAll("Data from input.data;", "Data from input.tags;"), "does not match"],
[text.replace("title to state Title;", "title to state Tags;"), "owned by this conformance"],
[
text.replaceAll("Data from input.data;", "Data from input.data; Data from input.data;"),
"Duplicate initialization owner",
],
]) {
const result = compile(edit!);
assert.equal(result.ok, false);
assert.match(JSON.stringify(result.diagnostics), new RegExp(message!));
}
});
test("defaults supply unmapped state and seeds cannot invoke an undeclared owner", () => {
const withDefault = text
.replace("on Note : list<string> policy register;", "on Note : list<string> policy register default [];")
.replaceAll("Tagged from input.tags;", "");
const result = compile(withDefault);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const missing = compile(text.replace("initialize { tags to state Tags; };", ""));
assert.equal(missing.ok, false);
assert.match(JSON.stringify(missing.diagnostics), /no declared initialization mapping/);
});
test("creation ports cannot be used in snapshot execution", () => {
const result = compileCapabilityResourceSource(
`external atom Note id "note";
package P id "p" revision "p@1" {
function createNote id "create" : unit -> atom-ref<Note> execution snapshot targets [browser, server] requires {
creation note id "note" : Note input record { title:string; };
};
}`,
{ source },
);
assert.equal(result.ok, false);
assert.match(JSON.stringify(result.diagnostics), /creation requires transaction/);
});
test("the authored storage policy is register, without an old spelling alias", () => {
const result = compileCapabilitySource(text.replaceAll("policy register", "policy optimistic-register"), "notes.qx");
assert.equal(result.ok, false);
assert.ok(result.diagnostics.some((d) => d.phase === "syntax"));
});
test("initialization contracts participate in executable source identity", async () => {
const { runtimeContracts } = await import("../src/capability-model/index.js");
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const before = runtimeContracts(result.workspace);
const changed = structuredClone(result.workspace);
changed.conformances[0]!.initialization![0]!.input = "renamed";
assert.notDeepEqual(runtimeContracts(changed), before);
});
test("to-many initialization requires stable entry keys and typed targets", async () => {
const { validateInitialization } = await import("../src/capability-model/initialization.js");
const { capabilityId } = await import("../src/capability-model/index.js");
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const workspace = result.workspace;
const owner = workspace.conformances[1]!;
const state = owner.privateAttachments[0]!;
assert.equal(state.kind, "state");
if (state.kind !== "state") return;
state.defaultValue = [];
const edgeId = capabilityId.edgeType("children"),
projectionId = capabilityId.edgeProjection("children:parent");
owner.privateAttachments.push({
kind: "edge",
id: edgeId,
displayName: "Children",
endpoints: [
{
projectionId,
displayName: "children",
constraint: { kind: "atom", atomId: owner.atomId },
cardinality: "many-unique",
ordered: false,
},
{
projectionId: capabilityId.edgeProjection("children:child"),
displayName: "parent",
constraint: { kind: "atom", atomId: owner.atomId },
cardinality: "optional-one",
ordered: false,
},
],
});
owner.initialization = [{ input: "children", binding: { kind: "edge", edgeTypeId: edgeId, projectionId } }];
const entryType = {
kind: "record" as const,
fields: {
key: { kind: "scalar" as const, name: "string" as const },
target: { kind: "object-ref" as const, expectation: { kind: "atom" as const, atomId: owner.atomId } },
},
};
for (const entry of workspace.packageImports[0]!.exports) {
const port = entry.dependencyPorts[0]!.requirement;
assert.ok(port.kind === "creation" && port.inputType.kind === "record");
port.inputType.fields.tags = { kind: "record", fields: { children: { kind: "list", value: entryType } } };
}
assert.deepEqual(validateInitialization(workspace), []);
Reflect.deleteProperty(entryType.fields, "key");
assert.ok(validateInitialization(workspace).some((i) => i.message.includes("does not match")));
});
+3 -3
View File
@@ -98,8 +98,8 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
); );
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" { const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled"; private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
private state Done${atom} id "${atom}:done" on ${atom} : bool policy optimistic-register default false; private state Done${atom} id "${atom}:done" on ${atom} : bool policy register default false;
private state Rank${atom} id "${atom}:rank" on ${atom} : int64 policy optimistic-register default 0; private state Rank${atom} id "${atom}:rank" on ${atom} : int64 policy register default 0;
private edge Assignee${atom} id "${atom}:assignee" { private edge Assignee${atom} id "${atom}:assignee" {
atom ${atom} projection assignee id "${atom}:assignee:forward" optional-one; atom ${atom} projection assignee id "${atom}:assignee:forward" optional-one;
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many; interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
@@ -114,7 +114,7 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
import interface PersonFacts; import interface TaskFacts; import interface Collection; import package Queries; import interface PersonFacts; import interface TaskFacts; import interface Collection; import package Queries;
atom Task id "Task"; atom Reminder id "Reminder"; atom Person id "Person"; atom Tasks id "Tasks"; atom Task id "Task"; atom Reminder id "Reminder"; atom Person id "Person"; atom Tasks id "Tasks";
conform Person as PersonFacts id "person-facts" { conform Person as PersonFacts id "person-facts" {
private state Name id "person:name" on Person : string policy optimistic-register default "Nobody"; private state Name id "person:name" on Person : string policy register default "Nobody";
bind name to state Name; bind name to state Name;
} }
${implementation("Task")} ${implementation("Reminder")} ${implementation("Task")} ${implementation("Reminder")}
+2 -2
View File
@@ -8,7 +8,7 @@ workspace Todo id "workspace:todo" revision "workspace:todo@1" commit "111111111
import package TodoRuntime; import package TodoRuntime;
shared state ProjectTitle id "slot:project:title" on Project : string shared state ProjectTitle id "slot:project:title" on Project : string
policy optimistic-register default "Untitled project"; policy register default "Untitled project";
conform Project as Named { conform Project as Named {
bind name.get to state ProjectTitle.read; bind name.get to state ProjectTitle.read;
@@ -19,7 +19,7 @@ workspace Todo id "workspace:todo" revision "workspace:todo@1" commit "111111111
conform Person as Named { conform Person as Named {
private state PersonName id "slot:person:name" on Person : string private state PersonName id "slot:person:name" on Person : string
policy optimistic-register default "Anonymous"; policy register default "Anonymous";
bind name.get to state PersonName.read; bind name.get to state PersonName.read;
bind name.set to state PersonName.write; bind name.set to state PersonName.write;
bind name.watch-start to state PersonName.watch-start; bind name.watch-start to state PersonName.watch-start;
+1 -1
View File
@@ -8,7 +8,7 @@ workspace WebStudioFixture id "workspace:web-studio-fixture" revision "workspace
import package ComponentRuntime; import package ComponentRuntime;
shared state ProjectName id "slot:project:name" on Project : string shared state ProjectName id "slot:project:name" on Project : string
policy optimistic-register default "Untitled project"; policy register default "Untitled project";
shared edge ProjectComponentEdge id "edge:project:component" { shared edge ProjectComponentEdge id "edge:project:component" {
atom ProjectComponent projection subject id "projection:component:subject" exactly-one; atom ProjectComponent projection subject id "projection:component:subject" exactly-one;
atom Project projection component id "projection:project:component" optional-one; atom Project projection component id "projection:project:component" optional-one;
+1 -1
View File
@@ -36,7 +36,7 @@ function fixture(transform = (s: string) => s, generic = false) {
${forward("Relay", "Alias")} ${forward("Relay", "Alias")}
${forward("Alias", generic ? "Owner<string>" : "Owner")} ${forward("Alias", generic ? "Owner<string>" : "Owner")}
conform Item as Owner${generic ? "<string>" : ""} id "owner-conformance" { conform Item as Owner${generic ? "<string>" : ""} id "owner-conformance" {
private state Title id "title" on Item : string policy optimistic-register default ""; private state Title id "title" on Item : string policy register default "";
bind title to state Title; bind title to state Title;
} }
}`; }`;
+2 -2
View File
@@ -272,7 +272,7 @@ test("generic conformances bind state against specialized signatures", () => {
atom Document id "document"; atom Document id "document";
import interface Reader; import interface Reader;
conform Document as Reader<string> id "reader-conformance" { conform Document as Reader<string> id "reader-conformance" {
private state Values id "values-slot" on Document : list<optional<string>> policy optimistic-register; private state Values id "values-slot" on Document : list<optional<string>> policy register;
bind values.get to state Values.read; bind values.get to state Values.read;
bind values.watch-start to state Values.watch-start; bind values.watch-start to state Values.watch-start;
bind values.watch-stop to state Values.watch-stop; bind values.watch-stop to state Values.watch-stop;
@@ -328,7 +328,7 @@ test("package receivers and injected dependencies select exact applications", ()
import interface Reader; import interface Summary; import package P; import interface Reader; import interface Summary; import package P;
atom Note id "note"; atom Note id "note";
conform Note as Reader<string> id "reader-conformance" { conform Note as Reader<string> id "reader-conformance" {
private state Values id "values" on Note : list<optional<string>> policy optimistic-register; private state Values id "values" on Note : list<optional<string>> policy register;
bind values.get to state Values.read; bind values.get to state Values.read;
bind values.watch-start to state Values.watch-start; bind values.watch-start to state Values.watch-start;
bind values.watch-stop to state Values.watch-stop; bind values.watch-stop to state Values.watch-stop;
+1 -1
View File
@@ -82,7 +82,7 @@ test("dependency scaffolding validates exact sources and preserves unrelated loc
}); });
test("conformance enrollment, major edits, imports, and private attachment removal preserve valid structure", () => { test("conformance enrollment, major edits, imports, and private attachment removal preserve valid structure", () => {
const source = `fragment { conform Game as Playable { private state Board id "slot:board" on Game : string policy optimistic-register; } }`; const source = `fragment { conform Game as Playable { private state Board id "slot:board" on Game : string policy register; } }`;
const selector = { kind: "conformanceDecl", names: ["Game", "Playable"] }; const selector = { kind: "conformanceDecl", names: ["Game", "Playable"] };
const enrolled = editStructure(source, { operation: "conformance-id", target: selector, id: "conformance:playable" }); const enrolled = editStructure(source, { operation: "conformance-id", target: selector, id: "conformance:playable" });
assert.ok(enrolled.includes('as Playable id "conformance:playable"')); assert.ok(enrolled.includes('as Playable id "conformance:playable"'));