@suss/extractor 0.21.1 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1893,6 +1893,50 @@ declare function guardsHoldOn(preconditions: readonly RawCondition[] | undefined
1893
1893
  */
1894
1894
  declare function runsBefore(effectLine: number, terminalEndLine: number): boolean;
1895
1895
 
1896
+ /**
1897
+ * moduleImports.ts: writing `metadata.moduleImports` on a summary set.
1898
+ *
1899
+ * Every adapter records the project files a summary's own file depends
1900
+ * on, and a checker rebuilds the module graph from that field to find
1901
+ * what runs inside a deployable unit. The adapters differ in where the
1902
+ * edges come from (TypeScript resolves import declarations, Python
1903
+ * resolves them through facts, Ruby has `require_relative` and constant
1904
+ * references), so the stamp takes a lookup and leaves the source to them.
1905
+ *
1906
+ * The paths a lookup returns must be spelled the way `location.file` is,
1907
+ * so both ends of the graph match. An empty list is stamped as one, which
1908
+ * makes the file a leaf of the graph; undefined leaves the summary alone.
1909
+ */
1910
+
1911
+ declare function stampModuleImports(summaries: BehavioralSummary[], importsOf: (file: string) => Iterable<string> | undefined): void;
1912
+
1913
+ /**
1914
+ * moduleInit.ts: the raw structure for what a module does when it loads.
1915
+ *
1916
+ * A module's top-level statements run once, when it is first imported,
1917
+ * and a service that reads its configuration there reads it there for
1918
+ * good. Walking unit bodies alone never sees that read, so the read gets
1919
+ * a unit of its own, one per file, named after the file, with no
1920
+ * boundary. Attributing it to each handler in the file would report one
1921
+ * read as several, and a file with no handler would still report nothing.
1922
+ *
1923
+ * The structure is one default branch whose terminal is `void`, because
1924
+ * module initialization returns to nobody. Every adapter builds it the
1925
+ * same way, so it lives here.
1926
+ */
1927
+
1928
+ interface ModuleInitOptions {
1929
+ /** The unit name, which is the file's base name in every adapter. */
1930
+ name: string;
1931
+ file: string;
1932
+ range: {
1933
+ start: number;
1934
+ end: number;
1935
+ };
1936
+ effects: Effect[];
1937
+ }
1938
+ declare function moduleInitStructure(options: ModuleInitOptions): RawCodeStructure;
1939
+
1896
1940
  /**
1897
1941
  * Small helpers for pattern packs that are built the same way.
1898
1942
  *
@@ -2207,6 +2251,605 @@ interface LanguageAdapter {
2207
2251
  extractAll(): Promise<BehavioralSummary[]>;
2208
2252
  }
2209
2253
 
2254
+ /**
2255
+ * adapterStamp.ts: the language-neutral half of an adapter's cache key.
2256
+ *
2257
+ * An adapter's own version.ts calls `createAdapterStamp` with its own
2258
+ * `import.meta.url` and its hand-bumped version, and keeps the result for
2259
+ * the life of the process. The stamp hashes the adapter's own dist file
2260
+ * plus every analysis package it ships beside (this package,
2261
+ * `@suss/resolution`, `@suss/datalog`, `@suss/behavioral-ir`), so a
2262
+ * release that changes any of them invalidates an older cache, and a
2263
+ * dev rebuild invalidates on its own with no version bump by hand.
2264
+ * Running from source has no dist file to hash, and that mode declines
2265
+ * to cache. The extraction cache's design is in this package's README.
2266
+ */
2267
+ /**
2268
+ * Whether this process can see the adapter's own code. `bundle` includes
2269
+ * a hash that changes whenever the code does. `source` means nothing
2270
+ * here could find it, so a cache key built from this stamp says nothing
2271
+ * about the code that will produce the results.
2272
+ */
2273
+ type AdapterCodeStamp = {
2274
+ kind: "bundle";
2275
+ hash: string;
2276
+ } | {
2277
+ kind: "source";
2278
+ };
2279
+ /** What `createAdapterStamp` hands back to an adapter's own version.ts. */
2280
+ interface AdapterStamp {
2281
+ /** The stamp for the running adapter, computed once per process. */
2282
+ codeStamp(): AdapterCodeStamp;
2283
+ /**
2284
+ * Cache-friendly identity for this adapter plus its packs. Stable
2285
+ * across processes given the same inputs; bumps when any pack arrives
2286
+ * with a new version stamp, the adapter version changes, or the
2287
+ * loaded adapter dist file changes (dev-mode rebuild auto-invalidation).
2288
+ */
2289
+ packsDigest(packVersions: ReadonlyArray<{
2290
+ name: string;
2291
+ version?: string;
2292
+ }>): string;
2293
+ /**
2294
+ * `cacheDir` unless this process loaded the adapter from source, where
2295
+ * nothing can tell one build of it from another; then this returns
2296
+ * null and, once per process, says why on stderr.
2297
+ */
2298
+ declineWhenRunFromSource(cacheDir: string | null): string | null;
2299
+ }
2300
+ /**
2301
+ * Build the code stamp and packs digest for one adapter. `moduleUrl` has
2302
+ * to be the calling module's own `import.meta.url`, so the stamp finds
2303
+ * the dist file the adapter itself was loaded from rather than some
2304
+ * other package's.
2305
+ */
2306
+ declare function createAdapterStamp(config: {
2307
+ moduleUrl: string;
2308
+ version: string;
2309
+ }): AdapterStamp;
2310
+ /**
2311
+ * The hash for a bundle directory: the bundle itself plus every analysis
2312
+ * package that can be located. Empty when the directory has no bundle in
2313
+ * it, which is what running from source looks like.
2314
+ */
2315
+ declare function computeDistHashFrom(dir: string): string;
2316
+ /**
2317
+ * Content hash of the given files, in the order they arrive. Empty when
2318
+ * the list is empty or a file cannot be read, so a caller that could not
2319
+ * locate a file gets the same "no stamp" result as a run from source
2320
+ * rather than a hash of a shorter list.
2321
+ *
2322
+ * The caller resolves the paths, because where a specifier resolves to
2323
+ * depends on which package is asking.
2324
+ */
2325
+ declare function computeContentHash(paths: readonly string[]): string;
2326
+ /**
2327
+ * Stamp for a set of project files a run reads without walking them: a
2328
+ * SAM template, a workspace package.json. Both the paths and the
2329
+ * content go in, so a file that moves counts as a change even when
2330
+ * every byte in it stayed the same.
2331
+ *
2332
+ * A file that cannot be read stamps as absent rather than voiding the
2333
+ * whole stamp, which is what `computeContentHash` does. These files
2334
+ * belong to the project rather than to the installed tool: one of them
2335
+ * being gone is a fact about the project the next run should notice,
2336
+ * not a reason to stop telling runs apart.
2337
+ */
2338
+ declare function projectFileStamp(paths: readonly string[]): string;
2339
+ /**
2340
+ * The digest a run looks its cache entry up under. A pack may read
2341
+ * project files that are not among the ones a run walks, a SAM template
2342
+ * that decides which handlers exist for instance, so those belong in
2343
+ * the key next to the pack's own config. Which files they are depends
2344
+ * on the files this run walks, so the digest is settled per run rather
2345
+ * than once per adapter.
2346
+ */
2347
+ declare function runDigest(packsDigest: string, packs: ReadonlyArray<{
2348
+ discoveryInputs?: (files: readonly string[]) => string[];
2349
+ }>, files: readonly string[]): string;
2350
+
2351
+ /**
2352
+ * The on-disk extraction cache, shared by every language adapter.
2353
+ *
2354
+ * A run reuses the previous one's summaries whole when nothing
2355
+ * changed, and per file when some files did. The entry directory is
2356
+ * named after everything that has to agree before any reuse is sound:
2357
+ * schema version, adapter version and code hash, pack versions, the
2358
+ * extraction config stamp and the config file path. The manifest
2359
+ * inside records a stamp and content hash per file, each summary's
2360
+ * owning files, and per owning file the other files its walk read.
2361
+ * An adapter attaches whatever it needs to a cached file's record
2362
+ * through `meta`, which the cache stores and returns opaquely. The
2363
+ * full design is in this package's README.
2364
+ */
2365
+
2366
+ /**
2367
+ * How many keys' worth of entries a cache directory keeps. Two lets a
2368
+ * pair of builds alternate (a branch switch, an adapter rebuild and a
2369
+ * revert) without either losing its entry, and bounds a directory at
2370
+ * twice one manifest.
2371
+ */
2372
+ declare const MAX_ENTRIES = 2;
2373
+ /**
2374
+ * What one walked file contributed to the run, and what its walk read.
2375
+ * `deps` are the other files whose content went into this file's
2376
+ * summaries; a change to any of them re-extracts this file. `claims`
2377
+ * are the units this file's walk claimed, replayed before a partial
2378
+ * run walks anything so precedence comes out the same. `meta` is
2379
+ * whatever else the adapter needs to revalidate this file on a
2380
+ * partial run (a route's mount prefixes, for instance); the cache
2381
+ * never reads it. A file marked `cacheable: false` recorded a
2382
+ * dependency the cache cannot pin to files, and is re-extracted on
2383
+ * every partial run.
2384
+ */
2385
+ interface RootRecord<Meta = unknown> {
2386
+ path: string;
2387
+ cacheable: boolean;
2388
+ deps: string[];
2389
+ claims: {
2390
+ key: string;
2391
+ pack: string;
2392
+ }[];
2393
+ meta: Meta;
2394
+ /** Packs that applied to the file, re-checked on a partial run. */
2395
+ packs: string[];
2396
+ }
2397
+ /**
2398
+ * Which files each summary belongs to. `owners[i]` lists the walked
2399
+ * files whose reuse keeps `summaries[i]` alive; an empty list marks a
2400
+ * run-level summary that a partial run always recomputes.
2401
+ */
2402
+ interface CacheAttribution<Meta = unknown> {
2403
+ roots: RootRecord<Meta>[];
2404
+ owners: string[][];
2405
+ }
2406
+ /** Reported by `lookup`: what the cache decided, and why. */
2407
+ interface CacheDiagnostic {
2408
+ kind: "hit" | "miss" | "partial";
2409
+ /**
2410
+ * Reason the lookup missed (only set when kind === "miss").
2411
+ * `key-changed` means the cache directory contains entries, but none
2412
+ * under this run's schema, adapter, packs and config path.
2413
+ * `files-changed` means the include set is not the one the entry
2414
+ * was written from.
2415
+ */
2416
+ missReason?: "no-manifest" | "key-changed" | "config-changed" | "files-changed";
2417
+ /** Set when kind === "partial". */
2418
+ partial?: {
2419
+ filesChanged: number;
2420
+ filesRemoved: number;
2421
+ rootsReused: number;
2422
+ rootsReextracted: number;
2423
+ rootsDeclined: number;
2424
+ summariesReused: number;
2425
+ };
2426
+ }
2427
+ /**
2428
+ * Result of a cache lookup. A hit gives back the whole summary set; a miss
2429
+ * says why, so a caller can render the reason.
2430
+ */
2431
+ type CacheLookup = {
2432
+ kind: "hit";
2433
+ summaries: BehavioralSummary[];
2434
+ diagnostic: CacheDiagnostic;
2435
+ } | {
2436
+ kind: "miss";
2437
+ diagnostic: CacheDiagnostic;
2438
+ };
2439
+ /**
2440
+ * What a `files-changed` miss can still reuse. `validRoots` is the
2441
+ * cache's own verdict from hashes and recorded dependencies; the
2442
+ * caller may demote further (a mount prefix that no longer matches)
2443
+ * before calling `reuse`. `reuse` returns the summaries owned by at
2444
+ * least one surviving root, in stored order, with their owners, so
2445
+ * the caller can merge them and write the result back.
2446
+ */
2447
+ interface PartialPlan<Meta = unknown> {
2448
+ /** Paths whose content hash differs, plus paths new to the set. */
2449
+ changed: Set<string>;
2450
+ removed: Set<string>;
2451
+ roots: Map<string, RootRecord<Meta>>;
2452
+ validRoots: Set<string>;
2453
+ rootsDeclined: number;
2454
+ reuse(valid: ReadonlySet<string>): {
2455
+ summaries: BehavioralSummary[];
2456
+ owners: string[][];
2457
+ };
2458
+ allSummaries(): BehavioralSummary[];
2459
+ /** The stored attribution decoded, for a write that changes nothing. */
2460
+ attribution(): CacheAttribution<Meta>;
2461
+ }
2462
+ interface CacheLayer<Meta = unknown> {
2463
+ /** The summary list on a hit, null on a miss. */
2464
+ tryHit(input: CacheInput): Promise<BehavioralSummary[] | null>;
2465
+ /**
2466
+ * The lookup behind `tryHit`, with the reason a miss missed. Costs
2467
+ * stats alone: no file reads, no AST work.
2468
+ */
2469
+ lookup(input: CacheInput): Promise<CacheLookup>;
2470
+ /**
2471
+ * After a `files-changed` miss: hash what the stats said moved and
2472
+ * work out which files' summaries survive. Null when the entry has
2473
+ * no per-file layer to reuse, or no entry matches the key at all.
2474
+ */
2475
+ plan(input: CacheInput): Promise<PartialPlan<Meta> | null>;
2476
+ /**
2477
+ * Persist a fresh extraction's summaries to the cache, keyed
2478
+ * against the same file list. Subsequent `lookup` calls with the
2479
+ * same files return them. Without `attribution` the entry can only
2480
+ * ever be reused whole.
2481
+ */
2482
+ write(input: CacheInput, summaries: BehavioralSummary[], attribution?: CacheAttribution<Meta>): Promise<void>;
2483
+ }
2484
+ interface CacheInput {
2485
+ /** Absolute paths of every file the run walks. */
2486
+ files: ReadonlyArray<string>;
2487
+ adapterPacksDigest: string;
2488
+ /**
2489
+ * One file whose stamp guards the whole entry alongside the file
2490
+ * list, such as a tsconfig or a project manifest. Optional because
2491
+ * not every adapter has one.
2492
+ */
2493
+ configPath?: string;
2494
+ }
2495
+ /**
2496
+ * Construct a cache layer rooted at `cacheDir`. Pass `null` to
2497
+ * opt out of caching entirely, the returned layer's `tryHit`
2498
+ * always misses and `write` is a no-op. Useful for one-shot
2499
+ * extracts where caching adds latency without payoff.
2500
+ */
2501
+ declare function createCacheLayer<Meta = unknown>(cacheDir: string | null): CacheLayer<Meta>;
2502
+
2503
+ /**
2504
+ * The extraction funnel, shared across languages.
2505
+ *
2506
+ * "Why did this run produce nothing" is always "at which stage did the
2507
+ * count reach zero", so the report is a funnel: files in the tsconfig,
2508
+ * files the import gates selected, units discovered, summaries built.
2509
+ * Each row is recorded by the stage that owns it, because the
2510
+ * alternative is the CLI re-deriving the pre-filter's decisions from a
2511
+ * second copy of its logic, and a second copy drifting from the first
2512
+ * is what made an entire pack family extract nothing in silence.
2513
+ *
2514
+ * TypeScript, Python and Ruby all build one of these, though only
2515
+ * TypeScript gates files by import specifier today. A pack with no gate
2516
+ * concept reports it the way TypeScript reports an ungated pack.
2517
+ */
2518
+
2519
+ /** One pack's path through the funnel. */
2520
+ interface PackFunnel {
2521
+ pack: string;
2522
+ /**
2523
+ * What the pack calls this build of itself, or null when it declares
2524
+ * nothing. The cache keys on it, so a pack that never changes its
2525
+ * stamp can serve a later run with an earlier build's results.
2526
+ */
2527
+ version: string | null;
2528
+ /**
2529
+ * Whether the pack looks for units of its own at all. A pack made only
2530
+ * of recognisers contributes effects to units other packs found, so it
2531
+ * always discovers nothing, and that tells you nothing about whether
2532
+ * it is working.
2533
+ */
2534
+ discovers: boolean;
2535
+ /**
2536
+ * Whether the pack has any recognisers. Those fire inside units other
2537
+ * packs discovered, so a pack made only of them contributes effects
2538
+ * and never a summary.
2539
+ */
2540
+ recognizes: boolean;
2541
+ /**
2542
+ * Import specifiers that make a file relevant to this pack. Empty
2543
+ * for an ungated pack, which walks every file.
2544
+ */
2545
+ gates: string[];
2546
+ /**
2547
+ * Gate specifiers that do not resolve from the project. Non-empty
2548
+ * here usually means the target project's dependencies are not
2549
+ * installed, which stops symbol-resolution packs while leaving
2550
+ * textual-gate packs working, so the run half-succeeds in a way
2551
+ * nothing else surfaces.
2552
+ */
2553
+ unresolvedGates: string[];
2554
+ /** Files the pre-filter selected for this pack. */
2555
+ candidateFiles: number;
2556
+ /** Units discovered across those files. */
2557
+ unitsDiscovered: number;
2558
+ /**
2559
+ * Unit bodies any pack walked in the files this pack's gate selected.
2560
+ *
2561
+ * This is what a recogniser pack had the chance to fire on. Its own
2562
+ * discovery count tells you nothing, since it discovers nothing by
2563
+ * design, and neither does its candidate-file count, because a
2564
+ * recogniser only runs where some pack found a unit to walk.
2565
+ */
2566
+ unitsInGatedFiles: number;
2567
+ /** Effects this pack's recognisers returned. */
2568
+ effectsRecognized: number;
2569
+ /**
2570
+ * Units this pack kept. A unit an earlier pack already claimed is
2571
+ * dropped here, so a pack can discover plenty and keep none when it
2572
+ * comes after a pack that recognises the same code.
2573
+ */
2574
+ unitsClaimed: number;
2575
+ /**
2576
+ * Units this pack discovered twice over. Cross-pack dedup is the
2577
+ * point of the claim set, but a pack colliding with itself means its
2578
+ * own discovery patterns overlap and one of the two is being thrown
2579
+ * away without anybody deciding which.
2580
+ */
2581
+ selfCollisions: number;
2582
+ /** Summaries built from those units. */
2583
+ summariesProduced: number;
2584
+ /** Summaries in the finished run that credit this pack for finding them. */
2585
+ summariesBound: number;
2586
+ /**
2587
+ * Bound summaries on the provider side of their boundary.
2588
+ *
2589
+ * Kept apart from the count below because a provider that produced
2590
+ * no transitions is a different problem from a client that produced
2591
+ * none.
2592
+ */
2593
+ providerSummaries: number;
2594
+ /**
2595
+ * Summaries with at least one transition, of any role. A provider's
2596
+ * transitions say what it does with a request and a client's say what
2597
+ * it expects back, so a pack whose summaries have none has bound
2598
+ * something and described nothing either way.
2599
+ */
2600
+ summariesWithBehavior: number;
2601
+ /**
2602
+ * Where one of this pack's hooks threw. Every count above is a floor
2603
+ * while this is non-empty.
2604
+ */
2605
+ failures: PackFailure[];
2606
+ /**
2607
+ * Registration helpers this pack's config asked for that no call in
2608
+ * the run matched. The helper belongs to the project, so a spelling
2609
+ * that matches nothing is a config mistake with no other symptom: the
2610
+ * routes go missing and every count reads the same as a project
2611
+ * without them.
2612
+ */
2613
+ helpersUnmatched: string[];
2614
+ /**
2615
+ * What the pack wrote as data rather than as code, or null for a pack
2616
+ * written as a hand-rolled walk, or for a language whose packs have
2617
+ * no declared-pattern system at all. This is the one thing in the
2618
+ * funnel that no run produces: it is the pack's own shape, and it is
2619
+ * here so the migration onto the declared surface can be measured.
2620
+ */
2621
+ declarations: PackDeclarations | null;
2622
+ }
2623
+ interface ExtractionReport {
2624
+ /** Files in the project's include set, or null when nothing states one separately from the walk. */
2625
+ filesInProject: number | null;
2626
+ /** Files the adapter loaded and the extract walked. */
2627
+ filesWalked: number;
2628
+ packs: PackFunnel[];
2629
+ summaries: number;
2630
+ /**
2631
+ * Files whose exports the checker could not follow, so the run read
2632
+ * them as exporting nothing.
2633
+ *
2634
+ * Without this the artifact cannot tell the two apart: a module whose
2635
+ * barrel chain outran the call stack and a module that really does
2636
+ * export nothing both produce no summaries and exit 0.
2637
+ * Anything reachable only through these files is missing, and every
2638
+ * count below is a floor while this is non-empty.
2639
+ */
2640
+ filesWithUnreadableExports: string[];
2641
+ /**
2642
+ * The first stage whose count was zero, when the run produced
2643
+ * nothing. Null when the run produced summaries.
2644
+ */
2645
+ emptyStage: EmptyStage | null;
2646
+ /**
2647
+ * Reassigned names the run stated nothing for, because control flow
2648
+ * decides which write a reader sees. Each is a value resolution the
2649
+ * facts decline; the count across a large corpus says whether scoped
2650
+ * reaching definitions is worth writing.
2651
+ */
2652
+ reassignedNamesUnstated: number;
2653
+ }
2654
+ type EmptyStage = "tsconfig" | "gateResolution" | "candidateFiles" | "discovery" | "assembly";
2655
+ /**
2656
+ * A pack's hook throwing on one file.
2657
+ *
2658
+ * The run continues with the other files, so every count for that pack
2659
+ * afterwards is a floor rather than a total. Somebody reading those
2660
+ * counts has to be told, or a pack that broke looks the same as a pack
2661
+ * that looked and found nothing.
2662
+ */
2663
+ interface PackFailure {
2664
+ /** The hook that threw, called what a pack author would call it. */
2665
+ hook: string;
2666
+ /** The file the pack was reading. */
2667
+ file: string;
2668
+ message: string;
2669
+ }
2670
+ /** Per-pack running counts, filled as the extract proceeds. */
2671
+ interface PackTally {
2672
+ candidateFiles: number;
2673
+ unitsDiscovered: number;
2674
+ unitsInGatedFiles: number;
2675
+ effectsRecognized: number;
2676
+ unitsClaimed: number;
2677
+ selfCollisions: number;
2678
+ summariesProduced: number;
2679
+ failures: PackFailure[];
2680
+ /** Registration helpers from this pack's config that produced a unit. */
2681
+ helpersMatched: Set<string>;
2682
+ }
2683
+ declare const emptyTally: () => PackTally;
2684
+ /**
2685
+ * Record that a pack's hook threw, and phrase it in one sentence a caller
2686
+ * can print. Both callers want the same wording, and a failure that only
2687
+ * reached stderr left the counts looking like an empty pack.
2688
+ */
2689
+ declare function recordPackFailure(tally: PackTally | undefined, failure: {
2690
+ pack: string;
2691
+ hook: string;
2692
+ file: string;
2693
+ error: unknown;
2694
+ }): string;
2695
+ declare function createPackTallies(packs: ReadonlyArray<{
2696
+ name: string;
2697
+ }>): Map<string, PackTally>;
2698
+ /** Credit the pack whose name a raw unit's boundary binding gives as `recognition`. */
2699
+ declare function tallyUnit(tallies: Map<string, PackTally>, recognition: string | undefined): void;
2700
+ /**
2701
+ * The summary-side funnel counts, per pack.
2702
+ *
2703
+ * These are read back off the finished run rather than tallied during
2704
+ * it, because the summaries a pack is responsible for are not all built
2705
+ * where the pack is in scope: wrapper expansion and sub-unit synthesis
2706
+ * both add summaries after discovery has moved on. Every summary
2707
+ * records what recognised it, so grouping on that gets each one back to
2708
+ * the pack that owns it however late it arrived.
2709
+ */
2710
+ interface SummaryCounts {
2711
+ bound: number;
2712
+ providers: number;
2713
+ withBehavior: number;
2714
+ }
2715
+ declare function summaryCountsByPack(summaries: ReadonlyArray<BehavioralSummary>): Map<string, SummaryCounts>;
2716
+ /**
2717
+ * An extraction report for an adapter with no gate stage of its own,
2718
+ * the way TypeScript reports an ungated pack: every file is a
2719
+ * candidate, and `unitsClaimed` tracks `unitsDiscovered` one for one
2720
+ * because nothing dedups across packs yet.
2721
+ */
2722
+ declare function buildUngatedExtractionReport(args: {
2723
+ packs: ReadonlyArray<{
2724
+ name: string;
2725
+ version: string | null;
2726
+ discovers: boolean;
2727
+ }>;
2728
+ tallies: ReadonlyMap<string, PackTally>;
2729
+ filesWalked: number;
2730
+ summaries: ReadonlyArray<BehavioralSummary>;
2731
+ }): ExtractionReport;
2732
+
2733
+ /**
2734
+ * Checks that tell you a pack is probably not working.
2735
+ *
2736
+ * The funnel shows where a run's counts dropped to zero. These checks
2737
+ * ask something narrower of each pack on its own: did this pack lose
2738
+ * everything at some stage, when the stage before it had something?
2739
+ *
2740
+ * A pack that finds nothing in a codebase that does not use its library
2741
+ * is working correctly, so a count of zero on its own is never the
2742
+ * signal. What makes zero a signal is the count before it. A pack whose
2743
+ * import gate picked forty files and whose discovery then found no unit
2744
+ * in any of them said "look here" and failed to look. Every later stage
2745
+ * works the same way, which is why these checks are one comparison run
2746
+ * over a list of pairs rather than one check written per stage.
2747
+ */
2748
+
2749
+ /** One thing that looks wrong, reported the way the dogfood invariants are. */
2750
+ interface HealthViolation {
2751
+ label: string;
2752
+ detail: string;
2753
+ }
2754
+ interface HealthCheck {
2755
+ /** The property being checked, as something either true or false. */
2756
+ name: string;
2757
+ /**
2758
+ * The short word this check reports under, kebab-case.
2759
+ *
2760
+ * It is the first column of every line the check prints, so a reader
2761
+ * works out what it means once and recognises it after that. `name`
2762
+ * is written as an assertion and would read backwards over a list of
2763
+ * things failing it.
2764
+ */
2765
+ code: string;
2766
+ /**
2767
+ * Who the finding is addressed to.
2768
+ *
2769
+ * A `run` check found something about the code in front of it, and
2770
+ * the person who started the run can do something about it: drop a
2771
+ * pack, install a dependency, open an issue with the file that broke.
2772
+ * A `pack` check found something about how a pack was built, which
2773
+ * only whoever ships that pack can fix. Printing the second kind on
2774
+ * every run would teach people to skim past the first.
2775
+ */
2776
+ audience: "run" | "pack";
2777
+ violations: HealthViolation[];
2778
+ }
2779
+ /**
2780
+ * What one pack paid for what it matches.
2781
+ *
2782
+ * Expressiveness is bought link by link, and #542 asks for the price to
2783
+ * be printed. A pack with every link written as data runs on any
2784
+ * adapter with the executor ops. A link written as a function runs only
2785
+ * where its own language does, and one that reads the syntax tree is
2786
+ * the floor. All three are allowed, and this says which is which.
2787
+ */
2788
+ interface PackGradient {
2789
+ pack: string;
2790
+ /** Links written as data, across every declaration. */
2791
+ dataLinks: number;
2792
+ /** Links written as a function, as "declaration.question". */
2793
+ functionLinks: string[];
2794
+ /** Links whose function reads the adapter's own syntax tree. */
2795
+ astLinks: string[];
2796
+ /** Declarations shipped without a line of code to run against them. */
2797
+ withoutExample: string[];
2798
+ }
2799
+ /** The gradient for every pack in a run that declared anything. */
2800
+ declare function packGradients(report: ExtractionReport): PackGradient[];
2801
+ /**
2802
+ * Run every health check over one extraction report and return what
2803
+ * fired, grouped by which check caught it.
2804
+ */
2805
+ declare function evaluatePackHealth(report: ExtractionReport): HealthCheck[];
2806
+ /**
2807
+ * The health checks that fired, as lines for a terminal.
2808
+ *
2809
+ * `audiences` is who the caller is printing for. It is required because
2810
+ * no single choice suits every caller: a CLI run prints what the person
2811
+ * who started it can act on, while a run a pack author is reading wants
2812
+ * both kinds.
2813
+ */
2814
+ declare function formatPackHealth(checks: ReadonlyArray<HealthCheck>, audiences: ReadonlyArray<HealthCheck["audience"]>): string;
2815
+
2816
+ /**
2817
+ * Lightweight phase-timing instrumentation.
2818
+ *
2819
+ * Measures wall time spent in named phases of an adapter run so the
2820
+ * CLI can surface "extract took 17s, of which 11s was in the
2821
+ * reachable closure" without engaging a profiler. Cheap enough to
2822
+ * always be on; a phase that isn't entered contributes zero.
2823
+ *
2824
+ * Not OpenTelemetry: this is single-process, stdout-bound, and should
2825
+ * not ship transitively to consumers. If suss ever becomes a
2826
+ * long-running daemon (LSP / file-watcher), revisit then.
2827
+ */
2828
+ interface TimingReport {
2829
+ totalMs: number;
2830
+ phases: Array<{
2831
+ label: string;
2832
+ durationMs: number;
2833
+ calls: number;
2834
+ }>;
2835
+ }
2836
+ interface Timer {
2837
+ /** Run `fn`, accumulate wall time under `label`, return its result. */
2838
+ time<T>(label: string, fn: () => T): T;
2839
+ /** Async variant: same accumulation rule. */
2840
+ timeAsync<T>(label: string, fn: () => Promise<T>): Promise<T>;
2841
+ /** Snapshot of all accumulated phases, ordered by total time descending. */
2842
+ report(): TimingReport;
2843
+ }
2844
+ /**
2845
+ * Build a fresh timer. Each adapter run gets its own, keeps
2846
+ * concurrent extracts independent (irrelevant today, will matter if
2847
+ * we add a server mode).
2848
+ */
2849
+ declare function createTimer(): Timer;
2850
+ /** Null implementation for callers that opt out of timing entirely. */
2851
+ declare function noopTimer(): Timer;
2852
+
2210
2853
  /**
2211
2854
  * The vocabulary a language adapter implements so that any declared
2212
2855
  * pack runs on it.
@@ -2757,4 +3400,4 @@ declare function rawConditionToPredicate(c: RawCondition): Predicate;
2757
3400
  declare function effectToIR(effect: RawEffect): Effect;
2758
3401
  declare function paramToInput(param: RawParameter): Input;
2759
3402
 
2760
- export { type AccessRecognizer, type AstCapableOps, type BindingExtraction, type BodyContent, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type ExitKind, type ExtractorOptions, type FailureDelivery, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_PATHS, type NodeVisitor, type OpsCarrier, type PackDeclarations, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, composeWrappers, configuredCallOption, detectGaps, effectToIR, enumerateOrDegrade, enumerateStructuredPaths, firstWrittenReading, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, paramToInput, rawConditionToPredicate, registrationHelperDiscovery, routeHelperIndex, runsBefore, scopeOption, sharedGatingConditions, storageSystemOption, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };
3403
+ export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };