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