@suss/recognize 0.23.0 → 0.24.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.
Files changed (2) hide show
  1. package/README.md +10 -398
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @suss/recognize
2
2
 
3
+ Part of [suss](https://github.com/nimbuscloud-ai/suss), which reads both sides of every call in a repository and says where the two disagree.
4
+
3
5
  Write a pack as data. A pack states where a match starts, which methods
4
6
  count, and what each one reads or writes; this package compiles that
5
7
  into the recognizer hooks the adapters already call, and any adapter
@@ -31,404 +33,14 @@ Nothing here imports a syntax tree, so the same declaration drives the
31
33
  TypeScript adapter today and the Python and Ruby adapters once they
32
34
  implement the ops.
33
35
 
34
- ## The spine
35
-
36
- A pack does four jobs: it discovers units, it recognizes calls inside
37
- them, it declares terminals, and it claims sub-units. They share one
38
- spine and differ in what the ending yields.
39
-
40
- ```
41
- where the match starts -> which method -> read the arguments -> yield
42
- effects
43
- a unit
44
- a terminal
45
- a sub-unit
46
- ```
47
-
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.
52
-
53
- ### Where a match starts
54
-
55
- The start is its own axis, and a receiver is one value on it. Across the
56
- 29 shipped packs a match starts from at least thirteen places, and most
57
- of them are not receivers at all: an exported name, a file path, a
58
- decorator, a parameter's type, a template file beside the module, a
59
- function's return type. `MatchStart` is where those arrive.
60
-
61
- The receiver-shaped starts are their own union, `ReceiverOrigin`:
62
-
63
- | origin | what it matches | built |
64
- |---|---|---|
65
- | `declaredBy` | a receiver whose method one of these modules declared | yes |
66
- | `constructed` | a client made from one of these modules' exports | yes |
67
- | `factoryMade` | `app = express()` | no |
68
- | `imported` | a function the file imports | no |
69
- | `anchored` | a chain read from its anchor call | no |
70
- | `inherits` | a Ruby or Python receiver matched by ancestry | no |
71
- | `global` | `process.env`, bare `fetch` | no |
72
-
73
- `declaredBy` is what a pack wants when the source never spells the
74
- client out: `const redis = await this.getClient()` says nothing about
75
- ioredis, and the declaration behind `redis.get` says everything.
76
- `constructed` asks the receiver itself, so it still works where the
77
- method is untyped.
78
-
79
- A chain can also match a bare call of the tracked value itself, with
80
- `.calls(meaning)` beside its methods table. A store hook is the case:
81
- `useAppStore((s) => s.bears)` reaches for no method, so there is no
82
- name for the table to list; the call matches when its callee is a
83
- bound name whose written value came from the chain's origin. The
84
- fields such a call reads are usually stated in a selector lambda, and
85
- `fields: { selectorParam: 0 }` picks them off the lambda's parameter,
86
- one per distinct first segment.
87
-
88
- ## The gradient
89
-
90
- Expressiveness is bought link by link, and the price is printed.
91
-
92
- 1. A chain of data links. Serializable, inspectable, and it runs
93
- wherever the ops do.
94
- 2. A link written as a function. That link is code, the rest stay data,
95
- and pack health says which ones are opaque.
96
- 3. `astLink` from `@suss/recognize/ast`, which hands the function the
97
- adapter's own node. Behind its own import, so reaching for it shows
98
- up in the diff, and pack health reports it the way it reports a pack
99
- that declares no version.
100
-
101
- `packGradients` in `@suss/adapter-typescript` reads the counts off a
102
- run, and three health checks fire on them: a link written as a function,
103
- a link that reads the syntax tree, and a declaration with no example.
104
-
105
- ## The executor ops
106
-
107
- `CallOps` is the whole of what a chain asks about one call site. An
108
- adapter implements it once. Today `callOpsFor` in
109
- `@suss/adapter-typescript` is the only implementation, and it puts the
110
- result on the recognizer context under `ops`.
111
-
112
- | op | what it gives back |
113
- |---|---|
114
- | `method()` | which method the call reaches for, as the source spells it |
115
- | `receiverIsFrom(origin)` | whether the receiver came from where the origin says |
116
- | `isFrom(origin)` | whether the call itself came from where the origin says |
117
- | `argumentCount()` | how many arguments the call passes |
118
- | `nameAt(index, unsettled)` | the name that argument gives, with the hole policy applied |
119
- | `calleeText()` | the callee, as the source writes it |
120
- | `receiver()` | the call the receiver is, as ops of its own |
121
- | `argument(index)` | the call that argument is, as ops of its own |
122
- | `callee()` | the call the callee itself was written as |
123
- | `namedCallee()` | whether the call goes to a name the program bound (optional) |
124
- | `parameterReadsAt(index)` | the fields a selector lambda in that position reads off its parameter (optional) |
125
- | `propertyAt(index, property, unsettled)` | what a named property of that argument says |
126
- | `valueAt(index)` | the value that argument states, as `ValueOps` |
127
-
128
- `receiver()`, `argument(index)` and `callee()` read a call next to the
129
- one in hand: each gives back another `CallOps`, so every question above
130
- works one step along. `callee()` is the one for a class a factory made, where the
131
- source writes `new User({ name })` and what `User` is comes from the
132
- `model("User", schema)` call it was declared as.
133
-
134
- The last two reach a value rather than a call. `propertyAt` pulls one
135
- name out of a property bag, which is what a pack that wants a bucket or
136
- a table wants. `valueAt` hands the value over whole, for a pack whose
137
- rule has to walk it:
138
-
139
- | op | what it gives back |
140
- |---|---|
141
- | `text()` | the string the source wrote, or null for anything else |
142
- | `flag()` | the yes or no the source wrote, taking a number the way it takes a boolean |
143
- | `entries(unsettled)` | what an object states, as key and value per entry |
144
- | `items()` | what a list states, item by item |
145
- | `property(name)` | what one named property states, as a value of its own |
146
- | `parts()` | the pieces of text the source wrote, with the holes left out |
147
- | `holes()` | what the source interpolated between those pieces, each as the call it was written as |
148
-
149
- A key the source computes, `{ [this.tableName]: [...] }`, is read the
150
- way any other name is, so an entry's key comes back settled where the
151
- source settles it.
152
-
153
- A chain running on an adapter with no ops matches nothing rather than
154
- throwing, since a pack loaded into the wrong adapter is a configuration
155
- mistake and not a crash.
156
-
157
- ## Reaching a call next to this one
158
-
159
- Two of the three storage shapes put what a pack needs somewhere other
160
- than the call the adapter is standing on.
161
-
162
- ```ts
163
- s3.send(new GetObjectCommand({ Bucket: "photos", Key: "a.jpg" }));
164
- storage.bucket("photos").file("a.jpg").download();
165
- ```
166
-
167
- The first says `send` at every call site in the codebase and puts the
168
- operation, the bucket and the key in the command. The second puts the
169
- bucket two hops back up the receivers and the object one hop back.
170
- Neither needs a question of its own: both are the questions above,
171
- asked of a different call.
172
-
173
- A pack says which call with `about`, and a pick says which call with
174
- `of`. Both take steps, and a step is data:
175
-
176
- | step | what it reaches |
177
- |---|---|
178
- | `{ to: "receiver" }` | the call the receiver is |
179
- | `{ to: "receiver", method: "bucket" }` | the first call to `bucket` up the receivers |
180
- | `{ to: "argument", at: 0 }` | the call the first argument is |
181
- | `{ to: "argument", at: { from: 0 } }` | every argument that is a call, first one that matches wins |
182
- | `{ to: "argument", at: { from: 0 }, origin }` | the same, keeping only the ones the origin pins down |
183
-
184
- ```ts
185
- storageCalls({ system: "s3", client: constructedFrom("@aws-sdk/client-s3") })
186
- .about({ to: "argument", at: { from: 0 } })
187
- .methods({ GetObjectCommand: { kind: "read", selector: KEY } })
188
- .container({ at: 0, property: ["Bucket"] });
189
- ```
190
-
191
- With a subject, every other link is asked of the call the steps reach:
192
- the operation is that call's own name, the origin check is about that
193
- call, and a pick with no `of` reads that call's arguments. The effect
194
- still records the call in hand as its callee, since that is where a
195
- reader would go and look.
196
-
197
- A step follows what the source wrote a name as, so `const side = await
198
- deck.side("a")` reaches `deck.side("a")` rather than stopping at the
199
- await.
200
-
201
- A step that says which method is searched for rather than counted to,
202
- because `bucket(b).file(p).download()` and `bucket(b).getFiles()` put
203
- the bucket a different distance back. The search is bounded at eight
204
- receivers: a receiver chain can come back round to itself through a
205
- variable, and a pack that meant more than eight hops has written
206
- something else by mistake.
207
-
208
- A step to an argument can say where that argument had to have come
209
- from, which is `isFrom` asked of each candidate in turn. `send(command)`
210
- takes one argument and a presigner takes two, and the one that matters
211
- is the command the SDK declares, so the step says so rather than
212
- reading whatever it lands on. The modules it says go into the pack's
213
- import gate the way a start link's do.
214
-
215
- A pick reads the argument itself, or a property of the object the
216
- argument states. The properties are tried in order, so `property:
217
- ["Key", "Prefix"]` is "the key, or the prefix a listing asked for
218
- instead". A construction is unwrapped first, since that is where a
219
- command puts its inputs.
220
-
221
- ## A call that states one request object
222
-
223
- An AWS SDK command puts everything the call is doing inside one object,
224
- and four of the things a storage effect records come out of it. Each is
225
- a link of its own.
226
-
227
- ```ts
228
- storageCalls({ system: "aws.dynamodb" })
229
- .about({ to: "argument", at: { from: 0 }, origin: constructedFrom(...MODULES) })
230
- .methods(COMMANDS)
231
- .input({ at: 0 })
232
- .container({ at: 0, property: ["TableName"] })
233
- .accessPath({ at: 0, property: ["IndexName"] })
234
- .containersIn({ at: 0, property: ["RequestItems"] });
235
- ```
236
-
237
- `input` says where the call states its inputs. A call that states none
238
- is not one of these calls, so the chain stops there, and a rule the pack
239
- wrote over the inputs is handed the object rather than a position to go
240
- looking in.
241
-
242
- `accessPath` is the way in the call took, which pairs against a
243
- declared index rather than against the container itself.
244
-
245
- `containersIn` is for a call that reaches several containers at once. A
246
- batch states them as a map, one entry per container, and the chain then
247
- yields one effect per entry: the entry's key is what the container is
248
- called, and the entry's value is what the call did there. A chain
249
- without it yields the single effect its container link addresses.
250
-
251
- A rule the pack writes for `selector` or `fields` is handed the input,
252
- that entry when there is one, and whether the call reads or writes:
253
-
254
- ```ts
255
- fields: ({ input, entry, kind }) =>
256
- (entry ?? input).property("ProjectionExpression")?.text()?.split(",") ?? [],
257
- ```
258
-
259
- A method table with a rule inside it is still a table, so the link
260
- stays counted as data and pack health prices the rule beside it.
261
-
262
- ## A rule that says which value it reads
263
-
264
- `input` suits a library that puts everything one call is doing in one
265
- object. Mongoose spreads it over two arguments instead:
266
-
267
- ```ts
268
- User.find({ email }, { name: 1, email: 1 });
269
- // ^ selector ^ fields
270
- ```
271
-
272
- Both are rules, since a pick gives back names and neither of these
273
- states one. So a rule can say where it reads, the way a pick already
274
- says which call it reads with `of`:
275
-
276
- ```ts
277
- find: {
278
- kind: "read",
279
- selector: { of: { at: 0 }, by: filterKeys },
280
- fields: { of: { at: 1 }, by: projected },
281
- }
282
- ```
283
-
284
- `of` is the same `OneArgument` a pick takes, steps and all, so a rule
285
- can read a value on another call of the chain rather than another
286
- argument of this one.
287
-
288
- A rule pointed at an argument the call left out still runs, and what it
289
- is handed states nothing. Only the pack knows what leaving it out
290
- means: a Mongoose read with no projection reads every field there is,
291
- where a call with no request object is not one of these calls at all.
292
- That guard stays with `input`, which is where it belongs.
293
-
294
- A method whose own name settles the answer states it as a plain list:
295
-
296
- ```ts
297
- findById: { kind: "read", selector: ["_id"], fields: projection(1) },
298
- deleteOne: { kind: "write", selector: filter(0), fields: ["*"] },
299
- ```
300
-
301
- ## An operation the call says rather than the name it goes to
302
-
303
- A project that wraps a whole API behind one helper puts the operation in
304
- an argument. `operation` says which one, and a `kind` written with no
305
- `otherwise` says that an answer the table does not list is not one of
306
- these calls at all:
307
-
308
- ```ts
309
- {
310
- operation: { at: 2 },
311
- kind: { asks: { at: 2 }, means: { Query: "read", PutItem: "write" } },
312
- }
313
- ```
314
-
315
- A pack matching a helper the project rather than a library wrote leaves
316
- `client` out of `storageCalls`, since the name that project gave is the
317
- whole of what it has, and passes `requiresImport` to `pack()` for the
318
- modules that make a file worth reading.
319
-
320
- ## A method the caller says which way round it goes
321
-
322
- Most methods read or write whatever the call site looks like, and a few
323
- are told. A signed URL is the one in the shipped packs: the same call
324
- hands back a URL for reading or for writing depending on what it was
325
- asked to sign for.
326
-
327
- ```ts
328
- kind: {
329
- asks: { at: 0, property: ["action"] },
330
- means: { read: "read", write: "write" },
331
- otherwise: "read",
332
- }
333
- ```
334
-
335
- `otherwise` is what the call comes to when it says nothing, which is
336
- the library's own default rather than a guess.
337
-
338
- ## A statement written as SQL
339
-
340
- A raw query says what it reached in its text rather than in its
341
- arguments. So the chain hands the text to `@suss/sql`, and the parse
342
- settles the container, the kind, the fields and the selector of every
343
- table the statement touches:
344
-
345
- ```ts
346
- sqlStatements({
347
- system: "postgresql",
348
- dialect: "postgresql",
349
- client: declaredBy("@prisma/client", ".prisma/client"),
350
- })
351
- .methods({ $queryRaw: { statement: { at: 0 } } })
352
- .example('prisma.$queryRawUnsafe("SELECT id, email FROM users")');
353
- ```
354
-
355
- One call comes out as one effect per table, each with its own kind. A
356
- statement that writes one table while reading another says both, which
357
- is the thing the storage ending cannot do: that one settles the kind
358
- once for the whole call.
359
-
360
- `dialect` is which SQL the statements are written in, and a pack always
361
- states it. Wherever the store is the database it is the store's own name
362
- again, which is what Prisma writes above. A Cloudflare D1 database is a
363
- store of its own whose statements are SQLite, so a pack for it says
364
- `system: "d1", dialect: "sqlite"`. Nothing defaults, because a reader
365
- that guessed Postgres for a MySQL project would report the wrong tables
366
- rather than none: backtick-quoted identifiers parse as something else
367
- entirely.
368
-
369
- A tagged template is a call the source wrote without parentheses: the
370
- tag is the callee and the template is the one argument. All three of
371
- these state their statement at position 0:
372
-
373
- ```ts
374
- prisma.$queryRaw`SELECT id FROM users WHERE id = ${id}`;
375
- prisma.$queryRawUnsafe("SELECT id FROM users");
376
- db.execute(sql`SELECT id FROM users`);
377
- ```
378
-
379
- The third is a tagged template handed to an ordinary call, and a pick at
380
- position 0 reads it because the text of a value comes back through the
381
- tag. So a pack whose client takes its statement either way writes one
382
- method table rather than two declarations.
383
-
384
- The invocation walk never reaches a tagged template, so `pack` puts a
385
- chain with this ending on the access walk instead. That walk visits
386
- calls as well, which is what catches the unsafe form.
387
-
388
- ### A hole that is a table rather than a value
389
-
390
- `parts()` gives the text either side of each hole, and by default what
391
- the query interpolated reaches the statement as a parameter, which is
392
- what it would have been anyway. Drizzle breaks that: a query says which
393
- table it reached by handing over the schema object.
394
-
395
- ```ts
396
- db.execute(sql`UPDATE ${users} SET name = ${name} WHERE id = ${id}`);
397
- ```
398
-
399
- Written as `UPDATE $1 SET name = $2 WHERE id = $3` that does not parse,
400
- and the query goes unread. Only the pack knows the first hole is a table
401
- and the other two are values, so the pack says so:
402
-
403
- ```ts
404
- sqlStatements({ system: "postgresql", dialect: "postgresql", client })
405
- .methods({ execute: { statement: { at: 0 } } })
406
- .interpolating({ from: constructedFrom("drizzle-orm"), named: { at: 0 } });
407
- ```
408
-
409
- `holes()` gives each hole as the call the source wrote it as, so
410
- `${users}` comes back as the `pgTable("users", {...})` its schema file
411
- declares and `named` reads the name off it the way any other pick reads
412
- an argument. `from` keeps the pack from reading a hole the library did
413
- not make; a hole that is not a call, or that came from somewhere else,
414
- stays the parameter it was.
415
-
416
- ## The example every declaration states
417
-
418
- ```ts
419
- .example('redis.get("user_online:42")')
420
- ```
36
+ ## More
421
37
 
422
- `runExamples(pack, run)` takes each declaration's example, hands it to
423
- the compiler the caller supplied, and gives back what came out. A pack's
424
- test asserts the stated effect, so the day the example stops matching
425
- the pack fails rather than the documentation quietly lying.
426
- `examplesMissing(pack)` lists the declarations that state none.
38
+ - [Write a pack](https://nimbuscloud-ai.github.io/suss/guides/writing-a-pack)
39
+ - [How packs work](https://nimbuscloud-ai.github.io/suss/packs)
40
+ - [What a declaration compiles to](./DESIGN.md)
41
+ - [Documentation](https://nimbuscloud-ai.github.io/suss/)
42
+ - [Source and issues](https://github.com/nimbuscloud-ai/suss)
427
43
 
428
- ## Ordering
44
+ ![coverage](../../.github/badges/coverage-recognize.svg)
429
45
 
430
- The links are guards, so the compiled hook checks the cheap one first:
431
- looking a method up in a table costs nothing, while following a receiver
432
- to the library that made it walks declarations. A link that changes what
433
- the receiver is, the way Prisma's model property will, needs the
434
- declared order back, and `compile.ts` is where that goes.
46
+ Apache 2.0. See [LICENSE](../../LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/recognize",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "type": "module",
5
5
  "description": "Teach suss to read a library you use, by writing a pack as data rather than code, which any language adapter can run.",
6
6
  "keywords": [
@@ -51,9 +51,9 @@
51
51
  "typecheck": "tsc --noEmit"
52
52
  },
53
53
  "dependencies": {
54
- "@suss/behavioral-ir": "0.23.0",
55
- "@suss/extractor": "0.23.0",
56
- "@suss/sql": "0.23.0"
54
+ "@suss/behavioral-ir": "0.24.1",
55
+ "@suss/extractor": "0.24.1",
56
+ "@suss/sql": "0.24.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsup": "*",