lurqrun 0.0.8 → 0.0.10

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
@@ -75,15 +75,119 @@ var init_types = __esm({
75
75
  }
76
76
  });
77
77
 
78
+ // package.json
79
+ var package_default;
80
+ var init_package = __esm({
81
+ "package.json"() {
82
+ package_default = {
83
+ name: "lurqrun",
84
+ version: "0.0.10",
85
+ description: "A continuously-updated, evidence-scored index of JS/TS frameworks and libraries, exposed as an MCP server, CLI, and agent skill \u2014 so AI coding assistants get fresh, objective dependency recommendations.",
86
+ type: "module",
87
+ license: "Apache-2.0",
88
+ author: "jadenryu",
89
+ repository: {
90
+ type: "git",
91
+ url: "git+https://github.com/jadenryu/lurq.git"
92
+ },
93
+ homepage: "https://github.com/jadenryu/lurq#readme",
94
+ bugs: {
95
+ url: "https://github.com/jadenryu/lurq/issues"
96
+ },
97
+ keywords: [
98
+ "mcp",
99
+ "model-context-protocol",
100
+ "npm",
101
+ "dependencies",
102
+ "recommendations",
103
+ "claude-code",
104
+ "ai-coding"
105
+ ],
106
+ bin: {
107
+ lurq: "dist/bin/lurq.js"
108
+ },
109
+ workspaces: [
110
+ "apps/*"
111
+ ],
112
+ files: [
113
+ "dist",
114
+ "src/data/seed.json",
115
+ "templates",
116
+ "drizzle",
117
+ "README.md"
118
+ ],
119
+ engines: {
120
+ node: ">=20"
121
+ },
122
+ scripts: {
123
+ build: "tsup",
124
+ dev: "tsx src/bin/lurq.ts",
125
+ lurq: "tsx src/bin/lurq.ts",
126
+ "dev:apps": "npm run dev --workspace @lurq/docs & npm run dev --workspace @lurq/web",
127
+ "benchmark:validate": "tsx src/bin/benchmark.ts validate",
128
+ "benchmark:run": "tsx src/bin/benchmark.ts run",
129
+ start: "node dist/bin/lurq.js",
130
+ test: "vitest run",
131
+ "test:watch": "vitest",
132
+ typecheck: "tsc --noEmit",
133
+ lint: "eslint .",
134
+ format: "prettier --write .",
135
+ "format:check": "prettier --check .",
136
+ "db:generate": "drizzle-kit generate",
137
+ "db:migrate": "tsx src/bin/operator.ts db migrate",
138
+ operator: "tsx src/bin/operator.ts",
139
+ "operator:prod": "LURQ_ENV_FILE=.env.production tsx src/bin/operator.ts",
140
+ prepublishOnly: "npm test && npm run build",
141
+ "content:drift": "tsx scripts/gen-drift.ts"
142
+ },
143
+ dependencies: {
144
+ "@inquirer/prompts": "^8.5.2",
145
+ "@modelcontextprotocol/sdk": "^1.29.0",
146
+ commander: "^12.1.0",
147
+ dotenv: "^16.4.5",
148
+ "drizzle-orm": "^0.45.2",
149
+ express: "^5.2.1",
150
+ "express-rate-limit": "^8.5.2",
151
+ helmet: "^8.2.0",
152
+ ioredis: "^5.11.1",
153
+ postgres: "^3.4.9",
154
+ "rate-limit-redis": "^4.3.1",
155
+ semver: "^7.8.5",
156
+ typescript: "^5.5.4",
157
+ "update-notifier": "^7.3.1",
158
+ zod: "^3.23.8"
159
+ },
160
+ devDependencies: {
161
+ "@eslint/js": "^9.9.0",
162
+ "@types/express": "^5.0.6",
163
+ "@types/node": "^22.5.0",
164
+ "@types/semver": "^7.7.1",
165
+ "drizzle-kit": "^0.31.10",
166
+ e2b: "^2.31.0",
167
+ eslint: "^9.9.0",
168
+ marked: "^18.0.9",
169
+ "pdfjs-dist": "^6.2.108",
170
+ playwright: "^1.62.1",
171
+ prettier: "^3.3.3",
172
+ tsup: "^8.2.4",
173
+ tsx: "^4.19.0",
174
+ "typescript-eslint": "^8.3.0",
175
+ vitest: "^2.0.5"
176
+ }
177
+ };
178
+ }
179
+ });
180
+
78
181
  // src/core/constants.ts
79
182
  var SERVER_NAME, PACKAGE_NAME, VERSION, DEFAULT_ENDPOINT, API_KEY_PREFIX, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
80
183
  var init_constants = __esm({
81
184
  "src/core/constants.ts"() {
82
185
  "use strict";
83
186
  init_esm_shims();
187
+ init_package();
84
188
  SERVER_NAME = "lurq";
85
189
  PACKAGE_NAME = "lurqrun";
86
- VERSION = "0.0.8";
190
+ VERSION = package_default.version;
87
191
  DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
88
192
  API_KEY_PREFIX = "lurq_live_";
89
193
  EMBEDDING_DIM = 1536;
@@ -1117,6 +1221,10 @@ var init_schema = __esm({
1117
1221
  peerDependencies: jsonb("peer_dependencies").$type(),
1118
1222
  peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
1119
1223
  engines: jsonb("engines").$type(),
1224
+ /** Where this package runs, classified from its own manifest at ingest.
1225
+ * Resolves the `framework` category's frontend/backend ambiguity for the
1226
+ * architecture diagram. Null = the manifest did not say clearly enough. */
1227
+ runtimeTarget: text("runtime_target").$type(),
1120
1228
  // Computed outputs
1121
1229
  healthScore: integer("health_score"),
1122
1230
  /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
@@ -1234,7 +1342,13 @@ var init_schema = __esm({
1234
1342
  {
1235
1343
  packageName: text("package_name").notNull(),
1236
1344
  version: text("version").notNull(),
1237
- publishedAt: ts("published_at")
1345
+ publishedAt: ts("published_at"),
1346
+ /** Peers/engines declared by this exact version. Lifted from the registry
1347
+ * document at ingest (it already carries every version's manifest), so a
1348
+ * conflict check against the versions a repo actually RUNS is one indexed
1349
+ * query rather than a registry read per dependency. */
1350
+ peerDependencies: jsonb("peer_dependencies").$type(),
1351
+ engines: jsonb("engines").$type()
1238
1352
  },
1239
1353
  (table2) => [
1240
1354
  primaryKey({ columns: [table2.packageName, table2.version] }),
@@ -1615,10 +1729,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
1615
1729
  import postgres from "postgres";
1616
1730
  function createDb(opts = {}) {
1617
1731
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
1618
- const sql12 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1732
+ const sql14 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
1619
1733
  } });
1620
- const db = drizzle(sql12, { schema: schema_exports });
1621
- return { db, sql: sql12, close: () => sql12.end() };
1734
+ const db = drizzle(sql14, { schema: schema_exports });
1735
+ return { db, sql: sql14, close: () => sql14.end() };
1622
1736
  }
1623
1737
  var init_client = __esm({
1624
1738
  "src/db/client.ts"() {
@@ -1648,7 +1762,9 @@ async function upsertPackageVersions(db, name, versions) {
1648
1762
  const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1649
1763
  packageName: name,
1650
1764
  version: v.version,
1651
- publishedAt: v.publishedAt
1765
+ publishedAt: v.publishedAt,
1766
+ peerDependencies: v.peerDependencies ?? null,
1767
+ engines: v.engines ?? null
1652
1768
  }));
1653
1769
  await db.insert(packageVersions).values(rows).onConflictDoNothing();
1654
1770
  }
@@ -2163,6 +2279,7 @@ function parseNpmRegistry(json2) {
2163
2279
  directDependenciesCount: countDeps(latestManifest?.dependencies),
2164
2280
  hasProvenance: detectProvenance(latestManifest),
2165
2281
  hasInstallScripts: detectInstallScripts(latestManifest),
2282
+ hasBrowserField: latestManifest?.browser != null,
2166
2283
  peerDependencies: parseDepMap(latestManifest?.peerDependencies),
2167
2284
  peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
2168
2285
  engines: parseDepMap(latestManifest?.engines),
@@ -2198,7 +2315,13 @@ function parseVersionTimeline(json2) {
2198
2315
  const versions = json2?.versions;
2199
2316
  if (!versions || typeof versions !== "object") return [];
2200
2317
  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));
2318
+ return Object.keys(versions).map((version) => ({
2319
+ version,
2320
+ publishedAt: toDate(time?.[version]),
2321
+ // Already in this document — no second fetch, no per-version round trip.
2322
+ peerDependencies: parseDepMap(versions[version]?.peerDependencies),
2323
+ engines: parseDepMap(versions[version]?.engines)
2324
+ })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
2202
2325
  }
2203
2326
  function parseKeywords(value) {
2204
2327
  if (!Array.isArray(value)) return [];
@@ -2778,18 +2901,21 @@ async function checkCompat(db, packages2, opts = {}) {
2778
2901
  provenance: edge.provenance,
2779
2902
  witnessCount: edge.witnessCount
2780
2903
  }));
2904
+ const versionOf = new Map(members.map((m) => [m.name, m.version]));
2905
+ const matchesCheckedVersions = (edge) => edgeMatchesVersions(edge, versionOf);
2781
2906
  for (const edge of edges) {
2782
2907
  if (edge.status === "conflict") {
2908
+ 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
2909
  conflicts.push({
2784
2910
  source: "sandbox",
2785
2911
  packages: [edge.packageA, edge.packageB],
2786
- detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
2912
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox${at}`
2787
2913
  });
2788
2914
  }
2789
2915
  }
2790
2916
  const checkedNames = members.map((m) => m.name);
2791
2917
  const provenCompatible = new Set(
2792
- edges.filter((e) => e.status === "compatible").map((e) => pairKey(e.packageA, e.packageB))
2918
+ edges.filter((e) => e.status === "compatible" && matchesCheckedVersions(e)).map((e) => pairKey(e.packageA, e.packageB))
2793
2919
  );
2794
2920
  if (checkedNames.length >= 2 && !conflicts.length && !fullyCovered(checkedNames, provenCompatible)) {
2795
2921
  await enqueueCompatVerify(db, checkedNames).catch(() => {
@@ -2811,6 +2937,11 @@ async function checkCompat(db, packages2, opts = {}) {
2811
2937
  pairs: enumeratePairs(checkedNames, conflicts)
2812
2938
  };
2813
2939
  }
2940
+ function edgeMatchesVersions(edge, versionOf) {
2941
+ const a = versionOf.get(edge.packageA);
2942
+ const b = versionOf.get(edge.packageB);
2943
+ return a != null && b != null && a === edge.versionA && b === edge.versionB;
2944
+ }
2814
2945
  function enumeratePairs(names2, conflicts) {
2815
2946
  const byPair = /* @__PURE__ */ new Map();
2816
2947
  for (const c of conflicts) {
@@ -3244,22 +3375,6 @@ var init_verification = __esm({
3244
3375
  }
3245
3376
  });
3246
3377
 
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
3378
  // src/data/successors.json
3264
3379
  var successors_default;
3265
3380
  var init_successors = __esm({
@@ -3322,9 +3437,30 @@ var init_successors = __esm({
3322
3437
  });
3323
3438
 
3324
3439
  // src/core/successors.ts
3325
- function lookupSuccessor(name) {
3326
- const hit = MAP[name.toLowerCase()];
3327
- return hit ? { name: hit.replacedBy, reason: hit.reason } : null;
3440
+ function lookupSuccessor(name, learned) {
3441
+ const key = name.toLowerCase();
3442
+ const curated = MAP[key];
3443
+ if (curated) return { name: curated.replacedBy, reason: curated.reason };
3444
+ return learned?.get(key) ?? null;
3445
+ }
3446
+ function buildLearnedSuccessors(observed, isSuperseded) {
3447
+ const best = /* @__PURE__ */ new Map();
3448
+ for (const row of observed) {
3449
+ if (!isSuperseded(row.from)) continue;
3450
+ const key = row.from.toLowerCase();
3451
+ const current = best.get(key);
3452
+ const better = !current || row.owners > current.owners || row.owners === current.owners && row.observations > current.observations;
3453
+ if (better) best.set(key, { to: row.to, owners: row.owners, observations: row.observations });
3454
+ }
3455
+ return new Map(
3456
+ [...best].map(([from, hit]) => [
3457
+ from,
3458
+ {
3459
+ name: hit.to,
3460
+ reason: `${hit.owners} teams moved here after ${from} failed for the same need`
3461
+ }
3462
+ ])
3463
+ );
3328
3464
  }
3329
3465
  var MAP;
3330
3466
  var init_successors2 = __esm({
@@ -3336,6 +3472,81 @@ var init_successors2 = __esm({
3336
3472
  }
3337
3473
  });
3338
3474
 
3475
+ // src/db/outcomes.ts
3476
+ import { count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
3477
+ import { alias } from "drizzle-orm/pg-core";
3478
+ async function recordOutcome(db, outcome) {
3479
+ await db.insert(recommendationOutcomes).values(outcome);
3480
+ }
3481
+ async function getFieldEvidence(db, names2) {
3482
+ if (names2 && names2.length === 0) return /* @__PURE__ */ new Map();
3483
+ const rows = await db.select({
3484
+ packageName: recommendationOutcomes.packageName,
3485
+ reports: count(),
3486
+ successes: sql5`count(*) FILTER (WHERE ${SUCCESS_PREDICATE})`.mapWith(Number)
3487
+ }).from(recommendationOutcomes).where(names2 ? inArray3(recommendationOutcomes.packageName, names2) : void 0).groupBy(recommendationOutcomes.packageName);
3488
+ return new Map(
3489
+ rows.map((r) => [r.packageName, { reports: Number(r.reports), successes: r.successes }])
3490
+ );
3491
+ }
3492
+ async function learnSuccessions(db) {
3493
+ const failed = alias(recommendationOutcomes, "failed");
3494
+ const worked = alias(recommendationOutcomes, "worked");
3495
+ const rows = await db.select({
3496
+ from: failed.packageName,
3497
+ to: worked.packageName,
3498
+ owners: sql5`count(DISTINCT ${failed.ownerId})`.mapWith(Number),
3499
+ observations: sql5`count(*)`.mapWith(Number)
3500
+ }).from(failed).innerJoin(
3501
+ worked,
3502
+ sql5`${worked.ownerId} = ${failed.ownerId}
3503
+ AND ${worked.need} = ${failed.need}
3504
+ AND ${worked.packageName} <> ${failed.packageName}
3505
+ AND ${worked.createdAt} > ${failed.createdAt}`
3506
+ ).where(
3507
+ sql5`${failed.need} IS NOT NULL
3508
+ AND ${failed.ownerId} IS NOT NULL
3509
+ AND NOT (${failed.accepted} AND (${failed.buildSignal} IS NULL OR ${failed.buildSignal} <> 'failed'))
3510
+ AND (${worked.accepted} AND (${worked.buildSignal} IS NULL OR ${worked.buildSignal} <> 'failed'))`
3511
+ ).groupBy(failed.packageName, worked.packageName).having(sql5`count(DISTINCT ${failed.ownerId}) >= ${MIN_SUCCESSION_OWNERS}`);
3512
+ return rows.map((r) => ({
3513
+ from: r.from,
3514
+ to: r.to,
3515
+ owners: Number(r.owners),
3516
+ observations: Number(r.observations)
3517
+ }));
3518
+ }
3519
+ async function loadLearnedSuccessors(db, now = Date.now()) {
3520
+ if (learnedCache && now - learnedCache.at < LEARNED_TTL_MS) return learnedCache.value;
3521
+ const observed = await learnSuccessions(db);
3522
+ if (observed.length === 0) {
3523
+ learnedCache = { at: now, value: /* @__PURE__ */ new Map() };
3524
+ return learnedCache.value;
3525
+ }
3526
+ const names2 = [...new Set(observed.map((o) => o.from))];
3527
+ const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(inArray3(packages.name, names2));
3528
+ const dead = new Set(rows.filter((r) => r.deprecated || r.archived).map((r) => r.name));
3529
+ const value = buildLearnedSuccessors(observed, (name) => dead.has(name));
3530
+ learnedCache = { at: now, value };
3531
+ return value;
3532
+ }
3533
+ async function getOutcomesByOwner(db, ownerId, opts = {}) {
3534
+ return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
3535
+ }
3536
+ var SUCCESS_PREDICATE, MIN_SUCCESSION_OWNERS, LEARNED_TTL_MS, learnedCache;
3537
+ var init_outcomes = __esm({
3538
+ "src/db/outcomes.ts"() {
3539
+ "use strict";
3540
+ init_esm_shims();
3541
+ init_successors2();
3542
+ init_schema();
3543
+ SUCCESS_PREDICATE = sql5`${recommendationOutcomes.accepted} AND (${recommendationOutcomes.buildSignal} IS NULL OR ${recommendationOutcomes.buildSignal} <> 'failed')`;
3544
+ MIN_SUCCESSION_OWNERS = 3;
3545
+ LEARNED_TTL_MS = 10 * 60 * 1e3;
3546
+ learnedCache = null;
3547
+ }
3548
+ });
3549
+
3339
3550
  // src/ingestion/sources/githubReadme.ts
3340
3551
  async function fetchGithubReadme(owner, repo, fetchImpl) {
3341
3552
  for (const file of CANDIDATES) {
@@ -3732,7 +3943,7 @@ function resetWeights() {
3732
3943
  resetWeightsCache();
3733
3944
  return removed;
3734
3945
  }
3735
- var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3946
+ var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
3736
3947
  var init_weights = __esm({
3737
3948
  "src/scoring/weights.ts"() {
3738
3949
  "use strict";
@@ -3811,6 +4022,26 @@ var init_weights = __esm({
3811
4022
  /** Bundle size at the category median maps to this score; smaller → higher. */
3812
4023
  medianScore: 50
3813
4024
  };
4025
+ FIELD = {
4026
+ /** Weight when evidence exists — ~9% of the blend (0.10 / 1.10). */
4027
+ weight: 0.1,
4028
+ /**
4029
+ * Bayesian shrinkage strength: how many prior observations the neutral rate is
4030
+ * worth. One failed report out of one is not a 0-scoring package, and a raw
4031
+ * success rate would say it is. At m=12, a single failure lands near 69 rather
4032
+ * than 0, and it takes real volume to move the number far — which is the
4033
+ * correct epistemics for a signal users can trivially poison.
4034
+ */
4035
+ priorWeight: 12,
4036
+ /**
4037
+ * The prior success rate a package is assumed to have before anyone reports.
4038
+ * Set at the observed base rate of "installs and builds fine", not 0.5:
4039
+ * most packages do work, so 0.5 would penalise the first honest reporter.
4040
+ */
4041
+ priorRate: 0.75,
4042
+ /** Reports at or above this cap the confidence note as fully-evidenced. */
4043
+ wellEvidencedReports: 25
4044
+ };
3814
4045
  DEFAULT_WEIGHTS = {
3815
4046
  health: { ...HEALTH_WEIGHTS },
3816
4047
  composite: { ...COMPOSITE }
@@ -3978,17 +4209,24 @@ function computeQuality(input) {
3978
4209
  if (components.length === 0) return null;
3979
4210
  return weightedAverage(components);
3980
4211
  }
4212
+ function computeFieldScore(evidence) {
4213
+ if (!evidence || evidence.reports <= 0) return null;
4214
+ const { reports, successes } = evidence;
4215
+ const m = FIELD.priorWeight;
4216
+ return clamp(Math.round(100 * (successes + m * FIELD.priorRate) / (reports + m)));
4217
+ }
3981
4218
  function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
3982
- const { maintenance, adoption, reliability, efficiency } = breakdown;
3983
4219
  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
4220
+ return weightedAverage(
4221
+ [
4222
+ { value: breakdown.maintenance, weight: w.maintenance },
4223
+ { value: breakdown.adoption, weight: w.adoption },
4224
+ { value: breakdown.reliability, weight: w.reliability },
4225
+ { value: breakdown.efficiency, weight: w.efficiency },
4226
+ // `?? null` — a breakdown stored before field evidence shipped has the key
4227
+ // absent, and undefined must redistribute, not score 0.
4228
+ { value: breakdown.field ?? null, weight: FIELD.weight }
4229
+ ].filter((c) => c.value !== null)
3992
4230
  );
3993
4231
  }
3994
4232
  function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
@@ -4233,7 +4471,7 @@ var init_types2 = __esm({
4233
4471
  });
4234
4472
 
4235
4473
  // src/db/repos.ts
4236
- import { and as and5, eq as eq7, sql as sql5 } from "drizzle-orm";
4474
+ import { and as and5, eq as eq7, sql as sql6 } from "drizzle-orm";
4237
4475
  async function upsertRepos(db, rows) {
4238
4476
  if (rows.length === 0) return 0;
4239
4477
  await db.insert(repos).values(
@@ -4248,9 +4486,9 @@ async function upsertRepos(db, rows) {
4248
4486
  ).onConflictDoUpdate({
4249
4487
  target: [repos.ownerId, repos.fullName],
4250
4488
  set: {
4251
- installationId: sql5`excluded.installation_id`,
4252
- defaultBranch: sql5`excluded.default_branch`,
4253
- isPrivate: sql5`excluded.is_private`
4489
+ installationId: sql6`excluded.installation_id`,
4490
+ defaultBranch: sql6`excluded.default_branch`,
4491
+ isPrivate: sql6`excluded.is_private`
4254
4492
  }
4255
4493
  });
4256
4494
  return rows.length;
@@ -4260,7 +4498,7 @@ async function listRepos(db, ownerId) {
4260
4498
  }
4261
4499
  async function reposDeclaring(db, name) {
4262
4500
  return db.select().from(repos).where(
4263
- sql5`exists (
4501
+ sql6`exists (
4264
4502
  select 1 from jsonb_array_elements(${repos.manifests}) as m
4265
4503
  where jsonb_exists(m -> 'deps', ${name})
4266
4504
  )`
@@ -4270,6 +4508,10 @@ async function getRepo(db, ownerId, id) {
4270
4508
  const rows = await db.select().from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).limit(1);
4271
4509
  return rows[0] ?? null;
4272
4510
  }
4511
+ async function findPolicyByFullName(db, ownerId, fullName) {
4512
+ const rows = await db.select({ policy: repos.policy }).from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.fullName, fullName))).limit(1);
4513
+ return rows[0]?.policy ?? null;
4514
+ }
4273
4515
  async function saveScan(db, id, scan) {
4274
4516
  await db.update(repos).set({
4275
4517
  manifests: scan.manifests,
@@ -4355,8 +4597,76 @@ var init_attribute = __esm({
4355
4597
  }
4356
4598
  });
4357
4599
 
4600
+ // src/ingestion/sources/osv.ts
4601
+ function installKey(name, version) {
4602
+ return `${name}@${version}`;
4603
+ }
4604
+ async function queryVulnerableInstalls(installs, fetchImpl) {
4605
+ const affected = /* @__PURE__ */ new Map();
4606
+ if (installs.length === 0) return { affected, complete: true };
4607
+ const unique = [...new Map(installs.map((i) => [installKey(i.name, i.version), i])).values()];
4608
+ const batches = [];
4609
+ for (let i = 0; i < unique.length; i += BATCH) batches.push(unique.slice(i, i + BATCH));
4610
+ let complete = true;
4611
+ let cursor = 0;
4612
+ const worker = async () => {
4613
+ for (let i = cursor++; i < batches.length; i = cursor++) {
4614
+ const batch = batches[i];
4615
+ try {
4616
+ const { data } = await httpRequest(URL2, {
4617
+ host: HOST7,
4618
+ method: "POST",
4619
+ headers: { "content-type": "application/json" },
4620
+ body: JSON.stringify({
4621
+ queries: batch.map((q) => ({
4622
+ version: q.version,
4623
+ package: { name: q.name, ecosystem: "npm" }
4624
+ }))
4625
+ }),
4626
+ // Uncached on purpose. The shared cache keys on the URL, and every
4627
+ // batch here posts to the same one with a different body — a cache hit
4628
+ // would answer one tree's question with another tree's vulnerabilities.
4629
+ ttlMs: 0,
4630
+ // Best-effort by contract: the caller keeps its coarser answer when
4631
+ // this fails, so exhausting the default retry ladder would only stall
4632
+ // a scheduled scan behind an outage nobody is waiting on.
4633
+ retries: 1,
4634
+ fetchImpl
4635
+ });
4636
+ const results = data?.results ?? [];
4637
+ if (results.length !== batch.length) {
4638
+ complete = false;
4639
+ continue;
4640
+ }
4641
+ results.forEach((result, j) => {
4642
+ const ids = (result?.vulns ?? []).map((v) => v?.id).filter((id) => typeof id === "string");
4643
+ if (ids.length === 0) return;
4644
+ const q = batch[j];
4645
+ affected.set(installKey(q.name, q.version), ids);
4646
+ });
4647
+ } catch {
4648
+ complete = false;
4649
+ }
4650
+ }
4651
+ };
4652
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, batches.length) }, worker));
4653
+ return { affected, complete };
4654
+ }
4655
+ var HOST7, URL2, BATCH, CONCURRENCY;
4656
+ var init_osv = __esm({
4657
+ "src/ingestion/sources/osv.ts"() {
4658
+ "use strict";
4659
+ init_esm_shims();
4660
+ init_http();
4661
+ HOST7 = "api.osv.dev";
4662
+ URL2 = `https://${HOST7}/v1/querybatch`;
4663
+ BATCH = 250;
4664
+ CONCURRENCY = 4;
4665
+ }
4666
+ });
4667
+
4358
4668
  // src/github/drift.ts
4359
- import { inArray as inArray3 } from "drizzle-orm";
4669
+ import { inArray as inArray4, sql as sql7 } from "drizzle-orm";
4360
4670
  import semver2 from "semver";
4361
4671
  function declaredDeps(manifests) {
4362
4672
  const out = /* @__PURE__ */ new Map();
@@ -4383,7 +4693,7 @@ async function loadIndexed(db, names2) {
4383
4693
  latestVersion: packages.latestVersion,
4384
4694
  deprecated: packages.deprecated,
4385
4695
  advisories: packages.advisories
4386
- }).from(packages).where(inArray3(packages.name, names2.slice(i, i + NAME_CHUNK)));
4696
+ }).from(packages).where(inArray4(packages.name, names2.slice(i, i + NAME_CHUNK)));
4387
4697
  for (const row of rows) {
4388
4698
  out.set(row.name, {
4389
4699
  latestVersion: row.latestVersion,
@@ -4397,7 +4707,7 @@ async function loadIndexed(db, names2) {
4397
4707
  async function loadVersions(db, names2) {
4398
4708
  const out = /* @__PURE__ */ new Map();
4399
4709
  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)));
4710
+ const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray4(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
4401
4711
  for (const row of rows) {
4402
4712
  const list = out.get(row.name);
4403
4713
  if (list) list.push(row.version);
@@ -4447,19 +4757,28 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4447
4757
  };
4448
4758
  if (transitives.length === 0) return empty2;
4449
4759
  const indexed = await loadIndexed(db, [...new Set(transitives.map((d) => d.name))]);
4760
+ const { affected, complete } = await queryVulnerableInstalls(
4761
+ transitives.map((d) => ({ name: d.name, version: d.version }))
4762
+ );
4450
4763
  const risks = [];
4451
4764
  let tracked = 0;
4452
4765
  for (const dep of transitives) {
4453
4766
  const row = indexed.get(dep.name);
4454
4767
  if (!row) continue;
4455
4768
  tracked++;
4456
- if (row.advisories === 0 && !row.deprecated) continue;
4769
+ const vulnIds = affected.get(installKey(dep.name, dep.version)) ?? [];
4770
+ if (row.advisories === 0 && !row.deprecated && vulnIds.length === 0) continue;
4457
4771
  risks.push({
4458
4772
  name: dep.name,
4459
4773
  version: dep.version,
4460
4774
  latest: row.latestVersion,
4461
4775
  advisories: row.advisories,
4462
4776
  deprecated: row.deprecated,
4777
+ // Empty when OSV cleared this version, which is the actionable difference
4778
+ // from `advisories`: the package has a history, this install does not.
4779
+ // Undefined when the lookup could not complete — never an empty array,
4780
+ // which would read as a clean bill of health nobody established.
4781
+ vulnerabilities: complete ? vulnIds : void 0,
4463
4782
  pulledInBy: attribution.parentsOf.get(dep.name) ?? []
4464
4783
  });
4465
4784
  }
@@ -4470,6 +4789,10 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
4470
4789
  resolved: transitives.length,
4471
4790
  tracked,
4472
4791
  advisoryPackages: risks.filter((r) => r.advisories > 0).length,
4792
+ // The number that can actually be acted on: installs OSV matched to a
4793
+ // vulnerability at their exact version. Undefined when the lookup did not
4794
+ // complete, so "not checked" never renders as zero.
4795
+ vulnerableInstalls: complete ? risks.filter((r) => r.vulnerabilities?.length).length : void 0,
4473
4796
  deprecated: risks.filter((r) => r.deprecated).length,
4474
4797
  risks: risks.slice(0, TRANSITIVE_DETAIL_CAP),
4475
4798
  truncated,
@@ -4481,6 +4804,29 @@ async function conflictsAtLatest(db, names2) {
4481
4804
  const { members } = await assembleMembers(db, names2);
4482
4805
  return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4483
4806
  }
4807
+ async function conflictsAtCurrent(db, deps) {
4808
+ const pinned = deps.filter((d) => Boolean(d.resolved));
4809
+ if (pinned.length === 0) return [];
4810
+ const rows = await db.select({
4811
+ packageName: packageVersions.packageName,
4812
+ version: packageVersions.version,
4813
+ peerDependencies: packageVersions.peerDependencies,
4814
+ engines: packageVersions.engines
4815
+ }).from(packageVersions).where(
4816
+ inArray4(
4817
+ sql7`(${packageVersions.packageName}, ${packageVersions.version})`,
4818
+ pinned.map((d) => sql7`(${d.name}, ${d.resolved})`)
4819
+ )
4820
+ );
4821
+ const members = rows.map((r) => ({
4822
+ name: r.packageName,
4823
+ version: r.version,
4824
+ peerDependencies: r.peerDependencies,
4825
+ peerDependenciesMeta: null,
4826
+ engines: r.engines
4827
+ }));
4828
+ return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
4829
+ }
4484
4830
  async function computeDrift(db, manifests, resolvedTree = null) {
4485
4831
  const declared = declaredDeps(manifests);
4486
4832
  const names2 = [...declared.keys()];
@@ -4502,7 +4848,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4502
4848
  advisories: 0,
4503
4849
  deps: [],
4504
4850
  transitive,
4505
- conflictsAtLatest: []
4851
+ conflictsAtLatest: [],
4852
+ conflictsAtCurrent: []
4506
4853
  };
4507
4854
  }
4508
4855
  const [indexed, versions] = await Promise.all([
@@ -4516,7 +4863,10 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4516
4863
  deps.push(depDrift(name, entry, row, versions.get(name) ?? []));
4517
4864
  }
4518
4865
  deps.sort((a, b) => severity(b) - severity(a) || a.name.localeCompare(b.name));
4519
- const conflicts = await conflictsAtLatest(db, deps.map((d) => d.name));
4866
+ const [conflicts, currentConflicts] = await Promise.all([
4867
+ conflictsAtLatest(db, deps.map((d) => d.name)),
4868
+ conflictsAtCurrent(db, deps)
4869
+ ]);
4520
4870
  return {
4521
4871
  depsDeclared: declared.size,
4522
4872
  depsTracked: deps.length,
@@ -4526,7 +4876,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
4526
4876
  advisories: deps.reduce((sum, d) => sum + d.advisories, 0),
4527
4877
  deps: deps.slice(0, REPO_DRIFT_DETAIL_CAP),
4528
4878
  transitive,
4529
- conflictsAtLatest: conflicts
4879
+ conflictsAtLatest: conflicts,
4880
+ conflictsAtCurrent: currentConflicts
4530
4881
  };
4531
4882
  }
4532
4883
  var NAME_CHUNK;
@@ -4537,6 +4888,7 @@ var init_drift = __esm({
4537
4888
  init_schema();
4538
4889
  init_types2();
4539
4890
  init_attribute();
4891
+ init_osv();
4540
4892
  init_members();
4541
4893
  init_peerCompat();
4542
4894
  NAME_CHUNK = 500;
@@ -4637,6 +4989,82 @@ var init_errors = __esm({
4637
4989
  }
4638
4990
  });
4639
4991
 
4992
+ // src/core/runtimeTarget.ts
4993
+ function classifyRuntimeTarget(input) {
4994
+ const override = OVERRIDES[input.name.toLowerCase()];
4995
+ if (override) return override;
4996
+ const peers = Object.keys(input.peerDependencies ?? {});
4997
+ if (peers.some((p) => RENDERER_PEERS.has(p))) return "browser";
4998
+ const keywords = input.keywords.map((k) => k.toLowerCase());
4999
+ const server = keywords.filter((k) => SERVER_KEYWORDS.has(k)).length;
5000
+ const browser = keywords.filter((k) => BROWSER_KEYWORDS.has(k)).length;
5001
+ if (browser > server) return "browser";
5002
+ if (server > browser) return "node";
5003
+ if (input.hasBrowserField) return "browser";
5004
+ if (input.engines?.node && !input.hasBrowserField && browser === 0) return "node";
5005
+ return null;
5006
+ }
5007
+ var SERVER_KEYWORDS, BROWSER_KEYWORDS, RENDERER_PEERS, OVERRIDES;
5008
+ var init_runtimeTarget = __esm({
5009
+ "src/core/runtimeTarget.ts"() {
5010
+ "use strict";
5011
+ init_esm_shims();
5012
+ SERVER_KEYWORDS = /* @__PURE__ */ new Set([
5013
+ "server",
5014
+ "http-server",
5015
+ "backend",
5016
+ "middleware",
5017
+ "rest",
5018
+ "restful",
5019
+ "api",
5020
+ "router",
5021
+ "routing",
5022
+ "orm",
5023
+ "database",
5024
+ "sql",
5025
+ "graphql-server",
5026
+ "microservice",
5027
+ "websocket",
5028
+ "rpc"
5029
+ ]);
5030
+ BROWSER_KEYWORDS = /* @__PURE__ */ new Set([
5031
+ "react",
5032
+ "vue",
5033
+ "svelte",
5034
+ "angular",
5035
+ "preact",
5036
+ "solid",
5037
+ "dom",
5038
+ "browser",
5039
+ "component",
5040
+ "components",
5041
+ "ui",
5042
+ "css",
5043
+ "styling",
5044
+ "animation",
5045
+ "frontend",
5046
+ "client-side",
5047
+ "jsx",
5048
+ "hooks"
5049
+ ]);
5050
+ RENDERER_PEERS = /* @__PURE__ */ new Set(["react", "react-dom", "vue", "svelte", "@angular/core", "preact"]);
5051
+ OVERRIDES = {
5052
+ express: "node",
5053
+ fastify: "node",
5054
+ koa: "node",
5055
+ hono: "node",
5056
+ "@nestjs/core": "node",
5057
+ "@hapi/hapi": "node",
5058
+ // Meta-frameworks run in both places; they are the server in an architecture
5059
+ // diagram, which is the question this answers.
5060
+ next: "node",
5061
+ nuxt: "node",
5062
+ "@remix-run/node": "node",
5063
+ astro: "node"
5064
+ };
5065
+ }
5066
+ });
5067
+
4640
5068
  // src/pipeline/mineEdges.ts
4641
5069
  function trackedPairs(nodes, tracked) {
4642
5070
  const t = nodes.filter((n) => tracked.has(n.name));
@@ -4717,6 +5145,13 @@ function assemblePackageRow(p) {
4717
5145
  peerDependencies: r?.peerDependencies ?? null,
4718
5146
  peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
4719
5147
  engines: r?.engines ?? null,
5148
+ runtimeTarget: r ? classifyRuntimeTarget({
5149
+ name: p.name,
5150
+ hasBrowserField: r.hasBrowserField,
5151
+ keywords: r.keywords,
5152
+ engines: r.engines,
5153
+ peerDependencies: r.peerDependencies
5154
+ }) : null,
4720
5155
  healthScore: p.healthScore,
4721
5156
  qualityScore: p.qualityScore,
4722
5157
  confidence: p.confidence,
@@ -4737,6 +5172,7 @@ var init_sync = __esm({
4737
5172
  init_errors();
4738
5173
  init_http();
4739
5174
  init_logger();
5175
+ init_runtimeTarget();
4740
5176
  init_collect();
4741
5177
  init_npmDownloads();
4742
5178
  init_summarize();
@@ -4810,7 +5246,7 @@ var init_ingestQueue = __esm({
4810
5246
  });
4811
5247
 
4812
5248
  // src/pipeline/single.ts
4813
- import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql6 } from "drizzle-orm";
5249
+ import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
4814
5250
  function raceTimeout(p, ms) {
4815
5251
  let timer;
4816
5252
  return Promise.race([
@@ -4826,7 +5262,7 @@ async function getSeedCategory(db, name) {
4826
5262
  }
4827
5263
  async function getCategoryMedianBundle(db, category) {
4828
5264
  const [row] = await db.select({
4829
- m: sql6`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
5265
+ m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
4830
5266
  }).from(packages).where(and6(eq8(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
4831
5267
  return row?.m ?? null;
4832
5268
  }
@@ -4858,12 +5294,14 @@ async function syncOnePackage(db, name, opts = {}) {
4858
5294
  const input = toScoringInput(signals, category);
4859
5295
  const median2 = category ? await getCategoryMedianBundle(db, category) : null;
4860
5296
  const quality = computeQuality(input);
5297
+ const evidence = await getFieldEvidence(db, [name]);
4861
5298
  const breakdown = {
4862
5299
  maintenance: computeMaintenance(input, now),
4863
5300
  adoption: computeAdoption(input),
4864
5301
  reliability: computeReliability(input),
4865
5302
  efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
4866
- quality
5303
+ quality,
5304
+ field: computeFieldScore(evidence.get(name) ?? null)
4867
5305
  };
4868
5306
  const healthScore = computeHealthScore(breakdown);
4869
5307
  const confidence = computeConfidence(input, now, quality);
@@ -4933,6 +5371,7 @@ var init_single = __esm({
4933
5371
  init_categoryInference();
4934
5372
  init_scoring();
4935
5373
  init_embeddings();
5374
+ init_outcomes();
4936
5375
  init_packages();
4937
5376
  init_schema();
4938
5377
  init_alerts2();
@@ -4944,7 +5383,7 @@ var init_single = __esm({
4944
5383
  });
4945
5384
 
4946
5385
  // 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";
5386
+ import { and as and7, cosineDistance, eq as eq9, inArray as inArray5, isNotNull as isNotNull4, lte, sql as sql9 } from "drizzle-orm";
4948
5387
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
4949
5388
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
4950
5389
  const [queryVec] = await provider.embed([opts.need]);
@@ -4997,7 +5436,7 @@ function buildConditions(constraints, category) {
4997
5436
  const allowed = ["proven", "emerging", "promising", "unproven"].filter(
4998
5437
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
4999
5438
  );
5000
- conditions.push(inArray4(packages.confidence, allowed));
5439
+ conditions.push(inArray5(packages.confidence, allowed));
5001
5440
  }
5002
5441
  return conditions;
5003
5442
  }
@@ -5011,10 +5450,10 @@ async function runVectorQuery(db, queryVec, providerId, constraints, category, p
5011
5450
  return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(distance).limit(pool);
5012
5451
  }
5013
5452
  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);
5453
+ const tsquery = sql9`websearch_to_tsquery('english', ${need})`;
5454
+ const rank2 = sql9`ts_rank(${packages.searchVector}, ${tsquery})`;
5455
+ const conditions = [sql9`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
5456
+ return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql9`${rank2} desc`).limit(pool);
5018
5457
  }
5019
5458
  function toCandidate(row) {
5020
5459
  return {
@@ -5152,7 +5591,7 @@ var init_types3 = __esm({
5152
5591
  });
5153
5592
 
5154
5593
  // src/db/selectionPolicy.ts
5155
- import { eq as eq10, inArray as inArray5 } from "drizzle-orm";
5594
+ import { eq as eq10, inArray as inArray6 } from "drizzle-orm";
5156
5595
  async function getSelectionPolicy(db, ownerId) {
5157
5596
  if (!ownerId) return DEFAULT_SELECTION_POLICY;
5158
5597
  const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(eq10(selectionPolicies.ownerId, ownerId)).limit(1);
@@ -5171,7 +5610,7 @@ async function loadPolicyFacts(db, names2) {
5171
5610
  name: packages.name,
5172
5611
  license: packages.license,
5173
5612
  deprecated: packages.deprecated
5174
- }).from(packages).where(inArray5(packages.name, names2));
5613
+ }).from(packages).where(inArray6(packages.name, names2));
5175
5614
  for (const row of rows) {
5176
5615
  out.set(row.name, { license: row.license, deprecated: row.deprecated });
5177
5616
  }
@@ -5488,7 +5927,7 @@ __export(handlers_exports, {
5488
5927
  rowToEvaluate: () => rowToEvaluate
5489
5928
  });
5490
5929
  import { createHash as createHash3 } from "crypto";
5491
- import { sql as sql8 } from "drizzle-orm";
5930
+ import { sql as sql10 } from "drizzle-orm";
5492
5931
  function isStale(dataAsOf) {
5493
5932
  if (!dataAsOf) return true;
5494
5933
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
@@ -5504,7 +5943,7 @@ function topAdvisories(advisories, max = 5) {
5504
5943
  if (!advisories?.length) return [];
5505
5944
  return [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).slice(0, max);
5506
5945
  }
5507
- function rowToEvaluate(row) {
5946
+ function rowToEvaluate(row, learned) {
5508
5947
  const breakdown = row.scoreBreakdown ?? {
5509
5948
  maintenance: 0,
5510
5949
  adoption: 0,
@@ -5533,11 +5972,11 @@ function rowToEvaluate(row) {
5533
5972
  summary: row.summary ? truncateSentences(row.summary, 3) : null,
5534
5973
  usageGuide: row.usageGuide ?? null,
5535
5974
  repoUrl: row.repoUrl,
5536
- replacedBy: lookupSuccessor(row.name)
5975
+ replacedBy: lookupSuccessor(row.name, learned)
5537
5976
  };
5538
5977
  }
5539
5978
  async function latestDataAsOf(db) {
5540
- const [row] = await db.select({ m: sql8`max(${packages.dataAsOf})` }).from(packages);
5979
+ const [row] = await db.select({ m: sql10`max(${packages.dataAsOf})` }).from(packages);
5541
5980
  return new Date(row?.m ?? Date.now()).toISOString();
5542
5981
  }
5543
5982
  function cacheKey(parts) {
@@ -5580,7 +6019,8 @@ async function handleEvaluate(db, input, ownerId = null) {
5580
6019
  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
6020
  };
5582
6021
  }
5583
- const evaluated2 = rowToEvaluate(row);
6022
+ const learned = row.deprecated || row.archived ? await loadLearnedSuccessors(db) : void 0;
6023
+ const evaluated2 = rowToEvaluate(row, learned);
5584
6024
  const verification = await getLatestVerificationByName(db, row.name);
5585
6025
  return verification ? { ...evaluated2, buildVerified: toBuildVerified(verification) } : evaluated2;
5586
6026
  },
@@ -5607,7 +6047,7 @@ async function handleCompare(db, input, ownerId = null) {
5607
6047
  const results = await Promise.all(
5608
6048
  input.packages.map((name) => getOrFetchPackage(db, name, { requestedByOwnerId: ownerId }))
5609
6049
  );
5610
- const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
6050
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map((row) => rowToEvaluate(row)).sort((a, b) => b.healthScore - a.healthScore);
5611
6051
  const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
5612
6052
  return {
5613
6053
  dataAsOf: await latestDataAsOf(db),
@@ -5892,7 +6332,8 @@ function parseRpcBody(raw) {
5892
6332
  }
5893
6333
  }
5894
6334
  function fetchUpgradePlan(deps, opts = {}) {
5895
- return post("/upgrade-plan", { deps }, opts);
6335
+ const body = opts.repo ? { deps, repo: opts.repo } : { deps };
6336
+ return post("/upgrade-plan", body, opts);
5896
6337
  }
5897
6338
  var RemoteError;
5898
6339
  var init_remote = __esm({
@@ -5987,10 +6428,10 @@ var init_optimize = __esm({
5987
6428
  });
5988
6429
 
5989
6430
  // src/mcp/diagram.ts
5990
- import { inArray as inArray6 } from "drizzle-orm";
6431
+ import { inArray as inArray7 } from "drizzle-orm";
5991
6432
  function layerFor(item) {
5992
6433
  if (!item.category) return UNCLASSIFIED;
5993
- if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
6434
+ if (item.category === "framework" && item.runtimeTarget === "node") return "Backend";
5994
6435
  return LAYER_OF[item.category] ?? UNCLASSIFIED;
5995
6436
  }
5996
6437
  function nodeId(label, i) {
@@ -6040,22 +6481,41 @@ async function handleDiagram(db, input) {
6040
6481
  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
6482
  };
6042
6483
  }
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
- }));
6484
+ const rows = await db.select({
6485
+ name: packages.name,
6486
+ category: packages.category,
6487
+ runtimeTarget: packages.runtimeTarget
6488
+ }).from(packages).where(inArray7(packages.name, input.stack));
6489
+ const known = new Map(rows.map((r) => [r.name, r]));
6490
+ const items = input.stack.map((name) => {
6491
+ const row = known.get(name);
6492
+ return {
6493
+ label: name,
6494
+ category: row?.category ?? inferCategory(name),
6495
+ // Fall back to the name-only classifier for a package that predates the
6496
+ // column or is not indexed at all: the curated overrides still resolve the
6497
+ // frameworks people actually diagram, and an unclassifiable one keeps the
6498
+ // old Presentation default rather than moving on a null.
6499
+ runtimeTarget: row?.runtimeTarget ?? classifyRuntimeTarget({
6500
+ name,
6501
+ hasBrowserField: false,
6502
+ keywords: [],
6503
+ engines: null,
6504
+ peerDependencies: null
6505
+ })
6506
+ };
6507
+ });
6049
6508
  const unclassified = items.filter((i) => layerFor(i) === UNCLASSIFIED).map((i) => i.label);
6050
6509
  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
6510
  return { mermaid: buildMermaid(items), note };
6052
6511
  }
6053
- var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS, BACKEND_FRAMEWORKS;
6512
+ var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS;
6054
6513
  var init_diagram = __esm({
6055
6514
  "src/mcp/diagram.ts"() {
6056
6515
  "use strict";
6057
6516
  init_esm_shims();
6058
6517
  init_categoryInference();
6518
+ init_runtimeTarget();
6059
6519
  init_schema();
6060
6520
  UNCLASSIFIED = "Unclassified";
6061
6521
  LAYER_OF = {
@@ -6083,14 +6543,6 @@ var init_diagram = __esm({
6083
6543
  testing: "Tooling"
6084
6544
  };
6085
6545
  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
6546
  }
6095
6547
  });
6096
6548
 
@@ -6105,7 +6557,7 @@ __export(plan_exports, {
6105
6557
  resolvePins: () => resolvePins
6106
6558
  });
6107
6559
  import { createHash as createHash4 } from "crypto";
6108
- import { inArray as inArray7 } from "drizzle-orm";
6560
+ import { inArray as inArray8 } from "drizzle-orm";
6109
6561
  function packageToCandidate(row) {
6110
6562
  return {
6111
6563
  name: row.name,
@@ -6326,7 +6778,7 @@ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
6326
6778
  async function bundleSizes(db, candidates) {
6327
6779
  const names2 = [...new Set(candidates.map((c) => c.name))];
6328
6780
  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));
6781
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray8(packages.name, names2));
6330
6782
  return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
6331
6783
  }
6332
6784
  async function decompose(document) {
@@ -7880,6 +8332,10 @@ var init_app = __esm({
7880
8332
  });
7881
8333
 
7882
8334
  // src/github/workflow.ts
8335
+ function cliSpec(version = VERSION) {
8336
+ const [major = "0", minor = "0"] = version.split(".");
8337
+ return major === "0" ? `${PACKAGE_NAME}@${major}.${minor}` : `${PACKAGE_NAME}@${major}`;
8338
+ }
7883
8339
  function detectInstallCommand(lockfiles) {
7884
8340
  if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm install --frozen-lockfile";
7885
8341
  if (lockfiles.includes("yarn.lock")) return "yarn install --frozen-lockfile";
@@ -7892,6 +8348,8 @@ function renderWorkflow(opts = {}) {
7892
8348
  const install = opts.installCommand ?? "npm ci";
7893
8349
  const max = opts.maxUpgrades ?? 3;
7894
8350
  const mode = opts.armed ? "pr" : "comment";
8351
+ const autoMerge = opts.autoMerge ?? false;
8352
+ const cli = cliSpec();
7895
8353
  return `# Managed by lurq, https://lurq.run
7896
8354
  #
7897
8355
  # Keeps this repository's dependencies current and rewrites the call sites an
@@ -7939,19 +8397,19 @@ jobs:
7939
8397
  - name: Plan
7940
8398
  env:
7941
8399
  LURQ_API_KEY: \${{ secrets.LURQ_API_KEY }}
7942
- run: npx -y lurqrun upgrade-plan . --json > lurq-plan.json
8400
+ run: npx -y ${cli} upgrade-plan . --json > lurq-plan.json
7943
8401
 
7944
8402
  # 2. Narrow that to symbols THIS repo references. Runs entirely locally
7945
8403
  # against both versions' npm tarballs, no API key, no test suite.
7946
8404
  - name: Check against this codebase
7947
- run: npx -y lurqrun check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
8405
+ run: npx -y ${cli} check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
7948
8406
 
7949
8407
  # The same report twice: once into the run summary, once as the body of
7950
8408
  # the pull request. Fenced, because the report is aligned plain text and
7951
8409
  # markdown would otherwise collapse its indentation into one paragraph.
7952
8410
  - name: Summarise
7953
8411
  run: |
7954
- npx -y lurqrun check-upgrade . --plan lurq-plan.json > lurq-report.txt
8412
+ npx -y ${cli} check-upgrade . --plan lurq-plan.json > lurq-report.txt
7955
8413
  { echo '\`\`\`'; cat lurq-report.txt; echo '\`\`\`'; } > lurq-report.md
7956
8414
  cat lurq-report.md >> "\${GITHUB_STEP_SUMMARY}"
7957
8415
 
@@ -8027,6 +8485,7 @@ jobs:
8027
8485
 
8028
8486
  # 4. The workflow does version control, never the model.
8029
8487
  - name: Open pull request
8488
+ id: pr
8030
8489
  if: env.LURQ_MODE == 'pr'
8031
8490
  uses: peter-evans/create-pull-request@v7
8032
8491
  with:
@@ -8039,19 +8498,30 @@ jobs:
8039
8498
  body-path: lurq-report.md
8040
8499
  labels: dependencies
8041
8500
  delete-branch: true
8042
- `;
8501
+ ${autoMerge ? MERGE_STEP : ""}`;
8043
8502
  }
8044
8503
  function newFileUrl(fullName, branch, content) {
8045
8504
  const params = new URLSearchParams({ filename: WORKFLOW_PATH, value: content });
8046
8505
  return `https://github.com/${fullName}/new/${encodeURIComponent(branch)}?${params.toString()}`;
8047
8506
  }
8048
- var WORKFLOW_PATH, DEFAULT_CRON;
8507
+ var WORKFLOW_PATH, DEFAULT_CRON, MERGE_STEP;
8049
8508
  var init_workflow = __esm({
8050
8509
  "src/github/workflow.ts"() {
8051
8510
  "use strict";
8052
8511
  init_esm_shims();
8512
+ init_constants();
8053
8513
  WORKFLOW_PATH = ".github/workflows/lurq-upgrade.yml";
8054
8514
  DEFAULT_CRON = "0 6 * * 1";
8515
+ MERGE_STEP = `
8516
+ # 5. Auto-merge, per this repository's lurq policy. GitHub merges only
8517
+ # once the repo's OWN required checks pass; lurq never makes that call.
8518
+ # Requires "Allow auto-merge" in repository settings.
8519
+ - name: Enable auto-merge
8520
+ if: env.LURQ_MODE == 'pr' && steps.pr.outputs.pull-request-number
8521
+ env:
8522
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
8523
+ run: gh pr merge --auto --squash "\${{ steps.pr.outputs.pull-request-number }}"
8524
+ `;
8055
8525
  }
8056
8526
  });
8057
8527
 
@@ -8148,6 +8618,7 @@ var init_manifests = __esm({
8148
8618
  var upgradePlan_exports = {};
8149
8619
  __export(upgradePlan_exports, {
8150
8620
  buildUpgradePlan: () => buildUpgradePlan,
8621
+ detectRepoFullName: () => detectRepoFullName,
8151
8622
  findManifests: () => findManifests,
8152
8623
  formatUpgradePlan: () => formatUpgradePlan,
8153
8624
  mergedDeps: () => mergedDeps,
@@ -8201,27 +8672,38 @@ function mergedDeps(manifests) {
8201
8672
  }
8202
8673
  return out;
8203
8674
  }
8675
+ function detectRepoFullName(explicit) {
8676
+ const value = explicit ?? process.env.GITHUB_REPOSITORY ?? null;
8677
+ return value && /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
8678
+ }
8204
8679
  async function buildUpgradePlan(dir, opts = {}) {
8205
8680
  const manifests = readManifests(dir);
8206
8681
  const deps = mergedDeps(manifests);
8207
8682
  if (Object.keys(deps).length === 0) {
8208
8683
  throw new Error(`No package.json with dependencies found under ${dir}`);
8209
8684
  }
8210
- const plan = await fetchUpgradePlan(deps, opts);
8685
+ const plan = await fetchUpgradePlan(deps, { ...opts, repo: detectRepoFullName(opts.repo) });
8211
8686
  return { ...plan, manifests: manifests.length, deps: Object.keys(deps).length };
8212
8687
  }
8213
8688
  function formatUpgradePlan(plan) {
8214
8689
  const out = [
8215
- `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`,
8216
- ""
8690
+ `lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`
8217
8691
  ];
8692
+ if (plan.scopeSource === "repo-policy") {
8693
+ out.push(
8694
+ `policy: scope=${plan.scope}` + (plan.outOfScope ? `, ${plan.outOfScope} upgrade(s) held back` : ", nothing held back")
8695
+ );
8696
+ }
8697
+ out.push("");
8218
8698
  if (plan.upgrades.length === 0) {
8219
8699
  out.push("Everything lurq tracks is already on its latest release.");
8220
8700
  }
8221
8701
  for (const upgrade of plan.upgrades) {
8702
+ const held = upgrade.inScope === false ? " [HELD]" : "";
8222
8703
  out.push(
8223
- `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "")
8704
+ `${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "") + held
8224
8705
  );
8706
+ if (upgrade.scopeReason) out.push(` held: ${upgrade.scopeReason}`);
8225
8707
  if (upgrade.removed.length) {
8226
8708
  out.push(` removes ${upgrade.removed.length}: ${upgrade.removed.slice(0, 8).join(", ")}`);
8227
8709
  }
@@ -9367,7 +9849,7 @@ function targetsFromPlanFile(path2) {
9367
9849
  if (!Array.isArray(parsed.upgrades)) {
9368
9850
  throw new Error(`${path2} is not an upgrade plan (no "upgrades" array)`);
9369
9851
  }
9370
- return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
9852
+ 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
9853
  }
9372
9854
  async function runCheckUpgrade(dir, opts) {
9373
9855
  const { scanReferences: scanReferences2 } = await Promise.resolve().then(() => (init_references(), references_exports));
@@ -9422,7 +9904,7 @@ var init_graph = __esm({
9422
9904
  });
9423
9905
 
9424
9906
  // src/db/surface.ts
9425
- import { and as and9, desc as desc5, eq as eq13, sql as sql9 } from "drizzle-orm";
9907
+ import { and as and9, desc as desc5, eq as eq13, sql as sql11 } from "drizzle-orm";
9426
9908
  function surfaceRef(pkg, version) {
9427
9909
  return { kind: "package_surface", namespace: "npm", name: pkg, version };
9428
9910
  }
@@ -9705,22 +10187,22 @@ var init_toolDescriptions = __esm({
9705
10187
  });
9706
10188
 
9707
10189
  // src/db/usage.ts
9708
- import { and as and11, desc as desc7, eq as eq15, gte, sql as sql10 } from "drizzle-orm";
10190
+ import { and as and11, desc as desc7, eq as eq15, gte, sql as sql12 } from "drizzle-orm";
9709
10191
  function windowStart(days) {
9710
- return sql10`CURRENT_DATE - (${days}::int - 1)`;
10192
+ return sql12`CURRENT_DATE - (${days}::int - 1)`;
9711
10193
  }
9712
10194
  async function recordUsage(db, ownerId, tool) {
9713
10195
  if (!ownerId) return;
9714
10196
  try {
9715
- await db.insert(ownerUsageDaily).values({ ownerId, date: sql10`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
10197
+ await db.insert(ownerUsageDaily).values({ ownerId, date: sql12`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
9716
10198
  target: [ownerUsageDaily.ownerId, ownerUsageDaily.date, ownerUsageDaily.tool],
9717
- set: { count: sql10`${ownerUsageDaily.count} + 1` }
10199
+ set: { count: sql12`${ownerUsageDaily.count} + 1` }
9718
10200
  });
9719
10201
  } catch {
9720
10202
  }
9721
10203
  }
9722
10204
  async function getUsageSummary(db, ownerId, days) {
9723
- const rows = await db.execute(sql10`
10205
+ const rows = await db.execute(sql12`
9724
10206
  select
9725
10207
  to_char(d, 'YYYY-MM-DD') as date,
9726
10208
  coalesce(sum(u.count), 0)::int as count
@@ -9737,13 +10219,13 @@ async function getUsageSummary(db, ownerId, days) {
9737
10219
  async function getUsageByTool(db, ownerId, days) {
9738
10220
  const rows = await db.select({
9739
10221
  tool: ownerUsageDaily.tool,
9740
- count: sql10`sum(${ownerUsageDaily.count})::int`
10222
+ count: sql12`sum(${ownerUsageDaily.count})::int`
9741
10223
  }).from(ownerUsageDaily).where(
9742
10224
  and11(
9743
10225
  eq15(ownerUsageDaily.ownerId, ownerId),
9744
10226
  gte(ownerUsageDaily.date, windowStart(days))
9745
10227
  )
9746
- ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql10`sum(${ownerUsageDaily.count})`));
10228
+ ).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql12`sum(${ownerUsageDaily.count})`));
9747
10229
  return rows.map((r) => ({ tool: r.tool, count: Number(r.count) }));
9748
10230
  }
9749
10231
  var init_usage = __esm({
@@ -10264,6 +10746,52 @@ var init_brief = __esm({
10264
10746
  }
10265
10747
  });
10266
10748
 
10749
+ // src/github/scope.ts
10750
+ function isUnassessed(u) {
10751
+ return u.verdict === "unknown";
10752
+ }
10753
+ function breaksSomething(u) {
10754
+ return u.verdict === "removes-exports" || u.verdict === "arity-changed";
10755
+ }
10756
+ function scopeVerdict(u, scope) {
10757
+ if (scope === "all") return { inScope: true };
10758
+ if (u.advisories > 0) return { inScope: true };
10759
+ if (scope === "security") {
10760
+ return { inScope: false, reason: "no advisory \u2014 scope is security-only" };
10761
+ }
10762
+ if (isUnassessed(u)) {
10763
+ return { inScope: false, reason: "surface not yet analysed \u2014 nothing for the agent to rewrite" };
10764
+ }
10765
+ if (breaksSomething(u)) return { inScope: true };
10766
+ return { inScope: false, reason: "nothing referenced breaks \u2014 scope is security + blocking" };
10767
+ }
10768
+ function applyScope(upgrades, policy) {
10769
+ if (!policy) {
10770
+ return {
10771
+ upgrades: upgrades.map((u) => ({ ...u, inScope: true })),
10772
+ scope: "all",
10773
+ scopeSource: "unconnected",
10774
+ outOfScope: 0
10775
+ };
10776
+ }
10777
+ const scoped = upgrades.map((u) => {
10778
+ const verdict = scopeVerdict(u, policy.scope);
10779
+ return verdict.inScope ? { ...u, inScope: true } : { ...u, inScope: false, scopeReason: verdict.reason };
10780
+ });
10781
+ return {
10782
+ upgrades: scoped,
10783
+ scope: policy.scope,
10784
+ scopeSource: "repo-policy",
10785
+ outOfScope: scoped.filter((u) => !u.inScope).length
10786
+ };
10787
+ }
10788
+ var init_scope = __esm({
10789
+ "src/github/scope.ts"() {
10790
+ "use strict";
10791
+ init_esm_shims();
10792
+ }
10793
+ });
10794
+
10267
10795
  // src/github/runs.ts
10268
10796
  function str2(value, max) {
10269
10797
  if (typeof value !== "string") return null;
@@ -10280,7 +10808,7 @@ function strList(value, maxItems, maxLen) {
10280
10808
  }
10281
10809
  return out;
10282
10810
  }
10283
- function count(value) {
10811
+ function count2(value) {
10284
10812
  const n = typeof value === "number" ? value : Number(value);
10285
10813
  return Number.isInteger(n) && n >= 0 && n < 1e6 ? n : null;
10286
10814
  }
@@ -10304,9 +10832,9 @@ function parseUpgradeRun(input) {
10304
10832
  severity: severity2,
10305
10833
  status,
10306
10834
  symbolsAffected: strList(raw.symbolsAffected, MAX_SYMBOLS, MAX_NAME),
10307
- callSites: count(raw.callSites) ?? 0,
10835
+ callSites: count2(raw.callSites) ?? 0,
10308
10836
  callSiteFiles: files,
10309
- filesChanged: count(raw.filesChanged),
10837
+ filesChanged: count2(raw.filesChanged),
10310
10838
  testsPassed: typeof raw.testsPassed === "boolean" ? raw.testsPassed : null,
10311
10839
  prUrl: str2(raw.prUrl, MAX_URL),
10312
10840
  // Empty string, not null: it is part of the dedup key (see schema).
@@ -10324,6 +10852,11 @@ function parseUpgradeRuns(input, max) {
10324
10852
  }
10325
10853
  return { runs, rejected };
10326
10854
  }
10855
+ function parseRepoFullName(input) {
10856
+ const value = str2(input, MAX_NAME * 2);
10857
+ if (!value) return null;
10858
+ return /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
10859
+ }
10327
10860
  function parseDepsInput(input) {
10328
10861
  if (!input || typeof input !== "object") return {};
10329
10862
  const out = {};
@@ -10351,7 +10884,7 @@ var init_runs = __esm({
10351
10884
  });
10352
10885
 
10353
10886
  // src/db/upgradeRuns.ts
10354
- import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql11 } from "drizzle-orm";
10887
+ import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql13 } from "drizzle-orm";
10355
10888
  async function recordUpgradeRuns(db, rows) {
10356
10889
  if (rows.length === 0) return 0;
10357
10890
  await db.insert(upgradeRuns).values(rows).onConflictDoUpdate({
@@ -10363,15 +10896,15 @@ async function recordUpgradeRuns(db, rows) {
10363
10896
  upgradeRuns.runUrl
10364
10897
  ],
10365
10898
  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`
10899
+ severity: sql13`excluded.severity`,
10900
+ status: sql13`excluded.status`,
10901
+ symbolsAffected: sql13`excluded.symbols_affected`,
10902
+ callSites: sql13`excluded.call_sites`,
10903
+ callSiteFiles: sql13`excluded.call_site_files`,
10904
+ filesChanged: sql13`excluded.files_changed`,
10905
+ testsPassed: sql13`excluded.tests_passed`,
10906
+ prUrl: sql13`excluded.pr_url`,
10907
+ repoId: sql13`excluded.repo_id`
10375
10908
  }
10376
10909
  });
10377
10910
  return rows.length;
@@ -10386,12 +10919,12 @@ async function listRunsForRepo(db, ownerId, repoId, limit = 50) {
10386
10919
  async function getUpgradeImpact(db, ownerId, days = 30) {
10387
10920
  const since = new Date(Date.now() - days * 864e5);
10388
10921
  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`
10922
+ analysed: sql13`count(*)::int`,
10923
+ blocking: sql13`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
10924
+ callSites: sql13`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
10925
+ prsOpened: sql13`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
10926
+ merged: sql13`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
10927
+ unverified: sql13`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
10395
10928
  }).from(upgradeRuns).where(and13(eq17(upgradeRuns.ownerId, ownerId), gte2(upgradeRuns.createdAt, since)));
10396
10929
  return rows[0] ?? {
10397
10930
  analysed: 0,
@@ -10946,7 +11479,8 @@ async function startHttpServer(opts = {}) {
10946
11479
  const runs = await listRunsForRepo(db, ownerId, row.id);
10947
11480
  const workflow = renderWorkflow({
10948
11481
  installCommand: row.installCommand ?? void 0,
10949
- armed: row.policy.enabled
11482
+ armed: row.policy.enabled,
11483
+ autoMerge: row.policy.autoMerge
10950
11484
  });
10951
11485
  res.status(200).json({
10952
11486
  repo: {
@@ -11076,8 +11610,13 @@ async function startHttpServer(opts = {}) {
11076
11610
  try {
11077
11611
  const drift = await computeDrift(db, [{ path: "package.json", deps }]);
11078
11612
  const brief = await briefRepo(db, drift);
11613
+ const ownerId = req.lurqKey?.ownerId ?? null;
11614
+ const repoFullName = parseRepoFullName((req.body ?? {}).repo);
11615
+ const policy = ownerId && repoFullName ? await findPolicyByFullName(db, ownerId, repoFullName) : null;
11616
+ const scoped = applyScope(brief.upgrades, policy);
11079
11617
  res.status(200).json({
11080
11618
  ...brief,
11619
+ ...scoped,
11081
11620
  // Surfaced so CI can log what lurq had no opinion on. An upgrade we do
11082
11621
  // not know about must not look like an upgrade we cleared.
11083
11622
  untracked: Object.keys(deps).length - drift.depsTracked
@@ -11158,6 +11697,7 @@ var init_http2 = __esm({
11158
11697
  init_app();
11159
11698
  init_brief();
11160
11699
  init_drift();
11700
+ init_scope();
11161
11701
  init_runs();
11162
11702
  init_upgradeRuns();
11163
11703
  init_manifests();
@@ -11249,9 +11789,9 @@ function buildProgram() {
11249
11789
  const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
11250
11790
  await runPlan2(file, opts);
11251
11791
  });
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) => {
11792
+ 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
11793
  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 });
11794
+ const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey, repo: opts.repo });
11255
11795
  console.log(opts.json ? JSON.stringify(plan, null, 2) : formatUpgradePlan2(plan));
11256
11796
  });
11257
11797
  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(