@dep-radius/core 0.1.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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/analyze/brief.d.ts +18 -0
  4. package/dist/analyze/brief.d.ts.map +1 -0
  5. package/dist/analyze/brief.js +89 -0
  6. package/dist/analyze/verdict.d.ts +16 -0
  7. package/dist/analyze/verdict.d.ts.map +1 -0
  8. package/dist/analyze/verdict.js +134 -0
  9. package/dist/context.d.ts +4 -0
  10. package/dist/context.d.ts.map +1 -0
  11. package/dist/context.js +36 -0
  12. package/dist/debug.d.ts +2 -0
  13. package/dist/debug.d.ts.map +1 -0
  14. package/dist/debug.js +130 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +8 -0
  18. package/dist/infra/cache.d.ts +18 -0
  19. package/dist/infra/cache.d.ts.map +1 -0
  20. package/dist/infra/cache.js +66 -0
  21. package/dist/infra/hash.d.ts +4 -0
  22. package/dist/infra/hash.d.ts.map +1 -0
  23. package/dist/infra/hash.js +18 -0
  24. package/dist/infra/http.d.ts +24 -0
  25. package/dist/infra/http.d.ts.map +1 -0
  26. package/dist/infra/http.js +44 -0
  27. package/dist/infra/limit.d.ts +4 -0
  28. package/dist/infra/limit.d.ts.map +1 -0
  29. package/dist/infra/limit.js +34 -0
  30. package/dist/infra/log.d.ts +8 -0
  31. package/dist/infra/log.d.ts.map +1 -0
  32. package/dist/infra/log.js +13 -0
  33. package/dist/inventory/installed.d.ts +15 -0
  34. package/dist/inventory/installed.d.ts.map +1 -0
  35. package/dist/inventory/installed.js +241 -0
  36. package/dist/inventory/lockfiles/bun.d.ts +3 -0
  37. package/dist/inventory/lockfiles/bun.d.ts.map +1 -0
  38. package/dist/inventory/lockfiles/bun.js +24 -0
  39. package/dist/inventory/lockfiles/npm.d.ts +3 -0
  40. package/dist/inventory/lockfiles/npm.d.ts.map +1 -0
  41. package/dist/inventory/lockfiles/npm.js +29 -0
  42. package/dist/inventory/lockfiles/pnpm.d.ts +3 -0
  43. package/dist/inventory/lockfiles/pnpm.d.ts.map +1 -0
  44. package/dist/inventory/lockfiles/pnpm.js +84 -0
  45. package/dist/inventory/lockfiles/types.d.ts +9 -0
  46. package/dist/inventory/lockfiles/types.d.ts.map +1 -0
  47. package/dist/inventory/lockfiles/types.js +1 -0
  48. package/dist/inventory/lockfiles/yarn.d.ts +3 -0
  49. package/dist/inventory/lockfiles/yarn.d.ts.map +1 -0
  50. package/dist/inventory/lockfiles/yarn.js +46 -0
  51. package/dist/inventory/manifests.d.ts +9 -0
  52. package/dist/inventory/manifests.d.ts.map +1 -0
  53. package/dist/inventory/manifests.js +135 -0
  54. package/dist/inventory/specifiers.d.ts +8 -0
  55. package/dist/inventory/specifiers.d.ts.map +1 -0
  56. package/dist/inventory/specifiers.js +53 -0
  57. package/dist/labels.d.ts +2 -0
  58. package/dist/labels.d.ts.map +1 -0
  59. package/dist/labels.js +18 -0
  60. package/dist/model.d.ts +276 -0
  61. package/dist/model.d.ts.map +1 -0
  62. package/dist/model.js +2 -0
  63. package/dist/notes/changelog.d.ts +11 -0
  64. package/dist/notes/changelog.d.ts.map +1 -0
  65. package/dist/notes/changelog.js +60 -0
  66. package/dist/notes/collect.d.ts +13 -0
  67. package/dist/notes/collect.d.ts.map +1 -0
  68. package/dist/notes/collect.js +225 -0
  69. package/dist/notes/entries.d.ts +3 -0
  70. package/dist/notes/entries.d.ts.map +1 -0
  71. package/dist/notes/entries.js +209 -0
  72. package/dist/notes/github.d.ts +25 -0
  73. package/dist/notes/github.d.ts.map +1 -0
  74. package/dist/notes/github.js +132 -0
  75. package/dist/notes/match.d.ts +13 -0
  76. package/dist/notes/match.d.ts.map +1 -0
  77. package/dist/notes/match.js +75 -0
  78. package/dist/notes/repo-url.d.ts +9 -0
  79. package/dist/notes/repo-url.d.ts.map +1 -0
  80. package/dist/notes/repo-url.js +46 -0
  81. package/dist/options.d.ts +53 -0
  82. package/dist/options.d.ts.map +1 -0
  83. package/dist/options.js +27 -0
  84. package/dist/registry/candidates.d.ts +24 -0
  85. package/dist/registry/candidates.d.ts.map +1 -0
  86. package/dist/registry/candidates.js +139 -0
  87. package/dist/registry/npmrc.d.ts +13 -0
  88. package/dist/registry/npmrc.d.ts.map +1 -0
  89. package/dist/registry/npmrc.js +99 -0
  90. package/dist/registry/packument.d.ts +34 -0
  91. package/dist/registry/packument.d.ts.map +1 -0
  92. package/dist/registry/packument.js +85 -0
  93. package/dist/registry/tar.d.ts +2 -0
  94. package/dist/registry/tar.d.ts.map +1 -0
  95. package/dist/registry/tar.js +75 -0
  96. package/dist/registry/tarball.d.ts +16 -0
  97. package/dist/registry/tarball.d.ts.map +1 -0
  98. package/dist/registry/tarball.js +59 -0
  99. package/dist/render/groups.d.ts +16 -0
  100. package/dist/render/groups.d.ts.map +1 -0
  101. package/dist/render/groups.js +105 -0
  102. package/dist/render/index.d.ts +2 -0
  103. package/dist/render/index.d.ts.map +1 -0
  104. package/dist/render/index.js +3 -0
  105. package/dist/render/json.d.ts +119 -0
  106. package/dist/render/json.d.ts.map +1 -0
  107. package/dist/render/json.js +130 -0
  108. package/dist/render/limits.d.ts +3 -0
  109. package/dist/render/limits.d.ts.map +1 -0
  110. package/dist/render/limits.js +25 -0
  111. package/dist/render/markdown.d.ts +3 -0
  112. package/dist/render/markdown.d.ts.map +1 -0
  113. package/dist/render/markdown.js +62 -0
  114. package/dist/run.d.ts +10 -0
  115. package/dist/run.d.ts.map +1 -0
  116. package/dist/run.js +218 -0
  117. package/dist/surface/analyze.d.ts +13 -0
  118. package/dist/surface/analyze.d.ts.map +1 -0
  119. package/dist/surface/analyze.js +166 -0
  120. package/dist/surface/delta.d.ts +5 -0
  121. package/dist/surface/delta.d.ts.map +1 -0
  122. package/dist/surface/delta.js +173 -0
  123. package/dist/surface/entries.d.ts +16 -0
  124. package/dist/surface/entries.d.ts.map +1 -0
  125. package/dist/surface/entries.js +187 -0
  126. package/dist/surface/extract.d.ts +15 -0
  127. package/dist/surface/extract.d.ts.map +1 -0
  128. package/dist/surface/extract.js +460 -0
  129. package/dist/surface/signature.d.ts +6 -0
  130. package/dist/surface/signature.d.ts.map +1 -0
  131. package/dist/surface/signature.js +229 -0
  132. package/dist/surface/vfs-host.d.ts +5 -0
  133. package/dist/surface/vfs-host.d.ts.map +1 -0
  134. package/dist/surface/vfs-host.js +54 -0
  135. package/dist/symbol-path.d.ts +28 -0
  136. package/dist/symbol-path.d.ts.map +1 -0
  137. package/dist/symbol-path.js +223 -0
  138. package/dist/usage/files.d.ts +18 -0
  139. package/dist/usage/files.d.ts.map +1 -0
  140. package/dist/usage/files.js +83 -0
  141. package/dist/usage/link.d.ts +23 -0
  142. package/dist/usage/link.d.ts.map +1 -0
  143. package/dist/usage/link.js +588 -0
  144. package/dist/usage/opaque.d.ts +7 -0
  145. package/dist/usage/opaque.d.ts.map +1 -0
  146. package/dist/usage/opaque.js +193 -0
  147. package/dist/usage/parse.d.ts +73 -0
  148. package/dist/usage/parse.d.ts.map +1 -0
  149. package/dist/usage/parse.js +608 -0
  150. package/dist/usage/resolve-local.d.ts +35 -0
  151. package/dist/usage/resolve-local.d.ts.map +1 -0
  152. package/dist/usage/resolve-local.js +340 -0
  153. package/dist/usage/scan.d.ts +4 -0
  154. package/dist/usage/scan.d.ts.map +1 -0
  155. package/dist/usage/scan.js +92 -0
  156. package/package.json +79 -0
  157. package/schema/brief-v1.schema.json +319 -0
package/dist/run.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type Ctx, type Options, type RunEvent } from "./options.ts";
2
+ import type { Brief } from "./model.ts";
3
+ export declare function toolVersion(): string;
4
+ export interface Spec {
5
+ name: string;
6
+ version?: string;
7
+ }
8
+ export declare function parseSpec(input: string): Spec;
9
+ export declare function run(opts: Options, ctx: Ctx, onEvent?: (e: RunEvent) => void): Promise<Brief>;
10
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,KAAK,GAAG,EAER,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EACV,KAAK,EAMN,MAAM,YAAY,CAAA;AAEnB,wBAAgB,WAAW,IAAI,MAAM,CAQpC;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI7C;AAED,wBAAsB,GAAG,CACvB,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAe,GACxC,OAAO,CAAC,KAAK,CAAC,CA4JhB"}
package/dist/run.js ADDED
@@ -0,0 +1,218 @@
1
+ import { createRequire } from "node:module";
2
+ import semver from "semver";
3
+ import ts from "typescript";
4
+ import { scanUsage } from "./usage/scan.js";
5
+ import { matchNotes } from "./notes/match.js";
6
+ import { limitsFor } from "./render/limits.js";
7
+ import { collectNotes } from "./notes/collect.js";
8
+ import { buildPackageBrief } from "./analyze/brief.js";
9
+ import { getPackument } from "./registry/packument.js";
10
+ import { buildInventory } from "./inventory/installed.js";
11
+ import { selectCandidate } from "./registry/candidates.js";
12
+ import { listProjectFiles } from "./inventory/manifests.js";
13
+ import { isAgeExcluded, loadRegistryConfig } from "./registry/npmrc.js";
14
+ import { analyzeSurface } from "./surface/analyze.js";
15
+ import { DEFAULT_MIN_AGE_MS, } from "./options.js";
16
+ export function toolVersion() {
17
+ try {
18
+ return createRequire(import.meta.url)("../package.json").version;
19
+ }
20
+ catch {
21
+ return "0.0.0";
22
+ }
23
+ }
24
+ export function parseSpec(input) {
25
+ const at = input.lastIndexOf("@");
26
+ if (at > 0)
27
+ return { name: input.slice(0, at), version: input.slice(at + 1) };
28
+ return { name: input };
29
+ }
30
+ export async function run(opts, ctx, onEvent = () => { }) {
31
+ const root = opts.root;
32
+ const files = await listProjectFiles(root);
33
+ const inventory = await buildInventory(root, { prod: opts.prod }, files);
34
+ const cfg = loadRegistryConfig(root, ctx.env);
35
+ const minAgeMs = opts.minAgeMs ?? cfg.minimumReleaseAgeMs ?? DEFAULT_MIN_AGE_MS;
36
+ const specs = opts.specs.map(parseSpec);
37
+ const notAnalyzed = [...inventory.notAnalyzed];
38
+ let targets = inventory.installed;
39
+ if (specs.length > 0) {
40
+ targets = inventory.installed.filter((d) => specs.some((s) => s.name === d.name || d.declaredBy.some((b) => b.key === s.name)));
41
+ for (const s of specs) {
42
+ if (!targets.some((d) => d.name === s.name || d.declaredBy.some((b) => b.key === s.name))) {
43
+ notAnalyzed.push({
44
+ pkg: s.name,
45
+ reason: inventory.local.includes(s.name)
46
+ ? "a local workspace package"
47
+ : "not a dependency of this project",
48
+ });
49
+ }
50
+ }
51
+ }
52
+ const scanned = targets.length > 0
53
+ ? await scanUsage(ctx, root, files, inventory, (done, total) => onEvent({ type: "files", done, total }))
54
+ : emptyUsage();
55
+ let upToDate = 0;
56
+ const briefs = [];
57
+ onEvent({ type: "packages", total: targets.length });
58
+ let finished = 0;
59
+ const analyse = async (dep) => {
60
+ const pack = await getPackument(ctx, cfg, dep.name);
61
+ if (!pack.ok) {
62
+ notAnalyzed.push({
63
+ pkg: dep.name,
64
+ reason: `registry: ${pack.reason}${pack.detail ? ` (${pack.detail})` : ""}`,
65
+ });
66
+ return;
67
+ }
68
+ if (!pack.packument.versions[dep.version] &&
69
+ !dep.flags.includes("not-in-registry"))
70
+ dep.flags.push("not-in-registry");
71
+ const spec = specs.find((s) => s.name === dep.name || dep.declaredBy.some((b) => b.key === s.name));
72
+ const choice = selectCandidate(pack.packument, dep.version, {
73
+ now: ctx.now,
74
+ minAgeMs,
75
+ ageExcluded: (v) => isAgeExcluded(cfg, dep.name, v),
76
+ latest: opts.latest,
77
+ ...(spec?.version ? { explicit: spec.version } : {}),
78
+ });
79
+ if (choice.kind === "error") {
80
+ notAnalyzed.push({ pkg: dep.name, reason: choice.reason });
81
+ return;
82
+ }
83
+ if (choice.kind === "up-to-date") {
84
+ upToDate++;
85
+ return;
86
+ }
87
+ const candidate = choice.candidate;
88
+ const usage = usageFor(scanned, dep, inventory.installed);
89
+ const typesPackageInstalled = inventory.installed.some((d) => d.name === typesPackageName(dep.name));
90
+ const disabled = { status: "disabled", touched: [] };
91
+ const [notes, surface] = await Promise.all([
92
+ opts.notes
93
+ ? collectNotes(ctx, cfg, pack.packument, candidate)
94
+ : Promise.resolve(undefined),
95
+ opts.surface
96
+ ? analyzeSurface(ctx, cfg, pack.packument, candidate, usage, {
97
+ typesPackageInstalled,
98
+ }).catch((error) => ({
99
+ status: "failed",
100
+ detail: String(error).slice(0, 200),
101
+ touched: [],
102
+ }))
103
+ : Promise.resolve(disabled),
104
+ ]);
105
+ const match = notes
106
+ ? matchNotes(notes.entries, usage?.strongNames ?? [], usage?.weakNames ?? [])
107
+ : undefined;
108
+ const { blindSpots: surfaceBlind, ...surfaceOut } = surface;
109
+ const merged = usage && surfaceBlind
110
+ ? { ...usage, blindSpots: [...usage.blindSpots, ...surfaceBlind] }
111
+ : usage;
112
+ briefs.push(buildPackageBrief({
113
+ dep,
114
+ candidate,
115
+ usage: merged,
116
+ surface: surfaceOut,
117
+ notes,
118
+ match,
119
+ notesDisabled: !opts.notes,
120
+ }));
121
+ };
122
+ await Promise.all(targets.map((dep) => {
123
+ onEvent({ type: "package-start", name: dep.name });
124
+ return analyse(dep).finally(() => {
125
+ finished++;
126
+ onEvent({
127
+ type: "package",
128
+ name: dep.name,
129
+ done: finished,
130
+ total: targets.length,
131
+ });
132
+ });
133
+ }));
134
+ briefs.sort((a, b) => a.pkg.localeCompare(b.pkg) || semver.compare(a.from, b.from));
135
+ const exitCode = briefs.some((b) => b.verdict === "blocked")
136
+ ? 2
137
+ : briefs.some((b) => b.verdict === "review")
138
+ ? 1
139
+ : 0;
140
+ return {
141
+ schemaVersion: 1,
142
+ tool: { version: toolVersion(), typescript: ts.version },
143
+ root,
144
+ generatedAt: new Date(ctx.now).toISOString(),
145
+ manifests: inventory.manifests.length,
146
+ packages: briefs,
147
+ upToDate,
148
+ notAnalyzed: dedupeNotAnalyzed(notAnalyzed),
149
+ global: scanned.global,
150
+ limits: limitsFor(briefs, scanned.global),
151
+ exitCode,
152
+ };
153
+ }
154
+ // @types/X is used exactly where X is used.
155
+ function usageFor(scanned, dep, installed) {
156
+ const own = scanned.packages[dep.id];
157
+ if (!dep.name.startsWith("@types/"))
158
+ return own;
159
+ const bare = dep.name.slice("@types/".length);
160
+ const target = bare.includes("__") ? `@${bare.replace("__", "/")}` : bare;
161
+ const sources = Object.values(scanned.packages).filter((p) => p.pkg === target || (target === "node" && p.pkg.startsWith("node:")));
162
+ if (sources.length === 0) {
163
+ const exists = installed.some((d) => d.name === target);
164
+ return {
165
+ installedId: dep.id,
166
+ pkg: dep.name,
167
+ refs: own?.refs ?? [],
168
+ strongNames: own?.strongNames ?? [],
169
+ weakNames: own?.weakNames ?? [],
170
+ files: own?.files ?? 0,
171
+ blindSpots: own?.blindSpots ?? [],
172
+ opaque: [
173
+ ...(own?.opaque ?? []),
174
+ {
175
+ kind: "ambient-types",
176
+ count: 1,
177
+ examples: [
178
+ {
179
+ file: dep.declaredBy[0]?.manifest ?? "package.json",
180
+ line: 1,
181
+ col: 1,
182
+ typeOnly: true,
183
+ text: exists
184
+ ? `types for ${target}, which no scanned file imports`
185
+ : "ambient types",
186
+ },
187
+ ],
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ const merged = {
193
+ installedId: dep.id,
194
+ pkg: dep.name,
195
+ refs: sources.flatMap((s) => s.refs),
196
+ strongNames: [...new Set(sources.flatMap((s) => s.strongNames))].sort(),
197
+ weakNames: [...new Set(sources.flatMap((s) => s.weakNames))].sort(),
198
+ files: sources.reduce((n, s) => n + s.files, 0),
199
+ blindSpots: sources.flatMap((s) => s.blindSpots),
200
+ opaque: [...(own?.opaque ?? [])],
201
+ };
202
+ return merged;
203
+ }
204
+ function typesPackageName(name) {
205
+ return name.startsWith("@")
206
+ ? `@types/${name.slice(1).replace("/", "__")}`
207
+ : `@types/${name}`;
208
+ }
209
+ function emptyUsage() {
210
+ return { scannedFiles: 0, packages: {}, global: [] };
211
+ }
212
+ function dedupeNotAnalyzed(list) {
213
+ const seen = new Map();
214
+ for (const n of list)
215
+ if (!seen.has(n.pkg))
216
+ seen.set(n.pkg, n);
217
+ return [...seen.values()].sort((a, b) => a.pkg.localeCompare(b.pkg));
218
+ }
@@ -0,0 +1,13 @@
1
+ import type { Ctx } from "../options.ts";
2
+ import type { Packument } from "../registry/packument.ts";
3
+ import type { RegistryConfig } from "../registry/npmrc.ts";
4
+ import type { BlindSpotKind, Candidate, Counted, PackageBrief, PackageUsage } from "../model.ts";
5
+ export type SurfaceOutcome = PackageBrief["surface"] & {
6
+ truncatedOnUsedEntry?: boolean;
7
+ blindSpots?: Counted<BlindSpotKind>[];
8
+ };
9
+ export interface SurfaceContext {
10
+ typesPackageInstalled: boolean;
11
+ }
12
+ export declare function analyzeSurface(ctx: Ctx, cfg: RegistryConfig, p: Packument, c: Candidate, usage: PackageUsage | undefined, sctx?: SurfaceContext): Promise<SurfaceOutcome>;
13
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/surface/analyze.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAKxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG1D,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,OAAO,EACP,YAAY,EACZ,YAAY,EAKb,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG;IACrD,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,EAAE,CAAA;CACtC,CAAA;AAED,MAAM,WAAW,cAAc;IAE7B,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,cAAc,EACnB,CAAC,EAAE,SAAS,EACZ,CAAC,EAAE,SAAS,EACZ,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,IAAI,GAAE,cAAiD,GACtD,OAAO,CAAC,cAAc,CAAC,CAsKzB"}
@@ -0,0 +1,166 @@
1
+ import { ALGO } from "./signature.js";
2
+ import { declaresTypes } from "./entries.js";
3
+ import { extractSurface } from "./extract.js";
4
+ import { uniqueSites } from "../analyze/brief.js";
5
+ import { getTarballFiles } from "../registry/tarball.js";
6
+ import { diffSurfaces, surfaceChangeCount } from "./delta.js";
7
+ import { lastName, parsePath, resolveRef } from "../symbol-path.js";
8
+ export async function analyzeSurface(ctx, cfg, p, c, usage, sctx = { typesPackageInstalled: false }) {
9
+ const refs = usage?.refs ?? [];
10
+ // no named usage: a diff would have nothing to land on, and costs two downloads
11
+ if (refs.length === 0)
12
+ return {
13
+ status: "disabled",
14
+ detail: "not computed: no named usage to compare",
15
+ touched: [],
16
+ };
17
+ const from = p.versions[c.from];
18
+ const to = p.versions[c.to];
19
+ if (!from || !to)
20
+ return {
21
+ status: "failed",
22
+ detail: "installed version missing from the registry",
23
+ touched: [],
24
+ };
25
+ if (declaresTypes(from) === undefined && declaresTypes(to) === undefined) {
26
+ const tb = await getTarballFiles(ctx, cfg, to);
27
+ if (tb.ok && ![...tb.files.keys()].some((f) => /\.d\.[cm]?ts$/.test(f))) {
28
+ return {
29
+ status: sctx.typesPackageInstalled ? "types-from-@types" : "no-types",
30
+ touched: [],
31
+ };
32
+ }
33
+ if (!tb.ok && tb.reason === "offline-uncached")
34
+ return { status: "offline-uncached", touched: [] };
35
+ }
36
+ const wanted = [...new Set(refs.map((r) => r.entry))];
37
+ const [a, b] = await Promise.all([
38
+ extractSurface(ctx, cfg, p, c.from, wanted),
39
+ extractSurface(ctx, cfg, p, c.to, wanted),
40
+ ]);
41
+ if (!a.ok || !b.ok) {
42
+ const bad = !a.ok ? a : !b.ok ? b : undefined;
43
+ if (bad?.reason === "no-types")
44
+ return {
45
+ status: sctx.typesPackageInstalled ? "types-from-@types" : "no-types",
46
+ touched: [],
47
+ };
48
+ if (bad?.reason === "offline-uncached")
49
+ return { status: "offline-uncached", touched: [] };
50
+ return {
51
+ status: "failed",
52
+ detail: `type surface: ${bad?.detail ?? "extraction failed"}`,
53
+ touched: [],
54
+ };
55
+ }
56
+ const deltaKey = `deltas/${keyOf(a.surface.integrity)}__${keyOf(b.surface.integrity)}.a${ALGO}.json`;
57
+ let delta = (await ctx.cache.getJson(deltaKey))?.value;
58
+ if (!delta) {
59
+ delta = diffSurfaces(a.surface, b.surface);
60
+ await ctx.cache.setJson(deltaKey, delta, ctx.now);
61
+ }
62
+ // resolve every reference against the OLD surface: what the code compiles against today
63
+ const byPath = new Map();
64
+ const byMember = new Map();
65
+ const missing = [];
66
+ const missingEntries = new Set();
67
+ const add = (m, k, s) => {
68
+ const l = m.get(k) ?? [];
69
+ l.push(s);
70
+ m.set(k, l);
71
+ };
72
+ for (const r of refs) {
73
+ const res = resolveRef(a.surface, r);
74
+ for (const path of res.paths)
75
+ add(byPath, path, r.site);
76
+ for (const name of res.unresolvedTail)
77
+ add(byMember, name, r.site);
78
+ if (res.missingHead) {
79
+ missing.push(r.site);
80
+ missingEntries.add(r.origin?.entry ?? r.entry);
81
+ }
82
+ }
83
+ // `export * from "other-lib"`: the names live in another package's types, which radius does not
84
+ // follow. The notes are the only net here, said as such rather than as a hundred blind spots.
85
+ const elsewhere = [
86
+ ...new Set([...missingEntries].flatMap((e) => a.surface.entries[e]?.externalReexports ?? [])),
87
+ ];
88
+ if (elsewhere.length > 0) {
89
+ return {
90
+ status: "types-elsewhere",
91
+ detail: `its types are re-exported from ${elsewhere.join(", ")}, which is not followed`,
92
+ touched: [],
93
+ };
94
+ }
95
+ const touched = [];
96
+ const consider = (bucket, change) => {
97
+ const all = [change.path, ...change.alsoAt];
98
+ const strongSites = [];
99
+ for (const path of all) {
100
+ strongSites.push(...(byPath.get(path) ?? []));
101
+ if (bucket === "removed") {
102
+ // a removed namespace or type takes every path under it
103
+ for (const [used, sites] of byPath)
104
+ if (used.startsWith(`${path}.`) || used.startsWith(`${path}#`))
105
+ strongSites.push(...sites);
106
+ }
107
+ }
108
+ if (strongSites.length > 0) {
109
+ touched.push({
110
+ change,
111
+ bucket,
112
+ strength: "strong",
113
+ sites: uniqueSites(strongSites),
114
+ });
115
+ return;
116
+ }
117
+ // a member you reach through a value the walk lost track of: same name, possibly the same thing
118
+ if (all.some((p) => parsePath(p).segs.at(-1)?.sep === "#")) {
119
+ const sites = byMember.get(lastName(change.path));
120
+ if (sites && sites.length > 0)
121
+ touched.push({
122
+ change,
123
+ bucket,
124
+ strength: "weak",
125
+ sites: uniqueSites(sites),
126
+ });
127
+ }
128
+ };
129
+ for (const ch of delta.removed)
130
+ consider("removed", ch);
131
+ for (const ch of delta.changed)
132
+ consider("changed", ch);
133
+ for (const ch of delta.deprecated)
134
+ consider("deprecated", ch);
135
+ const usedEntries = new Set(wanted);
136
+ const truncatedOnUsedEntry = a.surface.flags.includes("symbol-cap") ||
137
+ b.surface.flags.includes("symbol-cap") ||
138
+ [...usedEntries].some((e) => !a.surface.entries[e] &&
139
+ (a.surface.flags.includes("wildcard-truncated") ||
140
+ b.surface.flags.includes("wildcard-truncated")));
141
+ return {
142
+ status: "computed",
143
+ changes: surfaceChangeCount(delta),
144
+ added: delta.added.length,
145
+ touched: touched.sort((x, y) => order(x) - order(y) || x.change.path.localeCompare(y.change.path)),
146
+ truncatedOnUsedEntry,
147
+ ...(missing.length > 0
148
+ ? {
149
+ blindSpots: [
150
+ {
151
+ kind: "unresolved-against-surface",
152
+ count: missing.length,
153
+ examples: uniqueSites(missing).slice(0, 3),
154
+ },
155
+ ],
156
+ }
157
+ : {}),
158
+ };
159
+ }
160
+ function order(t) {
161
+ return ((t.bucket === "removed" ? 0 : t.bucket === "changed" ? 2 : 4) +
162
+ (t.strength === "weak" ? 1 : 0));
163
+ }
164
+ function keyOf(integrity) {
165
+ return Buffer.from(integrity).toString("base64url").slice(0, 48);
166
+ }
@@ -0,0 +1,5 @@
1
+ import type { Surface, SurfaceDelta } from "../model.ts";
2
+ export declare function diffSurfaces(a: Surface, b: Surface): SurfaceDelta;
3
+ export declare function sameModuloAny(a: string[], b: string[]): boolean;
4
+ export declare function surfaceChangeCount(d: SurfaceDelta): number;
5
+ //# sourceMappingURL=delta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta.d.ts","sourceRoot":"","sources":["../../src/surface/delta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,OAAO,EAEP,YAAY,EAEb,MAAM,aAAa,CAAA;AAGpB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,YAAY,CAwEjE;AASD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAK/D;AAqFD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D"}
@@ -0,0 +1,173 @@
1
+ import { parsePath } from "../symbol-path.js";
2
+ // A pure function of two surfaces: identical for everyone who compares these two versions.
3
+ export function diffSurfaces(a, b) {
4
+ const raw = {
5
+ removed: [],
6
+ changed: [],
7
+ deprecated: [],
8
+ widened: [],
9
+ added: [],
10
+ };
11
+ // a type from a dependency the virtual host cannot load prints as `any` on one side and by name
12
+ // on the other when a declaration merely adds an annotation: not a change anyone can act on
13
+ const lenient = a.flags.includes("external-types-unresolved") ||
14
+ b.flags.includes("external-types-unresolved");
15
+ const sigOf = (s, sym) => {
16
+ let cur = sym;
17
+ for (let i = 0; i < 5 && cur.aliasOf; i++) {
18
+ const next = s.symbols[cur.aliasOf];
19
+ if (!next)
20
+ break;
21
+ cur = next;
22
+ }
23
+ return cur.sig;
24
+ };
25
+ for (const [path, before] of Object.entries(a.symbols)) {
26
+ const after = b.symbols[path];
27
+ if (!after) {
28
+ raw.removed.push({
29
+ path,
30
+ kind: before.kind,
31
+ before: sigOf(a, before),
32
+ alsoAt: [],
33
+ });
34
+ continue;
35
+ }
36
+ if (before.kind === "namespace" && after.kind === "namespace")
37
+ continue;
38
+ const sa = sigOf(a, before);
39
+ const sb = sigOf(b, after);
40
+ if (!sameMultiset(sa, sb) && !(lenient && sameModuloAny(sa, sb))) {
41
+ raw[isWidening(sa, sb) ? "widened" : "changed"].push({
42
+ path,
43
+ kind: after.kind,
44
+ before: sa,
45
+ after: sb,
46
+ alsoAt: [],
47
+ });
48
+ }
49
+ else if (!before.deprecated && after.deprecated && !before.aliasOf) {
50
+ raw.deprecated.push({
51
+ path,
52
+ kind: after.kind,
53
+ before: sa,
54
+ after: sb,
55
+ alsoAt: [],
56
+ });
57
+ }
58
+ }
59
+ for (const [path, after] of Object.entries(b.symbols)) {
60
+ if (!a.symbols[path])
61
+ raw.added.push({
62
+ path,
63
+ kind: after.kind,
64
+ after: sigOf(b, after),
65
+ alsoAt: [],
66
+ });
67
+ }
68
+ return {
69
+ removed: collapse(dropChildrenOfRemoved(raw.removed)),
70
+ changed: collapse(raw.changed),
71
+ deprecated: collapse(raw.deprecated),
72
+ widened: collapse(raw.widened),
73
+ added: collapse(dropChildrenOfRemoved(raw.added)),
74
+ };
75
+ }
76
+ function sameMultiset(a, b) {
77
+ if (a.length !== b.length)
78
+ return false;
79
+ const x = [...a].sort();
80
+ const y = [...b].sort();
81
+ return x.every((v, i) => v === y[i]);
82
+ }
83
+ export function sameModuloAny(a, b) {
84
+ if (a.length !== b.length || a.length === 0)
85
+ return false;
86
+ const x = [...a].sort();
87
+ const y = [...b].sort();
88
+ return x.every((s, i) => anyMatches(s, y[i]) || anyMatches(y[i], s));
89
+ }
90
+ function anyMatches(withAny, other) {
91
+ if (!/(?<![\w$])any(?![\w$])/.test(withAny))
92
+ return false;
93
+ const pattern = withAny
94
+ .split(/(?<![\w$])any(?![\w$])/)
95
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
96
+ .join("[\\w$.<>\\[\\], |&]+?");
97
+ return new RegExp(`^${pattern}$`).test(other);
98
+ }
99
+ // Adding an overload, or appending optional parameters, breaks no existing call.
100
+ function isWidening(before, after) {
101
+ if (before.length === 0)
102
+ return false;
103
+ const afterSet = new Set(after);
104
+ if (before.every((s) => afterSet.has(s)))
105
+ return true;
106
+ return before.every((old) => after.some((neu) => appendsOptional(old, neu)));
107
+ }
108
+ function appendsOptional(old, neu) {
109
+ const po = /^(<[^>]*>)?\((.*)\) => (.*)$/.exec(old);
110
+ const pn = /^(<[^>]*>)?\((.*)\) => (.*)$/.exec(neu);
111
+ if (!po || !pn)
112
+ return false;
113
+ if ((po[1] ?? "") !== (pn[1] ?? "") || po[3] !== pn[3])
114
+ return false;
115
+ const a = po[2] ?? "";
116
+ const b = pn[2] ?? "";
117
+ if (!b.startsWith(a))
118
+ return false;
119
+ const extra = b.slice(a.length).replace(/^,\s*/, "");
120
+ return (extra.length > 0 &&
121
+ extra.split(/,\s*/).every((p) => p.endsWith("?") || p.startsWith("...")));
122
+ }
123
+ function parentOf(path) {
124
+ const { prefix, segs } = parsePath(path);
125
+ if (segs.length <= 1)
126
+ return undefined;
127
+ let out = `${prefix}:${segs[0].name}`;
128
+ for (const s of segs.slice(1, -1))
129
+ out += `${s.sep}${s.name}`;
130
+ return out;
131
+ }
132
+ // A removed class takes its members with it: report the class.
133
+ function dropChildrenOfRemoved(changes) {
134
+ const paths = new Set(changes.map((c) => c.path));
135
+ return changes.filter((c) => {
136
+ for (let p = parentOf(c.path); p; p = parentOf(p))
137
+ if (paths.has(p))
138
+ return false;
139
+ return true;
140
+ });
141
+ }
142
+ // The same change seen through several entries (lib:email, lib/v2:email, lib:ns.email) is one change.
143
+ function collapse(changes) {
144
+ const groups = new Map();
145
+ for (const c of changes) {
146
+ const segs = parsePath(c.path).segs;
147
+ const last = segs[segs.length - 1];
148
+ const key = JSON.stringify([
149
+ c.kind,
150
+ last?.sep,
151
+ last?.name,
152
+ c.before ?? null,
153
+ c.after ?? null,
154
+ ]);
155
+ const list = groups.get(key) ?? [];
156
+ list.push(c);
157
+ groups.set(key, list);
158
+ }
159
+ const out = [];
160
+ for (const list of groups.values()) {
161
+ list.sort((x, y) => rank(x.path) - rank(y.path) || x.path.localeCompare(y.path));
162
+ const [head, ...rest] = list;
163
+ out.push({ ...head, alsoAt: rest.map((r) => r.path) });
164
+ }
165
+ return out.sort((x, y) => x.path.localeCompare(y.path));
166
+ }
167
+ function rank(path) {
168
+ const { prefix, segs } = parsePath(path);
169
+ return prefix.split("/").length * 10 + segs.length;
170
+ }
171
+ export function surfaceChangeCount(d) {
172
+ return d.removed.length + d.changed.length + d.deprecated.length;
173
+ }
@@ -0,0 +1,16 @@
1
+ export interface EntryFile {
2
+ subpath: string;
3
+ typesFile: string;
4
+ }
5
+ export interface EntryResolution {
6
+ entries: EntryFile[];
7
+ wildcardTruncated: boolean;
8
+ }
9
+ export declare function typesForTarget(value: unknown, has: (p: string) => boolean, star?: string): string | undefined;
10
+ export declare function resolveEntries(pj: Record<string, unknown>, files: string[], wantedSubpaths?: string[]): EntryResolution;
11
+ export declare function declaresTypes(pv: {
12
+ types?: string;
13
+ typings?: string;
14
+ exports?: unknown;
15
+ }): boolean | undefined;
16
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../src/surface/entries.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAOD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CA2BpB;AAiBD,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3B,KAAK,EAAE,MAAM,EAAE,EACf,cAAc,GAAE,MAAM,EAAO,GAC5B,eAAe,CAkGjB;AAuBD,wBAAgB,aAAa,CAAC,EAAE,EAAE;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,OAAO,GAAG,SAAS,CAatB"}