@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/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(path, home, isAncestorOrSelf) {
159
- return path.map((cond) => {
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, path) {
224
+ function recordTerminal(ctx, terminal, home, path3) {
225
225
  chargeBudget(ctx.state);
226
- const info = classify(path, home, ctx.isAncestorOrSelf);
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, path, ctx) {
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
- ...path,
241
+ ...path3,
242
242
  branchCond(stmt, stmt.condition, "positive", elseExit)
243
243
  ];
244
244
  const elsePath = [
245
- ...path,
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, path, ctx) {
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 ? [...path, ...negations] : [...path, fixedCond(group.condition, "positive", "explicit")];
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([...path, ...negations]);
289
+ out.push([...path3, ...negations]);
290
290
  }
291
291
  return out;
292
292
  }
293
- function stepLoop(stmt, path, ctx) {
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
- ...path,
297
+ ...path3,
298
298
  loopIterationCond(stmt.condition.sourceText)
299
299
  ]);
300
300
  }
301
301
  if (bodyExit !== null) {
302
- return [[...path, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
302
+ return [[...path3, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
303
303
  }
304
- return [path];
304
+ return [path3];
305
305
  }
306
- function stepTry(stmt, path, ctx) {
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, path));
316
+ out.push(...enumerate(ctx, stmt.tryBody, path3));
317
317
  if (stmt.catchBody !== null) {
318
- const catchPath = [...path, catchEntryCond()];
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, path, ctx) {
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(path);
326
+ collector.push(path3);
327
327
  }
328
328
  return [];
329
329
  }
330
- function stepOpaque(_stmt, path, _ctx) {
331
- return [path];
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, path) {
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, path);
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 ? [path] : out;
351
+ return out.length === 0 ? [path3] : out;
352
352
  }
353
- function stepStatement(ctx, stmt, path) {
354
- let paths = [path];
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 path of mergeRejoined(frontiers)) {
436
- nextFrontiers.push(...stepStatement(ctx, stmt, path));
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 path of continuations) {
487
+ for (const path3 of continuations) {
488
488
  chargeBudget(state);
489
- state.fallthrough.push(classify(path, null, isAncestorOrSelf));
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 path of rest) {
882
- const here = new Set(path.map(keyOf2));
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 = createHash("sha1").update(conditionSig).digest("hex").slice(0, 7);
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,