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/bin/lurq.js CHANGED
@@ -1112,6 +1112,10 @@ var init_schema = __esm({
1112
1112
  peerDependencies: jsonb("peer_dependencies").$type(),
1113
1113
  peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
1114
1114
  engines: jsonb("engines").$type(),
1115
+ /** Where this package runs, classified from its own manifest at ingest.
1116
+ * Resolves the `framework` category's frontend/backend ambiguity for the
1117
+ * architecture diagram. Null = the manifest did not say clearly enough. */
1118
+ runtimeTarget: text("runtime_target").$type(),
1115
1119
  // Computed outputs
1116
1120
  healthScore: integer("health_score"),
1117
1121
  /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
@@ -1229,7 +1233,13 @@ var init_schema = __esm({
1229
1233
  {
1230
1234
  packageName: text("package_name").notNull(),
1231
1235
  version: text("version").notNull(),
1232
- publishedAt: ts("published_at")
1236
+ publishedAt: ts("published_at"),
1237
+ /** Peers/engines declared by this exact version. Lifted from the registry
1238
+ * document at ingest (it already carries every version's manifest), so a
1239
+ * conflict check against the versions a repo actually RUNS is one indexed
1240
+ * query rather than a registry read per dependency. */
1241
+ peerDependencies: jsonb("peer_dependencies").$type(),
1242
+ engines: jsonb("engines").$type()
1233
1243
  },
1234
1244
  (table2) => [
1235
1245
  primaryKey({ columns: [table2.packageName, table2.version] }),
@@ -1610,10 +1620,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
1610
1620
  import postgres from "postgres";
1611
1621
  function createDb(opts = {}) {
1612
1622
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
1613
- const sql12 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1623
+ const sql14 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1614
1624
  } });
1615
- const db = drizzle(sql12, { schema: schema_exports });
1616
- return { db, sql: sql12, close: () => sql12.end() };
1625
+ const db = drizzle(sql14, { schema: schema_exports });
1626
+ return { db, sql: sql14, close: () => sql14.end() };
1617
1627
  }
1618
1628
  var init_client = __esm({
1619
1629
  "src/db/client.ts"() {
@@ -1643,7 +1653,9 @@ async function upsertPackageVersions(db, name, versions) {
1643
1653
  const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1644
1654
  packageName: name,
1645
1655
  version: v.version,
1646
- publishedAt: v.publishedAt
1656
+ publishedAt: v.publishedAt,
1657
+ peerDependencies: v.peerDependencies ?? null,
1658
+ engines: v.engines ?? null
1647
1659
  }));
1648
1660
  await db.insert(packageVersions).values(rows).onConflictDoNothing();
1649
1661
  }
@@ -2158,6 +2170,7 @@ function parseNpmRegistry(json2) {
2158
2170
  directDependenciesCount: countDeps(latestManifest?.dependencies),
2159
2171
  hasProvenance: detectProvenance(latestManifest),
2160
2172
  hasInstallScripts: detectInstallScripts(latestManifest),
2173
+ hasBrowserField: latestManifest?.browser != null,
2161
2174
  peerDependencies: parseDepMap(latestManifest?.peerDependencies),
2162
2175
  peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
2163
2176
  engines: parseDepMap(latestManifest?.engines),
@@ -2193,7 +2206,13 @@ function parseVersionTimeline(json2) {
2193
2206
  const versions = json2?.versions;
2194
2207
  if (!versions || typeof versions !== "object") return [];
2195
2208
  const time = json2?.time ?? {};
2196
- return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
2209
+ return Object.keys(versions).map((version) => ({
2210
+ version,
2211
+ publishedAt: toDate(time?.[version]),
2212
+ // Already in this document — no second fetch, no per-version round trip.
2213
+ peerDependencies: parseDepMap(versions[version]?.peerDependencies),
2214
+ engines: parseDepMap(versions[version]?.engines)
2215
+ })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
2197
2216
  }
2198
2217
  function parseKeywords(value) {
2199
2218
  if (!Array.isArray(value)) return [];
@@ -2773,18 +2792,21 @@ async function checkCompat(db, packages2, opts = {}) {
2773
2792
  provenance: edge.provenance,
2774
2793
  witnessCount: edge.witnessCount
2775
2794
  }));
2795
+ const versionOf = new Map(members.map((m) => [m.name, m.version]));
2796
+ const matchesCheckedVersions = (edge) => edgeMatchesVersions(edge, versionOf);
2776
2797
  for (const edge of edges) {
2777
2798
  if (edge.status === "conflict") {
2799
+ const at = matchesCheckedVersions(edge) ? "" : ` (recorded at those versions; you are checking ${edge.packageA}@${versionOf.get(edge.packageA) ?? "?"} and ${edge.packageB}@${versionOf.get(edge.packageB) ?? "?"})`;
2778
2800
  conflicts.push({
2779
2801
  source: "sandbox",
2780
2802
  packages: [edge.packageA, edge.packageB],
2781
- detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
2803
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox${at}`
2782
2804
  });
2783
2805
  }
2784
2806
  }
2785
2807
  const checkedNames = members.map((m) => m.name);
2786
2808
  const provenCompatible = new Set(
2787
- edges.filter((e) => e.status === "compatible").map((e) => pairKey(e.packageA, e.packageB))
2809
+ edges.filter((e) => e.status === "compatible" && matchesCheckedVersions(e)).map((e) => pairKey(e.packageA, e.packageB))
2788
2810
  );
2789
2811
  if (checkedNames.length >= 2 && !conflicts.length && !fullyCovered(checkedNames, provenCompatible)) {
2790
2812
  await enqueueCompatVerify(db, checkedNames).catch(() => {
@@ -2806,6 +2828,11 @@ async function checkCompat(db, packages2, opts = {}) {
2806
2828
  pairs: enumeratePairs(checkedNames, conflicts)
2807
2829
  };
2808
2830
  }
2831
+ function edgeMatchesVersions(edge, versionOf) {
2832
+ const a = versionOf.get(edge.packageA);
2833
+ const b = versionOf.get(edge.packageB);
2834
+ return a != null && b != null && a === edge.versionA && b === edge.versionB;
2835
+ }
2809
2836
  function enumeratePairs(names2, conflicts) {
2810
2837
  const byPair = /* @__PURE__ */ new Map();
2811
2838
  for (const c of conflicts) {
@@ -3239,22 +3266,6 @@ var init_verification = __esm({
3239
3266
  }
3240
3267
  });
3241
3268
 
3242
- // src/db/outcomes.ts
3243
- import { desc as desc3, eq as eq5 } from "drizzle-orm";
3244
- async function recordOutcome(db, outcome) {
3245
- await db.insert(recommendationOutcomes).values(outcome);
3246
- }
3247
- async function getOutcomesByOwner(db, ownerId, opts = {}) {
3248
- return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
3249
- }
3250
- var init_outcomes = __esm({
3251
- "src/db/outcomes.ts"() {
3252
- "use strict";
3253
- init_esm_shims();
3254
- init_schema();
3255
- }
3256
- });
3257
-
3258
3269
  // src/data/successors.json
3259
3270
  var successors_default;
3260
3271
  var init_successors = __esm({
@@ -3317,9 +3328,30 @@ var init_successors = __esm({
3317
3328
  });
3318
3329
 
3319
3330
  // src/core/successors.ts
3320
- function lookupSuccessor(name) {
3321
- const hit = MAP[name.toLowerCase()];
3322
- return hit ? { name: hit.replacedBy, reason: hit.reason } : null;
3331
+ function lookupSuccessor(name, learned) {
3332
+ const key = name.toLowerCase();
3333
+ const curated = MAP[key];
3334
+ if (curated) return { name: curated.replacedBy, reason: curated.reason };
3335
+ return learned?.get(key) ?? null;
3336
+ }
3337
+ function buildLearnedSuccessors(observed, isSuperseded) {
3338
+ const best = /* @__PURE__ */ new Map();
3339
+ for (const row of observed) {
3340
+ if (!isSuperseded(row.from)) continue;
3341
+ const key = row.from.toLowerCase();
3342
+ const current = best.get(key);
3343
+ const better = !current || row.owners > current.owners || row.owners === current.owners && row.observations > current.observations;
3344
+ if (better) best.set(key, { to: row.to, owners: row.owners, observations: row.observations });
3345
+ }
3346
+ return new Map(
3347
+ [...best].map(([from, hit]) => [
3348
+ from,
3349
+ {
3350
+ name: hit.to,
3351
+ reason: `${hit.owners} teams moved here after ${from} failed for the same need`
3352
+ }
3353
+ ])
3354
+ );
3323
3355
  }
3324
3356
  var MAP;
3325
3357
  var init_successors2 = __esm({
@@ -3331,6 +3363,81 @@ var init_successors2 = __esm({
3331
3363
  }
3332
3364
  });
3333
3365
 
3366
+ // src/db/outcomes.ts
3367
+ import { count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
3368
+ import { alias } from "drizzle-orm/pg-core";
3369
+ async function recordOutcome(db, outcome) {
3370
+ await db.insert(recommendationOutcomes).values(outcome);
3371
+ }
3372
+ async function getFieldEvidence(db, names2) {
3373
+ if (names2 && names2.length === 0) return /* @__PURE__ */ new Map();
3374
+ const rows = await db.select({
3375
+ packageName: recommendationOutcomes.packageName,
3376
+ reports: count(),
3377
+ successes: sql5`count(*) FILTER (WHERE ${SUCCESS_PREDICATE})`.mapWith(Number)
3378
+ }).from(recommendationOutcomes).where(names2 ? inArray3(recommendationOutcomes.packageName, names2) : void 0).groupBy(recommendationOutcomes.packageName);
3379
+ return new Map(
3380
+ rows.map((r) => [r.packageName, { reports: Number(r.reports), successes: r.successes }])
3381
+ );
3382
+ }
3383
+ async function learnSuccessions(db) {
3384
+ const failed = alias(recommendationOutcomes, "failed");
3385
+ const worked = alias(recommendationOutcomes, "worked");
3386
+ const rows = await db.select({
3387
+ from: failed.packageName,
3388
+ to: worked.packageName,
3389
+ owners: sql5`count(DISTINCT ${failed.ownerId})`.mapWith(Number),
3390
+ observations: sql5`count(*)`.mapWith(Number)
3391
+ }).from(failed).innerJoin(
3392
+ worked,
3393
+ sql5`${worked.ownerId} = ${failed.ownerId}
3394
+ AND ${worked.need} = ${failed.need}
3395
+ AND ${worked.packageName} <> ${failed.packageName}
3396
+ AND ${worked.createdAt} > ${failed.createdAt}`
3397
+ ).where(
3398
+ sql5`${failed.need} IS NOT NULL
3399
+ AND ${failed.ownerId} IS NOT NULL
3400
+ AND NOT (${failed.accepted} AND (${failed.buildSignal} IS NULL OR ${failed.buildSignal} <> 'failed'))
3401
+ AND (${worked.accepted} AND (${worked.buildSignal} IS NULL OR ${worked.buildSignal} <> 'failed'))`
3402
+ ).groupBy(failed.packageName, worked.packageName).having(sql5`count(DISTINCT ${failed.ownerId}) >= ${MIN_SUCCESSION_OWNERS}`);
3403
+ return rows.map((r) => ({
3404
+ from: r.from,
3405
+ to: r.to,
3406
+ owners: Number(r.owners),
3407
+ observations: Number(r.observations)
3408
+ }));
3409
+ }
3410
+ async function loadLearnedSuccessors(db, now = Date.now()) {
3411
+ if (learnedCache && now - learnedCache.at < LEARNED_TTL_MS) return learnedCache.value;
3412
+ const observed = await learnSuccessions(db);
3413
+ if (observed.length === 0) {
3414
+ learnedCache = { at: now, value: /* @__PURE__ */ new Map() };
3415
+ return learnedCache.value;
3416
+ }
3417
+ const names2 = [...new Set(observed.map((o) => o.from))];
3418
+ const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(inArray3(packages.name, names2));
3419
+ const dead = new Set(rows.filter((r) => r.deprecated || r.archived).map((r) => r.name));
3420
+ const value = buildLearnedSuccessors(observed, (name) => dead.has(name));
3421
+ learnedCache = { at: now, value };
3422
+ return value;
3423
+ }
3424
+ async function getOutcomesByOwner(db, ownerId, opts = {}) {
3425
+ return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
3426
+ }
3427
+ var SUCCESS_PREDICATE, MIN_SUCCESSION_OWNERS, LEARNED_TTL_MS, learnedCache;
3428
+ var init_outcomes = __esm({
3429
+ "src/db/outcomes.ts"() {
3430
+ "use strict";
3431
+ init_esm_shims();
3432
+ init_successors2();
3433
+ init_schema();
3434
+ SUCCESS_PREDICATE = sql5`${recommendationOutcomes.accepted} AND (${recommendationOutcomes.buildSignal} IS NULL OR ${recommendationOutcomes.buildSignal} <> 'failed')`;
3435
+ MIN_SUCCESSION_OWNERS = 3;
3436
+ LEARNED_TTL_MS = 10 * 60 * 1e3;
3437
+ learnedCache = null;
3438
+ }
3439
+ });
3440
+
3334
3441
  // src/ingestion/sources/githubReadme.ts
3335
3442
  async function fetchGithubReadme(owner, repo, fetchImpl) {
3336
3443
  for (const file of CANDIDATES) {
@@ -3727,7 +3834,7 @@ function resetWeights() {
3727
3834
  resetWeightsCache();
3728
3835
  return removed;
3729
3836
  }
3730
- var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3837
+ var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3731
3838
  var init_weights = __esm({
3732
3839
  "src/scoring/weights.ts"() {
3733
3840
  "use strict";
@@ -3806,6 +3913,26 @@ var init_weights = __esm({
3806
3913
  /** Bundle size at the category median maps to this score; smaller → higher. */
3807
3914
  medianScore: 50
3808
3915
  };
3916
+ FIELD = {
3917
+ /** Weight when evidence exists — ~9% of the blend (0.10 / 1.10). */
3918
+ weight: 0.1,
3919
+ /**
3920
+ * Bayesian shrinkage strength: how many prior observations the neutral rate is
3921
+ * worth. One failed report out of one is not a 0-scoring package, and a raw
3922
+ * success rate would say it is. At m=12, a single failure lands near 69 rather
3923
+ * than 0, and it takes real volume to move the number far — which is the
3924
+ * correct epistemics for a signal users can trivially poison.
3925
+ */
3926
+ priorWeight: 12,
3927
+ /**
3928
+ * The prior success rate a package is assumed to have before anyone reports.
3929
+ * Set at the observed base rate of "installs and builds fine", not 0.5:
3930
+ * most packages do work, so 0.5 would penalise the first honest reporter.
3931
+ */
3932
+ priorRate: 0.75,
3933
+ /** Reports at or above this cap the confidence note as fully-evidenced. */
3934
+ wellEvidencedReports: 25
3935
+ };
3809
3936
  DEFAULT_WEIGHTS = {
3810
3937
  health: { ...HEALTH_WEIGHTS },
3811
3938
  composite: { ...COMPOSITE }
@@ -3973,17 +4100,24 @@ function computeQuality(input) {
3973
4100
  if (components.length === 0) return null;
3974
4101
  return weightedAverage(components);
3975
4102
  }
4103
+ function computeFieldScore(evidence) {
4104
+ if (!evidence || evidence.reports <= 0) return null;
4105
+ const { reports, successes } = evidence;
4106
+ const m = FIELD.priorWeight;
4107
+ return clamp(Math.round(100 * (successes + m * FIELD.priorRate) / (reports + m)));
4108
+ }
3976
4109
  function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
3977
- const { maintenance, adoption, reliability, efficiency } = breakdown;
3978
4110
  const w = weights;
3979
- if (efficiency === null) {
3980
- const denom = w.maintenance + w.adoption + w.reliability;
3981
- return Math.round(
3982
- (w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
3983
- );
3984
- }
3985
- return Math.round(
3986
- w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
4111
+ return weightedAverage(
4112
+ [
4113
+ { value: breakdown.maintenance, weight: w.maintenance },
4114
+ { value: breakdown.adoption, weight: w.adoption },
4115
+ { value: breakdown.reliability, weight: w.reliability },
4116
+ { value: breakdown.efficiency, weight: w.efficiency },
4117
+ // `?? null` — a breakdown stored before field evidence shipped has the key
4118
+ // absent, and undefined must redistribute, not score 0.
4119
+ { value: breakdown.field ?? null, weight: FIELD.weight }
4120
+ ].filter((c) => c.value !== null)
3987
4121
  );
3988
4122
  }
3989
4123
  function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
@@ -4228,7 +4362,7 @@ var init_types2 = __esm({
4228
4362
  });
4229
4363
 
4230
4364
  // src/db/repos.ts
4231
- import { and as and5, eq as eq7, sql as sql5 } from "drizzle-orm";
4365
+ import { and as and5, eq as eq7, sql as sql6 } from "drizzle-orm";
4232
4366
  async function upsertRepos(db, rows) {
4233
4367
  if (rows.length === 0) return 0;
4234
4368
  await db.insert(repos).values(
@@ -4243,9 +4377,9 @@ async function upsertRepos(db, rows) {
4243
4377
  ).onConflictDoUpdate({
4244
4378
  target: [repos.ownerId, repos.fullName],
4245
4379
  set: {
4246
- installationId: sql5`excluded.installation_id`,
4247
- defaultBranch: sql5`excluded.default_branch`,
4248
- isPrivate: sql5`excluded.is_private`
4380
+ installationId: sql6`excluded.installation_id`,
4381
+ defaultBranch: sql6`excluded.default_branch`,
4382
+ isPrivate: sql6`excluded.is_private`
4249
4383
  }
4250
4384
  });
4251
4385
  return rows.length;
@@ -4255,7 +4389,7 @@ async function listRepos(db, ownerId) {
4255
4389
  }
4256
4390
  async function reposDeclaring(db, name) {
4257
4391
  return db.select().from(repos).where(
4258
- sql5`exists (
4392
+ sql6`exists (
4259
4393
  select 1 from jsonb_array_elements(${repos.manifests}) as m
4260
4394
  where jsonb_exists(m -> 'deps', ${name})
4261
4395
  )`
@@ -4265,6 +4399,10 @@ async function getRepo(db, ownerId, id) {
4265
4399
  const rows = await db.select().from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).limit(1);
4266
4400
  return rows[0] ?? null;
4267
4401
  }
4402
+ async function findPolicyByFullName(db, ownerId, fullName) {
4403
+ const rows = await db.select({ policy: repos.policy }).from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.fullName, fullName))).limit(1);
4404
+ return rows[0]?.policy ?? null;
4405
+ }
4268
4406
  async function saveScan(db, id, scan) {
4269
4407
  await db.update(repos).set({
4270
4408
  manifests: scan.manifests,
@@ -4350,8 +4488,76 @@ var init_attribute = __esm({
4350
4488
  }
4351
4489
  });
4352
4490
 
4491
+ // src/ingestion/sources/osv.ts
4492
+ function installKey(name, version) {
4493
+ return `${name}@${version}`;
4494
+ }
4495
+ async function queryVulnerableInstalls(installs, fetchImpl) {
4496
+ const affected = /* @__PURE__ */ new Map();
4497
+ if (installs.length === 0) return { affected, complete: true };
4498
+ const unique = [...new Map(installs.map((i) => [installKey(i.name, i.version), i])).values()];
4499
+ const batches = [];
4500
+ for (let i = 0; i < unique.length; i += BATCH) batches.push(unique.slice(i, i + BATCH));
4501
+ let complete = true;
4502
+ let cursor = 0;
4503
+ const worker = async () => {
4504
+ for (let i = cursor++; i < batches.length; i = cursor++) {
4505
+ const batch = batches[i];
4506
+ try {
4507
+ const { data } = await httpRequest(URL2, {
4508
+ host: HOST7,
4509
+ method: "POST",
4510
+ headers: { "content-type": "application/json" },
4511
+ body: JSON.stringify({
4512
+ queries: batch.map((q) => ({
4513
+ version: q.version,
4514
+ package: { name: q.name, ecosystem: "npm" }
4515
+ }))
4516
+ }),
4517
+ // Uncached on purpose. The shared cache keys on the URL, and every
4518
+ // batch here posts to the same one with a different body — a cache hit
4519
+ // would answer one tree's question with another tree's vulnerabilities.
4520
+ ttlMs: 0,
4521
+ // Best-effort by contract: the caller keeps its coarser answer when
4522
+ // this fails, so exhausting the default retry ladder would only stall
4523
+ // a scheduled scan behind an outage nobody is waiting on.
4524
+ retries: 1,
4525
+ fetchImpl
4526
+ });
4527
+ const results = data?.results ?? [];
4528
+ if (results.length !== batch.length) {
4529
+ complete = false;
4530
+ continue;
4531
+ }
4532
+ results.forEach((result, j) => {
4533
+ const ids = (result?.vulns ?? []).map((v) => v?.id).filter((id) => typeof id === "string");
4534
+ if (ids.length === 0) return;
4535
+ const q = batch[j];
4536
+ affected.set(installKey(q.name, q.version), ids);
4537
+ });
4538
+ } catch {
4539
+ complete = false;
4540
+ }
4541
+ }
4542
+ };
4543
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, batches.length) }, worker));
4544
+ return { affected, complete };
4545
+ }
4546
+ var HOST7, URL2, BATCH, CONCURRENCY;
4547
+ var init_osv = __esm({
4548
+ "src/ingestion/sources/osv.ts"() {
4549
+ "use strict";
4550
+ init_esm_shims();
4551
+ init_http();
4552
+ HOST7 = "api.osv.dev";
4553
+ URL2 = `https://${HOST7}/v1/querybatch`;
4554
+ BATCH = 250;
4555
+ CONCURRENCY = 4;
4556
+ }
4557
+ });
4558
+
4353
4559
  // src/github/drift.ts
4354
- import { inArray as inArray3 } from "drizzle-orm";
4560
+ import { inArray as inArray4, sql as sql7 } from "drizzle-orm";
4355
4561
  import semver2 from "semver";
4356
4562
  function declaredDeps(manifests) {
4357
4563
  const out = /* @__PURE__ */ new Map();
@@ -4378,7 +4584,7 @@ async function loadIndexed(db, names2) {
4378
4584
  latestVersion: packages.latestVersion,
4379
4585
  deprecated: packages.deprecated,
4380
4586
  advisories: packages.advisories
4381
- }).from(packages).where(inArray3(packages.name, names2.slice(i, i + NAME_CHUNK)));
4587
+ }).from(packages).where(inArray4(packages.name, names2.slice(i, i + NAME_CHUNK)));
4382
4588
  for (const row of rows) {
4383
4589
  out.set(row.name, {
4384
4590
  latestVersion: row.latestVersion,
@@ -4392,7 +4598,7 @@ async function loadIndexed(db, names2) {
4392
4598
  async function loadVersions(db, names2) {
4393
4599
  const out = /* @__PURE__ */ new Map();
4394
4600
  for (let i = 0; i < names2.length; i += NAME_CHUNK) {
4395
- const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray3(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
4601
+ const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray4(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
4396
4602
  for (const row of rows) {
4397
4603
  const list = out.get(row.name);
4398
4604
  if (list) list.push(row.version);
@@ -4442,19 +4648,28 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4442
4648
  };
4443
4649
  if (transitives.length === 0) return empty2;
4444
4650
  const indexed = await loadIndexed(db, [...new Set(transitives.map((d) => d.name))]);
4651
+ const { affected, complete } = await queryVulnerableInstalls(
4652
+ transitives.map((d) => ({ name: d.name, version: d.version }))
4653
+ );
4445
4654
  const risks = [];
4446
4655
  let tracked = 0;
4447
4656
  for (const dep of transitives) {
4448
4657
  const row = indexed.get(dep.name);
4449
4658
  if (!row) continue;
4450
4659
  tracked++;
4451
- if (row.advisories === 0 && !row.deprecated) continue;
4660
+ const vulnIds = affected.get(installKey(dep.name, dep.version)) ?? [];
4661
+ if (row.advisories === 0 && !row.deprecated && vulnIds.length === 0) continue;
4452
4662
  risks.push({
4453
4663
  name: dep.name,
4454
4664
  version: dep.version,
4455
4665
  latest: row.latestVersion,
4456
4666
  advisories: row.advisories,
4457
4667
  deprecated: row.deprecated,
4668
+ // Empty when OSV cleared this version, which is the actionable difference
4669
+ // from `advisories`: the package has a history, this install does not.
4670
+ // Undefined when the lookup could not complete — never an empty array,
4671
+ // which would read as a clean bill of health nobody established.
4672
+ vulnerabilities: complete ? vulnIds : void 0,
4458
4673
  pulledInBy: attribution.parentsOf.get(dep.name) ?? []
4459
4674
  });
4460
4675
  }
@@ -4465,6 +4680,10 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4465
4680
  resolved: transitives.length,
4466
4681
  tracked,
4467
4682
  advisoryPackages: risks.filter((r) => r.advisories > 0).length,
4683
+ // The number that can actually be acted on: installs OSV matched to a
4684
+ // vulnerability at their exact version. Undefined when the lookup did not
4685
+ // complete, so "not checked" never renders as zero.
4686
+ vulnerableInstalls: complete ? risks.filter((r) => r.vulnerabilities?.length).length : void 0,
4468
4687
  deprecated: risks.filter((r) => r.deprecated).length,
4469
4688
  risks: risks.slice(0, TRANSITIVE_DETAIL_CAP),
4470
4689
  truncated,
@@ -4476,6 +4695,29 @@ async function conflictsAtLatest(db, names2) {
4476
4695
  const { members } = await assembleMembers(db, names2);
4477
4696
  return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4478
4697
  }
4698
+ async function conflictsAtCurrent(db, deps) {
4699
+ const pinned = deps.filter((d) => Boolean(d.resolved));
4700
+ if (pinned.length === 0) return [];
4701
+ const rows = await db.select({
4702
+ packageName: packageVersions.packageName,
4703
+ version: packageVersions.version,
4704
+ peerDependencies: packageVersions.peerDependencies,
4705
+ engines: packageVersions.engines
4706
+ }).from(packageVersions).where(
4707
+ inArray4(
4708
+ sql7`(${packageVersions.packageName}, ${packageVersions.version})`,
4709
+ pinned.map((d) => sql7`(${d.name}, ${d.resolved})`)
4710
+ )
4711
+ );
4712
+ const members = rows.map((r) => ({
4713
+ name: r.packageName,
4714
+ version: r.version,
4715
+ peerDependencies: r.peerDependencies,
4716
+ peerDependenciesMeta: null,
4717
+ engines: r.engines
4718
+ }));
4719
+ return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4720
+ }
4479
4721
  async function computeDrift(db, manifests, resolvedTree = null) {
4480
4722
  const declared = declaredDeps(manifests);
4481
4723
  const names2 = [...declared.keys()];
@@ -4497,7 +4739,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4497
4739
  advisories: 0,
4498
4740
  deps: [],
4499
4741
  transitive,
4500
- conflictsAtLatest: []
4742
+ conflictsAtLatest: [],
4743
+ conflictsAtCurrent: []
4501
4744
  };
4502
4745
  }
4503
4746
  const [indexed, versions] = await Promise.all([
@@ -4511,7 +4754,10 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4511
4754
  deps.push(depDrift(name, entry, row, versions.get(name) ?? []));
4512
4755
  }
4513
4756
  deps.sort((a, b) => severity(b) - severity(a) || a.name.localeCompare(b.name));
4514
- const conflicts = await conflictsAtLatest(db, deps.map((d) => d.name));
4757
+ const [conflicts, currentConflicts] = await Promise.all([
4758
+ conflictsAtLatest(db, deps.map((d) => d.name)),
4759
+ conflictsAtCurrent(db, deps)
4760
+ ]);
4515
4761
  return {
4516
4762
  depsDeclared: declared.size,
4517
4763
  depsTracked: deps.length,
@@ -4521,7 +4767,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4521
4767
  advisories: deps.reduce((sum, d) => sum + d.advisories, 0),
4522
4768
  deps: deps.slice(0, REPO_DRIFT_DETAIL_CAP),
4523
4769
  transitive,
4524
- conflictsAtLatest: conflicts
4770
+ conflictsAtLatest: conflicts,
4771
+ conflictsAtCurrent: currentConflicts
4525
4772
  };
4526
4773
  }
4527
4774
  var NAME_CHUNK;
@@ -4532,6 +4779,7 @@ var init_drift = __esm({
4532
4779
  init_schema();
4533
4780
  init_types2();
4534
4781
  init_attribute();
4782
+ init_osv();
4535
4783
  init_members();
4536
4784
  init_peerCompat();
4537
4785
  NAME_CHUNK = 500;
@@ -4632,6 +4880,82 @@ var init_errors = __esm({
4632
4880
  }
4633
4881
  });
4634
4882
 
4883
+ // src/core/runtimeTarget.ts
4884
+ function classifyRuntimeTarget(input) {
4885
+ const override = OVERRIDES[input.name.toLowerCase()];
4886
+ if (override) return override;
4887
+ const peers = Object.keys(input.peerDependencies ?? {});
4888
+ if (peers.some((p) => RENDERER_PEERS.has(p))) return "browser";
4889
+ const keywords = input.keywords.map((k) => k.toLowerCase());
4890
+ const server = keywords.filter((k) => SERVER_KEYWORDS.has(k)).length;
4891
+ const browser = keywords.filter((k) => BROWSER_KEYWORDS.has(k)).length;
4892
+ if (browser > server) return "browser";
4893
+ if (server > browser) return "node";
4894
+ if (input.hasBrowserField) return "browser";
4895
+ if (input.engines?.node && !input.hasBrowserField && browser === 0) return "node";
4896
+ return null;
4897
+ }
4898
+ var SERVER_KEYWORDS, BROWSER_KEYWORDS, RENDERER_PEERS, OVERRIDES;
4899
+ var init_runtimeTarget = __esm({
4900
+ "src/core/runtimeTarget.ts"() {
4901
+ "use strict";
4902
+ init_esm_shims();
4903
+ SERVER_KEYWORDS = /* @__PURE__ */ new Set([
4904
+ "server",
4905
+ "http-server",
4906
+ "backend",
4907
+ "middleware",
4908
+ "rest",
4909
+ "restful",
4910
+ "api",
4911
+ "router",
4912
+ "routing",
4913
+ "orm",
4914
+ "database",
4915
+ "sql",
4916
+ "graphql-server",
4917
+ "microservice",
4918
+ "websocket",
4919
+ "rpc"
4920
+ ]);
4921
+ BROWSER_KEYWORDS = /* @__PURE__ */ new Set([
4922
+ "react",
4923
+ "vue",
4924
+ "svelte",
4925
+ "angular",
4926
+ "preact",
4927
+ "solid",
4928
+ "dom",
4929
+ "browser",
4930
+ "component",
4931
+ "components",
4932
+ "ui",
4933
+ "css",
4934
+ "styling",
4935
+ "animation",
4936
+ "frontend",
4937
+ "client-side",
4938
+ "jsx",
4939
+ "hooks"
4940
+ ]);
4941
+ RENDERER_PEERS = /* @__PURE__ */ new Set(["react", "react-dom", "vue", "svelte", "@angular/core", "preact"]);
4942
+ OVERRIDES = {
4943
+ express: "node",
4944
+ fastify: "node",
4945
+ koa: "node",
4946
+ hono: "node",
4947
+ "@nestjs/core": "node",
4948
+ "@hapi/hapi": "node",
4949
+ // Meta-frameworks run in both places; they are the server in an architecture
4950
+ // diagram, which is the question this answers.
4951
+ next: "node",
4952
+ nuxt: "node",
4953
+ "@remix-run/node": "node",
4954
+ astro: "node"
4955
+ };
4956
+ }
4957
+ });
4958
+
4635
4959
  // src/pipeline/mineEdges.ts
4636
4960
  function trackedPairs(nodes, tracked) {
4637
4961
  const t = nodes.filter((n) => tracked.has(n.name));
@@ -4712,6 +5036,13 @@ function assemblePackageRow(p) {
4712
5036
  peerDependencies: r?.peerDependencies ?? null,
4713
5037
  peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
4714
5038
  engines: r?.engines ?? null,
5039
+ runtimeTarget: r ? classifyRuntimeTarget({
5040
+ name: p.name,
5041
+ hasBrowserField: r.hasBrowserField,
5042
+ keywords: r.keywords,
5043
+ engines: r.engines,
5044
+ peerDependencies: r.peerDependencies
5045
+ }) : null,
4715
5046
  healthScore: p.healthScore,
4716
5047
  qualityScore: p.qualityScore,
4717
5048
  confidence: p.confidence,
@@ -4732,6 +5063,7 @@ var init_sync = __esm({
4732
5063
  init_errors();
4733
5064
  init_http();
4734
5065
  init_logger();
5066
+ init_runtimeTarget();
4735
5067
  init_collect();
4736
5068
  init_npmDownloads();
4737
5069
  init_summarize();
@@ -4805,7 +5137,7 @@ var init_ingestQueue = __esm({
4805
5137
  });
4806
5138
 
4807
5139
  // src/pipeline/single.ts
4808
- import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql6 } from "drizzle-orm";
5140
+ import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
4809
5141
  function raceTimeout(p, ms) {
4810
5142
  let timer;
4811
5143
  return Promise.race([
@@ -4821,7 +5153,7 @@ async function getSeedCategory(db, name) {
4821
5153
  }
4822
5154
  async function getCategoryMedianBundle(db, category) {
4823
5155
  const [row] = await db.select({
4824
- m: sql6`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
5156
+ m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
4825
5157
  }).from(packages).where(and6(eq8(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
4826
5158
  return row?.m ?? null;
4827
5159
  }
@@ -4853,12 +5185,14 @@ async function syncOnePackage(db, name, opts = {}) {
4853
5185
  const input = toScoringInput(signals, category);
4854
5186
  const median2 = category ? await getCategoryMedianBundle(db, category) : null;
4855
5187
  const quality = computeQuality(input);
5188
+ const evidence = await getFieldEvidence(db, [name]);
4856
5189
  const breakdown = {
4857
5190
  maintenance: computeMaintenance(input, now),
4858
5191
  adoption: computeAdoption(input),
4859
5192
  reliability: computeReliability(input),
4860
5193
  efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
4861
- quality
5194
+ quality,
5195
+ field: computeFieldScore(evidence.get(name) ?? null)
4862
5196
  };
4863
5197
  const healthScore = computeHealthScore(breakdown);
4864
5198
  const confidence = computeConfidence(input, now, quality);
@@ -4928,6 +5262,7 @@ var init_single = __esm({
4928
5262
  init_categoryInference();
4929
5263
  init_scoring();
4930
5264
  init_embeddings();
5265
+ init_outcomes();
4931
5266
  init_packages();
4932
5267
  init_schema();
4933
5268
  init_alerts2();
@@ -4939,7 +5274,7 @@ var init_single = __esm({
4939
5274
  });
4940
5275
 
4941
5276
  // src/search/recommend.ts
4942
- import { and as and7, cosineDistance, eq as eq9, inArray as inArray4, isNotNull as isNotNull4, lte, sql as sql7 } from "drizzle-orm";
5277
+ import { and as and7, cosineDistance, eq as eq9, inArray as inArray5, isNotNull as isNotNull4, lte, sql as sql9 } from "drizzle-orm";
4943
5278
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
4944
5279
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
4945
5280
  const [queryVec] = await provider.embed([opts.need]);
@@ -4992,7 +5327,7 @@ function buildConditions(constraints, category) {
4992
5327
  const allowed = ["proven", "emerging", "promising", "unproven"].filter(
4993
5328
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
4994
5329
  );
4995
- conditions.push(inArray4(packages.confidence, allowed));
5330
+ conditions.push(inArray5(packages.confidence, allowed));
4996
5331
  }
4997
5332
  return conditions;
4998
5333
  }
@@ -5006,10 +5341,10 @@ async function runVectorQuery(db, queryVec, providerId, constraints, category, p
5006
5341
  return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(distance).limit(pool);
5007
5342
  }
5008
5343
  async function runLexicalQuery(db, need, constraints, category, pool) {
5009
- const tsquery = sql7`websearch_to_tsquery('english', ${need})`;
5010
- const rank2 = sql7`ts_rank(${packages.searchVector}, ${tsquery})`;
5011
- const conditions = [sql7`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
5012
- return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql7`${rank2} desc`).limit(pool);
5344
+ const tsquery = sql9`websearch_to_tsquery('english', ${need})`;
5345
+ const rank2 = sql9`ts_rank(${packages.searchVector}, ${tsquery})`;
5346
+ const conditions = [sql9`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
5347
+ return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql9`${rank2} desc`).limit(pool);
5013
5348
  }
5014
5349
  function toCandidate(row) {
5015
5350
  return {
@@ -5147,7 +5482,7 @@ var init_types3 = __esm({
5147
5482
  });
5148
5483
 
5149
5484
  // src/db/selectionPolicy.ts
5150
- import { eq as eq10, inArray as inArray5 } from "drizzle-orm";
5485
+ import { eq as eq10, inArray as inArray6 } from "drizzle-orm";
5151
5486
  async function getSelectionPolicy(db, ownerId) {
5152
5487
  if (!ownerId) return DEFAULT_SELECTION_POLICY;
5153
5488
  const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(eq10(selectionPolicies.ownerId, ownerId)).limit(1);
@@ -5166,7 +5501,7 @@ async function loadPolicyFacts(db, names2) {
5166
5501
  name: packages.name,
5167
5502
  license: packages.license,
5168
5503
  deprecated: packages.deprecated
5169
- }).from(packages).where(inArray5(packages.name, names2));
5504
+ }).from(packages).where(inArray6(packages.name, names2));
5170
5505
  for (const row of rows) {
5171
5506
  out.set(row.name, { license: row.license, deprecated: row.deprecated });
5172
5507
  }
@@ -5483,7 +5818,7 @@ __export(handlers_exports, {
5483
5818
  rowToEvaluate: () => rowToEvaluate
5484
5819
  });
5485
5820
  import { createHash as createHash3 } from "crypto";
5486
- import { sql as sql8 } from "drizzle-orm";
5821
+ import { sql as sql10 } from "drizzle-orm";
5487
5822
  function isStale(dataAsOf) {
5488
5823
  if (!dataAsOf) return true;
5489
5824
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
@@ -5499,7 +5834,7 @@ function topAdvisories(advisories, max = 5) {
5499
5834
  if (!advisories?.length) return [];
5500
5835
  return [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).slice(0, max);
5501
5836
  }
5502
- function rowToEvaluate(row) {
5837
+ function rowToEvaluate(row, learned) {
5503
5838
  const breakdown = row.scoreBreakdown ?? {
5504
5839
  maintenance: 0,
5505
5840
  adoption: 0,
@@ -5528,11 +5863,11 @@ function rowToEvaluate(row) {
5528
5863
  summary: row.summary ? truncateSentences(row.summary, 3) : null,
5529
5864
  usageGuide: row.usageGuide ?? null,
5530
5865
  repoUrl: row.repoUrl,
5531
- replacedBy: lookupSuccessor(row.name)
5866
+ replacedBy: lookupSuccessor(row.name, learned)
5532
5867
  };
5533
5868
  }
5534
5869
  async function latestDataAsOf(db) {
5535
- const [row] = await db.select({ m: sql8`max(${packages.dataAsOf})` }).from(packages);
5870
+ const [row] = await db.select({ m: sql10`max(${packages.dataAsOf})` }).from(packages);
5536
5871
  return new Date(row?.m ?? Date.now()).toISOString();
5537
5872
  }
5538
5873
  function cacheKey(parts) {
@@ -5575,7 +5910,8 @@ async function handleEvaluate(db, input, ownerId = null) {
5575
5910
  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.`
5576
5911
  };
5577
5912
  }
5578
- const evaluated2 = rowToEvaluate(row);
5913
+ const learned = row.deprecated || row.archived ? await loadLearnedSuccessors(db) : void 0;
5914
+ const evaluated2 = rowToEvaluate(row, learned);
5579
5915
  const verification = await getLatestVerificationByName(db, row.name);
5580
5916
  return verification ? { ...evaluated2, buildVerified: toBuildVerified(verification) } : evaluated2;
5581
5917
  },
@@ -5602,7 +5938,7 @@ async function handleCompare(db, input, ownerId = null) {
5602
5938
  const results = await Promise.all(
5603
5939
  input.packages.map((name) => getOrFetchPackage(db, name, { requestedByOwnerId: ownerId }))
5604
5940
  );
5605
- const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
5941
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map((row) => rowToEvaluate(row)).sort((a, b) => b.healthScore - a.healthScore);
5606
5942
  const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
5607
5943
  return {
5608
5944
  dataAsOf: await latestDataAsOf(db),
@@ -5887,7 +6223,8 @@ function parseRpcBody(raw) {
5887
6223
  }
5888
6224
  }
5889
6225
  function fetchUpgradePlan(deps, opts = {}) {
5890
- return post("/upgrade-plan", { deps }, opts);
6226
+ const body = opts.repo ? { deps, repo: opts.repo } : { deps };
6227
+ return post("/upgrade-plan", body, opts);
5891
6228
  }
5892
6229
  var RemoteError;
5893
6230
  var init_remote = __esm({
@@ -5982,10 +6319,10 @@ var init_optimize = __esm({
5982
6319
  });
5983
6320
 
5984
6321
  // src/mcp/diagram.ts
5985
- import { inArray as inArray6 } from "drizzle-orm";
6322
+ import { inArray as inArray7 } from "drizzle-orm";
5986
6323
  function layerFor(item) {
5987
6324
  if (!item.category) return UNCLASSIFIED;
5988
- if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
6325
+ if (item.category === "framework" && item.runtimeTarget === "node") return "Backend";
5989
6326
  return LAYER_OF[item.category] ?? UNCLASSIFIED;
5990
6327
  }
5991
6328
  function nodeId(label, i) {
@@ -6035,22 +6372,41 @@ async function handleDiagram(db, input) {
6035
6372
  note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
6036
6373
  };
6037
6374
  }
6038
- const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray6(packages.name, input.stack));
6039
- const known = new Map(rows.map((r) => [r.name, r.category]));
6040
- const items = input.stack.map((name) => ({
6041
- label: name,
6042
- category: known.get(name) ?? inferCategory(name)
6043
- }));
6375
+ const rows = await db.select({
6376
+ name: packages.name,
6377
+ category: packages.category,
6378
+ runtimeTarget: packages.runtimeTarget
6379
+ }).from(packages).where(inArray7(packages.name, input.stack));
6380
+ const known = new Map(rows.map((r) => [r.name, r]));
6381
+ const items = input.stack.map((name) => {
6382
+ const row = known.get(name);
6383
+ return {
6384
+ label: name,
6385
+ category: row?.category ?? inferCategory(name),
6386
+ // Fall back to the name-only classifier for a package that predates the
6387
+ // column or is not indexed at all: the curated overrides still resolve the
6388
+ // frameworks people actually diagram, and an unclassifiable one keeps the
6389
+ // old Presentation default rather than moving on a null.
6390
+ runtimeTarget: row?.runtimeTarget ?? classifyRuntimeTarget({
6391
+ name,
6392
+ hasBrowserField: false,
6393
+ keywords: [],
6394
+ engines: null,
6395
+ peerDependencies: null
6396
+ })
6397
+ };
6398
+ });
6044
6399
  const unclassified = items.filter((i) => layerFor(i) === UNCLASSIFIED).map((i) => i.label);
6045
6400
  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.";
6046
6401
  return { mermaid: buildMermaid(items), note };
6047
6402
  }
6048
- var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS, BACKEND_FRAMEWORKS;
6403
+ var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS;
6049
6404
  var init_diagram = __esm({
6050
6405
  "src/mcp/diagram.ts"() {
6051
6406
  "use strict";
6052
6407
  init_esm_shims();
6053
6408
  init_categoryInference();
6409
+ init_runtimeTarget();
6054
6410
  init_schema();
6055
6411
  UNCLASSIFIED = "Unclassified";
6056
6412
  LAYER_OF = {
@@ -6078,14 +6434,6 @@ var init_diagram = __esm({
6078
6434
  testing: "Tooling"
6079
6435
  };
6080
6436
  FLOW_LAYERS = ["Presentation", "Client Logic", "Backend", "Data"];
6081
- BACKEND_FRAMEWORKS = /* @__PURE__ */ new Set([
6082
- "express",
6083
- "fastify",
6084
- "koa",
6085
- "hono",
6086
- "@nestjs/core",
6087
- "@hapi/hapi"
6088
- ]);
6089
6437
  }
6090
6438
  });
6091
6439
 
@@ -6100,7 +6448,7 @@ __export(plan_exports, {
6100
6448
  resolvePins: () => resolvePins
6101
6449
  });
6102
6450
  import { createHash as createHash4 } from "crypto";
6103
- import { inArray as inArray7 } from "drizzle-orm";
6451
+ import { inArray as inArray8 } from "drizzle-orm";
6104
6452
  function packageToCandidate(row) {
6105
6453
  return {
6106
6454
  name: row.name,
@@ -6321,7 +6669,7 @@ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
6321
6669
  async function bundleSizes(db, candidates) {
6322
6670
  const names2 = [...new Set(candidates.map((c) => c.name))];
6323
6671
  if (names2.length === 0) return /* @__PURE__ */ new Map();
6324
- const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray7(packages.name, names2));
6672
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray8(packages.name, names2));
6325
6673
  return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
6326
6674
  }
6327
6675
  async function decompose(document) {
@@ -7875,6 +8223,10 @@ var init_app = __esm({
7875
8223
  });
7876
8224
 
7877
8225
  // src/github/workflow.ts
8226
+ function cliSpec(version = VERSION) {
8227
+ const [major = "0", minor = "0"] = version.split(".");
8228
+ return major === "0" ? `${PACKAGE_NAME}@${major}.${minor}` : `${PACKAGE_NAME}@${major}`;
8229
+ }
7878
8230
  function detectInstallCommand(lockfiles) {
7879
8231
  if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm install --frozen-lockfile";
7880
8232
  if (lockfiles.includes("yarn.lock")) return "yarn install --frozen-lockfile";
@@ -7887,6 +8239,8 @@ function renderWorkflow(opts = {}) {
7887
8239
  const install = opts.installCommand ?? "npm ci";
7888
8240
  const max = opts.maxUpgrades ?? 3;
7889
8241
  const mode = opts.armed ? "pr" : "comment";
8242
+ const autoMerge = opts.autoMerge ?? false;
8243
+ const cli = cliSpec();
7890
8244
  return `# Managed by lurq, https://lurq.run
7891
8245
  #
7892
8246
  # Keeps this repository's dependencies current and rewrites the call sites an
@@ -7934,19 +8288,19 @@ jobs:
7934
8288
  - name: Plan
7935
8289
  env:
7936
8290
  LURQ_API_KEY: \${{ secrets.LURQ_API_KEY }}
7937
- run: npx -y lurqrun upgrade-plan . --json > lurq-plan.json
8291
+ run: npx -y ${cli} upgrade-plan . --json > lurq-plan.json
7938
8292
 
7939
8293
  # 2. Narrow that to symbols THIS repo references. Runs entirely locally
7940
8294
  # against both versions' npm tarballs, no API key, no test suite.
7941
8295
  - name: Check against this codebase
7942
- run: npx -y lurqrun check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
8296
+ run: npx -y ${cli} check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
7943
8297
 
7944
8298
  # The same report twice: once into the run summary, once as the body of
7945
8299
  # the pull request. Fenced, because the report is aligned plain text and
7946
8300
  # markdown would otherwise collapse its indentation into one paragraph.
7947
8301
  - name: Summarise
7948
8302
  run: |
7949
- npx -y lurqrun check-upgrade . --plan lurq-plan.json > lurq-report.txt
8303
+ npx -y ${cli} check-upgrade . --plan lurq-plan.json > lurq-report.txt
7950
8304
  { echo '\`\`\`'; cat lurq-report.txt; echo '\`\`\`'; } > lurq-report.md
7951
8305
  cat lurq-report.md >> "\${GITHUB_STEP_SUMMARY}"
7952
8306
 
@@ -8022,6 +8376,7 @@ jobs:
8022
8376
 
8023
8377
  # 4. The workflow does version control, never the model.
8024
8378
  - name: Open pull request
8379
+ id: pr
8025
8380
  if: env.LURQ_MODE == 'pr'
8026
8381
  uses: peter-evans/create-pull-request@v7
8027
8382
  with:
@@ -8034,19 +8389,30 @@ jobs:
8034
8389
  body-path: lurq-report.md
8035
8390
  labels: dependencies
8036
8391
  delete-branch: true
8037
- `;
8392
+ ${autoMerge ? MERGE_STEP : ""}`;
8038
8393
  }
8039
8394
  function newFileUrl(fullName, branch, content) {
8040
8395
  const params = new URLSearchParams({ filename: WORKFLOW_PATH, value: content });
8041
8396
  return `https://github.com/${fullName}/new/${encodeURIComponent(branch)}?${params.toString()}`;
8042
8397
  }
8043
- var WORKFLOW_PATH, DEFAULT_CRON;
8398
+ var WORKFLOW_PATH, DEFAULT_CRON, MERGE_STEP;
8044
8399
  var init_workflow = __esm({
8045
8400
  "src/github/workflow.ts"() {
8046
8401
  "use strict";
8047
8402
  init_esm_shims();
8403
+ init_constants();
8048
8404
  WORKFLOW_PATH = ".github/workflows/lurq-upgrade.yml";
8049
8405
  DEFAULT_CRON = "0 6 * * 1";
8406
+ MERGE_STEP = `
8407
+ # 5. Auto-merge, per this repository's lurq policy. GitHub merges only
8408
+ # once the repo's OWN required checks pass; lurq never makes that call.
8409
+ # Requires "Allow auto-merge" in repository settings.
8410
+ - name: Enable auto-merge
8411
+ if: env.LURQ_MODE == 'pr' && steps.pr.outputs.pull-request-number
8412
+ env:
8413
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
8414
+ run: gh pr merge --auto --squash "\${{ steps.pr.outputs.pull-request-number }}"
8415
+ `;
8050
8416
  }
8051
8417
  });
8052
8418
 
@@ -8143,6 +8509,7 @@ var init_manifests = __esm({
8143
8509
  var upgradePlan_exports = {};
8144
8510
  __export(upgradePlan_exports, {
8145
8511
  buildUpgradePlan: () => buildUpgradePlan,
8512
+ detectRepoFullName: () => detectRepoFullName,
8146
8513
  findManifests: () => findManifests,
8147
8514
  formatUpgradePlan: () => formatUpgradePlan,
8148
8515
  mergedDeps: () => mergedDeps,
@@ -8196,27 +8563,38 @@ function mergedDeps(manifests) {
8196
8563
  }
8197
8564
  return out;
8198
8565
  }
8566
+ function detectRepoFullName(explicit) {
8567
+ const value = explicit ?? process.env.GITHUB_REPOSITORY ?? null;
8568
+ return value && /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
8569
+ }
8199
8570
  async function buildUpgradePlan(dir, opts = {}) {
8200
8571
  const manifests = readManifests(dir);
8201
8572
  const deps = mergedDeps(manifests);
8202
8573
  if (Object.keys(deps).length === 0) {
8203
8574
  throw new Error(`No package.json with dependencies found under ${dir}`);
8204
8575
  }
8205
- const plan = await fetchUpgradePlan(deps, opts);
8576
+ const plan = await fetchUpgradePlan(deps, { ...opts, repo: detectRepoFullName(opts.repo) });
8206
8577
  return { ...plan, manifests: manifests.length, deps: Object.keys(deps).length };
8207
8578
  }
8208
8579
  function formatUpgradePlan(plan) {
8209
8580
  const out = [
8210
- `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`,
8211
- ""
8581
+ `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`
8212
8582
  ];
8583
+ if (plan.scopeSource === "repo-policy") {
8584
+ out.push(
8585
+ `policy: scope=${plan.scope}` + (plan.outOfScope ? `, ${plan.outOfScope} upgrade(s) held back` : ", nothing held back")
8586
+ );
8587
+ }
8588
+ out.push("");
8213
8589
  if (plan.upgrades.length === 0) {
8214
8590
  out.push("Everything lurq tracks is already on its latest release.");
8215
8591
  }
8216
8592
  for (const upgrade of plan.upgrades) {
8593
+ const held = upgrade.inScope === false ? " [HELD]" : "";
8217
8594
  out.push(
8218
- `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "")
8595
+ `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "") + held
8219
8596
  );
8597
+ if (upgrade.scopeReason) out.push(` held: ${upgrade.scopeReason}`);
8220
8598
  if (upgrade.removed.length) {
8221
8599
  out.push(` removes ${upgrade.removed.length}: ${upgrade.removed.slice(0, 8).join(", ")}`);
8222
8600
  }
@@ -9362,7 +9740,7 @@ function targetsFromPlanFile(path2) {
9362
9740
  if (!Array.isArray(parsed.upgrades)) {
9363
9741
  throw new Error(`${path2} is not an upgrade plan (no "upgrades" array)`);
9364
9742
  }
9365
- return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
9743
+ 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 }));
9366
9744
  }
9367
9745
  async function runCheckUpgrade(dir, opts) {
9368
9746
  const { scanReferences: scanReferences2 } = await Promise.resolve().then(() => (init_references(), references_exports));
@@ -9417,7 +9795,7 @@ var init_graph = __esm({
9417
9795
  });
9418
9796
 
9419
9797
  // src/db/surface.ts
9420
- import { and as and9, desc as desc5, eq as eq13, sql as sql9 } from "drizzle-orm";
9798
+ import { and as and9, desc as desc5, eq as eq13, sql as sql11 } from "drizzle-orm";
9421
9799
  function surfaceRef(pkg, version) {
9422
9800
  return { kind: "package_surface", namespace: "npm", name: pkg, version };
9423
9801
  }
@@ -9700,22 +10078,22 @@ var init_toolDescriptions = __esm({
9700
10078
  });
9701
10079
 
9702
10080
  // src/db/usage.ts
9703
- import { and as and11, desc as desc7, eq as eq15, gte, sql as sql10 } from "drizzle-orm";
10081
+ import { and as and11, desc as desc7, eq as eq15, gte, sql as sql12 } from "drizzle-orm";
9704
10082
  function windowStart(days) {
9705
- return sql10`CURRENT_DATE - (${days}::int - 1)`;
10083
+ return sql12`CURRENT_DATE - (${days}::int - 1)`;
9706
10084
  }
9707
10085
  async function recordUsage(db, ownerId, tool) {
9708
10086
  if (!ownerId) return;
9709
10087
  try {
9710
- await db.insert(ownerUsageDaily).values({ ownerId, date: sql10`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
10088
+ await db.insert(ownerUsageDaily).values({ ownerId, date: sql12`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
9711
10089
  target: [ownerUsageDaily.ownerId, ownerUsageDaily.date, ownerUsageDaily.tool],
9712
- set: { count: sql10`${ownerUsageDaily.count} + 1` }
10090
+ set: { count: sql12`${ownerUsageDaily.count} + 1` }
9713
10091
  });
9714
10092
  } catch {
9715
10093
  }
9716
10094
  }
9717
10095
  async function getUsageSummary(db, ownerId, days) {
9718
- const rows = await db.execute(sql10`
10096
+ const rows = await db.execute(sql12`
9719
10097
  select
9720
10098
  to_char(d, 'YYYY-MM-DD') as date,
9721
10099
  coalesce(sum(u.count), 0)::int as count
@@ -9732,13 +10110,13 @@ async function getUsageSummary(db, ownerId, days) {
9732
10110
  async function getUsageByTool(db, ownerId, days) {
9733
10111
  const rows = await db.select({
9734
10112
  tool: ownerUsageDaily.tool,
9735
- count: sql10`sum(${ownerUsageDaily.count})::int`
10113
+ count: sql12`sum(${ownerUsageDaily.count})::int`
9736
10114
  }).from(ownerUsageDaily).where(
9737
10115
  and11(
9738
10116
  eq15(ownerUsageDaily.ownerId, ownerId),
9739
10117
  gte(ownerUsageDaily.date, windowStart(days))
9740
10118
  )
9741
- ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql10`sum(${ownerUsageDaily.count})`));
10119
+ ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql12`sum(${ownerUsageDaily.count})`));
9742
10120
  return rows.map((r) => ({ tool: r.tool, count: Number(r.count) }));
9743
10121
  }
9744
10122
  var init_usage = __esm({
@@ -10259,6 +10637,52 @@ var init_brief = __esm({
10259
10637
  }
10260
10638
  });
10261
10639
 
10640
+ // src/github/scope.ts
10641
+ function isUnassessed(u) {
10642
+ return u.verdict === "unknown";
10643
+ }
10644
+ function breaksSomething(u) {
10645
+ return u.verdict === "removes-exports" || u.verdict === "arity-changed";
10646
+ }
10647
+ function scopeVerdict(u, scope) {
10648
+ if (scope === "all") return { inScope: true };
10649
+ if (u.advisories > 0) return { inScope: true };
10650
+ if (scope === "security") {
10651
+ return { inScope: false, reason: "no advisory \u2014 scope is security-only" };
10652
+ }
10653
+ if (isUnassessed(u)) {
10654
+ return { inScope: false, reason: "surface not yet analysed \u2014 nothing for the agent to rewrite" };
10655
+ }
10656
+ if (breaksSomething(u)) return { inScope: true };
10657
+ return { inScope: false, reason: "nothing referenced breaks \u2014 scope is security + blocking" };
10658
+ }
10659
+ function applyScope(upgrades, policy) {
10660
+ if (!policy) {
10661
+ return {
10662
+ upgrades: upgrades.map((u) => ({ ...u, inScope: true })),
10663
+ scope: "all",
10664
+ scopeSource: "unconnected",
10665
+ outOfScope: 0
10666
+ };
10667
+ }
10668
+ const scoped = upgrades.map((u) => {
10669
+ const verdict = scopeVerdict(u, policy.scope);
10670
+ return verdict.inScope ? { ...u, inScope: true } : { ...u, inScope: false, scopeReason: verdict.reason };
10671
+ });
10672
+ return {
10673
+ upgrades: scoped,
10674
+ scope: policy.scope,
10675
+ scopeSource: "repo-policy",
10676
+ outOfScope: scoped.filter((u) => !u.inScope).length
10677
+ };
10678
+ }
10679
+ var init_scope = __esm({
10680
+ "src/github/scope.ts"() {
10681
+ "use strict";
10682
+ init_esm_shims();
10683
+ }
10684
+ });
10685
+
10262
10686
  // src/github/runs.ts
10263
10687
  function str2(value, max) {
10264
10688
  if (typeof value !== "string") return null;
@@ -10275,7 +10699,7 @@ function strList(value, maxItems, maxLen) {
10275
10699
  }
10276
10700
  return out;
10277
10701
  }
10278
- function count(value) {
10702
+ function count2(value) {
10279
10703
  const n = typeof value === "number" ? value : Number(value);
10280
10704
  return Number.isInteger(n) && n >= 0 && n < 1e6 ? n : null;
10281
10705
  }
@@ -10299,9 +10723,9 @@ function parseUpgradeRun(input) {
10299
10723
  severity: severity2,
10300
10724
  status,
10301
10725
  symbolsAffected: strList(raw.symbolsAffected, MAX_SYMBOLS, MAX_NAME),
10302
- callSites: count(raw.callSites) ?? 0,
10726
+ callSites: count2(raw.callSites) ?? 0,
10303
10727
  callSiteFiles: files,
10304
- filesChanged: count(raw.filesChanged),
10728
+ filesChanged: count2(raw.filesChanged),
10305
10729
  testsPassed: typeof raw.testsPassed === "boolean" ? raw.testsPassed : null,
10306
10730
  prUrl: str2(raw.prUrl, MAX_URL),
10307
10731
  // Empty string, not null: it is part of the dedup key (see schema).
@@ -10319,6 +10743,11 @@ function parseUpgradeRuns(input, max) {
10319
10743
  }
10320
10744
  return { runs, rejected };
10321
10745
  }
10746
+ function parseRepoFullName(input) {
10747
+ const value = str2(input, MAX_NAME * 2);
10748
+ if (!value) return null;
10749
+ return /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
10750
+ }
10322
10751
  function parseDepsInput(input) {
10323
10752
  if (!input || typeof input !== "object") return {};
10324
10753
  const out = {};
@@ -10346,7 +10775,7 @@ var init_runs = __esm({
10346
10775
  });
10347
10776
 
10348
10777
  // src/db/upgradeRuns.ts
10349
- import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql11 } from "drizzle-orm";
10778
+ import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql13 } from "drizzle-orm";
10350
10779
  async function recordUpgradeRuns(db, rows) {
10351
10780
  if (rows.length === 0) return 0;
10352
10781
  await db.insert(upgradeRuns).values(rows).onConflictDoUpdate({
@@ -10358,15 +10787,15 @@ async function recordUpgradeRuns(db, rows) {
10358
10787
  upgradeRuns.runUrl
10359
10788
  ],
10360
10789
  set: {
10361
- severity: sql11`excluded.severity`,
10362
- status: sql11`excluded.status`,
10363
- symbolsAffected: sql11`excluded.symbols_affected`,
10364
- callSites: sql11`excluded.call_sites`,
10365
- callSiteFiles: sql11`excluded.call_site_files`,
10366
- filesChanged: sql11`excluded.files_changed`,
10367
- testsPassed: sql11`excluded.tests_passed`,
10368
- prUrl: sql11`excluded.pr_url`,
10369
- repoId: sql11`excluded.repo_id`
10790
+ severity: sql13`excluded.severity`,
10791
+ status: sql13`excluded.status`,
10792
+ symbolsAffected: sql13`excluded.symbols_affected`,
10793
+ callSites: sql13`excluded.call_sites`,
10794
+ callSiteFiles: sql13`excluded.call_site_files`,
10795
+ filesChanged: sql13`excluded.files_changed`,
10796
+ testsPassed: sql13`excluded.tests_passed`,
10797
+ prUrl: sql13`excluded.pr_url`,
10798
+ repoId: sql13`excluded.repo_id`
10370
10799
  }
10371
10800
  });
10372
10801
  return rows.length;
@@ -10381,12 +10810,12 @@ async function listRunsForRepo(db, ownerId, repoId, limit = 50) {
10381
10810
  async function getUpgradeImpact(db, ownerId, days = 30) {
10382
10811
  const since = new Date(Date.now() - days * 864e5);
10383
10812
  const rows = await db.select({
10384
- analysed: sql11`count(*)::int`,
10385
- blocking: sql11`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
10386
- callSites: sql11`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
10387
- prsOpened: sql11`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
10388
- merged: sql11`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
10389
- unverified: sql11`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
10813
+ analysed: sql13`count(*)::int`,
10814
+ blocking: sql13`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
10815
+ callSites: sql13`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
10816
+ prsOpened: sql13`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
10817
+ merged: sql13`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
10818
+ unverified: sql13`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
10390
10819
  }).from(upgradeRuns).where(and13(eq17(upgradeRuns.ownerId, ownerId), gte2(upgradeRuns.createdAt, since)));
10391
10820
  return rows[0] ?? {
10392
10821
  analysed: 0,
@@ -10941,7 +11370,8 @@ async function startHttpServer(opts = {}) {
10941
11370
  const runs = await listRunsForRepo(db, ownerId, row.id);
10942
11371
  const workflow = renderWorkflow({
10943
11372
  installCommand: row.installCommand ?? void 0,
10944
- armed: row.policy.enabled
11373
+ armed: row.policy.enabled,
11374
+ autoMerge: row.policy.autoMerge
10945
11375
  });
10946
11376
  res.status(200).json({
10947
11377
  repo: {
@@ -11071,8 +11501,13 @@ async function startHttpServer(opts = {}) {
11071
11501
  try {
11072
11502
  const drift = await computeDrift(db, [{ path: "package.json", deps }]);
11073
11503
  const brief = await briefRepo(db, drift);
11504
+ const ownerId = req.lurqKey?.ownerId ?? null;
11505
+ const repoFullName = parseRepoFullName((req.body ?? {}).repo);
11506
+ const policy = ownerId && repoFullName ? await findPolicyByFullName(db, ownerId, repoFullName) : null;
11507
+ const scoped = applyScope(brief.upgrades, policy);
11074
11508
  res.status(200).json({
11075
11509
  ...brief,
11510
+ ...scoped,
11076
11511
  // Surfaced so CI can log what lurq had no opinion on. An upgrade we do
11077
11512
  // not know about must not look like an upgrade we cleared.
11078
11513
  untracked: Object.keys(deps).length - drift.depsTracked
@@ -11153,6 +11588,7 @@ var init_http2 = __esm({
11153
11588
  init_app();
11154
11589
  init_brief();
11155
11590
  init_drift();
11591
+ init_scope();
11156
11592
  init_runs();
11157
11593
  init_upgradeRuns();
11158
11594
  init_manifests();
@@ -11242,9 +11678,9 @@ function buildProgram() {
11242
11678
  const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
11243
11679
  await runPlan2(file, opts);
11244
11680
  });
11245
- 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) => {
11681
+ 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) => {
11246
11682
  const { buildUpgradePlan: buildUpgradePlan2, formatUpgradePlan: formatUpgradePlan2 } = await Promise.resolve().then(() => (init_upgradePlan(), upgradePlan_exports));
11247
- const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey });
11683
+ const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey, repo: opts.repo });
11248
11684
  console.log(opts.json ? JSON.stringify(plan, null, 2) : formatUpgradePlan2(plan));
11249
11685
  });
11250
11686
  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(