@suss/intent-ir 0.31.2 → 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/dist/index.js CHANGED
@@ -14,6 +14,12 @@ var IntentFindingKindSchema = z.enum([
14
14
  // intent boundary can't be keyed, so it can't be checked
15
15
  "renamedBoundary",
16
16
  // a declared store vanished and an undeclared one of the same system appeared with the same outcomes
17
+ // What the boundary is handed: the `receives` block against the paths
18
+ // the unit reads off its input.
19
+ "unreadInputField",
20
+ // intent declares a field no transition of the unit reads
21
+ "undeclaredInputRead",
22
+ // the unit reads a path the receives block does not list
17
23
  // Outcome intent (kind: prd), scenario link coverage against system
18
24
  // intent. These concretise the proposal's "scenario not linked /
19
25
  // dangling / ambiguous" set (design/proposals/intent-specs.md);
@@ -83,7 +89,9 @@ import {
83
89
  StorageSemanticsSchema,
84
90
  UnitInvocationSemanticsSchema
85
91
  } from "@suss/ir-core";
86
- var IntentSourceSchema = z2.enum(["author", "inferred", "inferred, curated"]).default("author");
92
+ var IntentSourceSchema = z2.enum(["author", "inferred", "inferred, curated"]).default("author").describe(
93
+ "Where the document came from: somebody wrote it, suss inferred it, or suss inferred it and somebody has since curated it."
94
+ );
87
95
  var UNCURATED_SOURCE = "inferred";
88
96
  var CURATED_FIELDS = ["title", "purpose", "audience", "when", "expect"];
89
97
  function blankIn(field) {
@@ -99,6 +107,24 @@ function blanksLeftEmpty(doc, failedFields) {
99
107
  }
100
108
  return CURATED_FIELDS.filter((blank) => blanks.includes(blank));
101
109
  }
110
+ var PLACEHOLDER = "not written yet";
111
+ var SCENARIO_BLANKS = /* @__PURE__ */ new Set(["when", "expect"]);
112
+ function fillBlanks(doc, blanks) {
113
+ const filled = { ...doc };
114
+ for (const blank of blanks.filter((one) => !SCENARIO_BLANKS.has(one))) {
115
+ filled[blank] = PLACEHOLDER;
116
+ }
117
+ const scenarios = filled.scenarios;
118
+ if (!Array.isArray(scenarios)) {
119
+ return filled;
120
+ }
121
+ const inScenario = blanks.filter((one) => SCENARIO_BLANKS.has(one));
122
+ filled.scenarios = scenarios.map((scenario) => ({
123
+ ...scenario,
124
+ ...Object.fromEntries(inScenario.map((blank) => [blank, PLACEHOLDER]))
125
+ }));
126
+ return filled;
127
+ }
102
128
  var PrimitiveTypeName = z2.enum([
103
129
  "string",
104
130
  "integer",
@@ -125,40 +151,91 @@ var BodyShapeSchema = z2.union([
125
151
  required: z2.array(z2.string()).optional()
126
152
  })
127
153
  ]);
128
- var RestBoundarySchema = z2.object({
154
+ var REQUIRED = {
155
+ required: z2.boolean().default(false).describe("Whether the boundary needs this field.")
156
+ };
157
+ var InputFieldSchema = z2.union([
158
+ z2.strictObject({ ...REQUIRED, type: PrimitiveTypeName }),
159
+ z2.strictObject({
160
+ ...REQUIRED,
161
+ type: z2.literal("array"),
162
+ items: ShapeSchema.optional()
163
+ }),
164
+ z2.strictObject({
165
+ ...REQUIRED,
166
+ type: z2.literal("object"),
167
+ properties: z2.record(z2.string(), ShapeSchema).optional()
168
+ }),
169
+ z2.strictObject(REQUIRED)
170
+ ]);
171
+ var ReceivesSchema = z2.record(z2.string().min(1), InputFieldSchema).describe(
172
+ "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."
173
+ ).optional();
174
+ var RestReceivesSchema = z2.strictObject({
175
+ headers: z2.record(z2.string().min(1), InputFieldSchema).describe("The request headers the boundary depends on, by name.").optional(),
176
+ query: z2.record(z2.string().min(1), InputFieldSchema).describe("The query-string parameters the boundary depends on, by name.").optional(),
177
+ params: z2.record(z2.string().min(1), InputFieldSchema).describe("The path parameters the boundary depends on, by name.").optional(),
178
+ body: BodyShapeSchema.describe(
179
+ "The shape of the request body the boundary depends on."
180
+ ).optional()
181
+ }).describe("The parts of the request the boundary depends on.").optional();
182
+ var RestBoundarySchema = z2.strictObject({
129
183
  transport: z2.literal("http").default("http"),
130
184
  semantics: z2.literal("rest"),
131
- method: z2.string().min(1),
132
- path: z2.string().min(1)
185
+ method: z2.string().min(1).describe("The HTTP method the route handles."),
186
+ path: z2.string().min(1).describe("The route path, written the way the framework declares it."),
187
+ receives: RestReceivesSchema
133
188
  });
134
- var FunctionCallBoundarySchema = z2.object({
189
+ var FunctionCallBoundarySchema = z2.strictObject({
135
190
  transport: z2.string().default("in-process"),
136
191
  semantics: z2.literal("function-call"),
137
- /** Repo-relative module path, when the boundary is an intra-repo unit. */
138
- module: z2.string().optional(),
139
- /** Named export within the module / package. */
140
- exportName: z2.string().optional(),
141
- /** Package name when the boundary is a public package export. */
142
- package: z2.string().optional(),
143
- /** Path to the export within the package (sub-path + nested names). */
144
- exportPath: z2.array(z2.string()).optional()
192
+ module: z2.string().describe(
193
+ "The repo-relative module path, when the boundary is a unit inside this repository."
194
+ ).optional(),
195
+ exportName: z2.string().describe("The name the module or package exports the function under.").optional(),
196
+ package: z2.string().describe(
197
+ "The package name, when the boundary is something a package publishes."
198
+ ).optional(),
199
+ exportPath: z2.array(z2.string()).describe(
200
+ "The path to the export inside the package: the sub-path, then any nested names."
201
+ ).optional(),
202
+ receives: ReceivesSchema
145
203
  });
146
- var MessageBusBoundarySchema = z2.object({
204
+ var MessageBusBoundarySchema = z2.strictObject({
147
205
  semantics: z2.literal("message-bus"),
148
- messageBus: MessageBusSemanticsSchema.shape.messageBus,
149
- channel: MessageBusSemanticsSchema.shape.channel.default(null)
206
+ messageBus: MessageBusSemanticsSchema.shape.messageBus.describe(
207
+ "Which bus carries the message, in the name suss gives that bus."
208
+ ),
209
+ channel: MessageBusSemanticsSchema.shape.channel.default(null).describe(
210
+ "The queue or topic the message travels on. Null when the document does not name one."
211
+ ),
212
+ receives: ReceivesSchema
150
213
  });
151
- var StorageBoundarySchema = z2.object({
214
+ var StorageBoundarySchema = z2.strictObject({
152
215
  semantics: z2.literal("storage"),
153
- storageSystem: StorageSemanticsSchema.shape.storageSystem,
154
- scope: StorageSemanticsSchema.shape.scope.default("default"),
155
- container: StorageSemanticsSchema.shape.container.default(null),
156
- accessPath: StorageSemanticsSchema.shape.accessPath.default(null)
216
+ storageSystem: StorageSemanticsSchema.shape.storageSystem.default(null).describe(
217
+ "Which store this is: postgresql, aws.dynamodb, s3, and so on. Null when the document does not say which engine."
218
+ ),
219
+ scope: StorageSemanticsSchema.shape.scope.default("default").describe(
220
+ "The ORM, schema or deployment scope the container is in. A setup with one database uses default."
221
+ ),
222
+ container: StorageSemanticsSchema.shape.container.default(null).describe(
223
+ "The table, bucket, collection or index. Null when the document does not name one."
224
+ ),
225
+ accessPath: StorageSemanticsSchema.shape.accessPath.default(null).describe(
226
+ "A secondary way into the container, such as a DynamoDB index or an Elasticsearch alias. Null means the container's own primary way in."
227
+ ),
228
+ receives: ReceivesSchema
157
229
  });
158
- var UnitInvocationBoundarySchema = z2.object({
230
+ var UnitInvocationBoundarySchema = z2.strictObject({
159
231
  semantics: z2.literal("unit-invocation"),
160
- deploymentTarget: UnitInvocationSemanticsSchema.shape.deploymentTarget,
161
- instanceName: UnitInvocationSemanticsSchema.shape.instanceName.default(null)
232
+ deploymentTarget: UnitInvocationSemanticsSchema.shape.deploymentTarget.describe(
233
+ "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."
234
+ ),
235
+ instanceName: UnitInvocationSemanticsSchema.shape.instanceName.default(null).describe(
236
+ "The name the deployment medium knows the unit by, such as a CloudFormation logical id. Null when the document does not name one."
237
+ ),
238
+ receives: ReceivesSchema
162
239
  });
163
240
  var BoundarySchema = z2.discriminatedUnion("semantics", [
164
241
  RestBoundarySchema,
@@ -168,18 +245,19 @@ var BoundarySchema = z2.discriminatedUnion("semantics", [
168
245
  UnitInvocationBoundarySchema
169
246
  ]);
170
247
  var ResponseOutcomeSchema = z2.object({
171
- status: z2.number().int().min(100).max(599),
172
- body: BodyShapeSchema.optional()
248
+ status: z2.number().int().min(100).max(599).describe("The HTTP status code the response carries."),
249
+ body: BodyShapeSchema.describe("The shape of the response body.").optional()
173
250
  });
174
251
  var ReturnsOutcomeSchema = z2.object({
175
- body: BodyShapeSchema.optional()
252
+ body: BodyShapeSchema.describe("The shape of the returned value.").optional()
176
253
  });
177
254
  var ThrowsOutcomeSchema = z2.object({
178
- errorType: z2.string().optional()
255
+ errorType: z2.string().describe("The name of the error class this outcome raises.").optional()
179
256
  });
180
257
  function emptyIfNull(schema) {
181
- return z2.preprocess((v) => v === null ? {} : v, schema);
258
+ return z2.preprocess((v) => v === null ? {} : v, schema).meta({ [ACCEPTS_NULL]: true });
182
259
  }
260
+ var ACCEPTS_NULL = "x-suss-accepts-null";
183
261
  var ONE_OR_MORE = z2.union([
184
262
  z2.string().min(1),
185
263
  z2.array(z2.string().min(1)).min(1)
@@ -229,13 +307,19 @@ var WhenSchema = z2.union([
229
307
  z2.string().min(1),
230
308
  z2.array(z2.union([z2.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
231
309
  ]);
232
- var BoundaryTransitionSchema = z2.object({
233
- id: z2.string().min(1),
234
- when: WhenSchema,
235
- response: emptyIfNull(ResponseOutcomeSchema).optional(),
236
- returns: emptyIfNull(ReturnsOutcomeSchema).optional(),
237
- throws: emptyIfNull(ThrowsOutcomeSchema).optional(),
238
- results: z2.array(EffectOutcomeSchema).min(1).optional()
310
+ var BoundaryTransitionSchema = z2.strictObject({
311
+ id: z2.string().min(1).describe(
312
+ "The outcome's name. A PRD scenario links to it as <intent-name>.<id>."
313
+ ),
314
+ when: WhenSchema.describe(
315
+ "What has to hold for this outcome, either as a list of clauses or as one sentence."
316
+ ),
317
+ response: emptyIfNull(ResponseOutcomeSchema).describe("This outcome sends an HTTP response.").optional(),
318
+ returns: emptyIfNull(ReturnsOutcomeSchema).describe("This outcome returns a value to its caller.").optional(),
319
+ throws: emptyIfNull(ThrowsOutcomeSchema).describe("This outcome raises an error.").optional(),
320
+ results: z2.array(EffectOutcomeSchema).min(1).describe(
321
+ "The effects this outcome has, written in the same verbs suss ask uses: reads, writes, invokes."
322
+ ).optional()
239
323
  }).refine((t) => endingsOf(t).length <= 1, {
240
324
  message: "a transition ends one way: give it at most one of response, returns, or throws"
241
325
  }).refine((t) => endingsOf(t).length === 1 || t.results !== void 0, {
@@ -244,22 +328,23 @@ var BoundaryTransitionSchema = z2.object({
244
328
  function endingsOf(t) {
245
329
  return [t.response, t.returns, t.throws].filter((o) => o !== void 0);
246
330
  }
247
- var BoundaryIntentSchema = z2.object({
248
- kind: z2.literal("boundary"),
249
- /** Name PRDs reference outcomes through: `<name>.<transition-id>`. */
250
- name: z2.string().min(1),
251
- purpose: z2.string().min(1),
252
- audience: z2.string().min(1),
331
+ var BoundaryIntentSchema = z2.strictObject({
332
+ kind: z2.literal("boundary").describe("Makes this document boundary intent for one boundary."),
333
+ name: z2.string().min(1).describe(
334
+ "What this document is called. A PRD scenario links to an outcome of it as <name>.<outcome-id>."
335
+ ),
336
+ purpose: z2.string().min(1).describe("What the boundary is for, in the author's own words."),
337
+ audience: z2.string().min(1).describe("Who calls this boundary and depends on what it does."),
253
338
  source: IntentSourceSchema,
254
- boundary: BoundarySchema,
255
- transitions: z2.array(BoundaryTransitionSchema).min(1)
339
+ boundary: BoundarySchema.describe(
340
+ "Which boundary in the code the document is about."
341
+ ),
342
+ transitions: z2.array(BoundaryTransitionSchema).min(1).describe("Every outcome the boundary can produce, one entry each.")
256
343
  });
257
- var PrdScenarioSchema = z2.object({
258
- title: z2.string().min(1).optional(),
259
- /** The condition, in the author's terms. */
260
- when: z2.string().min(1),
261
- /** The expected outcome, in the author's terms. Always present. */
262
- expect: z2.string().min(1),
344
+ var PrdScenarioSchema = z2.strictObject({
345
+ title: z2.string().min(1).describe("A short name for the scenario.").optional(),
346
+ when: z2.string().min(1).describe("The condition the scenario covers, in the author's own words."),
347
+ expect: z2.string().min(1).describe("What should happen then, in the author's own words."),
263
348
  /**
264
349
  * Optional structured link(s) to system-intent outcomes
265
350
  * (`<intent-name>.<outcome-id>`). A scenario without `link` is a
@@ -271,15 +356,17 @@ var PrdScenarioSchema = z2.object({
271
356
  * `link`, not `then`, because a data object with a `then` property is
272
357
  * treated as a thenable by Promise resolution, a latent footgun.)
273
358
  */
274
- link: z2.union([z2.string().min(1), z2.array(z2.string().min(1)).min(1)]).optional()
359
+ link: z2.union([z2.string().min(1), z2.array(z2.string().min(1)).min(1)]).describe(
360
+ "The boundary-intent outcomes this scenario is about, each written <intent-name>.<outcome-id>."
361
+ ).optional()
275
362
  });
276
- var PrdSchema = z2.object({
277
- kind: z2.literal("prd"),
278
- title: z2.string().min(1),
279
- purpose: z2.string().min(1),
280
- audience: z2.string().min(1),
363
+ var PrdSchema = z2.strictObject({
364
+ kind: z2.literal("prd").describe("Makes this document a PRD, a set of scenarios for a feature."),
365
+ title: z2.string().min(1).describe("What this document is called."),
366
+ purpose: z2.string().min(1).describe("What the feature is for, in the author's own words."),
367
+ audience: z2.string().min(1).describe("Who the feature is for."),
281
368
  source: IntentSourceSchema,
282
- scenarios: z2.array(PrdScenarioSchema).min(1)
369
+ scenarios: z2.array(PrdScenarioSchema).min(1).describe("The situations the feature covers, one entry each.")
283
370
  });
284
371
  var IntentDocSchema = z2.discriminatedUnion("kind", [
285
372
  BoundaryIntentSchema,
@@ -309,6 +396,7 @@ function boundaryIntentToSummary(doc) {
309
396
  audience: doc.audience,
310
397
  source: doc.source,
311
398
  boundary: toBoundaryBinding(doc.boundary),
399
+ receives: toReceives(doc.boundary),
312
400
  outcomes: doc.transitions.map(toOutcome)
313
401
  };
314
402
  }
@@ -364,6 +452,67 @@ function toBoundaryBinding(boundary) {
364
452
  const build = BINDINGS[boundary.semantics];
365
453
  return build(boundary);
366
454
  }
455
+ function toField(name, field) {
456
+ return {
457
+ path: name.split("."),
458
+ shape: fieldShape(field),
459
+ required: field.required
460
+ };
461
+ }
462
+ function fieldShape(field) {
463
+ if (!("type" in field)) {
464
+ return null;
465
+ }
466
+ const { required: _required, ...shape } = field;
467
+ return shapeToTypeShape(shape);
468
+ }
469
+ function dottedReceives(receives) {
470
+ return Object.entries(receives ?? {}).map(
471
+ ([name, field]) => toField(name, field)
472
+ );
473
+ }
474
+ var REST_SECTIONS = ["headers", "query", "params"];
475
+ function restReceives(boundary) {
476
+ const receives = boundary.receives;
477
+ if (receives === void 0) {
478
+ return [];
479
+ }
480
+ const fields = REST_SECTIONS.flatMap(
481
+ (section) => Object.entries(receives[section] ?? {}).map(
482
+ ([name, field]) => toField(`${section}.${name}`, field)
483
+ )
484
+ );
485
+ return [...fields, ...bodyFields(receives.body)];
486
+ }
487
+ function bodyFields(body) {
488
+ if (body === void 0) {
489
+ return [];
490
+ }
491
+ const shape = bodyToTypeShape(body);
492
+ if (shape === null) {
493
+ return [];
494
+ }
495
+ if (shape.type !== "record") {
496
+ return [{ path: ["body"], shape, required: false }];
497
+ }
498
+ const required = new Set("required" in body ? body.required ?? [] : []);
499
+ return Object.entries(shape.properties).map(([name, property]) => ({
500
+ path: ["body", name],
501
+ shape: property,
502
+ required: required.has(name)
503
+ }));
504
+ }
505
+ var RECEIVES = {
506
+ rest: restReceives,
507
+ "function-call": (boundary) => dottedReceives(boundary.receives),
508
+ "message-bus": (boundary) => dottedReceives(boundary.receives),
509
+ storage: (boundary) => dottedReceives(boundary.receives),
510
+ "unit-invocation": (boundary) => dottedReceives(boundary.receives)
511
+ };
512
+ function toReceives(boundary) {
513
+ const read = RECEIVES[boundary.semantics];
514
+ return read(boundary);
515
+ }
367
516
  var VERBS = EffectRelationSchema2.options;
368
517
  function toEffect(declared) {
369
518
  const [does, names] = Object.entries(declared).find(
@@ -492,6 +641,7 @@ var PRIMITIVE_TYPE_SHAPES = {
492
641
  unknown: { type: "unknown" }
493
642
  };
494
643
  export {
644
+ ACCEPTS_NULL,
495
645
  BodyShapeSchema,
496
646
  BoundarySchema,
497
647
  IntentDocSchema,
@@ -502,6 +652,8 @@ export {
502
652
  IntentRefSchema,
503
653
  IntentSourceSchema,
504
654
  blanksLeftEmpty,
655
+ fillBlanks,
505
656
  intentDocToSummary,
506
- toBoundaryBinding
657
+ toBoundaryBinding,
658
+ toReceives
507
659
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/intent-ir",
3
- "version": "0.31.2",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "description": "The types for the intent your team writes down, product outcomes and system behavior, in a shape suss can check the code against.",
6
6
  "keywords": [
@@ -30,15 +30,19 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "import": "./dist/index.js",
32
32
  "require": "./dist/index.cjs"
33
- }
33
+ },
34
+ "./schema/intent-doc.schema.json": "./schema/intent-doc.schema.json",
35
+ "./package.json": "./package.json"
34
36
  },
35
37
  "files": [
36
38
  "dist",
39
+ "schema",
37
40
  "README.md",
38
41
  "LICENSE"
39
42
  ],
40
43
  "scripts": {
41
- "build": "tsup",
44
+ "build": "tsup && node scripts/generate-schema.mjs && npx biome format --write schema/intent-doc.schema.json",
45
+ "schema:generate": "node scripts/generate-schema.mjs && npx biome format --write schema/intent-doc.schema.json",
42
46
  "test": "vitest run",
43
47
  "test:coverage": "vitest run --coverage",
44
48
  "lint": "biome check .",
@@ -49,12 +53,14 @@
49
53
  "zod": "^4.0.0"
50
54
  },
51
55
  "dependencies": {
52
- "@suss/ir-core": "0.31.2"
56
+ "@suss/ir-core": "0.33.0"
53
57
  },
54
58
  "devDependencies": {
59
+ "ajv": "^8.20.0",
55
60
  "tsup": "*",
56
61
  "typescript": "*",
57
62
  "vitest": "*",
63
+ "yaml": "^2.4.0",
58
64
  "zod": "^4.3.6"
59
65
  },
60
66
  "publishConfig": {