@suss/ir-core 0.33.1 → 0.34.0

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
@@ -2,7 +2,7 @@
2
2
 
3
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
4
 
5
- Primitives shared across suss's intermediate representations: the pieces every IR references, in one place that each of them can reach without depending on the others.
5
+ Primitives shared across suss's intermediate representations. Every IR refers to these pieces, and keeping them in one package lets each IR reach them without depending on the others.
6
6
 
7
7
  ## What this package is
8
8
 
@@ -23,37 +23,37 @@ const binding = restBinding({
23
23
  });
24
24
  ```
25
25
 
26
- ## Message-bus channels
26
+ The schemas in `@suss/ir-core/schemas` are the single source of truth, and the types are derived from them. The recursive `TypeShape` is a hand-written named export, so consuming packages refer to it by name across the package boundary and do not inline the recursion.
27
27
 
28
- A channel is a subject, optionally qualified by the bus it travels on, written `${bus}#${subject}`. Two channels pair when their subjects are equal and their buses agree, and buses agree when they are the same or when either side does not know its bus.
28
+ ## Message-bus channels
29
29
 
30
- The two sides rarely know the same amount. A CloudFormation template gives both the bus and the detail-type, and it goes out of its way to distinguish one bus from another, so when a side does know its bus we keep that precision. Code usually knows only the subject: the code pack reads `subject: 'order.placed'` from a handler's config, but which bus actually reaches that handler is deployment configuration the code never mentions. So `default#order.placed` pairs with `order.placed`, while `staging#order.placed` does not pair with `default#order.placed`.
30
+ A channel is a subject, optionally qualified by the bus it travels on, written `${bus}#${subject}`. Two channels pair when their subjects are equal and their buses agree. Buses agree when they are the same, or when either side does not know its bus.
31
31
 
32
- The boundary key uses only the subject, so both forms end up in the same bucket and `channelsPair` compares the buses within it.
32
+ The two sides rarely know the same amount. A CloudFormation template gives both the bus and the detail-type, and it distinguishes one bus from another on purpose, so when a side does know its bus, suss keeps that precision. Code usually knows only the subject. The code pack reads `subject: 'order.placed'` from a handler's config, but which bus reaches that handler is set by deployment configuration that the code never mentions. So `default#order.placed` pairs with `order.placed`, while `staging#order.placed` does not pair with `default#order.placed`.
33
33
 
34
- Schemas are the single source of truth (`@suss/ir-core/schemas`); the types are derived from them. The recursive `TypeShape` is a hand-written named export so consuming packages reference it by name across the package boundary rather than inlining the recursion.
34
+ The boundary key uses only the subject, so both forms end up in the same bucket, and `channelsPair` compares the buses within it.
35
35
 
36
36
  ## Boundary names
37
37
 
38
- A container or an access path in a summary is a string that means one of three things, and the braces alone say which:
38
+ A container or an access path in a summary is a string that means one of three things, and the braces alone decide which:
39
39
 
40
40
  - A **literal** (`orders-v1`) is the name itself.
41
- - A **pattern** (`{stage}-orders-v1`) is fixed text with `{}` holes for the parts a deployment fills in. A template writes `!Sub "${StageName}-orders-v1"` and the code writes `` `${stage}-orders-v1` ``: neither states a string, both agree about the fixed text, and each spells the parameter its own way.
42
- - A **reference** (`{location.bucket}`, `{ORDER_TABLE}`) is one hole and no fixed text, saying where to go and ask rather than what the name is.
41
+ - A **pattern** (`{stage}-orders-v1`) is fixed text with `{}` holes for the parts a deployment fills in. A template writes `!Sub "${StageName}-orders-v1"` and the code writes `` `${stage}-orders-v1` ``. Neither states the full string, both agree about the fixed text, and each writes the parameter its own way.
42
+ - A **reference** (`{location.bucket}`, `{ORDER_TABLE}`) is one hole with no fixed text. It records where to look up the name, because the name itself is not known.
43
43
 
44
- `boundaryName.ts` owns the syntax in both directions. `parseBoundaryName` turns a string into the discriminated `BoundaryName` value, `boundaryNameString` turns one back into exactly the string it came from, and everything else, `namesAgree`, `namesNothing`, `fixedTextLength`, `referenceName`, `referenceFromName`, is a view over that pair. Nothing outside the module reads or writes the braces itself: a second parser can disagree about which of the three a string is, and a second printer can spell a value the parser cannot read back. Both have happened. The two halves of caller grounding once spelled a reference differently and never met (#456), which is why writing one and reading one are the same module rather than a format each side implements for itself. `check:name-syntax` in CI keeps it that way.
44
+ `boundaryName.ts` handles the syntax in both directions. `parseBoundaryName` turns a string into the discriminated `BoundaryName` value, and `boundaryNameString` turns one back into exactly the string it came from. Everything else, `namesAgree`, `namesNothing`, `fixedTextLength`, `referenceName`, `referenceFromName`, is built on that pair. No code outside the module reads or writes the braces itself. A second parser can disagree about which of the three a string is, and a second printer can write a value the parser cannot read back. Both have happened. The two halves of caller grounding once wrote a reference differently and never matched (#456). That is why writing a name and reading one live in the same module, and each side does not implement the format for itself. `check:name-syntax` in CI keeps it that way.
45
45
 
46
- The wire format is the string. Summaries on disk contain `orders-v1`, `{stage}-orders-v1`, `{location.table}` unchanged, so nothing about this layout is a schema change, and an old summary reads the same as a new one.
46
+ The wire format is the string. Summaries on disk contain `orders-v1`, `{stage}-orders-v1` and `{location.table}` unchanged, so this layout is not a schema change, and an old summary reads the same as a new one.
47
47
 
48
48
  ### How two names pair
49
49
 
50
- `namesAgree` says whether two names are the same name. Two patterns agree when their fixed parts line up, since a hole on one side meets a hole on the other. A pattern and a literal agree when the fixed text is where the pattern says it is, which is what happens when one side hardcoded what the other parameterized. A reference agrees with nothing until something settles it, since one bare hole would otherwise agree with every name there is.
50
+ `namesAgree` decides whether two names are the same name. Two patterns agree when their fixed parts line up, since a hole on one side meets a hole on the other. A pattern and a literal agree when the fixed text is where the pattern puts it. That happens when one side hardcoded what the other parameterized. A reference agrees with nothing until something settles it, because one bare hole would otherwise agree with every name there is.
51
51
 
52
- A hole covers anything, because a name has no separator every project agrees on: a region is written `us-east-1`, and a hole that stopped at the first hyphen would miss it. The cost is that one name can be covered by two patterns, `orders-{suffix}` and `orders-blue-{suffix}` both cover `orders-blue-v1`, so choosing between them belongs to whichever pass picks a provider for an access. `fixedTextLength` is what such a pass ranks by: the pattern that states more fixed text is the more specific one, and two patterns that state the same amount settle nothing between them.
52
+ A hole covers anything, because projects do not share a separator for names. A region is written `us-east-1`, and a hole that stopped at the first hyphen would miss it. The cost is that one name can be covered by two patterns: `orders-{suffix}` and `orders-blue-{suffix}` both cover `orders-blue-v1`. Choosing between them is the job of whichever pass picks a provider for an access. Such a pass ranks by `fixedTextLength`. The pattern that states more fixed text is the more specific one, and two patterns that state the same amount leave the choice open.
53
53
 
54
- ### A name that says where to go and ask
54
+ ### References
55
55
 
56
- Some code cannot say what it reaches. A storage layer takes the bucket as an argument, and a service reads its table name out of the deployment. Neither states a name, and a name is what the rest of suss pairs on, so both write a reference instead.
56
+ Some code cannot state what it reaches. A storage layer takes the bucket as an argument, and a service reads its table name out of the deployment. Neither states a name, and the rest of suss pairs on names, so both write a reference instead.
57
57
 
58
58
  ```ts
59
59
  referenceName({ root: "location", fields: ["bucket"] }); // "{location.bucket}"
@@ -61,19 +61,19 @@ referenceName({ root: "ORDER_TABLE", fields: [] }); // "{ORDER_TABLE}"
61
61
  referenceFromName("{location.bucket}"); // { root: "location", fields: ["bucket"] }
62
62
  ```
63
63
 
64
- The root is the value the code starts from, and the fields are what it reads inside that value. A language adapter writes one while reading source, and the checker settles it much later by joining over a whole run: `{location.bucket}` against what each caller passed for `location`, `{ORDER_TABLE}` against what the deployment sets. Whether the root is a parameter of the unit or a variable the deployment sets is not in the string, on purpose: it depends on the unit's inputs and their roles, which the grounding pass already has in hand, and a bare `{X}` is deliberately tried both ways there. A root kind written into the name would either be ignored or change which of those runs.
64
+ The root is the value the code starts from, and the fields are what it reads inside that value. A language adapter writes a reference while reading source. The checker settles it much later by joining over a whole run: it matches `{location.bucket}` against what each caller passed for `location`, and `{ORDER_TABLE}` against what the deployment sets. The string does not record whether the root is a parameter of the unit or a variable the deployment sets, and that is on purpose. The answer depends on the unit's inputs and their roles, which the grounding pass already has, and that pass tries a bare `{X}` both ways on purpose. A root kind written into the name would either be ignored or change which of those attempts runs.
65
65
 
66
- ### Two look-alike conventions that stay apart
66
+ ### Route paths and message-bus channels
67
67
 
68
- A REST route path also spells its parameters with braces (`/users/{id}`), but a route hole stops at the `/` between segments, so route paths keep their own comparison beside the REST semantics. A message-bus channel never uses braces at all; its `bus#subject` form has its own module, described above.
68
+ A REST route path also writes its parameters with braces (`/users/{id}`). A route hole stops at the `/` between segments, so route paths keep their own comparison next to the REST semantics. A message-bus channel never uses braces. Its `bus#subject` form has its own module, described above.
69
69
 
70
- ## Words OpenTelemetry already has
70
+ ## OpenTelemetry vocabulary
71
71
 
72
- A summary says what a unit can reach. A trace says what it did reach. Comparing the two is the point, and it only works if both sides spell a boundary the same way, so wherever OpenTelemetry's semantic conventions have a word for something in a binding, that is the word suss writes.
72
+ A summary records what a unit can reach, and a trace records what it did reach. Comparing the two is the goal, and that only works if both sides write a boundary the same way. So wherever OpenTelemetry's semantic conventions have a word for something in a binding, suss writes that word.
73
73
 
74
- The values are theirs. A Postgres table is `postgresql`, not `postgres`; a DynamoDB table is `aws.dynamodb`; an SQS queue is `aws_sqs` and an SNS topic is `aws.sns`, spelled the two different ways the conventions spell them.
74
+ The values come from the conventions. A Postgres table is `postgresql`, and `postgres` is wrong. A DynamoDB table is `aws.dynamodb`. An SQS queue is `aws_sqs` and an SNS topic is `aws.sns`, spelled the two different ways the conventions spell them.
75
75
 
76
- The field names are ours. Each protocol module says which attribute each of its fields goes under, and `semconvAttributes(binding)` reads a binding as the attributes a span would state:
76
+ The field names are suss's own. Each protocol module declares which attribute each of its fields maps to, and `semconvAttributes(binding)` returns a binding as the attributes a span would have:
77
77
 
78
78
  ```ts
79
79
  semconvAttributes(
@@ -88,23 +88,23 @@ semconvAttributes(
88
88
  // "db.collection.name": "users" }
89
89
  ```
90
90
 
91
- A field appears in that projection only when the value suss writes is the value a span gets, so the comparison is byte for byte with nothing in between. Three kinds of field stay out of it:
91
+ A field appears in that projection only when the value suss writes is the value a span gets, so the comparison is byte for byte with no translation. Three kinds of field stay out of it:
92
92
 
93
- - **A field the conventions have no attribute for.** A secondary index (`accessPath`) is one, and a GraphQL resolver's type and field are another, since the conventions describe the operation a client sent and not the resolver the server ran for one field of it.
94
- - **A value suss writes where the source stated none.** `scope: "default"` means no source said which database, and a REST method of `"*"` means the route responds to every method. A span says neither.
95
- - **The same thing under a different string.** `service.name` and `cloud.resource_id` both point at the deployable a `runtime-config` boundary belongs to, but `instanceName` is the deployment template's logical id, which is neither of those strings.
93
+ - **A field the conventions have no attribute for.** A secondary index (`accessPath`) is one. A GraphQL resolver's type and field are another, because the conventions describe the operation a client sent and say nothing about the resolver the server ran for one field of it.
94
+ - **A value suss writes where the source stated none.** `scope: "default"` means no source said which database, and a REST method of `"*"` means the route responds to every method. A span has neither.
95
+ - **The same thing under a different string.** `service.name` and `cloud.resource_id` both identify the deployable that a `runtime-config` boundary belongs to. `instanceName` is the deployment template's logical id, which matches neither of those strings.
96
96
 
97
- Where the conventions have no word at all, suss keeps its own: a store they never covered (`s3`, `gcs`, `r2`, `d1`, `cloudflare-kv`), a bus they never covered (`eventbridge`, `bullmq`, `nats`, the Cloudflare triggers), a metric's system and type, and every boundary nobody crosses at run time, which is a function call across a package boundary, a contract a template declares, and an intent. Those are the boundaries suss exists for, and no observability convention has a word for them.
97
+ Where the conventions have no word at all, suss keeps its own. That covers a store they never covered (`s3`, `gcs`, `r2`, `d1`, `cloudflare-kv`), a bus they never covered (`eventbridge`, `bullmq`, `nats`, the Cloudflare triggers), and a metric's system and type. It also covers every boundary nobody crosses at run time: a function call across a package boundary, a contract a template declares, and an intent. suss exists for those boundaries, and no observability convention has a word for them.
98
98
 
99
- Adding a protocol means filling in `semconv` on its definition, empty included, so the question gets answered rather than skipped.
99
+ Adding a protocol means filling in `semconv` on its definition, even when it is empty, so the question is always answered and never skipped.
100
100
 
101
101
  ## Where it fits in suss
102
102
 
103
- Both `@suss/behavioral-ir` (what code does) and `@suss/intent-ir` (what the team meant) build on this package, so neither IR depends on the other. They describe boundaries in the same vocabulary, and suss compares them rather than merging them. `@suss/behavioral-ir` re-exports these primitives, so existing consumers keep importing them from there unchanged.
103
+ `@suss/behavioral-ir` (what code does) and `@suss/intent-ir` (what the team meant) both build on this package, so neither IR depends on the other. They describe boundaries in the same vocabulary, and suss compares them without merging them. `@suss/behavioral-ir` re-exports these primitives, so existing consumers keep importing them from there unchanged.
104
104
 
105
105
  ## Status
106
106
 
107
- v0: type shapes, boundary bindings + constructors, source locations, confidence. Stable surface; new boundary semantics are added as variants.
107
+ v0: type shapes, boundary bindings + constructors, source locations, confidence. The surface is stable, and new boundary semantics are added as variants.
108
108
 
109
109
  ## More
110
110