@suss/recognize 0.12.0 → 0.13.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/README.md CHANGED
@@ -45,8 +45,10 @@ where the match starts -> which method -> read the arguments -> yield
45
45
  a sub-unit
46
46
  ```
47
47
 
48
- Only the effects ending is built. The other three are further members of
49
- `Ending` with an entry in the compile table.
48
+ Two endings are built, and both of them yield effects: one asks the
49
+ call what it reached, and one reads the statement the call was handed.
50
+ Discovery, terminals and sub-units are further members of `Ending` with
51
+ an entry in the compile table.
50
52
 
51
53
  ### Where a match starts
52
54
 
@@ -102,14 +104,304 @@ result on the recognizer context under `ops`.
102
104
  |---|---|
103
105
  | `method()` | which method the call reaches for, as the source spells it |
104
106
  | `receiverIsFrom(origin)` | whether the receiver came from where the origin says |
107
+ | `isFrom(origin)` | whether the call itself came from where the origin says |
105
108
  | `argumentCount()` | how many arguments the call passes |
106
109
  | `nameAt(index, unsettled)` | the name that argument gives, with the hole policy applied |
107
110
  | `calleeText()` | the callee, as the source writes it |
111
+ | `receiver()` | the call the receiver is, as ops of its own |
112
+ | `argument(index)` | the call that argument is, as ops of its own |
113
+ | `callee()` | the call the callee itself was written as |
114
+ | `propertyAt(index, property, unsettled)` | what a named property of that argument says |
115
+ | `valueAt(index)` | the value that argument states, as `ValueOps` |
116
+
117
+ `receiver()`, `argument(index)` and `callee()` read a call next to the
118
+ one in hand: each gives back another `CallOps`, so every question above
119
+ works one step along. `callee()` is the one for a class a factory made, where the
120
+ source writes `new User({ name })` and what `User` is comes from the
121
+ `model("User", schema)` call it was declared as.
122
+
123
+ The last two reach a value rather than a call. `propertyAt` pulls one
124
+ name out of a property bag, which is what a pack that wants a bucket or
125
+ a table wants. `valueAt` hands the value over whole, for a pack whose
126
+ rule has to walk it:
127
+
128
+ | op | what it gives back |
129
+ |---|---|
130
+ | `text()` | the string the source wrote, or null for anything else |
131
+ | `flag()` | the yes or no the source wrote, taking a number the way it takes a boolean |
132
+ | `entries(unsettled)` | what an object states, as key and value per entry |
133
+ | `items()` | what a list states, item by item |
134
+ | `property(name)` | what one named property states, as a value of its own |
135
+ | `parts()` | the pieces of text the source wrote, with the holes left out |
136
+ | `holes()` | what the source interpolated between those pieces, each as the call it was written as |
137
+
138
+ A key the source computes, `{ [this.tableName]: [...] }`, is read the
139
+ way any other name is, so an entry's key comes back settled where the
140
+ source settles it.
108
141
 
109
142
  A chain running on an adapter with no ops matches nothing rather than
110
143
  throwing, since a pack loaded into the wrong adapter is a configuration
111
144
  mistake and not a crash.
112
145
 
146
+ ## Reaching a call next to this one
147
+
148
+ Two of the three storage shapes put what a pack needs somewhere other
149
+ than the call the adapter is standing on.
150
+
151
+ ```ts
152
+ s3.send(new GetObjectCommand({ Bucket: "photos", Key: "a.jpg" }));
153
+ storage.bucket("photos").file("a.jpg").download();
154
+ ```
155
+
156
+ The first says `send` at every call site in the codebase and puts the
157
+ operation, the bucket and the key in the command. The second puts the
158
+ bucket two hops back up the receivers and the object one hop back.
159
+ Neither needs a question of its own: both are the questions above,
160
+ asked of a different call.
161
+
162
+ A pack says which call with `about`, and a pick says which call with
163
+ `of`. Both take steps, and a step is data:
164
+
165
+ | step | what it reaches |
166
+ |---|---|
167
+ | `{ to: "receiver" }` | the call the receiver is |
168
+ | `{ to: "receiver", method: "bucket" }` | the first call to `bucket` up the receivers |
169
+ | `{ to: "argument", at: 0 }` | the call the first argument is |
170
+ | `{ to: "argument", at: { from: 0 } }` | every argument that is a call, first one that matches wins |
171
+ | `{ to: "argument", at: { from: 0 }, origin }` | the same, keeping only the ones the origin pins down |
172
+
173
+ ```ts
174
+ storageCalls({ system: "s3", client: constructedFrom("@aws-sdk/client-s3") })
175
+ .about({ to: "argument", at: { from: 0 } })
176
+ .methods({ GetObjectCommand: { kind: "read", selector: KEY } })
177
+ .container({ at: 0, property: ["Bucket"] });
178
+ ```
179
+
180
+ With a subject, every other link is asked of the call the steps reach:
181
+ the operation is that call's own name, the origin check is about that
182
+ call, and a pick with no `of` reads that call's arguments. The effect
183
+ still records the call in hand as its callee, since that is where a
184
+ reader would go and look.
185
+
186
+ A step follows what the source wrote a name as, so `const side = await
187
+ deck.side("a")` reaches `deck.side("a")` rather than stopping at the
188
+ await.
189
+
190
+ A step that says which method is searched for rather than counted to,
191
+ because `bucket(b).file(p).download()` and `bucket(b).getFiles()` put
192
+ the bucket a different distance back. The search is bounded at eight
193
+ receivers: a receiver chain can come back round to itself through a
194
+ variable, and a pack that meant more than eight hops has written
195
+ something else by mistake.
196
+
197
+ A step to an argument can say where that argument had to have come
198
+ from, which is `isFrom` asked of each candidate in turn. `send(command)`
199
+ takes one argument and a presigner takes two, and the one that matters
200
+ is the command the SDK declares, so the step says so rather than
201
+ reading whatever it lands on. The modules it says go into the pack's
202
+ import gate the way a start link's do.
203
+
204
+ A pick reads the argument itself, or a property of the object the
205
+ argument states. The properties are tried in order, so `property:
206
+ ["Key", "Prefix"]` is "the key, or the prefix a listing asked for
207
+ instead". A construction is unwrapped first, since that is where a
208
+ command puts its inputs.
209
+
210
+ ## A call that states one request object
211
+
212
+ An AWS SDK command puts everything the call is doing inside one object,
213
+ and four of the things a storage effect records come out of it. Each is
214
+ a link of its own.
215
+
216
+ ```ts
217
+ storageCalls({ system: "aws.dynamodb" })
218
+ .about({ to: "argument", at: { from: 0 }, origin: constructedFrom(...MODULES) })
219
+ .methods(COMMANDS)
220
+ .input({ at: 0 })
221
+ .container({ at: 0, property: ["TableName"] })
222
+ .accessPath({ at: 0, property: ["IndexName"] })
223
+ .containersIn({ at: 0, property: ["RequestItems"] });
224
+ ```
225
+
226
+ `input` says where the call states its inputs. A call that states none
227
+ is not one of these calls, so the chain stops there, and a rule the pack
228
+ wrote over the inputs is handed the object rather than a position to go
229
+ looking in.
230
+
231
+ `accessPath` is the way in the call took, which pairs against a
232
+ declared index rather than against the container itself.
233
+
234
+ `containersIn` is for a call that reaches several containers at once. A
235
+ batch states them as a map, one entry per container, and the chain then
236
+ yields one effect per entry: the entry's key is what the container is
237
+ called, and the entry's value is what the call did there. A chain
238
+ without it yields the single effect its container link addresses.
239
+
240
+ A rule the pack writes for `selector` or `fields` is handed the input,
241
+ that entry when there is one, and whether the call reads or writes:
242
+
243
+ ```ts
244
+ fields: ({ input, entry, kind }) =>
245
+ (entry ?? input).property("ProjectionExpression")?.text()?.split(",") ?? [],
246
+ ```
247
+
248
+ A method table with a rule inside it is still a table, so the link
249
+ stays counted as data and pack health prices the rule beside it.
250
+
251
+ ## A rule that says which value it reads
252
+
253
+ `input` suits a library that puts everything one call is doing in one
254
+ object. Mongoose spreads it over two arguments instead:
255
+
256
+ ```ts
257
+ User.find({ email }, { name: 1, email: 1 });
258
+ // ^ selector ^ fields
259
+ ```
260
+
261
+ Both are rules, since a pick gives back names and neither of these
262
+ states one. So a rule can say where it reads, the way a pick already
263
+ says which call it reads with `of`:
264
+
265
+ ```ts
266
+ find: {
267
+ kind: "read",
268
+ selector: { of: { at: 0 }, by: filterKeys },
269
+ fields: { of: { at: 1 }, by: projected },
270
+ }
271
+ ```
272
+
273
+ `of` is the same `OneArgument` a pick takes, steps and all, so a rule
274
+ can read a value on another call of the chain rather than another
275
+ argument of this one.
276
+
277
+ A rule pointed at an argument the call left out still runs, and what it
278
+ is handed states nothing. Only the pack knows what leaving it out
279
+ means: a Mongoose read with no projection reads every field there is,
280
+ where a call with no request object is not one of these calls at all.
281
+ That guard stays with `input`, which is where it belongs.
282
+
283
+ A method whose own name settles the answer states it as a plain list:
284
+
285
+ ```ts
286
+ findById: { kind: "read", selector: ["_id"], fields: projection(1) },
287
+ deleteOne: { kind: "write", selector: filter(0), fields: ["*"] },
288
+ ```
289
+
290
+ ## An operation the call says rather than the name it goes to
291
+
292
+ A project that wraps a whole API behind one helper puts the operation in
293
+ an argument. `operation` says which one, and a `kind` written with no
294
+ `otherwise` says that an answer the table does not list is not one of
295
+ these calls at all:
296
+
297
+ ```ts
298
+ {
299
+ operation: { at: 2 },
300
+ kind: { asks: { at: 2 }, means: { Query: "read", PutItem: "write" } },
301
+ }
302
+ ```
303
+
304
+ A pack matching a helper the project rather than a library wrote leaves
305
+ `client` out of `storageCalls`, since the name that project gave is the
306
+ whole of what it has, and passes `requiresImport` to `pack()` for the
307
+ modules that make a file worth reading.
308
+
309
+ ## A method the caller says which way round it goes
310
+
311
+ Most methods read or write whatever the call site looks like, and a few
312
+ are told. A signed URL is the one in the shipped packs: the same call
313
+ hands back a URL for reading or for writing depending on what it was
314
+ asked to sign for.
315
+
316
+ ```ts
317
+ kind: {
318
+ asks: { at: 0, property: ["action"] },
319
+ means: { read: "read", write: "write" },
320
+ otherwise: "read",
321
+ }
322
+ ```
323
+
324
+ `otherwise` is what the call comes to when it says nothing, which is
325
+ the library's own default rather than a guess.
326
+
327
+ ## A statement written as SQL
328
+
329
+ A raw query says what it reached in its text rather than in its
330
+ arguments. So the chain hands the text to `@suss/sql`, and the parse
331
+ settles the container, the kind, the fields and the selector of every
332
+ table the statement touches:
333
+
334
+ ```ts
335
+ sqlStatements({
336
+ system: "postgresql",
337
+ dialect: "postgresql",
338
+ client: declaredBy("@prisma/client", ".prisma/client"),
339
+ })
340
+ .methods({ $queryRaw: { statement: { at: 0 } } })
341
+ .example('prisma.$queryRawUnsafe("SELECT id, email FROM users")');
342
+ ```
343
+
344
+ One call comes out as one effect per table, each with its own kind. A
345
+ statement that writes one table while reading another says both, which
346
+ is the thing the storage ending cannot do: that one settles the kind
347
+ once for the whole call.
348
+
349
+ `dialect` is which SQL the statements are written in, and a pack always
350
+ states it. Wherever the store is the database it is the store's own name
351
+ again, which is what Prisma writes above. A Cloudflare D1 database is a
352
+ store of its own whose statements are SQLite, so a pack for it says
353
+ `system: "d1", dialect: "sqlite"`. Nothing defaults, because a reader
354
+ that guessed Postgres for a MySQL project would report the wrong tables
355
+ rather than none: backtick-quoted identifiers parse as something else
356
+ entirely.
357
+
358
+ A tagged template is a call the source wrote without parentheses: the
359
+ tag is the callee and the template is the one argument. All three of
360
+ these state their statement at position 0:
361
+
362
+ ```ts
363
+ prisma.$queryRaw`SELECT id FROM users WHERE id = ${id}`;
364
+ prisma.$queryRawUnsafe("SELECT id FROM users");
365
+ db.execute(sql`SELECT id FROM users`);
366
+ ```
367
+
368
+ The third is a tagged template handed to an ordinary call, and a pick at
369
+ position 0 reads it because the text of a value comes back through the
370
+ tag. So a pack whose client takes its statement either way writes one
371
+ method table rather than two declarations.
372
+
373
+ The invocation walk never reaches a tagged template, so `pack` puts a
374
+ chain with this ending on the access walk instead. That walk visits
375
+ calls as well, which is what catches the unsafe form.
376
+
377
+ ### A hole that is a table rather than a value
378
+
379
+ `parts()` gives the text either side of each hole, and by default what
380
+ the query interpolated reaches the statement as a parameter, which is
381
+ what it would have been anyway. Drizzle breaks that: a query says which
382
+ table it reached by handing over the schema object.
383
+
384
+ ```ts
385
+ db.execute(sql`UPDATE ${users} SET name = ${name} WHERE id = ${id}`);
386
+ ```
387
+
388
+ Written as `UPDATE $1 SET name = $2 WHERE id = $3` that does not parse,
389
+ and the query goes unread. Only the pack knows the first hole is a table
390
+ and the other two are values, so the pack says so:
391
+
392
+ ```ts
393
+ sqlStatements({ system: "postgresql", dialect: "postgresql", client })
394
+ .methods({ execute: { statement: { at: 0 } } })
395
+ .interpolating({ from: constructedFrom("drizzle-orm"), named: { at: 0 } });
396
+ ```
397
+
398
+ `holes()` gives each hole as the call the source wrote it as, so
399
+ `${users}` comes back as the `pgTable("users", {...})` its schema file
400
+ declares and `named` reads the name off it the way any other pick reads
401
+ an argument. `from` keeps the pack from reading a hole the library did
402
+ not make; a hole that is not a call, or that came from somewhere else,
403
+ stays the parameter it was.
404
+
113
405
  ## The example every declaration states
114
406
 
115
407
  ```ts
package/dist/ast.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CallOps, L as LinkFunction } from './chain-DTSij2Mn.cjs';
1
+ import { C as CallOps, L as LinkFunction } from './chain-BaxjT_-k.cjs';
2
2
 
3
3
  /**
4
4
  * The floor: a link that reads the adapter's own syntax tree.
package/dist/ast.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CallOps, L as LinkFunction } from './chain-DTSij2Mn.js';
1
+ import { C as CallOps, L as LinkFunction } from './chain-BaxjT_-k.js';
2
2
 
3
3
  /**
4
4
  * The floor: a link that reads the adapter's own syntax tree.