@suss/intent-ir 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 **or** function-call; each outcome is a `response` (status + body), a `returns` (a function/handler return value), or a `throws` (an error). The function-call form is what lets suss check its own non-HTTP surface.
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 same way, even when the invoking code reaches it through an env var: `deploymentOf` in `@suss/behavioral-ir` says which resource the template points that variable at, and the drafter and the intent checker both go through it, so a document says which function rather than which variable. A run with no template in it has nothing to put in, and then the line keeps the code's own spelling, `unit:lambda {ARCHIVE_WORKER_FUNCTION}`, and the document's header says which variable it is waiting on. A store the code reaches through a variable works the same way.
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 + function-call system intent, PRD outcome intent with optional links. The checker integration and reader migration build on this.
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
- intentDocToSummary: () => intentDocToSummary
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: import_zod2.z.string().min(1),
264
+ when: WhenSchema,
174
265
  response: emptyIfNull(ResponseOutcomeSchema).optional(),
175
266
  returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
176
- throws: emptyIfNull(ThrowsOutcomeSchema).optional()
177
- }).refine(
178
- (t) => [t.response, t.returns, t.throws].filter((o) => o !== void 0).length === 1,
179
- {
180
- message: "each transition must declare exactly one outcome: response, returns, or throws"
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 import_ir_core = require("@suss/ir-core");
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
- function toBoundaryBinding(boundary) {
260
- if (boundary.semantics === "rest") {
261
- return (0, import_ir_core.restBinding)({
262
- transport: boundary.transport,
263
- method: boundary.method,
264
- path: boundary.path,
265
- recognition: "intent"
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 base = { id: t.id, when: t.when };
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
- return {
298
- ...base,
299
- kind: "throw",
300
- status: null,
301
- body: null,
302
- errorType: t.throws?.errorType ?? null
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
- intentDocToSummary
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
- /** How a boundary outcome resolves, the cross-kind unification. */
283
- type IntentOutcomeKind = "response" | "return" | "throw";
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
- /** The condition, in human terms (opaque to the checker). */
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
- /** How a boundary outcome resolves, the cross-kind unification. */
283
- type IntentOutcomeKind = "response" | "return" | "throw";
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
- /** The condition, in human terms (opaque to the checker). */
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: z2.string().min(1),
232
+ when: WhenSchema,
139
233
  response: emptyIfNull(ResponseOutcomeSchema).optional(),
140
234
  returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
141
- throws: emptyIfNull(ThrowsOutcomeSchema).optional()
142
- }).refine(
143
- (t) => [t.response, t.returns, t.throws].filter((o) => o !== void 0).length === 1,
144
- {
145
- message: "each transition must declare exactly one outcome: response, returns, or throws"
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
- restBinding
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
- function toBoundaryBinding(boundary) {
228
- if (boundary.semantics === "rest") {
229
- return restBinding({
230
- transport: boundary.transport,
231
- method: boundary.method,
232
- path: boundary.path,
233
- recognition: "intent"
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 base = { id: t.id, when: t.when };
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
- return {
266
- ...base,
267
- kind: "throw",
268
- status: null,
269
- body: null,
270
- errorType: t.throws?.errorType ?? null
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
- intentDocToSummary
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.20.0",
3
+ "version": "0.21.1",
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.20.0"
50
+ "@suss/ir-core": "0.21.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "tsup": "*",