lurqrun 0.0.8 → 0.0.9

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
@@ -1117,6 +1117,10 @@ var init_schema = __esm({
1117
1117
  peerDependencies: jsonb("peer_dependencies").$type(),
1118
1118
  peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
1119
1119
  engines: jsonb("engines").$type(),
1120
+ /** Where this package runs, classified from its own manifest at ingest.
1121
+ * Resolves the `framework` category's frontend/backend ambiguity for the
1122
+ * architecture diagram. Null = the manifest did not say clearly enough. */
1123
+ runtimeTarget: text("runtime_target").$type(),
1120
1124
  // Computed outputs
1121
1125
  healthScore: integer("health_score"),
1122
1126
  /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
@@ -1234,7 +1238,13 @@ var init_schema = __esm({
1234
1238
  {
1235
1239
  packageName: text("package_name").notNull(),
1236
1240
  version: text("version").notNull(),
1237
- publishedAt: ts("published_at")
1241
+ publishedAt: ts("published_at"),
1242
+ /** Peers/engines declared by this exact version. Lifted from the registry
1243
+ * document at ingest (it already carries every version's manifest), so a
1244
+ * conflict check against the versions a repo actually RUNS is one indexed
1245
+ * query rather than a registry read per dependency. */
1246
+ peerDependencies: jsonb("peer_dependencies").$type(),
1247
+ engines: jsonb("engines").$type()
1238
1248
  },
1239
1249
  (table2) => [
1240
1250
  primaryKey({ columns: [table2.packageName, table2.version] }),
@@ -1615,10 +1625,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
1615
1625
  import postgres from "postgres";
1616
1626
  function createDb(opts = {}) {
1617
1627
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
1618
- const sql12 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1628
+ const sql14 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1619
1629
  } });
1620
- const db = drizzle(sql12, { schema: schema_exports });
1621
- return { db, sql: sql12, close: () => sql12.end() };
1630
+ const db = drizzle(sql14, { schema: schema_exports });
1631
+ return { db, sql: sql14, close: () => sql14.end() };
1622
1632
  }
1623
1633
  var init_client = __esm({
1624
1634
  "src/db/client.ts"() {
@@ -1648,7 +1658,9 @@ async function upsertPackageVersions(db, name, versions) {
1648
1658
  const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1649
1659
  packageName: name,
1650
1660
  version: v.version,
1651
- publishedAt: v.publishedAt
1661
+ publishedAt: v.publishedAt,
1662
+ peerDependencies: v.peerDependencies ?? null,
1663
+ engines: v.engines ?? null
1652
1664
  }));
1653
1665
  await db.insert(packageVersions).values(rows).onConflictDoNothing();
1654
1666
  }
@@ -2163,6 +2175,7 @@ function parseNpmRegistry(json2) {
2163
2175
  directDependenciesCount: countDeps(latestManifest?.dependencies),
2164
2176
  hasProvenance: detectProvenance(latestManifest),
2165
2177
  hasInstallScripts: detectInstallScripts(latestManifest),
2178
+ hasBrowserField: latestManifest?.browser != null,
2166
2179
  peerDependencies: parseDepMap(latestManifest?.peerDependencies),
2167
2180
  peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
2168
2181
  engines: parseDepMap(latestManifest?.engines),
@@ -2198,7 +2211,13 @@ function parseVersionTimeline(json2) {
2198
2211
  const versions = json2?.versions;
2199
2212
  if (!versions || typeof versions !== "object") return [];
2200
2213
  const time = json2?.time ?? {};
2201
- return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
2214
+ return Object.keys(versions).map((version) => ({
2215
+ version,
2216
+ publishedAt: toDate(time?.[version]),
2217
+ // Already in this document — no second fetch, no per-version round trip.
2218
+ peerDependencies: parseDepMap(versions[version]?.peerDependencies),
2219
+ engines: parseDepMap(versions[version]?.engines)
2220
+ })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
2202
2221
  }
2203
2222
  function parseKeywords(value) {
2204
2223
  if (!Array.isArray(value)) return [];
@@ -2778,18 +2797,21 @@ async function checkCompat(db, packages2, opts = {}) {
2778
2797
  provenance: edge.provenance,
2779
2798
  witnessCount: edge.witnessCount
2780
2799
  }));
2800
+ const versionOf = new Map(members.map((m) => [m.name, m.version]));
2801
+ const matchesCheckedVersions = (edge) => edgeMatchesVersions(edge, versionOf);
2781
2802
  for (const edge of edges) {
2782
2803
  if (edge.status === "conflict") {
2804
+ const at = matchesCheckedVersions(edge) ? "" : ` (recorded at those versions; you are checking ${edge.packageA}@${versionOf.get(edge.packageA) ?? "?"} and ${edge.packageB}@${versionOf.get(edge.packageB) ?? "?"})`;
2783
2805
  conflicts.push({
2784
2806
  source: "sandbox",
2785
2807
  packages: [edge.packageA, edge.packageB],
2786
- detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
2808
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox${at}`
2787
2809
  });
2788
2810
  }
2789
2811
  }
2790
2812
  const checkedNames = members.map((m) => m.name);
2791
2813
  const provenCompatible = new Set(
2792
- edges.filter((e) => e.status === "compatible").map((e) => pairKey(e.packageA, e.packageB))
2814
+ edges.filter((e) => e.status === "compatible" && matchesCheckedVersions(e)).map((e) => pairKey(e.packageA, e.packageB))
2793
2815
  );
2794
2816
  if (checkedNames.length >= 2 && !conflicts.length && !fullyCovered(checkedNames, provenCompatible)) {
2795
2817
  await enqueueCompatVerify(db, checkedNames).catch(() => {
@@ -2811,6 +2833,11 @@ async function checkCompat(db, packages2, opts = {}) {
2811
2833
  pairs: enumeratePairs(checkedNames, conflicts)
2812
2834
  };
2813
2835
  }
2836
+ function edgeMatchesVersions(edge, versionOf) {
2837
+ const a = versionOf.get(edge.packageA);
2838
+ const b = versionOf.get(edge.packageB);
2839
+ return a != null && b != null && a === edge.versionA && b === edge.versionB;
2840
+ }
2814
2841
  function enumeratePairs(names2, conflicts) {
2815
2842
  const byPair = /* @__PURE__ */ new Map();
2816
2843
  for (const c of conflicts) {
@@ -3244,22 +3271,6 @@ var init_verification = __esm({
3244
3271
  }
3245
3272
  });
3246
3273
 
3247
- // src/db/outcomes.ts
3248
- import { desc as desc3, eq as eq5 } from "drizzle-orm";
3249
- async function recordOutcome(db, outcome) {
3250
- await db.insert(recommendationOutcomes).values(outcome);
3251
- }
3252
- async function getOutcomesByOwner(db, ownerId, opts = {}) {
3253
- return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
3254
- }
3255
- var init_outcomes = __esm({
3256
- "src/db/outcomes.ts"() {
3257
- "use strict";
3258
- init_esm_shims();
3259
- init_schema();
3260
- }
3261
- });
3262
-
3263
3274
  // src/data/successors.json
3264
3275
  var successors_default;
3265
3276
  var init_successors = __esm({
@@ -3322,9 +3333,30 @@ var init_successors = __esm({
3322
3333
  });
3323
3334
 
3324
3335
  // src/core/successors.ts
3325
- function lookupSuccessor(name) {
3326
- const hit = MAP[name.toLowerCase()];
3327
- return hit ? { name: hit.replacedBy, reason: hit.reason } : null;
3336
+ function lookupSuccessor(name, learned) {
3337
+ const key = name.toLowerCase();
3338
+ const curated = MAP[key];
3339
+ if (curated) return { name: curated.replacedBy, reason: curated.reason };
3340
+ return learned?.get(key) ?? null;
3341
+ }
3342
+ function buildLearnedSuccessors(observed, isSuperseded) {
3343
+ const best = /* @__PURE__ */ new Map();
3344
+ for (const row of observed) {
3345
+ if (!isSuperseded(row.from)) continue;
3346
+ const key = row.from.toLowerCase();
3347
+ const current = best.get(key);
3348
+ const better = !current || row.owners > current.owners || row.owners === current.owners && row.observations > current.observations;
3349
+ if (better) best.set(key, { to: row.to, owners: row.owners, observations: row.observations });
3350
+ }
3351
+ return new Map(
3352
+ [...best].map(([from, hit]) => [
3353
+ from,
3354
+ {
3355
+ name: hit.to,
3356
+ reason: `${hit.owners} teams moved here after ${from} failed for the same need`
3357
+ }
3358
+ ])
3359
+ );
3328
3360
  }
3329
3361
  var MAP;
3330
3362
  var init_successors2 = __esm({
@@ -3336,6 +3368,81 @@ var init_successors2 = __esm({
3336
3368
  }
3337
3369
  });
3338
3370
 
3371
+ // src/db/outcomes.ts
3372
+ import { count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
3373
+ import { alias } from "drizzle-orm/pg-core";
3374
+ async function recordOutcome(db, outcome) {
3375
+ await db.insert(recommendationOutcomes).values(outcome);
3376
+ }
3377
+ async function getFieldEvidence(db, names2) {
3378
+ if (names2 && names2.length === 0) return /* @__PURE__ */ new Map();
3379
+ const rows = await db.select({
3380
+ packageName: recommendationOutcomes.packageName,
3381
+ reports: count(),
3382
+ successes: sql5`count(*) FILTER (WHERE ${SUCCESS_PREDICATE})`.mapWith(Number)
3383
+ }).from(recommendationOutcomes).where(names2 ? inArray3(recommendationOutcomes.packageName, names2) : void 0).groupBy(recommendationOutcomes.packageName);
3384
+ return new Map(
3385
+ rows.map((r) => [r.packageName, { reports: Number(r.reports), successes: r.successes }])
3386
+ );
3387
+ }
3388
+ async function learnSuccessions(db) {
3389
+ const failed = alias(recommendationOutcomes, "failed");
3390
+ const worked = alias(recommendationOutcomes, "worked");
3391
+ const rows = await db.select({
3392
+ from: failed.packageName,
3393
+ to: worked.packageName,
3394
+ owners: sql5`count(DISTINCT ${failed.ownerId})`.mapWith(Number),
3395
+ observations: sql5`count(*)`.mapWith(Number)
3396
+ }).from(failed).innerJoin(
3397
+ worked,
3398
+ sql5`${worked.ownerId} = ${failed.ownerId}
3399
+ AND ${worked.need} = ${failed.need}
3400
+ AND ${worked.packageName} <> ${failed.packageName}
3401
+ AND ${worked.createdAt} > ${failed.createdAt}`
3402
+ ).where(
3403
+ sql5`${failed.need} IS NOT NULL
3404
+ AND ${failed.ownerId} IS NOT NULL
3405
+ AND NOT (${failed.accepted} AND (${failed.buildSignal} IS NULL OR ${failed.buildSignal} <> 'failed'))
3406
+ AND (${worked.accepted} AND (${worked.buildSignal} IS NULL OR ${worked.buildSignal} <> 'failed'))`
3407
+ ).groupBy(failed.packageName, worked.packageName).having(sql5`count(DISTINCT ${failed.ownerId}) >= ${MIN_SUCCESSION_OWNERS}`);
3408
+ return rows.map((r) => ({
3409
+ from: r.from,
3410
+ to: r.to,
3411
+ owners: Number(r.owners),
3412
+ observations: Number(r.observations)
3413
+ }));
3414
+ }
3415
+ async function loadLearnedSuccessors(db, now = Date.now()) {
3416
+ if (learnedCache && now - learnedCache.at < LEARNED_TTL_MS) return learnedCache.value;
3417
+ const observed = await learnSuccessions(db);
3418
+ if (observed.length === 0) {
3419
+ learnedCache = { at: now, value: /* @__PURE__ */ new Map() };
3420
+ return learnedCache.value;
3421
+ }
3422
+ const names2 = [...new Set(observed.map((o) => o.from))];
3423
+ const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(inArray3(packages.name, names2));
3424
+ const dead = new Set(rows.filter((r) => r.deprecated || r.archived).map((r) => r.name));
3425
+ const value = buildLearnedSuccessors(observed, (name) => dead.has(name));
3426
+ learnedCache = { at: now, value };
3427
+ return value;
3428
+ }
3429
+ async function getOutcomesByOwner(db, ownerId, opts = {}) {
3430
+ return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
3431
+ }
3432
+ var SUCCESS_PREDICATE, MIN_SUCCESSION_OWNERS, LEARNED_TTL_MS, learnedCache;
3433
+ var init_outcomes = __esm({
3434
+ "src/db/outcomes.ts"() {
3435
+ "use strict";
3436
+ init_esm_shims();
3437
+ init_successors2();
3438
+ init_schema();
3439
+ SUCCESS_PREDICATE = sql5`${recommendationOutcomes.accepted} AND (${recommendationOutcomes.buildSignal} IS NULL OR ${recommendationOutcomes.buildSignal} <> 'failed')`;
3440
+ MIN_SUCCESSION_OWNERS = 3;
3441
+ LEARNED_TTL_MS = 10 * 60 * 1e3;
3442
+ learnedCache = null;
3443
+ }
3444
+ });
3445
+
3339
3446
  // src/ingestion/sources/githubReadme.ts
3340
3447
  async function fetchGithubReadme(owner, repo, fetchImpl) {
3341
3448
  for (const file of CANDIDATES) {
@@ -3732,7 +3839,7 @@ function resetWeights() {
3732
3839
  resetWeightsCache();
3733
3840
  return removed;
3734
3841
  }
3735
- var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3842
+ var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3736
3843
  var init_weights = __esm({
3737
3844
  "src/scoring/weights.ts"() {
3738
3845
  "use strict";
@@ -3811,6 +3918,26 @@ var init_weights = __esm({
3811
3918
  /** Bundle size at the category median maps to this score; smaller → higher. */
3812
3919
  medianScore: 50
3813
3920
  };
3921
+ FIELD = {
3922
+ /** Weight when evidence exists — ~9% of the blend (0.10 / 1.10). */
3923
+ weight: 0.1,
3924
+ /**
3925
+ * Bayesian shrinkage strength: how many prior observations the neutral rate is
3926
+ * worth. One failed report out of one is not a 0-scoring package, and a raw
3927
+ * success rate would say it is. At m=12, a single failure lands near 69 rather
3928
+ * than 0, and it takes real volume to move the number far — which is the
3929
+ * correct epistemics for a signal users can trivially poison.
3930
+ */
3931
+ priorWeight: 12,
3932
+ /**
3933
+ * The prior success rate a package is assumed to have before anyone reports.
3934
+ * Set at the observed base rate of "installs and builds fine", not 0.5:
3935
+ * most packages do work, so 0.5 would penalise the first honest reporter.
3936
+ */
3937
+ priorRate: 0.75,
3938
+ /** Reports at or above this cap the confidence note as fully-evidenced. */
3939
+ wellEvidencedReports: 25
3940
+ };
3814
3941
  DEFAULT_WEIGHTS = {
3815
3942
  health: { ...HEALTH_WEIGHTS },
3816
3943
  composite: { ...COMPOSITE }
@@ -3978,17 +4105,24 @@ function computeQuality(input) {
3978
4105
  if (components.length === 0) return null;
3979
4106
  return weightedAverage(components);
3980
4107
  }
4108
+ function computeFieldScore(evidence) {
4109
+ if (!evidence || evidence.reports <= 0) return null;
4110
+ const { reports, successes } = evidence;
4111
+ const m = FIELD.priorWeight;
4112
+ return clamp(Math.round(100 * (successes + m * FIELD.priorRate) / (reports + m)));
4113
+ }
3981
4114
  function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
3982
- const { maintenance, adoption, reliability, efficiency } = breakdown;
3983
4115
  const w = weights;
3984
- if (efficiency === null) {
3985
- const denom = w.maintenance + w.adoption + w.reliability;
3986
- return Math.round(
3987
- (w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
3988
- );
3989
- }
3990
- return Math.round(
3991
- w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
4116
+ return weightedAverage(
4117
+ [
4118
+ { value: breakdown.maintenance, weight: w.maintenance },
4119
+ { value: breakdown.adoption, weight: w.adoption },
4120
+ { value: breakdown.reliability, weight: w.reliability },
4121
+ { value: breakdown.efficiency, weight: w.efficiency },
4122
+ // `?? null` — a breakdown stored before field evidence shipped has the key
4123
+ // absent, and undefined must redistribute, not score 0.
4124
+ { value: breakdown.field ?? null, weight: FIELD.weight }
4125
+ ].filter((c) => c.value !== null)
3992
4126
  );
3993
4127
  }
3994
4128
  function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
@@ -4233,7 +4367,7 @@ var init_types2 = __esm({
4233
4367
  });
4234
4368
 
4235
4369
  // src/db/repos.ts
4236
- import { and as and5, eq as eq7, sql as sql5 } from "drizzle-orm";
4370
+ import { and as and5, eq as eq7, sql as sql6 } from "drizzle-orm";
4237
4371
  async function upsertRepos(db, rows) {
4238
4372
  if (rows.length === 0) return 0;
4239
4373
  await db.insert(repos).values(
@@ -4248,9 +4382,9 @@ async function upsertRepos(db, rows) {
4248
4382
  ).onConflictDoUpdate({
4249
4383
  target: [repos.ownerId, repos.fullName],
4250
4384
  set: {
4251
- installationId: sql5`excluded.installation_id`,
4252
- defaultBranch: sql5`excluded.default_branch`,
4253
- isPrivate: sql5`excluded.is_private`
4385
+ installationId: sql6`excluded.installation_id`,
4386
+ defaultBranch: sql6`excluded.default_branch`,
4387
+ isPrivate: sql6`excluded.is_private`
4254
4388
  }
4255
4389
  });
4256
4390
  return rows.length;
@@ -4260,7 +4394,7 @@ async function listRepos(db, ownerId) {
4260
4394
  }
4261
4395
  async function reposDeclaring(db, name) {
4262
4396
  return db.select().from(repos).where(
4263
- sql5`exists (
4397
+ sql6`exists (
4264
4398
  select 1 from jsonb_array_elements(${repos.manifests}) as m
4265
4399
  where jsonb_exists(m -> 'deps', ${name})
4266
4400
  )`
@@ -4270,6 +4404,10 @@ async function getRepo(db, ownerId, id) {
4270
4404
  const rows = await db.select().from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).limit(1);
4271
4405
  return rows[0] ?? null;
4272
4406
  }
4407
+ async function findPolicyByFullName(db, ownerId, fullName) {
4408
+ const rows = await db.select({ policy: repos.policy }).from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.fullName, fullName))).limit(1);
4409
+ return rows[0]?.policy ?? null;
4410
+ }
4273
4411
  async function saveScan(db, id, scan) {
4274
4412
  await db.update(repos).set({
4275
4413
  manifests: scan.manifests,
@@ -4355,8 +4493,76 @@ var init_attribute = __esm({
4355
4493
  }
4356
4494
  });
4357
4495
 
4496
+ // src/ingestion/sources/osv.ts
4497
+ function installKey(name, version) {
4498
+ return `${name}@${version}`;
4499
+ }
4500
+ async function queryVulnerableInstalls(installs, fetchImpl) {
4501
+ const affected = /* @__PURE__ */ new Map();
4502
+ if (installs.length === 0) return { affected, complete: true };
4503
+ const unique = [...new Map(installs.map((i) => [installKey(i.name, i.version), i])).values()];
4504
+ const batches = [];
4505
+ for (let i = 0; i < unique.length; i += BATCH) batches.push(unique.slice(i, i + BATCH));
4506
+ let complete = true;
4507
+ let cursor = 0;
4508
+ const worker = async () => {
4509
+ for (let i = cursor++; i < batches.length; i = cursor++) {
4510
+ const batch = batches[i];
4511
+ try {
4512
+ const { data } = await httpRequest(URL2, {
4513
+ host: HOST7,
4514
+ method: "POST",
4515
+ headers: { "content-type": "application/json" },
4516
+ body: JSON.stringify({
4517
+ queries: batch.map((q) => ({
4518
+ version: q.version,
4519
+ package: { name: q.name, ecosystem: "npm" }
4520
+ }))
4521
+ }),
4522
+ // Uncached on purpose. The shared cache keys on the URL, and every
4523
+ // batch here posts to the same one with a different body — a cache hit
4524
+ // would answer one tree's question with another tree's vulnerabilities.
4525
+ ttlMs: 0,
4526
+ // Best-effort by contract: the caller keeps its coarser answer when
4527
+ // this fails, so exhausting the default retry ladder would only stall
4528
+ // a scheduled scan behind an outage nobody is waiting on.
4529
+ retries: 1,
4530
+ fetchImpl
4531
+ });
4532
+ const results = data?.results ?? [];
4533
+ if (results.length !== batch.length) {
4534
+ complete = false;
4535
+ continue;
4536
+ }
4537
+ results.forEach((result, j) => {
4538
+ const ids = (result?.vulns ?? []).map((v) => v?.id).filter((id) => typeof id === "string");
4539
+ if (ids.length === 0) return;
4540
+ const q = batch[j];
4541
+ affected.set(installKey(q.name, q.version), ids);
4542
+ });
4543
+ } catch {
4544
+ complete = false;
4545
+ }
4546
+ }
4547
+ };
4548
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, batches.length) }, worker));
4549
+ return { affected, complete };
4550
+ }
4551
+ var HOST7, URL2, BATCH, CONCURRENCY;
4552
+ var init_osv = __esm({
4553
+ "src/ingestion/sources/osv.ts"() {
4554
+ "use strict";
4555
+ init_esm_shims();
4556
+ init_http();
4557
+ HOST7 = "api.osv.dev";
4558
+ URL2 = `https://${HOST7}/v1/querybatch`;
4559
+ BATCH = 250;
4560
+ CONCURRENCY = 4;
4561
+ }
4562
+ });
4563
+
4358
4564
  // src/github/drift.ts
4359
- import { inArray as inArray3 } from "drizzle-orm";
4565
+ import { inArray as inArray4, sql as sql7 } from "drizzle-orm";
4360
4566
  import semver2 from "semver";
4361
4567
  function declaredDeps(manifests) {
4362
4568
  const out = /* @__PURE__ */ new Map();
@@ -4383,7 +4589,7 @@ async function loadIndexed(db, names2) {
4383
4589
  latestVersion: packages.latestVersion,
4384
4590
  deprecated: packages.deprecated,
4385
4591
  advisories: packages.advisories
4386
- }).from(packages).where(inArray3(packages.name, names2.slice(i, i + NAME_CHUNK)));
4592
+ }).from(packages).where(inArray4(packages.name, names2.slice(i, i + NAME_CHUNK)));
4387
4593
  for (const row of rows) {
4388
4594
  out.set(row.name, {
4389
4595
  latestVersion: row.latestVersion,
@@ -4397,7 +4603,7 @@ async function loadIndexed(db, names2) {
4397
4603
  async function loadVersions(db, names2) {
4398
4604
  const out = /* @__PURE__ */ new Map();
4399
4605
  for (let i = 0; i < names2.length; i += NAME_CHUNK) {
4400
- const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray3(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
4606
+ const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray4(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
4401
4607
  for (const row of rows) {
4402
4608
  const list = out.get(row.name);
4403
4609
  if (list) list.push(row.version);
@@ -4447,19 +4653,28 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4447
4653
  };
4448
4654
  if (transitives.length === 0) return empty2;
4449
4655
  const indexed = await loadIndexed(db, [...new Set(transitives.map((d) => d.name))]);
4656
+ const { affected, complete } = await queryVulnerableInstalls(
4657
+ transitives.map((d) => ({ name: d.name, version: d.version }))
4658
+ );
4450
4659
  const risks = [];
4451
4660
  let tracked = 0;
4452
4661
  for (const dep of transitives) {
4453
4662
  const row = indexed.get(dep.name);
4454
4663
  if (!row) continue;
4455
4664
  tracked++;
4456
- if (row.advisories === 0 && !row.deprecated) continue;
4665
+ const vulnIds = affected.get(installKey(dep.name, dep.version)) ?? [];
4666
+ if (row.advisories === 0 && !row.deprecated && vulnIds.length === 0) continue;
4457
4667
  risks.push({
4458
4668
  name: dep.name,
4459
4669
  version: dep.version,
4460
4670
  latest: row.latestVersion,
4461
4671
  advisories: row.advisories,
4462
4672
  deprecated: row.deprecated,
4673
+ // Empty when OSV cleared this version, which is the actionable difference
4674
+ // from `advisories`: the package has a history, this install does not.
4675
+ // Undefined when the lookup could not complete — never an empty array,
4676
+ // which would read as a clean bill of health nobody established.
4677
+ vulnerabilities: complete ? vulnIds : void 0,
4463
4678
  pulledInBy: attribution.parentsOf.get(dep.name) ?? []
4464
4679
  });
4465
4680
  }
@@ -4470,6 +4685,10 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4470
4685
  resolved: transitives.length,
4471
4686
  tracked,
4472
4687
  advisoryPackages: risks.filter((r) => r.advisories > 0).length,
4688
+ // The number that can actually be acted on: installs OSV matched to a
4689
+ // vulnerability at their exact version. Undefined when the lookup did not
4690
+ // complete, so "not checked" never renders as zero.
4691
+ vulnerableInstalls: complete ? risks.filter((r) => r.vulnerabilities?.length).length : void 0,
4473
4692
  deprecated: risks.filter((r) => r.deprecated).length,
4474
4693
  risks: risks.slice(0, TRANSITIVE_DETAIL_CAP),
4475
4694
  truncated,
@@ -4481,6 +4700,29 @@ async function conflictsAtLatest(db, names2) {
4481
4700
  const { members } = await assembleMembers(db, names2);
4482
4701
  return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4483
4702
  }
4703
+ async function conflictsAtCurrent(db, deps) {
4704
+ const pinned = deps.filter((d) => Boolean(d.resolved));
4705
+ if (pinned.length === 0) return [];
4706
+ const rows = await db.select({
4707
+ packageName: packageVersions.packageName,
4708
+ version: packageVersions.version,
4709
+ peerDependencies: packageVersions.peerDependencies,
4710
+ engines: packageVersions.engines
4711
+ }).from(packageVersions).where(
4712
+ inArray4(
4713
+ sql7`(${packageVersions.packageName}, ${packageVersions.version})`,
4714
+ pinned.map((d) => sql7`(${d.name}, ${d.resolved})`)
4715
+ )
4716
+ );
4717
+ const members = rows.map((r) => ({
4718
+ name: r.packageName,
4719
+ version: r.version,
4720
+ peerDependencies: r.peerDependencies,
4721
+ peerDependenciesMeta: null,
4722
+ engines: r.engines
4723
+ }));
4724
+ return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4725
+ }
4484
4726
  async function computeDrift(db, manifests, resolvedTree = null) {
4485
4727
  const declared = declaredDeps(manifests);
4486
4728
  const names2 = [...declared.keys()];
@@ -4502,7 +4744,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4502
4744
  advisories: 0,
4503
4745
  deps: [],
4504
4746
  transitive,
4505
- conflictsAtLatest: []
4747
+ conflictsAtLatest: [],
4748
+ conflictsAtCurrent: []
4506
4749
  };
4507
4750
  }
4508
4751
  const [indexed, versions] = await Promise.all([
@@ -4516,7 +4759,10 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4516
4759
  deps.push(depDrift(name, entry, row, versions.get(name) ?? []));
4517
4760
  }
4518
4761
  deps.sort((a, b) => severity(b) - severity(a) || a.name.localeCompare(b.name));
4519
- const conflicts = await conflictsAtLatest(db, deps.map((d) => d.name));
4762
+ const [conflicts, currentConflicts] = await Promise.all([
4763
+ conflictsAtLatest(db, deps.map((d) => d.name)),
4764
+ conflictsAtCurrent(db, deps)
4765
+ ]);
4520
4766
  return {
4521
4767
  depsDeclared: declared.size,
4522
4768
  depsTracked: deps.length,
@@ -4526,7 +4772,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4526
4772
  advisories: deps.reduce((sum, d) => sum + d.advisories, 0),
4527
4773
  deps: deps.slice(0, REPO_DRIFT_DETAIL_CAP),
4528
4774
  transitive,
4529
- conflictsAtLatest: conflicts
4775
+ conflictsAtLatest: conflicts,
4776
+ conflictsAtCurrent: currentConflicts
4530
4777
  };
4531
4778
  }
4532
4779
  var NAME_CHUNK;
@@ -4537,6 +4784,7 @@ var init_drift = __esm({
4537
4784
  init_schema();
4538
4785
  init_types2();
4539
4786
  init_attribute();
4787
+ init_osv();
4540
4788
  init_members();
4541
4789
  init_peerCompat();
4542
4790
  NAME_CHUNK = 500;
@@ -4637,6 +4885,82 @@ var init_errors = __esm({
4637
4885
  }
4638
4886
  });
4639
4887
 
4888
+ // src/core/runtimeTarget.ts
4889
+ function classifyRuntimeTarget(input) {
4890
+ const override = OVERRIDES[input.name.toLowerCase()];
4891
+ if (override) return override;
4892
+ const peers = Object.keys(input.peerDependencies ?? {});
4893
+ if (peers.some((p) => RENDERER_PEERS.has(p))) return "browser";
4894
+ const keywords = input.keywords.map((k) => k.toLowerCase());
4895
+ const server = keywords.filter((k) => SERVER_KEYWORDS.has(k)).length;
4896
+ const browser = keywords.filter((k) => BROWSER_KEYWORDS.has(k)).length;
4897
+ if (browser > server) return "browser";
4898
+ if (server > browser) return "node";
4899
+ if (input.hasBrowserField) return "browser";
4900
+ if (input.engines?.node && !input.hasBrowserField && browser === 0) return "node";
4901
+ return null;
4902
+ }
4903
+ var SERVER_KEYWORDS, BROWSER_KEYWORDS, RENDERER_PEERS, OVERRIDES;
4904
+ var init_runtimeTarget = __esm({
4905
+ "src/core/runtimeTarget.ts"() {
4906
+ "use strict";
4907
+ init_esm_shims();
4908
+ SERVER_KEYWORDS = /* @__PURE__ */ new Set([
4909
+ "server",
4910
+ "http-server",
4911
+ "backend",
4912
+ "middleware",
4913
+ "rest",
4914
+ "restful",
4915
+ "api",
4916
+ "router",
4917
+ "routing",
4918
+ "orm",
4919
+ "database",
4920
+ "sql",
4921
+ "graphql-server",
4922
+ "microservice",
4923
+ "websocket",
4924
+ "rpc"
4925
+ ]);
4926
+ BROWSER_KEYWORDS = /* @__PURE__ */ new Set([
4927
+ "react",
4928
+ "vue",
4929
+ "svelte",
4930
+ "angular",
4931
+ "preact",
4932
+ "solid",
4933
+ "dom",
4934
+ "browser",
4935
+ "component",
4936
+ "components",
4937
+ "ui",
4938
+ "css",
4939
+ "styling",
4940
+ "animation",
4941
+ "frontend",
4942
+ "client-side",
4943
+ "jsx",
4944
+ "hooks"
4945
+ ]);
4946
+ RENDERER_PEERS = /* @__PURE__ */ new Set(["react", "react-dom", "vue", "svelte", "@angular/core", "preact"]);
4947
+ OVERRIDES = {
4948
+ express: "node",
4949
+ fastify: "node",
4950
+ koa: "node",
4951
+ hono: "node",
4952
+ "@nestjs/core": "node",
4953
+ "@hapi/hapi": "node",
4954
+ // Meta-frameworks run in both places; they are the server in an architecture
4955
+ // diagram, which is the question this answers.
4956
+ next: "node",
4957
+ nuxt: "node",
4958
+ "@remix-run/node": "node",
4959
+ astro: "node"
4960
+ };
4961
+ }
4962
+ });
4963
+
4640
4964
  // src/pipeline/mineEdges.ts
4641
4965
  function trackedPairs(nodes, tracked) {
4642
4966
  const t = nodes.filter((n) => tracked.has(n.name));
@@ -4717,6 +5041,13 @@ function assemblePackageRow(p) {
4717
5041
  peerDependencies: r?.peerDependencies ?? null,
4718
5042
  peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
4719
5043
  engines: r?.engines ?? null,
5044
+ runtimeTarget: r ? classifyRuntimeTarget({
5045
+ name: p.name,
5046
+ hasBrowserField: r.hasBrowserField,
5047
+ keywords: r.keywords,
5048
+ engines: r.engines,
5049
+ peerDependencies: r.peerDependencies
5050
+ }) : null,
4720
5051
  healthScore: p.healthScore,
4721
5052
  qualityScore: p.qualityScore,
4722
5053
  confidence: p.confidence,
@@ -4737,6 +5068,7 @@ var init_sync = __esm({
4737
5068
  init_errors();
4738
5069
  init_http();
4739
5070
  init_logger();
5071
+ init_runtimeTarget();
4740
5072
  init_collect();
4741
5073
  init_npmDownloads();
4742
5074
  init_summarize();
@@ -4810,7 +5142,7 @@ var init_ingestQueue = __esm({
4810
5142
  });
4811
5143
 
4812
5144
  // src/pipeline/single.ts
4813
- import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql6 } from "drizzle-orm";
5145
+ import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
4814
5146
  function raceTimeout(p, ms) {
4815
5147
  let timer;
4816
5148
  return Promise.race([
@@ -4826,7 +5158,7 @@ async function getSeedCategory(db, name) {
4826
5158
  }
4827
5159
  async function getCategoryMedianBundle(db, category) {
4828
5160
  const [row] = await db.select({
4829
- m: sql6`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
5161
+ m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
4830
5162
  }).from(packages).where(and6(eq8(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
4831
5163
  return row?.m ?? null;
4832
5164
  }
@@ -4858,12 +5190,14 @@ async function syncOnePackage(db, name, opts = {}) {
4858
5190
  const input = toScoringInput(signals, category);
4859
5191
  const median2 = category ? await getCategoryMedianBundle(db, category) : null;
4860
5192
  const quality = computeQuality(input);
5193
+ const evidence = await getFieldEvidence(db, [name]);
4861
5194
  const breakdown = {
4862
5195
  maintenance: computeMaintenance(input, now),
4863
5196
  adoption: computeAdoption(input),
4864
5197
  reliability: computeReliability(input),
4865
5198
  efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
4866
- quality
5199
+ quality,
5200
+ field: computeFieldScore(evidence.get(name) ?? null)
4867
5201
  };
4868
5202
  const healthScore = computeHealthScore(breakdown);
4869
5203
  const confidence = computeConfidence(input, now, quality);
@@ -4933,6 +5267,7 @@ var init_single = __esm({
4933
5267
  init_categoryInference();
4934
5268
  init_scoring();
4935
5269
  init_embeddings();
5270
+ init_outcomes();
4936
5271
  init_packages();
4937
5272
  init_schema();
4938
5273
  init_alerts2();
@@ -4944,7 +5279,7 @@ var init_single = __esm({
4944
5279
  });
4945
5280
 
4946
5281
  // src/search/recommend.ts
4947
- import { and as and7, cosineDistance, eq as eq9, inArray as inArray4, isNotNull as isNotNull4, lte, sql as sql7 } from "drizzle-orm";
5282
+ import { and as and7, cosineDistance, eq as eq9, inArray as inArray5, isNotNull as isNotNull4, lte, sql as sql9 } from "drizzle-orm";
4948
5283
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
4949
5284
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
4950
5285
  const [queryVec] = await provider.embed([opts.need]);
@@ -4997,7 +5332,7 @@ function buildConditions(constraints, category) {
4997
5332
  const allowed = ["proven", "emerging", "promising", "unproven"].filter(
4998
5333
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
4999
5334
  );
5000
- conditions.push(inArray4(packages.confidence, allowed));
5335
+ conditions.push(inArray5(packages.confidence, allowed));
5001
5336
  }
5002
5337
  return conditions;
5003
5338
  }
@@ -5011,10 +5346,10 @@ async function runVectorQuery(db, queryVec, providerId, constraints, category, p
5011
5346
  return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(distance).limit(pool);
5012
5347
  }
5013
5348
  async function runLexicalQuery(db, need, constraints, category, pool) {
5014
- const tsquery = sql7`websearch_to_tsquery('english', ${need})`;
5015
- const rank2 = sql7`ts_rank(${packages.searchVector}, ${tsquery})`;
5016
- const conditions = [sql7`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
5017
- return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql7`${rank2} desc`).limit(pool);
5349
+ const tsquery = sql9`websearch_to_tsquery('english', ${need})`;
5350
+ const rank2 = sql9`ts_rank(${packages.searchVector}, ${tsquery})`;
5351
+ const conditions = [sql9`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
5352
+ return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql9`${rank2} desc`).limit(pool);
5018
5353
  }
5019
5354
  function toCandidate(row) {
5020
5355
  return {
@@ -5152,7 +5487,7 @@ var init_types3 = __esm({
5152
5487
  });
5153
5488
 
5154
5489
  // src/db/selectionPolicy.ts
5155
- import { eq as eq10, inArray as inArray5 } from "drizzle-orm";
5490
+ import { eq as eq10, inArray as inArray6 } from "drizzle-orm";
5156
5491
  async function getSelectionPolicy(db, ownerId) {
5157
5492
  if (!ownerId) return DEFAULT_SELECTION_POLICY;
5158
5493
  const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(eq10(selectionPolicies.ownerId, ownerId)).limit(1);
@@ -5171,7 +5506,7 @@ async function loadPolicyFacts(db, names2) {
5171
5506
  name: packages.name,
5172
5507
  license: packages.license,
5173
5508
  deprecated: packages.deprecated
5174
- }).from(packages).where(inArray5(packages.name, names2));
5509
+ }).from(packages).where(inArray6(packages.name, names2));
5175
5510
  for (const row of rows) {
5176
5511
  out.set(row.name, { license: row.license, deprecated: row.deprecated });
5177
5512
  }
@@ -5488,7 +5823,7 @@ __export(handlers_exports, {
5488
5823
  rowToEvaluate: () => rowToEvaluate
5489
5824
  });
5490
5825
  import { createHash as createHash3 } from "crypto";
5491
- import { sql as sql8 } from "drizzle-orm";
5826
+ import { sql as sql10 } from "drizzle-orm";
5492
5827
  function isStale(dataAsOf) {
5493
5828
  if (!dataAsOf) return true;
5494
5829
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
@@ -5504,7 +5839,7 @@ function topAdvisories(advisories, max = 5) {
5504
5839
  if (!advisories?.length) return [];
5505
5840
  return [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).slice(0, max);
5506
5841
  }
5507
- function rowToEvaluate(row) {
5842
+ function rowToEvaluate(row, learned) {
5508
5843
  const breakdown = row.scoreBreakdown ?? {
5509
5844
  maintenance: 0,
5510
5845
  adoption: 0,
@@ -5533,11 +5868,11 @@ function rowToEvaluate(row) {
5533
5868
  summary: row.summary ? truncateSentences(row.summary, 3) : null,
5534
5869
  usageGuide: row.usageGuide ?? null,
5535
5870
  repoUrl: row.repoUrl,
5536
- replacedBy: lookupSuccessor(row.name)
5871
+ replacedBy: lookupSuccessor(row.name, learned)
5537
5872
  };
5538
5873
  }
5539
5874
  async function latestDataAsOf(db) {
5540
- const [row] = await db.select({ m: sql8`max(${packages.dataAsOf})` }).from(packages);
5875
+ const [row] = await db.select({ m: sql10`max(${packages.dataAsOf})` }).from(packages);
5541
5876
  return new Date(row?.m ?? Date.now()).toISOString();
5542
5877
  }
5543
5878
  function cacheKey(parts) {
@@ -5580,7 +5915,8 @@ async function handleEvaluate(db, input, ownerId = null) {
5580
5915
  suggestion: existsOnNpm ? `\u{1F389} Congrats, you're the first to add "${input.package}" to lurq's registry! It's being fetched and scored now; retry in a few seconds for the full evidence read.` : `"${input.package}" was not found on the npm registry. Check the package name.`
5581
5916
  };
5582
5917
  }
5583
- const evaluated2 = rowToEvaluate(row);
5918
+ const learned = row.deprecated || row.archived ? await loadLearnedSuccessors(db) : void 0;
5919
+ const evaluated2 = rowToEvaluate(row, learned);
5584
5920
  const verification = await getLatestVerificationByName(db, row.name);
5585
5921
  return verification ? { ...evaluated2, buildVerified: toBuildVerified(verification) } : evaluated2;
5586
5922
  },
@@ -5607,7 +5943,7 @@ async function handleCompare(db, input, ownerId = null) {
5607
5943
  const results = await Promise.all(
5608
5944
  input.packages.map((name) => getOrFetchPackage(db, name, { requestedByOwnerId: ownerId }))
5609
5945
  );
5610
- const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
5946
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map((row) => rowToEvaluate(row)).sort((a, b) => b.healthScore - a.healthScore);
5611
5947
  const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
5612
5948
  return {
5613
5949
  dataAsOf: await latestDataAsOf(db),
@@ -5892,7 +6228,8 @@ function parseRpcBody(raw) {
5892
6228
  }
5893
6229
  }
5894
6230
  function fetchUpgradePlan(deps, opts = {}) {
5895
- return post("/upgrade-plan", { deps }, opts);
6231
+ const body = opts.repo ? { deps, repo: opts.repo } : { deps };
6232
+ return post("/upgrade-plan", body, opts);
5896
6233
  }
5897
6234
  var RemoteError;
5898
6235
  var init_remote = __esm({
@@ -5987,10 +6324,10 @@ var init_optimize = __esm({
5987
6324
  });
5988
6325
 
5989
6326
  // src/mcp/diagram.ts
5990
- import { inArray as inArray6 } from "drizzle-orm";
6327
+ import { inArray as inArray7 } from "drizzle-orm";
5991
6328
  function layerFor(item) {
5992
6329
  if (!item.category) return UNCLASSIFIED;
5993
- if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
6330
+ if (item.category === "framework" && item.runtimeTarget === "node") return "Backend";
5994
6331
  return LAYER_OF[item.category] ?? UNCLASSIFIED;
5995
6332
  }
5996
6333
  function nodeId(label, i) {
@@ -6040,22 +6377,41 @@ async function handleDiagram(db, input) {
6040
6377
  note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
6041
6378
  };
6042
6379
  }
6043
- const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray6(packages.name, input.stack));
6044
- const known = new Map(rows.map((r) => [r.name, r.category]));
6045
- const items = input.stack.map((name) => ({
6046
- label: name,
6047
- category: known.get(name) ?? inferCategory(name)
6048
- }));
6380
+ const rows = await db.select({
6381
+ name: packages.name,
6382
+ category: packages.category,
6383
+ runtimeTarget: packages.runtimeTarget
6384
+ }).from(packages).where(inArray7(packages.name, input.stack));
6385
+ const known = new Map(rows.map((r) => [r.name, r]));
6386
+ const items = input.stack.map((name) => {
6387
+ const row = known.get(name);
6388
+ return {
6389
+ label: name,
6390
+ category: row?.category ?? inferCategory(name),
6391
+ // Fall back to the name-only classifier for a package that predates the
6392
+ // column or is not indexed at all: the curated overrides still resolve the
6393
+ // frameworks people actually diagram, and an unclassifiable one keeps the
6394
+ // old Presentation default rather than moving on a null.
6395
+ runtimeTarget: row?.runtimeTarget ?? classifyRuntimeTarget({
6396
+ name,
6397
+ hasBrowserField: false,
6398
+ keywords: [],
6399
+ engines: null,
6400
+ peerDependencies: null
6401
+ })
6402
+ };
6403
+ });
6049
6404
  const unclassified = items.filter((i) => layerFor(i) === UNCLASSIFIED).map((i) => i.label);
6050
6405
  const note = unclassified.length ? `Reference pattern derived from the provided stack: partial, not authoritative. Could not place ${unclassified.length} package(s): ${unclassified.join(", ")} (not in lurq's index and no category match), shown under Unclassified, outside the flow.` : "Reference pattern derived from the provided stack. A labeled starting point, not a validated architecture.";
6051
6406
  return { mermaid: buildMermaid(items), note };
6052
6407
  }
6053
- var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS, BACKEND_FRAMEWORKS;
6408
+ var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS;
6054
6409
  var init_diagram = __esm({
6055
6410
  "src/mcp/diagram.ts"() {
6056
6411
  "use strict";
6057
6412
  init_esm_shims();
6058
6413
  init_categoryInference();
6414
+ init_runtimeTarget();
6059
6415
  init_schema();
6060
6416
  UNCLASSIFIED = "Unclassified";
6061
6417
  LAYER_OF = {
@@ -6083,14 +6439,6 @@ var init_diagram = __esm({
6083
6439
  testing: "Tooling"
6084
6440
  };
6085
6441
  FLOW_LAYERS = ["Presentation", "Client Logic", "Backend", "Data"];
6086
- BACKEND_FRAMEWORKS = /* @__PURE__ */ new Set([
6087
- "express",
6088
- "fastify",
6089
- "koa",
6090
- "hono",
6091
- "@nestjs/core",
6092
- "@hapi/hapi"
6093
- ]);
6094
6442
  }
6095
6443
  });
6096
6444
 
@@ -6105,7 +6453,7 @@ __export(plan_exports, {
6105
6453
  resolvePins: () => resolvePins
6106
6454
  });
6107
6455
  import { createHash as createHash4 } from "crypto";
6108
- import { inArray as inArray7 } from "drizzle-orm";
6456
+ import { inArray as inArray8 } from "drizzle-orm";
6109
6457
  function packageToCandidate(row) {
6110
6458
  return {
6111
6459
  name: row.name,
@@ -6326,7 +6674,7 @@ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
6326
6674
  async function bundleSizes(db, candidates) {
6327
6675
  const names2 = [...new Set(candidates.map((c) => c.name))];
6328
6676
  if (names2.length === 0) return /* @__PURE__ */ new Map();
6329
- const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray7(packages.name, names2));
6677
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray8(packages.name, names2));
6330
6678
  return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
6331
6679
  }
6332
6680
  async function decompose(document) {
@@ -7880,6 +8228,10 @@ var init_app = __esm({
7880
8228
  });
7881
8229
 
7882
8230
  // src/github/workflow.ts
8231
+ function cliSpec(version = VERSION) {
8232
+ const [major = "0", minor = "0"] = version.split(".");
8233
+ return major === "0" ? `${PACKAGE_NAME}@${major}.${minor}` : `${PACKAGE_NAME}@${major}`;
8234
+ }
7883
8235
  function detectInstallCommand(lockfiles) {
7884
8236
  if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm install --frozen-lockfile";
7885
8237
  if (lockfiles.includes("yarn.lock")) return "yarn install --frozen-lockfile";
@@ -7892,6 +8244,8 @@ function renderWorkflow(opts = {}) {
7892
8244
  const install = opts.installCommand ?? "npm ci";
7893
8245
  const max = opts.maxUpgrades ?? 3;
7894
8246
  const mode = opts.armed ? "pr" : "comment";
8247
+ const autoMerge = opts.autoMerge ?? false;
8248
+ const cli = cliSpec();
7895
8249
  return `# Managed by lurq, https://lurq.run
7896
8250
  #
7897
8251
  # Keeps this repository's dependencies current and rewrites the call sites an
@@ -7939,19 +8293,19 @@ jobs:
7939
8293
  - name: Plan
7940
8294
  env:
7941
8295
  LURQ_API_KEY: \${{ secrets.LURQ_API_KEY }}
7942
- run: npx -y lurqrun upgrade-plan . --json > lurq-plan.json
8296
+ run: npx -y ${cli} upgrade-plan . --json > lurq-plan.json
7943
8297
 
7944
8298
  # 2. Narrow that to symbols THIS repo references. Runs entirely locally
7945
8299
  # against both versions' npm tarballs, no API key, no test suite.
7946
8300
  - name: Check against this codebase
7947
- run: npx -y lurqrun check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
8301
+ run: npx -y ${cli} check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
7948
8302
 
7949
8303
  # The same report twice: once into the run summary, once as the body of
7950
8304
  # the pull request. Fenced, because the report is aligned plain text and
7951
8305
  # markdown would otherwise collapse its indentation into one paragraph.
7952
8306
  - name: Summarise
7953
8307
  run: |
7954
- npx -y lurqrun check-upgrade . --plan lurq-plan.json > lurq-report.txt
8308
+ npx -y ${cli} check-upgrade . --plan lurq-plan.json > lurq-report.txt
7955
8309
  { echo '\`\`\`'; cat lurq-report.txt; echo '\`\`\`'; } > lurq-report.md
7956
8310
  cat lurq-report.md >> "\${GITHUB_STEP_SUMMARY}"
7957
8311
 
@@ -8027,6 +8381,7 @@ jobs:
8027
8381
 
8028
8382
  # 4. The workflow does version control, never the model.
8029
8383
  - name: Open pull request
8384
+ id: pr
8030
8385
  if: env.LURQ_MODE == 'pr'
8031
8386
  uses: peter-evans/create-pull-request@v7
8032
8387
  with:
@@ -8039,19 +8394,30 @@ jobs:
8039
8394
  body-path: lurq-report.md
8040
8395
  labels: dependencies
8041
8396
  delete-branch: true
8042
- `;
8397
+ ${autoMerge ? MERGE_STEP : ""}`;
8043
8398
  }
8044
8399
  function newFileUrl(fullName, branch, content) {
8045
8400
  const params = new URLSearchParams({ filename: WORKFLOW_PATH, value: content });
8046
8401
  return `https://github.com/${fullName}/new/${encodeURIComponent(branch)}?${params.toString()}`;
8047
8402
  }
8048
- var WORKFLOW_PATH, DEFAULT_CRON;
8403
+ var WORKFLOW_PATH, DEFAULT_CRON, MERGE_STEP;
8049
8404
  var init_workflow = __esm({
8050
8405
  "src/github/workflow.ts"() {
8051
8406
  "use strict";
8052
8407
  init_esm_shims();
8408
+ init_constants();
8053
8409
  WORKFLOW_PATH = ".github/workflows/lurq-upgrade.yml";
8054
8410
  DEFAULT_CRON = "0 6 * * 1";
8411
+ MERGE_STEP = `
8412
+ # 5. Auto-merge, per this repository's lurq policy. GitHub merges only
8413
+ # once the repo's OWN required checks pass; lurq never makes that call.
8414
+ # Requires "Allow auto-merge" in repository settings.
8415
+ - name: Enable auto-merge
8416
+ if: env.LURQ_MODE == 'pr' && steps.pr.outputs.pull-request-number
8417
+ env:
8418
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
8419
+ run: gh pr merge --auto --squash "\${{ steps.pr.outputs.pull-request-number }}"
8420
+ `;
8055
8421
  }
8056
8422
  });
8057
8423
 
@@ -8148,6 +8514,7 @@ var init_manifests = __esm({
8148
8514
  var upgradePlan_exports = {};
8149
8515
  __export(upgradePlan_exports, {
8150
8516
  buildUpgradePlan: () => buildUpgradePlan,
8517
+ detectRepoFullName: () => detectRepoFullName,
8151
8518
  findManifests: () => findManifests,
8152
8519
  formatUpgradePlan: () => formatUpgradePlan,
8153
8520
  mergedDeps: () => mergedDeps,
@@ -8201,27 +8568,38 @@ function mergedDeps(manifests) {
8201
8568
  }
8202
8569
  return out;
8203
8570
  }
8571
+ function detectRepoFullName(explicit) {
8572
+ const value = explicit ?? process.env.GITHUB_REPOSITORY ?? null;
8573
+ return value && /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
8574
+ }
8204
8575
  async function buildUpgradePlan(dir, opts = {}) {
8205
8576
  const manifests = readManifests(dir);
8206
8577
  const deps = mergedDeps(manifests);
8207
8578
  if (Object.keys(deps).length === 0) {
8208
8579
  throw new Error(`No package.json with dependencies found under ${dir}`);
8209
8580
  }
8210
- const plan = await fetchUpgradePlan(deps, opts);
8581
+ const plan = await fetchUpgradePlan(deps, { ...opts, repo: detectRepoFullName(opts.repo) });
8211
8582
  return { ...plan, manifests: manifests.length, deps: Object.keys(deps).length };
8212
8583
  }
8213
8584
  function formatUpgradePlan(plan) {
8214
8585
  const out = [
8215
- `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`,
8216
- ""
8586
+ `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`
8217
8587
  ];
8588
+ if (plan.scopeSource === "repo-policy") {
8589
+ out.push(
8590
+ `policy: scope=${plan.scope}` + (plan.outOfScope ? `, ${plan.outOfScope} upgrade(s) held back` : ", nothing held back")
8591
+ );
8592
+ }
8593
+ out.push("");
8218
8594
  if (plan.upgrades.length === 0) {
8219
8595
  out.push("Everything lurq tracks is already on its latest release.");
8220
8596
  }
8221
8597
  for (const upgrade of plan.upgrades) {
8598
+ const held = upgrade.inScope === false ? " [HELD]" : "";
8222
8599
  out.push(
8223
- `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "")
8600
+ `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "") + held
8224
8601
  );
8602
+ if (upgrade.scopeReason) out.push(` held: ${upgrade.scopeReason}`);
8225
8603
  if (upgrade.removed.length) {
8226
8604
  out.push(` removes ${upgrade.removed.length}: ${upgrade.removed.slice(0, 8).join(", ")}`);
8227
8605
  }
@@ -9367,7 +9745,7 @@ function targetsFromPlanFile(path2) {
9367
9745
  if (!Array.isArray(parsed.upgrades)) {
9368
9746
  throw new Error(`${path2} is not an upgrade plan (no "upgrades" array)`);
9369
9747
  }
9370
- return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
9748
+ return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).filter((u) => u.inScope !== false).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
9371
9749
  }
9372
9750
  async function runCheckUpgrade(dir, opts) {
9373
9751
  const { scanReferences: scanReferences2 } = await Promise.resolve().then(() => (init_references(), references_exports));
@@ -9422,7 +9800,7 @@ var init_graph = __esm({
9422
9800
  });
9423
9801
 
9424
9802
  // src/db/surface.ts
9425
- import { and as and9, desc as desc5, eq as eq13, sql as sql9 } from "drizzle-orm";
9803
+ import { and as and9, desc as desc5, eq as eq13, sql as sql11 } from "drizzle-orm";
9426
9804
  function surfaceRef(pkg, version) {
9427
9805
  return { kind: "package_surface", namespace: "npm", name: pkg, version };
9428
9806
  }
@@ -9705,22 +10083,22 @@ var init_toolDescriptions = __esm({
9705
10083
  });
9706
10084
 
9707
10085
  // src/db/usage.ts
9708
- import { and as and11, desc as desc7, eq as eq15, gte, sql as sql10 } from "drizzle-orm";
10086
+ import { and as and11, desc as desc7, eq as eq15, gte, sql as sql12 } from "drizzle-orm";
9709
10087
  function windowStart(days) {
9710
- return sql10`CURRENT_DATE - (${days}::int - 1)`;
10088
+ return sql12`CURRENT_DATE - (${days}::int - 1)`;
9711
10089
  }
9712
10090
  async function recordUsage(db, ownerId, tool) {
9713
10091
  if (!ownerId) return;
9714
10092
  try {
9715
- await db.insert(ownerUsageDaily).values({ ownerId, date: sql10`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
10093
+ await db.insert(ownerUsageDaily).values({ ownerId, date: sql12`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
9716
10094
  target: [ownerUsageDaily.ownerId, ownerUsageDaily.date, ownerUsageDaily.tool],
9717
- set: { count: sql10`${ownerUsageDaily.count} + 1` }
10095
+ set: { count: sql12`${ownerUsageDaily.count} + 1` }
9718
10096
  });
9719
10097
  } catch {
9720
10098
  }
9721
10099
  }
9722
10100
  async function getUsageSummary(db, ownerId, days) {
9723
- const rows = await db.execute(sql10`
10101
+ const rows = await db.execute(sql12`
9724
10102
  select
9725
10103
  to_char(d, 'YYYY-MM-DD') as date,
9726
10104
  coalesce(sum(u.count), 0)::int as count
@@ -9737,13 +10115,13 @@ async function getUsageSummary(db, ownerId, days) {
9737
10115
  async function getUsageByTool(db, ownerId, days) {
9738
10116
  const rows = await db.select({
9739
10117
  tool: ownerUsageDaily.tool,
9740
- count: sql10`sum(${ownerUsageDaily.count})::int`
10118
+ count: sql12`sum(${ownerUsageDaily.count})::int`
9741
10119
  }).from(ownerUsageDaily).where(
9742
10120
  and11(
9743
10121
  eq15(ownerUsageDaily.ownerId, ownerId),
9744
10122
  gte(ownerUsageDaily.date, windowStart(days))
9745
10123
  )
9746
- ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql10`sum(${ownerUsageDaily.count})`));
10124
+ ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql12`sum(${ownerUsageDaily.count})`));
9747
10125
  return rows.map((r) => ({ tool: r.tool, count: Number(r.count) }));
9748
10126
  }
9749
10127
  var init_usage = __esm({
@@ -10264,6 +10642,52 @@ var init_brief = __esm({
10264
10642
  }
10265
10643
  });
10266
10644
 
10645
+ // src/github/scope.ts
10646
+ function isUnassessed(u) {
10647
+ return u.verdict === "unknown";
10648
+ }
10649
+ function breaksSomething(u) {
10650
+ return u.verdict === "removes-exports" || u.verdict === "arity-changed";
10651
+ }
10652
+ function scopeVerdict(u, scope) {
10653
+ if (scope === "all") return { inScope: true };
10654
+ if (u.advisories > 0) return { inScope: true };
10655
+ if (scope === "security") {
10656
+ return { inScope: false, reason: "no advisory \u2014 scope is security-only" };
10657
+ }
10658
+ if (isUnassessed(u)) {
10659
+ return { inScope: false, reason: "surface not yet analysed \u2014 nothing for the agent to rewrite" };
10660
+ }
10661
+ if (breaksSomething(u)) return { inScope: true };
10662
+ return { inScope: false, reason: "nothing referenced breaks \u2014 scope is security + blocking" };
10663
+ }
10664
+ function applyScope(upgrades, policy) {
10665
+ if (!policy) {
10666
+ return {
10667
+ upgrades: upgrades.map((u) => ({ ...u, inScope: true })),
10668
+ scope: "all",
10669
+ scopeSource: "unconnected",
10670
+ outOfScope: 0
10671
+ };
10672
+ }
10673
+ const scoped = upgrades.map((u) => {
10674
+ const verdict = scopeVerdict(u, policy.scope);
10675
+ return verdict.inScope ? { ...u, inScope: true } : { ...u, inScope: false, scopeReason: verdict.reason };
10676
+ });
10677
+ return {
10678
+ upgrades: scoped,
10679
+ scope: policy.scope,
10680
+ scopeSource: "repo-policy",
10681
+ outOfScope: scoped.filter((u) => !u.inScope).length
10682
+ };
10683
+ }
10684
+ var init_scope = __esm({
10685
+ "src/github/scope.ts"() {
10686
+ "use strict";
10687
+ init_esm_shims();
10688
+ }
10689
+ });
10690
+
10267
10691
  // src/github/runs.ts
10268
10692
  function str2(value, max) {
10269
10693
  if (typeof value !== "string") return null;
@@ -10280,7 +10704,7 @@ function strList(value, maxItems, maxLen) {
10280
10704
  }
10281
10705
  return out;
10282
10706
  }
10283
- function count(value) {
10707
+ function count2(value) {
10284
10708
  const n = typeof value === "number" ? value : Number(value);
10285
10709
  return Number.isInteger(n) && n >= 0 && n < 1e6 ? n : null;
10286
10710
  }
@@ -10304,9 +10728,9 @@ function parseUpgradeRun(input) {
10304
10728
  severity: severity2,
10305
10729
  status,
10306
10730
  symbolsAffected: strList(raw.symbolsAffected, MAX_SYMBOLS, MAX_NAME),
10307
- callSites: count(raw.callSites) ?? 0,
10731
+ callSites: count2(raw.callSites) ?? 0,
10308
10732
  callSiteFiles: files,
10309
- filesChanged: count(raw.filesChanged),
10733
+ filesChanged: count2(raw.filesChanged),
10310
10734
  testsPassed: typeof raw.testsPassed === "boolean" ? raw.testsPassed : null,
10311
10735
  prUrl: str2(raw.prUrl, MAX_URL),
10312
10736
  // Empty string, not null: it is part of the dedup key (see schema).
@@ -10324,6 +10748,11 @@ function parseUpgradeRuns(input, max) {
10324
10748
  }
10325
10749
  return { runs, rejected };
10326
10750
  }
10751
+ function parseRepoFullName(input) {
10752
+ const value = str2(input, MAX_NAME * 2);
10753
+ if (!value) return null;
10754
+ return /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
10755
+ }
10327
10756
  function parseDepsInput(input) {
10328
10757
  if (!input || typeof input !== "object") return {};
10329
10758
  const out = {};
@@ -10351,7 +10780,7 @@ var init_runs = __esm({
10351
10780
  });
10352
10781
 
10353
10782
  // src/db/upgradeRuns.ts
10354
- import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql11 } from "drizzle-orm";
10783
+ import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql13 } from "drizzle-orm";
10355
10784
  async function recordUpgradeRuns(db, rows) {
10356
10785
  if (rows.length === 0) return 0;
10357
10786
  await db.insert(upgradeRuns).values(rows).onConflictDoUpdate({
@@ -10363,15 +10792,15 @@ async function recordUpgradeRuns(db, rows) {
10363
10792
  upgradeRuns.runUrl
10364
10793
  ],
10365
10794
  set: {
10366
- severity: sql11`excluded.severity`,
10367
- status: sql11`excluded.status`,
10368
- symbolsAffected: sql11`excluded.symbols_affected`,
10369
- callSites: sql11`excluded.call_sites`,
10370
- callSiteFiles: sql11`excluded.call_site_files`,
10371
- filesChanged: sql11`excluded.files_changed`,
10372
- testsPassed: sql11`excluded.tests_passed`,
10373
- prUrl: sql11`excluded.pr_url`,
10374
- repoId: sql11`excluded.repo_id`
10795
+ severity: sql13`excluded.severity`,
10796
+ status: sql13`excluded.status`,
10797
+ symbolsAffected: sql13`excluded.symbols_affected`,
10798
+ callSites: sql13`excluded.call_sites`,
10799
+ callSiteFiles: sql13`excluded.call_site_files`,
10800
+ filesChanged: sql13`excluded.files_changed`,
10801
+ testsPassed: sql13`excluded.tests_passed`,
10802
+ prUrl: sql13`excluded.pr_url`,
10803
+ repoId: sql13`excluded.repo_id`
10375
10804
  }
10376
10805
  });
10377
10806
  return rows.length;
@@ -10386,12 +10815,12 @@ async function listRunsForRepo(db, ownerId, repoId, limit = 50) {
10386
10815
  async function getUpgradeImpact(db, ownerId, days = 30) {
10387
10816
  const since = new Date(Date.now() - days * 864e5);
10388
10817
  const rows = await db.select({
10389
- analysed: sql11`count(*)::int`,
10390
- blocking: sql11`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
10391
- callSites: sql11`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
10392
- prsOpened: sql11`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
10393
- merged: sql11`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
10394
- unverified: sql11`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
10818
+ analysed: sql13`count(*)::int`,
10819
+ blocking: sql13`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
10820
+ callSites: sql13`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
10821
+ prsOpened: sql13`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
10822
+ merged: sql13`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
10823
+ unverified: sql13`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
10395
10824
  }).from(upgradeRuns).where(and13(eq17(upgradeRuns.ownerId, ownerId), gte2(upgradeRuns.createdAt, since)));
10396
10825
  return rows[0] ?? {
10397
10826
  analysed: 0,
@@ -10946,7 +11375,8 @@ async function startHttpServer(opts = {}) {
10946
11375
  const runs = await listRunsForRepo(db, ownerId, row.id);
10947
11376
  const workflow = renderWorkflow({
10948
11377
  installCommand: row.installCommand ?? void 0,
10949
- armed: row.policy.enabled
11378
+ armed: row.policy.enabled,
11379
+ autoMerge: row.policy.autoMerge
10950
11380
  });
10951
11381
  res.status(200).json({
10952
11382
  repo: {
@@ -11076,8 +11506,13 @@ async function startHttpServer(opts = {}) {
11076
11506
  try {
11077
11507
  const drift = await computeDrift(db, [{ path: "package.json", deps }]);
11078
11508
  const brief = await briefRepo(db, drift);
11509
+ const ownerId = req.lurqKey?.ownerId ?? null;
11510
+ const repoFullName = parseRepoFullName((req.body ?? {}).repo);
11511
+ const policy = ownerId && repoFullName ? await findPolicyByFullName(db, ownerId, repoFullName) : null;
11512
+ const scoped = applyScope(brief.upgrades, policy);
11079
11513
  res.status(200).json({
11080
11514
  ...brief,
11515
+ ...scoped,
11081
11516
  // Surfaced so CI can log what lurq had no opinion on. An upgrade we do
11082
11517
  // not know about must not look like an upgrade we cleared.
11083
11518
  untracked: Object.keys(deps).length - drift.depsTracked
@@ -11158,6 +11593,7 @@ var init_http2 = __esm({
11158
11593
  init_app();
11159
11594
  init_brief();
11160
11595
  init_drift();
11596
+ init_scope();
11161
11597
  init_runs();
11162
11598
  init_upgradeRuns();
11163
11599
  init_manifests();
@@ -11249,9 +11685,9 @@ function buildProgram() {
11249
11685
  const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
11250
11686
  await runPlan2(file, opts);
11251
11687
  });
11252
- program.command("upgrade-plan").argument("[dir]", "project directory (default: current)", ".").description("what is behind in this project, and what each upgrade removes from its API").option("--json", "output the plan as JSON (feed to `check-upgrade --plan`)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--api-key <key>", "hosted API key (defaults to $LURQ_API_KEY)").action(async (dir, opts) => {
11688
+ program.command("upgrade-plan").argument("[dir]", "project directory (default: current)", ".").description("what is behind in this project, and what each upgrade removes from its API").option("--json", "output the plan as JSON (feed to `check-upgrade --plan`)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--api-key <key>", "hosted API key (defaults to $LURQ_API_KEY)").option("--repo <owner/name>", "apply this repo's policy (defaults to $GITHUB_REPOSITORY)").action(async (dir, opts) => {
11253
11689
  const { buildUpgradePlan: buildUpgradePlan2, formatUpgradePlan: formatUpgradePlan2 } = await Promise.resolve().then(() => (init_upgradePlan(), upgradePlan_exports));
11254
- const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey });
11690
+ const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey, repo: opts.repo });
11255
11691
  console.log(opts.json ? JSON.stringify(plan, null, 2) : formatUpgradePlan2(plan));
11256
11692
  });
11257
11693
  program.command("check-upgrade").argument("[dir]", "project directory to scan (default: current)", ".").description("do these upgrades remove symbols your code actually references?").option("--plan <file>", "targets from `upgrade-plan --json`").option("--upgrade <spec...>", "pkg@from..to (repeatable), e.g. commander@11.1.0..12.1.0").option("--json", "output the report as JSON").option("--exit-code", "exit 1 when the report is not safe (for CI)").action(