@listen-fire/handbook 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__test__/adapter_schemas.fixture.json +2 -2
- package/dist/__test__/kg_ontology.fixture.json +313 -210
- package/dist/chapters/asks.js +4 -4
- package/dist/chapters/branching.js +1 -1
- package/dist/chapters/builtins.js +1 -1
- package/dist/chapters/expressions.js +6 -6
- package/dist/chapters/extraction.js +6 -6
- package/dist/chapters/reference.js +8 -5
- package/dist/chapters/traversal.js +1 -1
- package/dist/chapters/use_cases.js +1 -1
- package/dist/front_page.js +1 -1
- package/dist/language_search.js +1 -1
- package/dist/prose_rules.js +1 -1
- package/dist/testing.js +1 -0
- package/package.json +1 -1
|
@@ -1465,7 +1465,7 @@
|
|
|
1465
1465
|
"Message": "The message body. Required unless a File is attached or Blocks are set (at least one of Message / File / Blocks). Use Slack-native formatting (*bold*, _italic_, <url|text>).",
|
|
1466
1466
|
"File": "Optional file to upload with the message — the Message text rides as the file caption, one visible Slack message. Send-side only: a received message's files are on its Files edge — read `msg-[:Files]->.File`. A message is one of File, Blocks or Buttons.",
|
|
1467
1467
|
"Blocks": "Slack Block Kit blocks, exactly as in Slack's docs — assembled verbatim and passed through (no wrapping, no typed shape here; a malformed block surfaces as a loud Slack error at run time). For plain buttons write Buttons instead. A press on any control in the blocks arrives on the message's Interactions, its value in the Action's Value: a button's or option's `value`, a picker's pick. Build strings with \"${…}\" interpolation (never `+`, which is numeric addition). A message is one of File, Blocks or Buttons.",
|
|
1468
|
-
"Buttons": "Buttons under the Message text, each `{ Text: \"Approve\", Value: \"approve\" }`: Text is the label, Value what a press answers with. Wait for a press with `await msg-[:Interactions]->`; `only(press-[:Actions]->)
|
|
1468
|
+
"Buttons": "Buttons under the Message text, each `{ Text: \"Approve\", Value: \"approve\" }`: Text is the label, Value what a press answers with. Wait for a press with `await msg-[:Interactions]->`; `only(press-[:Actions]->)?.Value` is one of the Values written. Needs Message text; never with File or Blocks.",
|
|
1469
1469
|
"Channel": "The C… channel id (read-only — a post is written along a Channel's `Messages` edge).",
|
|
1470
1470
|
"Timestamp": "When the message was posted.",
|
|
1471
1471
|
"Topic": "A marker of your own for a message with Buttons (`Topic: \"lunch-poll\"`), so a listen can tell its presses from other messages' presses: `listen to chat-[:Channels]->-[m:Messages WHERE m.Topic == \"lunch-poll\"]->-[:Interactions]->`. Only a message with Buttons takes one: Slack keeps no other data on a message that comes back with a press, so the Topic is stored as the id of the block its buttons sit in (`lf:<topic>`), and read back from there — on the pressed message and on a listed one alike. It is written only with Buttons, and block ids starting `lf:` are reserved for it: a message whose own Blocks use one is refused.",
|
|
@@ -1745,7 +1745,7 @@
|
|
|
1745
1745
|
"Message": "The message body. Required unless a File is attached or Blocks are set (at least one of Message / File / Blocks). Use Slack-native formatting (*bold*, _italic_, <url|text>).",
|
|
1746
1746
|
"File": "Optional file to upload with the message — the Message text rides as the file caption, one visible Slack message. Send-side only: a received message's files are on its Files edge — read `msg-[:Files]->.File`. A message is one of File, Blocks or Buttons.",
|
|
1747
1747
|
"Blocks": "Slack Block Kit blocks, exactly as in Slack's docs — assembled verbatim and passed through (no wrapping, no typed shape here; a malformed block surfaces as a loud Slack error at run time). For plain buttons write Buttons instead. A press on any control in the blocks arrives on the message's Interactions, its value in the Action's Value: a button's or option's `value`, a picker's pick. Build strings with \"${…}\" interpolation (never `+`, which is numeric addition). A message is one of File, Blocks or Buttons.",
|
|
1748
|
-
"Buttons": "Buttons under the Message text, each `{ Text: \"Approve\", Value: \"approve\" }`: Text is the label, Value what a press answers with. Wait for a press with `await msg-[:Interactions]->`; `only(press-[:Actions]->)
|
|
1748
|
+
"Buttons": "Buttons under the Message text, each `{ Text: \"Approve\", Value: \"approve\" }`: Text is the label, Value what a press answers with. Wait for a press with `await msg-[:Interactions]->`; `only(press-[:Actions]->)?.Value` is one of the Values written. Needs Message text; never with File or Blocks.",
|
|
1749
1749
|
"Topic": "A marker of your own for a message with Buttons (`Topic: \"lunch-poll\"`), so a listen can tell its presses from other messages' presses: `listen to chat-[:Channels]->-[m:Messages WHERE m.Topic == \"lunch-poll\"]->-[:Interactions]->`. Only a message with Buttons takes one: Slack keeps no other data on a message that comes back with a press, so the Topic is stored as the id of the block its buttons sit in (`lf:<topic>`), and read back from there — on the pressed message and on a listed one alike. It is written only with Buttons, and block ids starting `lf:` are reserved for it: a message whose own Blocks use one is refused."
|
|
1750
1750
|
},
|
|
1751
1751
|
"writeUnion": {
|
|
@@ -3,266 +3,282 @@
|
|
|
3
3
|
"canFire": true,
|
|
4
4
|
"schema": {
|
|
5
5
|
"positions": {
|
|
6
|
-
"
|
|
7
|
-
"properties": {},
|
|
8
|
-
"edges": {},
|
|
9
|
-
"undescribed": true
|
|
10
|
-
},
|
|
11
|
-
"Record Change": {
|
|
6
|
+
"Company": {
|
|
12
7
|
"properties": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"record.updated",
|
|
18
|
-
"record.deleted"
|
|
19
|
-
]
|
|
8
|
+
"Name": "text",
|
|
9
|
+
"Domains": {
|
|
10
|
+
"kind": "list",
|
|
11
|
+
"of": "text"
|
|
20
12
|
},
|
|
21
|
-
"
|
|
22
|
-
"record": "text"
|
|
13
|
+
"Stage": "text"
|
|
23
14
|
},
|
|
24
15
|
"edges": {
|
|
25
|
-
"
|
|
26
|
-
"target": "
|
|
27
|
-
"
|
|
28
|
-
"requiresLiveRecord": true
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"[\"Record Change\",[[\"action\",\"record.created\"],[\"type\",\"Company\"]]]": {
|
|
33
|
-
"properties": {
|
|
34
|
-
"action": {
|
|
35
|
-
"kind": "enum",
|
|
36
|
-
"options": [
|
|
37
|
-
"record.created",
|
|
38
|
-
"record.updated",
|
|
39
|
-
"record.deleted"
|
|
40
|
-
]
|
|
16
|
+
"Team": {
|
|
17
|
+
"target": "Person",
|
|
18
|
+
"writable": true
|
|
41
19
|
},
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"target": "
|
|
48
|
-
"
|
|
20
|
+
"Support Tickets": {
|
|
21
|
+
"target": "Support Ticket",
|
|
22
|
+
"writable": true
|
|
23
|
+
},
|
|
24
|
+
"Resources": {
|
|
25
|
+
"target": "Attached Resource",
|
|
26
|
+
"resources": true
|
|
27
|
+
},
|
|
28
|
+
"Changes": {
|
|
29
|
+
"target": "Company Change",
|
|
30
|
+
"readable": false,
|
|
31
|
+
"listenable": true,
|
|
32
|
+
"identity": "Id",
|
|
33
|
+
"delivery": "connection",
|
|
34
|
+
"carries": [
|
|
35
|
+
"Company"
|
|
36
|
+
],
|
|
37
|
+
"attributes": true,
|
|
38
|
+
"description": "What happens to a Company in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
49
39
|
}
|
|
50
40
|
},
|
|
51
|
-
"
|
|
41
|
+
"deletable": true,
|
|
42
|
+
"fieldIds": {
|
|
43
|
+
"Name": "pt-company-name",
|
|
44
|
+
"Domains": "pt-company-domains",
|
|
45
|
+
"Stage": "pt-company-stage"
|
|
46
|
+
}
|
|
52
47
|
},
|
|
53
|
-
"
|
|
48
|
+
"Person": {
|
|
54
49
|
"properties": {
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
"options": [
|
|
58
|
-
"record.created",
|
|
59
|
-
"record.updated",
|
|
60
|
-
"record.deleted"
|
|
61
|
-
]
|
|
62
|
-
},
|
|
63
|
-
"type": "text",
|
|
64
|
-
"record": "text"
|
|
50
|
+
"Name": "text",
|
|
51
|
+
"Email": "text"
|
|
65
52
|
},
|
|
66
53
|
"edges": {
|
|
67
|
-
"
|
|
54
|
+
"Companies": {
|
|
68
55
|
"target": "Company",
|
|
69
|
-
"
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
"displayName": "Record Change where action=record.updated, type=Company"
|
|
73
|
-
},
|
|
74
|
-
"[\"Record Change\",[[\"action\",\"record.deleted\"],[\"type\",\"Company\"]]]": {
|
|
75
|
-
"properties": {
|
|
76
|
-
"action": {
|
|
77
|
-
"kind": "enum",
|
|
78
|
-
"options": [
|
|
79
|
-
"record.created",
|
|
80
|
-
"record.updated",
|
|
81
|
-
"record.deleted"
|
|
82
|
-
]
|
|
56
|
+
"writable": true
|
|
83
57
|
},
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"edges": {},
|
|
88
|
-
"displayName": "Record Change where action=record.deleted, type=Company"
|
|
89
|
-
},
|
|
90
|
-
"[\"Record Change\",[[\"action\",\"record.created\"],[\"type\",\"Person\"]]]": {
|
|
91
|
-
"properties": {
|
|
92
|
-
"action": {
|
|
93
|
-
"kind": "enum",
|
|
94
|
-
"options": [
|
|
95
|
-
"record.created",
|
|
96
|
-
"record.updated",
|
|
97
|
-
"record.deleted"
|
|
98
|
-
]
|
|
58
|
+
"Reported Tickets": {
|
|
59
|
+
"target": "Support Ticket",
|
|
60
|
+
"writable": true
|
|
99
61
|
},
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
"target": "Person",
|
|
106
|
-
"
|
|
62
|
+
"Resources": {
|
|
63
|
+
"target": "Attached Resource",
|
|
64
|
+
"resources": true
|
|
65
|
+
},
|
|
66
|
+
"Changes": {
|
|
67
|
+
"target": "Person Change",
|
|
68
|
+
"readable": false,
|
|
69
|
+
"listenable": true,
|
|
70
|
+
"identity": "Id",
|
|
71
|
+
"delivery": "connection",
|
|
72
|
+
"carries": [
|
|
73
|
+
"Person"
|
|
74
|
+
],
|
|
75
|
+
"attributes": true,
|
|
76
|
+
"description": "What happens to a Person in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
107
77
|
}
|
|
108
78
|
},
|
|
109
|
-
"
|
|
79
|
+
"deletable": true,
|
|
80
|
+
"fieldIds": {
|
|
81
|
+
"Name": "pt-person-name",
|
|
82
|
+
"Email": "pt-person-email"
|
|
83
|
+
}
|
|
110
84
|
},
|
|
111
|
-
"
|
|
85
|
+
"Support Ticket": {
|
|
112
86
|
"properties": {
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
"options": [
|
|
116
|
-
"record.created",
|
|
117
|
-
"record.updated",
|
|
118
|
-
"record.deleted"
|
|
119
|
-
]
|
|
120
|
-
},
|
|
121
|
-
"type": "text",
|
|
122
|
-
"record": "text"
|
|
87
|
+
"Status": "text",
|
|
88
|
+
"Priority": "number"
|
|
123
89
|
},
|
|
124
90
|
"edges": {
|
|
125
|
-
"
|
|
91
|
+
"Reporter": {
|
|
126
92
|
"target": "Person",
|
|
127
|
-
"
|
|
93
|
+
"writable": true
|
|
94
|
+
},
|
|
95
|
+
"Company": {
|
|
96
|
+
"target": "Company",
|
|
97
|
+
"writable": true
|
|
98
|
+
},
|
|
99
|
+
"Resources": {
|
|
100
|
+
"target": "Attached Resource",
|
|
101
|
+
"resources": true
|
|
102
|
+
},
|
|
103
|
+
"Changes": {
|
|
104
|
+
"target": "Support Ticket Change",
|
|
105
|
+
"readable": false,
|
|
106
|
+
"listenable": true,
|
|
107
|
+
"identity": "Id",
|
|
108
|
+
"delivery": "connection",
|
|
109
|
+
"carries": [
|
|
110
|
+
"Support Ticket"
|
|
111
|
+
],
|
|
112
|
+
"attributes": true,
|
|
113
|
+
"description": "What happens to a Support Ticket in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
128
114
|
}
|
|
129
115
|
},
|
|
130
|
-
"
|
|
116
|
+
"deletable": true,
|
|
117
|
+
"fieldIds": {
|
|
118
|
+
"Status": "pt-ticket-status",
|
|
119
|
+
"Priority": "pt-ticket-priority"
|
|
120
|
+
}
|
|
131
121
|
},
|
|
132
|
-
"
|
|
122
|
+
"Company Change": {
|
|
133
123
|
"properties": {
|
|
134
|
-
"
|
|
124
|
+
"Id": "text",
|
|
125
|
+
"Kind": {
|
|
135
126
|
"kind": "enum",
|
|
136
127
|
"options": [
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
128
|
+
"created",
|
|
129
|
+
"updated",
|
|
130
|
+
"deleted"
|
|
140
131
|
]
|
|
141
132
|
},
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"record.updated",
|
|
155
|
-
"record.deleted"
|
|
156
|
-
]
|
|
133
|
+
"Changed Fields": {
|
|
134
|
+
"kind": "list",
|
|
135
|
+
"of": {
|
|
136
|
+
"kind": "enum",
|
|
137
|
+
"options": [
|
|
138
|
+
"Name",
|
|
139
|
+
"Domains",
|
|
140
|
+
"Stage",
|
|
141
|
+
"Team",
|
|
142
|
+
"Support Tickets"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
157
145
|
},
|
|
158
|
-
"
|
|
159
|
-
"
|
|
146
|
+
"Record Id": "text",
|
|
147
|
+
"Current": {
|
|
148
|
+
"kind": "maybeAbsent",
|
|
149
|
+
"of": {
|
|
150
|
+
"kind": "record",
|
|
151
|
+
"position": {
|
|
152
|
+
"kind": "position",
|
|
153
|
+
"position": "Company"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
160
157
|
},
|
|
161
158
|
"edges": {
|
|
162
159
|
"Record": {
|
|
163
|
-
"target": "
|
|
164
|
-
"
|
|
160
|
+
"target": "Company",
|
|
161
|
+
"requiresLiveRecord": true,
|
|
162
|
+
"description": "The record as it is now, read from the graph: none once it is deleted."
|
|
165
163
|
}
|
|
166
164
|
},
|
|
167
|
-
"
|
|
165
|
+
"description": "A change to a Company in the graph: what happened (`Kind`), which properties and edges moved (`Changed Fields`), and the record as the change left it (`Current`, absent once deleted; test it present before reading through it: `exists(c.Current) AND c.Current.Status == \"Open\"`). Walk `Record` for the record as it is now.",
|
|
166
|
+
"fieldDocs": {
|
|
167
|
+
"Id": "Names the change: the same each time the graph reports it.",
|
|
168
|
+
"Kind": "What happened to the record.",
|
|
169
|
+
"Changed Fields": "The properties and edges the change moved, by name: every one a created record was given, none for a deleted one.",
|
|
170
|
+
"Record Id": "The graph's id for the record, a deleted one's too.",
|
|
171
|
+
"Current": "The record as the change left it, every property by name. Absent when it was deleted."
|
|
172
|
+
}
|
|
168
173
|
},
|
|
169
|
-
"
|
|
174
|
+
"Person Change": {
|
|
170
175
|
"properties": {
|
|
171
|
-
"
|
|
176
|
+
"Id": "text",
|
|
177
|
+
"Kind": {
|
|
172
178
|
"kind": "enum",
|
|
173
179
|
"options": [
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
180
|
+
"created",
|
|
181
|
+
"updated",
|
|
182
|
+
"deleted"
|
|
177
183
|
]
|
|
178
184
|
},
|
|
179
|
-
"
|
|
180
|
-
|
|
185
|
+
"Changed Fields": {
|
|
186
|
+
"kind": "list",
|
|
187
|
+
"of": {
|
|
188
|
+
"kind": "enum",
|
|
189
|
+
"options": [
|
|
190
|
+
"Name",
|
|
191
|
+
"Email",
|
|
192
|
+
"Companies",
|
|
193
|
+
"Reported Tickets"
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"Record Id": "text",
|
|
198
|
+
"Current": {
|
|
199
|
+
"kind": "maybeAbsent",
|
|
200
|
+
"of": {
|
|
201
|
+
"kind": "record",
|
|
202
|
+
"position": {
|
|
203
|
+
"kind": "position",
|
|
204
|
+
"position": "Person"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
181
208
|
},
|
|
182
209
|
"edges": {
|
|
183
210
|
"Record": {
|
|
184
|
-
"target": "
|
|
185
|
-
"
|
|
211
|
+
"target": "Person",
|
|
212
|
+
"requiresLiveRecord": true,
|
|
213
|
+
"description": "The record as it is now, read from the graph: none once it is deleted."
|
|
186
214
|
}
|
|
187
215
|
},
|
|
188
|
-
"
|
|
216
|
+
"description": "A change to a Person in the graph: what happened (`Kind`), which properties and edges moved (`Changed Fields`), and the record as the change left it (`Current`, absent once deleted; test it present before reading through it: `exists(c.Current) AND c.Current.Status == \"Open\"`). Walk `Record` for the record as it is now.",
|
|
217
|
+
"fieldDocs": {
|
|
218
|
+
"Id": "Names the change: the same each time the graph reports it.",
|
|
219
|
+
"Kind": "What happened to the record.",
|
|
220
|
+
"Changed Fields": "The properties and edges the change moved, by name: every one a created record was given, none for a deleted one.",
|
|
221
|
+
"Record Id": "The graph's id for the record, a deleted one's too.",
|
|
222
|
+
"Current": "The record as the change left it, every property by name. Absent when it was deleted."
|
|
223
|
+
}
|
|
189
224
|
},
|
|
190
|
-
"
|
|
225
|
+
"Support Ticket Change": {
|
|
191
226
|
"properties": {
|
|
192
|
-
"
|
|
227
|
+
"Id": "text",
|
|
228
|
+
"Kind": {
|
|
193
229
|
"kind": "enum",
|
|
194
230
|
"options": [
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
231
|
+
"created",
|
|
232
|
+
"updated",
|
|
233
|
+
"deleted"
|
|
198
234
|
]
|
|
199
235
|
},
|
|
200
|
-
"
|
|
201
|
-
"record": "text"
|
|
202
|
-
},
|
|
203
|
-
"edges": {},
|
|
204
|
-
"displayName": "Record Change where action=record.deleted, type=Support Ticket"
|
|
205
|
-
},
|
|
206
|
-
"Company": {
|
|
207
|
-
"properties": {
|
|
208
|
-
"Name": "text",
|
|
209
|
-
"Domains": {
|
|
236
|
+
"Changed Fields": {
|
|
210
237
|
"kind": "list",
|
|
211
|
-
"of":
|
|
238
|
+
"of": {
|
|
239
|
+
"kind": "enum",
|
|
240
|
+
"options": [
|
|
241
|
+
"Status",
|
|
242
|
+
"Priority",
|
|
243
|
+
"Company",
|
|
244
|
+
"Reporter"
|
|
245
|
+
]
|
|
246
|
+
}
|
|
212
247
|
},
|
|
213
|
-
"
|
|
248
|
+
"Record Id": "text",
|
|
249
|
+
"Current": {
|
|
250
|
+
"kind": "maybeAbsent",
|
|
251
|
+
"of": {
|
|
252
|
+
"kind": "record",
|
|
253
|
+
"position": {
|
|
254
|
+
"kind": "position",
|
|
255
|
+
"position": "Support Ticket"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
214
259
|
},
|
|
215
260
|
"edges": {
|
|
216
|
-
"
|
|
217
|
-
"target": "Person",
|
|
218
|
-
"readable": true,
|
|
219
|
-
"writable": true
|
|
220
|
-
},
|
|
221
|
-
"Support Tickets": {
|
|
261
|
+
"Record": {
|
|
222
262
|
"target": "Support Ticket",
|
|
223
|
-
"
|
|
224
|
-
"
|
|
263
|
+
"requiresLiveRecord": true,
|
|
264
|
+
"description": "The record as it is now, read from the graph: none once it is deleted."
|
|
225
265
|
}
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
"Person": {
|
|
229
|
-
"properties": {
|
|
230
|
-
"Name": "text",
|
|
231
|
-
"Email": "text"
|
|
232
|
-
},
|
|
233
|
-
"edges": {}
|
|
234
|
-
},
|
|
235
|
-
"Support Ticket": {
|
|
236
|
-
"properties": {
|
|
237
|
-
"Status": "text",
|
|
238
|
-
"Priority": "number"
|
|
239
266
|
},
|
|
240
|
-
"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
"description": "A change to a Support Ticket in the graph: what happened (`Kind`), which properties and edges moved (`Changed Fields`), and the record as the change left it (`Current`, absent once deleted; test it present before reading through it: `exists(c.Current) AND c.Current.Status == \"Open\"`). Walk `Record` for the record as it is now.",
|
|
268
|
+
"fieldDocs": {
|
|
269
|
+
"Id": "Names the change: the same each time the graph reports it.",
|
|
270
|
+
"Kind": "What happened to the record.",
|
|
271
|
+
"Changed Fields": "The properties and edges the change moved, by name: every one a created record was given, none for a deleted one.",
|
|
272
|
+
"Record Id": "The graph's id for the record, a deleted one's too.",
|
|
273
|
+
"Current": "The record as the change left it, every property by name. Absent when it was deleted."
|
|
246
274
|
}
|
|
275
|
+
},
|
|
276
|
+
"Attached Resource": {
|
|
277
|
+
"properties": {},
|
|
278
|
+
"edges": {},
|
|
279
|
+
"undescribed": true
|
|
247
280
|
}
|
|
248
281
|
},
|
|
249
|
-
"unions": {
|
|
250
|
-
"[\"Record Change\",[[\"type\",\"Company\"]]]": [
|
|
251
|
-
"[\"Record Change\",[[\"action\",\"record.created\"],[\"type\",\"Company\"]]]",
|
|
252
|
-
"[\"Record Change\",[[\"action\",\"record.updated\"],[\"type\",\"Company\"]]]",
|
|
253
|
-
"[\"Record Change\",[[\"action\",\"record.deleted\"],[\"type\",\"Company\"]]]"
|
|
254
|
-
],
|
|
255
|
-
"[\"Record Change\",[[\"type\",\"Person\"]]]": [
|
|
256
|
-
"[\"Record Change\",[[\"action\",\"record.created\"],[\"type\",\"Person\"]]]",
|
|
257
|
-
"[\"Record Change\",[[\"action\",\"record.updated\"],[\"type\",\"Person\"]]]",
|
|
258
|
-
"[\"Record Change\",[[\"action\",\"record.deleted\"],[\"type\",\"Person\"]]]"
|
|
259
|
-
],
|
|
260
|
-
"[\"Record Change\",[[\"type\",\"Support Ticket\"]]]": [
|
|
261
|
-
"[\"Record Change\",[[\"action\",\"record.created\"],[\"type\",\"Support Ticket\"]]]",
|
|
262
|
-
"[\"Record Change\",[[\"action\",\"record.updated\"],[\"type\",\"Support Ticket\"]]]",
|
|
263
|
-
"[\"Record Change\",[[\"action\",\"record.deleted\"],[\"type\",\"Support Ticket\"]]]"
|
|
264
|
-
]
|
|
265
|
-
},
|
|
266
282
|
"collections": {
|
|
267
283
|
"Company": {
|
|
268
284
|
"target": "Company"
|
|
@@ -274,7 +290,6 @@
|
|
|
274
290
|
"target": "Support Ticket"
|
|
275
291
|
}
|
|
276
292
|
},
|
|
277
|
-
"supportsInPlaceUpdate": true,
|
|
278
293
|
"writableRoots": {
|
|
279
294
|
"Company": {
|
|
280
295
|
"fields": {
|
|
@@ -285,35 +300,123 @@
|
|
|
285
300
|
},
|
|
286
301
|
"Stage": "text"
|
|
287
302
|
},
|
|
288
|
-
"requiredFields": [],
|
|
289
|
-
"fuzzyResolution": true,
|
|
290
303
|
"resultShape": {
|
|
291
304
|
"externalId": "text",
|
|
292
|
-
"
|
|
293
|
-
|
|
305
|
+
"url": "text",
|
|
306
|
+
"Name": "text",
|
|
307
|
+
"Domains": {
|
|
308
|
+
"kind": "list",
|
|
309
|
+
"of": "text"
|
|
310
|
+
},
|
|
311
|
+
"Stage": "text"
|
|
312
|
+
},
|
|
313
|
+
"edges": {
|
|
314
|
+
"Team": {
|
|
315
|
+
"target": "Person",
|
|
316
|
+
"writable": true
|
|
317
|
+
},
|
|
318
|
+
"Support Tickets": {
|
|
319
|
+
"target": "Support Ticket",
|
|
320
|
+
"writable": true
|
|
321
|
+
},
|
|
322
|
+
"Resources": {
|
|
323
|
+
"target": "Attached Resource",
|
|
324
|
+
"resources": true
|
|
325
|
+
},
|
|
326
|
+
"Changes": {
|
|
327
|
+
"target": "Company Change",
|
|
328
|
+
"readable": false,
|
|
329
|
+
"listenable": true,
|
|
330
|
+
"identity": "Id",
|
|
331
|
+
"delivery": "connection",
|
|
332
|
+
"carries": [
|
|
333
|
+
"Company"
|
|
334
|
+
],
|
|
335
|
+
"attributes": true,
|
|
336
|
+
"description": "What happens to a Company in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"fuzzyResolution": true
|
|
294
340
|
},
|
|
295
341
|
"Person": {
|
|
296
342
|
"fields": {
|
|
297
343
|
"Name": "text",
|
|
298
344
|
"Email": "text"
|
|
299
345
|
},
|
|
300
|
-
"requiredFields": [],
|
|
301
346
|
"resultShape": {
|
|
302
347
|
"externalId": "text",
|
|
303
|
-
"
|
|
304
|
-
|
|
348
|
+
"url": "text",
|
|
349
|
+
"Name": "text",
|
|
350
|
+
"Email": "text"
|
|
351
|
+
},
|
|
352
|
+
"edges": {
|
|
353
|
+
"Companies": {
|
|
354
|
+
"target": "Company",
|
|
355
|
+
"writable": true
|
|
356
|
+
},
|
|
357
|
+
"Reported Tickets": {
|
|
358
|
+
"target": "Support Ticket",
|
|
359
|
+
"writable": true
|
|
360
|
+
},
|
|
361
|
+
"Resources": {
|
|
362
|
+
"target": "Attached Resource",
|
|
363
|
+
"resources": true
|
|
364
|
+
},
|
|
365
|
+
"Changes": {
|
|
366
|
+
"target": "Person Change",
|
|
367
|
+
"readable": false,
|
|
368
|
+
"listenable": true,
|
|
369
|
+
"identity": "Id",
|
|
370
|
+
"delivery": "connection",
|
|
371
|
+
"carries": [
|
|
372
|
+
"Person"
|
|
373
|
+
],
|
|
374
|
+
"attributes": true,
|
|
375
|
+
"description": "What happens to a Person in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"fuzzyResolution": true
|
|
305
379
|
},
|
|
306
380
|
"Support Ticket": {
|
|
307
381
|
"fields": {
|
|
308
382
|
"Status": "text",
|
|
309
383
|
"Priority": "number"
|
|
310
384
|
},
|
|
311
|
-
"requiredFields": [],
|
|
312
385
|
"resultShape": {
|
|
313
386
|
"externalId": "text",
|
|
314
|
-
"
|
|
315
|
-
|
|
387
|
+
"url": "text",
|
|
388
|
+
"Status": "text",
|
|
389
|
+
"Priority": "number"
|
|
390
|
+
},
|
|
391
|
+
"edges": {
|
|
392
|
+
"Reporter": {
|
|
393
|
+
"target": "Person",
|
|
394
|
+
"writable": true
|
|
395
|
+
},
|
|
396
|
+
"Company": {
|
|
397
|
+
"target": "Company",
|
|
398
|
+
"writable": true
|
|
399
|
+
},
|
|
400
|
+
"Resources": {
|
|
401
|
+
"target": "Attached Resource",
|
|
402
|
+
"resources": true
|
|
403
|
+
},
|
|
404
|
+
"Changes": {
|
|
405
|
+
"target": "Support Ticket Change",
|
|
406
|
+
"readable": false,
|
|
407
|
+
"listenable": true,
|
|
408
|
+
"identity": "Id",
|
|
409
|
+
"delivery": "connection",
|
|
410
|
+
"carries": [
|
|
411
|
+
"Support Ticket"
|
|
412
|
+
],
|
|
413
|
+
"attributes": true,
|
|
414
|
+
"description": "What happens to a Support Ticket in the graph, each change once: created, updated (`Changed Fields` names what moved), deleted."
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
"fuzzyResolution": true
|
|
316
418
|
}
|
|
317
|
-
}
|
|
419
|
+
},
|
|
420
|
+
"supportsInPlaceUpdate": true
|
|
318
421
|
}
|
|
319
422
|
}
|
package/dist/chapters/asks.js
CHANGED
|
@@ -21,7 +21,7 @@ msg = write deals-[:Messages]-> {
|
|
|
21
21
|
r = race(
|
|
22
22
|
() => {
|
|
23
23
|
press = await msg-[:Interactions]->
|
|
24
|
-
return only(press-[:Actions]->)
|
|
24
|
+
return only(press-[:Actions]->)?.Value
|
|
25
25
|
},
|
|
26
26
|
() => { await sleep(2d) },
|
|
27
27
|
)
|
|
@@ -82,7 +82,7 @@ msg = write approvals-[:Messages]-> {
|
|
|
82
82
|
r = race(
|
|
83
83
|
() => {
|
|
84
84
|
press = await msg-[:Interactions]->
|
|
85
|
-
return only(press-[:Actions]->)
|
|
85
|
+
return only(press-[:Actions]->)?.Value
|
|
86
86
|
},
|
|
87
87
|
() => { await sleep(1d) },
|
|
88
88
|
)
|
|
@@ -222,7 +222,7 @@ A deadline, a reminder and an escalation are all **composed** from \`race\` and
|
|
|
222
222
|
r = race(
|
|
223
223
|
() => {
|
|
224
224
|
press = await msg-[:Interactions]->
|
|
225
|
-
return only(press-[:Actions]->)
|
|
225
|
+
return only(press-[:Actions]->)?.Value
|
|
226
226
|
},
|
|
227
227
|
() => {
|
|
228
228
|
await sleep(6h)
|
|
@@ -422,7 +422,7 @@ function \`Pursue Or Pass\`(go: <manual-[:Invocation]->>) {
|
|
|
422
422
|
r = race(
|
|
423
423
|
() => {
|
|
424
424
|
press = await msg-[:Interactions]->
|
|
425
|
-
return only(press-[:Actions]->)
|
|
425
|
+
return only(press-[:Actions]->)?.Value
|
|
426
426
|
},
|
|
427
427
|
() => {
|
|
428
428
|
await sleep(6h)
|
|
@@ -79,7 +79,7 @@ function \`Notify Company Change\`(evt: <crm-[:CompanyChange]->>) {
|
|
|
79
79
|
### Pitfalls
|
|
80
80
|
|
|
81
81
|
- **A flag set inside a block to detect presence.** A binding means one thing everywhere it is visible, so re-using an outer name inside a block is refused when you save. Nothing needs the flag: the block already runs once per position and not at all when there are none, so the block itself *is* the "has one" branch; pair it with \`if (!exists(msg-[:Attachments]->)) { … }\` for "has none". \`count(…)\` for how many, \`exists(…)\` for whether-any.
|
|
82
|
-
- **Binding a field just to test it.** Compare a field off a record directly — \`if (x
|
|
82
|
+
- **Binding a field just to test it.** Compare a field off a record directly — \`if (x?.Field != null) { … }\`, \`if (x?.Field == null) { fail("…") }\` — and that narrows that field, and the record it came off, for what follows. \`?.\` reads through a record that may be absent; on one that is present, \`x.Field\` tests the same.
|
|
83
83
|
- **Using \`fail(…)\` for an ordinary skip.** A plain \`if\` with no \`else\` already does nothing; keep \`fail(…)\` for failures worth surfacing.`,
|
|
84
84
|
engineClaims: [
|
|
85
85
|
{
|
|
@@ -68,7 +68,7 @@ function m(evt: <crm-[:CompanyChange]->>) {
|
|
|
68
68
|
oldest = List.last(sort(names))
|
|
69
69
|
head = sort(names)[0]
|
|
70
70
|
write co {
|
|
71
|
-
Description: "\${n} people, \${Text.join(sort(names), ", ")}, \${coalesce(owner
|
|
71
|
+
Description: "\${n} people, \${Text.join(sort(names), ", ")}, \${coalesce(owner?.Name, "no CEO")}, \${coalesce(newest, "")}/\${coalesce(oldest, "")}/\${coalesce(head, "")}"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -174,7 +174,7 @@ doubled = map(counts, (x) => x * 2)
|
|
|
174
174
|
Because a function's arguments are ordinary expressions, a call can sit inside any expression: \`double(n) + 1\`, \`extractOne(content, Company)\`, \`count(map(xs, f))\`, \`map(map(xs, f), g)\`, \`log(map(xs, f))\`.
|
|
175
175
|
|
|
176
176
|
- A nested call means exactly what binding it to a name first and reading the name means, and it is checked and typed that way. A call that returns text written into a number field is refused. Its effects count towards the function around it.
|
|
177
|
-
- A field reads off any call's result, as it reads off a name: \`
|
|
177
|
+
- A field reads off any call's result, as it reads off a name: \`lookup(id).email\`. A call whose result may be absent is read with \`?.\`, and the field is absent when the result is: \`extractOne(content, Company)?.name\`, \`first(people)?.Name\`.
|
|
178
178
|
- **Order is left to right, as the text reads.** Every operand before a nested call is read before the call runs, and a call's arguments are all evaluated before its body runs, so \`Text.concat(mark("a"), mark("b"))\` writes \`a\` and then \`b\`.
|
|
179
179
|
- **\`? :\`, \`&&\`, \`||\` and \`coalesce\` short-circuit**, as TypeScript's \`? :\`, \`&&\`, \`||\` and \`??\` do. An operand they do not need is never evaluated, so a call in an arm that is not taken never runs and its writes never happen. \`coalesce(x, only(list))\` does not fail when \`x\` is present, however many values \`list\` holds.
|
|
180
180
|
- **Suspension is for statements only.** A call that may wait (a function that \`await\`s) is refused inside an expression, because a wait parks the run where it is written and a place inside an expression has nowhere to come back to. Call it on its own line and use the name.
|
|
@@ -248,18 +248,18 @@ Name: coalesce(msg-[:Sender]->.Name, msg-[:Sender]->.Email)
|
|
|
248
248
|
|
|
249
249
|
A traversal that yields nothing makes the value empty rather than failing, and \`coalesce\` takes the first non-empty of its arguments. To test rather than fall back: \`exists(msg-[:Sender]->)\` tells whether a relationship has any records, and \`x != null\` or \`x == null\` tells whether a binding or a field read off one has a value — \`\`Suggested Action\` != null\`, \`channel.Topic == null\`.
|
|
250
250
|
|
|
251
|
-
A separate and stronger thing is a value typed as *possibly not there at all* — \`T | absent\`. That is a real part of the type, not a caution.
|
|
251
|
+
A separate and stronger thing is a value typed as *possibly not there at all* — \`T | absent\`. That is a real part of the type, not a caution. These produce one:
|
|
252
252
|
|
|
253
253
|
- **an aggregate that can come up empty.** \`only\`, \`first\`, \`List.last\`, \`List.min\`, \`List.max\`, and \`xs[0]\` all answer null when there's nothing to aggregate over, so \`only(msg-[:Attachments]->.Name)\` may be absent exactly when there are no attachments. Over a *bare* traversal (no field on the end) they bind the whole record, not one of its fields — \`channel = only(chat-[ch:Channels WHERE …]->)\` — see *looking-up-existing-records* in the writes chapter for that idiom;
|
|
254
254
|
- **a named \`match\`.** \`existing = match crm-[:Companies]-> { … }\` is the record or absent; test \`existing == null\` (the \`else\` arm sees it present) or \`!= null\` before reading a field off it or linking it;
|
|
255
|
-
- **a record held in a field.** A field may hold a record: a value with typed fields, such as a change's row before and after (\`c.Previous\`, \`c.Current\`). Its fields are read with \`.\` (\`c.Current.Stage\`); it has no edges, so the record as it is now is reached by its edge (\`c-[:Record]->\`). \`c.Previous\` is absent on a created row, and a field read through it is refused until tested: \`exists(c.Previous) && c.Previous.Stage == "Applied"\`, \`!exists(c.Previous) || c.Previous.Stage != "Interview"\`, or inside \`if (exists(c.Previous)) { … }\` (in a \`WHERE\`: \`AND\`, \`OR\`, \`NOT\`);
|
|
255
|
+
- **a record held in a field.** A field may hold a record: a value with typed fields, such as a change's row before and after (\`c.Previous\`, \`c.Current\`). Its fields are read with \`.\` (\`c.Current.Stage\`); it has no edges, so the record as it is now is reached by its edge (\`c-[:Record]->\`). \`c.Previous\` is absent on a created row, and a field read through it with \`.\` is refused until tested: \`c.Previous?.Stage\` reads it (absent on a created row), or test it first, \`exists(c.Previous) && c.Previous.Stage == "Applied"\`, \`!exists(c.Previous) || c.Previous.Stage != "Interview"\`, or inside \`if (exists(c.Previous)) { … }\` (in a \`WHERE\`: \`AND\`, \`OR\`, \`NOT\`);
|
|
256
256
|
- **a \`race\` slot** — \`r[0]\` reads what the first arm of a \`race\` returned, and only the arm that settled first has its slot filled, so every slot off a \`race\` may be absent. (A \`map\` over a list of functions runs every one, so its slots are not.)
|
|
257
|
-
- **a field read off a record that may not be there.** \`details = extractOne(…)\` may have found nothing, so \`details.summary\` is \`text | absent
|
|
257
|
+
- **a field read off a record that may not be there.** \`details = extractOne(…)\` may have found nothing, so \`details.summary\` is refused, as TypeScript refuses it under strict null checks. Read it with \`details?.summary\`, which is \`text | absent\`: absent when \`details\` is, so \`details?.summary == null\` holds then. Or test \`exists(details)\` (or \`details != null\`) first and read \`details.summary\` after. \`details?.summary != null\` proves \`details\` present too. A graph literal's required field refuses such a value too: declare the field \`<text | null>\`, or fall back.
|
|
258
258
|
- **a typed extracted field, and a lookup by a computed key.** A \`<number>\`, \`<date>\`, \`<boolean>\` or set-of-values field of an \`extract\` is something the model was asked for and may not have found, so reading one is \`T | absent\` — a write field takes it with the \`?:\` fill rather than plainly. A text field is never absent: one the model did not find reads as \`""\`, so test it with \`!= ""\` — a null test on it is refused. Annotate it \`<text | null>\` to have a missing one arrive null instead: it is then \`text | absent\` like the rest, and a null test narrows it. \`d[key]\` with a key worked out at run time is the same: a key that is not there reads nothing.
|
|
259
259
|
|
|
260
260
|
Reading such a value is always fine, and so is interpolating it — it prints as nothing. Testing it needs no guard either. \`==\` and \`!=\` take it on either side: a missing value equals only \`null\`, so \`o.stage == "Seed"\` is false and \`o.stage != "Seed"\` true when the model found nothing. A condition takes a \`<boolean>\` that may be missing and reads a missing one as false — \`if (o.viable) { … }\`, \`o.viable ? a : b\`, \`a && o.viable\`, \`!o.viable\`.
|
|
261
261
|
|
|
262
|
-
What is refused is **using** it somewhere a value is genuinely required — a write field, a write or traversal target, a field read off it, an ordered comparison (\`<\`, \`<=\`, \`>\`, \`>=\`), arithmetic (\`n + 1\`, \`-n\`, \`Number.round(n)\`, and \`List.sum\`, \`List.avg\`, \`List.min\` or \`List.max\` over a list that may hold an absent member) — and the refusal comes while you write rather than at run time. There is no "it will probably be there".
|
|
262
|
+
What is refused is **using** it somewhere a value is genuinely required — a write field, a write or traversal target, a field read off it with \`.\` (read it with \`?.\` instead), an ordered comparison (\`<\`, \`<=\`, \`>\`, \`>=\`), arithmetic (\`n + 1\`, \`-n\`, \`Number.round(n)\`, and \`List.sum\`, \`List.avg\`, \`List.min\` or \`List.max\` over a list that may hold an absent member) — and the refusal comes while you write rather than at run time. There is no "it will probably be there".
|
|
263
263
|
|
|
264
264
|
Four things discharge it, and one of them always fits:
|
|
265
265
|
|
|
@@ -270,7 +270,7 @@ Four things discharge it, and one of them always fits:
|
|
|
270
270
|
\`\`\`
|
|
271
271
|
|
|
272
272
|
- **Gate on it with a traversal.** A block headed by the landing runs only when the landing is there, and everything inside it knows so: \`q-[a:Response]-> { … }\`.
|
|
273
|
-
- **Test it with \`==\`.** Comparing against \`null\` (\`x == null\` / \`x != null\`) is the dedicated presence test, and the one place \`==\` is deliberately loose: \`null\` and absent mean the same thing here, so it's legal against *any* possibly-missing value. Comparing against a present value works too and narrows the same way (\`if (a.Answer == "pursue") { … }\` is legal even though \`a.Answer\` may be absent). Either way, the arm that took the comparison knows the value is present — and for \`== null\` used as a guard clause (\`if (x == null) { fail("…") }\`), so does everything **after** the \`if\`, not just the arm. A test on a field read off a bound record proves the record too (\`if (channel
|
|
273
|
+
- **Test it with \`==\`.** Comparing against \`null\` (\`x == null\` / \`x != null\`) is the dedicated presence test, and the one place \`==\` is deliberately loose: \`null\` and absent mean the same thing here, so it's legal against *any* possibly-missing value. Comparing against a present value works too and narrows the same way (\`if (a.Answer == "pursue") { … }\` is legal even though \`a.Answer\` may be absent). Either way, the arm that took the comparison knows the value is present — and for \`== null\` used as a guard clause (\`if (x == null) { fail("…") }\`), so does everything **after** the \`if\`, not just the arm. A test on a field read off a bound record proves the record too (\`if (channel?.Topic != null)\`), so that arm may write off \`channel\`. The value-level form \`x != null ? … : …\` narrows only its first branch. See *narrowing-past-a-guard* in the branching chapter.
|
|
274
274
|
- **Fall back with \`coalesce\`.** \`coalesce(a, b)\` answers the first of its arguments that has a value, so the moment one of them always does — a literal, or anything already known to be there — the result always does too: \`Name: coalesce(only(msg-[:Attachments]->.Name), "no attachment")\`. A \`coalesce\` whose arguments might *all* be empty is itself possibly-empty and is refused in the same places, so the one-argument form discharges nothing at all.
|
|
275
275
|
|
|
276
276
|
For a list that may hold absent members, drop them (\`List.sum(List.compact(xs))\`) or fill them (\`List.sum(map(xs, (x) => coalesce(x, 0)))\`) before folding.
|
|
@@ -54,7 +54,7 @@ function \`Intake\`(m: <inbox-[:Emails]->>) {
|
|
|
54
54
|
sender = extractOne(content, node Sender: "the company that sent this message" { name: <text> "its name" })
|
|
55
55
|
\`\`\`
|
|
56
56
|
|
|
57
|
-
- Describe the one thing (\`"the company"\`, not \`"each company…"\`). The result is \`Shape | absent\`; handle it like any maybe-absent value (\`coalesce(sender
|
|
57
|
+
- Describe the one thing (\`"the company"\`, not \`"each company…"\`). The result is \`Shape | absent\`; handle it like any maybe-absent value (\`coalesce(sender?.name, "unknown")\`, a guard).
|
|
58
58
|
- A reply naming several is tried once more, then the call fails.
|
|
59
59
|
|
|
60
60
|
A reply that does not fit the shape is tried once more, then the call fails. Inside \`map\`, \`onError: "warn"\` leaves that record out and carries on.
|
|
@@ -132,7 +132,7 @@ map(detailed, (d) => {
|
|
|
132
132
|
|
|
133
133
|
- A plugin is imported (\`import { fetchUrl } from plugins\`) and called as a function is: its required input by position, any other in one settings record last (\`research(c.name, { context: m.Subject })\`). It returns a value: \`fetchUrl\` the page's text, \`research\` a record read by name (\`more.summary\`, \`more.dossier\`). Anything it found nothing for is absent, so \`coalesce\` it before it goes into content. Each plugin's own chapter (\`plugin:…\`) says what it takes and returns.
|
|
134
134
|
- \`graph<Detailed> { ...c, ...details }\` copies the record and then its detail as a snapshot you can walk and write from; the declaration decides which fields, and a nested node is followed through the record's edge of the same name. A later field wins.
|
|
135
|
-
- \`details\` may be absent, so a field only it supplies may be too. A required field fed from it is refused when you save: declare it \`<text | null>\`, or write it after the spread with a fallback (\`summary: coalesce(details
|
|
135
|
+
- \`details\` may be absent, so a field only it supplies may be too. A required field fed from it is refused when you save: declare it \`<text | null>\`, or write it after the spread with a fallback (\`summary: coalesce(details?.summary, "")\`).
|
|
136
136
|
- \`{ ...c, ...details }\` builds a dict instead, read with a dot. Use it when nothing needs to walk the result.
|
|
137
137
|
|
|
138
138
|
### long-documents
|
|
@@ -428,7 +428,7 @@ function \`Enrich\`(m: <inbox-[:Emails]->>) {
|
|
|
428
428
|
write crm-[:Companies]-> {
|
|
429
429
|
unique by (FUZZY Name)
|
|
430
430
|
Name: m.Subject
|
|
431
|
-
Description ?: coalesce(more.summary, page, deck
|
|
431
|
+
Description ?: coalesce(more.summary, page, deck?.text)
|
|
432
432
|
Domains ?: more.website
|
|
433
433
|
}
|
|
434
434
|
}
|
|
@@ -461,7 +461,7 @@ function \`Intake\`(m: <inbox-[:Emails]->>) {
|
|
|
461
461
|
write crm-[:Companies]-> {
|
|
462
462
|
unique by (FUZZY Name)
|
|
463
463
|
Name: c.name
|
|
464
|
-
Description ?: detail
|
|
464
|
+
Description ?: detail?.summary
|
|
465
465
|
}
|
|
466
466
|
}, { initialConcurrency: 1, concurrency: 4, onError: "warn" })
|
|
467
467
|
}
|
|
@@ -498,8 +498,8 @@ function \`Enrich\`(m: <inbox-[:Emails]->>) {
|
|
|
498
498
|
})
|
|
499
499
|
|
|
500
500
|
sender = extractOne(content, node Sender: "the company that sent this message" { name: <text> "its name" })
|
|
501
|
-
big = coalesce(extractOne(content, CompanyDetail)
|
|
502
|
-
write crm-[:Companies]-> { unique by (FUZZY Name) Name: coalesce(sender
|
|
501
|
+
big = coalesce(extractOne(content, CompanyDetail)?.employees, 0) > 50
|
|
502
|
+
write crm-[:Companies]-> { unique by (FUZZY Name) Name: coalesce(sender?.name, "unknown"), Description: big ? "large" : "small" }
|
|
503
503
|
}
|
|
504
504
|
`,
|
|
505
505
|
},
|
|
@@ -49,7 +49,8 @@ Types **always** wear angle brackets; positions, scalar values, and handles **ne
|
|
|
49
49
|
- Brackets: automation params (\`m: <inbox-[:Emails]->>\`, \`root: <crm>\`), extract field annotations (\`amount: <number>\`, borrowed \`stage: <crm-[:Requests]->.Stage>\`), declared fields (\`Name: <text>\`), \`is\` tests (\`rec is <crm-[:Companies]->>\`), explicit polymorphic targets (\`match a-[:related]-><Companies> { … }\`).
|
|
50
50
|
- No brackets: write targets (\`write crm-[:Companies]->\`), traversal hops and edge names, \`unique by\` components, call arguments, per-family answer types (a \`Check\` answers boolean, a \`Provide\` answers whatever its \`Answer Type\` names — no bracket-generic syntax).
|
|
51
51
|
- Primitives: \`<text>\`, \`<number>\`, \`<boolean>\`, \`<date>\`, \`<datetime>\`, \`<file>\`, \`<json>\`. Borrowed path: \`<instance-[:record_type]->.field>\`. An extract or declared field may add \`| null\` (\`<text | null>\`): a missing one arrives null.
|
|
52
|
-
- One of several record types: \`n: <crm-[:Memo]-> | crm-[:Remark]->>\` takes a Memo or a Remark. It reads, walks and writes only what every member has, and takes a value of any member, or of a union whose every member it holds.
|
|
52
|
+
- One of several record types: \`n: <crm-[:Memo]-> | crm-[:Remark]->>\` takes a Memo or a Remark. It reads, walks and writes only what every member has, and takes a value of any member, or of a union whose every member it holds. \`is\` narrows it: \`if (n is <crm-[:Memo]->>) { … n.Mood … } else { … n.Tone … }\`, and below an arm that always returns or fails, it is the rest.
|
|
53
|
+
- A walk off a union lands the same way: where a Person's \`Notes\` land on a Memo and a Company's on a Remark, \`crm-[:Subjects]->-[n:Notes]->\` lands a Memo or a Remark, narrowed by \`is\` as above. So is a \`? :\` or \`coalesce\` of records of different types.
|
|
53
54
|
- Declared refinement: \`type Thesis = <"Consumer" | "Infra">\` at file scope, then \`<Thesis>\` anywhere a type goes. It constrains an extraction to those values, tells the model which to pick from, and flags a literal that is not one of them; the values are ordinary text everywhere else.
|
|
54
55
|
- \`<json>\` is a **structured value** — an object or list literal written verbatim into a field that takes one (a system's own rich-message document; its chapter names the field). Opaque: pass it through, never operate on it. Keys inside an object literal are comma-separated; a write body's fields are newline-separated.
|
|
55
56
|
|
|
@@ -158,13 +159,15 @@ fail("unexpected record kind") # abort the whole run with a re
|
|
|
158
159
|
\`\`\`
|
|
159
160
|
n = only(company-[t:Notes WHERE t.Title == "Brief"]->) # T | absent — a lookup that might match nothing
|
|
160
161
|
d = first(company.Domains) # T | absent — same, over a field
|
|
162
|
+
t = n?.Title # reads through n: absent when n is
|
|
163
|
+
if (n?.Title != null) { … } # proves n present INSIDE this arm, as exists(n) does
|
|
161
164
|
if (n == null) { fail("no notes found") } # guard clause — n is present for every line after
|
|
162
165
|
if (n != null) { … } # narrows n present INSIDE this arm only
|
|
163
166
|
x = d != null ? "\${d}" : "" # narrows d present in the true arm only
|
|
164
167
|
Field ?: d # '?:' takes a maybe-absent value, no guard needed
|
|
165
168
|
\`\`\`
|
|
166
169
|
|
|
167
|
-
\`first\`/\`List.last\`/\`List.min\`/\`List.max\`/\`xs[n]\` over anything that can come up empty, and a named \`match\` that found nothing, type \`T | absent\`. Using such a value where a present one is required — a write or traversal target, a
|
|
170
|
+
\`first\`/\`List.last\`/\`List.min\`/\`List.max\`/\`xs[n]\` over anything that can come up empty, and a named \`match\` that found nothing, type \`T | absent\`. Using such a value where a present one is required — a write or traversal target, a field read with \`.\`, an ordered comparison (\`<\` \`<=\` \`>\` \`>=\`) — is refused until narrowed, naming the fix. \`==\`/\`!=\` against \`null\` is the presence test (loose: \`null\` and absent mean the same thing); against any other value a maybe-absent side needs no guard (absent is never equal to a present value), and a condition (\`if\`, \`? :\`, \`&&\`/\`||\`/\`!\`) reads a missing boolean as false. Both sides must still match in kind. \`exists(…)\` takes related records (\`exists(msg-[:Attachments]->)\`) or a value (\`exists(x)\`, \`exists(x?.Field)\`: the same test as \`x?.Field != null\`); \`x != null\` and \`x == null\` take a bound name or a field read off one, and a proven field proves the record it came off. A field may hold a record, a value read with \`.\` (\`c.Current.Stage\`): it has fields only, no edges, and the record as it is now is reached by its edge (\`c-[:Record]->\`). A field is never read with \`.\` through a value that may be absent: any of the above, a call's result (\`only(…).F\`), an index read (\`xs[0].F\`), a change's row (\`c.Previous\` is absent on a created row) or a map member. Read it with \`?.\` (\`n?.Title\`, \`c.Previous?.Stage\`), absent when the value is, or test it first: \`exists(c.Previous) && c.Previous.Stage == "Applied"\`, \`!exists(c.Previous) || c.Previous.Stage != "Interview"\`, or inside \`if (exists(c.Previous)) { … }\`. A test of an index read at a literal (\`exists(xs[0])\`) proves every later \`xs[0]\`. A WHERE writes them \`AND\`, \`OR\`, \`NOT\`.
|
|
168
171
|
|
|
169
172
|
### listeners
|
|
170
173
|
|
|
@@ -191,7 +194,7 @@ msg = write ops-[:Messages]-> {
|
|
|
191
194
|
r = race(
|
|
192
195
|
() => {
|
|
193
196
|
press = await msg-[:Interactions]-> # one press
|
|
194
|
-
return only(press-[:Actions]->)
|
|
197
|
+
return only(press-[:Actions]->)?.Value # "pursue" | "pass", from the Buttons written
|
|
195
198
|
},
|
|
196
199
|
() => {
|
|
197
200
|
reply = await new msg-[:Replies]-> # only a reply arriving from now
|
|
@@ -254,8 +257,8 @@ Content is a list (shared content first, record-specific content last, so repeat
|
|
|
254
257
|
- **Leaving a wait with no timeout.** An \`await\` on a person or a system waits forever when nothing comes, or when its record is deleted; race it against \`() => { await sleep(2d) }\` so a default takes over when time runs out.
|
|
255
258
|
- **Expecting one extraction per piece from a list.** \`extract(pieces, Shape)\` reads them all as a single job; \`map(pieces, (p) => extract([p], Shape))\` is the per-piece form.
|
|
256
259
|
- **Verifying from the target system only.** The run record shows what was sent and why; read provenance before editing the automation.
|
|
257
|
-
- **Using a maybe-absent value unguarded.** A write/traversal target, a link end, a
|
|
258
|
-
- **Binding a field just to test it.** \`x.Field != null\` reads the field directly; no intermediate binding.
|
|
260
|
+
- **Using a maybe-absent value unguarded.** A write/traversal target, a link end, a field read with \`.\`, or an ordered comparison off a \`first\`/\`List.last\`/\`List.min\`/\`List.max\`/\`xs[n]\` result or a named \`match\` is refused until narrowed — guard with \`if (x == null) { fail(…) }\` (or an \`else\` arm), \`x != null\`, or \`?:\` on the write field, or read the field with \`x?.Field\`.
|
|
261
|
+
- **Binding a field just to test it.** \`x.Field != null\` reads the field directly (\`x?.Field != null\` when \`x\` may be absent, which proves \`x\` present too); no intermediate binding.
|
|
259
262
|
- **Keeping a closure as data.** A closure lives in a variable or a parameter, or is passed as an argument where it is written (\`map(xs, (x) => …)\`, \`all(f, g)\`). In a list, under a key, in a field or as what a \`map\` returns it is refused when you save.
|
|
260
263
|
- **Calling something that may not be a function.** A name is called when its type shows it is a closure (\`g = label\` then \`g()\`, a closure-typed parameter); a value that may be something else is refused when you save.`,
|
|
261
264
|
};
|
|
@@ -122,7 +122,7 @@ Message: "Logged \${count(orgs)} companies"
|
|
|
122
122
|
|
|
123
123
|
### Pitfalls
|
|
124
124
|
|
|
125
|
-
- **Reaching for a loop.**
|
|
125
|
+
- **Reaching for a loop.** Working through every attachment is a traversal-headed block. If you're trying to enumerate, you're describing an edge to traverse.
|
|
126
126
|
- **Counting the wrong thing.** \`count\` over a *source* walk (\`count(msg-[:Attachments]->)\`) counts what the source holds; \`count\` over a bound block (\`count(orgs)\`) counts what the block *returned*. Both run — make sure you aggregate the one you mean.
|
|
127
127
|
- **Reaching inside a block from outside it.** A name bound in the body — the head's alias \`a\` included — does not exist after the closing brace. \`return\` what the rest of the automation needs.`,
|
|
128
128
|
engineClaims: [
|
|
@@ -100,7 +100,7 @@ function \`Log Dealflow\`(msg: <inbox-[:Emails]->>) {
|
|
|
100
100
|
unique by (FUZZY Name)
|
|
101
101
|
Name: c.name
|
|
102
102
|
Domains: c.website
|
|
103
|
-
Description ?: "Introduced by \${introducer
|
|
103
|
+
Description ?: "Introduced by \${introducer?.name}"
|
|
104
104
|
}
|
|
105
105
|
c-[f:founder]-> {
|
|
106
106
|
write company-[:Team]-> {
|
package/dist/front_page.js
CHANGED
|
@@ -76,7 +76,7 @@ exports.TS_EXCEPT = [
|
|
|
76
76
|
{
|
|
77
77
|
anchor: 'maybe-absent',
|
|
78
78
|
title: 'Absent, not undefined',
|
|
79
|
-
text: 'No `undefined
|
|
79
|
+
text: 'No `undefined` or `??`. What may be missing (`first`, `only`, `xs[i]`, `extractOne`, a named `match`) is `T | absent`. `x?.F` reads through it, absent when `x` is; `x.F` and arithmetic are refused until narrowed: `if (x == null) { fail("…") }`, `coalesce(x, fallback)`, `Field ?: x`.',
|
|
80
80
|
example: {
|
|
81
81
|
body: 'co = only(crm-[c:Companies WHERE c.Name == m.Subject]->)\nif (co == null) { fail("no company ${m.Subject}") }\nseen = match crm-[:People]-> { unique by (Email), Email: m.From }\nif (seen != null) { link seen -[:Company]-> co }',
|
|
82
82
|
},
|
package/dist/language_search.js
CHANGED
|
@@ -57,7 +57,7 @@ exports.CONTROL_FORMS = [
|
|
|
57
57
|
signature: 'race(() => { … }, …) → receipt; r[i] is arm i\'s returned value, null unless it settled first',
|
|
58
58
|
purpose: 'wait for whichever of several things happens first, such as an answer or a timeout',
|
|
59
59
|
example: {
|
|
60
|
-
body: 'ops = only(chat-[c:Channels WHERE c.Name == "ops"]->)\nif (ops == null) { fail("no #ops channel") }\nmsg = write ops-[:Messages]-> {\n Message: "Log ${m.Subject}?"\n Buttons: [{ Text: "Log", Value: "log" }, { Text: "Skip", Value: "skip" }]\n}\nr = race(\n () => {\n press = await msg-[:Interactions]->\n return only(press-[:Actions]->)
|
|
60
|
+
body: 'ops = only(chat-[c:Channels WHERE c.Name == "ops"]->)\nif (ops == null) { fail("no #ops channel") }\nmsg = write ops-[:Messages]-> {\n Message: "Log ${m.Subject}?"\n Buttons: [{ Text: "Log", Value: "log" }, { Text: "Skip", Value: "skip" }]\n}\nr = race(\n () => {\n press = await msg-[:Interactions]->\n return only(press-[:Actions]->)?.Value\n },\n () => { await sleep(2d) },\n)\napproved = r[0] == "log"',
|
|
61
61
|
},
|
|
62
62
|
anchor: 'front#runs',
|
|
63
63
|
},
|
package/dist/prose_rules.js
CHANGED
|
@@ -30,7 +30,7 @@ const AUTHORING_TOOL_NAMES = [
|
|
|
30
30
|
const RETIRED_SURFACE = [
|
|
31
31
|
{ pattern: /EXTRACT_VALUE/, label: 'EXTRACT_VALUE' },
|
|
32
32
|
{ pattern: /#extract/, label: '#extract' },
|
|
33
|
-
{ pattern: /\bfor
|
|
33
|
+
{ pattern: /\bfor\s+each\b/i, label: 'for each' },
|
|
34
34
|
{ pattern: /\bcallback\(/, label: 'callback(…) — buttons on the message, or a form from `@lf/forms`, then `await` the edge the answer lands on' },
|
|
35
35
|
{ pattern: /\bawait\s+first\(/, label: 'await first(…) — write `await x-[:Edge]->`' },
|
|
36
36
|
];
|
package/dist/testing.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@listen-fire/handbook",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.1",
|
|
5
5
|
"description": "The movement language's handbook: the hand-written chapters, the front page, the routes into them and the language search over them. A host adds its own systems' and plugins' sections on top (the platform assembles system:* and plugin:* chapters). `testing` is the catalog every shown example is checked against, for a host's tests of its own sections.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.23.4 <23"
|