@suss/intent-ir 0.33.1 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.cjs +543 -214
- package/dist/index.d.cts +364 -104
- package/dist/index.d.ts +364 -104
- package/dist/index.js +516 -198
- package/package.json +3 -3
- package/schema/intent-doc.schema.json +430 -0
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/
|
|
42
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
172
|
-
() =>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
type:
|
|
177
|
-
properties:
|
|
178
|
-
required:
|
|
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 =
|
|
150
|
+
var BodyShapeSchema = import_zod.z.union([
|
|
183
151
|
ShapeSchema,
|
|
184
|
-
|
|
185
|
-
properties:
|
|
186
|
-
required:
|
|
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:
|
|
158
|
+
required: import_zod.z.boolean().default(false).describe("Whether the boundary needs this field.")
|
|
191
159
|
};
|
|
192
|
-
var InputFieldSchema =
|
|
193
|
-
|
|
194
|
-
|
|
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:
|
|
164
|
+
type: import_zod.z.literal("array"),
|
|
197
165
|
items: ShapeSchema.optional()
|
|
198
166
|
}),
|
|
199
|
-
|
|
167
|
+
import_zod.z.strictObject({
|
|
200
168
|
...REQUIRED,
|
|
201
|
-
type:
|
|
202
|
-
properties:
|
|
169
|
+
type: import_zod.z.literal("object"),
|
|
170
|
+
properties: import_zod.z.record(import_zod.z.string(), ShapeSchema).optional()
|
|
203
171
|
}),
|
|
204
|
-
|
|
172
|
+
import_zod.z.strictObject(REQUIRED)
|
|
205
173
|
]);
|
|
206
|
-
var ReceivesSchema =
|
|
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 =
|
|
210
|
-
headers:
|
|
211
|
-
query:
|
|
212
|
-
params:
|
|
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 =
|
|
218
|
-
transport:
|
|
219
|
-
semantics:
|
|
220
|
-
method:
|
|
221
|
-
path:
|
|
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 =
|
|
225
|
-
transport:
|
|
226
|
-
semantics:
|
|
227
|
-
module:
|
|
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:
|
|
231
|
-
package:
|
|
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:
|
|
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 =
|
|
240
|
-
semantics:
|
|
241
|
-
messageBus:
|
|
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:
|
|
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 =
|
|
250
|
-
semantics:
|
|
251
|
-
storageSystem:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
266
|
-
semantics:
|
|
267
|
-
deploymentTarget:
|
|
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:
|
|
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 =
|
|
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 =
|
|
283
|
-
status:
|
|
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 =
|
|
254
|
+
var ReturnsOutcomeSchema = import_zod.z.object({
|
|
287
255
|
body: BodyShapeSchema.describe("The shape of the returned value.").optional()
|
|
288
256
|
});
|
|
289
|
-
var ThrowsOutcomeSchema =
|
|
290
|
-
errorType:
|
|
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
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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:
|
|
301
|
+
finds: import_zod.z.enum(["nothing", "something"]),
|
|
311
302
|
/** What state the value was in: `set`, `missing`, `null`, `a string`. */
|
|
312
|
-
is:
|
|
303
|
+
is: import_zod.z.string().min(1),
|
|
313
304
|
/** The value it was equal to. */
|
|
314
|
-
equals:
|
|
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:
|
|
307
|
+
has: import_zod.z.string().min(1)
|
|
317
308
|
};
|
|
318
309
|
var SUBJECT_KEYS = [
|
|
319
|
-
...
|
|
310
|
+
...import_ir_core2.EffectRelationSchema.options,
|
|
320
311
|
"input"
|
|
321
312
|
];
|
|
322
313
|
var WHEN_CLAUSE_BY_SUBJECT = SUBJECT_KEYS.map(
|
|
323
|
-
(key) =>
|
|
324
|
-
[key]:
|
|
325
|
-
where:
|
|
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 =
|
|
342
|
-
|
|
343
|
-
|
|
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 =
|
|
346
|
-
id:
|
|
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
|
-
|
|
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
|
|
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(
|
|
356
|
+
return [t.response, t.returns, t.throws, t.exits].filter(
|
|
357
|
+
(o) => o !== void 0
|
|
358
|
+
);
|
|
365
359
|
}
|
|
366
|
-
var
|
|
367
|
-
kind:
|
|
368
|
-
name:
|
|
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:
|
|
372
|
-
audience:
|
|
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:
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
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
|
-
*
|
|
391
|
-
*
|
|
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:
|
|
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
|
|
399
|
-
kind:
|
|
400
|
-
title:
|
|
401
|
-
purpose:
|
|
402
|
-
audience:
|
|
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:
|
|
451
|
+
scenarios: import_zod.z.array(PrdScenarioSchema).min(1).describe("The situations the feature covers, one entry each.")
|
|
405
452
|
});
|
|
406
|
-
var
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
545
|
-
function
|
|
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(
|
|
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
|
});
|