lurqrun 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -75
- package/dist/bin/lurq.js +667 -127
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +667 -127
- 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 +2 -2
package/dist/bin/lurq.js
CHANGED
|
@@ -18,15 +18,119 @@ var init_esm_shims = __esm({
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
// package.json
|
|
22
|
+
var package_default;
|
|
23
|
+
var init_package = __esm({
|
|
24
|
+
"package.json"() {
|
|
25
|
+
package_default = {
|
|
26
|
+
name: "lurqrun",
|
|
27
|
+
version: "0.0.10",
|
|
28
|
+
description: "A continuously-updated, evidence-scored index of JS/TS frameworks and libraries, exposed as an MCP server, CLI, and agent skill \u2014 so AI coding assistants get fresh, objective dependency recommendations.",
|
|
29
|
+
type: "module",
|
|
30
|
+
license: "Apache-2.0",
|
|
31
|
+
author: "jadenryu",
|
|
32
|
+
repository: {
|
|
33
|
+
type: "git",
|
|
34
|
+
url: "git+https://github.com/jadenryu/lurq.git"
|
|
35
|
+
},
|
|
36
|
+
homepage: "https://github.com/jadenryu/lurq#readme",
|
|
37
|
+
bugs: {
|
|
38
|
+
url: "https://github.com/jadenryu/lurq/issues"
|
|
39
|
+
},
|
|
40
|
+
keywords: [
|
|
41
|
+
"mcp",
|
|
42
|
+
"model-context-protocol",
|
|
43
|
+
"npm",
|
|
44
|
+
"dependencies",
|
|
45
|
+
"recommendations",
|
|
46
|
+
"claude-code",
|
|
47
|
+
"ai-coding"
|
|
48
|
+
],
|
|
49
|
+
bin: {
|
|
50
|
+
lurq: "dist/bin/lurq.js"
|
|
51
|
+
},
|
|
52
|
+
workspaces: [
|
|
53
|
+
"apps/*"
|
|
54
|
+
],
|
|
55
|
+
files: [
|
|
56
|
+
"dist",
|
|
57
|
+
"src/data/seed.json",
|
|
58
|
+
"templates",
|
|
59
|
+
"drizzle",
|
|
60
|
+
"README.md"
|
|
61
|
+
],
|
|
62
|
+
engines: {
|
|
63
|
+
node: ">=20"
|
|
64
|
+
},
|
|
65
|
+
scripts: {
|
|
66
|
+
build: "tsup",
|
|
67
|
+
dev: "tsx src/bin/lurq.ts",
|
|
68
|
+
lurq: "tsx src/bin/lurq.ts",
|
|
69
|
+
"dev:apps": "npm run dev --workspace @lurq/docs & npm run dev --workspace @lurq/web",
|
|
70
|
+
"benchmark:validate": "tsx src/bin/benchmark.ts validate",
|
|
71
|
+
"benchmark:run": "tsx src/bin/benchmark.ts run",
|
|
72
|
+
start: "node dist/bin/lurq.js",
|
|
73
|
+
test: "vitest run",
|
|
74
|
+
"test:watch": "vitest",
|
|
75
|
+
typecheck: "tsc --noEmit",
|
|
76
|
+
lint: "eslint .",
|
|
77
|
+
format: "prettier --write .",
|
|
78
|
+
"format:check": "prettier --check .",
|
|
79
|
+
"db:generate": "drizzle-kit generate",
|
|
80
|
+
"db:migrate": "tsx src/bin/operator.ts db migrate",
|
|
81
|
+
operator: "tsx src/bin/operator.ts",
|
|
82
|
+
"operator:prod": "LURQ_ENV_FILE=.env.production tsx src/bin/operator.ts",
|
|
83
|
+
prepublishOnly: "npm test && npm run build",
|
|
84
|
+
"content:drift": "tsx scripts/gen-drift.ts"
|
|
85
|
+
},
|
|
86
|
+
dependencies: {
|
|
87
|
+
"@inquirer/prompts": "^8.5.2",
|
|
88
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
89
|
+
commander: "^12.1.0",
|
|
90
|
+
dotenv: "^16.4.5",
|
|
91
|
+
"drizzle-orm": "^0.45.2",
|
|
92
|
+
express: "^5.2.1",
|
|
93
|
+
"express-rate-limit": "^8.5.2",
|
|
94
|
+
helmet: "^8.2.0",
|
|
95
|
+
ioredis: "^5.11.1",
|
|
96
|
+
postgres: "^3.4.9",
|
|
97
|
+
"rate-limit-redis": "^4.3.1",
|
|
98
|
+
semver: "^7.8.5",
|
|
99
|
+
typescript: "^5.5.4",
|
|
100
|
+
"update-notifier": "^7.3.1",
|
|
101
|
+
zod: "^3.23.8"
|
|
102
|
+
},
|
|
103
|
+
devDependencies: {
|
|
104
|
+
"@eslint/js": "^9.9.0",
|
|
105
|
+
"@types/express": "^5.0.6",
|
|
106
|
+
"@types/node": "^22.5.0",
|
|
107
|
+
"@types/semver": "^7.7.1",
|
|
108
|
+
"drizzle-kit": "^0.31.10",
|
|
109
|
+
e2b: "^2.31.0",
|
|
110
|
+
eslint: "^9.9.0",
|
|
111
|
+
marked: "^18.0.9",
|
|
112
|
+
"pdfjs-dist": "^6.2.108",
|
|
113
|
+
playwright: "^1.62.1",
|
|
114
|
+
prettier: "^3.3.3",
|
|
115
|
+
tsup: "^8.2.4",
|
|
116
|
+
tsx: "^4.19.0",
|
|
117
|
+
"typescript-eslint": "^8.3.0",
|
|
118
|
+
vitest: "^2.0.5"
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
21
124
|
// src/core/constants.ts
|
|
22
125
|
var SERVER_NAME, PACKAGE_NAME, VERSION, DEFAULT_ENDPOINT, API_KEY_PREFIX, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
|
|
23
126
|
var init_constants = __esm({
|
|
24
127
|
"src/core/constants.ts"() {
|
|
25
128
|
"use strict";
|
|
26
129
|
init_esm_shims();
|
|
130
|
+
init_package();
|
|
27
131
|
SERVER_NAME = "lurq";
|
|
28
132
|
PACKAGE_NAME = "lurqrun";
|
|
29
|
-
VERSION =
|
|
133
|
+
VERSION = package_default.version;
|
|
30
134
|
DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
|
|
31
135
|
API_KEY_PREFIX = "lurq_live_";
|
|
32
136
|
EMBEDDING_DIM = 1536;
|
|
@@ -1112,6 +1216,10 @@ var init_schema = __esm({
|
|
|
1112
1216
|
peerDependencies: jsonb("peer_dependencies").$type(),
|
|
1113
1217
|
peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
|
|
1114
1218
|
engines: jsonb("engines").$type(),
|
|
1219
|
+
/** Where this package runs, classified from its own manifest at ingest.
|
|
1220
|
+
* Resolves the `framework` category's frontend/backend ambiguity for the
|
|
1221
|
+
* architecture diagram. Null = the manifest did not say clearly enough. */
|
|
1222
|
+
runtimeTarget: text("runtime_target").$type(),
|
|
1115
1223
|
// Computed outputs
|
|
1116
1224
|
healthScore: integer("health_score"),
|
|
1117
1225
|
/** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
|
|
@@ -1229,7 +1337,13 @@ var init_schema = __esm({
|
|
|
1229
1337
|
{
|
|
1230
1338
|
packageName: text("package_name").notNull(),
|
|
1231
1339
|
version: text("version").notNull(),
|
|
1232
|
-
publishedAt: ts("published_at")
|
|
1340
|
+
publishedAt: ts("published_at"),
|
|
1341
|
+
/** Peers/engines declared by this exact version. Lifted from the registry
|
|
1342
|
+
* document at ingest (it already carries every version's manifest), so a
|
|
1343
|
+
* conflict check against the versions a repo actually RUNS is one indexed
|
|
1344
|
+
* query rather than a registry read per dependency. */
|
|
1345
|
+
peerDependencies: jsonb("peer_dependencies").$type(),
|
|
1346
|
+
engines: jsonb("engines").$type()
|
|
1233
1347
|
},
|
|
1234
1348
|
(table2) => [
|
|
1235
1349
|
primaryKey({ columns: [table2.packageName, table2.version] }),
|
|
@@ -1610,10 +1724,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
1610
1724
|
import postgres from "postgres";
|
|
1611
1725
|
function createDb(opts = {}) {
|
|
1612
1726
|
const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
|
|
1613
|
-
const
|
|
1727
|
+
const sql14 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
|
|
1614
1728
|
} });
|
|
1615
|
-
const db = drizzle(
|
|
1616
|
-
return { db, sql:
|
|
1729
|
+
const db = drizzle(sql14, { schema: schema_exports });
|
|
1730
|
+
return { db, sql: sql14, close: () => sql14.end() };
|
|
1617
1731
|
}
|
|
1618
1732
|
var init_client = __esm({
|
|
1619
1733
|
"src/db/client.ts"() {
|
|
@@ -1643,7 +1757,9 @@ async function upsertPackageVersions(db, name, versions) {
|
|
|
1643
1757
|
const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
|
|
1644
1758
|
packageName: name,
|
|
1645
1759
|
version: v.version,
|
|
1646
|
-
publishedAt: v.publishedAt
|
|
1760
|
+
publishedAt: v.publishedAt,
|
|
1761
|
+
peerDependencies: v.peerDependencies ?? null,
|
|
1762
|
+
engines: v.engines ?? null
|
|
1647
1763
|
}));
|
|
1648
1764
|
await db.insert(packageVersions).values(rows).onConflictDoNothing();
|
|
1649
1765
|
}
|
|
@@ -2158,6 +2274,7 @@ function parseNpmRegistry(json2) {
|
|
|
2158
2274
|
directDependenciesCount: countDeps(latestManifest?.dependencies),
|
|
2159
2275
|
hasProvenance: detectProvenance(latestManifest),
|
|
2160
2276
|
hasInstallScripts: detectInstallScripts(latestManifest),
|
|
2277
|
+
hasBrowserField: latestManifest?.browser != null,
|
|
2161
2278
|
peerDependencies: parseDepMap(latestManifest?.peerDependencies),
|
|
2162
2279
|
peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
|
|
2163
2280
|
engines: parseDepMap(latestManifest?.engines),
|
|
@@ -2193,7 +2310,13 @@ function parseVersionTimeline(json2) {
|
|
|
2193
2310
|
const versions = json2?.versions;
|
|
2194
2311
|
if (!versions || typeof versions !== "object") return [];
|
|
2195
2312
|
const time = json2?.time ?? {};
|
|
2196
|
-
return Object.keys(versions).map((version) => ({
|
|
2313
|
+
return Object.keys(versions).map((version) => ({
|
|
2314
|
+
version,
|
|
2315
|
+
publishedAt: toDate(time?.[version]),
|
|
2316
|
+
// Already in this document — no second fetch, no per-version round trip.
|
|
2317
|
+
peerDependencies: parseDepMap(versions[version]?.peerDependencies),
|
|
2318
|
+
engines: parseDepMap(versions[version]?.engines)
|
|
2319
|
+
})).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
|
|
2197
2320
|
}
|
|
2198
2321
|
function parseKeywords(value) {
|
|
2199
2322
|
if (!Array.isArray(value)) return [];
|
|
@@ -2773,18 +2896,21 @@ async function checkCompat(db, packages2, opts = {}) {
|
|
|
2773
2896
|
provenance: edge.provenance,
|
|
2774
2897
|
witnessCount: edge.witnessCount
|
|
2775
2898
|
}));
|
|
2899
|
+
const versionOf = new Map(members.map((m) => [m.name, m.version]));
|
|
2900
|
+
const matchesCheckedVersions = (edge) => edgeMatchesVersions(edge, versionOf);
|
|
2776
2901
|
for (const edge of edges) {
|
|
2777
2902
|
if (edge.status === "conflict") {
|
|
2903
|
+
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
2904
|
conflicts.push({
|
|
2779
2905
|
source: "sandbox",
|
|
2780
2906
|
packages: [edge.packageA, edge.packageB],
|
|
2781
|
-
detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
|
|
2907
|
+
detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox${at}`
|
|
2782
2908
|
});
|
|
2783
2909
|
}
|
|
2784
2910
|
}
|
|
2785
2911
|
const checkedNames = members.map((m) => m.name);
|
|
2786
2912
|
const provenCompatible = new Set(
|
|
2787
|
-
edges.filter((e) => e.status === "compatible").map((e) => pairKey(e.packageA, e.packageB))
|
|
2913
|
+
edges.filter((e) => e.status === "compatible" && matchesCheckedVersions(e)).map((e) => pairKey(e.packageA, e.packageB))
|
|
2788
2914
|
);
|
|
2789
2915
|
if (checkedNames.length >= 2 && !conflicts.length && !fullyCovered(checkedNames, provenCompatible)) {
|
|
2790
2916
|
await enqueueCompatVerify(db, checkedNames).catch(() => {
|
|
@@ -2806,6 +2932,11 @@ async function checkCompat(db, packages2, opts = {}) {
|
|
|
2806
2932
|
pairs: enumeratePairs(checkedNames, conflicts)
|
|
2807
2933
|
};
|
|
2808
2934
|
}
|
|
2935
|
+
function edgeMatchesVersions(edge, versionOf) {
|
|
2936
|
+
const a = versionOf.get(edge.packageA);
|
|
2937
|
+
const b = versionOf.get(edge.packageB);
|
|
2938
|
+
return a != null && b != null && a === edge.versionA && b === edge.versionB;
|
|
2939
|
+
}
|
|
2809
2940
|
function enumeratePairs(names2, conflicts) {
|
|
2810
2941
|
const byPair = /* @__PURE__ */ new Map();
|
|
2811
2942
|
for (const c of conflicts) {
|
|
@@ -3239,22 +3370,6 @@ var init_verification = __esm({
|
|
|
3239
3370
|
}
|
|
3240
3371
|
});
|
|
3241
3372
|
|
|
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
3373
|
// src/data/successors.json
|
|
3259
3374
|
var successors_default;
|
|
3260
3375
|
var init_successors = __esm({
|
|
@@ -3317,9 +3432,30 @@ var init_successors = __esm({
|
|
|
3317
3432
|
});
|
|
3318
3433
|
|
|
3319
3434
|
// src/core/successors.ts
|
|
3320
|
-
function lookupSuccessor(name) {
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3435
|
+
function lookupSuccessor(name, learned) {
|
|
3436
|
+
const key = name.toLowerCase();
|
|
3437
|
+
const curated = MAP[key];
|
|
3438
|
+
if (curated) return { name: curated.replacedBy, reason: curated.reason };
|
|
3439
|
+
return learned?.get(key) ?? null;
|
|
3440
|
+
}
|
|
3441
|
+
function buildLearnedSuccessors(observed, isSuperseded) {
|
|
3442
|
+
const best = /* @__PURE__ */ new Map();
|
|
3443
|
+
for (const row of observed) {
|
|
3444
|
+
if (!isSuperseded(row.from)) continue;
|
|
3445
|
+
const key = row.from.toLowerCase();
|
|
3446
|
+
const current = best.get(key);
|
|
3447
|
+
const better = !current || row.owners > current.owners || row.owners === current.owners && row.observations > current.observations;
|
|
3448
|
+
if (better) best.set(key, { to: row.to, owners: row.owners, observations: row.observations });
|
|
3449
|
+
}
|
|
3450
|
+
return new Map(
|
|
3451
|
+
[...best].map(([from, hit]) => [
|
|
3452
|
+
from,
|
|
3453
|
+
{
|
|
3454
|
+
name: hit.to,
|
|
3455
|
+
reason: `${hit.owners} teams moved here after ${from} failed for the same need`
|
|
3456
|
+
}
|
|
3457
|
+
])
|
|
3458
|
+
);
|
|
3323
3459
|
}
|
|
3324
3460
|
var MAP;
|
|
3325
3461
|
var init_successors2 = __esm({
|
|
@@ -3331,6 +3467,81 @@ var init_successors2 = __esm({
|
|
|
3331
3467
|
}
|
|
3332
3468
|
});
|
|
3333
3469
|
|
|
3470
|
+
// src/db/outcomes.ts
|
|
3471
|
+
import { count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
|
|
3472
|
+
import { alias } from "drizzle-orm/pg-core";
|
|
3473
|
+
async function recordOutcome(db, outcome) {
|
|
3474
|
+
await db.insert(recommendationOutcomes).values(outcome);
|
|
3475
|
+
}
|
|
3476
|
+
async function getFieldEvidence(db, names2) {
|
|
3477
|
+
if (names2 && names2.length === 0) return /* @__PURE__ */ new Map();
|
|
3478
|
+
const rows = await db.select({
|
|
3479
|
+
packageName: recommendationOutcomes.packageName,
|
|
3480
|
+
reports: count(),
|
|
3481
|
+
successes: sql5`count(*) FILTER (WHERE ${SUCCESS_PREDICATE})`.mapWith(Number)
|
|
3482
|
+
}).from(recommendationOutcomes).where(names2 ? inArray3(recommendationOutcomes.packageName, names2) : void 0).groupBy(recommendationOutcomes.packageName);
|
|
3483
|
+
return new Map(
|
|
3484
|
+
rows.map((r) => [r.packageName, { reports: Number(r.reports), successes: r.successes }])
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
async function learnSuccessions(db) {
|
|
3488
|
+
const failed = alias(recommendationOutcomes, "failed");
|
|
3489
|
+
const worked = alias(recommendationOutcomes, "worked");
|
|
3490
|
+
const rows = await db.select({
|
|
3491
|
+
from: failed.packageName,
|
|
3492
|
+
to: worked.packageName,
|
|
3493
|
+
owners: sql5`count(DISTINCT ${failed.ownerId})`.mapWith(Number),
|
|
3494
|
+
observations: sql5`count(*)`.mapWith(Number)
|
|
3495
|
+
}).from(failed).innerJoin(
|
|
3496
|
+
worked,
|
|
3497
|
+
sql5`${worked.ownerId} = ${failed.ownerId}
|
|
3498
|
+
AND ${worked.need} = ${failed.need}
|
|
3499
|
+
AND ${worked.packageName} <> ${failed.packageName}
|
|
3500
|
+
AND ${worked.createdAt} > ${failed.createdAt}`
|
|
3501
|
+
).where(
|
|
3502
|
+
sql5`${failed.need} IS NOT NULL
|
|
3503
|
+
AND ${failed.ownerId} IS NOT NULL
|
|
3504
|
+
AND NOT (${failed.accepted} AND (${failed.buildSignal} IS NULL OR ${failed.buildSignal} <> 'failed'))
|
|
3505
|
+
AND (${worked.accepted} AND (${worked.buildSignal} IS NULL OR ${worked.buildSignal} <> 'failed'))`
|
|
3506
|
+
).groupBy(failed.packageName, worked.packageName).having(sql5`count(DISTINCT ${failed.ownerId}) >= ${MIN_SUCCESSION_OWNERS}`);
|
|
3507
|
+
return rows.map((r) => ({
|
|
3508
|
+
from: r.from,
|
|
3509
|
+
to: r.to,
|
|
3510
|
+
owners: Number(r.owners),
|
|
3511
|
+
observations: Number(r.observations)
|
|
3512
|
+
}));
|
|
3513
|
+
}
|
|
3514
|
+
async function loadLearnedSuccessors(db, now = Date.now()) {
|
|
3515
|
+
if (learnedCache && now - learnedCache.at < LEARNED_TTL_MS) return learnedCache.value;
|
|
3516
|
+
const observed = await learnSuccessions(db);
|
|
3517
|
+
if (observed.length === 0) {
|
|
3518
|
+
learnedCache = { at: now, value: /* @__PURE__ */ new Map() };
|
|
3519
|
+
return learnedCache.value;
|
|
3520
|
+
}
|
|
3521
|
+
const names2 = [...new Set(observed.map((o) => o.from))];
|
|
3522
|
+
const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(inArray3(packages.name, names2));
|
|
3523
|
+
const dead = new Set(rows.filter((r) => r.deprecated || r.archived).map((r) => r.name));
|
|
3524
|
+
const value = buildLearnedSuccessors(observed, (name) => dead.has(name));
|
|
3525
|
+
learnedCache = { at: now, value };
|
|
3526
|
+
return value;
|
|
3527
|
+
}
|
|
3528
|
+
async function getOutcomesByOwner(db, ownerId, opts = {}) {
|
|
3529
|
+
return db.select().from(recommendationOutcomes).where(eq5(recommendationOutcomes.ownerId, ownerId)).orderBy(desc3(recommendationOutcomes.createdAt)).limit(opts.limit ?? 50);
|
|
3530
|
+
}
|
|
3531
|
+
var SUCCESS_PREDICATE, MIN_SUCCESSION_OWNERS, LEARNED_TTL_MS, learnedCache;
|
|
3532
|
+
var init_outcomes = __esm({
|
|
3533
|
+
"src/db/outcomes.ts"() {
|
|
3534
|
+
"use strict";
|
|
3535
|
+
init_esm_shims();
|
|
3536
|
+
init_successors2();
|
|
3537
|
+
init_schema();
|
|
3538
|
+
SUCCESS_PREDICATE = sql5`${recommendationOutcomes.accepted} AND (${recommendationOutcomes.buildSignal} IS NULL OR ${recommendationOutcomes.buildSignal} <> 'failed')`;
|
|
3539
|
+
MIN_SUCCESSION_OWNERS = 3;
|
|
3540
|
+
LEARNED_TTL_MS = 10 * 60 * 1e3;
|
|
3541
|
+
learnedCache = null;
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
|
|
3334
3545
|
// src/ingestion/sources/githubReadme.ts
|
|
3335
3546
|
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
3336
3547
|
for (const file of CANDIDATES) {
|
|
@@ -3727,7 +3938,7 @@ function resetWeights() {
|
|
|
3727
3938
|
resetWeightsCache();
|
|
3728
3939
|
return removed;
|
|
3729
3940
|
}
|
|
3730
|
-
var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
3941
|
+
var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
3731
3942
|
var init_weights = __esm({
|
|
3732
3943
|
"src/scoring/weights.ts"() {
|
|
3733
3944
|
"use strict";
|
|
@@ -3806,6 +4017,26 @@ var init_weights = __esm({
|
|
|
3806
4017
|
/** Bundle size at the category median maps to this score; smaller → higher. */
|
|
3807
4018
|
medianScore: 50
|
|
3808
4019
|
};
|
|
4020
|
+
FIELD = {
|
|
4021
|
+
/** Weight when evidence exists — ~9% of the blend (0.10 / 1.10). */
|
|
4022
|
+
weight: 0.1,
|
|
4023
|
+
/**
|
|
4024
|
+
* Bayesian shrinkage strength: how many prior observations the neutral rate is
|
|
4025
|
+
* worth. One failed report out of one is not a 0-scoring package, and a raw
|
|
4026
|
+
* success rate would say it is. At m=12, a single failure lands near 69 rather
|
|
4027
|
+
* than 0, and it takes real volume to move the number far — which is the
|
|
4028
|
+
* correct epistemics for a signal users can trivially poison.
|
|
4029
|
+
*/
|
|
4030
|
+
priorWeight: 12,
|
|
4031
|
+
/**
|
|
4032
|
+
* The prior success rate a package is assumed to have before anyone reports.
|
|
4033
|
+
* Set at the observed base rate of "installs and builds fine", not 0.5:
|
|
4034
|
+
* most packages do work, so 0.5 would penalise the first honest reporter.
|
|
4035
|
+
*/
|
|
4036
|
+
priorRate: 0.75,
|
|
4037
|
+
/** Reports at or above this cap the confidence note as fully-evidenced. */
|
|
4038
|
+
wellEvidencedReports: 25
|
|
4039
|
+
};
|
|
3809
4040
|
DEFAULT_WEIGHTS = {
|
|
3810
4041
|
health: { ...HEALTH_WEIGHTS },
|
|
3811
4042
|
composite: { ...COMPOSITE }
|
|
@@ -3973,17 +4204,24 @@ function computeQuality(input) {
|
|
|
3973
4204
|
if (components.length === 0) return null;
|
|
3974
4205
|
return weightedAverage(components);
|
|
3975
4206
|
}
|
|
4207
|
+
function computeFieldScore(evidence) {
|
|
4208
|
+
if (!evidence || evidence.reports <= 0) return null;
|
|
4209
|
+
const { reports, successes } = evidence;
|
|
4210
|
+
const m = FIELD.priorWeight;
|
|
4211
|
+
return clamp(Math.round(100 * (successes + m * FIELD.priorRate) / (reports + m)));
|
|
4212
|
+
}
|
|
3976
4213
|
function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
|
|
3977
|
-
const { maintenance, adoption, reliability, efficiency } = breakdown;
|
|
3978
4214
|
const w = weights;
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
4215
|
+
return weightedAverage(
|
|
4216
|
+
[
|
|
4217
|
+
{ value: breakdown.maintenance, weight: w.maintenance },
|
|
4218
|
+
{ value: breakdown.adoption, weight: w.adoption },
|
|
4219
|
+
{ value: breakdown.reliability, weight: w.reliability },
|
|
4220
|
+
{ value: breakdown.efficiency, weight: w.efficiency },
|
|
4221
|
+
// `?? null` — a breakdown stored before field evidence shipped has the key
|
|
4222
|
+
// absent, and undefined must redistribute, not score 0.
|
|
4223
|
+
{ value: breakdown.field ?? null, weight: FIELD.weight }
|
|
4224
|
+
].filter((c) => c.value !== null)
|
|
3987
4225
|
);
|
|
3988
4226
|
}
|
|
3989
4227
|
function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
|
|
@@ -4228,7 +4466,7 @@ var init_types2 = __esm({
|
|
|
4228
4466
|
});
|
|
4229
4467
|
|
|
4230
4468
|
// src/db/repos.ts
|
|
4231
|
-
import { and as and5, eq as eq7, sql as
|
|
4469
|
+
import { and as and5, eq as eq7, sql as sql6 } from "drizzle-orm";
|
|
4232
4470
|
async function upsertRepos(db, rows) {
|
|
4233
4471
|
if (rows.length === 0) return 0;
|
|
4234
4472
|
await db.insert(repos).values(
|
|
@@ -4243,9 +4481,9 @@ async function upsertRepos(db, rows) {
|
|
|
4243
4481
|
).onConflictDoUpdate({
|
|
4244
4482
|
target: [repos.ownerId, repos.fullName],
|
|
4245
4483
|
set: {
|
|
4246
|
-
installationId:
|
|
4247
|
-
defaultBranch:
|
|
4248
|
-
isPrivate:
|
|
4484
|
+
installationId: sql6`excluded.installation_id`,
|
|
4485
|
+
defaultBranch: sql6`excluded.default_branch`,
|
|
4486
|
+
isPrivate: sql6`excluded.is_private`
|
|
4249
4487
|
}
|
|
4250
4488
|
});
|
|
4251
4489
|
return rows.length;
|
|
@@ -4255,7 +4493,7 @@ async function listRepos(db, ownerId) {
|
|
|
4255
4493
|
}
|
|
4256
4494
|
async function reposDeclaring(db, name) {
|
|
4257
4495
|
return db.select().from(repos).where(
|
|
4258
|
-
|
|
4496
|
+
sql6`exists (
|
|
4259
4497
|
select 1 from jsonb_array_elements(${repos.manifests}) as m
|
|
4260
4498
|
where jsonb_exists(m -> 'deps', ${name})
|
|
4261
4499
|
)`
|
|
@@ -4265,6 +4503,10 @@ async function getRepo(db, ownerId, id) {
|
|
|
4265
4503
|
const rows = await db.select().from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).limit(1);
|
|
4266
4504
|
return rows[0] ?? null;
|
|
4267
4505
|
}
|
|
4506
|
+
async function findPolicyByFullName(db, ownerId, fullName) {
|
|
4507
|
+
const rows = await db.select({ policy: repos.policy }).from(repos).where(and5(eq7(repos.ownerId, ownerId), eq7(repos.fullName, fullName))).limit(1);
|
|
4508
|
+
return rows[0]?.policy ?? null;
|
|
4509
|
+
}
|
|
4268
4510
|
async function saveScan(db, id, scan) {
|
|
4269
4511
|
await db.update(repos).set({
|
|
4270
4512
|
manifests: scan.manifests,
|
|
@@ -4350,8 +4592,76 @@ var init_attribute = __esm({
|
|
|
4350
4592
|
}
|
|
4351
4593
|
});
|
|
4352
4594
|
|
|
4595
|
+
// src/ingestion/sources/osv.ts
|
|
4596
|
+
function installKey(name, version) {
|
|
4597
|
+
return `${name}@${version}`;
|
|
4598
|
+
}
|
|
4599
|
+
async function queryVulnerableInstalls(installs, fetchImpl) {
|
|
4600
|
+
const affected = /* @__PURE__ */ new Map();
|
|
4601
|
+
if (installs.length === 0) return { affected, complete: true };
|
|
4602
|
+
const unique = [...new Map(installs.map((i) => [installKey(i.name, i.version), i])).values()];
|
|
4603
|
+
const batches = [];
|
|
4604
|
+
for (let i = 0; i < unique.length; i += BATCH) batches.push(unique.slice(i, i + BATCH));
|
|
4605
|
+
let complete = true;
|
|
4606
|
+
let cursor = 0;
|
|
4607
|
+
const worker = async () => {
|
|
4608
|
+
for (let i = cursor++; i < batches.length; i = cursor++) {
|
|
4609
|
+
const batch = batches[i];
|
|
4610
|
+
try {
|
|
4611
|
+
const { data } = await httpRequest(URL2, {
|
|
4612
|
+
host: HOST7,
|
|
4613
|
+
method: "POST",
|
|
4614
|
+
headers: { "content-type": "application/json" },
|
|
4615
|
+
body: JSON.stringify({
|
|
4616
|
+
queries: batch.map((q) => ({
|
|
4617
|
+
version: q.version,
|
|
4618
|
+
package: { name: q.name, ecosystem: "npm" }
|
|
4619
|
+
}))
|
|
4620
|
+
}),
|
|
4621
|
+
// Uncached on purpose. The shared cache keys on the URL, and every
|
|
4622
|
+
// batch here posts to the same one with a different body — a cache hit
|
|
4623
|
+
// would answer one tree's question with another tree's vulnerabilities.
|
|
4624
|
+
ttlMs: 0,
|
|
4625
|
+
// Best-effort by contract: the caller keeps its coarser answer when
|
|
4626
|
+
// this fails, so exhausting the default retry ladder would only stall
|
|
4627
|
+
// a scheduled scan behind an outage nobody is waiting on.
|
|
4628
|
+
retries: 1,
|
|
4629
|
+
fetchImpl
|
|
4630
|
+
});
|
|
4631
|
+
const results = data?.results ?? [];
|
|
4632
|
+
if (results.length !== batch.length) {
|
|
4633
|
+
complete = false;
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
results.forEach((result, j) => {
|
|
4637
|
+
const ids = (result?.vulns ?? []).map((v) => v?.id).filter((id) => typeof id === "string");
|
|
4638
|
+
if (ids.length === 0) return;
|
|
4639
|
+
const q = batch[j];
|
|
4640
|
+
affected.set(installKey(q.name, q.version), ids);
|
|
4641
|
+
});
|
|
4642
|
+
} catch {
|
|
4643
|
+
complete = false;
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
};
|
|
4647
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, batches.length) }, worker));
|
|
4648
|
+
return { affected, complete };
|
|
4649
|
+
}
|
|
4650
|
+
var HOST7, URL2, BATCH, CONCURRENCY;
|
|
4651
|
+
var init_osv = __esm({
|
|
4652
|
+
"src/ingestion/sources/osv.ts"() {
|
|
4653
|
+
"use strict";
|
|
4654
|
+
init_esm_shims();
|
|
4655
|
+
init_http();
|
|
4656
|
+
HOST7 = "api.osv.dev";
|
|
4657
|
+
URL2 = `https://${HOST7}/v1/querybatch`;
|
|
4658
|
+
BATCH = 250;
|
|
4659
|
+
CONCURRENCY = 4;
|
|
4660
|
+
}
|
|
4661
|
+
});
|
|
4662
|
+
|
|
4353
4663
|
// src/github/drift.ts
|
|
4354
|
-
import { inArray as
|
|
4664
|
+
import { inArray as inArray4, sql as sql7 } from "drizzle-orm";
|
|
4355
4665
|
import semver2 from "semver";
|
|
4356
4666
|
function declaredDeps(manifests) {
|
|
4357
4667
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4378,7 +4688,7 @@ async function loadIndexed(db, names2) {
|
|
|
4378
4688
|
latestVersion: packages.latestVersion,
|
|
4379
4689
|
deprecated: packages.deprecated,
|
|
4380
4690
|
advisories: packages.advisories
|
|
4381
|
-
}).from(packages).where(
|
|
4691
|
+
}).from(packages).where(inArray4(packages.name, names2.slice(i, i + NAME_CHUNK)));
|
|
4382
4692
|
for (const row of rows) {
|
|
4383
4693
|
out.set(row.name, {
|
|
4384
4694
|
latestVersion: row.latestVersion,
|
|
@@ -4392,7 +4702,7 @@ async function loadIndexed(db, names2) {
|
|
|
4392
4702
|
async function loadVersions(db, names2) {
|
|
4393
4703
|
const out = /* @__PURE__ */ new Map();
|
|
4394
4704
|
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(
|
|
4705
|
+
const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray4(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
|
|
4396
4706
|
for (const row of rows) {
|
|
4397
4707
|
const list = out.get(row.name);
|
|
4398
4708
|
if (list) list.push(row.version);
|
|
@@ -4442,19 +4752,28 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
|
|
|
4442
4752
|
};
|
|
4443
4753
|
if (transitives.length === 0) return empty2;
|
|
4444
4754
|
const indexed = await loadIndexed(db, [...new Set(transitives.map((d) => d.name))]);
|
|
4755
|
+
const { affected, complete } = await queryVulnerableInstalls(
|
|
4756
|
+
transitives.map((d) => ({ name: d.name, version: d.version }))
|
|
4757
|
+
);
|
|
4445
4758
|
const risks = [];
|
|
4446
4759
|
let tracked = 0;
|
|
4447
4760
|
for (const dep of transitives) {
|
|
4448
4761
|
const row = indexed.get(dep.name);
|
|
4449
4762
|
if (!row) continue;
|
|
4450
4763
|
tracked++;
|
|
4451
|
-
|
|
4764
|
+
const vulnIds = affected.get(installKey(dep.name, dep.version)) ?? [];
|
|
4765
|
+
if (row.advisories === 0 && !row.deprecated && vulnIds.length === 0) continue;
|
|
4452
4766
|
risks.push({
|
|
4453
4767
|
name: dep.name,
|
|
4454
4768
|
version: dep.version,
|
|
4455
4769
|
latest: row.latestVersion,
|
|
4456
4770
|
advisories: row.advisories,
|
|
4457
4771
|
deprecated: row.deprecated,
|
|
4772
|
+
// Empty when OSV cleared this version, which is the actionable difference
|
|
4773
|
+
// from `advisories`: the package has a history, this install does not.
|
|
4774
|
+
// Undefined when the lookup could not complete — never an empty array,
|
|
4775
|
+
// which would read as a clean bill of health nobody established.
|
|
4776
|
+
vulnerabilities: complete ? vulnIds : void 0,
|
|
4458
4777
|
pulledInBy: attribution.parentsOf.get(dep.name) ?? []
|
|
4459
4778
|
});
|
|
4460
4779
|
}
|
|
@@ -4465,6 +4784,10 @@ async function computeTransitiveDrift(db, resolved, directNames, truncated, edge
|
|
|
4465
4784
|
resolved: transitives.length,
|
|
4466
4785
|
tracked,
|
|
4467
4786
|
advisoryPackages: risks.filter((r) => r.advisories > 0).length,
|
|
4787
|
+
// The number that can actually be acted on: installs OSV matched to a
|
|
4788
|
+
// vulnerability at their exact version. Undefined when the lookup did not
|
|
4789
|
+
// complete, so "not checked" never renders as zero.
|
|
4790
|
+
vulnerableInstalls: complete ? risks.filter((r) => r.vulnerabilities?.length).length : void 0,
|
|
4468
4791
|
deprecated: risks.filter((r) => r.deprecated).length,
|
|
4469
4792
|
risks: risks.slice(0, TRANSITIVE_DETAIL_CAP),
|
|
4470
4793
|
truncated,
|
|
@@ -4476,6 +4799,29 @@ async function conflictsAtLatest(db, names2) {
|
|
|
4476
4799
|
const { members } = await assembleMembers(db, names2);
|
|
4477
4800
|
return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
|
|
4478
4801
|
}
|
|
4802
|
+
async function conflictsAtCurrent(db, deps) {
|
|
4803
|
+
const pinned = deps.filter((d) => Boolean(d.resolved));
|
|
4804
|
+
if (pinned.length === 0) return [];
|
|
4805
|
+
const rows = await db.select({
|
|
4806
|
+
packageName: packageVersions.packageName,
|
|
4807
|
+
version: packageVersions.version,
|
|
4808
|
+
peerDependencies: packageVersions.peerDependencies,
|
|
4809
|
+
engines: packageVersions.engines
|
|
4810
|
+
}).from(packageVersions).where(
|
|
4811
|
+
inArray4(
|
|
4812
|
+
sql7`(${packageVersions.packageName}, ${packageVersions.version})`,
|
|
4813
|
+
pinned.map((d) => sql7`(${d.name}, ${d.resolved})`)
|
|
4814
|
+
)
|
|
4815
|
+
);
|
|
4816
|
+
const members = rows.map((r) => ({
|
|
4817
|
+
name: r.packageName,
|
|
4818
|
+
version: r.version,
|
|
4819
|
+
peerDependencies: r.peerDependencies,
|
|
4820
|
+
peerDependenciesMeta: null,
|
|
4821
|
+
engines: r.engines
|
|
4822
|
+
}));
|
|
4823
|
+
return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
|
|
4824
|
+
}
|
|
4479
4825
|
async function computeDrift(db, manifests, resolvedTree = null) {
|
|
4480
4826
|
const declared = declaredDeps(manifests);
|
|
4481
4827
|
const names2 = [...declared.keys()];
|
|
@@ -4497,7 +4843,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
|
|
|
4497
4843
|
advisories: 0,
|
|
4498
4844
|
deps: [],
|
|
4499
4845
|
transitive,
|
|
4500
|
-
conflictsAtLatest: []
|
|
4846
|
+
conflictsAtLatest: [],
|
|
4847
|
+
conflictsAtCurrent: []
|
|
4501
4848
|
};
|
|
4502
4849
|
}
|
|
4503
4850
|
const [indexed, versions] = await Promise.all([
|
|
@@ -4511,7 +4858,10 @@ async function computeDrift(db, manifests, resolvedTree = null) {
|
|
|
4511
4858
|
deps.push(depDrift(name, entry, row, versions.get(name) ?? []));
|
|
4512
4859
|
}
|
|
4513
4860
|
deps.sort((a, b) => severity(b) - severity(a) || a.name.localeCompare(b.name));
|
|
4514
|
-
const conflicts = await
|
|
4861
|
+
const [conflicts, currentConflicts] = await Promise.all([
|
|
4862
|
+
conflictsAtLatest(db, deps.map((d) => d.name)),
|
|
4863
|
+
conflictsAtCurrent(db, deps)
|
|
4864
|
+
]);
|
|
4515
4865
|
return {
|
|
4516
4866
|
depsDeclared: declared.size,
|
|
4517
4867
|
depsTracked: deps.length,
|
|
@@ -4521,7 +4871,8 @@ async function computeDrift(db, manifests, resolvedTree = null) {
|
|
|
4521
4871
|
advisories: deps.reduce((sum, d) => sum + d.advisories, 0),
|
|
4522
4872
|
deps: deps.slice(0, REPO_DRIFT_DETAIL_CAP),
|
|
4523
4873
|
transitive,
|
|
4524
|
-
conflictsAtLatest: conflicts
|
|
4874
|
+
conflictsAtLatest: conflicts,
|
|
4875
|
+
conflictsAtCurrent: currentConflicts
|
|
4525
4876
|
};
|
|
4526
4877
|
}
|
|
4527
4878
|
var NAME_CHUNK;
|
|
@@ -4532,6 +4883,7 @@ var init_drift = __esm({
|
|
|
4532
4883
|
init_schema();
|
|
4533
4884
|
init_types2();
|
|
4534
4885
|
init_attribute();
|
|
4886
|
+
init_osv();
|
|
4535
4887
|
init_members();
|
|
4536
4888
|
init_peerCompat();
|
|
4537
4889
|
NAME_CHUNK = 500;
|
|
@@ -4632,6 +4984,82 @@ var init_errors = __esm({
|
|
|
4632
4984
|
}
|
|
4633
4985
|
});
|
|
4634
4986
|
|
|
4987
|
+
// src/core/runtimeTarget.ts
|
|
4988
|
+
function classifyRuntimeTarget(input) {
|
|
4989
|
+
const override = OVERRIDES[input.name.toLowerCase()];
|
|
4990
|
+
if (override) return override;
|
|
4991
|
+
const peers = Object.keys(input.peerDependencies ?? {});
|
|
4992
|
+
if (peers.some((p) => RENDERER_PEERS.has(p))) return "browser";
|
|
4993
|
+
const keywords = input.keywords.map((k) => k.toLowerCase());
|
|
4994
|
+
const server = keywords.filter((k) => SERVER_KEYWORDS.has(k)).length;
|
|
4995
|
+
const browser = keywords.filter((k) => BROWSER_KEYWORDS.has(k)).length;
|
|
4996
|
+
if (browser > server) return "browser";
|
|
4997
|
+
if (server > browser) return "node";
|
|
4998
|
+
if (input.hasBrowserField) return "browser";
|
|
4999
|
+
if (input.engines?.node && !input.hasBrowserField && browser === 0) return "node";
|
|
5000
|
+
return null;
|
|
5001
|
+
}
|
|
5002
|
+
var SERVER_KEYWORDS, BROWSER_KEYWORDS, RENDERER_PEERS, OVERRIDES;
|
|
5003
|
+
var init_runtimeTarget = __esm({
|
|
5004
|
+
"src/core/runtimeTarget.ts"() {
|
|
5005
|
+
"use strict";
|
|
5006
|
+
init_esm_shims();
|
|
5007
|
+
SERVER_KEYWORDS = /* @__PURE__ */ new Set([
|
|
5008
|
+
"server",
|
|
5009
|
+
"http-server",
|
|
5010
|
+
"backend",
|
|
5011
|
+
"middleware",
|
|
5012
|
+
"rest",
|
|
5013
|
+
"restful",
|
|
5014
|
+
"api",
|
|
5015
|
+
"router",
|
|
5016
|
+
"routing",
|
|
5017
|
+
"orm",
|
|
5018
|
+
"database",
|
|
5019
|
+
"sql",
|
|
5020
|
+
"graphql-server",
|
|
5021
|
+
"microservice",
|
|
5022
|
+
"websocket",
|
|
5023
|
+
"rpc"
|
|
5024
|
+
]);
|
|
5025
|
+
BROWSER_KEYWORDS = /* @__PURE__ */ new Set([
|
|
5026
|
+
"react",
|
|
5027
|
+
"vue",
|
|
5028
|
+
"svelte",
|
|
5029
|
+
"angular",
|
|
5030
|
+
"preact",
|
|
5031
|
+
"solid",
|
|
5032
|
+
"dom",
|
|
5033
|
+
"browser",
|
|
5034
|
+
"component",
|
|
5035
|
+
"components",
|
|
5036
|
+
"ui",
|
|
5037
|
+
"css",
|
|
5038
|
+
"styling",
|
|
5039
|
+
"animation",
|
|
5040
|
+
"frontend",
|
|
5041
|
+
"client-side",
|
|
5042
|
+
"jsx",
|
|
5043
|
+
"hooks"
|
|
5044
|
+
]);
|
|
5045
|
+
RENDERER_PEERS = /* @__PURE__ */ new Set(["react", "react-dom", "vue", "svelte", "@angular/core", "preact"]);
|
|
5046
|
+
OVERRIDES = {
|
|
5047
|
+
express: "node",
|
|
5048
|
+
fastify: "node",
|
|
5049
|
+
koa: "node",
|
|
5050
|
+
hono: "node",
|
|
5051
|
+
"@nestjs/core": "node",
|
|
5052
|
+
"@hapi/hapi": "node",
|
|
5053
|
+
// Meta-frameworks run in both places; they are the server in an architecture
|
|
5054
|
+
// diagram, which is the question this answers.
|
|
5055
|
+
next: "node",
|
|
5056
|
+
nuxt: "node",
|
|
5057
|
+
"@remix-run/node": "node",
|
|
5058
|
+
astro: "node"
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
});
|
|
5062
|
+
|
|
4635
5063
|
// src/pipeline/mineEdges.ts
|
|
4636
5064
|
function trackedPairs(nodes, tracked) {
|
|
4637
5065
|
const t = nodes.filter((n) => tracked.has(n.name));
|
|
@@ -4712,6 +5140,13 @@ function assemblePackageRow(p) {
|
|
|
4712
5140
|
peerDependencies: r?.peerDependencies ?? null,
|
|
4713
5141
|
peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
|
|
4714
5142
|
engines: r?.engines ?? null,
|
|
5143
|
+
runtimeTarget: r ? classifyRuntimeTarget({
|
|
5144
|
+
name: p.name,
|
|
5145
|
+
hasBrowserField: r.hasBrowserField,
|
|
5146
|
+
keywords: r.keywords,
|
|
5147
|
+
engines: r.engines,
|
|
5148
|
+
peerDependencies: r.peerDependencies
|
|
5149
|
+
}) : null,
|
|
4715
5150
|
healthScore: p.healthScore,
|
|
4716
5151
|
qualityScore: p.qualityScore,
|
|
4717
5152
|
confidence: p.confidence,
|
|
@@ -4732,6 +5167,7 @@ var init_sync = __esm({
|
|
|
4732
5167
|
init_errors();
|
|
4733
5168
|
init_http();
|
|
4734
5169
|
init_logger();
|
|
5170
|
+
init_runtimeTarget();
|
|
4735
5171
|
init_collect();
|
|
4736
5172
|
init_npmDownloads();
|
|
4737
5173
|
init_summarize();
|
|
@@ -4805,7 +5241,7 @@ var init_ingestQueue = __esm({
|
|
|
4805
5241
|
});
|
|
4806
5242
|
|
|
4807
5243
|
// src/pipeline/single.ts
|
|
4808
|
-
import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as
|
|
5244
|
+
import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
|
|
4809
5245
|
function raceTimeout(p, ms) {
|
|
4810
5246
|
let timer;
|
|
4811
5247
|
return Promise.race([
|
|
@@ -4821,7 +5257,7 @@ async function getSeedCategory(db, name) {
|
|
|
4821
5257
|
}
|
|
4822
5258
|
async function getCategoryMedianBundle(db, category) {
|
|
4823
5259
|
const [row] = await db.select({
|
|
4824
|
-
m:
|
|
5260
|
+
m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
4825
5261
|
}).from(packages).where(and6(eq8(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
|
|
4826
5262
|
return row?.m ?? null;
|
|
4827
5263
|
}
|
|
@@ -4853,12 +5289,14 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
4853
5289
|
const input = toScoringInput(signals, category);
|
|
4854
5290
|
const median2 = category ? await getCategoryMedianBundle(db, category) : null;
|
|
4855
5291
|
const quality = computeQuality(input);
|
|
5292
|
+
const evidence = await getFieldEvidence(db, [name]);
|
|
4856
5293
|
const breakdown = {
|
|
4857
5294
|
maintenance: computeMaintenance(input, now),
|
|
4858
5295
|
adoption: computeAdoption(input),
|
|
4859
5296
|
reliability: computeReliability(input),
|
|
4860
5297
|
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
|
|
4861
|
-
quality
|
|
5298
|
+
quality,
|
|
5299
|
+
field: computeFieldScore(evidence.get(name) ?? null)
|
|
4862
5300
|
};
|
|
4863
5301
|
const healthScore = computeHealthScore(breakdown);
|
|
4864
5302
|
const confidence = computeConfidence(input, now, quality);
|
|
@@ -4928,6 +5366,7 @@ var init_single = __esm({
|
|
|
4928
5366
|
init_categoryInference();
|
|
4929
5367
|
init_scoring();
|
|
4930
5368
|
init_embeddings();
|
|
5369
|
+
init_outcomes();
|
|
4931
5370
|
init_packages();
|
|
4932
5371
|
init_schema();
|
|
4933
5372
|
init_alerts2();
|
|
@@ -4939,7 +5378,7 @@ var init_single = __esm({
|
|
|
4939
5378
|
});
|
|
4940
5379
|
|
|
4941
5380
|
// src/search/recommend.ts
|
|
4942
|
-
import { and as and7, cosineDistance, eq as eq9, inArray as
|
|
5381
|
+
import { and as and7, cosineDistance, eq as eq9, inArray as inArray5, isNotNull as isNotNull4, lte, sql as sql9 } from "drizzle-orm";
|
|
4943
5382
|
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
4944
5383
|
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
4945
5384
|
const [queryVec] = await provider.embed([opts.need]);
|
|
@@ -4992,7 +5431,7 @@ function buildConditions(constraints, category) {
|
|
|
4992
5431
|
const allowed = ["proven", "emerging", "promising", "unproven"].filter(
|
|
4993
5432
|
(c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
|
|
4994
5433
|
);
|
|
4995
|
-
conditions.push(
|
|
5434
|
+
conditions.push(inArray5(packages.confidence, allowed));
|
|
4996
5435
|
}
|
|
4997
5436
|
return conditions;
|
|
4998
5437
|
}
|
|
@@ -5006,10 +5445,10 @@ async function runVectorQuery(db, queryVec, providerId, constraints, category, p
|
|
|
5006
5445
|
return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(distance).limit(pool);
|
|
5007
5446
|
}
|
|
5008
5447
|
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(
|
|
5448
|
+
const tsquery = sql9`websearch_to_tsquery('english', ${need})`;
|
|
5449
|
+
const rank2 = sql9`ts_rank(${packages.searchVector}, ${tsquery})`;
|
|
5450
|
+
const conditions = [sql9`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
|
|
5451
|
+
return db.select(ROW_COLUMNS).from(packages).where(and7(...conditions)).orderBy(sql9`${rank2} desc`).limit(pool);
|
|
5013
5452
|
}
|
|
5014
5453
|
function toCandidate(row) {
|
|
5015
5454
|
return {
|
|
@@ -5147,7 +5586,7 @@ var init_types3 = __esm({
|
|
|
5147
5586
|
});
|
|
5148
5587
|
|
|
5149
5588
|
// src/db/selectionPolicy.ts
|
|
5150
|
-
import { eq as eq10, inArray as
|
|
5589
|
+
import { eq as eq10, inArray as inArray6 } from "drizzle-orm";
|
|
5151
5590
|
async function getSelectionPolicy(db, ownerId) {
|
|
5152
5591
|
if (!ownerId) return DEFAULT_SELECTION_POLICY;
|
|
5153
5592
|
const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(eq10(selectionPolicies.ownerId, ownerId)).limit(1);
|
|
@@ -5166,7 +5605,7 @@ async function loadPolicyFacts(db, names2) {
|
|
|
5166
5605
|
name: packages.name,
|
|
5167
5606
|
license: packages.license,
|
|
5168
5607
|
deprecated: packages.deprecated
|
|
5169
|
-
}).from(packages).where(
|
|
5608
|
+
}).from(packages).where(inArray6(packages.name, names2));
|
|
5170
5609
|
for (const row of rows) {
|
|
5171
5610
|
out.set(row.name, { license: row.license, deprecated: row.deprecated });
|
|
5172
5611
|
}
|
|
@@ -5483,7 +5922,7 @@ __export(handlers_exports, {
|
|
|
5483
5922
|
rowToEvaluate: () => rowToEvaluate
|
|
5484
5923
|
});
|
|
5485
5924
|
import { createHash as createHash3 } from "crypto";
|
|
5486
|
-
import { sql as
|
|
5925
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
5487
5926
|
function isStale(dataAsOf) {
|
|
5488
5927
|
if (!dataAsOf) return true;
|
|
5489
5928
|
return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
|
|
@@ -5499,7 +5938,7 @@ function topAdvisories(advisories, max = 5) {
|
|
|
5499
5938
|
if (!advisories?.length) return [];
|
|
5500
5939
|
return [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).slice(0, max);
|
|
5501
5940
|
}
|
|
5502
|
-
function rowToEvaluate(row) {
|
|
5941
|
+
function rowToEvaluate(row, learned) {
|
|
5503
5942
|
const breakdown = row.scoreBreakdown ?? {
|
|
5504
5943
|
maintenance: 0,
|
|
5505
5944
|
adoption: 0,
|
|
@@ -5528,11 +5967,11 @@ function rowToEvaluate(row) {
|
|
|
5528
5967
|
summary: row.summary ? truncateSentences(row.summary, 3) : null,
|
|
5529
5968
|
usageGuide: row.usageGuide ?? null,
|
|
5530
5969
|
repoUrl: row.repoUrl,
|
|
5531
|
-
replacedBy: lookupSuccessor(row.name)
|
|
5970
|
+
replacedBy: lookupSuccessor(row.name, learned)
|
|
5532
5971
|
};
|
|
5533
5972
|
}
|
|
5534
5973
|
async function latestDataAsOf(db) {
|
|
5535
|
-
const [row] = await db.select({ m:
|
|
5974
|
+
const [row] = await db.select({ m: sql10`max(${packages.dataAsOf})` }).from(packages);
|
|
5536
5975
|
return new Date(row?.m ?? Date.now()).toISOString();
|
|
5537
5976
|
}
|
|
5538
5977
|
function cacheKey(parts) {
|
|
@@ -5575,7 +6014,8 @@ async function handleEvaluate(db, input, ownerId = null) {
|
|
|
5575
6014
|
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
6015
|
};
|
|
5577
6016
|
}
|
|
5578
|
-
const
|
|
6017
|
+
const learned = row.deprecated || row.archived ? await loadLearnedSuccessors(db) : void 0;
|
|
6018
|
+
const evaluated2 = rowToEvaluate(row, learned);
|
|
5579
6019
|
const verification = await getLatestVerificationByName(db, row.name);
|
|
5580
6020
|
return verification ? { ...evaluated2, buildVerified: toBuildVerified(verification) } : evaluated2;
|
|
5581
6021
|
},
|
|
@@ -5602,7 +6042,7 @@ async function handleCompare(db, input, ownerId = null) {
|
|
|
5602
6042
|
const results = await Promise.all(
|
|
5603
6043
|
input.packages.map((name) => getOrFetchPackage(db, name, { requestedByOwnerId: ownerId }))
|
|
5604
6044
|
);
|
|
5605
|
-
const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
|
|
6045
|
+
const rows = results.map((r) => r.row).filter((row) => row !== null).map((row) => rowToEvaluate(row)).sort((a, b) => b.healthScore - a.healthScore);
|
|
5606
6046
|
const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
|
|
5607
6047
|
return {
|
|
5608
6048
|
dataAsOf: await latestDataAsOf(db),
|
|
@@ -5887,7 +6327,8 @@ function parseRpcBody(raw) {
|
|
|
5887
6327
|
}
|
|
5888
6328
|
}
|
|
5889
6329
|
function fetchUpgradePlan(deps, opts = {}) {
|
|
5890
|
-
|
|
6330
|
+
const body = opts.repo ? { deps, repo: opts.repo } : { deps };
|
|
6331
|
+
return post("/upgrade-plan", body, opts);
|
|
5891
6332
|
}
|
|
5892
6333
|
var RemoteError;
|
|
5893
6334
|
var init_remote = __esm({
|
|
@@ -5982,10 +6423,10 @@ var init_optimize = __esm({
|
|
|
5982
6423
|
});
|
|
5983
6424
|
|
|
5984
6425
|
// src/mcp/diagram.ts
|
|
5985
|
-
import { inArray as
|
|
6426
|
+
import { inArray as inArray7 } from "drizzle-orm";
|
|
5986
6427
|
function layerFor(item) {
|
|
5987
6428
|
if (!item.category) return UNCLASSIFIED;
|
|
5988
|
-
if (item.category === "framework" &&
|
|
6429
|
+
if (item.category === "framework" && item.runtimeTarget === "node") return "Backend";
|
|
5989
6430
|
return LAYER_OF[item.category] ?? UNCLASSIFIED;
|
|
5990
6431
|
}
|
|
5991
6432
|
function nodeId(label, i) {
|
|
@@ -6035,22 +6476,41 @@ async function handleDiagram(db, input) {
|
|
|
6035
6476
|
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
6477
|
};
|
|
6037
6478
|
}
|
|
6038
|
-
const rows = await db.select({
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6479
|
+
const rows = await db.select({
|
|
6480
|
+
name: packages.name,
|
|
6481
|
+
category: packages.category,
|
|
6482
|
+
runtimeTarget: packages.runtimeTarget
|
|
6483
|
+
}).from(packages).where(inArray7(packages.name, input.stack));
|
|
6484
|
+
const known = new Map(rows.map((r) => [r.name, r]));
|
|
6485
|
+
const items = input.stack.map((name) => {
|
|
6486
|
+
const row = known.get(name);
|
|
6487
|
+
return {
|
|
6488
|
+
label: name,
|
|
6489
|
+
category: row?.category ?? inferCategory(name),
|
|
6490
|
+
// Fall back to the name-only classifier for a package that predates the
|
|
6491
|
+
// column or is not indexed at all: the curated overrides still resolve the
|
|
6492
|
+
// frameworks people actually diagram, and an unclassifiable one keeps the
|
|
6493
|
+
// old Presentation default rather than moving on a null.
|
|
6494
|
+
runtimeTarget: row?.runtimeTarget ?? classifyRuntimeTarget({
|
|
6495
|
+
name,
|
|
6496
|
+
hasBrowserField: false,
|
|
6497
|
+
keywords: [],
|
|
6498
|
+
engines: null,
|
|
6499
|
+
peerDependencies: null
|
|
6500
|
+
})
|
|
6501
|
+
};
|
|
6502
|
+
});
|
|
6044
6503
|
const unclassified = items.filter((i) => layerFor(i) === UNCLASSIFIED).map((i) => i.label);
|
|
6045
6504
|
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
6505
|
return { mermaid: buildMermaid(items), note };
|
|
6047
6506
|
}
|
|
6048
|
-
var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS
|
|
6507
|
+
var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS;
|
|
6049
6508
|
var init_diagram = __esm({
|
|
6050
6509
|
"src/mcp/diagram.ts"() {
|
|
6051
6510
|
"use strict";
|
|
6052
6511
|
init_esm_shims();
|
|
6053
6512
|
init_categoryInference();
|
|
6513
|
+
init_runtimeTarget();
|
|
6054
6514
|
init_schema();
|
|
6055
6515
|
UNCLASSIFIED = "Unclassified";
|
|
6056
6516
|
LAYER_OF = {
|
|
@@ -6078,14 +6538,6 @@ var init_diagram = __esm({
|
|
|
6078
6538
|
testing: "Tooling"
|
|
6079
6539
|
};
|
|
6080
6540
|
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
6541
|
}
|
|
6090
6542
|
});
|
|
6091
6543
|
|
|
@@ -6100,7 +6552,7 @@ __export(plan_exports, {
|
|
|
6100
6552
|
resolvePins: () => resolvePins
|
|
6101
6553
|
});
|
|
6102
6554
|
import { createHash as createHash4 } from "crypto";
|
|
6103
|
-
import { inArray as
|
|
6555
|
+
import { inArray as inArray8 } from "drizzle-orm";
|
|
6104
6556
|
function packageToCandidate(row) {
|
|
6105
6557
|
return {
|
|
6106
6558
|
name: row.name,
|
|
@@ -6321,7 +6773,7 @@ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
|
|
|
6321
6773
|
async function bundleSizes(db, candidates) {
|
|
6322
6774
|
const names2 = [...new Set(candidates.map((c) => c.name))];
|
|
6323
6775
|
if (names2.length === 0) return /* @__PURE__ */ new Map();
|
|
6324
|
-
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(
|
|
6776
|
+
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray8(packages.name, names2));
|
|
6325
6777
|
return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
|
|
6326
6778
|
}
|
|
6327
6779
|
async function decompose(document) {
|
|
@@ -7875,6 +8327,10 @@ var init_app = __esm({
|
|
|
7875
8327
|
});
|
|
7876
8328
|
|
|
7877
8329
|
// src/github/workflow.ts
|
|
8330
|
+
function cliSpec(version = VERSION) {
|
|
8331
|
+
const [major = "0", minor = "0"] = version.split(".");
|
|
8332
|
+
return major === "0" ? `${PACKAGE_NAME}@${major}.${minor}` : `${PACKAGE_NAME}@${major}`;
|
|
8333
|
+
}
|
|
7878
8334
|
function detectInstallCommand(lockfiles) {
|
|
7879
8335
|
if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm install --frozen-lockfile";
|
|
7880
8336
|
if (lockfiles.includes("yarn.lock")) return "yarn install --frozen-lockfile";
|
|
@@ -7887,6 +8343,8 @@ function renderWorkflow(opts = {}) {
|
|
|
7887
8343
|
const install = opts.installCommand ?? "npm ci";
|
|
7888
8344
|
const max = opts.maxUpgrades ?? 3;
|
|
7889
8345
|
const mode = opts.armed ? "pr" : "comment";
|
|
8346
|
+
const autoMerge = opts.autoMerge ?? false;
|
|
8347
|
+
const cli = cliSpec();
|
|
7890
8348
|
return `# Managed by lurq, https://lurq.run
|
|
7891
8349
|
#
|
|
7892
8350
|
# Keeps this repository's dependencies current and rewrites the call sites an
|
|
@@ -7934,19 +8392,19 @@ jobs:
|
|
|
7934
8392
|
- name: Plan
|
|
7935
8393
|
env:
|
|
7936
8394
|
LURQ_API_KEY: \${{ secrets.LURQ_API_KEY }}
|
|
7937
|
-
run: npx -y
|
|
8395
|
+
run: npx -y ${cli} upgrade-plan . --json > lurq-plan.json
|
|
7938
8396
|
|
|
7939
8397
|
# 2. Narrow that to symbols THIS repo references. Runs entirely locally
|
|
7940
8398
|
# against both versions' npm tarballs, no API key, no test suite.
|
|
7941
8399
|
- name: Check against this codebase
|
|
7942
|
-
run: npx -y
|
|
8400
|
+
run: npx -y ${cli} check-upgrade . --plan lurq-plan.json --json > lurq-brief.json
|
|
7943
8401
|
|
|
7944
8402
|
# The same report twice: once into the run summary, once as the body of
|
|
7945
8403
|
# the pull request. Fenced, because the report is aligned plain text and
|
|
7946
8404
|
# markdown would otherwise collapse its indentation into one paragraph.
|
|
7947
8405
|
- name: Summarise
|
|
7948
8406
|
run: |
|
|
7949
|
-
npx -y
|
|
8407
|
+
npx -y ${cli} check-upgrade . --plan lurq-plan.json > lurq-report.txt
|
|
7950
8408
|
{ echo '\`\`\`'; cat lurq-report.txt; echo '\`\`\`'; } > lurq-report.md
|
|
7951
8409
|
cat lurq-report.md >> "\${GITHUB_STEP_SUMMARY}"
|
|
7952
8410
|
|
|
@@ -8022,6 +8480,7 @@ jobs:
|
|
|
8022
8480
|
|
|
8023
8481
|
# 4. The workflow does version control, never the model.
|
|
8024
8482
|
- name: Open pull request
|
|
8483
|
+
id: pr
|
|
8025
8484
|
if: env.LURQ_MODE == 'pr'
|
|
8026
8485
|
uses: peter-evans/create-pull-request@v7
|
|
8027
8486
|
with:
|
|
@@ -8034,19 +8493,30 @@ jobs:
|
|
|
8034
8493
|
body-path: lurq-report.md
|
|
8035
8494
|
labels: dependencies
|
|
8036
8495
|
delete-branch: true
|
|
8037
|
-
`;
|
|
8496
|
+
${autoMerge ? MERGE_STEP : ""}`;
|
|
8038
8497
|
}
|
|
8039
8498
|
function newFileUrl(fullName, branch, content) {
|
|
8040
8499
|
const params = new URLSearchParams({ filename: WORKFLOW_PATH, value: content });
|
|
8041
8500
|
return `https://github.com/${fullName}/new/${encodeURIComponent(branch)}?${params.toString()}`;
|
|
8042
8501
|
}
|
|
8043
|
-
var WORKFLOW_PATH, DEFAULT_CRON;
|
|
8502
|
+
var WORKFLOW_PATH, DEFAULT_CRON, MERGE_STEP;
|
|
8044
8503
|
var init_workflow = __esm({
|
|
8045
8504
|
"src/github/workflow.ts"() {
|
|
8046
8505
|
"use strict";
|
|
8047
8506
|
init_esm_shims();
|
|
8507
|
+
init_constants();
|
|
8048
8508
|
WORKFLOW_PATH = ".github/workflows/lurq-upgrade.yml";
|
|
8049
8509
|
DEFAULT_CRON = "0 6 * * 1";
|
|
8510
|
+
MERGE_STEP = `
|
|
8511
|
+
# 5. Auto-merge, per this repository's lurq policy. GitHub merges only
|
|
8512
|
+
# once the repo's OWN required checks pass; lurq never makes that call.
|
|
8513
|
+
# Requires "Allow auto-merge" in repository settings.
|
|
8514
|
+
- name: Enable auto-merge
|
|
8515
|
+
if: env.LURQ_MODE == 'pr' && steps.pr.outputs.pull-request-number
|
|
8516
|
+
env:
|
|
8517
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
8518
|
+
run: gh pr merge --auto --squash "\${{ steps.pr.outputs.pull-request-number }}"
|
|
8519
|
+
`;
|
|
8050
8520
|
}
|
|
8051
8521
|
});
|
|
8052
8522
|
|
|
@@ -8143,6 +8613,7 @@ var init_manifests = __esm({
|
|
|
8143
8613
|
var upgradePlan_exports = {};
|
|
8144
8614
|
__export(upgradePlan_exports, {
|
|
8145
8615
|
buildUpgradePlan: () => buildUpgradePlan,
|
|
8616
|
+
detectRepoFullName: () => detectRepoFullName,
|
|
8146
8617
|
findManifests: () => findManifests,
|
|
8147
8618
|
formatUpgradePlan: () => formatUpgradePlan,
|
|
8148
8619
|
mergedDeps: () => mergedDeps,
|
|
@@ -8196,27 +8667,38 @@ function mergedDeps(manifests) {
|
|
|
8196
8667
|
}
|
|
8197
8668
|
return out;
|
|
8198
8669
|
}
|
|
8670
|
+
function detectRepoFullName(explicit) {
|
|
8671
|
+
const value = explicit ?? process.env.GITHUB_REPOSITORY ?? null;
|
|
8672
|
+
return value && /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
|
|
8673
|
+
}
|
|
8199
8674
|
async function buildUpgradePlan(dir, opts = {}) {
|
|
8200
8675
|
const manifests = readManifests(dir);
|
|
8201
8676
|
const deps = mergedDeps(manifests);
|
|
8202
8677
|
if (Object.keys(deps).length === 0) {
|
|
8203
8678
|
throw new Error(`No package.json with dependencies found under ${dir}`);
|
|
8204
8679
|
}
|
|
8205
|
-
const plan = await fetchUpgradePlan(deps, opts);
|
|
8680
|
+
const plan = await fetchUpgradePlan(deps, { ...opts, repo: detectRepoFullName(opts.repo) });
|
|
8206
8681
|
return { ...plan, manifests: manifests.length, deps: Object.keys(deps).length };
|
|
8207
8682
|
}
|
|
8208
8683
|
function formatUpgradePlan(plan) {
|
|
8209
8684
|
const out = [
|
|
8210
|
-
`lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))
|
|
8211
|
-
""
|
|
8685
|
+
`lurq upgrade plan (${plan.deps} dependencies across ${plan.manifests} manifest(s))`
|
|
8212
8686
|
];
|
|
8687
|
+
if (plan.scopeSource === "repo-policy") {
|
|
8688
|
+
out.push(
|
|
8689
|
+
`policy: scope=${plan.scope}` + (plan.outOfScope ? `, ${plan.outOfScope} upgrade(s) held back` : ", nothing held back")
|
|
8690
|
+
);
|
|
8691
|
+
}
|
|
8692
|
+
out.push("");
|
|
8213
8693
|
if (plan.upgrades.length === 0) {
|
|
8214
8694
|
out.push("Everything lurq tracks is already on its latest release.");
|
|
8215
8695
|
}
|
|
8216
8696
|
for (const upgrade of plan.upgrades) {
|
|
8697
|
+
const held = upgrade.inScope === false ? " [HELD]" : "";
|
|
8217
8698
|
out.push(
|
|
8218
|
-
`${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "")
|
|
8699
|
+
`${VERDICT_LABEL[upgrade.verdict]} ${upgrade.package} ${upgrade.fromVersion} \u2192 ${upgrade.toVersion}` + (upgrade.advisories ? ` (${upgrade.advisories} advisory)` : "") + held
|
|
8219
8700
|
);
|
|
8701
|
+
if (upgrade.scopeReason) out.push(` held: ${upgrade.scopeReason}`);
|
|
8220
8702
|
if (upgrade.removed.length) {
|
|
8221
8703
|
out.push(` removes ${upgrade.removed.length}: ${upgrade.removed.slice(0, 8).join(", ")}`);
|
|
8222
8704
|
}
|
|
@@ -9362,7 +9844,7 @@ function targetsFromPlanFile(path2) {
|
|
|
9362
9844
|
if (!Array.isArray(parsed.upgrades)) {
|
|
9363
9845
|
throw new Error(`${path2} is not an upgrade plan (no "upgrades" array)`);
|
|
9364
9846
|
}
|
|
9365
|
-
return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
|
|
9847
|
+
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
9848
|
}
|
|
9367
9849
|
async function runCheckUpgrade(dir, opts) {
|
|
9368
9850
|
const { scanReferences: scanReferences2 } = await Promise.resolve().then(() => (init_references(), references_exports));
|
|
@@ -9417,7 +9899,7 @@ var init_graph = __esm({
|
|
|
9417
9899
|
});
|
|
9418
9900
|
|
|
9419
9901
|
// src/db/surface.ts
|
|
9420
|
-
import { and as and9, desc as desc5, eq as eq13, sql as
|
|
9902
|
+
import { and as and9, desc as desc5, eq as eq13, sql as sql11 } from "drizzle-orm";
|
|
9421
9903
|
function surfaceRef(pkg, version) {
|
|
9422
9904
|
return { kind: "package_surface", namespace: "npm", name: pkg, version };
|
|
9423
9905
|
}
|
|
@@ -9700,22 +10182,22 @@ var init_toolDescriptions = __esm({
|
|
|
9700
10182
|
});
|
|
9701
10183
|
|
|
9702
10184
|
// src/db/usage.ts
|
|
9703
|
-
import { and as and11, desc as desc7, eq as eq15, gte, sql as
|
|
10185
|
+
import { and as and11, desc as desc7, eq as eq15, gte, sql as sql12 } from "drizzle-orm";
|
|
9704
10186
|
function windowStart(days) {
|
|
9705
|
-
return
|
|
10187
|
+
return sql12`CURRENT_DATE - (${days}::int - 1)`;
|
|
9706
10188
|
}
|
|
9707
10189
|
async function recordUsage(db, ownerId, tool) {
|
|
9708
10190
|
if (!ownerId) return;
|
|
9709
10191
|
try {
|
|
9710
|
-
await db.insert(ownerUsageDaily).values({ ownerId, date:
|
|
10192
|
+
await db.insert(ownerUsageDaily).values({ ownerId, date: sql12`CURRENT_DATE`, tool, count: 1 }).onConflictDoUpdate({
|
|
9711
10193
|
target: [ownerUsageDaily.ownerId, ownerUsageDaily.date, ownerUsageDaily.tool],
|
|
9712
|
-
set: { count:
|
|
10194
|
+
set: { count: sql12`${ownerUsageDaily.count} + 1` }
|
|
9713
10195
|
});
|
|
9714
10196
|
} catch {
|
|
9715
10197
|
}
|
|
9716
10198
|
}
|
|
9717
10199
|
async function getUsageSummary(db, ownerId, days) {
|
|
9718
|
-
const rows = await db.execute(
|
|
10200
|
+
const rows = await db.execute(sql12`
|
|
9719
10201
|
select
|
|
9720
10202
|
to_char(d, 'YYYY-MM-DD') as date,
|
|
9721
10203
|
coalesce(sum(u.count), 0)::int as count
|
|
@@ -9732,13 +10214,13 @@ async function getUsageSummary(db, ownerId, days) {
|
|
|
9732
10214
|
async function getUsageByTool(db, ownerId, days) {
|
|
9733
10215
|
const rows = await db.select({
|
|
9734
10216
|
tool: ownerUsageDaily.tool,
|
|
9735
|
-
count:
|
|
10217
|
+
count: sql12`sum(${ownerUsageDaily.count})::int`
|
|
9736
10218
|
}).from(ownerUsageDaily).where(
|
|
9737
10219
|
and11(
|
|
9738
10220
|
eq15(ownerUsageDaily.ownerId, ownerId),
|
|
9739
10221
|
gte(ownerUsageDaily.date, windowStart(days))
|
|
9740
10222
|
)
|
|
9741
|
-
).groupBy(ownerUsageDaily.tool).orderBy(desc7(
|
|
10223
|
+
).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql12`sum(${ownerUsageDaily.count})`));
|
|
9742
10224
|
return rows.map((r) => ({ tool: r.tool, count: Number(r.count) }));
|
|
9743
10225
|
}
|
|
9744
10226
|
var init_usage = __esm({
|
|
@@ -10259,6 +10741,52 @@ var init_brief = __esm({
|
|
|
10259
10741
|
}
|
|
10260
10742
|
});
|
|
10261
10743
|
|
|
10744
|
+
// src/github/scope.ts
|
|
10745
|
+
function isUnassessed(u) {
|
|
10746
|
+
return u.verdict === "unknown";
|
|
10747
|
+
}
|
|
10748
|
+
function breaksSomething(u) {
|
|
10749
|
+
return u.verdict === "removes-exports" || u.verdict === "arity-changed";
|
|
10750
|
+
}
|
|
10751
|
+
function scopeVerdict(u, scope) {
|
|
10752
|
+
if (scope === "all") return { inScope: true };
|
|
10753
|
+
if (u.advisories > 0) return { inScope: true };
|
|
10754
|
+
if (scope === "security") {
|
|
10755
|
+
return { inScope: false, reason: "no advisory \u2014 scope is security-only" };
|
|
10756
|
+
}
|
|
10757
|
+
if (isUnassessed(u)) {
|
|
10758
|
+
return { inScope: false, reason: "surface not yet analysed \u2014 nothing for the agent to rewrite" };
|
|
10759
|
+
}
|
|
10760
|
+
if (breaksSomething(u)) return { inScope: true };
|
|
10761
|
+
return { inScope: false, reason: "nothing referenced breaks \u2014 scope is security + blocking" };
|
|
10762
|
+
}
|
|
10763
|
+
function applyScope(upgrades, policy) {
|
|
10764
|
+
if (!policy) {
|
|
10765
|
+
return {
|
|
10766
|
+
upgrades: upgrades.map((u) => ({ ...u, inScope: true })),
|
|
10767
|
+
scope: "all",
|
|
10768
|
+
scopeSource: "unconnected",
|
|
10769
|
+
outOfScope: 0
|
|
10770
|
+
};
|
|
10771
|
+
}
|
|
10772
|
+
const scoped = upgrades.map((u) => {
|
|
10773
|
+
const verdict = scopeVerdict(u, policy.scope);
|
|
10774
|
+
return verdict.inScope ? { ...u, inScope: true } : { ...u, inScope: false, scopeReason: verdict.reason };
|
|
10775
|
+
});
|
|
10776
|
+
return {
|
|
10777
|
+
upgrades: scoped,
|
|
10778
|
+
scope: policy.scope,
|
|
10779
|
+
scopeSource: "repo-policy",
|
|
10780
|
+
outOfScope: scoped.filter((u) => !u.inScope).length
|
|
10781
|
+
};
|
|
10782
|
+
}
|
|
10783
|
+
var init_scope = __esm({
|
|
10784
|
+
"src/github/scope.ts"() {
|
|
10785
|
+
"use strict";
|
|
10786
|
+
init_esm_shims();
|
|
10787
|
+
}
|
|
10788
|
+
});
|
|
10789
|
+
|
|
10262
10790
|
// src/github/runs.ts
|
|
10263
10791
|
function str2(value, max) {
|
|
10264
10792
|
if (typeof value !== "string") return null;
|
|
@@ -10275,7 +10803,7 @@ function strList(value, maxItems, maxLen) {
|
|
|
10275
10803
|
}
|
|
10276
10804
|
return out;
|
|
10277
10805
|
}
|
|
10278
|
-
function
|
|
10806
|
+
function count2(value) {
|
|
10279
10807
|
const n = typeof value === "number" ? value : Number(value);
|
|
10280
10808
|
return Number.isInteger(n) && n >= 0 && n < 1e6 ? n : null;
|
|
10281
10809
|
}
|
|
@@ -10299,9 +10827,9 @@ function parseUpgradeRun(input) {
|
|
|
10299
10827
|
severity: severity2,
|
|
10300
10828
|
status,
|
|
10301
10829
|
symbolsAffected: strList(raw.symbolsAffected, MAX_SYMBOLS, MAX_NAME),
|
|
10302
|
-
callSites:
|
|
10830
|
+
callSites: count2(raw.callSites) ?? 0,
|
|
10303
10831
|
callSiteFiles: files,
|
|
10304
|
-
filesChanged:
|
|
10832
|
+
filesChanged: count2(raw.filesChanged),
|
|
10305
10833
|
testsPassed: typeof raw.testsPassed === "boolean" ? raw.testsPassed : null,
|
|
10306
10834
|
prUrl: str2(raw.prUrl, MAX_URL),
|
|
10307
10835
|
// Empty string, not null: it is part of the dedup key (see schema).
|
|
@@ -10319,6 +10847,11 @@ function parseUpgradeRuns(input, max) {
|
|
|
10319
10847
|
}
|
|
10320
10848
|
return { runs, rejected };
|
|
10321
10849
|
}
|
|
10850
|
+
function parseRepoFullName(input) {
|
|
10851
|
+
const value = str2(input, MAX_NAME * 2);
|
|
10852
|
+
if (!value) return null;
|
|
10853
|
+
return /^[\w.-]+\/[\w.-]+$/.test(value) ? value : null;
|
|
10854
|
+
}
|
|
10322
10855
|
function parseDepsInput(input) {
|
|
10323
10856
|
if (!input || typeof input !== "object") return {};
|
|
10324
10857
|
const out = {};
|
|
@@ -10346,7 +10879,7 @@ var init_runs = __esm({
|
|
|
10346
10879
|
});
|
|
10347
10880
|
|
|
10348
10881
|
// src/db/upgradeRuns.ts
|
|
10349
|
-
import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as
|
|
10882
|
+
import { and as and13, desc as desc9, eq as eq17, gte as gte2, sql as sql13 } from "drizzle-orm";
|
|
10350
10883
|
async function recordUpgradeRuns(db, rows) {
|
|
10351
10884
|
if (rows.length === 0) return 0;
|
|
10352
10885
|
await db.insert(upgradeRuns).values(rows).onConflictDoUpdate({
|
|
@@ -10358,15 +10891,15 @@ async function recordUpgradeRuns(db, rows) {
|
|
|
10358
10891
|
upgradeRuns.runUrl
|
|
10359
10892
|
],
|
|
10360
10893
|
set: {
|
|
10361
|
-
severity:
|
|
10362
|
-
status:
|
|
10363
|
-
symbolsAffected:
|
|
10364
|
-
callSites:
|
|
10365
|
-
callSiteFiles:
|
|
10366
|
-
filesChanged:
|
|
10367
|
-
testsPassed:
|
|
10368
|
-
prUrl:
|
|
10369
|
-
repoId:
|
|
10894
|
+
severity: sql13`excluded.severity`,
|
|
10895
|
+
status: sql13`excluded.status`,
|
|
10896
|
+
symbolsAffected: sql13`excluded.symbols_affected`,
|
|
10897
|
+
callSites: sql13`excluded.call_sites`,
|
|
10898
|
+
callSiteFiles: sql13`excluded.call_site_files`,
|
|
10899
|
+
filesChanged: sql13`excluded.files_changed`,
|
|
10900
|
+
testsPassed: sql13`excluded.tests_passed`,
|
|
10901
|
+
prUrl: sql13`excluded.pr_url`,
|
|
10902
|
+
repoId: sql13`excluded.repo_id`
|
|
10370
10903
|
}
|
|
10371
10904
|
});
|
|
10372
10905
|
return rows.length;
|
|
@@ -10381,12 +10914,12 @@ async function listRunsForRepo(db, ownerId, repoId, limit = 50) {
|
|
|
10381
10914
|
async function getUpgradeImpact(db, ownerId, days = 30) {
|
|
10382
10915
|
const since = new Date(Date.now() - days * 864e5);
|
|
10383
10916
|
const rows = await db.select({
|
|
10384
|
-
analysed:
|
|
10385
|
-
blocking:
|
|
10386
|
-
callSites:
|
|
10387
|
-
prsOpened:
|
|
10388
|
-
merged:
|
|
10389
|
-
unverified:
|
|
10917
|
+
analysed: sql13`count(*)::int`,
|
|
10918
|
+
blocking: sql13`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
|
|
10919
|
+
callSites: sql13`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
|
|
10920
|
+
prsOpened: sql13`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
|
|
10921
|
+
merged: sql13`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
|
|
10922
|
+
unverified: sql13`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
|
|
10390
10923
|
}).from(upgradeRuns).where(and13(eq17(upgradeRuns.ownerId, ownerId), gte2(upgradeRuns.createdAt, since)));
|
|
10391
10924
|
return rows[0] ?? {
|
|
10392
10925
|
analysed: 0,
|
|
@@ -10941,7 +11474,8 @@ async function startHttpServer(opts = {}) {
|
|
|
10941
11474
|
const runs = await listRunsForRepo(db, ownerId, row.id);
|
|
10942
11475
|
const workflow = renderWorkflow({
|
|
10943
11476
|
installCommand: row.installCommand ?? void 0,
|
|
10944
|
-
armed: row.policy.enabled
|
|
11477
|
+
armed: row.policy.enabled,
|
|
11478
|
+
autoMerge: row.policy.autoMerge
|
|
10945
11479
|
});
|
|
10946
11480
|
res.status(200).json({
|
|
10947
11481
|
repo: {
|
|
@@ -11071,8 +11605,13 @@ async function startHttpServer(opts = {}) {
|
|
|
11071
11605
|
try {
|
|
11072
11606
|
const drift = await computeDrift(db, [{ path: "package.json", deps }]);
|
|
11073
11607
|
const brief = await briefRepo(db, drift);
|
|
11608
|
+
const ownerId = req.lurqKey?.ownerId ?? null;
|
|
11609
|
+
const repoFullName = parseRepoFullName((req.body ?? {}).repo);
|
|
11610
|
+
const policy = ownerId && repoFullName ? await findPolicyByFullName(db, ownerId, repoFullName) : null;
|
|
11611
|
+
const scoped = applyScope(brief.upgrades, policy);
|
|
11074
11612
|
res.status(200).json({
|
|
11075
11613
|
...brief,
|
|
11614
|
+
...scoped,
|
|
11076
11615
|
// Surfaced so CI can log what lurq had no opinion on. An upgrade we do
|
|
11077
11616
|
// not know about must not look like an upgrade we cleared.
|
|
11078
11617
|
untracked: Object.keys(deps).length - drift.depsTracked
|
|
@@ -11153,6 +11692,7 @@ var init_http2 = __esm({
|
|
|
11153
11692
|
init_app();
|
|
11154
11693
|
init_brief();
|
|
11155
11694
|
init_drift();
|
|
11695
|
+
init_scope();
|
|
11156
11696
|
init_runs();
|
|
11157
11697
|
init_upgradeRuns();
|
|
11158
11698
|
init_manifests();
|
|
@@ -11242,9 +11782,9 @@ function buildProgram() {
|
|
|
11242
11782
|
const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
11243
11783
|
await runPlan2(file, opts);
|
|
11244
11784
|
});
|
|
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) => {
|
|
11785
|
+
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
11786
|
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 });
|
|
11787
|
+
const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey, repo: opts.repo });
|
|
11248
11788
|
console.log(opts.json ? JSON.stringify(plan, null, 2) : formatUpgradePlan2(plan));
|
|
11249
11789
|
});
|
|
11250
11790
|
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(
|