@suss/intent-ir 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,8 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ACCEPTS_NULL: () => ACCEPTS_NULL,
24
+ AlwaysEffectSchema: () => AlwaysEffectSchema,
24
25
  BodyShapeSchema: () => BodyShapeSchema,
25
26
  BoundarySchema: () => BoundarySchema,
27
+ ChangeListSchema: () => ChangeListSchema,
28
+ EffectLineSchema: () => EffectLineSchema,
29
+ EffectOutcomeSchema: () => EffectOutcomeSchema,
30
+ INPUT_SOURCE_PREFIX: () => INPUT_SOURCE_PREFIX,
26
31
  IntentDocSchema: () => IntentDocSchema,
27
32
  IntentFindingKindSchema: () => IntentFindingKindSchema,
28
33
  IntentFindingSchema: () => IntentFindingSchema,
@@ -30,101 +35,57 @@ __export(index_exports, {
30
35
  IntentFindingSuppressionSchema: () => IntentFindingSuppressionSchema,
31
36
  IntentRefSchema: () => IntentRefSchema,
32
37
  IntentSourceSchema: () => IntentSourceSchema,
38
+ NODE_ID_SEPARATOR: () => NODE_ID_SEPARATOR,
39
+ TEST_TITLE_SEPARATOR: () => import_ir_core.TEST_TITLE_SEPARATOR,
33
40
  blanksLeftEmpty: () => blanksLeftEmpty,
41
+ changeListToSummary: () => changeListToSummary,
34
42
  fillBlanks: () => fillBlanks,
35
43
  intentDocToSummary: () => intentDocToSummary,
44
+ parseChangeList: () => parseChangeList,
36
45
  toBoundaryBinding: () => toBoundaryBinding,
46
+ toCoveringTest: () => toCoveringTest,
47
+ toIntentEffect: () => toIntentEffect,
37
48
  toReceives: () => toReceives
38
49
  });
39
50
  module.exports = __toCommonJS(index_exports);
40
51
 
41
- // src/findings.ts
42
- var import_zod = require("zod");
43
- var IntentFindingKindSchema = import_zod.z.enum([
44
- // System intent (kind: boundary) vs code:
45
- "uncoveredOutcome",
46
- // intent declares an outcome the code never produces
47
- "unimplementedBoundary",
48
- // intent boundary has no implementing code at all
49
- "outcomeShapeMismatch",
50
- // a matched outcome whose body shapes disagree
51
- "undeclaredOutcome",
52
- // code produces a REST status the intent doesn't declare
53
- "unkeyableBoundary",
54
- // intent boundary can't be keyed, so it can't be checked
55
- "renamedBoundary",
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
63
- // Outcome intent (kind: prd), scenario link coverage against system
64
- // intent. These concretise the proposal's "scenario not linked /
65
- // dangling / ambiguous" set (design/proposals/intent-specs.md);
66
- // the proposal deferred concrete names to implementation time.
67
- "unlinkedScenario",
68
- // scenario carries no structured link (info — a valid pending state)
69
- "danglingScenarioLink",
70
- // link names an intent / outcome no boundary intent declares
71
- "ambiguousScenarioLink",
72
- // link resolves to two or more boundary intents sharing the name
73
- // The same coverage question asked the other way: which declared
74
- // behaviour has nobody written a reason for.
75
- "undescribedOutcome"
76
- // a declared outcome no PRD scenario links to (info)
77
- ]);
78
- var IntentFindingSeveritySchema = import_zod.z.enum(["error", "warning", "info"]);
79
- var IntentRefSchema = import_zod.z.object({
80
- /** The intent doc's `name` (boundary) or `title` (prd). */
81
- name: import_zod.z.string(),
82
- /** The declared outcome id, when the finding is outcome-specific. */
83
- outcomeId: import_zod.z.string().optional()
84
- });
85
- var IntentFindingSuppressionSchema = import_zod.z.object({
86
- /** The rule's human-written justification. */
87
- reason: import_zod.z.string(),
88
- effect: import_zod.z.enum(["mark", "downgrade", "hide"]),
89
- /** Original severity, present only when effect is "downgrade". */
90
- originalSeverity: IntentFindingSeveritySchema.optional()
91
- });
92
- var IntentFindingSchema = import_zod.z.object({
93
- kind: IntentFindingKindSchema,
94
- severity: IntentFindingSeveritySchema,
95
- /**
96
- * Human-readable boundary label: e.g. `GET /users/:id` or
97
- * `fn:@suss/cli::contract`. The key the intent and code were paired on.
98
- */
99
- boundary: import_zod.z.string(),
100
- /** The intent side of the finding. */
101
- intent: IntentRefSchema,
102
- /**
103
- * The matched code summary as `${file}::${name}`, when the finding
104
- * concerns a specific implementation. Absent for unimplementedBoundary.
105
- */
106
- code: import_zod.z.string().optional(),
107
- /**
108
- * PRD-scenario extension: present only on outcome-intent findings
109
- * (unlinkedScenario / danglingScenarioLink / ambiguousScenarioLink).
110
- * Identifies the scenario (by its optional title) and the qualified
111
- * outcome ref (`<intent-name>.<outcome-id>`) that failed to resolve.
112
- * The peer / boundary findings leave it unset. This is the "intent
113
- * finding extension" decision 2 of the proposal anticipated.
114
- */
115
- scenario: import_zod.z.object({
116
- title: import_zod.z.string().optional(),
117
- link: import_zod.z.string().optional()
118
- }).optional(),
119
- message: import_zod.z.string(),
120
- /** Present when a .sussignore rule matched this finding. */
121
- suppressed: IntentFindingSuppressionSchema.optional()
122
- });
52
+ // src/changeList.ts
53
+ var import_zod2 = require("zod");
123
54
 
124
55
  // src/schema.ts
125
- var import_zod2 = require("zod");
56
+ var import_zod = require("zod");
57
+ var import_ir_core2 = require("@suss/ir-core");
58
+
59
+ // src/coveringTest.ts
126
60
  var import_ir_core = require("@suss/ir-core");
127
- var IntentSourceSchema = import_zod2.z.enum(["author", "inferred", "inferred, curated"]).default("author").describe(
61
+ var NODE_ID_SEPARATOR = "::";
62
+ function toCoveringTest(spelledAs) {
63
+ if (!spelledAs.includes(import_ir_core.TEST_TITLE_SEPARATOR)) {
64
+ return nodeIdSpelling(spelledAs);
65
+ }
66
+ const [file, ...titles] = spelledAs.split(import_ir_core.TEST_TITLE_SEPARATOR);
67
+ return { spelledAs, file: file.trim(), titles };
68
+ }
69
+ function nodeIdSpelling(spelledAs) {
70
+ const parts = spelledAs.trim().split(NODE_ID_SEPARATOR);
71
+ const last = parts.length - 1;
72
+ parts[last] = parts[last].replace(/\[.*\]$/, "");
73
+ let fileAt = last;
74
+ while (fileAt >= 0 && !/[./]/.test(parts[fileAt])) {
75
+ fileAt -= 1;
76
+ }
77
+ if (fileAt < 0) {
78
+ return { spelledAs, file: spelledAs.trim(), titles: [] };
79
+ }
80
+ return {
81
+ spelledAs,
82
+ file: parts.slice(0, fileAt + 1).join(NODE_ID_SEPARATOR),
83
+ titles: parts.slice(fileAt + 1)
84
+ };
85
+ }
86
+
87
+ // src/schema.ts
88
+ var IntentSourceSchema = import_zod.z.enum(["author", "inferred", "inferred, curated"]).default("author").describe(
128
89
  "Where the document came from: somebody wrote it, suss inferred it, or suss inferred it and somebody has since curated it."
129
90
  );
130
91
  var UNCURATED_SOURCE = "inferred";
@@ -160,7 +121,7 @@ function fillBlanks(doc, blanks) {
160
121
  }));
161
122
  return filled;
162
123
  }
163
- var PrimitiveTypeName = import_zod2.z.enum([
124
+ var PrimitiveTypeName = import_zod.z.enum([
164
125
  "string",
165
126
  "integer",
166
127
  "number",
@@ -168,161 +129,191 @@ var PrimitiveTypeName = import_zod2.z.enum([
168
129
  "null",
169
130
  "unknown"
170
131
  ]);
171
- var ShapeSchema = import_zod2.z.lazy(
172
- () => import_zod2.z.union([
173
- import_zod2.z.object({ type: PrimitiveTypeName }),
174
- import_zod2.z.object({ type: import_zod2.z.literal("array"), items: ShapeSchema.optional() }),
175
- import_zod2.z.object({
176
- type: import_zod2.z.literal("object"),
177
- properties: import_zod2.z.record(import_zod2.z.string(), ShapeSchema).optional(),
178
- required: import_zod2.z.array(import_zod2.z.string()).optional()
132
+ var ShapeSchema = import_zod.z.lazy(
133
+ () => import_zod.z.union([
134
+ import_zod.z.object({ type: PrimitiveTypeName }),
135
+ import_zod.z.object({ type: import_zod.z.literal("array"), items: ShapeSchema.optional() }),
136
+ import_zod.z.object({
137
+ type: import_zod.z.literal("object"),
138
+ properties: import_zod.z.record(import_zod.z.string(), ShapeSchema).optional(),
139
+ required: import_zod.z.array(import_zod.z.string()).optional()
140
+ }),
141
+ import_zod.z.strictObject({
142
+ const: import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean(), import_zod.z.null()])
143
+ }),
144
+ import_zod.z.strictObject({
145
+ properties: import_zod.z.record(import_zod.z.string(), ShapeSchema),
146
+ required: import_zod.z.array(import_zod.z.string()).optional()
179
147
  })
180
148
  ])
181
149
  );
182
- var BodyShapeSchema = import_zod2.z.union([
150
+ var BodyShapeSchema = import_zod.z.union([
183
151
  ShapeSchema,
184
- import_zod2.z.object({
185
- properties: import_zod2.z.record(import_zod2.z.string(), ShapeSchema).optional(),
186
- required: import_zod2.z.array(import_zod2.z.string()).optional()
152
+ import_zod.z.object({
153
+ properties: import_zod.z.record(import_zod.z.string(), ShapeSchema).optional(),
154
+ required: import_zod.z.array(import_zod.z.string()).optional()
187
155
  })
188
156
  ]);
189
157
  var REQUIRED = {
190
- required: import_zod2.z.boolean().default(false).describe("Whether the boundary needs this field.")
158
+ required: import_zod.z.boolean().default(false).describe("Whether the boundary needs this field.")
191
159
  };
192
- var InputFieldSchema = import_zod2.z.union([
193
- import_zod2.z.strictObject({ ...REQUIRED, type: PrimitiveTypeName }),
194
- import_zod2.z.strictObject({
160
+ var InputFieldSchema = import_zod.z.union([
161
+ import_zod.z.strictObject({ ...REQUIRED, type: PrimitiveTypeName }),
162
+ import_zod.z.strictObject({
195
163
  ...REQUIRED,
196
- type: import_zod2.z.literal("array"),
164
+ type: import_zod.z.literal("array"),
197
165
  items: ShapeSchema.optional()
198
166
  }),
199
- import_zod2.z.strictObject({
167
+ import_zod.z.strictObject({
200
168
  ...REQUIRED,
201
- type: import_zod2.z.literal("object"),
202
- properties: import_zod2.z.record(import_zod2.z.string(), ShapeSchema).optional()
169
+ type: import_zod.z.literal("object"),
170
+ properties: import_zod.z.record(import_zod.z.string(), ShapeSchema).optional()
203
171
  }),
204
- import_zod2.z.strictObject(REQUIRED)
172
+ import_zod.z.strictObject(REQUIRED)
205
173
  ]);
206
- var ReceivesSchema = import_zod2.z.record(import_zod2.z.string().min(1), InputFieldSchema).describe(
174
+ var ReceivesSchema = import_zod.z.record(import_zod.z.string().min(1), InputFieldSchema).describe(
207
175
  "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
176
  ).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(),
177
+ var RestReceivesSchema = import_zod.z.strictObject({
178
+ headers: import_zod.z.record(import_zod.z.string().min(1), InputFieldSchema).describe("The request headers the boundary depends on, by name.").optional(),
179
+ query: import_zod.z.record(import_zod.z.string().min(1), InputFieldSchema).describe("The query-string parameters the boundary depends on, by name.").optional(),
180
+ params: import_zod.z.record(import_zod.z.string().min(1), InputFieldSchema).describe("The path parameters the boundary depends on, by name.").optional(),
213
181
  body: BodyShapeSchema.describe(
214
182
  "The shape of the request body the boundary depends on."
215
183
  ).optional()
216
184
  }).describe("The parts of the request the boundary depends on.").optional();
217
- var RestBoundarySchema = import_zod2.z.strictObject({
218
- transport: import_zod2.z.literal("http").default("http"),
219
- semantics: import_zod2.z.literal("rest"),
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."),
185
+ var RestBoundarySchema = import_zod.z.strictObject({
186
+ transport: import_zod.z.literal("http").default("http"),
187
+ semantics: import_zod.z.literal("rest"),
188
+ method: import_zod.z.string().min(1).describe("The HTTP method the route handles."),
189
+ path: import_zod.z.string().min(1).describe("The route path, written the way the framework declares it."),
222
190
  receives: RestReceivesSchema
223
191
  });
224
- var FunctionCallBoundarySchema = import_zod2.z.strictObject({
225
- transport: import_zod2.z.string().default("in-process"),
226
- semantics: import_zod2.z.literal("function-call"),
227
- module: import_zod2.z.string().describe(
192
+ var FunctionCallBoundarySchema = import_zod.z.strictObject({
193
+ transport: import_zod.z.string().default("in-process"),
194
+ semantics: import_zod.z.literal("function-call"),
195
+ module: import_zod.z.string().describe(
228
196
  "The repo-relative module path, when the boundary is a unit inside this repository."
229
197
  ).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(
198
+ exportName: import_zod.z.string().describe("The name the module or package exports the function under.").optional(),
199
+ package: import_zod.z.string().describe(
232
200
  "The package name, when the boundary is something a package publishes."
233
201
  ).optional(),
234
- exportPath: import_zod2.z.array(import_zod2.z.string()).describe(
202
+ exportPath: import_zod.z.array(import_zod.z.string()).describe(
235
203
  "The path to the export inside the package: the sub-path, then any nested names."
236
204
  ).optional(),
237
205
  receives: ReceivesSchema
238
206
  });
239
- var MessageBusBoundarySchema = import_zod2.z.strictObject({
240
- semantics: import_zod2.z.literal("message-bus"),
241
- messageBus: import_ir_core.MessageBusSemanticsSchema.shape.messageBus.describe(
207
+ var MessageBusBoundarySchema = import_zod.z.strictObject({
208
+ semantics: import_zod.z.literal("message-bus"),
209
+ messageBus: import_ir_core2.MessageBusSemanticsSchema.shape.messageBus.describe(
242
210
  "Which bus carries the message, in the name suss gives that bus."
243
211
  ),
244
- channel: import_ir_core.MessageBusSemanticsSchema.shape.channel.default(null).describe(
212
+ channel: import_ir_core2.MessageBusSemanticsSchema.shape.channel.default(null).describe(
245
213
  "The queue or topic the message travels on. Null when the document does not name one."
246
214
  ),
247
215
  receives: ReceivesSchema
248
216
  });
249
- var StorageBoundarySchema = import_zod2.z.strictObject({
250
- semantics: import_zod2.z.literal("storage"),
251
- storageSystem: import_ir_core.StorageSemanticsSchema.shape.storageSystem.default(null).describe(
217
+ var StorageBoundarySchema = import_zod.z.strictObject({
218
+ semantics: import_zod.z.literal("storage"),
219
+ storageSystem: import_ir_core2.StorageSemanticsSchema.shape.storageSystem.default(null).describe(
252
220
  "Which store this is: postgresql, aws.dynamodb, s3, and so on. Null when the document does not say which engine."
253
221
  ),
254
- scope: import_ir_core.StorageSemanticsSchema.shape.scope.default("default").describe(
222
+ scope: import_ir_core2.StorageSemanticsSchema.shape.scope.default("default").describe(
255
223
  "The ORM, schema or deployment scope the container is in. A setup with one database uses default."
256
224
  ),
257
- container: import_ir_core.StorageSemanticsSchema.shape.container.default(null).describe(
225
+ container: import_ir_core2.StorageSemanticsSchema.shape.container.default(null).describe(
258
226
  "The table, bucket, collection or index. Null when the document does not name one."
259
227
  ),
260
- accessPath: import_ir_core.StorageSemanticsSchema.shape.accessPath.default(null).describe(
228
+ accessPath: import_ir_core2.StorageSemanticsSchema.shape.accessPath.default(null).describe(
261
229
  "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
230
  ),
263
231
  receives: ReceivesSchema
264
232
  });
265
- var UnitInvocationBoundarySchema = import_zod2.z.strictObject({
266
- semantics: import_zod2.z.literal("unit-invocation"),
267
- deploymentTarget: import_ir_core.UnitInvocationSemanticsSchema.shape.deploymentTarget.describe(
233
+ var UnitInvocationBoundarySchema = import_zod.z.strictObject({
234
+ semantics: import_zod.z.literal("unit-invocation"),
235
+ deploymentTarget: import_ir_core2.UnitInvocationSemanticsSchema.shape.deploymentTarget.describe(
268
236
  "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
237
  ),
270
- instanceName: import_ir_core.UnitInvocationSemanticsSchema.shape.instanceName.default(null).describe(
238
+ instanceName: import_ir_core2.UnitInvocationSemanticsSchema.shape.instanceName.default(null).describe(
271
239
  "The name the deployment medium knows the unit by, such as a CloudFormation logical id. Null when the document does not name one."
272
240
  ),
273
241
  receives: ReceivesSchema
274
242
  });
275
- var BoundarySchema = import_zod2.z.discriminatedUnion("semantics", [
243
+ var BoundarySchema = import_zod.z.discriminatedUnion("semantics", [
276
244
  RestBoundarySchema,
277
245
  FunctionCallBoundarySchema,
278
246
  MessageBusBoundarySchema,
279
247
  StorageBoundarySchema,
280
248
  UnitInvocationBoundarySchema
281
249
  ]);
282
- var ResponseOutcomeSchema = import_zod2.z.object({
283
- status: import_zod2.z.number().int().min(100).max(599).describe("The HTTP status code the response carries."),
250
+ var ResponseOutcomeSchema = import_zod.z.object({
251
+ status: import_zod.z.number().int().min(100).max(599).describe("The HTTP status code the response carries."),
284
252
  body: BodyShapeSchema.describe("The shape of the response body.").optional()
285
253
  });
286
- var ReturnsOutcomeSchema = import_zod2.z.object({
254
+ var ReturnsOutcomeSchema = import_zod.z.object({
287
255
  body: BodyShapeSchema.describe("The shape of the returned value.").optional()
288
256
  });
289
- var ThrowsOutcomeSchema = import_zod2.z.object({
290
- errorType: import_zod2.z.string().describe("The name of the error class this outcome raises.").optional()
257
+ var ThrowsOutcomeSchema = import_zod.z.object({
258
+ errorType: import_zod.z.string().describe("The name of the error class this outcome raises.").optional()
291
259
  });
292
260
  function emptyIfNull(schema) {
293
- return import_zod2.z.preprocess((v) => v === null ? {} : v, schema).meta({ [ACCEPTS_NULL]: true });
261
+ return import_zod.z.preprocess((v) => v === null ? {} : v, schema).meta({ [ACCEPTS_NULL]: true });
294
262
  }
295
263
  var ACCEPTS_NULL = "x-suss-accepts-null";
296
- var ONE_OR_MORE = import_zod2.z.union([
297
- import_zod2.z.string().min(1),
298
- import_zod2.z.array(import_zod2.z.string().min(1)).min(1)
264
+ var INPUT_SOURCE_PREFIX = "input.";
265
+ var SourcesSchema = import_zod.z.record(
266
+ import_zod.z.string().min(1),
267
+ import_zod.z.string().regex(/^input\.[^.]+(\.[^.]+)*$/, {
268
+ message: "a source is written input.<path>, such as input.headers.x-tenant-id or input.body.email"
269
+ })
270
+ ).describe(
271
+ "Where the value of a column the line lists under fields or by comes from, keyed by the column: input.<path>, spelled the way receives spells it."
272
+ );
273
+ var ONE_OR_MORE = import_zod.z.union([
274
+ import_zod.z.string().min(1),
275
+ import_zod.z.array(import_zod.z.string().min(1)).min(1)
299
276
  ]);
300
- var EFFECT_BY_VERB = import_ir_core.EffectRelationSchema.options.map(
301
- (verb) => import_zod2.z.strictObject({
302
- [verb]: import_zod2.z.string().min(1),
303
- fields: import_zod2.z.array(import_zod2.z.string().min(1)).min(1).optional(),
304
- by: ONE_OR_MORE.optional()
277
+ function effectByVerb(extra = {}) {
278
+ return import_ir_core2.EffectRelationSchema.options.map(
279
+ (verb) => import_zod.z.strictObject({
280
+ [verb]: import_zod.z.string().min(1),
281
+ fields: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
282
+ by: ONE_OR_MORE.optional(),
283
+ shape: BodyShapeSchema.describe(
284
+ "The shape of what the effect writes, such as the report a command prints."
285
+ ).optional(),
286
+ ...extra
287
+ })
288
+ );
289
+ }
290
+ var EffectLineSchema = import_zod.z.union(effectByVerb());
291
+ var EffectOutcomeSchema = import_zod.z.union(
292
+ effectByVerb({ from: SourcesSchema.optional() })
293
+ );
294
+ var AlwaysEffectSchema = import_zod.z.union(
295
+ effectByVerb({
296
+ except: import_zod.z.array(import_zod.z.string().min(1)).min(1).describe("The ids of the outcomes that do not have to have the effect.").optional()
305
297
  })
306
298
  );
307
- var EffectOutcomeSchema = import_zod2.z.union(EFFECT_BY_VERB);
308
299
  var CHECKS = {
309
300
  /** What a lookup came back with. */
310
- finds: import_zod2.z.enum(["nothing", "something"]),
301
+ finds: import_zod.z.enum(["nothing", "something"]),
311
302
  /** What state the value was in: `set`, `missing`, `null`, `a string`. */
312
- is: import_zod2.z.string().min(1),
303
+ is: import_zod.z.string().min(1),
313
304
  /** The value it was equal to. */
314
- equals: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.number(), import_zod2.z.boolean(), import_zod2.z.null()]),
305
+ equals: import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean(), import_zod.z.null()]),
315
306
  /** A property it had. */
316
- has: import_zod2.z.string().min(1)
307
+ has: import_zod.z.string().min(1)
317
308
  };
318
309
  var SUBJECT_KEYS = [
319
- ...import_ir_core.EffectRelationSchema.options,
310
+ ...import_ir_core2.EffectRelationSchema.options,
320
311
  "input"
321
312
  ];
322
313
  var WHEN_CLAUSE_BY_SUBJECT = SUBJECT_KEYS.map(
323
- (key) => import_zod2.z.strictObject({
324
- [key]: import_zod2.z.string().min(1),
325
- where: import_zod2.z.string().min(1).optional(),
314
+ (key) => import_zod.z.strictObject({
315
+ [key]: import_zod.z.string().min(1),
316
+ where: import_zod.z.string().min(1).optional(),
326
317
  ...Object.fromEntries(
327
318
  Object.entries(CHECKS).map(([check, schema]) => [
328
319
  check,
@@ -338,12 +329,12 @@ var WHEN_CLAUSE_BY_SUBJECT = SUBJECT_KEYS.map(
338
329
  }
339
330
  )
340
331
  );
341
- var WhenSchema = import_zod2.z.union([
342
- import_zod2.z.string().min(1),
343
- import_zod2.z.array(import_zod2.z.union([import_zod2.z.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
332
+ var WhenSchema = import_zod.z.union([
333
+ import_zod.z.string().min(1),
334
+ import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), ...WHEN_CLAUSE_BY_SUBJECT])).min(1)
344
335
  ]);
345
- var BoundaryTransitionSchema = import_zod2.z.strictObject({
346
- id: import_zod2.z.string().min(1).describe(
336
+ var BoundaryTransitionSchema = import_zod.z.strictObject({
337
+ id: import_zod.z.string().min(1).describe(
347
338
  "The outcome's name. A PRD scenario links to it as <intent-name>.<id>."
348
339
  ),
349
340
  when: WhenSchema.describe(
@@ -352,64 +343,155 @@ var BoundaryTransitionSchema = import_zod2.z.strictObject({
352
343
  response: emptyIfNull(ResponseOutcomeSchema).describe("This outcome sends an HTTP response.").optional(),
353
344
  returns: emptyIfNull(ReturnsOutcomeSchema).describe("This outcome returns a value to its caller.").optional(),
354
345
  throws: emptyIfNull(ThrowsOutcomeSchema).describe("This outcome raises an error.").optional(),
355
- results: import_zod2.z.array(EffectOutcomeSchema).min(1).describe(
346
+ exits: import_zod.z.number().int().min(0).max(255).describe("This outcome ends the process with this exit code.").optional(),
347
+ results: import_zod.z.array(EffectOutcomeSchema).min(1).describe(
356
348
  "The effects this outcome has, written in the same verbs suss ask uses: reads, writes, invokes."
357
349
  ).optional()
358
350
  }).refine((t) => endingsOf(t).length <= 1, {
359
- message: "a transition ends one way: give it at most one of response, returns, or throws"
351
+ message: "a transition ends one way: give it at most one of response, returns, throws, or exits"
360
352
  }).refine((t) => endingsOf(t).length === 1 || t.results !== void 0, {
361
- message: "each transition must declare an outcome: response, returns, throws, or the effects it results in"
353
+ message: "each transition must declare an outcome: response, returns, throws, exits, or the effects it results in"
362
354
  });
363
355
  function endingsOf(t) {
364
- return [t.response, t.returns, t.throws].filter((o) => o !== void 0);
356
+ return [t.response, t.returns, t.throws, t.exits].filter(
357
+ (o) => o !== void 0
358
+ );
365
359
  }
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(
360
+ var BoundaryIntentFields = import_zod.z.strictObject({
361
+ kind: import_zod.z.literal("boundary").describe("Makes this document boundary intent for one boundary."),
362
+ name: import_zod.z.string().min(1).describe(
369
363
  "What this document is called. A PRD scenario links to an outcome of it as <name>.<outcome-id>."
370
364
  ),
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."),
365
+ purpose: import_zod.z.string().min(1).describe("What the boundary is for, in the author's own words."),
366
+ audience: import_zod.z.string().min(1).describe("Who calls this boundary and depends on what it does."),
373
367
  source: IntentSourceSchema,
374
368
  boundary: BoundarySchema.describe(
375
369
  "Which boundary in the code the document is about."
376
370
  ),
377
- transitions: import_zod2.z.array(BoundaryTransitionSchema).min(1).describe("Every outcome the boundary can produce, one entry each.")
371
+ transitions: import_zod.z.array(BoundaryTransitionSchema).min(1).describe("Every outcome the boundary can produce, one entry each."),
372
+ always: import_zod.z.array(AlwaysEffectSchema).min(1).describe(
373
+ "Effects that every transition producing a declared outcome has, apart from the outcomes listed under except."
374
+ ).optional()
375
+ });
376
+ var BoundaryIntentSchema = BoundaryIntentFields.superRefine((doc, ctx) => {
377
+ for (const [line, entry] of (doc.always ?? []).entries()) {
378
+ for (const [at, id] of (entry.except ?? []).entries()) {
379
+ const problem = exceptProblem(doc.transitions, id);
380
+ if (problem !== null) {
381
+ ctx.addIssue({
382
+ code: "custom",
383
+ path: ["always", line, "except", at],
384
+ message: problem
385
+ });
386
+ }
387
+ }
388
+ }
389
+ for (const [at, transition] of doc.transitions.entries()) {
390
+ for (const [line, effect] of (transition.results ?? []).entries()) {
391
+ for (const column of Object.keys(effect.from ?? {})) {
392
+ if (!listsColumn(effect, column)) {
393
+ ctx.addIssue({
394
+ code: "custom",
395
+ path: ["transitions", at, "results", line, "from", column],
396
+ message: `from says where ${column} comes from, and the line lists ${column} under neither fields nor by`
397
+ });
398
+ }
399
+ }
400
+ }
401
+ }
378
402
  });
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."),
403
+ function listsColumn(effect, column) {
404
+ return (effect.fields ?? []).includes(column) || oneOrMore(effect.by).includes(column);
405
+ }
406
+ function exceptProblem(transitions, id) {
407
+ const outcome = transitions.find((t) => t.id === id);
408
+ if (outcome === void 0) {
409
+ return `except lists ${id}, and no transition has that id (known: ${transitions.map((t) => t.id).join(", ")})`;
410
+ }
411
+ if (endingsOf(outcome).length === 0) {
412
+ return `except lists ${id}, which states only its effects; an outcome with no response, returns, throws or exits matches every transition, so it cannot be exempted`;
413
+ }
414
+ return null;
415
+ }
416
+ var PrdScenarioSchema = import_zod.z.strictObject({
417
+ title: import_zod.z.string().min(1).describe("A short name for the scenario.").optional(),
418
+ when: import_zod.z.string().min(1).describe("The condition the scenario covers, in the author's own words."),
419
+ expect: import_zod.z.string().min(1).describe("What should happen then, in the author's own words."),
383
420
  /**
384
- * Optional structured link(s) to system-intent outcomes
385
- * (`<intent-name>.<outcome-id>`). A scenario without `link` is a
386
- * valid pending-link state: fully human-readable, not yet machine-
387
- * linked. The link is filled in later by a facilitator (a person, a
388
- * platform, or an LLM at authoring time), never required to author.
421
+ * Links to system-intent outcomes, each written
422
+ * `<intent-name>.<outcome-id>`. A scenario is backed by a link or by
423
+ * `coveredBy`, and the checker warns about one with neither. The
424
+ * schema still accepts it, so a PRD can be written before either
425
+ * exists.
389
426
  *
390
- * (The human-readable parts are `when` / `expect`; the field is named
391
- * `link`, not `then`, because a data object with a `then` property is
392
- * treated as a thenable by Promise resolution, a latent footgun.)
427
+ * The field is `link` and not `then`, because Promise resolution
428
+ * treats an object with a `then` property as a thenable.
393
429
  */
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(
430
+ link: ONE_OR_MORE.describe(
395
431
  "The boundary-intent outcomes this scenario is about, each written <intent-name>.<outcome-id>."
432
+ ).optional(),
433
+ /**
434
+ * The tests that exercise this scenario, for a promise about which
435
+ * values come back that no outcome can state. Each is spelled the way
436
+ * the runner prints it, as `toCoveringTest` reads it.
437
+ */
438
+ coveredBy: ONE_OR_MORE.describe(
439
+ "The tests that exercise this scenario, each written the way its runner prints it: for vitest or RSpec, the test file, then each describe or context title, then the test's own title, joined with ' > '; for pytest, the node id, as in tests/test_orders.py::TestCancel::test_twice."
440
+ ).optional(),
441
+ about: ONE_OR_MORE.describe(
442
+ "What a covering test has to reach for the scenario to count as covered: a boundary or a unit, spelled the way suss ask takes one. Without it, a test has to reach one of the boundaries the PRD's other scenarios link to."
396
443
  ).optional()
397
444
  });
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."),
445
+ var PrdFields = import_zod.z.strictObject({
446
+ kind: import_zod.z.literal("prd").describe("Makes this document a PRD, a set of scenarios for a feature."),
447
+ title: import_zod.z.string().min(1).describe("What this document is called."),
448
+ purpose: import_zod.z.string().min(1).describe("What the feature is for, in the author's own words."),
449
+ audience: import_zod.z.string().min(1).describe("Who the feature is for."),
403
450
  source: IntentSourceSchema,
404
- scenarios: import_zod2.z.array(PrdScenarioSchema).min(1).describe("The situations the feature covers, one entry each.")
451
+ scenarios: import_zod.z.array(PrdScenarioSchema).min(1).describe("The situations the feature covers, one entry each.")
405
452
  });
406
- var IntentDocSchema = import_zod2.z.discriminatedUnion("kind", [
453
+ var PrdSchema = PrdFields.superRefine((doc, ctx) => {
454
+ const linksAnything = doc.scenarios.some((s) => s.link !== void 0);
455
+ for (const [at, scenario] of doc.scenarios.entries()) {
456
+ const label = scenario.title === void 0 ? `#${at + 1}` : `"${scenario.title}"`;
457
+ for (const [line, spelled] of oneOrMore(scenario.coveredBy).entries()) {
458
+ if (toCoveringTest(spelled).titles.length === 0) {
459
+ ctx.addIssue({
460
+ code: "custom",
461
+ path: ["scenarios", at, "coveredBy", line],
462
+ message: `scenario ${label} lists the test "${spelled}", which has no title; write the file, then each describe title, then the test's own title, joined with " > ", or for pytest the node id, file::test_name`
463
+ });
464
+ }
465
+ }
466
+ if (scenario.about !== void 0 && scenario.coveredBy === void 0) {
467
+ ctx.addIssue({
468
+ code: "custom",
469
+ path: ["scenarios", at, "about"],
470
+ message: `scenario ${label} says what a covering test has to reach, and lists no test under coveredBy`
471
+ });
472
+ }
473
+ if (scenario.coveredBy !== void 0 && scenario.about === void 0 && !linksAnything) {
474
+ ctx.addIssue({
475
+ code: "custom",
476
+ path: ["scenarios", at, "about"],
477
+ message: `scenario ${label} lists a covering test, and no scenario in this PRD links to an outcome, so there is nothing to check the test reaches; say what it has to reach under about`
478
+ });
479
+ }
480
+ }
481
+ });
482
+ function oneOrMore(written) {
483
+ if (written === void 0) {
484
+ return [];
485
+ }
486
+ return typeof written === "string" ? [written] : written;
487
+ }
488
+ var IntentDocSchema = import_zod.z.discriminatedUnion("kind", [
407
489
  BoundaryIntentSchema,
408
490
  PrdSchema
409
491
  ]);
410
492
 
411
493
  // src/summary.ts
412
- var import_ir_core2 = require("@suss/ir-core");
494
+ var import_ir_core3 = require("@suss/ir-core");
413
495
  function intentDocToSummary(doc) {
414
496
  if (doc.kind === "prd") {
415
497
  return prdToSummary(doc);
@@ -425,7 +507,11 @@ function boundaryIntentToSummary(doc) {
425
507
  source: doc.source,
426
508
  boundary: toBoundaryBinding(doc.boundary),
427
509
  receives: toReceives(doc.boundary),
428
- outcomes: doc.transitions.map(toOutcome)
510
+ outcomes: doc.transitions.map(toOutcome),
511
+ always: (doc.always ?? []).map((declared) => ({
512
+ effect: toIntentEffect(declared),
513
+ except: declared.except ?? []
514
+ }))
429
515
  };
430
516
  }
431
517
  function prdToSummary(doc) {
@@ -439,18 +525,20 @@ function prdToSummary(doc) {
439
525
  title: s.title ?? null,
440
526
  when: s.when,
441
527
  expect: s.expect,
442
- link: s.link === void 0 ? [] : Array.isArray(s.link) ? s.link : [s.link]
528
+ link: oneOrMore(s.link),
529
+ coveredBy: oneOrMore(s.coveredBy).map(toCoveringTest),
530
+ about: oneOrMore(s.about)
443
531
  }))
444
532
  };
445
533
  }
446
534
  var BINDINGS = {
447
- rest: (boundary) => (0, import_ir_core2.restBinding)({
535
+ rest: (boundary) => (0, import_ir_core3.restBinding)({
448
536
  transport: boundary.transport,
449
537
  method: boundary.method,
450
538
  path: boundary.path,
451
539
  recognition: "intent"
452
540
  }),
453
- "function-call": (boundary) => (0, import_ir_core2.functionCallBinding)({
541
+ "function-call": (boundary) => (0, import_ir_core3.functionCallBinding)({
454
542
  transport: boundary.transport,
455
543
  recognition: "intent",
456
544
  ...boundary.module !== void 0 ? { module: boundary.module } : {},
@@ -458,17 +546,17 @@ var BINDINGS = {
458
546
  ...boundary.package !== void 0 ? { package: boundary.package } : {},
459
547
  ...boundary.exportPath !== void 0 ? { exportPath: boundary.exportPath } : {}
460
548
  }),
461
- "message-bus": (boundary) => (0, import_ir_core2.messageBusBinding)({
549
+ "message-bus": (boundary) => (0, import_ir_core3.messageBusBinding)({
462
550
  recognition: "intent",
463
551
  messageBus: boundary.messageBus,
464
552
  channel: boundary.channel
465
553
  }),
466
- "unit-invocation": (boundary) => (0, import_ir_core2.unitInvocationBinding)({
554
+ "unit-invocation": (boundary) => (0, import_ir_core3.unitInvocationBinding)({
467
555
  recognition: "intent",
468
556
  deploymentTarget: boundary.deploymentTarget,
469
557
  instanceName: boundary.instanceName
470
558
  }),
471
- storage: (boundary) => (0, import_ir_core2.storageBinding)({
559
+ storage: (boundary) => (0, import_ir_core3.storageBinding)({
472
560
  recognition: "intent",
473
561
  storageSystem: boundary.storageSystem,
474
562
  scope: boundary.scope,
@@ -541,24 +629,25 @@ function toReceives(boundary) {
541
629
  const read = RECEIVES[boundary.semantics];
542
630
  return read(boundary);
543
631
  }
544
- var VERBS = import_ir_core2.EffectRelationSchema.options;
545
- function toEffect(declared) {
632
+ var VERBS = import_ir_core3.EffectRelationSchema.options;
633
+ function toIntentEffect(declared) {
546
634
  const [does, names] = Object.entries(declared).find(
547
635
  ([key]) => VERBS.includes(key)
548
636
  );
637
+ const shape = declared.shape === void 0 ? null : bodyToTypeShape(declared.shape);
638
+ const from = Object.entries(declared.from ?? {}).map(([column, source]) => ({
639
+ column,
640
+ path: source.slice(INPUT_SOURCE_PREFIX.length).split(".")
641
+ }));
549
642
  return {
550
643
  does,
551
644
  names,
552
645
  fields: declared.fields ?? [],
553
- by: oneOrMore(declared.by)
646
+ by: oneOrMore(declared.by),
647
+ ...shape === null ? {} : { shape },
648
+ ...from.length === 0 ? {} : { from }
554
649
  };
555
650
  }
556
- function oneOrMore(written) {
557
- if (written === void 0) {
558
- return [];
559
- }
560
- return typeof written === "string" ? [written] : written;
561
- }
562
651
  function subjectOf(clause) {
563
652
  for (const does of VERBS) {
564
653
  const names = clause[does];
@@ -596,7 +685,7 @@ function whenAsOneLine(when) {
596
685
  return typeof when === "string" ? when : toConditions(when).map((condition) => condition.said).join(" and ");
597
686
  }
598
687
  function toOutcome(t) {
599
- const effects = (t.results ?? []).map(toEffect);
688
+ const effects = (t.results ?? []).map(toIntentEffect);
600
689
  const base = {
601
690
  id: t.id,
602
691
  when: whenAsOneLine(t.when),
@@ -630,9 +719,21 @@ function toOutcome(t) {
630
719
  errorType: t.throws.errorType ?? null
631
720
  };
632
721
  }
722
+ if (t.exits !== void 0) {
723
+ return {
724
+ ...base,
725
+ kind: "exit",
726
+ status: t.exits,
727
+ body: null,
728
+ errorType: null
729
+ };
730
+ }
633
731
  return { ...base, kind: "effect", status: null, body: null, errorType: null };
634
732
  }
635
733
  function bodyToTypeShape(body) {
734
+ if ("const" in body) {
735
+ return constantShape(body.const);
736
+ }
636
737
  if (!("type" in body)) {
637
738
  if (body.properties === void 0) {
638
739
  return null;
@@ -642,6 +743,12 @@ function bodyToTypeShape(body) {
642
743
  return shapeToTypeShape(body);
643
744
  }
644
745
  function shapeToTypeShape(shape) {
746
+ if ("const" in shape) {
747
+ return constantShape(shape.const);
748
+ }
749
+ if (!("type" in shape)) {
750
+ return recordShape(shape.properties);
751
+ }
645
752
  if (shape.type === "array") {
646
753
  return {
647
754
  type: "array",
@@ -653,6 +760,9 @@ function shapeToTypeShape(shape) {
653
760
  }
654
761
  return PRIMITIVE_TYPE_SHAPES[shape.type];
655
762
  }
763
+ function constantShape(value) {
764
+ return value === null ? { type: "null" } : { type: "literal", value };
765
+ }
656
766
  function recordShape(authored) {
657
767
  const properties = {};
658
768
  for (const [name, prop] of Object.entries(authored)) {
@@ -668,11 +778,224 @@ var PRIMITIVE_TYPE_SHAPES = {
668
778
  null: { type: "null" },
669
779
  unknown: { type: "unknown" }
670
780
  };
781
+
782
+ // src/changeList.ts
783
+ var VERBS2 = ["adds", "removes", "changes"];
784
+ var OutcomeWordSchema = import_zod2.z.union([
785
+ import_zod2.z.number().int().min(100).max(599),
786
+ import_zod2.z.enum(["returns", "throws"]),
787
+ import_zod2.z.strictObject({ throws: import_zod2.z.string().min(1) })
788
+ ]);
789
+ var SubjectSchema = import_zod2.z.union([import_zod2.z.string().min(1), EffectLineSchema]);
790
+ var ENTRY_FIELDS = {
791
+ adds: SubjectSchema.optional(),
792
+ removes: SubjectSchema.optional(),
793
+ changes: SubjectSchema.optional(),
794
+ outcomes: import_zod2.z.array(OutcomeWordSchema).min(1).describe(
795
+ "The outcomes the boundary should have: statuses, returns or throws."
796
+ ).optional(),
797
+ at: import_zod2.z.string().min(1).describe("The boundary an effect happens at, spelled as suss spells it.").optional()
798
+ };
799
+ function verbsOf(entry) {
800
+ return VERBS2.filter((verb) => entry[verb] !== void 0);
801
+ }
802
+ function subjectOf2(entry) {
803
+ const [verb] = verbsOf(entry);
804
+ return verb === void 0 ? void 0 : entry[verb];
805
+ }
806
+ function withEntryRules(schema) {
807
+ return schema.refine((entry) => verbsOf(entry).length === 1, {
808
+ message: "an entry has exactly one of adds, removes or changes"
809
+ }).refine(
810
+ (entry) => entry.at === void 0 || typeof subjectOf2(entry) !== "string",
811
+ {
812
+ message: "at says which boundary an effect happens at, so it goes with an effect; a boundary entry names its boundary itself"
813
+ }
814
+ ).refine(
815
+ (entry) => entry.outcomes === void 0 || typeof subjectOf2(entry) === "string",
816
+ {
817
+ message: "outcomes belong to a boundary; an effect entry says what it touches instead"
818
+ }
819
+ );
820
+ }
821
+ var ChangeEntrySchema = withEntryRules(
822
+ import_zod2.z.strictObject({
823
+ ...ENTRY_FIELDS,
824
+ asked: import_zod2.z.string().min(1).describe(
825
+ "The developer's words that asked for this entry, when they are not the list's."
826
+ ).optional(),
827
+ note: import_zod2.z.string().min(1).describe("What the change is, for a subject suss has no words for.").optional()
828
+ })
829
+ );
830
+ var ExplainedEntrySchema = withEntryRules(
831
+ import_zod2.z.strictObject({
832
+ ...ENTRY_FIELDS,
833
+ why: import_zod2.z.string().min(1).describe("Why the change stays, for the developer to read.")
834
+ })
835
+ );
836
+ var ChangeListSchema = import_zod2.z.strictObject({
837
+ asked: import_zod2.z.string().min(1).describe("The developer's request, quoted.").optional(),
838
+ changes: import_zod2.z.array(ChangeEntrySchema).default([]).describe("Each behavior change the agent intends, one per entry."),
839
+ explained: import_zod2.z.array(ExplainedEntrySchema).default([]).describe(
840
+ "Changes nobody asked for that the agent keeps, each with the reason."
841
+ )
842
+ });
843
+ var WORD_OUTCOMES = {
844
+ returns: { kind: "return", status: null, errorType: null },
845
+ throws: { kind: "throw", status: null, errorType: null }
846
+ };
847
+ function toOutcome2(word) {
848
+ if (typeof word === "number") {
849
+ return { kind: "response", status: word, errorType: null };
850
+ }
851
+ if (typeof word === "string") {
852
+ return WORD_OUTCOMES[word];
853
+ }
854
+ return { kind: "throw", status: null, errorType: word.throws };
855
+ }
856
+ function toSubject(written) {
857
+ return typeof written === "string" ? { kind: "boundary", names: written } : { kind: "effect", effect: toIntentEffect(written) };
858
+ }
859
+ function commonOf(entry) {
860
+ const [verb] = verbsOf(entry);
861
+ return {
862
+ verb,
863
+ subject: toSubject(entry[verb]),
864
+ outcomes: (entry.outcomes ?? []).map(toOutcome2),
865
+ at: entry.at ?? null
866
+ };
867
+ }
868
+ function parseChangeList(raw) {
869
+ const result = ChangeListSchema.safeParse(raw);
870
+ if (result.success) {
871
+ return { ok: true, list: changeListToSummary(result.data) };
872
+ }
873
+ return {
874
+ ok: false,
875
+ problems: result.error.issues.map((issue) => ({
876
+ path: issue.path.join("."),
877
+ message: issue.message
878
+ }))
879
+ };
880
+ }
881
+ function changeListToSummary(list) {
882
+ const asked = list.asked ?? null;
883
+ return {
884
+ asked,
885
+ changes: list.changes.map((entry) => ({
886
+ ...commonOf(entry),
887
+ asked: entry.asked ?? asked,
888
+ note: entry.note ?? null
889
+ })),
890
+ explained: list.explained.map((entry) => ({
891
+ ...commonOf(entry),
892
+ why: entry.why
893
+ }))
894
+ };
895
+ }
896
+
897
+ // src/findings.ts
898
+ var import_zod3 = require("zod");
899
+ var IntentFindingKindSchema = import_zod3.z.enum([
900
+ // System intent (kind: boundary) vs code:
901
+ "uncoveredOutcome",
902
+ // intent declares an outcome the code never produces
903
+ "unimplementedBoundary",
904
+ // intent boundary has no implementing code at all
905
+ "outcomeShapeMismatch",
906
+ // a matched outcome whose body shapes disagree
907
+ "undeclaredOutcome",
908
+ // code produces a REST status the intent doesn't declare
909
+ "unkeyableBoundary",
910
+ // intent boundary can't be keyed, so it can't be checked
911
+ "renamedBoundary",
912
+ // a declared store vanished and an undeclared one of the same system appeared with the same outcomes
913
+ "pathWithoutEffect",
914
+ // a transition producing a declared outcome lacks an effect the always block says every outcome has
915
+ "valueFromElsewhere",
916
+ // a column's value comes from somewhere other than the source a results line gives
917
+ // What the boundary is handed: the `receives` block against the paths
918
+ // the unit reads off its input.
919
+ "unreadInputField",
920
+ // intent declares a field no transition of the unit reads
921
+ "undeclaredInputRead",
922
+ // the unit reads a path the receives block does not list
923
+ // Outcome intent (kind: prd): whether each scenario's link resolves to
924
+ // a system-intent outcome.
925
+ "unlinkedScenario",
926
+ // scenario has neither a link nor a covering test
927
+ "danglingScenarioLink",
928
+ // link names an intent / outcome no boundary intent declares
929
+ "ambiguousScenarioLink",
930
+ // link resolves to two or more boundary intents sharing the name
931
+ // The same question for a scenario backed by a test: whether the test
932
+ // it lists exists, runs, and reaches what the scenario is about.
933
+ "missingCoveringTest",
934
+ // coveredBy lists a test no summary here is
935
+ "testMissesSubject",
936
+ // the test never reaches the subject, or reaches it only through a mock
937
+ "coveringTestSkipped",
938
+ // the test is marked to be skipped, so it does not run
939
+ // The same coverage question asked the other way: which declared
940
+ // behaviour has nobody written a reason for.
941
+ "undescribedOutcome"
942
+ // a declared outcome no PRD scenario links to (info)
943
+ ]);
944
+ var IntentFindingSeveritySchema = import_zod3.z.enum(["error", "warning", "info"]);
945
+ var IntentRefSchema = import_zod3.z.object({
946
+ /** The intent doc's `name` (boundary) or `title` (prd). */
947
+ name: import_zod3.z.string(),
948
+ /** The declared outcome id, when the finding is outcome-specific. */
949
+ outcomeId: import_zod3.z.string().optional()
950
+ });
951
+ var IntentFindingSuppressionSchema = import_zod3.z.object({
952
+ /** The rule's human-written justification. */
953
+ reason: import_zod3.z.string(),
954
+ effect: import_zod3.z.enum(["mark", "downgrade", "hide"]),
955
+ /** Original severity, present only when effect is "downgrade". */
956
+ originalSeverity: IntentFindingSeveritySchema.optional()
957
+ });
958
+ var IntentFindingSchema = import_zod3.z.object({
959
+ kind: IntentFindingKindSchema,
960
+ severity: IntentFindingSeveritySchema,
961
+ /**
962
+ * Human-readable boundary label: e.g. `GET /users/:id` or
963
+ * `fn:@suss/cli::contract`. The key the intent and code were paired on.
964
+ */
965
+ boundary: import_zod3.z.string(),
966
+ /** The intent side of the finding. */
967
+ intent: IntentRefSchema,
968
+ /**
969
+ * The matched code summary as `${file}::${name}`, when the finding
970
+ * concerns a specific implementation. Absent for unimplementedBoundary.
971
+ */
972
+ code: import_zod3.z.string().optional(),
973
+ /**
974
+ * Present only on outcome-intent findings. It gives the scenario, by
975
+ * its optional title, and the qualified outcome ref
976
+ * (`<intent-name>.<outcome-id>`) that failed to resolve, or the
977
+ * `coveredBy` entry whose test failed the check. Boundary findings
978
+ * leave it unset.
979
+ */
980
+ scenario: import_zod3.z.object({
981
+ title: import_zod3.z.string().optional(),
982
+ link: import_zod3.z.string().optional(),
983
+ coveredBy: import_zod3.z.string().optional()
984
+ }).optional(),
985
+ message: import_zod3.z.string(),
986
+ /** Present when a .sussignore rule matched this finding. */
987
+ suppressed: IntentFindingSuppressionSchema.optional()
988
+ });
671
989
  // Annotate the CommonJS export names for ESM import in node:
672
990
  0 && (module.exports = {
673
991
  ACCEPTS_NULL,
992
+ AlwaysEffectSchema,
674
993
  BodyShapeSchema,
675
994
  BoundarySchema,
995
+ ChangeListSchema,
996
+ EffectLineSchema,
997
+ EffectOutcomeSchema,
998
+ INPUT_SOURCE_PREFIX,
676
999
  IntentDocSchema,
677
1000
  IntentFindingKindSchema,
678
1001
  IntentFindingSchema,
@@ -680,9 +1003,15 @@ var PRIMITIVE_TYPE_SHAPES = {
680
1003
  IntentFindingSuppressionSchema,
681
1004
  IntentRefSchema,
682
1005
  IntentSourceSchema,
1006
+ NODE_ID_SEPARATOR,
1007
+ TEST_TITLE_SEPARATOR,
683
1008
  blanksLeftEmpty,
1009
+ changeListToSummary,
684
1010
  fillBlanks,
685
1011
  intentDocToSummary,
1012
+ parseChangeList,
686
1013
  toBoundaryBinding,
1014
+ toCoveringTest,
1015
+ toIntentEffect,
687
1016
  toReceives
688
1017
  });