@suss/intent-ir 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,11 +19,13 @@ It is a peer of `@suss/behavioral-ir`; both build on `@suss/ir-core`. Readers (e
19
19
 
20
20
  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.
21
21
 
22
+ The file format is also published as a [JSON Schema](schema/intent-doc.schema.json), generated from the zod schemas at build time, so an editor can check a document and a tool in another language can read one without depending on this package. [Intent format](../../docs/reference/intent-format.md) goes through both document kinds field by field.
23
+
22
24
  ## More
23
25
 
24
26
  - [What an intent document states](./DESIGN.md)
25
- - [Documentation](https://nimbuscloud-ai.github.io/suss/)
26
- - [Every package and pack suss ships](https://nimbuscloud-ai.github.io/suss/reference/packages)
27
+ - [Documentation](https://suss.sh/)
28
+ - [Every package and pack suss ships](https://suss.sh/packs/catalog)
27
29
  - [Source and issues](https://github.com/nimbuscloud-ai/suss)
28
30
 
29
31
  ## Coverage
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACCEPTS_NULL: () => ACCEPTS_NULL,
23
24
  BodyShapeSchema: () => BodyShapeSchema,
24
25
  BoundarySchema: () => BoundarySchema,
25
26
  IntentDocSchema: () => IntentDocSchema,
@@ -30,8 +31,10 @@ __export(index_exports, {
30
31
  IntentRefSchema: () => IntentRefSchema,
31
32
  IntentSourceSchema: () => IntentSourceSchema,
32
33
  blanksLeftEmpty: () => blanksLeftEmpty,
34
+ fillBlanks: () => fillBlanks,
33
35
  intentDocToSummary: () => intentDocToSummary,
34
- toBoundaryBinding: () => toBoundaryBinding
36
+ toBoundaryBinding: () => toBoundaryBinding,
37
+ toReceives: () => toReceives
35
38
  });
36
39
  module.exports = __toCommonJS(index_exports);
37
40
 
@@ -51,6 +54,12 @@ var IntentFindingKindSchema = import_zod.z.enum([
51
54
  // intent boundary can't be keyed, so it can't be checked
52
55
  "renamedBoundary",
53
56
  // a declared store vanished and an undeclared one of the same system appeared with the same outcomes
57
+ // What the boundary is handed: the `receives` block against the paths
58
+ // the unit reads off its input.
59
+ "unreadInputField",
60
+ // intent declares a field no transition of the unit reads
61
+ "undeclaredInputRead",
62
+ // the unit reads a path the receives block does not list
54
63
  // Outcome intent (kind: prd), scenario link coverage against system
55
64
  // intent. These concretise the proposal's "scenario not linked /
56
65
  // dangling / ambiguous" set (design/proposals/intent-specs.md);
@@ -115,7 +124,9 @@ var IntentFindingSchema = import_zod.z.object({
115
124
  // src/schema.ts
116
125
  var import_zod2 = require("zod");
117
126
  var import_ir_core = require("@suss/ir-core");
118
- var IntentSourceSchema = import_zod2.z.enum(["author", "inferred", "inferred, curated"]).default("author");
127
+ var IntentSourceSchema = import_zod2.z.enum(["author", "inferred", "inferred, curated"]).default("author").describe(
128
+ "Where the document came from: somebody wrote it, suss inferred it, or suss inferred it and somebody has since curated it."
129
+ );
119
130
  var UNCURATED_SOURCE = "inferred";
120
131
  var CURATED_FIELDS = ["title", "purpose", "audience", "when", "expect"];
121
132
  function blankIn(field) {
@@ -131,6 +142,24 @@ function blanksLeftEmpty(doc, failedFields) {
131
142
  }
132
143
  return CURATED_FIELDS.filter((blank) => blanks.includes(blank));
133
144
  }
145
+ var PLACEHOLDER = "not written yet";
146
+ var SCENARIO_BLANKS = /* @__PURE__ */ new Set(["when", "expect"]);
147
+ function fillBlanks(doc, blanks) {
148
+ const filled = { ...doc };
149
+ for (const blank of blanks.filter((one) => !SCENARIO_BLANKS.has(one))) {
150
+ filled[blank] = PLACEHOLDER;
151
+ }
152
+ const scenarios = filled.scenarios;
153
+ if (!Array.isArray(scenarios)) {
154
+ return filled;
155
+ }
156
+ const inScenario = blanks.filter((one) => SCENARIO_BLANKS.has(one));
157
+ filled.scenarios = scenarios.map((scenario) => ({
158
+ ...scenario,
159
+ ...Object.fromEntries(inScenario.map((blank) => [blank, PLACEHOLDER]))
160
+ }));
161
+ return filled;
162
+ }
134
163
  var PrimitiveTypeName = import_zod2.z.enum([
135
164
  "string",
136
165
  "integer",
@@ -157,40 +186,91 @@ var BodyShapeSchema = import_zod2.z.union([
157
186
  required: import_zod2.z.array(import_zod2.z.string()).optional()
158
187
  })
159
188
  ]);
160
- var RestBoundarySchema = import_zod2.z.object({
189
+ var REQUIRED = {
190
+ required: import_zod2.z.boolean().default(false).describe("Whether the boundary needs this field.")
191
+ };
192
+ var InputFieldSchema = import_zod2.z.union([
193
+ import_zod2.z.strictObject({ ...REQUIRED, type: PrimitiveTypeName }),
194
+ import_zod2.z.strictObject({
195
+ ...REQUIRED,
196
+ type: import_zod2.z.literal("array"),
197
+ items: ShapeSchema.optional()
198
+ }),
199
+ import_zod2.z.strictObject({
200
+ ...REQUIRED,
201
+ type: import_zod2.z.literal("object"),
202
+ properties: import_zod2.z.record(import_zod2.z.string(), ShapeSchema).optional()
203
+ }),
204
+ import_zod2.z.strictObject(REQUIRED)
205
+ ]);
206
+ var ReceivesSchema = import_zod2.z.record(import_zod2.z.string().min(1), InputFieldSchema).describe(
207
+ "The fields the boundary is handed, by field name. On a function-call boundary a name is a parameter, on a message bus it is a field of the message body."
208
+ ).optional();
209
+ var RestReceivesSchema = import_zod2.z.strictObject({
210
+ headers: import_zod2.z.record(import_zod2.z.string().min(1), InputFieldSchema).describe("The request headers the boundary depends on, by name.").optional(),
211
+ query: import_zod2.z.record(import_zod2.z.string().min(1), InputFieldSchema).describe("The query-string parameters the boundary depends on, by name.").optional(),
212
+ params: import_zod2.z.record(import_zod2.z.string().min(1), InputFieldSchema).describe("The path parameters the boundary depends on, by name.").optional(),
213
+ body: BodyShapeSchema.describe(
214
+ "The shape of the request body the boundary depends on."
215
+ ).optional()
216
+ }).describe("The parts of the request the boundary depends on.").optional();
217
+ var RestBoundarySchema = import_zod2.z.strictObject({
161
218
  transport: import_zod2.z.literal("http").default("http"),
162
219
  semantics: import_zod2.z.literal("rest"),
163
- method: import_zod2.z.string().min(1),
164
- path: import_zod2.z.string().min(1)
220
+ method: import_zod2.z.string().min(1).describe("The HTTP method the route handles."),
221
+ path: import_zod2.z.string().min(1).describe("The route path, written the way the framework declares it."),
222
+ receives: RestReceivesSchema
165
223
  });
166
- var FunctionCallBoundarySchema = import_zod2.z.object({
224
+ var FunctionCallBoundarySchema = import_zod2.z.strictObject({
167
225
  transport: import_zod2.z.string().default("in-process"),
168
226
  semantics: import_zod2.z.literal("function-call"),
169
- /** Repo-relative module path, when the boundary is an intra-repo unit. */
170
- module: import_zod2.z.string().optional(),
171
- /** Named export within the module / package. */
172
- exportName: import_zod2.z.string().optional(),
173
- /** Package name when the boundary is a public package export. */
174
- package: import_zod2.z.string().optional(),
175
- /** Path to the export within the package (sub-path + nested names). */
176
- exportPath: import_zod2.z.array(import_zod2.z.string()).optional()
227
+ module: import_zod2.z.string().describe(
228
+ "The repo-relative module path, when the boundary is a unit inside this repository."
229
+ ).optional(),
230
+ exportName: import_zod2.z.string().describe("The name the module or package exports the function under.").optional(),
231
+ package: import_zod2.z.string().describe(
232
+ "The package name, when the boundary is something a package publishes."
233
+ ).optional(),
234
+ exportPath: import_zod2.z.array(import_zod2.z.string()).describe(
235
+ "The path to the export inside the package: the sub-path, then any nested names."
236
+ ).optional(),
237
+ receives: ReceivesSchema
177
238
  });
178
- var MessageBusBoundarySchema = import_zod2.z.object({
239
+ var MessageBusBoundarySchema = import_zod2.z.strictObject({
179
240
  semantics: import_zod2.z.literal("message-bus"),
180
- messageBus: import_ir_core.MessageBusSemanticsSchema.shape.messageBus,
181
- channel: import_ir_core.MessageBusSemanticsSchema.shape.channel.default(null)
241
+ messageBus: import_ir_core.MessageBusSemanticsSchema.shape.messageBus.describe(
242
+ "Which bus carries the message, in the name suss gives that bus."
243
+ ),
244
+ channel: import_ir_core.MessageBusSemanticsSchema.shape.channel.default(null).describe(
245
+ "The queue or topic the message travels on. Null when the document does not name one."
246
+ ),
247
+ receives: ReceivesSchema
182
248
  });
183
- var StorageBoundarySchema = import_zod2.z.object({
249
+ var StorageBoundarySchema = import_zod2.z.strictObject({
184
250
  semantics: import_zod2.z.literal("storage"),
185
- storageSystem: import_ir_core.StorageSemanticsSchema.shape.storageSystem,
186
- scope: import_ir_core.StorageSemanticsSchema.shape.scope.default("default"),
187
- container: import_ir_core.StorageSemanticsSchema.shape.container.default(null),
188
- accessPath: import_ir_core.StorageSemanticsSchema.shape.accessPath.default(null)
251
+ storageSystem: import_ir_core.StorageSemanticsSchema.shape.storageSystem.default(null).describe(
252
+ "Which store this is: postgresql, aws.dynamodb, s3, and so on. Null when the document does not say which engine."
253
+ ),
254
+ scope: import_ir_core.StorageSemanticsSchema.shape.scope.default("default").describe(
255
+ "The ORM, schema or deployment scope the container is in. A setup with one database uses default."
256
+ ),
257
+ container: import_ir_core.StorageSemanticsSchema.shape.container.default(null).describe(
258
+ "The table, bucket, collection or index. Null when the document does not name one."
259
+ ),
260
+ accessPath: import_ir_core.StorageSemanticsSchema.shape.accessPath.default(null).describe(
261
+ "A secondary way into the container, such as a DynamoDB index or an Elasticsearch alias. Null means the container's own primary way in."
262
+ ),
263
+ receives: ReceivesSchema
189
264
  });
190
- var UnitInvocationBoundarySchema = import_zod2.z.object({
265
+ var UnitInvocationBoundarySchema = import_zod2.z.strictObject({
191
266
  semantics: import_zod2.z.literal("unit-invocation"),
192
- deploymentTarget: import_ir_core.UnitInvocationSemanticsSchema.shape.deploymentTarget,
193
- instanceName: import_ir_core.UnitInvocationSemanticsSchema.shape.instanceName.default(null)
267
+ deploymentTarget: import_ir_core.UnitInvocationSemanticsSchema.shape.deploymentTarget.describe(
268
+ "What sort of deployed thing this is: a Lambda function, an ECS task's container, a plain container, a k8s deployment or an edge worker."
269
+ ),
270
+ instanceName: import_ir_core.UnitInvocationSemanticsSchema.shape.instanceName.default(null).describe(
271
+ "The name the deployment medium knows the unit by, such as a CloudFormation logical id. Null when the document does not name one."
272
+ ),
273
+ receives: ReceivesSchema
194
274
  });
195
275
  var BoundarySchema = import_zod2.z.discriminatedUnion("semantics", [
196
276
  RestBoundarySchema,
@@ -200,18 +280,19 @@ var BoundarySchema = import_zod2.z.discriminatedUnion("semantics", [
200
280
  UnitInvocationBoundarySchema
201
281
  ]);
202
282
  var ResponseOutcomeSchema = import_zod2.z.object({
203
- status: import_zod2.z.number().int().min(100).max(599),
204
- body: BodyShapeSchema.optional()
283
+ status: import_zod2.z.number().int().min(100).max(599).describe("The HTTP status code the response carries."),
284
+ body: BodyShapeSchema.describe("The shape of the response body.").optional()
205
285
  });
206
286
  var ReturnsOutcomeSchema = import_zod2.z.object({
207
- body: BodyShapeSchema.optional()
287
+ body: BodyShapeSchema.describe("The shape of the returned value.").optional()
208
288
  });
209
289
  var ThrowsOutcomeSchema = import_zod2.z.object({
210
- errorType: import_zod2.z.string().optional()
290
+ errorType: import_zod2.z.string().describe("The name of the error class this outcome raises.").optional()
211
291
  });
212
292
  function emptyIfNull(schema) {
213
- return import_zod2.z.preprocess((v) => v === null ? {} : v, schema);
293
+ return import_zod2.z.preprocess((v) => v === null ? {} : v, schema).meta({ [ACCEPTS_NULL]: true });
214
294
  }
295
+ var ACCEPTS_NULL = "x-suss-accepts-null";
215
296
  var ONE_OR_MORE = import_zod2.z.union([
216
297
  import_zod2.z.string().min(1),
217
298
  import_zod2.z.array(import_zod2.z.string().min(1)).min(1)
@@ -261,13 +342,19 @@ var WhenSchema = import_zod2.z.union([
261
342
  import_zod2.z.string().min(1),
262
343
  import_zod2.z.array(import_zod2.z.union([import_zod2.z.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
263
344
  ]);
264
- var BoundaryTransitionSchema = import_zod2.z.object({
265
- id: import_zod2.z.string().min(1),
266
- when: WhenSchema,
267
- response: emptyIfNull(ResponseOutcomeSchema).optional(),
268
- returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
269
- throws: emptyIfNull(ThrowsOutcomeSchema).optional(),
270
- results: import_zod2.z.array(EffectOutcomeSchema).min(1).optional()
345
+ var BoundaryTransitionSchema = import_zod2.z.strictObject({
346
+ id: import_zod2.z.string().min(1).describe(
347
+ "The outcome's name. A PRD scenario links to it as <intent-name>.<id>."
348
+ ),
349
+ when: WhenSchema.describe(
350
+ "What has to hold for this outcome, either as a list of clauses or as one sentence."
351
+ ),
352
+ response: emptyIfNull(ResponseOutcomeSchema).describe("This outcome sends an HTTP response.").optional(),
353
+ returns: emptyIfNull(ReturnsOutcomeSchema).describe("This outcome returns a value to its caller.").optional(),
354
+ throws: emptyIfNull(ThrowsOutcomeSchema).describe("This outcome raises an error.").optional(),
355
+ results: import_zod2.z.array(EffectOutcomeSchema).min(1).describe(
356
+ "The effects this outcome has, written in the same verbs suss ask uses: reads, writes, invokes."
357
+ ).optional()
271
358
  }).refine((t) => endingsOf(t).length <= 1, {
272
359
  message: "a transition ends one way: give it at most one of response, returns, or throws"
273
360
  }).refine((t) => endingsOf(t).length === 1 || t.results !== void 0, {
@@ -276,22 +363,23 @@ var BoundaryTransitionSchema = import_zod2.z.object({
276
363
  function endingsOf(t) {
277
364
  return [t.response, t.returns, t.throws].filter((o) => o !== void 0);
278
365
  }
279
- var BoundaryIntentSchema = import_zod2.z.object({
280
- kind: import_zod2.z.literal("boundary"),
281
- /** Name PRDs reference outcomes through: `<name>.<transition-id>`. */
282
- name: import_zod2.z.string().min(1),
283
- purpose: import_zod2.z.string().min(1),
284
- audience: import_zod2.z.string().min(1),
366
+ var BoundaryIntentSchema = import_zod2.z.strictObject({
367
+ kind: import_zod2.z.literal("boundary").describe("Makes this document boundary intent for one boundary."),
368
+ name: import_zod2.z.string().min(1).describe(
369
+ "What this document is called. A PRD scenario links to an outcome of it as <name>.<outcome-id>."
370
+ ),
371
+ purpose: import_zod2.z.string().min(1).describe("What the boundary is for, in the author's own words."),
372
+ audience: import_zod2.z.string().min(1).describe("Who calls this boundary and depends on what it does."),
285
373
  source: IntentSourceSchema,
286
- boundary: BoundarySchema,
287
- transitions: import_zod2.z.array(BoundaryTransitionSchema).min(1)
374
+ boundary: BoundarySchema.describe(
375
+ "Which boundary in the code the document is about."
376
+ ),
377
+ transitions: import_zod2.z.array(BoundaryTransitionSchema).min(1).describe("Every outcome the boundary can produce, one entry each.")
288
378
  });
289
- var PrdScenarioSchema = import_zod2.z.object({
290
- title: import_zod2.z.string().min(1).optional(),
291
- /** The condition, in the author's terms. */
292
- when: import_zod2.z.string().min(1),
293
- /** The expected outcome, in the author's terms. Always present. */
294
- expect: import_zod2.z.string().min(1),
379
+ var PrdScenarioSchema = import_zod2.z.strictObject({
380
+ title: import_zod2.z.string().min(1).describe("A short name for the scenario.").optional(),
381
+ when: import_zod2.z.string().min(1).describe("The condition the scenario covers, in the author's own words."),
382
+ expect: import_zod2.z.string().min(1).describe("What should happen then, in the author's own words."),
295
383
  /**
296
384
  * Optional structured link(s) to system-intent outcomes
297
385
  * (`<intent-name>.<outcome-id>`). A scenario without `link` is a
@@ -303,15 +391,17 @@ var PrdScenarioSchema = import_zod2.z.object({
303
391
  * `link`, not `then`, because a data object with a `then` property is
304
392
  * treated as a thenable by Promise resolution, a latent footgun.)
305
393
  */
306
- link: import_zod2.z.union([import_zod2.z.string().min(1), import_zod2.z.array(import_zod2.z.string().min(1)).min(1)]).optional()
394
+ link: import_zod2.z.union([import_zod2.z.string().min(1), import_zod2.z.array(import_zod2.z.string().min(1)).min(1)]).describe(
395
+ "The boundary-intent outcomes this scenario is about, each written <intent-name>.<outcome-id>."
396
+ ).optional()
307
397
  });
308
- var PrdSchema = import_zod2.z.object({
309
- kind: import_zod2.z.literal("prd"),
310
- title: import_zod2.z.string().min(1),
311
- purpose: import_zod2.z.string().min(1),
312
- audience: import_zod2.z.string().min(1),
398
+ var PrdSchema = import_zod2.z.strictObject({
399
+ kind: import_zod2.z.literal("prd").describe("Makes this document a PRD, a set of scenarios for a feature."),
400
+ title: import_zod2.z.string().min(1).describe("What this document is called."),
401
+ purpose: import_zod2.z.string().min(1).describe("What the feature is for, in the author's own words."),
402
+ audience: import_zod2.z.string().min(1).describe("Who the feature is for."),
313
403
  source: IntentSourceSchema,
314
- scenarios: import_zod2.z.array(PrdScenarioSchema).min(1)
404
+ scenarios: import_zod2.z.array(PrdScenarioSchema).min(1).describe("The situations the feature covers, one entry each.")
315
405
  });
316
406
  var IntentDocSchema = import_zod2.z.discriminatedUnion("kind", [
317
407
  BoundaryIntentSchema,
@@ -334,6 +424,7 @@ function boundaryIntentToSummary(doc) {
334
424
  audience: doc.audience,
335
425
  source: doc.source,
336
426
  boundary: toBoundaryBinding(doc.boundary),
427
+ receives: toReceives(doc.boundary),
337
428
  outcomes: doc.transitions.map(toOutcome)
338
429
  };
339
430
  }
@@ -389,6 +480,67 @@ function toBoundaryBinding(boundary) {
389
480
  const build = BINDINGS[boundary.semantics];
390
481
  return build(boundary);
391
482
  }
483
+ function toField(name, field) {
484
+ return {
485
+ path: name.split("."),
486
+ shape: fieldShape(field),
487
+ required: field.required
488
+ };
489
+ }
490
+ function fieldShape(field) {
491
+ if (!("type" in field)) {
492
+ return null;
493
+ }
494
+ const { required: _required, ...shape } = field;
495
+ return shapeToTypeShape(shape);
496
+ }
497
+ function dottedReceives(receives) {
498
+ return Object.entries(receives ?? {}).map(
499
+ ([name, field]) => toField(name, field)
500
+ );
501
+ }
502
+ var REST_SECTIONS = ["headers", "query", "params"];
503
+ function restReceives(boundary) {
504
+ const receives = boundary.receives;
505
+ if (receives === void 0) {
506
+ return [];
507
+ }
508
+ const fields = REST_SECTIONS.flatMap(
509
+ (section) => Object.entries(receives[section] ?? {}).map(
510
+ ([name, field]) => toField(`${section}.${name}`, field)
511
+ )
512
+ );
513
+ return [...fields, ...bodyFields(receives.body)];
514
+ }
515
+ function bodyFields(body) {
516
+ if (body === void 0) {
517
+ return [];
518
+ }
519
+ const shape = bodyToTypeShape(body);
520
+ if (shape === null) {
521
+ return [];
522
+ }
523
+ if (shape.type !== "record") {
524
+ return [{ path: ["body"], shape, required: false }];
525
+ }
526
+ const required = new Set("required" in body ? body.required ?? [] : []);
527
+ return Object.entries(shape.properties).map(([name, property]) => ({
528
+ path: ["body", name],
529
+ shape: property,
530
+ required: required.has(name)
531
+ }));
532
+ }
533
+ var RECEIVES = {
534
+ rest: restReceives,
535
+ "function-call": (boundary) => dottedReceives(boundary.receives),
536
+ "message-bus": (boundary) => dottedReceives(boundary.receives),
537
+ storage: (boundary) => dottedReceives(boundary.receives),
538
+ "unit-invocation": (boundary) => dottedReceives(boundary.receives)
539
+ };
540
+ function toReceives(boundary) {
541
+ const read = RECEIVES[boundary.semantics];
542
+ return read(boundary);
543
+ }
392
544
  var VERBS = import_ir_core2.EffectRelationSchema.options;
393
545
  function toEffect(declared) {
394
546
  const [does, names] = Object.entries(declared).find(
@@ -518,6 +670,7 @@ var PRIMITIVE_TYPE_SHAPES = {
518
670
  };
519
671
  // Annotate the CommonJS export names for ESM import in node:
520
672
  0 && (module.exports = {
673
+ ACCEPTS_NULL,
521
674
  BodyShapeSchema,
522
675
  BoundarySchema,
523
676
  IntentDocSchema,
@@ -528,6 +681,8 @@ var PRIMITIVE_TYPE_SHAPES = {
528
681
  IntentRefSchema,
529
682
  IntentSourceSchema,
530
683
  blanksLeftEmpty,
684
+ fillBlanks,
531
685
  intentDocToSummary,
532
- toBoundaryBinding
686
+ toBoundaryBinding,
687
+ toReceives
533
688
  });