@suss/extractor 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +630 -17
- package/dist/index.d.cts +962 -571
- package/dist/index.d.ts +962 -571
- package/dist/index.js +618 -16
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
exchangesHttpResponses,
|
|
5
|
+
withGraphqlMetadata,
|
|
6
|
+
withHttpMetadata
|
|
7
|
+
} from "@suss/behavioral-ir";
|
|
3
8
|
|
|
4
9
|
// src/inputReads.ts
|
|
5
10
|
function inputReadsOf(args) {
|
|
@@ -93,13 +98,525 @@ function httpRouteDiscovery(opts) {
|
|
|
93
98
|
registrationChain: [...opts.methods]
|
|
94
99
|
},
|
|
95
100
|
bindingExtraction: {
|
|
96
|
-
method
|
|
101
|
+
// `.all` registers every method, so it records "*", which the pairing
|
|
102
|
+
// engine treats as agreeing with any method at all.
|
|
103
|
+
method: {
|
|
104
|
+
type: "fromRegistration",
|
|
105
|
+
position: "methodName",
|
|
106
|
+
nameMap: { all: "*" }
|
|
107
|
+
},
|
|
97
108
|
path: { type: "fromRegistration", position: 0 }
|
|
98
109
|
},
|
|
110
|
+
...opts.mount !== void 0 ? { mount: opts.mount } : {},
|
|
99
111
|
requiresImport: [opts.importModule]
|
|
100
112
|
}));
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
// src/paths/enumeratePaths.ts
|
|
116
|
+
var MAX_PATHS = 256;
|
|
117
|
+
var UnmodeledFlow = class extends Error {
|
|
118
|
+
};
|
|
119
|
+
var PathBudgetExceeded = class extends Error {
|
|
120
|
+
};
|
|
121
|
+
function branchCond(branchStmt, condition, polarity, oppositeExit) {
|
|
122
|
+
return {
|
|
123
|
+
info: {
|
|
124
|
+
sourceText: condition.sourceText,
|
|
125
|
+
polarity,
|
|
126
|
+
source: "explicit",
|
|
127
|
+
expression: condition.expression
|
|
128
|
+
},
|
|
129
|
+
branchStmt,
|
|
130
|
+
oppositeExit
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function fixedCond(condition, polarity, source) {
|
|
134
|
+
return {
|
|
135
|
+
info: {
|
|
136
|
+
sourceText: condition.sourceText,
|
|
137
|
+
polarity,
|
|
138
|
+
source,
|
|
139
|
+
expression: condition.expression
|
|
140
|
+
},
|
|
141
|
+
branchStmt: null,
|
|
142
|
+
oppositeExit: null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
var loopIterationCond = (headerText) => fixedCond(
|
|
146
|
+
{ sourceText: `some iteration of: ${headerText}`, expression: null },
|
|
147
|
+
"positive",
|
|
148
|
+
"explicit"
|
|
149
|
+
);
|
|
150
|
+
var loopCompletedCond = (headerText, exit) => fixedCond(
|
|
151
|
+
{
|
|
152
|
+
sourceText: `loop exited via ${exit ?? "return"}: ${headerText}`,
|
|
153
|
+
expression: null
|
|
154
|
+
},
|
|
155
|
+
"negative",
|
|
156
|
+
exit === "throw" ? "earlyThrow" : "earlyReturn"
|
|
157
|
+
);
|
|
158
|
+
var catchEntryCond = () => fixedCond(
|
|
159
|
+
{ sourceText: "catch", expression: null },
|
|
160
|
+
"positive",
|
|
161
|
+
"catchBlock"
|
|
162
|
+
);
|
|
163
|
+
function classify(path, home, isAncestorOrSelf) {
|
|
164
|
+
return path.map((cond) => {
|
|
165
|
+
if (cond.branchStmt === null) {
|
|
166
|
+
return cond.info;
|
|
167
|
+
}
|
|
168
|
+
const encloses = home !== null && isAncestorOrSelf(cond.branchStmt, home);
|
|
169
|
+
if (encloses || cond.info.polarity === "positive") {
|
|
170
|
+
return { ...cond.info, source: "explicit" };
|
|
171
|
+
}
|
|
172
|
+
const source = cond.oppositeExit === "throw" ? "earlyThrow" : "earlyReturn";
|
|
173
|
+
return { ...cond.info, source };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function childrenOf(stmt) {
|
|
177
|
+
if (stmt.kind === "if") {
|
|
178
|
+
return stmt.elseBody === null ? stmt.thenBody : [...stmt.thenBody, ...stmt.elseBody];
|
|
179
|
+
}
|
|
180
|
+
if (stmt.kind === "switch") {
|
|
181
|
+
return stmt.groups.flatMap((g) => g.body);
|
|
182
|
+
}
|
|
183
|
+
if (stmt.kind === "loop") {
|
|
184
|
+
return stmt.body;
|
|
185
|
+
}
|
|
186
|
+
if (stmt.kind === "try") {
|
|
187
|
+
return [
|
|
188
|
+
...stmt.tryBody,
|
|
189
|
+
...stmt.catchBody ?? [],
|
|
190
|
+
...stmt.finallyBody ?? []
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
function hasStrayBreak(nodes) {
|
|
196
|
+
return nodes.some((node) => {
|
|
197
|
+
if (node.kind === "exit" && node.exit === "break") {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (node.kind === "switch" || node.kind === "loop") {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return hasStrayBreak(childrenOf(node));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function exitKindOfList(stmts) {
|
|
207
|
+
let sawReturn = false;
|
|
208
|
+
for (const stmt of stmts) {
|
|
209
|
+
if (stmt.exitKind === "throw") {
|
|
210
|
+
return "throw";
|
|
211
|
+
}
|
|
212
|
+
if (stmt.exitKind === "return") {
|
|
213
|
+
sawReturn = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return sawReturn ? "return" : null;
|
|
217
|
+
}
|
|
218
|
+
function chargeBudget(state) {
|
|
219
|
+
state.pathCount++;
|
|
220
|
+
if (state.pathCount > MAX_PATHS) {
|
|
221
|
+
throw new PathBudgetExceeded(
|
|
222
|
+
`path budget exceeded, more than ${MAX_PATHS} paths`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function recordTerminal(ctx, terminal, home, path) {
|
|
227
|
+
chargeBudget(ctx.state);
|
|
228
|
+
const info = classify(path, home, ctx.isAncestorOrSelf);
|
|
229
|
+
const existing = ctx.state.byTerminal.get(terminal) ?? [];
|
|
230
|
+
existing.push(info);
|
|
231
|
+
ctx.state.byTerminal.set(terminal, existing);
|
|
232
|
+
}
|
|
233
|
+
function containsTerminal(ctx, stmt) {
|
|
234
|
+
if ((ctx.terminalsByStmt.get(stmt)?.length ?? 0) > 0) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
|
|
238
|
+
}
|
|
239
|
+
function stepIf(stmt, path, ctx) {
|
|
240
|
+
const armsExit = stmt.exitKind !== null;
|
|
241
|
+
const armsHaveTerminals = containsTerminal(ctx, stmt);
|
|
242
|
+
if (!armsExit && !armsHaveTerminals) {
|
|
243
|
+
return [path];
|
|
244
|
+
}
|
|
245
|
+
const thenExit = exitKindOfList(stmt.thenBody);
|
|
246
|
+
const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
|
|
247
|
+
const thenPath = [
|
|
248
|
+
...path,
|
|
249
|
+
branchCond(stmt, stmt.condition, "positive", elseExit)
|
|
250
|
+
];
|
|
251
|
+
const elsePath = [
|
|
252
|
+
...path,
|
|
253
|
+
branchCond(stmt, stmt.condition, "negative", thenExit)
|
|
254
|
+
];
|
|
255
|
+
const out = [];
|
|
256
|
+
out.push(...enumerate(ctx, stmt.thenBody, thenPath));
|
|
257
|
+
if (stmt.elseBody !== null) {
|
|
258
|
+
out.push(...enumerate(ctx, stmt.elseBody, elsePath));
|
|
259
|
+
} else {
|
|
260
|
+
out.push(elsePath);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function stepSwitch(stmt, path, ctx) {
|
|
265
|
+
const out = [];
|
|
266
|
+
const negations = [];
|
|
267
|
+
let defaultRanBody = false;
|
|
268
|
+
for (let i = 0; i < stmt.groups.length; i++) {
|
|
269
|
+
const group = stmt.groups[i];
|
|
270
|
+
const fallthroughSafe = i === stmt.groups.length - 1;
|
|
271
|
+
if (hasStrayBreak(group.body)) {
|
|
272
|
+
throw new UnmodeledFlow("non-trailing break in switch clause");
|
|
273
|
+
}
|
|
274
|
+
const groupPath = group.condition === null ? [...path, ...negations] : [...path, fixedCond(group.condition, "positive", "explicit")];
|
|
275
|
+
const conts = enumerate(ctx, group.body, groupPath);
|
|
276
|
+
if (conts.length > 0) {
|
|
277
|
+
if (!group.hasTrailingBreak && !fallthroughSafe) {
|
|
278
|
+
throw new UnmodeledFlow("fallthrough into a non-empty switch clause");
|
|
279
|
+
}
|
|
280
|
+
out.push(...conts);
|
|
281
|
+
}
|
|
282
|
+
if (group.condition === null) {
|
|
283
|
+
defaultRanBody = true;
|
|
284
|
+
} else {
|
|
285
|
+
const exit = exitKindOfList(group.body);
|
|
286
|
+
const negationSource = exit === "throw" ? "earlyThrow" : exit === "return" ? "earlyReturn" : "explicit";
|
|
287
|
+
negations.push(fixedCond(group.condition, "negative", negationSource));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!defaultRanBody) {
|
|
291
|
+
out.push([...path, ...negations]);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function stepLoop(stmt, path, ctx) {
|
|
296
|
+
const bodyExit = exitKindOfList(stmt.body);
|
|
297
|
+
if (containsTerminal(ctx, stmt)) {
|
|
298
|
+
enumerate(ctx, stmt.body, [
|
|
299
|
+
...path,
|
|
300
|
+
loopIterationCond(stmt.condition.sourceText)
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
if (bodyExit !== null) {
|
|
304
|
+
return [[...path, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
|
|
305
|
+
}
|
|
306
|
+
return [path];
|
|
307
|
+
}
|
|
308
|
+
function stepTry(stmt, path, ctx) {
|
|
309
|
+
if (stmt.finallyBody !== null) {
|
|
310
|
+
const finallyHasTerminal = stmt.finallyBody.some(
|
|
311
|
+
(s) => containsTerminal(ctx, s)
|
|
312
|
+
);
|
|
313
|
+
if (exitKindOfList(stmt.finallyBody) !== null || finallyHasTerminal) {
|
|
314
|
+
throw new UnmodeledFlow("finally block with exits or terminals");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const out = [];
|
|
318
|
+
out.push(...enumerate(ctx, stmt.tryBody, path));
|
|
319
|
+
if (stmt.catchBody !== null) {
|
|
320
|
+
const catchPath = [...path, catchEntryCond()];
|
|
321
|
+
out.push(...enumerate(ctx, stmt.catchBody, catchPath));
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
function stepExit(_stmt, _path, _ctx) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
function stepOpaque(_stmt, path, _ctx) {
|
|
329
|
+
return [path];
|
|
330
|
+
}
|
|
331
|
+
var stepHandlers = {
|
|
332
|
+
if: stepIf,
|
|
333
|
+
switch: stepSwitch,
|
|
334
|
+
loop: stepLoop,
|
|
335
|
+
try: stepTry,
|
|
336
|
+
exit: stepExit,
|
|
337
|
+
opaque: stepOpaque
|
|
338
|
+
};
|
|
339
|
+
function stepStatement(ctx, stmt, path) {
|
|
340
|
+
const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
|
|
341
|
+
for (const terminal of ownTerminals) {
|
|
342
|
+
recordTerminal(ctx, terminal, stmt, path);
|
|
343
|
+
}
|
|
344
|
+
const handler = stepHandlers[stmt.kind];
|
|
345
|
+
return handler(
|
|
346
|
+
stmt,
|
|
347
|
+
path,
|
|
348
|
+
ctx
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
function soleDisagreement(left, right) {
|
|
352
|
+
if (left.length !== right.length) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
let found = null;
|
|
356
|
+
for (let at = 0; at < left.length; at++) {
|
|
357
|
+
const one = left[at];
|
|
358
|
+
const other = right[at];
|
|
359
|
+
if (one === other) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (found !== null) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
if (one === void 0 || other === void 0 || one.branchStmt === null || one.branchStmt !== other.branchStmt || one.info.polarity === other.info.polarity) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
found = at;
|
|
369
|
+
}
|
|
370
|
+
return found;
|
|
371
|
+
}
|
|
372
|
+
function mergeRejoined(paths) {
|
|
373
|
+
let current = paths;
|
|
374
|
+
for (let round = 0; round < current.length; round++) {
|
|
375
|
+
const out = [];
|
|
376
|
+
const taken = /* @__PURE__ */ new Set();
|
|
377
|
+
let mergedAny = false;
|
|
378
|
+
for (let i = 0; i < current.length; i++) {
|
|
379
|
+
if (taken.has(i)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
let combined = current[i];
|
|
383
|
+
for (let j = i + 1; j < current.length; j++) {
|
|
384
|
+
if (taken.has(j)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const at = soleDisagreement(combined, current[j]);
|
|
388
|
+
if (at === null) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
combined = [...combined.slice(0, at), ...combined.slice(at + 1)];
|
|
392
|
+
taken.add(j);
|
|
393
|
+
mergedAny = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
out.push(combined);
|
|
397
|
+
}
|
|
398
|
+
current = out;
|
|
399
|
+
if (!mergedAny) {
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return current;
|
|
404
|
+
}
|
|
405
|
+
function enumerate(ctx, stmts, prefix) {
|
|
406
|
+
let frontiers = [[...prefix]];
|
|
407
|
+
for (const stmt of stmts) {
|
|
408
|
+
const nextFrontiers = [];
|
|
409
|
+
for (const path of frontiers) {
|
|
410
|
+
nextFrontiers.push(...stepStatement(ctx, stmt, path));
|
|
411
|
+
}
|
|
412
|
+
frontiers = mergeRejoined(nextFrontiers);
|
|
413
|
+
if (frontiers.length > MAX_PATHS) {
|
|
414
|
+
throw new PathBudgetExceeded(
|
|
415
|
+
`path budget exceeded, more than ${MAX_PATHS} paths`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (frontiers.length === 0) {
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return frontiers;
|
|
423
|
+
}
|
|
424
|
+
function buildParentMap(statements) {
|
|
425
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
426
|
+
const visit = (stmt, parent) => {
|
|
427
|
+
parentOf.set(stmt, parent);
|
|
428
|
+
for (const child of childrenOf(stmt)) {
|
|
429
|
+
visit(child, stmt);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
for (const stmt of statements) {
|
|
433
|
+
visit(stmt, null);
|
|
434
|
+
}
|
|
435
|
+
return parentOf;
|
|
436
|
+
}
|
|
437
|
+
function enumerateStructuredPaths(input) {
|
|
438
|
+
const parentOf = buildParentMap(input.statements);
|
|
439
|
+
const isAncestorOrSelf = (a, b) => {
|
|
440
|
+
let current = b;
|
|
441
|
+
while (current !== null) {
|
|
442
|
+
if (current === a) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
current = parentOf.get(current) ?? null;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
};
|
|
449
|
+
const state = {
|
|
450
|
+
byTerminal: /* @__PURE__ */ new Map(),
|
|
451
|
+
fallthrough: [],
|
|
452
|
+
pathCount: 0
|
|
453
|
+
};
|
|
454
|
+
const ctx = {
|
|
455
|
+
terminalsByStmt: input.terminalsByStmt,
|
|
456
|
+
state,
|
|
457
|
+
isAncestorOrSelf
|
|
458
|
+
};
|
|
459
|
+
const continuations = enumerate(ctx, input.statements, []);
|
|
460
|
+
for (const path of continuations) {
|
|
461
|
+
chargeBudget(state);
|
|
462
|
+
state.fallthrough.push(classify(path, null, isAncestorOrSelf));
|
|
463
|
+
}
|
|
464
|
+
return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
|
|
465
|
+
}
|
|
466
|
+
function enumerateOrDegrade(input, terminals) {
|
|
467
|
+
try {
|
|
468
|
+
return { ...enumerateStructuredPaths(input), degraded: null };
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const reason = reasonToDegrade(error);
|
|
471
|
+
if (reason === null) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
const marker = {
|
|
475
|
+
sourceText: `unmodeled control flow (${reason})`,
|
|
476
|
+
polarity: "positive",
|
|
477
|
+
source: "explicit",
|
|
478
|
+
expression: null
|
|
479
|
+
};
|
|
480
|
+
const byTerminal = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const terminal of terminals) {
|
|
482
|
+
byTerminal.set(terminal, [[marker]]);
|
|
483
|
+
}
|
|
484
|
+
return { byTerminal, fallthrough: [[marker]], degraded: reason };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function reasonToDegrade(error) {
|
|
488
|
+
if (error instanceof PathBudgetExceeded) {
|
|
489
|
+
return error.message === "" ? "path budget exceeded" : error.message;
|
|
490
|
+
}
|
|
491
|
+
if (error instanceof UnmodeledFlow) {
|
|
492
|
+
return error.message;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/paths/gatingConditions.ts
|
|
498
|
+
var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
|
|
499
|
+
function sharedGatingConditions(paths, readPredicate) {
|
|
500
|
+
const [first, ...rest] = paths ?? [];
|
|
501
|
+
if (first === void 0) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
const shared = new Set(first.map(keyOf));
|
|
505
|
+
for (const path of rest) {
|
|
506
|
+
const here = new Set(path.map(keyOf));
|
|
507
|
+
for (const value of shared) {
|
|
508
|
+
if (!here.has(value)) {
|
|
509
|
+
shared.delete(value);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return first.filter((condition) => shared.has(keyOf(condition))).map((condition) => ({
|
|
514
|
+
sourceText: condition.sourceText,
|
|
515
|
+
structured: readPredicate === void 0 || condition.expression === null ? null : readPredicate(condition.expression),
|
|
516
|
+
polarity: condition.polarity,
|
|
517
|
+
source: condition.source
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/paths/nodeIdentity.ts
|
|
522
|
+
var IdSet = class {
|
|
523
|
+
byKey = /* @__PURE__ */ new Map();
|
|
524
|
+
constructor(nodes = []) {
|
|
525
|
+
for (const node of nodes) {
|
|
526
|
+
this.add(node);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
add(node) {
|
|
530
|
+
this.byKey.set(node.id, node);
|
|
531
|
+
return this;
|
|
532
|
+
}
|
|
533
|
+
has(node) {
|
|
534
|
+
return this.byKey.has(node.id);
|
|
535
|
+
}
|
|
536
|
+
/** The node this set was built with, which is the caller's own handle for it. */
|
|
537
|
+
get(node) {
|
|
538
|
+
return this.byKey.get(node.id);
|
|
539
|
+
}
|
|
540
|
+
get size() {
|
|
541
|
+
return this.byKey.size;
|
|
542
|
+
}
|
|
543
|
+
[Symbol.iterator]() {
|
|
544
|
+
return this.byKey.values();
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var IdMap = class {
|
|
548
|
+
entries = /* @__PURE__ */ new Map();
|
|
549
|
+
set(node, value) {
|
|
550
|
+
this.entries.set(node.id, [node, value]);
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
get(node) {
|
|
554
|
+
return this.entries.get(node.id)?.[1];
|
|
555
|
+
}
|
|
556
|
+
has(node) {
|
|
557
|
+
return this.entries.has(node.id);
|
|
558
|
+
}
|
|
559
|
+
get size() {
|
|
560
|
+
return this.entries.size;
|
|
561
|
+
}
|
|
562
|
+
[Symbol.iterator]() {
|
|
563
|
+
return this.entries.values();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// src/reading.ts
|
|
568
|
+
function writtenReading(value, range) {
|
|
569
|
+
return { kind: "written", value, range };
|
|
570
|
+
}
|
|
571
|
+
var absentReading = { kind: "absent" };
|
|
572
|
+
function unreadableReading(reason, range) {
|
|
573
|
+
return { kind: "unreadable", reason, range };
|
|
574
|
+
}
|
|
575
|
+
function ambiguousReading(candidates, reason, range) {
|
|
576
|
+
return { kind: "ambiguous", candidates, reason, range };
|
|
577
|
+
}
|
|
578
|
+
function dispatchReading(table, reading) {
|
|
579
|
+
return table[reading.kind](reading);
|
|
580
|
+
}
|
|
581
|
+
function mapReading(reading, f) {
|
|
582
|
+
const table = {
|
|
583
|
+
written: (r) => writtenReading(f(r.value), r.range),
|
|
584
|
+
absent: () => absentReading,
|
|
585
|
+
unreadable: (r) => unreadableReading(r.reason, r.range),
|
|
586
|
+
ambiguous: (r) => ambiguousReading(r.candidates.map(f), r.reason, r.range)
|
|
587
|
+
};
|
|
588
|
+
return dispatchReading(table, reading);
|
|
589
|
+
}
|
|
590
|
+
function andThenReading(reading, f) {
|
|
591
|
+
const table = {
|
|
592
|
+
written: (r) => f(r.value, r.range),
|
|
593
|
+
absent: () => absentReading,
|
|
594
|
+
unreadable: (r) => unreadableReading(r.reason, r.range),
|
|
595
|
+
ambiguous: (r) => ambiguousReading(
|
|
596
|
+
r.candidates.flatMap((candidate) => {
|
|
597
|
+
const next = f(candidate, r.range);
|
|
598
|
+
return next.kind === "written" ? [next.value] : [];
|
|
599
|
+
}),
|
|
600
|
+
r.reason,
|
|
601
|
+
r.range
|
|
602
|
+
)
|
|
603
|
+
};
|
|
604
|
+
return dispatchReading(table, reading);
|
|
605
|
+
}
|
|
606
|
+
function firstWrittenReading(readings) {
|
|
607
|
+
const written = readings.find((reading) => reading.kind === "written");
|
|
608
|
+
const chosen = written ?? readings.find((reading) => reading.kind !== "absent") ?? absentReading;
|
|
609
|
+
return {
|
|
610
|
+
reading: chosen,
|
|
611
|
+
passedOver: readings.filter(
|
|
612
|
+
(reading) => reading !== chosen && (reading.kind === "unreadable" || reading.kind === "ambiguous")
|
|
613
|
+
)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function valueToReadFurtherFrom(reading) {
|
|
617
|
+
return reading.kind === "written" ? reading.value : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
103
620
|
// src/index.ts
|
|
104
621
|
function valuesOfOutput(output) {
|
|
105
622
|
if (output.type === "response" && output.statusCode !== null) {
|
|
@@ -115,8 +632,69 @@ function makeTransitionId(functionName, branch) {
|
|
|
115
632
|
const conditionHash = createHash("sha1").update(conditionSig).digest("hex").slice(0, 7);
|
|
116
633
|
return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
|
|
117
634
|
}
|
|
635
|
+
function collapseReading(table, reading) {
|
|
636
|
+
return table[reading.kind](reading);
|
|
637
|
+
}
|
|
638
|
+
function claimedValue(defaulted) {
|
|
639
|
+
const table = {
|
|
640
|
+
written: (r) => r.value,
|
|
641
|
+
absent: () => defaulted.libraryDefault ?? null,
|
|
642
|
+
unreadable: () => null,
|
|
643
|
+
ambiguous: () => null
|
|
644
|
+
};
|
|
645
|
+
return collapseReading(table, defaulted.reading);
|
|
646
|
+
}
|
|
647
|
+
function unreadReasonOf(reading) {
|
|
648
|
+
const table = {
|
|
649
|
+
written: () => null,
|
|
650
|
+
absent: () => null,
|
|
651
|
+
unreadable: (r) => r.reason,
|
|
652
|
+
ambiguous: (r) => r.reason
|
|
653
|
+
};
|
|
654
|
+
return collapseReading(table, reading);
|
|
655
|
+
}
|
|
656
|
+
function bodyWithClaimedShape(terminal, reading) {
|
|
657
|
+
return {
|
|
658
|
+
typeText: terminal.body?.typeText ?? null,
|
|
659
|
+
shape: claimedValue(reading)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function branchWithCollapsedReadings(branch) {
|
|
663
|
+
const { statusCodeReading, bodyShapeReading } = branch;
|
|
664
|
+
if (statusCodeReading === void 0 && bodyShapeReading === void 0) {
|
|
665
|
+
return branch;
|
|
666
|
+
}
|
|
667
|
+
const status = statusCodeReading !== void 0 ? claimedValue(statusCodeReading) : null;
|
|
668
|
+
return {
|
|
669
|
+
...branch,
|
|
670
|
+
terminal: {
|
|
671
|
+
...branch.terminal,
|
|
672
|
+
...statusCodeReading !== void 0 ? {
|
|
673
|
+
statusCode: status !== null ? { type: "literal", value: status } : null
|
|
674
|
+
} : {},
|
|
675
|
+
...bodyShapeReading !== void 0 ? { body: bodyWithClaimedShape(branch.terminal, bodyShapeReading) } : {}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function readingsOfBranch(branch) {
|
|
680
|
+
return [
|
|
681
|
+
...branch.bodyShapeReading !== void 0 ? [branch.bodyShapeReading.reading] : [],
|
|
682
|
+
...branch.statusCodeReading !== void 0 ? [branch.statusCodeReading.reading] : []
|
|
683
|
+
];
|
|
684
|
+
}
|
|
685
|
+
function unreadSentences(raw) {
|
|
686
|
+
const handedOver = [
|
|
687
|
+
...raw.readings ?? [],
|
|
688
|
+
...raw.branches.flatMap(readingsOfBranch)
|
|
689
|
+
];
|
|
690
|
+
return [
|
|
691
|
+
...raw.unreadBinding !== void 0 ? [raw.unreadBinding] : [],
|
|
692
|
+
...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
|
|
693
|
+
];
|
|
694
|
+
}
|
|
118
695
|
function assembleSummary(raw, options = DEFAULT_OPTIONS) {
|
|
119
|
-
const transitions = raw.branches.map((
|
|
696
|
+
const transitions = raw.branches.map((rawBranch) => {
|
|
697
|
+
const branch = branchWithCollapsedReadings(rawBranch);
|
|
120
698
|
const conditions = branch.conditions.map(
|
|
121
699
|
rawConditionToPredicate
|
|
122
700
|
);
|
|
@@ -153,6 +731,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
|
|
|
153
731
|
},
|
|
154
732
|
identity: {
|
|
155
733
|
name: raw.identity.name,
|
|
734
|
+
...raw.identity.nameKind !== void 0 ? { nameKind: raw.identity.nameKind } : {},
|
|
156
735
|
exportPath: raw.identity.exportPath,
|
|
157
736
|
boundaryBinding: raw.boundaryBinding ?? null,
|
|
158
737
|
...raw.deployableUnit !== void 0 ? { deployableUnit: raw.deployableUnit } : {}
|
|
@@ -167,21 +746,25 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
|
|
|
167
746
|
};
|
|
168
747
|
}
|
|
169
748
|
function buildMetadata(raw) {
|
|
170
|
-
|
|
171
|
-
const http =
|
|
749
|
+
let metadata = {};
|
|
750
|
+
const http = buildHttpMetadataValue(raw);
|
|
172
751
|
if (http !== null) {
|
|
173
|
-
metadata
|
|
752
|
+
metadata = withHttpMetadata(metadata, http);
|
|
174
753
|
}
|
|
175
|
-
const graphql =
|
|
754
|
+
const graphql = buildGraphqlMetadataValue(raw);
|
|
176
755
|
if (graphql !== null) {
|
|
177
|
-
metadata
|
|
756
|
+
metadata = withGraphqlMetadata(metadata, graphql);
|
|
178
757
|
}
|
|
179
758
|
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
180
759
|
}
|
|
181
|
-
function
|
|
760
|
+
function buildHttpMetadataValue(raw) {
|
|
182
761
|
const http = {};
|
|
183
762
|
if (raw.declaredContract !== null) {
|
|
184
|
-
http.declaredContract =
|
|
763
|
+
http.declaredContract = {
|
|
764
|
+
framework: raw.declaredContract.framework,
|
|
765
|
+
responses: raw.declaredContract.responses,
|
|
766
|
+
provenance: raw.declaredContract.provenance ?? "independent"
|
|
767
|
+
};
|
|
185
768
|
}
|
|
186
769
|
if (raw.bodyAccessors !== void 0 && raw.bodyAccessors.length > 0) {
|
|
187
770
|
http.bodyAccessors = raw.bodyAccessors;
|
|
@@ -191,7 +774,7 @@ function buildHttpMetadata(raw) {
|
|
|
191
774
|
}
|
|
192
775
|
return Object.keys(http).length > 0 ? http : null;
|
|
193
776
|
}
|
|
194
|
-
function
|
|
777
|
+
function buildGraphqlMetadataValue(raw) {
|
|
195
778
|
const graphql = {};
|
|
196
779
|
if (raw.graphqlDocument !== void 0) {
|
|
197
780
|
graphql.document = raw.graphqlDocument;
|
|
@@ -207,6 +790,12 @@ function buildGraphqlMetadata(raw) {
|
|
|
207
790
|
}
|
|
208
791
|
return Object.keys(graphql).length > 0 ? graphql : null;
|
|
209
792
|
}
|
|
793
|
+
function answersWithHttpResponses(raw) {
|
|
794
|
+
if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
return exchangesHttpResponses(raw.boundaryBinding);
|
|
798
|
+
}
|
|
210
799
|
function detectGaps(raw, transitions, options) {
|
|
211
800
|
if (options.gapHandling === "silent") {
|
|
212
801
|
return [];
|
|
@@ -215,20 +804,18 @@ function detectGaps(raw, transitions, options) {
|
|
|
215
804
|
if (raw.unmatchedReturns !== void 0 && raw.unmatchedReturns > 0) {
|
|
216
805
|
const count = raw.unmatchedReturns;
|
|
217
806
|
gaps.push({
|
|
218
|
-
// Not a contract violation: the handler may be answering
|
|
219
|
-
// correctly, in a shape nobody taught the pack.
|
|
220
807
|
type: "unreadOutcome",
|
|
221
808
|
conditions: [],
|
|
222
809
|
consequence: "unknown",
|
|
223
810
|
description: count === 1 ? "One return in this function matches none of the terminal shapes this pack looks for, so what it produces is not described here" : `${count} returns in this function match none of the terminal shapes this pack looks for, so what they produce is not described here`
|
|
224
811
|
});
|
|
225
812
|
}
|
|
226
|
-
|
|
813
|
+
for (const sentence of unreadSentences(raw)) {
|
|
227
814
|
gaps.push({
|
|
228
815
|
type: "unreadOutcome",
|
|
229
816
|
conditions: [],
|
|
230
817
|
consequence: "unknown",
|
|
231
|
-
description:
|
|
818
|
+
description: sentence
|
|
232
819
|
});
|
|
233
820
|
}
|
|
234
821
|
const unreadBody = describeUnreadBody(raw);
|
|
@@ -240,7 +827,7 @@ function detectGaps(raw, transitions, options) {
|
|
|
240
827
|
description: unreadBody
|
|
241
828
|
});
|
|
242
829
|
}
|
|
243
|
-
if (raw.declaredContract) {
|
|
830
|
+
if (raw.declaredContract && answersWithHttpResponses(raw)) {
|
|
244
831
|
const producedStatuses = new Set(
|
|
245
832
|
transitions.flatMap((t) => {
|
|
246
833
|
if (t.output.type !== "response") {
|
|
@@ -419,12 +1006,27 @@ function paramToInput(param) {
|
|
|
419
1006
|
};
|
|
420
1007
|
}
|
|
421
1008
|
export {
|
|
1009
|
+
IdMap,
|
|
1010
|
+
IdSet,
|
|
1011
|
+
PathBudgetExceeded,
|
|
1012
|
+
UnmodeledFlow,
|
|
1013
|
+
absentReading,
|
|
1014
|
+
ambiguousReading,
|
|
1015
|
+
andThenReading,
|
|
422
1016
|
assembleSummary,
|
|
423
1017
|
assessConfidence,
|
|
424
1018
|
detectGaps,
|
|
425
1019
|
effectToIR,
|
|
1020
|
+
enumerateOrDegrade,
|
|
1021
|
+
enumerateStructuredPaths,
|
|
1022
|
+
firstWrittenReading,
|
|
426
1023
|
httpRouteDiscovery,
|
|
427
1024
|
makeTransitionId,
|
|
1025
|
+
mapReading,
|
|
428
1026
|
paramToInput,
|
|
429
|
-
|
|
1027
|
+
sharedGatingConditions,
|
|
1028
|
+
terminalToOutput,
|
|
1029
|
+
unreadableReading,
|
|
1030
|
+
valueToReadFurtherFrom,
|
|
1031
|
+
writtenReading
|
|
430
1032
|
};
|