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