@suss/checker-intent 0.33.0 → 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,17 +2,18 @@
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
- Pairs team-authored intent against derived code and reports where the
6
- code fails to satisfy what the team declared.
5
+ Pairs the intent a team wrote with the summaries derived from code, and
6
+ reports where the code does not do what the team declared.
7
7
 
8
8
  ## What this package is
9
9
 
10
- This is the intent half of suss's checking, and it is deliberately
11
- separate from `@suss/checker` (the behavioral peer checker). The inputs
12
- differ: an `IntentSummary` (from `@suss/contract-intent`) on one side, a
13
- `BehavioralSummary` on the other. So does the output. An `IntentFinding`
14
- is one-sided coverage ("the team declared X; does the code do it?"),
15
- rather than a symmetric provider↔consumer mismatch.
10
+ This package does the intent half of suss's checking. It is kept
11
+ separate from `@suss/checker`, the behavioral peer checker, on purpose,
12
+ because the inputs differ. One side is an `IntentSummary` (from
13
+ `@suss/contract-intent`) and the other is a `BehavioralSummary`. The
14
+ output differs too. An `IntentFinding` is one-sided coverage: "the team
15
+ declared X; does the code do it?" A behavioral finding is a symmetric
16
+ provider↔consumer mismatch.
16
17
 
17
18
  ```ts
18
19
  import { checkIntentAgreement } from "@suss/checker-intent";
@@ -20,7 +21,7 @@ import { checkIntentAgreement } from "@suss/checker-intent";
20
21
  const findings = checkIntentAgreement(intentSummaries, codeSummaries);
21
22
  ```
22
23
 
23
- For each boundary intent, it pairs against the code summaries that share
24
+ For each boundary intent, it pairs with the code summaries that share
24
25
  the same boundary key, and emits:
25
26
 
26
27
  - `unimplementedBoundary`: the intent declares a boundary no code produces.
@@ -29,14 +30,23 @@ the same boundary key, and emits:
29
30
  - `undeclaredOutcome`: code produces a REST status, or reaches a boundary, that the intent doesn't declare (info; intent under-specifies).
30
31
  - `unkeyableBoundary`: the intent's boundary has no key to pair on, so nothing was compared (warning).
31
32
  - `undescribedOutcome`: a declared outcome no PRD scenario links to (info).
32
- - `renamedBoundary`: a declared store the unit never touches, paired with an undeclared store of the same system it touches with the same verbs on the same outcomes instead.
33
-
34
- v0 checks system intent (`kind: boundary`). PRD outcome intent
35
- (`kind: prd`), which covers scenarios and links, is a separate pass.
33
+ - `renamedBoundary`: a declared store the unit never touches, paired with an undeclared store of the same system that the unit does touch, with the same verbs on the same outcomes.
34
+ - `pathWithoutEffect`: a transition producing a declared outcome lacks an effect the document's `always` block says every outcome has.
35
+ - `valueFromElsewhere`: a column a `results` line gives a source for under `from` takes its value from somewhere else. A source the code gives no answer for goes under `unchecked` as `unreadValue` instead.
36
+
37
+ For each PRD, it resolves every scenario's links against the loaded
38
+ boundary intents, and checks the tests a scenario lists under
39
+ `coveredBy` through the `CoveringTestLookup` passed as the third
40
+ argument. That lookup turns a spelled test or subject into summaries,
41
+ and the CLI builds it with the resolver `suss ask` uses. Whether a test
42
+ reaches its subject is asked of `@suss/checker`'s call facts, once for
43
+ every test. It emits `unlinkedScenario`, `danglingScenarioLink`,
44
+ `ambiguousScenarioLink`, `missingCoveringTest`, `testMissesSubject` and
45
+ `coveringTestSkipped`, each a warning.
36
46
 
37
47
  ## How a declared effect is compared
38
48
 
39
- An outcome can declare the effects it has, written the way `suss ask` asks about one:
49
+ An outcome can declare its effects, written the same way `suss ask` asks about one:
40
50
 
41
51
  ```yaml
42
52
  results:
@@ -44,49 +54,49 @@ results:
44
54
  - invokes: unit:lambda ArchiveWorker
45
55
  ```
46
56
 
47
- Both directions are checked, and both resolve the boundary with `namesBoundary` in `@suss/ir-core`, the same matcher that settles what somebody types at `suss ask`. So `writes: Invoices` and `writes: aws.dynamodb:Invoices` both pick out the table, exactly as the two spellings do at the command line, and there is no second parser here to drift from that one.
57
+ The pass checks both directions, and both resolve the boundary with `namesBoundary` in `@suss/ir-core`. That is the same matcher `suss ask` uses for what somebody types. So `writes: Invoices` and `writes: aws.dynamodb:Invoices` both pick out the table, as the two spellings do at the command line, and this package has no second parser that could drift from that one.
48
58
 
49
- For each declared effect, the pass walks what the matching code transitions reached, each a verb from `relationsOf` and the binding it reached, and reports `uncoveredOutcome` when nothing there matches what the intent said. An outcome that declares an ending narrows to the transitions that end that way; an outcome that declares only effects is checked against every transition of the unit.
59
+ For each declared effect, the pass walks what the matching code transitions reached. Each of those is a verb from `relationsOf` plus the binding it reached. When none of them matches the intent, the pass reports `uncoveredOutcome`. An outcome that declares an ending narrows to the transitions that end that way. An outcome that declares only effects is checked against every transition of the unit.
50
60
 
51
- For the other direction, the pass walks every boundary the unit's transitions reach and reports `undeclaredOutcome` for each one no outcome declares. An intent listing three writes on a unit doing four has one nobody wrote down. It is info rather than an error for the same reason an undeclared status is: an intent doc declares the floor rather than a closed list, so code beyond it usually means somebody has not written that part down yet.
61
+ In the other direction, the pass walks every boundary the unit's transitions reach, and reports `undeclaredOutcome` for each one that no outcome declares. If an intent lists three writes on a unit that does four, one write was never written down. The finding is info, for the same reason an undeclared status is: an intent doc declares a minimum and is not a closed list, so code that goes beyond it usually means somebody has not documented that part yet.
52
62
 
53
- An access whose container the code cannot settle (a wrapper handed its table as an argument) spells a name with a hole in it and matches nothing here, because grounding that name is the storage pass's job and this pass loads no contracts. An invoke that reads its callee out of an env var is in the same position: the code spells it `unit:lambda {ARCHIVE_WORKER_FUNCTION}`, collapsing that chain is the unit-invocation pass's job, and a document has to spell the callee the way the code does for the two to line up.
63
+ When the code cannot settle an access's container, as with a wrapper that receives its table as an argument, the access's name has a hole in it and matches nothing here. Grounding that name is the storage pass's job, and this pass does not load contracts. An invoke that reads its callee out of an env var is in the same position. The code writes it as `unit:lambda {ARCHIVE_WORKER_FUNCTION}`, and resolving that chain is the unit-invocation pass's job. For the two to match, a document has to write the callee the same way the code does.
54
64
 
55
65
  ## How a declared condition is compared
56
66
 
57
- A `when` clause that says which boundary the branch read is compared too, which is what makes `when` more than prose.
67
+ When a `when` clause states which boundary the branch read, the pass compares that too. This is what makes `when` more than prose.
58
68
 
59
- `boundaryGuardsOf` in `@suss/behavioral-ir` says, for each branch of the code, which boundary its guards turned on and whether the guard passed because something was there. A declared outcome then narrows to the branches whose guards match every boundary clause the intent stated, and `uncoveredOutcome` reports one that produces the ending on a different condition. So an intent saying "404 when a read of `aws.dynamodb:Invoices` finds nothing" fails when the code's 404 turns on the row being present. When the unit makes no effect against that boundary anywhere, in any transition, the message points at the boundary as the cause instead.
69
+ For each branch of the code, `boundaryGuardsOf` in `@suss/behavioral-ir` returns which boundary its guards depended on, and whether the guard passed because something was there. A declared outcome then narrows to the branches whose guards match every boundary clause the intent stated. `uncoveredOutcome` reports an outcome whose ending the code produces under a different condition. So an intent saying "404 when a read of `aws.dynamodb:Invoices` finds nothing" fails when the code's 404 depends on the row being present. When the unit has no effect against that boundary in any transition, the message points at the boundary as the cause instead.
60
70
 
61
- The boundary resolves through `namesBoundary` again, and `finds` has to agree when the clause states it. A clause about an input, and a clause left as a sentence, are prose to this pass and are not compared: the paths and the words have no counterpart in the summary to check against.
71
+ The boundary resolves through `namesBoundary` again, and when the clause states `finds`, it has to agree. A clause about an input, and a clause left as a sentence, are treated as prose and not compared, because the summary has nothing that corresponds to those paths or words.
62
72
 
63
73
  ## When a store was renamed
64
74
 
65
- A store that gets renamed in code without the intent doc catching up produces a pile of findings from one cause: the old name is declared and never touched, the new one is touched and never declared, and both an `uncoveredOutcome` (for the vanished store) and an `undeclaredOutcome` (for the one that appeared) fire for every verb and every outcome that used it. `renamedBoundary` folds all of those into one finding when the pairing is unambiguous: the two boundaries share a system prefix, their verbs match exactly, the new one satisfies every declared use the old one had, and each side has exactly one candidate on the other. It stays an error, same as the findings it replaces: the document and the code still disagree, and folding is a guess about the cause rather than a change in whether that disagreement matters.
75
+ When a store is renamed in code and the intent doc is not updated, one cause produces a pile of findings. The old name is declared and never touched, and the new one is touched and never declared. An `uncoveredOutcome` for the store that vanished and an `undeclaredOutcome` for the one that appeared fire for every verb and every outcome that used it. `renamedBoundary` folds all of those into one finding when the pairing is unambiguous. That requires four things: the two boundaries share a system prefix, their verbs match exactly, the new one satisfies every declared use the old one had, and each side has exactly one candidate on the other. The folded finding is still an error, like the findings it replaces. The document and the code still disagree, and folding is only a guess about the cause. It does not change whether the disagreement matters.
66
76
 
67
- Pairing only considers reads and writes. A queue channel or a deployed unit is addressed by name, so a different callee produces its own uncoveredOutcome and undeclaredOutcome findings without folding.
77
+ Pairing only considers reads and writes. A queue channel or a deployed unit is addressed by name, so a different callee produces its own uncoveredOutcome and undeclaredOutcome findings, and they are not folded.
68
78
 
69
79
  ## The coverage question, both ways
70
80
 
71
- The three scenario kinds ask whether a scenario points at an outcome that exists. `undescribedOutcome` asks it the other way: which declared behaviour has nobody written down a reason for. That is the question a product reader has, and the two artifacts to answer it from are the same two.
81
+ The three scenario kinds check whether a scenario points at an outcome that exists. `undescribedOutcome` checks the reverse: which declared behaviour has no written reason. A product reader asks that question, and the same two documents answer it.
72
82
 
73
- It stays quiet until at least one PRD is loaded, since before that the answer is every outcome. `suss infer prd` writes a scenario per outcome, so a fresh set of drafts starts with none of these and they appear as the boundary documents grow outcomes past what the PRDs cover.
83
+ The check stays quiet until at least one PRD is loaded, because before that every outcome would be reported. `suss infer prd` writes a scenario per outcome, so a fresh set of drafts starts with no such findings. They appear as the boundary documents gain outcomes that the PRDs do not cover.
74
84
 
75
85
  ## Which summaries count as the code
76
86
 
77
- Two rules narrow the summaries an intent doc is compared against, and both are about not arguing with a document over something that was never its job.
87
+ Two rules narrow the summaries an intent doc is compared against. Both keep suss from reporting a document for something the document was never meant to cover.
78
88
 
79
- A consumer at the same key is a caller. A client calling `GET /users/{id}` shares the key with the route and provides nothing, so comparing outcomes against its returns would report every declared outcome as uncovered.
89
+ A consumer at the same key is a caller. A client calling `GET /users/{id}` shares the key with the route but provides nothing, so comparing outcomes against its returns would report every declared outcome as uncovered.
80
90
 
81
- A summary a manifest produced is a declaration. A CloudFormation queue resource says the channel exists and stops there, so it has no transition that could satisfy an outcome, and the code that does is the handler in the same run. Summaries with `confidence.source: "declared"` are left out for that reason. A boundary with nothing else behind it still reports `unimplementedBoundary`, which is the state it is in: declared and not written yet.
91
+ A summary produced from a manifest is a declaration. A CloudFormation queue resource declares that the channel exists and nothing more, so it has no transition that could satisfy an outcome. The handler in the same run is the code that satisfies it. So summaries with `confidence.source: "declared"` are left out. A boundary with nothing else behind it still reports `unimplementedBoundary`, which matches its state: declared and not written yet.
82
92
 
83
- The handler behind a queue needs the declaration for its identity even so, because a SAM template decides which queue delivers to it and its own summary has no channel. `withDeclaredDelivery` in `@suss/behavioral-ir` puts the two together by the deployable unit they both give, and both this pass and `suss infer intent` call it before they index anything by boundary key. It fills in only what a summary left null, and only from a declaration, so a channel the code stated for itself stays as it is.
93
+ The handler behind a queue still needs the declaration for its identity. A SAM template decides which queue delivers to it, and its own summary has no channel. `withDeclaredDelivery` in `@suss/behavioral-ir` combines the two through the deployable unit they both record. Both this pass and `suss infer intent` call it before they index anything by boundary key. It fills in only what a summary left null, and only from a declaration, so a channel the code stated for itself stays as it is.
84
94
 
85
95
  ## Which boundaries can be paired
86
96
 
87
- `whatWouldKeyIt` says, per protocol, what an intent doc needs before it can be paired, and the message on an `unkeyableBoundary` finding and the reason `suss infer intent` reports for a boundary it skipped both come from it.
97
+ `whatWouldKeyIt` returns, for each protocol, what an intent doc needs before it can be paired. The message on an `unkeyableBoundary` finding comes from it, and so does the reason `suss infer intent` gives for a boundary it skipped.
88
98
 
89
- A store is the case worth knowing: it has no identity key at all, by design in `@suss/ir-core`, so a `kind: boundary` doc for one is authorable and always unkeyable. Naming the store as the target of an effect on the boundary that touches it is what pairs today.
99
+ A store is the case to know about. By design in `@suss/ir-core` it has no identity key at all, so you can write a `kind: boundary` doc for one, but it can never be paired. What pairs today is naming the store as the target of an effect on the boundary that touches it.
90
100
 
91
101
  ## Where it fits in suss
92
102
 
@@ -100,6 +110,7 @@ It does **not** depend on
100
110
 
101
111
  ## More
102
112
 
113
+ - [How intent findings are graded](./DESIGN.md)
103
114
  - [Documentation](https://suss.sh/)
104
115
  - [Every package and pack suss ships](https://suss.sh/packs/catalog)
105
116
  - [Source and issues](https://github.com/nimbuscloud-ai/suss)