@suss/intent-ir 0.20.0 → 0.21.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 +102 -2
- package/dist/index.cjs +210 -32
- package/dist/index.d.cts +172 -8
- package/dist/index.d.ts +172 -8
- package/dist/index.js +216 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,9 +6,109 @@ The team-authored side of the loop: what the code was *meant* to do, in a form t
|
|
|
6
6
|
|
|
7
7
|
Two citizens, discriminated by `kind`, both built on `@suss/ir-core` so intent and behaviour describe boundaries the same way:
|
|
8
8
|
|
|
9
|
-
- **System intent** (`kind: boundary`): what one boundary should do, as named outcomes. The boundary is REST
|
|
9
|
+
- **System intent** (`kind: boundary`): what one boundary should do, as named outcomes. The boundary is REST, function-call, message-bus, storage or unit-invocation, and every field of one comes off the `@suss/ir-core` schema for that protocol.
|
|
10
10
|
- **Outcome intent** (`kind: prd`): human `when` / `expect` scenarios, each with an optional `link` to a system-intent outcome (`<intent-name>.<outcome-id>`). A scenario with no `link` is a valid state to be in: it reads fully, and nothing has linked it to an outcome yet.
|
|
11
11
|
|
|
12
|
+
## What an outcome is
|
|
13
|
+
|
|
14
|
+
A transition says three things: what it turned on, how it ends, and what it did.
|
|
15
|
+
|
|
16
|
+
**What it turned on**: `when`, a list of clauses in the same verbs `results` uses.
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
transitions:
|
|
20
|
+
- id: invoice-not-found
|
|
21
|
+
when:
|
|
22
|
+
- reads: aws.dynamodb:Invoices
|
|
23
|
+
finds: nothing
|
|
24
|
+
response:
|
|
25
|
+
status: 404
|
|
26
|
+
|
|
27
|
+
- id: invoice-settled
|
|
28
|
+
when:
|
|
29
|
+
- reads: aws.dynamodb:Invoices
|
|
30
|
+
finds: something
|
|
31
|
+
where: settledAt is set
|
|
32
|
+
response:
|
|
33
|
+
status: 409
|
|
34
|
+
|
|
35
|
+
- id: invoice-returned
|
|
36
|
+
when:
|
|
37
|
+
- reads: aws.dynamodb:Invoices
|
|
38
|
+
finds: something
|
|
39
|
+
where: settledAt is missing
|
|
40
|
+
response:
|
|
41
|
+
status: 200
|
|
42
|
+
results:
|
|
43
|
+
- reads: aws.dynamodb:Invoices
|
|
44
|
+
by: invoiceId
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A clause says which subject, then at most one thing about it.
|
|
48
|
+
|
|
49
|
+
The subject is a boundary verb (`reads`, `writes`) whose value is the boundary's name, or `input:` with the path the caller sent. Those come from the IR's own subject types: a guard on the result of a call that crossed a boundary becomes the boundary form, one on an input becomes `input:`, and anything else falls back.
|
|
50
|
+
|
|
51
|
+
The check is one of `finds` (`nothing` or `something`, what a lookup came back with), `is` (`set`, `missing`, `null`, `a string`), `equals`, or `has`. `where` narrows the clause with whatever the guard said about a deeper read of the same result.
|
|
52
|
+
|
|
53
|
+
A fall-through branch states its own condition rather than pointing at the ones above it, since the summary records that branch's guards as the negations of theirs. `otherwise` is left for the one case that has none to state, which is a default branch the summary recorded nothing for. A word that means "not the branches above" would change what it claims the moment somebody inserts a transition over it, in a file people hand-edit.
|
|
54
|
+
|
|
55
|
+
A guard that maps to none of this keeps a sentence, and `when` written as one plain string stays valid, which is what existing authored documents use.
|
|
56
|
+
|
|
57
|
+
The point of naming the boundary is that the line survives a rename of the variable the source used, and that the checker can compare it. `when: "!doc.send().Item"` could do neither.
|
|
58
|
+
|
|
59
|
+
**How it ends**: at most one of `response` (a status and a body), `returns` (a function or handler return value), or `throws` (an error).
|
|
60
|
+
|
|
61
|
+
**What it did**: `results`, a list of the effects the outcome has, written in the verbs `suss ask` asks with.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
transitions:
|
|
65
|
+
- id: invoice-recorded
|
|
66
|
+
when: the message gives an invoice id we have not recorded
|
|
67
|
+
returns:
|
|
68
|
+
body:
|
|
69
|
+
properties:
|
|
70
|
+
recorded: { type: boolean }
|
|
71
|
+
results:
|
|
72
|
+
- writes: aws.dynamodb:Invoices
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`suss ask "what writes aws.dynamodb:Invoices"` is the question and this is the assertion, spelled the same way. The key is the verb, `reads`, `writes` or `invokes`, which are the three `relationsOf` in `@suss/behavioral-ir` gives an effect. The value is the boundary's own name, the string every report writes and `namesBoundary` in `@suss/ir-core` resolves, so what you type in a document and what you type at `ask` pick out the same boundary. Nothing here is per protocol, so a queue consumer and a table writer say what they do without a fourth outcome shape each.
|
|
76
|
+
|
|
77
|
+
`invokes` is the verb for calling a deployed unit by name, which is how a service built out of Lambdas hangs together:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
results:
|
|
81
|
+
- invokes: unit:lambda ArchiveWorker
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A run that reads the deployment template gives a unit its name, so the boundary a document declares is `unit:lambda ReportBuilder`. The callee on a `results` line is spelled the way the invoking code spells it, which for a function named through an env var is `unit:lambda {ARCHIVE_WORKER_FUNCTION}`. Collapsing that env-var chain is the checker's job today and nothing hands the answer back to a drafted document, so the two spellings stay apart until it does.
|
|
85
|
+
|
|
86
|
+
A clause can go further, since the summary does. `fields` is the columns the access touches and `by` is what it picks the item out by:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
results:
|
|
90
|
+
- reads: aws.dynamodb:Accounts
|
|
91
|
+
by: accountId
|
|
92
|
+
- writes: aws.dynamodb:Profiles
|
|
93
|
+
fields: [email, phone, address]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Both are optional and a clause with only the verb and the name means what it always did. What they buy is that "the customer's contact details are erased" stops being satisfied by any write at all to that table. The checker asks that the access cover every column the clause stated; an access that states none is unread rather than empty (no pack parses a DynamoDB `UpdateExpression`), and one that asked for every column covers whatever the clause stated.
|
|
97
|
+
|
|
98
|
+
The outcome id stays free-form on purpose. That is where a person writes what the outcome means, and it is what a PRD scenario links to: `invoice-intake.invoice-recorded`, read as "a duplicate delivery changes nothing". A `kind: prd` document is the level where a scenario reads as prose; `kind: boundary` is system intent for engineers, so it stays structural.
|
|
99
|
+
|
|
100
|
+
Adding `results` changes nothing about `response`, `returns` and `throws`. A doc that states only those parses and pairs the way it always did.
|
|
101
|
+
|
|
102
|
+
## Which boundaries pair, and which are pending
|
|
103
|
+
|
|
104
|
+
A boundary intent pairs when its boundary has a key. REST has one from its method and path, a function-call export has one from its package and export path, a message-bus boundary has one from its channel, and an invoked unit has one from its deployment target and the name the platform knows it by. A unit whose name only the runtime settles has none, so a document for one is authorable and reported `unkeyableBoundary`.
|
|
105
|
+
|
|
106
|
+
A store has none. `storage.ts` in `@suss/ir-core` returns null for a storage identity key on purpose: a container name can be a pattern with holes that only a caller or the deployment settles, and the storage pass in `@suss/checker` grounds it before pairing. So a `kind: boundary` doc whose boundary is a store is authorable, and the checker reports it as `unkeyableBoundary` and puts it in `unchecked` rather than pretending to have compared it. That is the same pending state a module-level function-call boundary is in.
|
|
107
|
+
|
|
108
|
+
What does pair today is a store named as the target of an effect: put `- writes: aws.dynamodb:Invoices` on an outcome of the boundary that touches the store, and the checker compares it against the storage accesses on that boundary's transitions.
|
|
109
|
+
|
|
110
|
+
Every boundary kind has a name of its own in `@suss/ir-core`, `storageLabel` being the one this change added, so an effect always takes a label and never a structured block. A binding with no name at all, a REST call whose method and path the code never settles, is left out of a draft rather than written as a string nobody would type back.
|
|
111
|
+
|
|
12
112
|
```ts
|
|
13
113
|
import { IntentDocSchema, intentDocToSummary } from "@suss/intent-ir";
|
|
14
114
|
|
|
@@ -26,7 +126,7 @@ It is a peer of `@suss/behavioral-ir`; both build on `@suss/ir-core`. Readers (e
|
|
|
26
126
|
|
|
27
127
|
## Status
|
|
28
128
|
|
|
29
|
-
v0: REST
|
|
129
|
+
v0: REST, function-call, message-bus, storage and unit-invocation system intent, effects as outcomes, PRD outcome intent with optional links. GraphQL, runtime-config and metric boundaries have no block yet.
|
|
30
130
|
|
|
31
131
|
## Coverage
|
|
32
132
|
|
package/dist/index.cjs
CHANGED
|
@@ -29,7 +29,9 @@ __export(index_exports, {
|
|
|
29
29
|
IntentFindingSuppressionSchema: () => IntentFindingSuppressionSchema,
|
|
30
30
|
IntentRefSchema: () => IntentRefSchema,
|
|
31
31
|
IntentSourceSchema: () => IntentSourceSchema,
|
|
32
|
-
|
|
32
|
+
blanksLeftEmpty: () => blanksLeftEmpty,
|
|
33
|
+
intentDocToSummary: () => intentDocToSummary,
|
|
34
|
+
toBoundaryBinding: () => toBoundaryBinding
|
|
33
35
|
});
|
|
34
36
|
module.exports = __toCommonJS(index_exports);
|
|
35
37
|
|
|
@@ -55,8 +57,12 @@ var IntentFindingKindSchema = import_zod.z.enum([
|
|
|
55
57
|
// scenario carries no structured link (info — a valid pending state)
|
|
56
58
|
"danglingScenarioLink",
|
|
57
59
|
// link names an intent / outcome no boundary intent declares
|
|
58
|
-
"ambiguousScenarioLink"
|
|
60
|
+
"ambiguousScenarioLink",
|
|
59
61
|
// link resolves to two or more boundary intents sharing the name
|
|
62
|
+
// The same coverage question asked the other way: which declared
|
|
63
|
+
// behaviour has nobody written a reason for.
|
|
64
|
+
"undescribedOutcome"
|
|
65
|
+
// a declared outcome no PRD scenario links to (info)
|
|
60
66
|
]);
|
|
61
67
|
var IntentFindingSeveritySchema = import_zod.z.enum(["error", "warning", "info"]);
|
|
62
68
|
var IntentRefSchema = import_zod.z.object({
|
|
@@ -106,7 +112,23 @@ var IntentFindingSchema = import_zod.z.object({
|
|
|
106
112
|
|
|
107
113
|
// src/schema.ts
|
|
108
114
|
var import_zod2 = require("zod");
|
|
115
|
+
var import_ir_core = require("@suss/ir-core");
|
|
109
116
|
var IntentSourceSchema = import_zod2.z.enum(["author", "inferred", "inferred, curated"]).default("author");
|
|
117
|
+
var UNCURATED_SOURCE = "inferred";
|
|
118
|
+
var CURATED_FIELDS = ["title", "purpose", "audience", "when", "expect"];
|
|
119
|
+
function blankIn(field) {
|
|
120
|
+
return field.slice(field.lastIndexOf(".") + 1);
|
|
121
|
+
}
|
|
122
|
+
function blanksLeftEmpty(doc, failedFields) {
|
|
123
|
+
if (doc.source !== UNCURATED_SOURCE) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const blanks = failedFields.map(blankIn);
|
|
127
|
+
if (blanks.length === 0 || !blanks.every((field) => CURATED_FIELDS.includes(field))) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return CURATED_FIELDS.filter((blank) => blanks.includes(blank));
|
|
131
|
+
}
|
|
110
132
|
var PrimitiveTypeName = import_zod2.z.enum([
|
|
111
133
|
"string",
|
|
112
134
|
"integer",
|
|
@@ -151,9 +173,29 @@ var FunctionCallBoundarySchema = import_zod2.z.object({
|
|
|
151
173
|
/** Path to the export within the package (sub-path + nested names). */
|
|
152
174
|
exportPath: import_zod2.z.array(import_zod2.z.string()).optional()
|
|
153
175
|
});
|
|
176
|
+
var MessageBusBoundarySchema = import_zod2.z.object({
|
|
177
|
+
semantics: import_zod2.z.literal("message-bus"),
|
|
178
|
+
messageBus: import_ir_core.MessageBusSemanticsSchema.shape.messageBus,
|
|
179
|
+
channel: import_ir_core.MessageBusSemanticsSchema.shape.channel.default(null)
|
|
180
|
+
});
|
|
181
|
+
var StorageBoundarySchema = import_zod2.z.object({
|
|
182
|
+
semantics: import_zod2.z.literal("storage"),
|
|
183
|
+
storageSystem: import_ir_core.StorageSemanticsSchema.shape.storageSystem,
|
|
184
|
+
scope: import_ir_core.StorageSemanticsSchema.shape.scope.default("default"),
|
|
185
|
+
container: import_ir_core.StorageSemanticsSchema.shape.container.default(null),
|
|
186
|
+
accessPath: import_ir_core.StorageSemanticsSchema.shape.accessPath.default(null)
|
|
187
|
+
});
|
|
188
|
+
var UnitInvocationBoundarySchema = import_zod2.z.object({
|
|
189
|
+
semantics: import_zod2.z.literal("unit-invocation"),
|
|
190
|
+
deploymentTarget: import_ir_core.UnitInvocationSemanticsSchema.shape.deploymentTarget,
|
|
191
|
+
instanceName: import_ir_core.UnitInvocationSemanticsSchema.shape.instanceName.default(null)
|
|
192
|
+
});
|
|
154
193
|
var BoundarySchema = import_zod2.z.discriminatedUnion("semantics", [
|
|
155
194
|
RestBoundarySchema,
|
|
156
|
-
FunctionCallBoundarySchema
|
|
195
|
+
FunctionCallBoundarySchema,
|
|
196
|
+
MessageBusBoundarySchema,
|
|
197
|
+
StorageBoundarySchema,
|
|
198
|
+
UnitInvocationBoundarySchema
|
|
157
199
|
]);
|
|
158
200
|
var ResponseOutcomeSchema = import_zod2.z.object({
|
|
159
201
|
status: import_zod2.z.number().int().min(100).max(599),
|
|
@@ -168,18 +210,70 @@ var ThrowsOutcomeSchema = import_zod2.z.object({
|
|
|
168
210
|
function emptyIfNull(schema) {
|
|
169
211
|
return import_zod2.z.preprocess((v) => v === null ? {} : v, schema);
|
|
170
212
|
}
|
|
213
|
+
var ONE_OR_MORE = import_zod2.z.union([
|
|
214
|
+
import_zod2.z.string().min(1),
|
|
215
|
+
import_zod2.z.array(import_zod2.z.string().min(1)).min(1)
|
|
216
|
+
]);
|
|
217
|
+
var EFFECT_BY_VERB = import_ir_core.EffectRelationSchema.options.map(
|
|
218
|
+
(verb) => import_zod2.z.strictObject({
|
|
219
|
+
[verb]: import_zod2.z.string().min(1),
|
|
220
|
+
fields: import_zod2.z.array(import_zod2.z.string().min(1)).min(1).optional(),
|
|
221
|
+
by: ONE_OR_MORE.optional()
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
var EffectOutcomeSchema = import_zod2.z.union(EFFECT_BY_VERB);
|
|
225
|
+
var CHECKS = {
|
|
226
|
+
/** What a lookup came back with. */
|
|
227
|
+
finds: import_zod2.z.enum(["nothing", "something"]),
|
|
228
|
+
/** What state the value was in: `set`, `missing`, `null`, `a string`. */
|
|
229
|
+
is: import_zod2.z.string().min(1),
|
|
230
|
+
/** The value it was equal to. */
|
|
231
|
+
equals: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number(), import_zod2.z.boolean(), import_zod2.z.null()]),
|
|
232
|
+
/** A property it had. */
|
|
233
|
+
has: import_zod2.z.string().min(1)
|
|
234
|
+
};
|
|
235
|
+
var SUBJECT_KEYS = [
|
|
236
|
+
...import_ir_core.EffectRelationSchema.options,
|
|
237
|
+
"input"
|
|
238
|
+
];
|
|
239
|
+
var WHEN_CLAUSE_BY_SUBJECT = SUBJECT_KEYS.map(
|
|
240
|
+
(key) => import_zod2.z.strictObject({
|
|
241
|
+
[key]: import_zod2.z.string().min(1),
|
|
242
|
+
where: import_zod2.z.string().min(1).optional(),
|
|
243
|
+
...Object.fromEntries(
|
|
244
|
+
Object.entries(CHECKS).map(([check, schema]) => [
|
|
245
|
+
check,
|
|
246
|
+
schema.optional()
|
|
247
|
+
])
|
|
248
|
+
)
|
|
249
|
+
}).refine(
|
|
250
|
+
(clause) => Object.keys(CHECKS).filter(
|
|
251
|
+
(check) => clause[check] !== void 0
|
|
252
|
+
).length <= 1,
|
|
253
|
+
{
|
|
254
|
+
message: `a when clause says at most one of ${Object.keys(CHECKS).join(", ")} about its subject`
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
var WhenSchema = import_zod2.z.union([
|
|
259
|
+
import_zod2.z.string().min(1),
|
|
260
|
+
import_zod2.z.array(import_zod2.z.union([import_zod2.z.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
|
|
261
|
+
]);
|
|
171
262
|
var BoundaryTransitionSchema = import_zod2.z.object({
|
|
172
263
|
id: import_zod2.z.string().min(1),
|
|
173
|
-
when:
|
|
264
|
+
when: WhenSchema,
|
|
174
265
|
response: emptyIfNull(ResponseOutcomeSchema).optional(),
|
|
175
266
|
returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
|
|
176
|
-
throws: emptyIfNull(ThrowsOutcomeSchema).optional()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
267
|
+
throws: emptyIfNull(ThrowsOutcomeSchema).optional(),
|
|
268
|
+
results: import_zod2.z.array(EffectOutcomeSchema).min(1).optional()
|
|
269
|
+
}).refine((t) => endingsOf(t).length <= 1, {
|
|
270
|
+
message: "a transition ends one way: give it at most one of response, returns, or throws"
|
|
271
|
+
}).refine((t) => endingsOf(t).length === 1 || t.results !== void 0, {
|
|
272
|
+
message: "each transition must declare an outcome: response, returns, throws, or the effects it results in"
|
|
273
|
+
});
|
|
274
|
+
function endingsOf(t) {
|
|
275
|
+
return [t.response, t.returns, t.throws].filter((o) => o !== void 0);
|
|
276
|
+
}
|
|
183
277
|
var BoundaryIntentSchema = import_zod2.z.object({
|
|
184
278
|
kind: import_zod2.z.literal("boundary"),
|
|
185
279
|
/** Name PRDs reference outcomes through: `<name>.<transition-id>`. */
|
|
@@ -223,7 +317,7 @@ var IntentDocSchema = import_zod2.z.discriminatedUnion("kind", [
|
|
|
223
317
|
]);
|
|
224
318
|
|
|
225
319
|
// src/summary.ts
|
|
226
|
-
var
|
|
320
|
+
var import_ir_core2 = require("@suss/ir-core");
|
|
227
321
|
function intentDocToSummary(doc) {
|
|
228
322
|
if (doc.kind === "prd") {
|
|
229
323
|
return prdToSummary(doc);
|
|
@@ -256,26 +350,105 @@ function prdToSummary(doc) {
|
|
|
256
350
|
}))
|
|
257
351
|
};
|
|
258
352
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
return (0, import_ir_core.functionCallBinding)({
|
|
353
|
+
var BINDINGS = {
|
|
354
|
+
rest: (boundary) => (0, import_ir_core2.restBinding)({
|
|
355
|
+
transport: boundary.transport,
|
|
356
|
+
method: boundary.method,
|
|
357
|
+
path: boundary.path,
|
|
358
|
+
recognition: "intent"
|
|
359
|
+
}),
|
|
360
|
+
"function-call": (boundary) => (0, import_ir_core2.functionCallBinding)({
|
|
269
361
|
transport: boundary.transport,
|
|
270
362
|
recognition: "intent",
|
|
271
363
|
...boundary.module !== void 0 ? { module: boundary.module } : {},
|
|
272
364
|
...boundary.exportName !== void 0 ? { exportName: boundary.exportName } : {},
|
|
273
365
|
...boundary.package !== void 0 ? { package: boundary.package } : {},
|
|
274
366
|
...boundary.exportPath !== void 0 ? { exportPath: boundary.exportPath } : {}
|
|
275
|
-
})
|
|
367
|
+
}),
|
|
368
|
+
"message-bus": (boundary) => (0, import_ir_core2.messageBusBinding)({
|
|
369
|
+
recognition: "intent",
|
|
370
|
+
messageBus: boundary.messageBus,
|
|
371
|
+
channel: boundary.channel
|
|
372
|
+
}),
|
|
373
|
+
"unit-invocation": (boundary) => (0, import_ir_core2.unitInvocationBinding)({
|
|
374
|
+
recognition: "intent",
|
|
375
|
+
deploymentTarget: boundary.deploymentTarget,
|
|
376
|
+
instanceName: boundary.instanceName
|
|
377
|
+
}),
|
|
378
|
+
storage: (boundary) => (0, import_ir_core2.storageBinding)({
|
|
379
|
+
recognition: "intent",
|
|
380
|
+
storageSystem: boundary.storageSystem,
|
|
381
|
+
scope: boundary.scope,
|
|
382
|
+
container: boundary.container,
|
|
383
|
+
accessPath: boundary.accessPath
|
|
384
|
+
})
|
|
385
|
+
};
|
|
386
|
+
function toBoundaryBinding(boundary) {
|
|
387
|
+
const build = BINDINGS[boundary.semantics];
|
|
388
|
+
return build(boundary);
|
|
389
|
+
}
|
|
390
|
+
var VERBS = import_ir_core2.EffectRelationSchema.options;
|
|
391
|
+
function toEffect(declared) {
|
|
392
|
+
const [does, names] = Object.entries(declared).find(
|
|
393
|
+
([key]) => VERBS.includes(key)
|
|
394
|
+
);
|
|
395
|
+
return {
|
|
396
|
+
does,
|
|
397
|
+
names,
|
|
398
|
+
fields: declared.fields ?? [],
|
|
399
|
+
by: oneOrMore(declared.by)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function oneOrMore(written) {
|
|
403
|
+
if (written === void 0) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
return typeof written === "string" ? [written] : written;
|
|
407
|
+
}
|
|
408
|
+
function subjectOf(clause) {
|
|
409
|
+
for (const does of VERBS) {
|
|
410
|
+
const names = clause[does];
|
|
411
|
+
if (names !== void 0) {
|
|
412
|
+
return { does, names, fields: [], by: [] };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
function toCondition(clause) {
|
|
418
|
+
if (typeof clause === "string") {
|
|
419
|
+
return { at: null, input: null, finds: null, said: clause };
|
|
420
|
+
}
|
|
421
|
+
const at = subjectOf(clause);
|
|
422
|
+
return {
|
|
423
|
+
at,
|
|
424
|
+
input: clause.input ?? null,
|
|
425
|
+
finds: clause.finds ?? null,
|
|
426
|
+
said: saidAsOneLine(clause, at)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function saidAsOneLine(clause, at) {
|
|
430
|
+
const subject = at !== null ? `${at.does} ${at.names}` : `input ${String(clause.input)}`;
|
|
431
|
+
const check = CHECK_KEYS.map(
|
|
432
|
+
(key) => clause[key] === void 0 ? null : `${key} ${String(clause[key])}`
|
|
433
|
+
).find((said) => said !== null);
|
|
434
|
+
const narrows = clause.where === void 0 ? null : `where ${clause.where}`;
|
|
435
|
+
return [subject, check, narrows].filter((part) => part !== null).join(" ");
|
|
436
|
+
}
|
|
437
|
+
var CHECK_KEYS = ["finds", "is", "equals", "has"];
|
|
438
|
+
function toConditions(when) {
|
|
439
|
+
return (typeof when === "string" ? [when] : when).map(toCondition);
|
|
440
|
+
}
|
|
441
|
+
function whenAsOneLine(when) {
|
|
442
|
+
return typeof when === "string" ? when : toConditions(when).map((condition) => condition.said).join(" and ");
|
|
276
443
|
}
|
|
277
444
|
function toOutcome(t) {
|
|
278
|
-
const
|
|
445
|
+
const effects = (t.results ?? []).map(toEffect);
|
|
446
|
+
const base = {
|
|
447
|
+
id: t.id,
|
|
448
|
+
when: whenAsOneLine(t.when),
|
|
449
|
+
conditions: toConditions(t.when),
|
|
450
|
+
effects
|
|
451
|
+
};
|
|
279
452
|
if (t.response !== void 0) {
|
|
280
453
|
return {
|
|
281
454
|
...base,
|
|
@@ -294,13 +467,16 @@ function toOutcome(t) {
|
|
|
294
467
|
errorType: null
|
|
295
468
|
};
|
|
296
469
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
470
|
+
if (t.throws !== void 0) {
|
|
471
|
+
return {
|
|
472
|
+
...base,
|
|
473
|
+
kind: "throw",
|
|
474
|
+
status: null,
|
|
475
|
+
body: null,
|
|
476
|
+
errorType: t.throws.errorType ?? null
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return { ...base, kind: "effect", status: null, body: null, errorType: null };
|
|
304
480
|
}
|
|
305
481
|
function bodyToTypeShape(body) {
|
|
306
482
|
if (!("type" in body)) {
|
|
@@ -349,5 +525,7 @@ var PRIMITIVE_TYPE_SHAPES = {
|
|
|
349
525
|
IntentFindingSuppressionSchema,
|
|
350
526
|
IntentRefSchema,
|
|
351
527
|
IntentSourceSchema,
|
|
352
|
-
|
|
528
|
+
blanksLeftEmpty,
|
|
529
|
+
intentDocToSummary,
|
|
530
|
+
toBoundaryBinding
|
|
353
531
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { BoundaryBinding, TypeShape } from '@suss/ir-core';
|
|
2
|
+
import { EffectRelation, BoundaryBinding, TypeShape } from '@suss/ir-core';
|
|
3
3
|
|
|
4
4
|
declare const IntentFindingKindSchema: z.ZodEnum<{
|
|
5
5
|
uncoveredOutcome: "uncoveredOutcome";
|
|
@@ -10,6 +10,7 @@ declare const IntentFindingKindSchema: z.ZodEnum<{
|
|
|
10
10
|
unlinkedScenario: "unlinkedScenario";
|
|
11
11
|
danglingScenarioLink: "danglingScenarioLink";
|
|
12
12
|
ambiguousScenarioLink: "ambiguousScenarioLink";
|
|
13
|
+
undescribedOutcome: "undescribedOutcome";
|
|
13
14
|
}>;
|
|
14
15
|
type IntentFindingKind = z.infer<typeof IntentFindingKindSchema>;
|
|
15
16
|
declare const IntentFindingSeveritySchema: z.ZodEnum<{
|
|
@@ -53,6 +54,7 @@ declare const IntentFindingSchema: z.ZodObject<{
|
|
|
53
54
|
unlinkedScenario: "unlinkedScenario";
|
|
54
55
|
danglingScenarioLink: "danglingScenarioLink";
|
|
55
56
|
ambiguousScenarioLink: "ambiguousScenarioLink";
|
|
57
|
+
undescribedOutcome: "undescribedOutcome";
|
|
56
58
|
}>;
|
|
57
59
|
severity: z.ZodEnum<{
|
|
58
60
|
error: "error";
|
|
@@ -91,6 +93,16 @@ declare const IntentSourceSchema: z.ZodDefault<z.ZodEnum<{
|
|
|
91
93
|
inferred: "inferred";
|
|
92
94
|
"inferred, curated": "inferred, curated";
|
|
93
95
|
}>>;
|
|
96
|
+
/**
|
|
97
|
+
* The blanks a draft is still waiting on, given which fields its schema
|
|
98
|
+
* failures landed on. Empty for a doc that failed some other way, so a
|
|
99
|
+
* reader can tell an uncurated draft from a broken file.
|
|
100
|
+
*
|
|
101
|
+
* `suss infer intent` writes those fields empty, which the schema
|
|
102
|
+
* rejects on purpose: an uncurated draft is not something to check yet,
|
|
103
|
+
* and a placeholder that validated would read as finished.
|
|
104
|
+
*/
|
|
105
|
+
declare function blanksLeftEmpty(doc: unknown, failedFields: string[]): string[];
|
|
94
106
|
interface AuthoredShape {
|
|
95
107
|
type: z.infer<typeof PrimitiveTypeName> | "array" | "object";
|
|
96
108
|
items?: AuthoredShape | undefined;
|
|
@@ -113,10 +125,59 @@ declare const BoundarySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
113
125
|
exportName: z.ZodOptional<z.ZodString>;
|
|
114
126
|
package: z.ZodOptional<z.ZodString>;
|
|
115
127
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
128
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
129
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
130
|
+
messageBus: z.ZodEnum<{
|
|
131
|
+
aws_sqs: "aws_sqs";
|
|
132
|
+
"aws.sns": "aws.sns";
|
|
133
|
+
s3: "s3";
|
|
134
|
+
eventbridge: "eventbridge";
|
|
135
|
+
bullmq: "bullmq";
|
|
136
|
+
kafka: "kafka";
|
|
137
|
+
nats: "nats";
|
|
138
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
139
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
140
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
141
|
+
}>;
|
|
142
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
143
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
144
|
+
semantics: z.ZodLiteral<"storage">;
|
|
145
|
+
storageSystem: z.ZodString;
|
|
146
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
147
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
148
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
149
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
150
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
151
|
+
deploymentTarget: z.ZodEnum<{
|
|
152
|
+
lambda: "lambda";
|
|
153
|
+
"ecs-task": "ecs-task";
|
|
154
|
+
container: "container";
|
|
155
|
+
"k8s-deployment": "k8s-deployment";
|
|
156
|
+
worker: "worker";
|
|
157
|
+
}>;
|
|
158
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
116
159
|
}, z.core.$strip>], "semantics">;
|
|
160
|
+
/** One effect, written `- writes: postgresql:invoices`. */
|
|
161
|
+
type DeclaredEffect = Partial<Record<EffectRelation, string>> & {
|
|
162
|
+
/** The columns it touches, when the outcome turns on which ones. */
|
|
163
|
+
fields?: string[];
|
|
164
|
+
/** What it picks the item out by. */
|
|
165
|
+
by?: string | string[];
|
|
166
|
+
};
|
|
167
|
+
declare const EffectOutcomeSchema: z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>;
|
|
168
|
+
/** One clause of a structured `when`, or the sentence for a guard that maps to none. */
|
|
169
|
+
type WhenClause = string | ({
|
|
170
|
+
where?: string;
|
|
171
|
+
} & Partial<Record<EffectRelation | "input", string>> & {
|
|
172
|
+
finds?: "nothing" | "something";
|
|
173
|
+
is?: string;
|
|
174
|
+
equals?: string | number | boolean | null;
|
|
175
|
+
has?: string;
|
|
176
|
+
});
|
|
177
|
+
declare const WhenSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
117
178
|
declare const BoundaryTransitionSchema: z.ZodObject<{
|
|
118
179
|
id: z.ZodString;
|
|
119
|
-
when: z.ZodString
|
|
180
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
120
181
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
121
182
|
status: z.ZodNumber;
|
|
122
183
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -133,6 +194,7 @@ declare const BoundaryTransitionSchema: z.ZodObject<{
|
|
|
133
194
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
134
195
|
errorType: z.ZodOptional<z.ZodString>;
|
|
135
196
|
}, z.core.$strip>>>;
|
|
197
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
136
198
|
}, z.core.$strip>;
|
|
137
199
|
declare const BoundaryIntentSchema: z.ZodObject<{
|
|
138
200
|
kind: z.ZodLiteral<"boundary">;
|
|
@@ -156,10 +218,41 @@ declare const BoundaryIntentSchema: z.ZodObject<{
|
|
|
156
218
|
exportName: z.ZodOptional<z.ZodString>;
|
|
157
219
|
package: z.ZodOptional<z.ZodString>;
|
|
158
220
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
221
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
222
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
223
|
+
messageBus: z.ZodEnum<{
|
|
224
|
+
aws_sqs: "aws_sqs";
|
|
225
|
+
"aws.sns": "aws.sns";
|
|
226
|
+
s3: "s3";
|
|
227
|
+
eventbridge: "eventbridge";
|
|
228
|
+
bullmq: "bullmq";
|
|
229
|
+
kafka: "kafka";
|
|
230
|
+
nats: "nats";
|
|
231
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
232
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
233
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
234
|
+
}>;
|
|
235
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
236
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
237
|
+
semantics: z.ZodLiteral<"storage">;
|
|
238
|
+
storageSystem: z.ZodString;
|
|
239
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
240
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
241
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
242
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
243
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
244
|
+
deploymentTarget: z.ZodEnum<{
|
|
245
|
+
lambda: "lambda";
|
|
246
|
+
"ecs-task": "ecs-task";
|
|
247
|
+
container: "container";
|
|
248
|
+
"k8s-deployment": "k8s-deployment";
|
|
249
|
+
worker: "worker";
|
|
250
|
+
}>;
|
|
251
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
159
252
|
}, z.core.$strip>], "semantics">;
|
|
160
253
|
transitions: z.ZodArray<z.ZodObject<{
|
|
161
254
|
id: z.ZodString;
|
|
162
|
-
when: z.ZodString
|
|
255
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
163
256
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
164
257
|
status: z.ZodNumber;
|
|
165
258
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -176,6 +269,7 @@ declare const BoundaryIntentSchema: z.ZodObject<{
|
|
|
176
269
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
177
270
|
errorType: z.ZodOptional<z.ZodString>;
|
|
178
271
|
}, z.core.$strip>>>;
|
|
272
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
179
273
|
}, z.core.$strip>>;
|
|
180
274
|
}, z.core.$strip>;
|
|
181
275
|
declare const PrdScenarioSchema: z.ZodObject<{
|
|
@@ -223,10 +317,41 @@ declare const IntentDocSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
223
317
|
exportName: z.ZodOptional<z.ZodString>;
|
|
224
318
|
package: z.ZodOptional<z.ZodString>;
|
|
225
319
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
320
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
321
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
322
|
+
messageBus: z.ZodEnum<{
|
|
323
|
+
aws_sqs: "aws_sqs";
|
|
324
|
+
"aws.sns": "aws.sns";
|
|
325
|
+
s3: "s3";
|
|
326
|
+
eventbridge: "eventbridge";
|
|
327
|
+
bullmq: "bullmq";
|
|
328
|
+
kafka: "kafka";
|
|
329
|
+
nats: "nats";
|
|
330
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
331
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
332
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
333
|
+
}>;
|
|
334
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
335
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
336
|
+
semantics: z.ZodLiteral<"storage">;
|
|
337
|
+
storageSystem: z.ZodString;
|
|
338
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
339
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
340
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
341
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
342
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
343
|
+
deploymentTarget: z.ZodEnum<{
|
|
344
|
+
lambda: "lambda";
|
|
345
|
+
"ecs-task": "ecs-task";
|
|
346
|
+
container: "container";
|
|
347
|
+
"k8s-deployment": "k8s-deployment";
|
|
348
|
+
worker: "worker";
|
|
349
|
+
}>;
|
|
350
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
226
351
|
}, z.core.$strip>], "semantics">;
|
|
227
352
|
transitions: z.ZodArray<z.ZodObject<{
|
|
228
353
|
id: z.ZodString;
|
|
229
|
-
when: z.ZodString
|
|
354
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
230
355
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
231
356
|
status: z.ZodNumber;
|
|
232
357
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -243,6 +368,7 @@ declare const IntentDocSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
243
368
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
244
369
|
errorType: z.ZodOptional<z.ZodString>;
|
|
245
370
|
}, z.core.$strip>>>;
|
|
371
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
246
372
|
}, z.core.$strip>>;
|
|
247
373
|
}, z.core.$strip>, z.ZodObject<{
|
|
248
374
|
kind: z.ZodLiteral<"prd">;
|
|
@@ -265,8 +391,12 @@ type IntentDoc = z.infer<typeof IntentDocSchema>;
|
|
|
265
391
|
type BoundaryIntent = z.infer<typeof BoundaryIntentSchema>;
|
|
266
392
|
type Prd = z.infer<typeof PrdSchema>;
|
|
267
393
|
type PrdScenario = z.infer<typeof PrdScenarioSchema>;
|
|
394
|
+
type When = z.infer<typeof WhenSchema>;
|
|
268
395
|
type Boundary = z.infer<typeof BoundarySchema>;
|
|
396
|
+
/** A boundary block as somebody writes it, before defaults are put in. */
|
|
397
|
+
type AuthoredBoundary = z.input<typeof BoundarySchema>;
|
|
269
398
|
type BoundaryTransition = z.infer<typeof BoundaryTransitionSchema>;
|
|
399
|
+
type EffectOutcome = z.infer<typeof EffectOutcomeSchema>;
|
|
270
400
|
type BodyShape = z.infer<typeof BodyShapeSchema>;
|
|
271
401
|
type IntentSource = z.infer<typeof IntentSourceSchema>;
|
|
272
402
|
declare const PrimitiveTypeName: z.ZodEnum<{
|
|
@@ -279,13 +409,44 @@ declare const PrimitiveTypeName: z.ZodEnum<{
|
|
|
279
409
|
}>;
|
|
280
410
|
type PrimitiveTypeName = z.infer<typeof PrimitiveTypeName>;
|
|
281
411
|
|
|
282
|
-
/**
|
|
283
|
-
|
|
412
|
+
/**
|
|
413
|
+
* How a boundary outcome ends. `effect` is an outcome that declares
|
|
414
|
+
* only what it did, which is what a queue consumer or a table writer
|
|
415
|
+
* has to say instead of a status.
|
|
416
|
+
*/
|
|
417
|
+
type IntentOutcomeKind = "response" | "return" | "throw" | "effect";
|
|
418
|
+
/** One effect an outcome has, in the verbs `suss ask` asks with. */
|
|
419
|
+
interface IntentEffect {
|
|
420
|
+
does: EffectRelation;
|
|
421
|
+
/** The boundary it reaches, as the author spelled it. */
|
|
422
|
+
names: string;
|
|
423
|
+
/** The columns it touches. Empty when the doc states none. */
|
|
424
|
+
fields: string[];
|
|
425
|
+
/** What it picks the item out by. Empty when the doc states none. */
|
|
426
|
+
by: string[];
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* One clause of what a branch turned on. A clause whose subject is a
|
|
430
|
+
* boundary is the one the checker compares; the rest carry through for
|
|
431
|
+
* a reader.
|
|
432
|
+
*/
|
|
433
|
+
interface IntentCondition {
|
|
434
|
+
/** The boundary the guard read, when the clause says one. */
|
|
435
|
+
at: IntentEffect | null;
|
|
436
|
+
/** What the caller sent, when the clause says that instead. */
|
|
437
|
+
input: string | null;
|
|
438
|
+
/** Whether the lookup came back with something, when the clause says. */
|
|
439
|
+
finds: "nothing" | "something" | null;
|
|
440
|
+
/** The clause as one line, for a finding to quote. */
|
|
441
|
+
said: string;
|
|
442
|
+
}
|
|
284
443
|
interface IntentOutcome {
|
|
285
444
|
/** Author-declared id PRD scenarios reference. */
|
|
286
445
|
id: string;
|
|
287
|
-
/**
|
|
446
|
+
/** What the branch turned on, as one line, for a reader. */
|
|
288
447
|
when: string;
|
|
448
|
+
/** The same, clause by clause, for the checker to compare. */
|
|
449
|
+
conditions: IntentCondition[];
|
|
289
450
|
kind: IntentOutcomeKind;
|
|
290
451
|
/** Set only for `response` outcomes (REST status code). */
|
|
291
452
|
status: number | null;
|
|
@@ -293,6 +454,8 @@ interface IntentOutcome {
|
|
|
293
454
|
body: TypeShape | null;
|
|
294
455
|
/** Error type name, set only for `throw` outcomes when declared. */
|
|
295
456
|
errorType: string | null;
|
|
457
|
+
/** The effects this outcome declares. Empty when it declares none. */
|
|
458
|
+
effects: IntentEffect[];
|
|
296
459
|
}
|
|
297
460
|
interface BoundaryIntentSummary {
|
|
298
461
|
kind: "boundary";
|
|
@@ -322,5 +485,6 @@ interface PrdSummary {
|
|
|
322
485
|
}
|
|
323
486
|
type IntentSummary = BoundaryIntentSummary | PrdSummary;
|
|
324
487
|
declare function intentDocToSummary(doc: IntentDoc): IntentSummary;
|
|
488
|
+
declare function toBoundaryBinding(boundary: Boundary): BoundaryBinding;
|
|
325
489
|
|
|
326
|
-
export { type BodyShape, BodyShapeSchema, type Boundary, type BoundaryIntent, type BoundaryIntentSummary, BoundarySchema, type BoundaryTransition, type IntentDoc, IntentDocSchema, type IntentFinding, type IntentFindingKind, IntentFindingKindSchema, IntentFindingSchema, type IntentFindingSeverity, IntentFindingSeveritySchema, type IntentFindingSuppression, IntentFindingSuppressionSchema, type IntentOutcome, type IntentOutcomeKind, type IntentRef, IntentRefSchema, type IntentSource, IntentSourceSchema, type IntentSummary, type Prd, type PrdScenario, type PrdScenarioSummary, type PrdSummary, PrimitiveTypeName, intentDocToSummary };
|
|
490
|
+
export { type AuthoredBoundary, type AuthoredShape, type BodyShape, BodyShapeSchema, type Boundary, type BoundaryIntent, type BoundaryIntentSummary, BoundarySchema, type BoundaryTransition, type EffectOutcome, type IntentCondition, type IntentDoc, IntentDocSchema, type IntentEffect, type IntentFinding, type IntentFindingKind, IntentFindingKindSchema, IntentFindingSchema, type IntentFindingSeverity, IntentFindingSeveritySchema, type IntentFindingSuppression, IntentFindingSuppressionSchema, type IntentOutcome, type IntentOutcomeKind, type IntentRef, IntentRefSchema, type IntentSource, IntentSourceSchema, type IntentSummary, type Prd, type PrdScenario, type PrdScenarioSummary, type PrdSummary, PrimitiveTypeName, type When, type WhenClause, blanksLeftEmpty, intentDocToSummary, toBoundaryBinding };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { BoundaryBinding, TypeShape } from '@suss/ir-core';
|
|
2
|
+
import { EffectRelation, BoundaryBinding, TypeShape } from '@suss/ir-core';
|
|
3
3
|
|
|
4
4
|
declare const IntentFindingKindSchema: z.ZodEnum<{
|
|
5
5
|
uncoveredOutcome: "uncoveredOutcome";
|
|
@@ -10,6 +10,7 @@ declare const IntentFindingKindSchema: z.ZodEnum<{
|
|
|
10
10
|
unlinkedScenario: "unlinkedScenario";
|
|
11
11
|
danglingScenarioLink: "danglingScenarioLink";
|
|
12
12
|
ambiguousScenarioLink: "ambiguousScenarioLink";
|
|
13
|
+
undescribedOutcome: "undescribedOutcome";
|
|
13
14
|
}>;
|
|
14
15
|
type IntentFindingKind = z.infer<typeof IntentFindingKindSchema>;
|
|
15
16
|
declare const IntentFindingSeveritySchema: z.ZodEnum<{
|
|
@@ -53,6 +54,7 @@ declare const IntentFindingSchema: z.ZodObject<{
|
|
|
53
54
|
unlinkedScenario: "unlinkedScenario";
|
|
54
55
|
danglingScenarioLink: "danglingScenarioLink";
|
|
55
56
|
ambiguousScenarioLink: "ambiguousScenarioLink";
|
|
57
|
+
undescribedOutcome: "undescribedOutcome";
|
|
56
58
|
}>;
|
|
57
59
|
severity: z.ZodEnum<{
|
|
58
60
|
error: "error";
|
|
@@ -91,6 +93,16 @@ declare const IntentSourceSchema: z.ZodDefault<z.ZodEnum<{
|
|
|
91
93
|
inferred: "inferred";
|
|
92
94
|
"inferred, curated": "inferred, curated";
|
|
93
95
|
}>>;
|
|
96
|
+
/**
|
|
97
|
+
* The blanks a draft is still waiting on, given which fields its schema
|
|
98
|
+
* failures landed on. Empty for a doc that failed some other way, so a
|
|
99
|
+
* reader can tell an uncurated draft from a broken file.
|
|
100
|
+
*
|
|
101
|
+
* `suss infer intent` writes those fields empty, which the schema
|
|
102
|
+
* rejects on purpose: an uncurated draft is not something to check yet,
|
|
103
|
+
* and a placeholder that validated would read as finished.
|
|
104
|
+
*/
|
|
105
|
+
declare function blanksLeftEmpty(doc: unknown, failedFields: string[]): string[];
|
|
94
106
|
interface AuthoredShape {
|
|
95
107
|
type: z.infer<typeof PrimitiveTypeName> | "array" | "object";
|
|
96
108
|
items?: AuthoredShape | undefined;
|
|
@@ -113,10 +125,59 @@ declare const BoundarySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
113
125
|
exportName: z.ZodOptional<z.ZodString>;
|
|
114
126
|
package: z.ZodOptional<z.ZodString>;
|
|
115
127
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
128
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
129
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
130
|
+
messageBus: z.ZodEnum<{
|
|
131
|
+
aws_sqs: "aws_sqs";
|
|
132
|
+
"aws.sns": "aws.sns";
|
|
133
|
+
s3: "s3";
|
|
134
|
+
eventbridge: "eventbridge";
|
|
135
|
+
bullmq: "bullmq";
|
|
136
|
+
kafka: "kafka";
|
|
137
|
+
nats: "nats";
|
|
138
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
139
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
140
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
141
|
+
}>;
|
|
142
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
143
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
144
|
+
semantics: z.ZodLiteral<"storage">;
|
|
145
|
+
storageSystem: z.ZodString;
|
|
146
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
147
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
148
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
149
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
150
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
151
|
+
deploymentTarget: z.ZodEnum<{
|
|
152
|
+
lambda: "lambda";
|
|
153
|
+
"ecs-task": "ecs-task";
|
|
154
|
+
container: "container";
|
|
155
|
+
"k8s-deployment": "k8s-deployment";
|
|
156
|
+
worker: "worker";
|
|
157
|
+
}>;
|
|
158
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
116
159
|
}, z.core.$strip>], "semantics">;
|
|
160
|
+
/** One effect, written `- writes: postgresql:invoices`. */
|
|
161
|
+
type DeclaredEffect = Partial<Record<EffectRelation, string>> & {
|
|
162
|
+
/** The columns it touches, when the outcome turns on which ones. */
|
|
163
|
+
fields?: string[];
|
|
164
|
+
/** What it picks the item out by. */
|
|
165
|
+
by?: string | string[];
|
|
166
|
+
};
|
|
167
|
+
declare const EffectOutcomeSchema: z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>;
|
|
168
|
+
/** One clause of a structured `when`, or the sentence for a guard that maps to none. */
|
|
169
|
+
type WhenClause = string | ({
|
|
170
|
+
where?: string;
|
|
171
|
+
} & Partial<Record<EffectRelation | "input", string>> & {
|
|
172
|
+
finds?: "nothing" | "something";
|
|
173
|
+
is?: string;
|
|
174
|
+
equals?: string | number | boolean | null;
|
|
175
|
+
has?: string;
|
|
176
|
+
});
|
|
177
|
+
declare const WhenSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
117
178
|
declare const BoundaryTransitionSchema: z.ZodObject<{
|
|
118
179
|
id: z.ZodString;
|
|
119
|
-
when: z.ZodString
|
|
180
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
120
181
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
121
182
|
status: z.ZodNumber;
|
|
122
183
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -133,6 +194,7 @@ declare const BoundaryTransitionSchema: z.ZodObject<{
|
|
|
133
194
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
134
195
|
errorType: z.ZodOptional<z.ZodString>;
|
|
135
196
|
}, z.core.$strip>>>;
|
|
197
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
136
198
|
}, z.core.$strip>;
|
|
137
199
|
declare const BoundaryIntentSchema: z.ZodObject<{
|
|
138
200
|
kind: z.ZodLiteral<"boundary">;
|
|
@@ -156,10 +218,41 @@ declare const BoundaryIntentSchema: z.ZodObject<{
|
|
|
156
218
|
exportName: z.ZodOptional<z.ZodString>;
|
|
157
219
|
package: z.ZodOptional<z.ZodString>;
|
|
158
220
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
221
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
222
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
223
|
+
messageBus: z.ZodEnum<{
|
|
224
|
+
aws_sqs: "aws_sqs";
|
|
225
|
+
"aws.sns": "aws.sns";
|
|
226
|
+
s3: "s3";
|
|
227
|
+
eventbridge: "eventbridge";
|
|
228
|
+
bullmq: "bullmq";
|
|
229
|
+
kafka: "kafka";
|
|
230
|
+
nats: "nats";
|
|
231
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
232
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
233
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
234
|
+
}>;
|
|
235
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
236
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
237
|
+
semantics: z.ZodLiteral<"storage">;
|
|
238
|
+
storageSystem: z.ZodString;
|
|
239
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
240
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
241
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
242
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
243
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
244
|
+
deploymentTarget: z.ZodEnum<{
|
|
245
|
+
lambda: "lambda";
|
|
246
|
+
"ecs-task": "ecs-task";
|
|
247
|
+
container: "container";
|
|
248
|
+
"k8s-deployment": "k8s-deployment";
|
|
249
|
+
worker: "worker";
|
|
250
|
+
}>;
|
|
251
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
159
252
|
}, z.core.$strip>], "semantics">;
|
|
160
253
|
transitions: z.ZodArray<z.ZodObject<{
|
|
161
254
|
id: z.ZodString;
|
|
162
|
-
when: z.ZodString
|
|
255
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
163
256
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
164
257
|
status: z.ZodNumber;
|
|
165
258
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -176,6 +269,7 @@ declare const BoundaryIntentSchema: z.ZodObject<{
|
|
|
176
269
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
177
270
|
errorType: z.ZodOptional<z.ZodString>;
|
|
178
271
|
}, z.core.$strip>>>;
|
|
272
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
179
273
|
}, z.core.$strip>>;
|
|
180
274
|
}, z.core.$strip>;
|
|
181
275
|
declare const PrdScenarioSchema: z.ZodObject<{
|
|
@@ -223,10 +317,41 @@ declare const IntentDocSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
223
317
|
exportName: z.ZodOptional<z.ZodString>;
|
|
224
318
|
package: z.ZodOptional<z.ZodString>;
|
|
225
319
|
exportPath: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
320
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
321
|
+
semantics: z.ZodLiteral<"message-bus">;
|
|
322
|
+
messageBus: z.ZodEnum<{
|
|
323
|
+
aws_sqs: "aws_sqs";
|
|
324
|
+
"aws.sns": "aws.sns";
|
|
325
|
+
s3: "s3";
|
|
326
|
+
eventbridge: "eventbridge";
|
|
327
|
+
bullmq: "bullmq";
|
|
328
|
+
kafka: "kafka";
|
|
329
|
+
nats: "nats";
|
|
330
|
+
"cloudflare-queues": "cloudflare-queues";
|
|
331
|
+
"cloudflare-cron": "cloudflare-cron";
|
|
332
|
+
"cloudflare-tail": "cloudflare-tail";
|
|
333
|
+
}>;
|
|
334
|
+
channel: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
335
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
336
|
+
semantics: z.ZodLiteral<"storage">;
|
|
337
|
+
storageSystem: z.ZodString;
|
|
338
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
339
|
+
container: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
340
|
+
accessPath: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
341
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
342
|
+
semantics: z.ZodLiteral<"unit-invocation">;
|
|
343
|
+
deploymentTarget: z.ZodEnum<{
|
|
344
|
+
lambda: "lambda";
|
|
345
|
+
"ecs-task": "ecs-task";
|
|
346
|
+
container: "container";
|
|
347
|
+
"k8s-deployment": "k8s-deployment";
|
|
348
|
+
worker: "worker";
|
|
349
|
+
}>;
|
|
350
|
+
instanceName: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
226
351
|
}, z.core.$strip>], "semantics">;
|
|
227
352
|
transitions: z.ZodArray<z.ZodObject<{
|
|
228
353
|
id: z.ZodString;
|
|
229
|
-
when: z.ZodString
|
|
354
|
+
when: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>, ...z.ZodType<WhenClause, unknown, z.core.$ZodTypeInternals<WhenClause, unknown>>[]]>>]>;
|
|
230
355
|
response: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
231
356
|
status: z.ZodNumber;
|
|
232
357
|
body: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<AuthoredShape, unknown, z.core.$ZodTypeInternals<AuthoredShape, unknown>>, z.ZodObject<{
|
|
@@ -243,6 +368,7 @@ declare const IntentDocSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
243
368
|
throws: z.ZodOptional<z.ZodPreprocess<z.ZodObject<{
|
|
244
369
|
errorType: z.ZodOptional<z.ZodString>;
|
|
245
370
|
}, z.core.$strip>>>;
|
|
371
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>, ...z.ZodType<DeclaredEffect, unknown, z.core.$ZodTypeInternals<DeclaredEffect, unknown>>[]]>>>;
|
|
246
372
|
}, z.core.$strip>>;
|
|
247
373
|
}, z.core.$strip>, z.ZodObject<{
|
|
248
374
|
kind: z.ZodLiteral<"prd">;
|
|
@@ -265,8 +391,12 @@ type IntentDoc = z.infer<typeof IntentDocSchema>;
|
|
|
265
391
|
type BoundaryIntent = z.infer<typeof BoundaryIntentSchema>;
|
|
266
392
|
type Prd = z.infer<typeof PrdSchema>;
|
|
267
393
|
type PrdScenario = z.infer<typeof PrdScenarioSchema>;
|
|
394
|
+
type When = z.infer<typeof WhenSchema>;
|
|
268
395
|
type Boundary = z.infer<typeof BoundarySchema>;
|
|
396
|
+
/** A boundary block as somebody writes it, before defaults are put in. */
|
|
397
|
+
type AuthoredBoundary = z.input<typeof BoundarySchema>;
|
|
269
398
|
type BoundaryTransition = z.infer<typeof BoundaryTransitionSchema>;
|
|
399
|
+
type EffectOutcome = z.infer<typeof EffectOutcomeSchema>;
|
|
270
400
|
type BodyShape = z.infer<typeof BodyShapeSchema>;
|
|
271
401
|
type IntentSource = z.infer<typeof IntentSourceSchema>;
|
|
272
402
|
declare const PrimitiveTypeName: z.ZodEnum<{
|
|
@@ -279,13 +409,44 @@ declare const PrimitiveTypeName: z.ZodEnum<{
|
|
|
279
409
|
}>;
|
|
280
410
|
type PrimitiveTypeName = z.infer<typeof PrimitiveTypeName>;
|
|
281
411
|
|
|
282
|
-
/**
|
|
283
|
-
|
|
412
|
+
/**
|
|
413
|
+
* How a boundary outcome ends. `effect` is an outcome that declares
|
|
414
|
+
* only what it did, which is what a queue consumer or a table writer
|
|
415
|
+
* has to say instead of a status.
|
|
416
|
+
*/
|
|
417
|
+
type IntentOutcomeKind = "response" | "return" | "throw" | "effect";
|
|
418
|
+
/** One effect an outcome has, in the verbs `suss ask` asks with. */
|
|
419
|
+
interface IntentEffect {
|
|
420
|
+
does: EffectRelation;
|
|
421
|
+
/** The boundary it reaches, as the author spelled it. */
|
|
422
|
+
names: string;
|
|
423
|
+
/** The columns it touches. Empty when the doc states none. */
|
|
424
|
+
fields: string[];
|
|
425
|
+
/** What it picks the item out by. Empty when the doc states none. */
|
|
426
|
+
by: string[];
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* One clause of what a branch turned on. A clause whose subject is a
|
|
430
|
+
* boundary is the one the checker compares; the rest carry through for
|
|
431
|
+
* a reader.
|
|
432
|
+
*/
|
|
433
|
+
interface IntentCondition {
|
|
434
|
+
/** The boundary the guard read, when the clause says one. */
|
|
435
|
+
at: IntentEffect | null;
|
|
436
|
+
/** What the caller sent, when the clause says that instead. */
|
|
437
|
+
input: string | null;
|
|
438
|
+
/** Whether the lookup came back with something, when the clause says. */
|
|
439
|
+
finds: "nothing" | "something" | null;
|
|
440
|
+
/** The clause as one line, for a finding to quote. */
|
|
441
|
+
said: string;
|
|
442
|
+
}
|
|
284
443
|
interface IntentOutcome {
|
|
285
444
|
/** Author-declared id PRD scenarios reference. */
|
|
286
445
|
id: string;
|
|
287
|
-
/**
|
|
446
|
+
/** What the branch turned on, as one line, for a reader. */
|
|
288
447
|
when: string;
|
|
448
|
+
/** The same, clause by clause, for the checker to compare. */
|
|
449
|
+
conditions: IntentCondition[];
|
|
289
450
|
kind: IntentOutcomeKind;
|
|
290
451
|
/** Set only for `response` outcomes (REST status code). */
|
|
291
452
|
status: number | null;
|
|
@@ -293,6 +454,8 @@ interface IntentOutcome {
|
|
|
293
454
|
body: TypeShape | null;
|
|
294
455
|
/** Error type name, set only for `throw` outcomes when declared. */
|
|
295
456
|
errorType: string | null;
|
|
457
|
+
/** The effects this outcome declares. Empty when it declares none. */
|
|
458
|
+
effects: IntentEffect[];
|
|
296
459
|
}
|
|
297
460
|
interface BoundaryIntentSummary {
|
|
298
461
|
kind: "boundary";
|
|
@@ -322,5 +485,6 @@ interface PrdSummary {
|
|
|
322
485
|
}
|
|
323
486
|
type IntentSummary = BoundaryIntentSummary | PrdSummary;
|
|
324
487
|
declare function intentDocToSummary(doc: IntentDoc): IntentSummary;
|
|
488
|
+
declare function toBoundaryBinding(boundary: Boundary): BoundaryBinding;
|
|
325
489
|
|
|
326
|
-
export { type BodyShape, BodyShapeSchema, type Boundary, type BoundaryIntent, type BoundaryIntentSummary, BoundarySchema, type BoundaryTransition, type IntentDoc, IntentDocSchema, type IntentFinding, type IntentFindingKind, IntentFindingKindSchema, IntentFindingSchema, type IntentFindingSeverity, IntentFindingSeveritySchema, type IntentFindingSuppression, IntentFindingSuppressionSchema, type IntentOutcome, type IntentOutcomeKind, type IntentRef, IntentRefSchema, type IntentSource, IntentSourceSchema, type IntentSummary, type Prd, type PrdScenario, type PrdScenarioSummary, type PrdSummary, PrimitiveTypeName, intentDocToSummary };
|
|
490
|
+
export { type AuthoredBoundary, type AuthoredShape, type BodyShape, BodyShapeSchema, type Boundary, type BoundaryIntent, type BoundaryIntentSummary, BoundarySchema, type BoundaryTransition, type EffectOutcome, type IntentCondition, type IntentDoc, IntentDocSchema, type IntentEffect, type IntentFinding, type IntentFindingKind, IntentFindingKindSchema, IntentFindingSchema, type IntentFindingSeverity, IntentFindingSeveritySchema, type IntentFindingSuppression, IntentFindingSuppressionSchema, type IntentOutcome, type IntentOutcomeKind, type IntentRef, IntentRefSchema, type IntentSource, IntentSourceSchema, type IntentSummary, type Prd, type PrdScenario, type PrdScenarioSummary, type PrdSummary, PrimitiveTypeName, type When, type WhenClause, blanksLeftEmpty, intentDocToSummary, toBoundaryBinding };
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,12 @@ var IntentFindingKindSchema = z.enum([
|
|
|
20
20
|
// scenario carries no structured link (info — a valid pending state)
|
|
21
21
|
"danglingScenarioLink",
|
|
22
22
|
// link names an intent / outcome no boundary intent declares
|
|
23
|
-
"ambiguousScenarioLink"
|
|
23
|
+
"ambiguousScenarioLink",
|
|
24
24
|
// link resolves to two or more boundary intents sharing the name
|
|
25
|
+
// The same coverage question asked the other way: which declared
|
|
26
|
+
// behaviour has nobody written a reason for.
|
|
27
|
+
"undescribedOutcome"
|
|
28
|
+
// a declared outcome no PRD scenario links to (info)
|
|
25
29
|
]);
|
|
26
30
|
var IntentFindingSeveritySchema = z.enum(["error", "warning", "info"]);
|
|
27
31
|
var IntentRefSchema = z.object({
|
|
@@ -71,7 +75,28 @@ var IntentFindingSchema = z.object({
|
|
|
71
75
|
|
|
72
76
|
// src/schema.ts
|
|
73
77
|
import { z as z2 } from "zod";
|
|
78
|
+
import {
|
|
79
|
+
EffectRelationSchema,
|
|
80
|
+
MessageBusSemanticsSchema,
|
|
81
|
+
StorageSemanticsSchema,
|
|
82
|
+
UnitInvocationSemanticsSchema
|
|
83
|
+
} from "@suss/ir-core";
|
|
74
84
|
var IntentSourceSchema = z2.enum(["author", "inferred", "inferred, curated"]).default("author");
|
|
85
|
+
var UNCURATED_SOURCE = "inferred";
|
|
86
|
+
var CURATED_FIELDS = ["title", "purpose", "audience", "when", "expect"];
|
|
87
|
+
function blankIn(field) {
|
|
88
|
+
return field.slice(field.lastIndexOf(".") + 1);
|
|
89
|
+
}
|
|
90
|
+
function blanksLeftEmpty(doc, failedFields) {
|
|
91
|
+
if (doc.source !== UNCURATED_SOURCE) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const blanks = failedFields.map(blankIn);
|
|
95
|
+
if (blanks.length === 0 || !blanks.every((field) => CURATED_FIELDS.includes(field))) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return CURATED_FIELDS.filter((blank) => blanks.includes(blank));
|
|
99
|
+
}
|
|
75
100
|
var PrimitiveTypeName = z2.enum([
|
|
76
101
|
"string",
|
|
77
102
|
"integer",
|
|
@@ -116,9 +141,29 @@ var FunctionCallBoundarySchema = z2.object({
|
|
|
116
141
|
/** Path to the export within the package (sub-path + nested names). */
|
|
117
142
|
exportPath: z2.array(z2.string()).optional()
|
|
118
143
|
});
|
|
144
|
+
var MessageBusBoundarySchema = z2.object({
|
|
145
|
+
semantics: z2.literal("message-bus"),
|
|
146
|
+
messageBus: MessageBusSemanticsSchema.shape.messageBus,
|
|
147
|
+
channel: MessageBusSemanticsSchema.shape.channel.default(null)
|
|
148
|
+
});
|
|
149
|
+
var StorageBoundarySchema = z2.object({
|
|
150
|
+
semantics: z2.literal("storage"),
|
|
151
|
+
storageSystem: StorageSemanticsSchema.shape.storageSystem,
|
|
152
|
+
scope: StorageSemanticsSchema.shape.scope.default("default"),
|
|
153
|
+
container: StorageSemanticsSchema.shape.container.default(null),
|
|
154
|
+
accessPath: StorageSemanticsSchema.shape.accessPath.default(null)
|
|
155
|
+
});
|
|
156
|
+
var UnitInvocationBoundarySchema = z2.object({
|
|
157
|
+
semantics: z2.literal("unit-invocation"),
|
|
158
|
+
deploymentTarget: UnitInvocationSemanticsSchema.shape.deploymentTarget,
|
|
159
|
+
instanceName: UnitInvocationSemanticsSchema.shape.instanceName.default(null)
|
|
160
|
+
});
|
|
119
161
|
var BoundarySchema = z2.discriminatedUnion("semantics", [
|
|
120
162
|
RestBoundarySchema,
|
|
121
|
-
FunctionCallBoundarySchema
|
|
163
|
+
FunctionCallBoundarySchema,
|
|
164
|
+
MessageBusBoundarySchema,
|
|
165
|
+
StorageBoundarySchema,
|
|
166
|
+
UnitInvocationBoundarySchema
|
|
122
167
|
]);
|
|
123
168
|
var ResponseOutcomeSchema = z2.object({
|
|
124
169
|
status: z2.number().int().min(100).max(599),
|
|
@@ -133,18 +178,70 @@ var ThrowsOutcomeSchema = z2.object({
|
|
|
133
178
|
function emptyIfNull(schema) {
|
|
134
179
|
return z2.preprocess((v) => v === null ? {} : v, schema);
|
|
135
180
|
}
|
|
181
|
+
var ONE_OR_MORE = z2.union([
|
|
182
|
+
z2.string().min(1),
|
|
183
|
+
z2.array(z2.string().min(1)).min(1)
|
|
184
|
+
]);
|
|
185
|
+
var EFFECT_BY_VERB = EffectRelationSchema.options.map(
|
|
186
|
+
(verb) => z2.strictObject({
|
|
187
|
+
[verb]: z2.string().min(1),
|
|
188
|
+
fields: z2.array(z2.string().min(1)).min(1).optional(),
|
|
189
|
+
by: ONE_OR_MORE.optional()
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
var EffectOutcomeSchema = z2.union(EFFECT_BY_VERB);
|
|
193
|
+
var CHECKS = {
|
|
194
|
+
/** What a lookup came back with. */
|
|
195
|
+
finds: z2.enum(["nothing", "something"]),
|
|
196
|
+
/** What state the value was in: `set`, `missing`, `null`, `a string`. */
|
|
197
|
+
is: z2.string().min(1),
|
|
198
|
+
/** The value it was equal to. */
|
|
199
|
+
equals: z2.union([z2.string(), z2.number(), z2.boolean(), z2.null()]),
|
|
200
|
+
/** A property it had. */
|
|
201
|
+
has: z2.string().min(1)
|
|
202
|
+
};
|
|
203
|
+
var SUBJECT_KEYS = [
|
|
204
|
+
...EffectRelationSchema.options,
|
|
205
|
+
"input"
|
|
206
|
+
];
|
|
207
|
+
var WHEN_CLAUSE_BY_SUBJECT = SUBJECT_KEYS.map(
|
|
208
|
+
(key) => z2.strictObject({
|
|
209
|
+
[key]: z2.string().min(1),
|
|
210
|
+
where: z2.string().min(1).optional(),
|
|
211
|
+
...Object.fromEntries(
|
|
212
|
+
Object.entries(CHECKS).map(([check, schema]) => [
|
|
213
|
+
check,
|
|
214
|
+
schema.optional()
|
|
215
|
+
])
|
|
216
|
+
)
|
|
217
|
+
}).refine(
|
|
218
|
+
(clause) => Object.keys(CHECKS).filter(
|
|
219
|
+
(check) => clause[check] !== void 0
|
|
220
|
+
).length <= 1,
|
|
221
|
+
{
|
|
222
|
+
message: `a when clause says at most one of ${Object.keys(CHECKS).join(", ")} about its subject`
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
var WhenSchema = z2.union([
|
|
227
|
+
z2.string().min(1),
|
|
228
|
+
z2.array(z2.union([z2.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
|
|
229
|
+
]);
|
|
136
230
|
var BoundaryTransitionSchema = z2.object({
|
|
137
231
|
id: z2.string().min(1),
|
|
138
|
-
when:
|
|
232
|
+
when: WhenSchema,
|
|
139
233
|
response: emptyIfNull(ResponseOutcomeSchema).optional(),
|
|
140
234
|
returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
|
|
141
|
-
throws: emptyIfNull(ThrowsOutcomeSchema).optional()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
235
|
+
throws: emptyIfNull(ThrowsOutcomeSchema).optional(),
|
|
236
|
+
results: z2.array(EffectOutcomeSchema).min(1).optional()
|
|
237
|
+
}).refine((t) => endingsOf(t).length <= 1, {
|
|
238
|
+
message: "a transition ends one way: give it at most one of response, returns, or throws"
|
|
239
|
+
}).refine((t) => endingsOf(t).length === 1 || t.results !== void 0, {
|
|
240
|
+
message: "each transition must declare an outcome: response, returns, throws, or the effects it results in"
|
|
241
|
+
});
|
|
242
|
+
function endingsOf(t) {
|
|
243
|
+
return [t.response, t.returns, t.throws].filter((o) => o !== void 0);
|
|
244
|
+
}
|
|
148
245
|
var BoundaryIntentSchema = z2.object({
|
|
149
246
|
kind: z2.literal("boundary"),
|
|
150
247
|
/** Name PRDs reference outcomes through: `<name>.<transition-id>`. */
|
|
@@ -189,8 +286,12 @@ var IntentDocSchema = z2.discriminatedUnion("kind", [
|
|
|
189
286
|
|
|
190
287
|
// src/summary.ts
|
|
191
288
|
import {
|
|
289
|
+
EffectRelationSchema as EffectRelationSchema2,
|
|
192
290
|
functionCallBinding,
|
|
193
|
-
|
|
291
|
+
messageBusBinding,
|
|
292
|
+
restBinding,
|
|
293
|
+
storageBinding,
|
|
294
|
+
unitInvocationBinding
|
|
194
295
|
} from "@suss/ir-core";
|
|
195
296
|
function intentDocToSummary(doc) {
|
|
196
297
|
if (doc.kind === "prd") {
|
|
@@ -224,26 +325,105 @@ function prdToSummary(doc) {
|
|
|
224
325
|
}))
|
|
225
326
|
};
|
|
226
327
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
return functionCallBinding({
|
|
328
|
+
var BINDINGS = {
|
|
329
|
+
rest: (boundary) => restBinding({
|
|
330
|
+
transport: boundary.transport,
|
|
331
|
+
method: boundary.method,
|
|
332
|
+
path: boundary.path,
|
|
333
|
+
recognition: "intent"
|
|
334
|
+
}),
|
|
335
|
+
"function-call": (boundary) => functionCallBinding({
|
|
237
336
|
transport: boundary.transport,
|
|
238
337
|
recognition: "intent",
|
|
239
338
|
...boundary.module !== void 0 ? { module: boundary.module } : {},
|
|
240
339
|
...boundary.exportName !== void 0 ? { exportName: boundary.exportName } : {},
|
|
241
340
|
...boundary.package !== void 0 ? { package: boundary.package } : {},
|
|
242
341
|
...boundary.exportPath !== void 0 ? { exportPath: boundary.exportPath } : {}
|
|
243
|
-
})
|
|
342
|
+
}),
|
|
343
|
+
"message-bus": (boundary) => messageBusBinding({
|
|
344
|
+
recognition: "intent",
|
|
345
|
+
messageBus: boundary.messageBus,
|
|
346
|
+
channel: boundary.channel
|
|
347
|
+
}),
|
|
348
|
+
"unit-invocation": (boundary) => unitInvocationBinding({
|
|
349
|
+
recognition: "intent",
|
|
350
|
+
deploymentTarget: boundary.deploymentTarget,
|
|
351
|
+
instanceName: boundary.instanceName
|
|
352
|
+
}),
|
|
353
|
+
storage: (boundary) => storageBinding({
|
|
354
|
+
recognition: "intent",
|
|
355
|
+
storageSystem: boundary.storageSystem,
|
|
356
|
+
scope: boundary.scope,
|
|
357
|
+
container: boundary.container,
|
|
358
|
+
accessPath: boundary.accessPath
|
|
359
|
+
})
|
|
360
|
+
};
|
|
361
|
+
function toBoundaryBinding(boundary) {
|
|
362
|
+
const build = BINDINGS[boundary.semantics];
|
|
363
|
+
return build(boundary);
|
|
364
|
+
}
|
|
365
|
+
var VERBS = EffectRelationSchema2.options;
|
|
366
|
+
function toEffect(declared) {
|
|
367
|
+
const [does, names] = Object.entries(declared).find(
|
|
368
|
+
([key]) => VERBS.includes(key)
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
does,
|
|
372
|
+
names,
|
|
373
|
+
fields: declared.fields ?? [],
|
|
374
|
+
by: oneOrMore(declared.by)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function oneOrMore(written) {
|
|
378
|
+
if (written === void 0) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
return typeof written === "string" ? [written] : written;
|
|
382
|
+
}
|
|
383
|
+
function subjectOf(clause) {
|
|
384
|
+
for (const does of VERBS) {
|
|
385
|
+
const names = clause[does];
|
|
386
|
+
if (names !== void 0) {
|
|
387
|
+
return { does, names, fields: [], by: [] };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function toCondition(clause) {
|
|
393
|
+
if (typeof clause === "string") {
|
|
394
|
+
return { at: null, input: null, finds: null, said: clause };
|
|
395
|
+
}
|
|
396
|
+
const at = subjectOf(clause);
|
|
397
|
+
return {
|
|
398
|
+
at,
|
|
399
|
+
input: clause.input ?? null,
|
|
400
|
+
finds: clause.finds ?? null,
|
|
401
|
+
said: saidAsOneLine(clause, at)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function saidAsOneLine(clause, at) {
|
|
405
|
+
const subject = at !== null ? `${at.does} ${at.names}` : `input ${String(clause.input)}`;
|
|
406
|
+
const check = CHECK_KEYS.map(
|
|
407
|
+
(key) => clause[key] === void 0 ? null : `${key} ${String(clause[key])}`
|
|
408
|
+
).find((said) => said !== null);
|
|
409
|
+
const narrows = clause.where === void 0 ? null : `where ${clause.where}`;
|
|
410
|
+
return [subject, check, narrows].filter((part) => part !== null).join(" ");
|
|
411
|
+
}
|
|
412
|
+
var CHECK_KEYS = ["finds", "is", "equals", "has"];
|
|
413
|
+
function toConditions(when) {
|
|
414
|
+
return (typeof when === "string" ? [when] : when).map(toCondition);
|
|
415
|
+
}
|
|
416
|
+
function whenAsOneLine(when) {
|
|
417
|
+
return typeof when === "string" ? when : toConditions(when).map((condition) => condition.said).join(" and ");
|
|
244
418
|
}
|
|
245
419
|
function toOutcome(t) {
|
|
246
|
-
const
|
|
420
|
+
const effects = (t.results ?? []).map(toEffect);
|
|
421
|
+
const base = {
|
|
422
|
+
id: t.id,
|
|
423
|
+
when: whenAsOneLine(t.when),
|
|
424
|
+
conditions: toConditions(t.when),
|
|
425
|
+
effects
|
|
426
|
+
};
|
|
247
427
|
if (t.response !== void 0) {
|
|
248
428
|
return {
|
|
249
429
|
...base,
|
|
@@ -262,13 +442,16 @@ function toOutcome(t) {
|
|
|
262
442
|
errorType: null
|
|
263
443
|
};
|
|
264
444
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
445
|
+
if (t.throws !== void 0) {
|
|
446
|
+
return {
|
|
447
|
+
...base,
|
|
448
|
+
kind: "throw",
|
|
449
|
+
status: null,
|
|
450
|
+
body: null,
|
|
451
|
+
errorType: t.throws.errorType ?? null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return { ...base, kind: "effect", status: null, body: null, errorType: null };
|
|
272
455
|
}
|
|
273
456
|
function bodyToTypeShape(body) {
|
|
274
457
|
if (!("type" in body)) {
|
|
@@ -316,5 +499,7 @@ export {
|
|
|
316
499
|
IntentFindingSuppressionSchema,
|
|
317
500
|
IntentRefSchema,
|
|
318
501
|
IntentSourceSchema,
|
|
319
|
-
|
|
502
|
+
blanksLeftEmpty,
|
|
503
|
+
intentDocToSummary,
|
|
504
|
+
toBoundaryBinding
|
|
320
505
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/intent-ir",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Intent IR for suss — the team-authored side of the loop. PRD (outcome) intent and system intent, in shapes that pair against derived behavioral summaries.",
|
|
6
6
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"zod": "^4.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@suss/ir-core": "0.
|
|
50
|
+
"@suss/ir-core": "0.21.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"tsup": "*",
|