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