@quilt-dev/cli
Advanced tools
| import { readAuthorship } from "./authorship.js"; | ||
| import { buildModel } from "./engine.js"; | ||
| import { latestPromptBefore, locateTranscript } from "./transcripts.js"; | ||
| function matchingEvent(events, path, line, actor) { | ||
| for (let i = events.length - 1; i >= 0; i--) { | ||
| const event = events[i]; | ||
| if (event.path !== path || event.actor !== actor) | ||
| continue; | ||
| if (line.type === "add") { | ||
| if (line.key && event.addedKeys?.includes(line.key)) | ||
| return event; | ||
| if (!event.addedKeys && event.added.includes(line.text)) | ||
| return event; | ||
| } | ||
| else if (line.type === "del") { | ||
| if (line.key && event.removedKeys?.includes(line.key)) | ||
| return event; | ||
| if (!event.removedKeys && event.removed.includes(line.text)) | ||
| return event; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Diff HEAD to the worktree and retain the engine's ownership call per line. | ||
| * Transcript reads happen only when this function is called by the review API. | ||
| */ | ||
| export function fileBlame(store, relPath, transcriptOptions = {}) { | ||
| const file = buildModel(store, null, { ledgerOverlay: true }).files.find((candidate) => candidate.path === relPath); | ||
| if (!file) | ||
| return null; | ||
| if (file.binary) | ||
| return { path: file.path, isNew: file.isNew, isDeleted: file.isDeleted, binary: true, lines: [] }; | ||
| const events = readAuthorship(store); | ||
| const transcriptCache = new Map(); | ||
| const lines = file.hunks.flatMap((hunk) => hunk.lines.map((line) => { | ||
| const provenance = line.actors.map((actor) => { | ||
| const event = matchingEvent(events, file.path, line, actor); | ||
| if (!transcriptCache.has(actor)) { | ||
| transcriptCache.set(actor, locateTranscript(actor, store.paths.repoRoot, transcriptOptions)); | ||
| } | ||
| const transcript = transcriptCache.get(actor) ?? null; | ||
| const prompt = event && transcript ? latestPromptBefore(transcript.prompts, event.ts) : null; | ||
| return { | ||
| actor, | ||
| editTs: event?.ts ?? null, | ||
| provider: transcript?.provider ?? null, | ||
| sessionId: transcript?.sessionId ?? null, | ||
| promptTs: prompt?.ts ?? null, | ||
| prompt: prompt?.prompt ?? null, | ||
| inferred: prompt !== null, | ||
| }; | ||
| }); | ||
| return { | ||
| ...line, | ||
| actor: line.actors[0] ?? null, | ||
| lineNumber: line.type === "del" ? line.oldLineNumber : line.newLineNumber, | ||
| provenance, | ||
| }; | ||
| })); | ||
| return { path: file.path, isNew: file.isNew, isDeleted: file.isDeleted, binary: false, lines }; | ||
| } |
| import { existsSync, readFileSync, readdirSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { basename, join, resolve, sep } from "node:path"; | ||
| function jsonLines(path) { | ||
| try { | ||
| return readFileSync(path, "utf8") | ||
| .split("\n") | ||
| .filter((line) => line.trim()) | ||
| .flatMap((line) => { | ||
| try { | ||
| return [JSON.parse(line)]; | ||
| } | ||
| catch { | ||
| return []; | ||
| } | ||
| }); | ||
| } | ||
| catch { | ||
| return []; | ||
| } | ||
| } | ||
| function filesUnder(root, accept) { | ||
| if (!existsSync(root)) | ||
| return []; | ||
| const out = []; | ||
| const walk = (dir) => { | ||
| let entries; | ||
| try { | ||
| entries = readdirSync(dir, { withFileTypes: true }); | ||
| } | ||
| catch { | ||
| return; | ||
| } | ||
| for (const entry of entries) { | ||
| const path = join(dir, entry.name); | ||
| if (entry.isDirectory()) | ||
| walk(path); | ||
| else if (entry.isFile() && accept(path)) | ||
| out.push(path); | ||
| } | ||
| }; | ||
| walk(root); | ||
| return out; | ||
| } | ||
| function actorParts(actorId) { | ||
| const match = /^(claude|codex)-(.+)$/.exec(actorId); | ||
| if (!match) | ||
| return null; | ||
| return { provider: match[1], prefix: match[2] }; | ||
| } | ||
| function insideRepo(cwd, repoRoot) { | ||
| if (typeof cwd !== "string") | ||
| return false; | ||
| const root = resolve(repoRoot); | ||
| const at = resolve(cwd); | ||
| return at === root || at.startsWith(root + sep) || root.startsWith(at + sep); | ||
| } | ||
| function claudeText(content) { | ||
| if (typeof content === "string") | ||
| return content.trim() || null; | ||
| if (!Array.isArray(content)) | ||
| return null; | ||
| const text = content | ||
| .filter((part) => !!part && typeof part === "object") | ||
| .filter((part) => part.type === "text" && typeof part.text === "string") | ||
| .map((part) => String(part.text).trim()) | ||
| .filter(Boolean) | ||
| .join("\n"); | ||
| return text || null; | ||
| } | ||
| function claudeMatch(prefix, repoRoot, root) { | ||
| const encodedRepo = resolve(repoRoot).replace(/[^a-zA-Z0-9]/g, "-"); | ||
| const projectRoot = join(root, encodedRepo); | ||
| const searchRoot = existsSync(projectRoot) ? projectRoot : root; | ||
| const candidates = filesUnder(searchRoot, (path) => path.endsWith(".jsonl") && basename(path).startsWith(prefix)); | ||
| for (const path of candidates) { | ||
| const rows = jsonLines(path); | ||
| const sessionId = rows.find((row) => typeof row.sessionId === "string")?.sessionId; | ||
| if (typeof sessionId !== "string" || !sessionId.startsWith(prefix)) | ||
| continue; | ||
| if (!rows.some((row) => insideRepo(row.cwd, repoRoot))) | ||
| continue; | ||
| const prompts = rows.flatMap((row) => { | ||
| if (row.type !== "user" || row.message?.role !== "user" || typeof row.timestamp !== "string") | ||
| return []; | ||
| const prompt = claudeText(row.message.content); | ||
| return prompt ? [{ ts: row.timestamp, prompt }] : []; | ||
| }); | ||
| return { provider: "claude", sessionId, prompts }; | ||
| } | ||
| return null; | ||
| } | ||
| function rolloutMeta(path) { | ||
| const rows = jsonLines(path); | ||
| const meta = rows.find((row) => row.type === "session_meta")?.payload; | ||
| if (!meta || typeof meta.id !== "string" || typeof meta.cwd !== "string") | ||
| return null; | ||
| const prompts = rows.flatMap((row) => { | ||
| if (row.type !== "event_msg" || row.payload?.type !== "user_message") | ||
| return []; | ||
| if (typeof row.timestamp !== "string" || typeof row.payload.message !== "string") | ||
| return []; | ||
| const prompt = row.payload.message.trim(); | ||
| return prompt ? [{ ts: row.timestamp, prompt }] : []; | ||
| }); | ||
| return { id: meta.id, cwd: meta.cwd, prompts }; | ||
| } | ||
| function codexMatch(prefix, repoRoot, root) { | ||
| const sessionsDir = join(root, "sessions"); | ||
| let match = null; | ||
| for (const path of filesUnder(sessionsDir, (p) => p.endsWith(".jsonl") && basename(p).startsWith("rollout-"))) { | ||
| if (!basename(path).includes(prefix)) | ||
| continue; | ||
| const meta = rolloutMeta(path); | ||
| if (meta?.id.startsWith(prefix) && insideRepo(meta.cwd, repoRoot)) { | ||
| match = { id: meta.id, prompts: meta.prompts }; | ||
| break; | ||
| } | ||
| } | ||
| if (!match) | ||
| return null; | ||
| const history = jsonLines(join(root, "history.jsonl")); | ||
| const primary = history.flatMap((row) => { | ||
| if (row.session_id !== match.id || typeof row.ts !== "number" || typeof row.text !== "string") | ||
| return []; | ||
| const prompt = row.text.trim(); | ||
| return prompt ? [{ ts: new Date(row.ts * 1000).toISOString(), prompt }] : []; | ||
| }); | ||
| return { provider: "codex", sessionId: match.id, prompts: primary.length ? primary : match.prompts }; | ||
| } | ||
| /** Locate the local transcript for an auto-derived Quilt actor. Read-only. */ | ||
| export function locateTranscript(actorId, repoRoot, options = {}) { | ||
| const parts = actorParts(actorId); | ||
| if (!parts) | ||
| return null; | ||
| if (parts.provider === "claude") { | ||
| const root = options.claudeDir ?? process.env.QUILT_CLAUDE_DIR ?? join(homedir(), ".claude", "projects"); | ||
| return claudeMatch(parts.prefix, repoRoot, root); | ||
| } | ||
| const root = options.codexDir ?? process.env.QUILT_CODEX_DIR ?? join(homedir(), ".codex"); | ||
| return codexMatch(parts.prefix, repoRoot, root); | ||
| } | ||
| /** Latest user prompt at or before an edit, which is a time-based inference. */ | ||
| export function latestPromptBefore(prompts, editTs) { | ||
| const limit = Date.parse(editTs); | ||
| if (!Number.isFinite(limit)) | ||
| return null; | ||
| let best = null; | ||
| for (const prompt of prompts) { | ||
| const at = Date.parse(prompt.ts); | ||
| if (!Number.isFinite(at) || at > limit) | ||
| continue; | ||
| if (!best || at > Date.parse(best.ts)) | ||
| best = prompt; | ||
| } | ||
| return best; | ||
| } |
+44
-4
@@ -554,2 +554,5 @@ import { lstatSync, readFileSync } from "node:fs"; | ||
| let conflicted = false; | ||
| const lines = []; | ||
| let oldLine = hunk.oldStart; | ||
| let newLine = hunk.newStart; | ||
| // A fresh keyer per hunk, started at the hunk's line offsets. Called on every | ||
@@ -560,4 +563,37 @@ // op (incl. eq/trivial) so the keys line up with what reconcile recorded. | ||
| const key = keyOf(op); | ||
| if (op.type === "eq") | ||
| const opOldLine = op.type === "add" ? null : oldLine; | ||
| const opNewLine = op.type === "del" ? null : newLine; | ||
| if (op.type !== "add") | ||
| oldLine++; | ||
| if (op.type !== "del") | ||
| newLine++; | ||
| if (op.type === "eq") { | ||
| lines.push({ | ||
| type: op.type, | ||
| text: op.text, | ||
| key, | ||
| oldLineNumber: opOldLine, | ||
| newLineNumber: opNewLine, | ||
| actors: [], | ||
| conflicted: false, | ||
| unowned: false, | ||
| }); | ||
| continue; | ||
| } | ||
| const map = op.type === "add" ? file?.added : file?.removed; | ||
| const owner = map?.[key]; | ||
| const lineActors = new Set(owner ? [owner] : []); | ||
| const contenders = fileConflicts[key] ?? []; | ||
| for (const actor of contenders) | ||
| lineActors.add(actor); | ||
| lines.push({ | ||
| type: op.type, | ||
| text: op.text, | ||
| key, | ||
| oldLineNumber: opOldLine, | ||
| newLineNumber: opNewLine, | ||
| actors: [...lineActors], | ||
| conflicted: contenders.length > 0, | ||
| unowned: lineActors.size === 0, | ||
| }); | ||
| // Trivial lines (braces, blanks) are neither owned nor counted as | ||
@@ -567,4 +603,2 @@ // unattributed — they ride along with the hunk's substantive changes. | ||
| continue; | ||
| const map = op.type === "add" ? file?.added : file?.removed; | ||
| const owner = map?.[key]; | ||
| if (owner) { | ||
@@ -609,2 +643,8 @@ owners.add(owner); | ||
| unownedLines = 0; | ||
| for (const line of lines) { | ||
| if (line.type !== "eq" && line.unowned) { | ||
| line.actors = [holder]; | ||
| line.unowned = false; | ||
| } | ||
| } | ||
| } | ||
@@ -625,3 +665,3 @@ else { | ||
| : undefined; | ||
| return { hunk, ownership: ownership_, actors, conflicted, overlap, linesByActor, unownedLines }; | ||
| return { hunk, ownership: ownership_, actors, conflicted, overlap, linesByActor, unownedLines, lines }; | ||
| } | ||
@@ -628,0 +668,0 @@ /** |
+124
-2
@@ -5,3 +5,5 @@ import { createServer } from "node:http"; | ||
| import { fleetSnapshot } from "./fleet.js"; | ||
| import { fileBlame } from "./blame.js"; | ||
| import { shortHead } from "./git.js"; | ||
| import { repoRelative } from "./paths.js"; | ||
| import { initSymbols } from "./symbols.js"; | ||
@@ -41,3 +43,4 @@ import { VERSION } from "./version.js"; | ||
| } | ||
| const url = (req.url ?? "/").split("?")[0]; | ||
| const parsedUrl = new URL(req.url ?? "/", "http://127.0.0.1"); | ||
| const url = parsedUrl.pathname; | ||
| if (url === "/") { | ||
@@ -67,2 +70,26 @@ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }); | ||
| } | ||
| if (url === "/api/blame") { | ||
| const requested = parsedUrl.searchParams.get("path"); | ||
| const relPath = requested ? repoRelative(store.paths.repoRoot, requested) : null; | ||
| if (!relPath || relPath !== requested) { | ||
| res.writeHead(400, { "content-type": "application/json", "cache-control": "no-store" }); | ||
| res.end(JSON.stringify({ error: "path must be a normalized file inside the repository" })); | ||
| return; | ||
| } | ||
| try { | ||
| const blame = fileBlame(store, relPath); | ||
| if (!blame) { | ||
| res.writeHead(404, { "content-type": "application/json", "cache-control": "no-store" }); | ||
| res.end(JSON.stringify({ error: "file has no uncommitted changes" })); | ||
| return; | ||
| } | ||
| res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }); | ||
| res.end(JSON.stringify(blame)); | ||
| } | ||
| catch (err) { | ||
| res.writeHead(500, { "content-type": "application/json", "cache-control": "no-store" }); | ||
| res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) })); | ||
| } | ||
| return; | ||
| } | ||
| res.writeHead(404, { "content-type": "text/plain" }); | ||
@@ -175,2 +202,25 @@ res.end("not found\n"); | ||
| .un { color: var(--faint); font-size: 11.5px; } | ||
| .review-toggle { appearance: none; border: 0; background: none; color: var(--text); padding: 0; cursor: pointer; | ||
| font: inherit; font-family: var(--mono); text-align: left; } | ||
| .review-toggle::before { content: "▸"; color: var(--faint); display: inline-block; width: 15px; } | ||
| .review-toggle.open::before { content: "▾"; } | ||
| .review-cell { padding: 0 10px 12px; background: color-mix(in srgb, var(--bg) 38%, var(--panel)); } | ||
| .review { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } | ||
| .review-state { padding: 14px; color: var(--dim); font-size: 12px; } | ||
| .diff-line { display: grid; grid-template-columns: 42px 42px 18px minmax(0, 1fr) auto; align-items: start; | ||
| min-height: 25px; border-top: 1px solid color-mix(in srgb, var(--border) 65%, transparent); font-family: var(--mono); font-size: 11.5px; } | ||
| .diff-line:first-child { border-top: 0; } | ||
| .diff-line.add { background: color-mix(in srgb, var(--green) 7%, transparent); } | ||
| .diff-line.del { background: color-mix(in srgb, var(--red) 7%, transparent); } | ||
| .diff-line.conflict { box-shadow: inset 3px 0 var(--red); } | ||
| .ln { color: var(--faint); text-align: right; padding: 4px 7px 4px 2px; user-select: none; } | ||
| .sign { color: var(--faint); padding: 4px 3px; } | ||
| .code { white-space: pre-wrap; overflow-wrap: anywhere; padding: 4px 8px 4px 2px; } | ||
| .line-meta { padding: 2px 6px; max-width: 360px; text-align: right; } | ||
| .prompt { display: inline-block; text-align: left; margin-left: 4px; vertical-align: top; } | ||
| .prompt summary { color: var(--cyan); cursor: pointer; list-style: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 10.5px; } | ||
| .prompt summary::-webkit-details-marker { display: none; } | ||
| .prompt pre { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 340px; max-height: 180px; overflow: auto; | ||
| color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px; margin: 4px 0; } | ||
| .review-notes { padding: 9px 11px; color: var(--faint); font-size: 10.5px; border-top: 1px solid var(--border); } | ||
@@ -208,2 +258,5 @@ .kv { font-family: var(--mono); font-size: 12.5px; } | ||
| var PALETTE = ["#e06c75","#e5a06b","#e3c46b","#8fc46f","#56c2a8","#5aa9e6","#8a7fe8","#d074c4"]; | ||
| var OPEN_REVIEWS = Object.create(null); | ||
| var OPEN_PROMPTS = Object.create(null); | ||
| var REVIEW_CACHE = Object.create(null); | ||
| function colorFor(id) { | ||
@@ -256,2 +309,52 @@ var h = 0; | ||
| function renderReviewData(d, panel) { | ||
| panel.replaceChildren(); | ||
| if (d.binary) { | ||
| panel.appendChild(el("div", "review-state", "Binary file: line review is unavailable.")); | ||
| return; | ||
| } | ||
| d.lines.forEach(function (line, lineIndex) { | ||
| var meta = el("div", "line-meta"); | ||
| if (line.actors.length) line.actors.forEach(function (actor) { meta.appendChild(chip(actor)); }); | ||
| else if (line.type !== "eq") meta.appendChild(el("span", "un", "unattributed")); | ||
| if (line.conflicted) meta.appendChild(badge("contended", "conflict")); | ||
| line.provenance.forEach(function (p, provenanceIndex) { | ||
| if (!p.prompt) return; | ||
| var details = el("details", "prompt"); | ||
| var promptKey = d.path + ":" + lineIndex + ":" + provenanceIndex; | ||
| details.open = !!OPEN_PROMPTS[promptKey]; | ||
| details.addEventListener("toggle", function () { OPEN_PROMPTS[promptKey] = details.open; }); | ||
| details.appendChild(el("summary", null, "prompt")); | ||
| details.appendChild(el("pre", null, p.prompt)); | ||
| meta.appendChild(details); | ||
| }); | ||
| var kind = line.type === "add" ? "add" : line.type === "del" ? "del" : "eq"; | ||
| var sign = line.type === "add" ? "+" : line.type === "del" ? "−" : " "; | ||
| panel.appendChild(el("div", "diff-line " + kind + (line.conflicted ? " conflict" : ""), | ||
| el("span", "ln", line.oldLineNumber == null ? "" : String(line.oldLineNumber)), | ||
| el("span", "ln", line.newLineNumber == null ? "" : String(line.newLineNumber)), | ||
| el("span", "sign", sign), el("span", "code", line.text), meta)); | ||
| }); | ||
| panel.appendChild(el("div", "review-notes", | ||
| "Actor means a Quilt session or subagent, not necessarily a person or a single prompt. Prompt matches are inferred by time. " + | ||
| "Local prompts are read only when this panel opens and never leave this server. Claude Code and Codex transcripts are supported; other actors remain per-agent only. Unattributed lines can be legitimate.")); | ||
| } | ||
| function loadReview(path, panel) { | ||
| if (REVIEW_CACHE[path]) { | ||
| renderReviewData(REVIEW_CACHE[path], panel); | ||
| return; | ||
| } | ||
| panel.replaceChildren(el("div", "review-state", "Loading local provenance…")); | ||
| fetch("/api/blame?path=" + encodeURIComponent(path)).then(function (r) { | ||
| if (!r.ok) throw new Error("http " + r.status); | ||
| return r.json(); | ||
| }).then(function (d) { | ||
| REVIEW_CACHE[path] = d; | ||
| if (OPEN_REVIEWS[path]) renderReviewData(d, panel); | ||
| }).catch(function () { | ||
| panel.replaceChildren(el("div", "review-state", "Could not load this review.")); | ||
| }); | ||
| } | ||
| function render(d) { | ||
@@ -345,3 +448,22 @@ document.getElementById("repo").textContent = "· " + d.repo; | ||
| if (f.binary) badges.appendChild(badge("binary", "binary")); | ||
| table.appendChild(el("tr", null, el("td", "path", f.path), authors, badges)); | ||
| var toggle = el("button", "review-toggle" + (OPEN_REVIEWS[f.path] ? " open" : ""), f.path); | ||
| toggle.type = "button"; | ||
| toggle.setAttribute("aria-expanded", OPEN_REVIEWS[f.path] ? "true" : "false"); | ||
| var fileRow = el("tr", null, el("td", "path", toggle), authors, badges); | ||
| table.appendChild(fileRow); | ||
| var reviewCell = el("td", "review-cell"); | ||
| reviewCell.colSpan = 3; | ||
| var reviewPanel = el("div", "review"); | ||
| reviewCell.appendChild(reviewPanel); | ||
| var reviewRow = el("tr", null, reviewCell); | ||
| reviewRow.hidden = !OPEN_REVIEWS[f.path]; | ||
| table.appendChild(reviewRow); | ||
| toggle.addEventListener("click", function () { | ||
| OPEN_REVIEWS[f.path] = !OPEN_REVIEWS[f.path]; | ||
| reviewRow.hidden = !OPEN_REVIEWS[f.path]; | ||
| toggle.classList.toggle("open", OPEN_REVIEWS[f.path]); | ||
| toggle.setAttribute("aria-expanded", OPEN_REVIEWS[f.path] ? "true" : "false"); | ||
| if (OPEN_REVIEWS[f.path]) loadReview(f.path, reviewPanel); | ||
| }); | ||
| if (OPEN_REVIEWS[f.path]) loadReview(f.path, reviewPanel); | ||
| }); | ||
@@ -348,0 +470,0 @@ var wrap = el("div", "card", table); |
+1
-1
| { | ||
| "name": "@quilt-dev/cli", | ||
| "version": "0.5.0", | ||
| "version": "0.5.1", | ||
| "mcpName": "io.github.wkoverfield/quilt", | ||
@@ -5,0 +5,0 @@ "description": "Actor-owned patches for Git. Same repo. Many agents. Clean commits.", |
| // Sanity-check granted symbol claims against the file's actual symbols, so a | ||
| // typo'd target (`utils.js#formatPirce`) doesn't silently reserve nothing — | ||
| // the claimant walks away believing the real function is protected when no | ||
| // edit-time check will ever match it. | ||
| // | ||
| // Warnings, not denials: claiming a symbol you are ABOUT to add is a | ||
| // legitimate move (reserve the name before writing the function), so a missing | ||
| // symbol can't be an error. The warning tells the actor what Quilt can see and | ||
| // suggests a near-miss when one exists. | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { safeAbs } from "./authorship.js"; | ||
| import { canParse, parseSymbols } from "./symbols.js"; | ||
| /** | ||
| * Warnings for granted symbol claims whose symbol isn't in the file. Quiet for | ||
| * whole-file claims, denied claims, files that don't exist yet (creating a file | ||
| * is exactly when you'd pre-claim its symbols), and languages Quilt can't parse | ||
| * (no symbol list to check against — the claim still works whole-file-wise). | ||
| */ | ||
| export function verifyClaimTargets(store, results) { | ||
| const warnings = []; | ||
| for (const r of results) { | ||
| if (!r.granted || !r.symbol) | ||
| continue; | ||
| const abs = safeAbs(store.paths.repoRoot, r.path); | ||
| if (!abs || !existsSync(abs) || !canParse(r.path)) | ||
| continue; | ||
| let names; | ||
| try { | ||
| names = parseSymbols(r.path, readFileSync(abs, "utf8")).map((s) => s.name); | ||
| } | ||
| catch { | ||
| continue; // unreadable/unparseable — nothing to check against | ||
| } | ||
| if (names.includes(r.symbol)) | ||
| continue; | ||
| const near = closest(r.symbol, names); | ||
| warnings.push({ | ||
| target: `${r.path}#${r.symbol}`, | ||
| message: `symbol "${r.symbol}" not found in ${r.path}` + | ||
| (near ? ` — did you mean "${near}"?` : "") + | ||
| ` (claim granted anyway — fine if you're about to add it)`, | ||
| }); | ||
| } | ||
| return warnings; | ||
| } | ||
| /** The nearest existing symbol within a small edit distance, or null. */ | ||
| function closest(target, names) { | ||
| let best = null; | ||
| let bestDist = 3; // only suggest genuinely-close names (distance <= 2) | ||
| for (const n of names) { | ||
| const d = editDistance(target.toLowerCase(), n.toLowerCase(), bestDist); | ||
| if (d < bestDist) { | ||
| bestDist = d; | ||
| best = n; | ||
| } | ||
| } | ||
| return best; | ||
| } | ||
| /** Levenshtein distance, capped: returns `cap` when the true distance is >= cap. */ | ||
| function editDistance(a, b, cap) { | ||
| if (Math.abs(a.length - b.length) >= cap) | ||
| return cap; | ||
| let prev = Array.from({ length: b.length + 1 }, (_, i) => i); | ||
| for (let i = 1; i <= a.length; i++) { | ||
| const cur = [i]; | ||
| let rowMin = i; | ||
| for (let j = 1; j <= b.length; j++) { | ||
| const d = Math.min((prev[j] ?? cap) + 1, (cur[j - 1] ?? cap) + 1, (prev[j - 1] ?? cap) + (a[i - 1] === b[j - 1] ? 0 : 1)); | ||
| cur[j] = d; | ||
| rowMin = Math.min(rowMin, d); | ||
| } | ||
| if (rowMin >= cap) | ||
| return cap; | ||
| prev = cur; | ||
| } | ||
| return Math.min(prev[b.length] ?? cap, cap); | ||
| } |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
435898
3.51%32
3.23%9255
3.48%46
4.55%