@suss/extractor 0.21.1 → 0.22.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 +40 -0
- package/dist/index.cjs +990 -41
- package/dist/index.d.cts +644 -1
- package/dist/index.d.ts +644 -1
- package/dist/index.js +952 -34
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHash } from "crypto";
|
|
2
|
+
import { createHash as createHash3 } from "crypto";
|
|
3
3
|
import {
|
|
4
4
|
exchangesHttpResponses,
|
|
5
5
|
withGraphqlMetadata,
|
|
@@ -155,8 +155,8 @@ var catchEntryCond = () => fixedCond(
|
|
|
155
155
|
"positive",
|
|
156
156
|
"catchBlock"
|
|
157
157
|
);
|
|
158
|
-
function classify(
|
|
159
|
-
return
|
|
158
|
+
function classify(path3, home, isAncestorOrSelf) {
|
|
159
|
+
return path3.map((cond) => {
|
|
160
160
|
if (cond.branchStmt === null) {
|
|
161
161
|
return cond.info;
|
|
162
162
|
}
|
|
@@ -221,9 +221,9 @@ function chargeBudget(state) {
|
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
function recordTerminal(ctx, terminal, home,
|
|
224
|
+
function recordTerminal(ctx, terminal, home, path3) {
|
|
225
225
|
chargeBudget(ctx.state);
|
|
226
|
-
const info = classify(
|
|
226
|
+
const info = classify(path3, home, ctx.isAncestorOrSelf);
|
|
227
227
|
const existing = ctx.state.byTerminal.get(terminal) ?? [];
|
|
228
228
|
existing.push(info);
|
|
229
229
|
ctx.state.byTerminal.set(terminal, existing);
|
|
@@ -234,15 +234,15 @@ function containsTerminal(ctx, stmt) {
|
|
|
234
234
|
}
|
|
235
235
|
return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
|
|
236
236
|
}
|
|
237
|
-
function stepIf(stmt,
|
|
237
|
+
function stepIf(stmt, path3, ctx) {
|
|
238
238
|
const thenExit = exitKindOfList(stmt.thenBody);
|
|
239
239
|
const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
|
|
240
240
|
const thenPath = [
|
|
241
|
-
...
|
|
241
|
+
...path3,
|
|
242
242
|
branchCond(stmt, stmt.condition, "positive", elseExit)
|
|
243
243
|
];
|
|
244
244
|
const elsePath = [
|
|
245
|
-
...
|
|
245
|
+
...path3,
|
|
246
246
|
branchCond(
|
|
247
247
|
stmt,
|
|
248
248
|
stmt.condition,
|
|
@@ -259,7 +259,7 @@ function stepIf(stmt, path, ctx) {
|
|
|
259
259
|
}
|
|
260
260
|
return out;
|
|
261
261
|
}
|
|
262
|
-
function stepSwitch(stmt,
|
|
262
|
+
function stepSwitch(stmt, path3, ctx) {
|
|
263
263
|
const out = [];
|
|
264
264
|
const negations = [];
|
|
265
265
|
let defaultRanBody = false;
|
|
@@ -269,7 +269,7 @@ function stepSwitch(stmt, path, ctx) {
|
|
|
269
269
|
if (hasStrayBreak(group.body)) {
|
|
270
270
|
throw new UnmodeledFlow("non-trailing break in switch clause");
|
|
271
271
|
}
|
|
272
|
-
const groupPath = group.condition === null ? [...
|
|
272
|
+
const groupPath = group.condition === null ? [...path3, ...negations] : [...path3, fixedCond(group.condition, "positive", "explicit")];
|
|
273
273
|
const conts = enumerate(ctx, group.body, groupPath);
|
|
274
274
|
if (conts.length > 0) {
|
|
275
275
|
if (!group.hasTrailingBreak && !fallthroughSafe) {
|
|
@@ -286,24 +286,24 @@ function stepSwitch(stmt, path, ctx) {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
if (!defaultRanBody) {
|
|
289
|
-
out.push([...
|
|
289
|
+
out.push([...path3, ...negations]);
|
|
290
290
|
}
|
|
291
291
|
return out;
|
|
292
292
|
}
|
|
293
|
-
function stepLoop(stmt,
|
|
293
|
+
function stepLoop(stmt, path3, ctx) {
|
|
294
294
|
const bodyExit = exitKindOfList(stmt.body);
|
|
295
295
|
if (containsTerminal(ctx, stmt)) {
|
|
296
296
|
enumerate(ctx, stmt.body, [
|
|
297
|
-
...
|
|
297
|
+
...path3,
|
|
298
298
|
loopIterationCond(stmt.condition.sourceText)
|
|
299
299
|
]);
|
|
300
300
|
}
|
|
301
301
|
if (bodyExit !== null) {
|
|
302
|
-
return [[...
|
|
302
|
+
return [[...path3, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
|
|
303
303
|
}
|
|
304
|
-
return [
|
|
304
|
+
return [path3];
|
|
305
305
|
}
|
|
306
|
-
function stepTry(stmt,
|
|
306
|
+
function stepTry(stmt, path3, ctx) {
|
|
307
307
|
if (stmt.finallyBody !== null) {
|
|
308
308
|
const finallyHasTerminal = stmt.finallyBody.some(
|
|
309
309
|
(s) => containsTerminal(ctx, s)
|
|
@@ -313,22 +313,22 @@ function stepTry(stmt, path, ctx) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
const out = [];
|
|
316
|
-
out.push(...enumerate(ctx, stmt.tryBody,
|
|
316
|
+
out.push(...enumerate(ctx, stmt.tryBody, path3));
|
|
317
317
|
if (stmt.catchBody !== null) {
|
|
318
|
-
const catchPath = [...
|
|
318
|
+
const catchPath = [...path3, catchEntryCond()];
|
|
319
319
|
out.push(...enumerate(ctx, stmt.catchBody, catchPath));
|
|
320
320
|
}
|
|
321
321
|
return out;
|
|
322
322
|
}
|
|
323
|
-
function stepExit(stmt,
|
|
323
|
+
function stepExit(stmt, path3, ctx) {
|
|
324
324
|
const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
|
|
325
325
|
if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
|
|
326
|
-
collector.push(
|
|
326
|
+
collector.push(path3);
|
|
327
327
|
}
|
|
328
328
|
return [];
|
|
329
329
|
}
|
|
330
|
-
function stepOpaque(_stmt,
|
|
331
|
-
return [
|
|
330
|
+
function stepOpaque(_stmt, path3, _ctx) {
|
|
331
|
+
return [path3];
|
|
332
332
|
}
|
|
333
333
|
var stepHandlers = {
|
|
334
334
|
if: stepIf,
|
|
@@ -338,20 +338,20 @@ var stepHandlers = {
|
|
|
338
338
|
exit: stepExit,
|
|
339
339
|
opaque: stepOpaque
|
|
340
340
|
};
|
|
341
|
-
function stepCallback(ctx, body,
|
|
341
|
+
function stepCallback(ctx, body, path3) {
|
|
342
342
|
const exits = [];
|
|
343
343
|
ctx.state.callbackExits.push(exits);
|
|
344
344
|
let fellThrough;
|
|
345
345
|
try {
|
|
346
|
-
fellThrough = enumerate(ctx, body,
|
|
346
|
+
fellThrough = enumerate(ctx, body, path3);
|
|
347
347
|
} finally {
|
|
348
348
|
ctx.state.callbackExits.pop();
|
|
349
349
|
}
|
|
350
350
|
const out = [...fellThrough, ...exits];
|
|
351
|
-
return out.length === 0 ? [
|
|
351
|
+
return out.length === 0 ? [path3] : out;
|
|
352
352
|
}
|
|
353
|
-
function stepStatement(ctx, stmt,
|
|
354
|
-
let paths = [
|
|
353
|
+
function stepStatement(ctx, stmt, path3) {
|
|
354
|
+
let paths = [path3];
|
|
355
355
|
for (const body of stmt.callbacks ?? []) {
|
|
356
356
|
paths = paths.flatMap((each) => stepCallback(ctx, body, each));
|
|
357
357
|
}
|
|
@@ -432,8 +432,8 @@ function enumerate(ctx, stmts, prefix) {
|
|
|
432
432
|
let frontiers = [[...prefix]];
|
|
433
433
|
for (const stmt of stmts) {
|
|
434
434
|
const nextFrontiers = [];
|
|
435
|
-
for (const
|
|
436
|
-
nextFrontiers.push(...stepStatement(ctx, stmt,
|
|
435
|
+
for (const path3 of mergeRejoined(frontiers)) {
|
|
436
|
+
nextFrontiers.push(...stepStatement(ctx, stmt, path3));
|
|
437
437
|
}
|
|
438
438
|
frontiers = nextFrontiers;
|
|
439
439
|
if (frontiers.length > MAX_PATHS) {
|
|
@@ -484,9 +484,9 @@ function enumerateStructuredPaths(input) {
|
|
|
484
484
|
isAncestorOrSelf
|
|
485
485
|
};
|
|
486
486
|
const continuations = withRejoined(enumerate(ctx, input.statements, []));
|
|
487
|
-
for (const
|
|
487
|
+
for (const path3 of continuations) {
|
|
488
488
|
chargeBudget(state);
|
|
489
|
-
state.fallthrough.push(classify(
|
|
489
|
+
state.fallthrough.push(classify(path3, null, isAncestorOrSelf));
|
|
490
490
|
}
|
|
491
491
|
return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
|
|
492
492
|
}
|
|
@@ -681,6 +681,70 @@ function runsBefore(effectLine, terminalEndLine) {
|
|
|
681
681
|
return effectLine <= terminalEndLine;
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
+
// src/moduleImports.ts
|
|
685
|
+
function stampModuleImports(summaries, importsOf) {
|
|
686
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
687
|
+
for (const summary of summaries) {
|
|
688
|
+
const file = summary.location.file;
|
|
689
|
+
if (!byFile.has(file)) {
|
|
690
|
+
const found = importsOf(file);
|
|
691
|
+
byFile.set(
|
|
692
|
+
file,
|
|
693
|
+
found === void 0 ? void 0 : [...new Set(found)].sort()
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const imports = byFile.get(file);
|
|
697
|
+
if (imports === void 0) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
summary.metadata = {
|
|
701
|
+
...summary.metadata ?? {},
|
|
702
|
+
moduleImports: imports
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/moduleInit.ts
|
|
708
|
+
function moduleInitStructure(options) {
|
|
709
|
+
const { name, file, range, effects } = options;
|
|
710
|
+
return {
|
|
711
|
+
identity: {
|
|
712
|
+
name,
|
|
713
|
+
kind: "module-init",
|
|
714
|
+
file,
|
|
715
|
+
range,
|
|
716
|
+
exportName: null,
|
|
717
|
+
exportPath: null
|
|
718
|
+
},
|
|
719
|
+
boundaryBinding: null,
|
|
720
|
+
parameters: [],
|
|
721
|
+
branches: [
|
|
722
|
+
{
|
|
723
|
+
conditions: [],
|
|
724
|
+
terminal: {
|
|
725
|
+
kind: "void",
|
|
726
|
+
statusCode: null,
|
|
727
|
+
body: null,
|
|
728
|
+
exceptionType: null,
|
|
729
|
+
message: null,
|
|
730
|
+
component: null,
|
|
731
|
+
renderTree: null,
|
|
732
|
+
delegateTarget: null,
|
|
733
|
+
emitEvent: null,
|
|
734
|
+
location: range
|
|
735
|
+
},
|
|
736
|
+
effects: [],
|
|
737
|
+
extraEffects: effects,
|
|
738
|
+
location: range,
|
|
739
|
+
isDefault: true
|
|
740
|
+
}
|
|
741
|
+
],
|
|
742
|
+
unmatchedReturns: 0,
|
|
743
|
+
dependencyCalls: [],
|
|
744
|
+
declaredContract: null
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
684
748
|
// src/packHelpers.ts
|
|
685
749
|
function httpRouteDiscovery(opts) {
|
|
686
750
|
const kind = opts.kind ?? "handler";
|
|
@@ -878,8 +942,8 @@ function sharedGatingConditions(paths, readPredicate) {
|
|
|
878
942
|
return [];
|
|
879
943
|
}
|
|
880
944
|
const shared = new Set(first.map(keyOf2));
|
|
881
|
-
for (const
|
|
882
|
-
const here = new Set(
|
|
945
|
+
for (const path3 of rest) {
|
|
946
|
+
const here = new Set(path3.map(keyOf2));
|
|
883
947
|
for (const value of shared) {
|
|
884
948
|
if (!here.has(value)) {
|
|
885
949
|
shared.delete(value);
|
|
@@ -1008,6 +1072,840 @@ function valueToReadFurtherFrom(reading) {
|
|
|
1008
1072
|
return reading.kind === "written" ? reading.value : null;
|
|
1009
1073
|
}
|
|
1010
1074
|
|
|
1075
|
+
// src/adapterStamp.ts
|
|
1076
|
+
import { createHash } from "crypto";
|
|
1077
|
+
import fs from "fs";
|
|
1078
|
+
import { createRequire } from "module";
|
|
1079
|
+
import path from "path";
|
|
1080
|
+
import { fileURLToPath } from "url";
|
|
1081
|
+
var ANALYSIS_PACKAGES = [
|
|
1082
|
+
"@suss/extractor",
|
|
1083
|
+
"@suss/resolution",
|
|
1084
|
+
"@suss/datalog",
|
|
1085
|
+
"@suss/behavioral-ir"
|
|
1086
|
+
];
|
|
1087
|
+
var SOURCE_STAMP = { kind: "source" };
|
|
1088
|
+
function createAdapterStamp(config) {
|
|
1089
|
+
let cachedCodeStamp = null;
|
|
1090
|
+
let saidWhyNoCache = false;
|
|
1091
|
+
function codeStamp() {
|
|
1092
|
+
if (cachedCodeStamp !== null) {
|
|
1093
|
+
return cachedCodeStamp;
|
|
1094
|
+
}
|
|
1095
|
+
cachedCodeStamp = readAdapterCodeStamp(config.moduleUrl);
|
|
1096
|
+
return cachedCodeStamp;
|
|
1097
|
+
}
|
|
1098
|
+
function packsDigest(packVersions) {
|
|
1099
|
+
const sortedPacks = [...packVersions].map((p) => `${p.name}@${p.version ?? "unset"}`).sort();
|
|
1100
|
+
const stamp = codeStamp();
|
|
1101
|
+
const adapterStamp = stamp.kind === "bundle" ? `adapter@${config.version}+${stamp.hash}` : `adapter@${config.version}+source`;
|
|
1102
|
+
return `${adapterStamp}|${sortedPacks.join(",")}`;
|
|
1103
|
+
}
|
|
1104
|
+
function declineWhenRunFromSource(cacheDir) {
|
|
1105
|
+
if (cacheDir === null || codeStamp().kind === "bundle") {
|
|
1106
|
+
return cacheDir;
|
|
1107
|
+
}
|
|
1108
|
+
if (!saidWhyNoCache) {
|
|
1109
|
+
saidWhyNoCache = true;
|
|
1110
|
+
process.stderr.write(
|
|
1111
|
+
"[suss] extraction cache off: this process loaded the adapter from source, where nothing can tell one build of it from another. Run the built adapter to cache.\n"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return { codeStamp, packsDigest, declineWhenRunFromSource };
|
|
1117
|
+
}
|
|
1118
|
+
function readAdapterCodeStamp(moduleUrl) {
|
|
1119
|
+
try {
|
|
1120
|
+
const selfPath = fileURLToPath(moduleUrl);
|
|
1121
|
+
const hash = computeDistHashFrom(path.dirname(selfPath));
|
|
1122
|
+
return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
|
|
1123
|
+
} catch {
|
|
1124
|
+
return SOURCE_STAMP;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function computeDistHashFrom(dir) {
|
|
1128
|
+
const candidates = [path.join(dir, "index.js"), path.join(dir, "index.cjs")];
|
|
1129
|
+
for (const candidate of candidates) {
|
|
1130
|
+
if (!fs.existsSync(candidate)) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
return computeContentHash([candidate, ...analysisBundles()]);
|
|
1134
|
+
}
|
|
1135
|
+
return "";
|
|
1136
|
+
}
|
|
1137
|
+
function computeContentHash(paths) {
|
|
1138
|
+
if (paths.length === 0) {
|
|
1139
|
+
return "";
|
|
1140
|
+
}
|
|
1141
|
+
const hash = createHash("sha256");
|
|
1142
|
+
for (const file of paths) {
|
|
1143
|
+
try {
|
|
1144
|
+
hash.update(fs.readFileSync(file));
|
|
1145
|
+
} catch {
|
|
1146
|
+
return "";
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return hash.digest("hex").slice(0, 16);
|
|
1150
|
+
}
|
|
1151
|
+
function projectFileStamp(paths) {
|
|
1152
|
+
if (paths.length === 0) {
|
|
1153
|
+
return "none";
|
|
1154
|
+
}
|
|
1155
|
+
const hash = createHash("sha256");
|
|
1156
|
+
for (const file of [...paths].sort()) {
|
|
1157
|
+
hash.update(file);
|
|
1158
|
+
hash.update("\0");
|
|
1159
|
+
try {
|
|
1160
|
+
hash.update(fs.readFileSync(file));
|
|
1161
|
+
} catch {
|
|
1162
|
+
hash.update("absent");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return hash.digest("hex").slice(0, 16);
|
|
1166
|
+
}
|
|
1167
|
+
function runDigest(packsDigest, packs, files) {
|
|
1168
|
+
const inputs = packs.flatMap((pack) => pack.discoveryInputs?.(files) ?? []);
|
|
1169
|
+
return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
|
|
1170
|
+
}
|
|
1171
|
+
function analysisBundles() {
|
|
1172
|
+
const require2 = createRequire(import.meta.url);
|
|
1173
|
+
const found = [];
|
|
1174
|
+
for (const name of ANALYSIS_PACKAGES) {
|
|
1175
|
+
try {
|
|
1176
|
+
found.push(require2.resolve(name));
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return found;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/cache.ts
|
|
1184
|
+
import { createHash as createHash2 } from "crypto";
|
|
1185
|
+
import fs2 from "fs/promises";
|
|
1186
|
+
import path2 from "path";
|
|
1187
|
+
var SCHEMA_VERSION = "7";
|
|
1188
|
+
var MAX_ENTRIES = 2;
|
|
1189
|
+
var ENTRY_PREFIX = "key-";
|
|
1190
|
+
var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
|
|
1191
|
+
function createCacheLayer(cacheDir) {
|
|
1192
|
+
if (cacheDir === null) {
|
|
1193
|
+
return {
|
|
1194
|
+
tryHit: async () => null,
|
|
1195
|
+
lookup: async () => ({
|
|
1196
|
+
kind: "miss",
|
|
1197
|
+
diagnostic: { kind: "miss", missReason: "no-manifest" }
|
|
1198
|
+
}),
|
|
1199
|
+
plan: async () => null,
|
|
1200
|
+
write: async () => {
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
async tryHit(input) {
|
|
1206
|
+
const result = await this.lookup(input);
|
|
1207
|
+
return result.kind === "hit" ? result.summaries : null;
|
|
1208
|
+
},
|
|
1209
|
+
async lookup(input) {
|
|
1210
|
+
const entryDir = entryDirFor(cacheDir, input);
|
|
1211
|
+
const manifest = await readManifest(
|
|
1212
|
+
path2.join(entryDir, "manifest.json")
|
|
1213
|
+
);
|
|
1214
|
+
if (manifest === null) {
|
|
1215
|
+
return missDiag(await describeAbsentEntry(cacheDir));
|
|
1216
|
+
}
|
|
1217
|
+
const currentConfigStamp = await stampConfigFile(input.configPath);
|
|
1218
|
+
if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
|
|
1219
|
+
return missDiag("config-changed");
|
|
1220
|
+
}
|
|
1221
|
+
const currentFiles = await resolveFileStamps(input);
|
|
1222
|
+
if (!fileStampsEqual(manifest.files, currentFiles)) {
|
|
1223
|
+
return missDiag("files-changed");
|
|
1224
|
+
}
|
|
1225
|
+
await markUsed(entryDir);
|
|
1226
|
+
return {
|
|
1227
|
+
kind: "hit",
|
|
1228
|
+
summaries: manifest.summaries,
|
|
1229
|
+
diagnostic: { kind: "hit" }
|
|
1230
|
+
};
|
|
1231
|
+
},
|
|
1232
|
+
async plan(input) {
|
|
1233
|
+
const entryDir = entryDirFor(cacheDir, input);
|
|
1234
|
+
const manifest = await readManifest(
|
|
1235
|
+
path2.join(entryDir, "manifest.json")
|
|
1236
|
+
);
|
|
1237
|
+
if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
const currentConfigStamp = await stampConfigFile(input.configPath);
|
|
1241
|
+
if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
await markUsed(entryDir);
|
|
1245
|
+
return buildPlan(manifest, await resolveFileStamps(input));
|
|
1246
|
+
},
|
|
1247
|
+
async write(input, summaries, attribution) {
|
|
1248
|
+
const entryDir = entryDirFor(cacheDir, input);
|
|
1249
|
+
const previous = await readManifest(
|
|
1250
|
+
path2.join(entryDir, "manifest.json")
|
|
1251
|
+
);
|
|
1252
|
+
const configStamp = await stampConfigFile(input.configPath);
|
|
1253
|
+
const files = await hashStamps(await resolveFileStamps(input), previous);
|
|
1254
|
+
const manifest = {
|
|
1255
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1256
|
+
adapterPacksDigest: input.adapterPacksDigest,
|
|
1257
|
+
configStamp,
|
|
1258
|
+
files,
|
|
1259
|
+
summaries,
|
|
1260
|
+
...attribution === void 0 ? {} : encodeAttribution(attribution)
|
|
1261
|
+
};
|
|
1262
|
+
await fs2.mkdir(entryDir, { recursive: true });
|
|
1263
|
+
await fs2.writeFile(
|
|
1264
|
+
path2.join(entryDir, "manifest.json"),
|
|
1265
|
+
JSON.stringify(manifest)
|
|
1266
|
+
);
|
|
1267
|
+
await evictOldEntries(cacheDir, entryDir);
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
async function buildPlan(manifest, currentStamps) {
|
|
1272
|
+
const stored = new Map(manifest.files.map((f) => [f.path, f]));
|
|
1273
|
+
const current = new Map(currentStamps.map((f) => [f.path, f]));
|
|
1274
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1275
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1276
|
+
for (const p of stored.keys()) {
|
|
1277
|
+
if (!current.has(p)) {
|
|
1278
|
+
removed.add(p);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const toVerify = [];
|
|
1282
|
+
for (const [p, stamp] of current) {
|
|
1283
|
+
const before = stored.get(p);
|
|
1284
|
+
if (before === void 0) {
|
|
1285
|
+
changed.add(p);
|
|
1286
|
+
} else if (!fileStampEquals(before, stamp)) {
|
|
1287
|
+
if (before.contentHash === void 0) {
|
|
1288
|
+
changed.add(p);
|
|
1289
|
+
} else {
|
|
1290
|
+
toVerify.push(p);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
await Promise.all(
|
|
1295
|
+
toVerify.map(async (p) => {
|
|
1296
|
+
const hash = await hashFile(p);
|
|
1297
|
+
if (hash === null || hash !== stored.get(p)?.contentHash) {
|
|
1298
|
+
changed.add(p);
|
|
1299
|
+
}
|
|
1300
|
+
})
|
|
1301
|
+
);
|
|
1302
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1303
|
+
const rootNames = manifest.roots ?? [];
|
|
1304
|
+
const depPaths = manifest.depPaths ?? [];
|
|
1305
|
+
let rootsDeclined = 0;
|
|
1306
|
+
rootNames.forEach((rootPath, i) => {
|
|
1307
|
+
const meta = manifest.rootMeta?.[i];
|
|
1308
|
+
if (meta === void 0) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (!meta.cacheable) {
|
|
1312
|
+
rootsDeclined += 1;
|
|
1313
|
+
}
|
|
1314
|
+
roots.set(rootPath, {
|
|
1315
|
+
path: rootPath,
|
|
1316
|
+
cacheable: meta.cacheable,
|
|
1317
|
+
deps: meta.deps.flatMap((d) => {
|
|
1318
|
+
const p = depPaths[d];
|
|
1319
|
+
return p === void 0 ? [] : [p];
|
|
1320
|
+
}),
|
|
1321
|
+
claims: meta.claims,
|
|
1322
|
+
meta: meta.meta,
|
|
1323
|
+
packs: meta.packs
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
const validRoots = /* @__PURE__ */ new Set();
|
|
1327
|
+
for (const [rootPath, record] of roots) {
|
|
1328
|
+
if (!record.cacheable || !current.has(rootPath)) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
if (changed.has(rootPath)) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
const depMoved = record.deps.some((d) => changed.has(d) || removed.has(d));
|
|
1335
|
+
if (!depMoved) {
|
|
1336
|
+
validRoots.add(rootPath);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const owners = manifest.owners ?? [];
|
|
1340
|
+
return {
|
|
1341
|
+
changed,
|
|
1342
|
+
removed,
|
|
1343
|
+
roots,
|
|
1344
|
+
validRoots,
|
|
1345
|
+
rootsDeclined,
|
|
1346
|
+
reuse(valid) {
|
|
1347
|
+
const summaries = [];
|
|
1348
|
+
const reusedOwners = [];
|
|
1349
|
+
manifest.summaries.forEach((summary, i) => {
|
|
1350
|
+
const ownerPaths = (owners[i] ?? []).flatMap((o) => {
|
|
1351
|
+
const p = rootNames[o];
|
|
1352
|
+
return p === void 0 ? [] : [p];
|
|
1353
|
+
});
|
|
1354
|
+
if (ownerPaths.some((p) => valid.has(p))) {
|
|
1355
|
+
summaries.push(summary);
|
|
1356
|
+
reusedOwners.push(ownerPaths.filter((p) => valid.has(p)));
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
return { summaries, owners: reusedOwners };
|
|
1360
|
+
},
|
|
1361
|
+
allSummaries() {
|
|
1362
|
+
return manifest.summaries;
|
|
1363
|
+
},
|
|
1364
|
+
attribution() {
|
|
1365
|
+
return {
|
|
1366
|
+
roots: [...roots.values()],
|
|
1367
|
+
owners: owners.map(
|
|
1368
|
+
(ownerIds) => ownerIds.flatMap((o) => {
|
|
1369
|
+
const p = rootNames[o];
|
|
1370
|
+
return p === void 0 ? [] : [p];
|
|
1371
|
+
})
|
|
1372
|
+
)
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function encodeAttribution(attribution) {
|
|
1378
|
+
const roots = attribution.roots.map((r) => r.path);
|
|
1379
|
+
const rootIndex = new Map(roots.map((p, i) => [p, i]));
|
|
1380
|
+
const depIndex = /* @__PURE__ */ new Map();
|
|
1381
|
+
const depPaths = [];
|
|
1382
|
+
const depIdOf = (p) => {
|
|
1383
|
+
const existing = depIndex.get(p);
|
|
1384
|
+
if (existing !== void 0) {
|
|
1385
|
+
return existing;
|
|
1386
|
+
}
|
|
1387
|
+
const id = depPaths.length;
|
|
1388
|
+
depPaths.push(p);
|
|
1389
|
+
depIndex.set(p, id);
|
|
1390
|
+
return id;
|
|
1391
|
+
};
|
|
1392
|
+
const rootMeta = attribution.roots.map((r) => ({
|
|
1393
|
+
cacheable: r.cacheable,
|
|
1394
|
+
deps: r.deps.map(depIdOf),
|
|
1395
|
+
claims: r.claims,
|
|
1396
|
+
meta: r.meta,
|
|
1397
|
+
packs: r.packs
|
|
1398
|
+
}));
|
|
1399
|
+
const owners = attribution.owners.map(
|
|
1400
|
+
(ownerPaths) => ownerPaths.flatMap((p) => {
|
|
1401
|
+
const i = rootIndex.get(p);
|
|
1402
|
+
return i === void 0 ? [] : [i];
|
|
1403
|
+
})
|
|
1404
|
+
);
|
|
1405
|
+
return { roots, rootMeta, depPaths, owners };
|
|
1406
|
+
}
|
|
1407
|
+
async function hashStamps(stamps, previous) {
|
|
1408
|
+
const before = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
1409
|
+
return Promise.all(
|
|
1410
|
+
stamps.map(async (stamp) => {
|
|
1411
|
+
const prior = before.get(stamp.path);
|
|
1412
|
+
if (prior !== void 0 && prior.contentHash !== void 0 && fileStampEquals(prior, stamp)) {
|
|
1413
|
+
return { ...stamp, contentHash: prior.contentHash };
|
|
1414
|
+
}
|
|
1415
|
+
const contentHash = await hashFile(stamp.path);
|
|
1416
|
+
return contentHash === null ? stamp : { ...stamp, contentHash };
|
|
1417
|
+
})
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
async function hashFile(filePath) {
|
|
1421
|
+
try {
|
|
1422
|
+
const content = await fs2.readFile(filePath);
|
|
1423
|
+
return createHash2("sha256").update(content).digest("hex").slice(0, 16);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function describeAbsentEntry(cacheDir) {
|
|
1429
|
+
try {
|
|
1430
|
+
const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
|
|
1431
|
+
return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
|
|
1432
|
+
} catch {
|
|
1433
|
+
return "no-manifest";
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function entryDirFor(cacheDir, input) {
|
|
1437
|
+
const key = [
|
|
1438
|
+
SCHEMA_VERSION,
|
|
1439
|
+
input.adapterPacksDigest,
|
|
1440
|
+
input.configPath ?? ""
|
|
1441
|
+
].join(" ");
|
|
1442
|
+
const name = createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
1443
|
+
return path2.join(cacheDir, `${ENTRY_PREFIX}${name}`);
|
|
1444
|
+
}
|
|
1445
|
+
function isEntryDir(name) {
|
|
1446
|
+
return ENTRY_DIR_NAME.test(name);
|
|
1447
|
+
}
|
|
1448
|
+
async function markUsed(entryDir) {
|
|
1449
|
+
const now = /* @__PURE__ */ new Date();
|
|
1450
|
+
try {
|
|
1451
|
+
await fs2.utimes(entryDir, now, now);
|
|
1452
|
+
} catch {
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function evictOldEntries(cacheDir, keepDir) {
|
|
1456
|
+
try {
|
|
1457
|
+
await fs2.rm(path2.join(cacheDir, "manifest.json"), { force: true });
|
|
1458
|
+
const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
|
|
1459
|
+
const dirs = await Promise.all(
|
|
1460
|
+
entries.filter((e) => e.isDirectory() && isEntryDir(e.name)).map(async (e) => {
|
|
1461
|
+
const dir = path2.join(cacheDir, e.name);
|
|
1462
|
+
const stat = await fs2.stat(dir);
|
|
1463
|
+
return { dir, mtimeMs: stat.mtimeMs };
|
|
1464
|
+
})
|
|
1465
|
+
);
|
|
1466
|
+
const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
|
|
1467
|
+
await Promise.all(
|
|
1468
|
+
doomed.map((d) => fs2.rm(d.dir, { recursive: true, force: true }))
|
|
1469
|
+
);
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function missDiag(reason) {
|
|
1474
|
+
return {
|
|
1475
|
+
kind: "miss",
|
|
1476
|
+
diagnostic: { kind: "miss", missReason: reason }
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async function readManifest(manifestPath) {
|
|
1480
|
+
try {
|
|
1481
|
+
const raw = await fs2.readFile(manifestPath, "utf-8");
|
|
1482
|
+
return JSON.parse(raw);
|
|
1483
|
+
} catch {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function stampConfigFile(configPath) {
|
|
1488
|
+
if (configPath === void 0) {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const stat = await fs2.stat(configPath);
|
|
1493
|
+
return { path: configPath, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
1494
|
+
} catch {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async function resolveFileStamps(input) {
|
|
1499
|
+
const stamped = await Promise.all(
|
|
1500
|
+
input.files.map(async (p) => {
|
|
1501
|
+
try {
|
|
1502
|
+
const stat = await fs2.stat(p);
|
|
1503
|
+
return { path: p, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
1504
|
+
} catch {
|
|
1505
|
+
return { path: p, mtimeMs: -1, size: -1 };
|
|
1506
|
+
}
|
|
1507
|
+
})
|
|
1508
|
+
);
|
|
1509
|
+
stamped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1510
|
+
return stamped;
|
|
1511
|
+
}
|
|
1512
|
+
function fileStampsEqual(a, b) {
|
|
1513
|
+
return a.length === b.length && a.every((s, i) => fileStampEquals(s, b[i]));
|
|
1514
|
+
}
|
|
1515
|
+
function fileStampEquals(a, b) {
|
|
1516
|
+
if (a === null && b === null) {
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
if (a === null || b === null) {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/extractionReport.ts
|
|
1526
|
+
import { BOUNDARY_ROLE } from "@suss/behavioral-ir";
|
|
1527
|
+
var emptyTally = () => ({
|
|
1528
|
+
candidateFiles: 0,
|
|
1529
|
+
unitsDiscovered: 0,
|
|
1530
|
+
unitsInGatedFiles: 0,
|
|
1531
|
+
effectsRecognized: 0,
|
|
1532
|
+
unitsClaimed: 0,
|
|
1533
|
+
selfCollisions: 0,
|
|
1534
|
+
summariesProduced: 0,
|
|
1535
|
+
failures: [],
|
|
1536
|
+
helpersMatched: /* @__PURE__ */ new Set()
|
|
1537
|
+
});
|
|
1538
|
+
function recordPackFailure(tally, failure) {
|
|
1539
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
1540
|
+
tally?.failures.push({
|
|
1541
|
+
hook: failure.hook,
|
|
1542
|
+
file: failure.file,
|
|
1543
|
+
message
|
|
1544
|
+
});
|
|
1545
|
+
return `[suss] pack "${failure.pack}" threw from ${failure.hook} while reading ${failure.file}: ${message}
|
|
1546
|
+
`;
|
|
1547
|
+
}
|
|
1548
|
+
function createPackTallies(packs) {
|
|
1549
|
+
const tallies = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const pack of packs) {
|
|
1551
|
+
tallies.set(pack.name, emptyTally());
|
|
1552
|
+
}
|
|
1553
|
+
return tallies;
|
|
1554
|
+
}
|
|
1555
|
+
function tallyUnit(tallies, recognition) {
|
|
1556
|
+
const tally = recognition === void 0 ? void 0 : tallies.get(recognition);
|
|
1557
|
+
if (tally === void 0) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
tally.unitsDiscovered += 1;
|
|
1561
|
+
tally.summariesProduced += 1;
|
|
1562
|
+
}
|
|
1563
|
+
var emptyCounts = () => ({
|
|
1564
|
+
bound: 0,
|
|
1565
|
+
providers: 0,
|
|
1566
|
+
withBehavior: 0
|
|
1567
|
+
});
|
|
1568
|
+
function summaryCountsByPack(summaries) {
|
|
1569
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1570
|
+
for (const summary of summaries) {
|
|
1571
|
+
const recognition = summary.identity.boundaryBinding?.recognition;
|
|
1572
|
+
if (recognition === void 0 || recognition === null) {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const entry = counts.get(recognition) ?? emptyCounts();
|
|
1576
|
+
entry.bound += 1;
|
|
1577
|
+
if (BOUNDARY_ROLE[summary.kind] === "provider") {
|
|
1578
|
+
entry.providers += 1;
|
|
1579
|
+
}
|
|
1580
|
+
if (summary.transitions.length > 0) {
|
|
1581
|
+
entry.withBehavior += 1;
|
|
1582
|
+
}
|
|
1583
|
+
counts.set(recognition, entry);
|
|
1584
|
+
}
|
|
1585
|
+
return counts;
|
|
1586
|
+
}
|
|
1587
|
+
function buildUngatedExtractionReport(args) {
|
|
1588
|
+
const bySummary = summaryCountsByPack(args.summaries);
|
|
1589
|
+
const packFunnels = args.packs.map((pack) => {
|
|
1590
|
+
const tally = args.tallies.get(pack.name) ?? emptyTally();
|
|
1591
|
+
const counted = bySummary.get(pack.name) ?? emptyCounts();
|
|
1592
|
+
return {
|
|
1593
|
+
pack: pack.name,
|
|
1594
|
+
version: pack.version,
|
|
1595
|
+
discovers: pack.discovers,
|
|
1596
|
+
recognizes: false,
|
|
1597
|
+
gates: [],
|
|
1598
|
+
unresolvedGates: [],
|
|
1599
|
+
candidateFiles: args.filesWalked,
|
|
1600
|
+
unitsDiscovered: tally.unitsDiscovered,
|
|
1601
|
+
unitsInGatedFiles: 0,
|
|
1602
|
+
effectsRecognized: 0,
|
|
1603
|
+
unitsClaimed: tally.unitsDiscovered,
|
|
1604
|
+
selfCollisions: 0,
|
|
1605
|
+
summariesProduced: tally.summariesProduced,
|
|
1606
|
+
failures: tally.failures,
|
|
1607
|
+
helpersUnmatched: [],
|
|
1608
|
+
summariesBound: counted.bound,
|
|
1609
|
+
providerSummaries: counted.providers,
|
|
1610
|
+
summariesWithBehavior: counted.withBehavior,
|
|
1611
|
+
declarations: null
|
|
1612
|
+
};
|
|
1613
|
+
});
|
|
1614
|
+
return {
|
|
1615
|
+
filesInProject: null,
|
|
1616
|
+
filesWalked: args.filesWalked,
|
|
1617
|
+
packs: packFunnels,
|
|
1618
|
+
summaries: args.summaries.length,
|
|
1619
|
+
filesWithUnreadableExports: [],
|
|
1620
|
+
reassignedNamesUnstated: 0,
|
|
1621
|
+
emptyStage: args.summaries.length === 0 ? firstEmptyUngatedStage(packFunnels, args.filesWalked) : null
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function firstEmptyUngatedStage(packs, filesWalked) {
|
|
1625
|
+
if (filesWalked === 0) {
|
|
1626
|
+
return "candidateFiles";
|
|
1627
|
+
}
|
|
1628
|
+
if (packs.every((p) => p.unitsDiscovered === 0)) {
|
|
1629
|
+
return "discovery";
|
|
1630
|
+
}
|
|
1631
|
+
return "assembly";
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/packHealth.ts
|
|
1635
|
+
function stagesOf(funnel) {
|
|
1636
|
+
const stages = [];
|
|
1637
|
+
const gateSaysSomething = funnel.gates.length > 0 && funnel.unresolvedGates.length === 0;
|
|
1638
|
+
if (funnel.recognizes && !funnel.discovers && gateSaysSomething) {
|
|
1639
|
+
stages.push({
|
|
1640
|
+
from: { count: funnel.unitsInGatedFiles, name: "unit bodies" },
|
|
1641
|
+
to: { count: funnel.effectsRecognized, name: "effects" }
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (funnel.discovers && gateSaysSomething) {
|
|
1645
|
+
stages.push({
|
|
1646
|
+
from: { count: funnel.candidateFiles, name: "source files" },
|
|
1647
|
+
to: { count: funnel.unitsDiscovered, name: "units" }
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
stages.push(
|
|
1651
|
+
{
|
|
1652
|
+
from: { count: funnel.unitsClaimed, name: "units" },
|
|
1653
|
+
to: { count: funnel.summariesBound, name: "summaries" }
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
from: { count: funnel.summariesBound, name: "summaries" },
|
|
1657
|
+
to: { count: funnel.summariesWithBehavior, name: "transitions" }
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
return stages;
|
|
1661
|
+
}
|
|
1662
|
+
function funnelDrops(packs) {
|
|
1663
|
+
const violations = [];
|
|
1664
|
+
for (const funnel of packs) {
|
|
1665
|
+
for (const stage of stagesOf(funnel)) {
|
|
1666
|
+
if (stage.from.count === 0 || stage.to.count > 0) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
violations.push({
|
|
1670
|
+
label: funnel.pack,
|
|
1671
|
+
detail: `${stage.from.count} ${stage.from.name} -> 0 ${stage.to.name}`
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return violations;
|
|
1676
|
+
}
|
|
1677
|
+
function recognizersWithNoUnits(packs) {
|
|
1678
|
+
return packs.filter(
|
|
1679
|
+
(funnel) => funnel.recognizes && !funnel.discovers && funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles > 0 && funnel.unitsInGatedFiles === 0 && // The closure walks a recognizer-only pack's gated exports as
|
|
1680
|
+
// roots, so effects recognized there mean the run worked and
|
|
1681
|
+
// the missing framework pack costs attribution, not existence.
|
|
1682
|
+
funnel.effectsRecognized === 0
|
|
1683
|
+
).map((funnel) => ({
|
|
1684
|
+
label: funnel.pack,
|
|
1685
|
+
detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
|
|
1686
|
+
}));
|
|
1687
|
+
}
|
|
1688
|
+
function helpersThatMatchedNothing(packs) {
|
|
1689
|
+
return packs.flatMap(
|
|
1690
|
+
(funnel) => funnel.helpersUnmatched.map((helper) => ({
|
|
1691
|
+
label: funnel.pack,
|
|
1692
|
+
detail: `${helper} matched no call in this run, so whatever it registers is missing. suss read that helper out of the project and then failed to match a call to it, which is a bug in suss rather than in your code.`
|
|
1693
|
+
}))
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
function unversionedPacks(packs) {
|
|
1697
|
+
return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
|
|
1698
|
+
label: funnel.pack,
|
|
1699
|
+
detail: "no version declared"
|
|
1700
|
+
}));
|
|
1701
|
+
}
|
|
1702
|
+
function selfCollisions(packs) {
|
|
1703
|
+
return packs.filter((funnel) => funnel.selfCollisions > 0).map((funnel) => ({
|
|
1704
|
+
label: funnel.pack,
|
|
1705
|
+
detail: `${funnel.selfCollisions} ${funnel.selfCollisions === 1 ? "unit" : "units"} matched twice, second dropped`
|
|
1706
|
+
}));
|
|
1707
|
+
}
|
|
1708
|
+
function threwWhileReading(packs) {
|
|
1709
|
+
return packs.filter((funnel) => funnel.failures.length > 0).map((funnel) => {
|
|
1710
|
+
const first = funnel.failures[0];
|
|
1711
|
+
const rest = funnel.failures.length - 1;
|
|
1712
|
+
const alsoIn = rest > 0 ? ` (+${rest} more)` : "";
|
|
1713
|
+
return {
|
|
1714
|
+
label: funnel.pack,
|
|
1715
|
+
detail: `${first.hook} on ${first.file}${alsoIn}: ${first.message}`
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
function packGradients(report) {
|
|
1720
|
+
const gradients = [];
|
|
1721
|
+
for (const funnel of report.packs) {
|
|
1722
|
+
if (funnel.declarations === null) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
gradients.push(gradientOf(funnel.pack, funnel.declarations.declarations));
|
|
1726
|
+
}
|
|
1727
|
+
return gradients;
|
|
1728
|
+
}
|
|
1729
|
+
function gradientOf(pack, declarations) {
|
|
1730
|
+
const gradient = {
|
|
1731
|
+
pack,
|
|
1732
|
+
dataLinks: 0,
|
|
1733
|
+
functionLinks: [],
|
|
1734
|
+
astLinks: [],
|
|
1735
|
+
withoutExample: []
|
|
1736
|
+
};
|
|
1737
|
+
for (const declaration of declarations) {
|
|
1738
|
+
gradient.dataLinks += declaration.dataLinks;
|
|
1739
|
+
for (const question of declaration.functionLinks) {
|
|
1740
|
+
gradient.functionLinks.push(`${declaration.name}.${question}`);
|
|
1741
|
+
}
|
|
1742
|
+
for (const question of declaration.astLinks) {
|
|
1743
|
+
gradient.astLinks.push(`${declaration.name}.${question}`);
|
|
1744
|
+
}
|
|
1745
|
+
if (declaration.example === null) {
|
|
1746
|
+
gradient.withoutExample.push(declaration.name);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return gradient;
|
|
1750
|
+
}
|
|
1751
|
+
function opaqueLinks(gradients) {
|
|
1752
|
+
return gradients.filter((gradient) => gradient.functionLinks.length > 0).map((gradient) => ({
|
|
1753
|
+
label: gradient.pack,
|
|
1754
|
+
detail: `${gradient.functionLinks.join(", ")} (${gradient.dataLinks} data)`
|
|
1755
|
+
}));
|
|
1756
|
+
}
|
|
1757
|
+
function reachesTheSyntaxTree(gradients) {
|
|
1758
|
+
return gradients.filter((gradient) => gradient.astLinks.length > 0).map((gradient) => ({
|
|
1759
|
+
label: gradient.pack,
|
|
1760
|
+
detail: gradient.astLinks.join(", ")
|
|
1761
|
+
}));
|
|
1762
|
+
}
|
|
1763
|
+
function undocumentedDeclarations(gradients) {
|
|
1764
|
+
return gradients.filter((gradient) => gradient.withoutExample.length > 0).map((gradient) => ({
|
|
1765
|
+
label: gradient.pack,
|
|
1766
|
+
detail: gradient.withoutExample.join(", ")
|
|
1767
|
+
}));
|
|
1768
|
+
}
|
|
1769
|
+
function evaluatePackHealth(report) {
|
|
1770
|
+
const gradients = packGradients(report);
|
|
1771
|
+
return [
|
|
1772
|
+
{
|
|
1773
|
+
name: "no pack throws while it reads",
|
|
1774
|
+
code: "threw",
|
|
1775
|
+
audience: "run",
|
|
1776
|
+
violations: threwWhileReading(report.packs)
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
name: "no pack finds something and records nothing",
|
|
1780
|
+
code: "no-output",
|
|
1781
|
+
audience: "run",
|
|
1782
|
+
violations: funnelDrops(report.packs)
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
name: "no pack collides with itself",
|
|
1786
|
+
code: "double-match",
|
|
1787
|
+
audience: "run",
|
|
1788
|
+
violations: selfCollisions(report.packs)
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
name: "every recognizer had units to look inside",
|
|
1792
|
+
code: "no-units",
|
|
1793
|
+
audience: "run",
|
|
1794
|
+
violations: recognizersWithNoUnits(report.packs)
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
name: "every registration helper the run read matched a call",
|
|
1798
|
+
code: "no-helper",
|
|
1799
|
+
audience: "run",
|
|
1800
|
+
violations: helpersThatMatchedNothing(report.packs)
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
name: "every pack declares a version",
|
|
1804
|
+
code: "no-version",
|
|
1805
|
+
audience: "pack",
|
|
1806
|
+
violations: unversionedPacks(report.packs)
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: "every link a declared pack states is data",
|
|
1810
|
+
code: "fn-link",
|
|
1811
|
+
audience: "pack",
|
|
1812
|
+
violations: opaqueLinks(gradients)
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
name: "no declared pack reads the syntax tree",
|
|
1816
|
+
code: "ast-link",
|
|
1817
|
+
audience: "pack",
|
|
1818
|
+
violations: reachesTheSyntaxTree(gradients)
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
name: "every declaration states an example",
|
|
1822
|
+
code: "no-example",
|
|
1823
|
+
audience: "pack",
|
|
1824
|
+
violations: undocumentedDeclarations(gradients)
|
|
1825
|
+
}
|
|
1826
|
+
];
|
|
1827
|
+
}
|
|
1828
|
+
function formatPackHealth(checks, audiences) {
|
|
1829
|
+
const fired = checks.filter(
|
|
1830
|
+
(check) => check.violations.length > 0 && audiences.includes(check.audience)
|
|
1831
|
+
);
|
|
1832
|
+
if (fired.length === 0) {
|
|
1833
|
+
return "";
|
|
1834
|
+
}
|
|
1835
|
+
const rows = fired.flatMap(
|
|
1836
|
+
(check) => check.violations.map((violation) => ({
|
|
1837
|
+
code: check.code,
|
|
1838
|
+
pack: violation.label,
|
|
1839
|
+
detail: violation.detail
|
|
1840
|
+
}))
|
|
1841
|
+
);
|
|
1842
|
+
const codeWidth = Math.max(...rows.map((row) => row.code.length));
|
|
1843
|
+
const packWidth = Math.max(...rows.map((row) => row.pack.length));
|
|
1844
|
+
const lines = ["", `Pack health (${rows.length}):`];
|
|
1845
|
+
for (const row of rows) {
|
|
1846
|
+
lines.push(
|
|
1847
|
+
` ${row.code.padEnd(codeWidth)} ${row.pack.padEnd(packWidth)} ${row.detail}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
return `${lines.join("\n")}
|
|
1851
|
+
`;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/timing.ts
|
|
1855
|
+
function createTimer() {
|
|
1856
|
+
const phases = /* @__PURE__ */ new Map();
|
|
1857
|
+
const start = performance.now();
|
|
1858
|
+
function record(label, deltaMs) {
|
|
1859
|
+
const existing = phases.get(label);
|
|
1860
|
+
if (existing === void 0) {
|
|
1861
|
+
phases.set(label, { durationMs: deltaMs, calls: 1 });
|
|
1862
|
+
} else {
|
|
1863
|
+
existing.durationMs += deltaMs;
|
|
1864
|
+
existing.calls += 1;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
time(label, fn) {
|
|
1869
|
+
const t0 = performance.now();
|
|
1870
|
+
try {
|
|
1871
|
+
return fn();
|
|
1872
|
+
} finally {
|
|
1873
|
+
record(label, performance.now() - t0);
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
async timeAsync(label, fn) {
|
|
1877
|
+
const t0 = performance.now();
|
|
1878
|
+
try {
|
|
1879
|
+
return await fn();
|
|
1880
|
+
} finally {
|
|
1881
|
+
record(label, performance.now() - t0);
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
report() {
|
|
1885
|
+
const totalMs = performance.now() - start;
|
|
1886
|
+
const ordered = [...phases.entries()].map(([label, s]) => ({
|
|
1887
|
+
label,
|
|
1888
|
+
durationMs: s.durationMs,
|
|
1889
|
+
calls: s.calls
|
|
1890
|
+
})).sort((a, b) => b.durationMs - a.durationMs);
|
|
1891
|
+
return { totalMs, phases: ordered };
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function noopTimer() {
|
|
1896
|
+
return {
|
|
1897
|
+
time(_label, fn) {
|
|
1898
|
+
return fn();
|
|
1899
|
+
},
|
|
1900
|
+
async timeAsync(_label, fn) {
|
|
1901
|
+
return fn();
|
|
1902
|
+
},
|
|
1903
|
+
report() {
|
|
1904
|
+
return { totalMs: 0, phases: [] };
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1011
1909
|
// src/index.ts
|
|
1012
1910
|
function valuesOfOutput(output) {
|
|
1013
1911
|
if (output.type === "response" && output.statusCode !== null) {
|
|
@@ -1020,7 +1918,7 @@ function makeTransitionId(functionName, branch) {
|
|
|
1020
1918
|
const { terminal } = branch;
|
|
1021
1919
|
const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
|
|
1022
1920
|
const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
|
|
1023
|
-
const conditionHash =
|
|
1921
|
+
const conditionHash = createHash3("sha1").update(conditionSig).digest("hex").slice(0, 7);
|
|
1024
1922
|
return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
|
|
1025
1923
|
}
|
|
1026
1924
|
function collapseReading(table, reading) {
|
|
@@ -1423,6 +2321,7 @@ function paramToInput(param) {
|
|
|
1423
2321
|
export {
|
|
1424
2322
|
IdMap,
|
|
1425
2323
|
IdSet,
|
|
2324
|
+
MAX_ENTRIES,
|
|
1426
2325
|
MAX_PATHS,
|
|
1427
2326
|
PathBudgetExceeded,
|
|
1428
2327
|
SKIP_CHILDREN,
|
|
@@ -1432,25 +2331,44 @@ export {
|
|
|
1432
2331
|
andThenReading,
|
|
1433
2332
|
assembleSummary,
|
|
1434
2333
|
assessConfidence,
|
|
2334
|
+
buildUngatedExtractionReport,
|
|
1435
2335
|
composeWrappers,
|
|
2336
|
+
computeContentHash,
|
|
2337
|
+
computeDistHashFrom,
|
|
1436
2338
|
configuredCallOption,
|
|
2339
|
+
createAdapterStamp,
|
|
2340
|
+
createCacheLayer,
|
|
2341
|
+
createPackTallies,
|
|
2342
|
+
createTimer,
|
|
1437
2343
|
detectGaps,
|
|
1438
2344
|
effectToIR,
|
|
2345
|
+
emptyTally,
|
|
1439
2346
|
enumerateOrDegrade,
|
|
1440
2347
|
enumerateStructuredPaths,
|
|
2348
|
+
evaluatePackHealth,
|
|
1441
2349
|
firstWrittenReading,
|
|
2350
|
+
formatPackHealth,
|
|
1442
2351
|
guardsHoldOn,
|
|
1443
2352
|
httpRouteDiscovery,
|
|
1444
2353
|
makeTransitionId,
|
|
1445
2354
|
mapReading,
|
|
2355
|
+
moduleInitStructure,
|
|
2356
|
+
noopTimer,
|
|
2357
|
+
packGradients,
|
|
1446
2358
|
paramToInput,
|
|
2359
|
+
projectFileStamp,
|
|
1447
2360
|
rawConditionToPredicate,
|
|
2361
|
+
recordPackFailure,
|
|
1448
2362
|
registrationHelperDiscovery,
|
|
1449
2363
|
routeHelperIndex,
|
|
2364
|
+
runDigest,
|
|
1450
2365
|
runsBefore,
|
|
1451
2366
|
scopeOption,
|
|
1452
2367
|
sharedGatingConditions,
|
|
2368
|
+
stampModuleImports,
|
|
1453
2369
|
storageSystemOption,
|
|
2370
|
+
summaryCountsByPack,
|
|
2371
|
+
tallyUnit,
|
|
1454
2372
|
terminalToOutput,
|
|
1455
2373
|
unreadableReading,
|
|
1456
2374
|
unwrapJsonStringify,
|