# Project export: SlopGuard

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Codex-powered PR review that catches what only AI-generated code gets wrong: hallucinated packages, silent scope creep, and dropped auth checks.
- Devpost: https://devpost.com/software/slopguard-s84wfx
- GitHub: https://github.com/vishal1610kamal/slopcheck
- Video: https://www.youtube.com/embed/IwJPEWBrV2c?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

A fascinating challenge highlighted in a 2025 USENIX Security study. Researchers evaluated 16 code-generation models across 576,000 Python and JavaScript code samples and discovered 205,474 unique hallucinated package names—packages that simply didn't exist. It's called slopsquatting. These fake package names being exploited by attackers as a weapon, register them on npm or PyPI, embed malware, and wait for developers to unknowingly install them. We saw an opportunity to contribute by building an intelligent review layer that automatically validates AI-generated dependencies before code is merged or deployed, helping developers catch hallucinated or malicious packages early. Our view is, if AI is accelerating software development, it should also be accompanied by smarter safeguards that make the ecosystem safer for everyone i.e. SlopGuard. How SlopGuard Works SlopGuard acts as an intelligent security reviewer for AI-generated code by performing the following checks on every Pull Request: Validate AI-Generated Dependencies: Extracts all newly added Python and JavaScript / TypeScript package imports and verifies them against the official npm and PyPI registries. It flags packages that don't exist, were recently registered, or have suspiciously limited metadata. Detect Unusual Code Changes: Identifies Pull Requests that modify significantly more files than described, helping reviewers spot unexpected scope creep. Protect Critical Security Logic: Detects the removal of authentication or authorization checks without an equivalent replacement, reducing the risk of accidental or malicious security regressions. Learn from Previous AI Hallucinations: Maintains a history of previously identified hallucinated package names and alerts reviewers when the same fake dependency appears again across future Pull Requests. Generate AI-Powered Risk Analysis: Sends only structured security findings—not the source code or raw diff—to GPT for analysis, which returns an overall risk score, a plain-language explanation, and actionable recommendations for reviewers. Provide Reliable Pull Request Feedback: Publishes a single, easy-to-understand review comment on the Pull Request. If the AI service is unavailable, SlopGuard automatically falls back to deterministic rule-based checks and clearly informs reviewers that AI analysis was skipped. Complement Existing Security Tools: Works alongside GitHub's Dependency Review and other security scanners, focusing specifically on AI-generated code risks instead of duplicating traditional dependency vulnerability checks. What Makes SlopGuard Different? We recognize that this problem is already being addressed by some excellent tools. For example, Slopcheck (https://github.com/vishal1610kamal/slopcheck) focuses on detecting hallucinated packages across multiple ecosystems, while Open Code Review (https://github.com/marketplace/actions/open-code-review) provides comprehensive AI-powered code reviews. SlopGuard takes a more focused approach instead of replacing these solutions. Our goal is to secure AI-generated code in Pull Requests. SlopGuard concentrates on Python and JavaScript ecosystems (PyPI and npm), validates AI-suggested dependencies, detects unexpected scope changes, identifies missing authentication or authorization checks, and combines all of these signals into a single, easy-to-understand risk assessment using GPT. Instead of presenting reviewers with a collection of isolated findings, SlopGuard provides one clear verdict with actionable insights, helping teams review AI-generated code faster and with greater confidence. How We Built SlopGuard SlopGuard built using Node.js 20, the GitHub Octokit SDK, and the OpenAI SDK, keeping the architecture intentionally simple. Before involving AI, SlopGuard performs its own deterministic security checks—such as validating package names against npm and PyPI, detecting unusual scope changes, and identifying removed authentication or authorization checks. Only these structured findings are sent to GPT for analysis, never the raw Pull Request code or diff. GPT then combines the evidence into a single risk score with a clear explanation and actionable recommendations for reviewers. One of our biggest design decisions was security. Although GitHub provides workflows that can access repository secrets on Pull Requests from forks, we intentionally avoided that approach because it could expose sensitive credentials to untrusted code. Instead, SlopGuard runs only in a secure workflow, uses trusted repository code, and skips forked Pull Requests in the first version. We also leveraged AI tools like Codex to accelerate development by generating the initial scaffolding, test cases, and registry integrations, while the security architecture, review logic, and AI decision-making workflow were carefully designed and implemented by us. Challenges We Faced One of our biggest lessons came while testing SlopGuard on our own Pull Requests. During one test, an invalid API key caused the GPT analysis to fail. However, the GitHub Action still posted a review comment based only on the rule-based checks, making it look as if the AI had reviewed the code successfully. That wasn't the experience we wanted, because transparency is essential when developers rely on AI for security insights. We fixed this by making the behavior explicit. Now, if GPT is unavailable for any reason, SlopGuard clearly states that the review is based only on deterministic rule-based checks and that AI analysis was skipped. We'd rather be honest about what happened than give developers a false sense of confidence. What We Learned One of our biggest takeaways was that AI works best as a decision-support tool, not as a replacement for security checks. SlopGuard first performs reliable rule-based validations, and then GPT brings those findings together into a simple, easy-to-understand risk summary that helps reviewers make faster and better decisions instead of interpreting multiple security signals on their own. We also learned how much AI can accelerate development. Codex helped us quickly build the initial code, registry integrations, and test cases, allowing us to focus our time on the parts that mattered most-designing a secure architecture, deciding what should be checked, and ensuring the tool remained transparent and trustworthy for developers. What's Next This is just the first version of SlopGuard, and we already have a clear roadmap for improving it. In future releases, we plan to suggest the most likely legitimate package when a hallucinated package is detected, use AI-powered tool calling to make package validation faster and more efficient, and leverage GPT with web search to verify packages that exist but appear suspicious or lack enough credibility. We also plan to update the same Pull Request comment instead of creating a new one after every code change, making reviews cleaner and less noisy. We intentionally left these features out of the first release so we could focus on building a solution that is simple, reliable, and trustworthy. Our priority was to get the core security checks right before adding more advanced capabilities.

## README (from the GitHub repository)

# SlopGuard

A fascinating challenge highlighted in a 2025 USENIX Security study. Researchers evaluated 16 code-generation models across 576,000 Python and JavaScript code samples and discovered 205,474 unique hallucinated package names—packages that simply didn't exist. It's called slopsquatting. These fake package names being exploited by attackers as a weapon, register them on npm or PyPI, embed malware, and wait for developers to unknowingly install them.

We saw an opportunity to contribute by building an intelligent review layer that automatically validates AI-generated dependencies before code is merged or deployed, helping developers catch hallucinated or malicious packages early. Our view is, if AI is accelerating software development, it should also be accompanied by smarter safeguards that make the ecosystem safer for everyone i.e. SlopGuard. [Read the paper and
results.](https://www.usenix.org/conference/usenixsecurity25/presentation/spracklen)

## What it reviews

1. Reads the PR description and changed-file patches through GitHub's pull
   request API.
2. Extracts package imports from added JavaScript/TypeScript `import` and
   `require` statements, and Python `import` / `from … import` statements.
3. Queries npm or PyPI concurrently for each unique package. Both registries
   provide a registration age; packages registered in the last 30 days are
   marked for provenance review. For those new packages, SlopGuard also uses
   release count and the presence of source/repository and maintainer metadata
   as soft review cues.
4. Applies two transparent heuristics: a description-to-files-changed ratio and
   removed auth/permission lines with no same-category replacement in that
   file.
5. Sends **structured signals only**—never the raw diff—to GPT-5.6 using
   Structured Outputs. The model returns a 0–100 score, level, explanation,
   flags, and packages to verify.
6. Posts one advisory issue comment on the PR. If GPT-5.6 is unavailable or
   returns malformed data, SlopGuard posts a deterministic fallback assessment
   with a visible disclosure instead of hiding the observed signals.
7. Persists names confirmed missing from a registry in a bounded repository
   Actions variable. On a later PR, it identifies a repeat offender and cites
   the previous PR number.

An unavailable npm or PyPI request is always reported as **couldn't verify**;
it is never treated as evidence that a package does not exist.

## Install

This repository already contains the workflow at
[.github/workflows/slopguard.yml](.github/workflows/slopguard.yml). Add an
`OPENAI_API_KEY` repository secret, then commit the repository to the project
you want to protect. The workflow runs for `opened`, `synchronize`, and
`reopened` `pull_request` events and asks for `contents: read`,
`pull-requests: write`, and `actions: write` permissions. The last permission
only enables bounded repeat-offender memory; the review itself still works if a
repository policy denies it.

To call the action from another workflow:

```yaml
name: SlopGuard

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  actions: write
  contents: read
  pull-requests: write

concurrency:
  group: slopguard-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  dependency-review:
    # This complements SlopGuard's import heuristics with GitHub's manifest
    # and lockfile dependency diff. Keep it advisory to match SlopGuard.
    if: ${{ github.event.pull_request.head.repo.fork == false }}
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Check out pull request
        uses: actions/checkout@v4
      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        continue-on-error: true
        with:
          deny-licenses: ${{ vars.SLOPGUARD_DENY_LICENSES }}
  review:
    if: ${{ github.event.pull_request.head.repo.fork == false }}
    runs-on: ubuntu-latest
    steps:
      - name: Run SlopGuard
        uses: vishal1610kamal/slopcheck@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          pr-number: ${{ github.event.pull_request.number }}
          repo-owner: ${{ github.repository_owner }}
          repo-name: ${{ github.event.repository.name }}
          deny-licenses: ${{ vars.SLOPGUARD_DENY_LICENSES }}

```

The action defaults to `gpt-5.6`; set its optional `model` input or the
`OPENAI_MODEL` environment variable to override it. GPT-5.6 and the Responses
API's JSON-schema Structured Outputs are documented by
[OpenAI](https://developers.openai.com/api/docs/guides/structured-outputs).

## Optional policy configuration

Set the repository Actions variable `SLOPGUARD_DENY_LICENSES` to a comma-
separated list of SPDX identifiers, such as `GPL-3.0-only,AGPL-3.0-only`.
SlopGuard then flags a newly imported package whose registry license field
matches one of those identifiers. This is an advisory policy match, not a
license-compliance engine or legal determination.

SlopGuard stores confirmed missing package names in the non-secret repository
Actions variable `SLOPGUARD_REPEAT_OFFENDERS`. It is capped at 100 package names
and 20 PR numbers per name. Delete that variable in repository settings to reset
the memory. The workflow needs `actions: write` to update it; if an organization
policy denies that permission, the review still runs but the memory feature is
disabled for that run.

## Dependency-manifest review

The bundled workflow also runs GitHub's Dependency Review action alongside
SlopGuard. It covers dependency-manifest and lockfile changes, including
GitHub's vulnerability and license data, instead of duplicating that analysis in
SlopGuard. Its step uses `continue-on-error: true`, so it remains advisory and
does not become a merge gate. Copy this separate job when installing SlopGuard
in another repository. See GitHub's [Dependency Review
documentation](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review)
for the managed dependency-diff checks it supplies:

```yaml
dependency-review:
  if: ${{ github.event.pull_request.head.repo.fork == false }}
  permissions:
    contents: read
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/dependency-review-action@v4
      continue-on-error: true
      with:
        deny-licenses: ${{ vars.SLOPGUARD_DENY_LICENSES }}
```

## Run and test locally

Node.js 20 or newer is required. Create a token with permission to read PRs and
write PR comments, then run:

```bash
npm ci
npm test

GITHUB_TOKEN=... \
OPENAI_API_KEY=... \
PR_NUMBER=123 \
REPO_OWNER=your-org \
REPO_NAME=your-repository \
node slopguard.js
```

The test suite uses Node's built-in test runner and mocks network and model
calls. It covers import extraction, registry outcomes (including failure),
parallel checks, scope creep, auth removal, structured synthesis, and comment
wording.

## Security and limitations

- The bundled workflow deliberately uses `pull_request`, checks out the base
  commit's action code, and skips forked PRs. GitHub withholds repository
  secrets and makes the token read-only for forked PRs; using `pull_request_target`
  merely to expose an OpenAI key would be an unsafe trade-off. A trusted,
  separately designed fork-review flow is outside v1.
- It only examines patches returned by GitHub. Binary files and very large diffs
  can lack a patch, so SlopGuard does not infer their contents.
- Dynamic imports, re-exports, dependency manifests, and ecosystems other than
  npm/PyPI are out of scope. Python import names do not always equal PyPI
  distribution names, so SlopGuard maps a few common cases (for example,
  `yaml` → `PyYAML`) but cannot reliably distinguish every local module from a
  third-party dependency.
- Registry existence and registry metadata—license, age, release count, source
  URL, and maintainer presence—do not 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 65 KB.
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- TypeScript (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (7 of 7)

```
.github/workflows/slopguard.yml
.gitignore
action.yml
package.json
README.md
slopguard.js
test/slopguard.test.js
```

### Dependencies

- package.json: @octokit/rest@^22.0.0, openai@^6.0.0

### Recent commits (newest first)

- Added security patch
- changes
- changes
- Add provenance and repeat-offender signals
- Disclose unavailable GPT synthesis
- Fix action manifest YAML syntax
- Build SlopGuard v1 GitHub Action

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "slopguard",
  "version": "1.0.0",
  "private": true,
  "description": "Advisory pull-request review for AI-code failure patterns.",
  "type": "module",
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "start": "node slopguard.js",
    "test": "node --test"
  },
  "dependencies": {
    "@octokit/rest": "^22.0.0",
    "openai": "^6.0.0"
  }
}

```

### action.yml

```yaml
name: SlopGuard
description: Advisory PR review for hallucinated packages, scope creep, and removed auth checks.
inputs:
  github-token:
    description: "Token with pull-requests: write permission and optional Actions-variable write permission."
    required: true
  openai-api-key:
    description: "OpenAI API key used for GPT-5.6 synthesis."
    required: true
  pr-number:
    description: Pull request number to review.
    required: true
  repo-owner:
    description: Repository owner.
    required: true
  repo-name:
    description: Repository name.
    required: true
  model:
    description: OpenAI model used to synthesize signals.
    required: false
    default: gpt-5.6
  deny-licenses:
    description: Comma-separated SPDX license identifiers to flag for manual review.
    required: false
    default: ""
runs:
  using: composite
  steps:
    - name: Install SlopGuard dependencies
      shell: bash
      working-directory: ${{ github.action_path }}
      run: npm ci --omit=dev --ignore-scripts
    - name: Review pull request
      shell: bash
      working-directory: ${{ github.action_path }}
      run: node slopguard.js
      env:
        GITHUB_TOKEN: ${{ inputs.github-token }}
        OPENAI_API_KEY: ${{ inputs.openai-api-key }}
        PR_NUMBER: ${{ inputs.pr-number }}
        REPO_OWNER: ${{ inputs.repo-owner }}
        REPO_NAME: ${{ inputs.repo-name }}
        OPENAI_MODEL: ${{ inputs.model }}
        SLOPGUARD_DENY_LICENSES: ${{ inputs.deny-licenses }}

```

### slopguard.js

```javascript
import { builtinModules } from "node:module";
import { fileURLToPath } from "node:url";
import { Octokit } from "@octokit/rest";
import OpenAI from "openai";

const DAY_MS = 24 * 60 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 8_000;
const RECENT_PACKAGE_DAYS = 30;
const REPEAT_OFFENDERS_VARIABLE = "SLOPGUARD_REPEAT_OFFENDERS";
const MAX_REPEAT_OFFENDERS = 100;
const MAX_REPEAT_PULLS = 20;
const JAVASCRIPT_EXTENSIONS = new Set([
  ".js",
  ".jsx",
  ".mjs",
  ".cjs",
  ".ts",
  ".tsx",
  ".mts",
  ".cts",
]);

const PYTHON_STANDARD_LIBRARY = new Set([
  "__future__", "abc", "argparse", "array", "ast", "asyncio", "base64",
  "builtins", "calendar", "cgi", "cmath", "cmd", "code", "collections",
  "concurrent", "contextlib", "contextvars", "copy", "csv", "ctypes",
  "dataclasses", "datetime", "decimal", "difflib", "dis", "email", "enum",
  "errno", "faulthandler", "filecmp", "fileinput", "fnmatch", "fractions",
  "ftplib", "functools", "gc", "getopt", "getpass", "gettext", "glob",
  "graphlib", "gzip", "hashlib", "heapq", "hmac", "html", "http", "imaplib",
  "imp", "importlib", "inspect", "io", "ipaddress", "itertools", "json",
  "keyword", "linecache", "locale", "logging", "lzma", "mailbox", "mailcap",
  "marshal", "math", "mimetypes", "multiprocessing", "netrc", "nntplib",
  "numbers", "operator", "optparse", "os", "pathlib", "pdb", "pickle",
  "pickletools", "pkgutil", "platform", "plistlib", "poplib", "posixpath",
  "pprint", "profile", "pstats", "pty", "py_compile", "pyclbr", "pydoc",
  "queue", "quopri", "random", "re", "readline", "reprlib", "resource",
  "rlcompleter", "runpy", "sched", "secrets", "select", "selectors", "shelve",
  "shlex", "shutil", "signal", "site", "smtpd", "smtplib", "sndhdr", "socket",
  "socketserver", "sqlite3", "ssl", "stat", "statistics", "string", "stringprep",
  "struct", "subprocess", "sunau", "symbol", "symtable", "sys", "sysconfig",
  "tabnanny", "tarfile", "telnetlib", "tempfile", "termios", "textwrap", "this",
  "threading", "time", "timeit", "tkinter", "token", "tokenize", "tomllib",
  "trace", "traceback", "tracemalloc", "tty", "turtle", "types", "typing",
  "unicodedata", "unittest", "urllib", "uu", "uuid", "venv", "warnings", "wave",
  "weakref", "webbrowser", "wsgiref", "xml", "xmlrpc", "zipapp", "zipfile", "zipimport",
  "zlib",
]);

// Python import names are not always their package-index distribution names.
// This deliberately small map prevents avoidable false "not found" reports for
// common, established dependencies; arbitrary application-local imports remain
// an inherent limitation of import-only analysis and are documented in README.
const PYTHON_MODULE_TO_DISTRIBUTION = new Map([
  ["yaml", "PyYAML"],
  ["bs4", "beautifulsoup4"],
  ["sklearn", "scikit-learn"],
  ["cv2", "opencv-python"],
  ["pil", "Pillow"],
  ["dotenv", "python-dotenv"],
  ["dateutil", "python-dateutil"],
  ["crypto", "pycryptodome"],
]);

const NODE_BUILTINS = new Set(
  builtinModules.flatMap((name) => [name, name.replace(/^node:/, "")]),
);

const SYNTHESIS_SCHEMA = {
  type: "object",
  additionalProperties: false,
  properties: {
    score: { type: "integer", minimum: 0, maximum: 100 },
    level: { type: "string", enum: ["low", "medium", "high", "critical"] },
    explanation: { type: "string" },
    flags: {
      type: "array",
      items: {
        type: "object",
        additionalProperties: false,
        properties: {
          category: { type: "string" },
          message: { type: "string" },
          location: { type: "string" },
        },
        required: ["category", "message", "location"],
      },
    },
    packages_to_verify: {
      type: "array",
      items: {
        type: "object",
        additionalProperties: false,
        properties: {
          name: { type: "string" },
          ecosystem: { type: "string", enum: ["npm", "pypi"] },
          reason: { type: "string" },
        },
        required: ["name", "ecosystem", "reason"],
      },
    },
  },
  required: ["score", "level", "explanation", "flags", "packages_to_verify"],
};

function extensionOf(filename) {
  const match = /\.[^.]+$/.exec(filename.toLowerCase());
  return match ? match[0] : "";
}

function packageNameFromJavaScriptSpecifier(specifier) {
  if (
    !specifier ||
    specifier.startsWith(".") ||
    specifier.startsWith("/") ||
    specifier.startsWith("#") ||
    specifier.includes(":") ||
    NODE_BUILTINS.has(specifier)
  ) {
    return null;
  }

  const parts = specifier.split("/");
  return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
}

function packageNameFromPythonModule(moduleName) {
  if (!moduleName || moduleName.startsWith(".")) return null;
  const packageName = moduleName.split(".")[0];
  if (PYTHON_STANDARD_LIBRARY.has(packageName)) return null;
  return PYTHON_MODULE_TO_DISTRIBUTION.get(packageName.toLowerCase()) || packageName;
}

/**
 * Converts a GitHub patch into added and removed source lines with line numbers.
 * GitHub omits a patch for binary and especially large files; callers receive no
 * lines in that case rather than attempting to infer content that was not sent.
 */
export function parsePatchLines(patch) {
  if (typeof patch !== "string" || patch.length === 0) return [];

  const result = [];
  let oldLine = null;
  let newLine = null;

  for (const text of patch.split("\n")) {
    const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(text);
    if (hunk) {
      oldLine = Number(hunk[1]);
      newLine = Number(hunk[2]);
      continue;
    }

    if (oldLine === null || newLine === null || text === "\\ No newline at end of file") {
      continue;
    }

    if (text.startsWith("+") && !text.startsWith("+++")) {
      result.push({ type: "added", line: newLine, content: text.slice(1) });
      newLine += 1;
    } else if (text.startsWith("-") && !text.startsWith("---")) {
      result.push({ type: "removed", line: oldLine, content: text.slice(1) });
      oldLine += 1;
    } else if (text.startsWith(
[truncated — 29060 more characters]
```

### test/slopguard.test.js

```javascript
import test from "node:test";
import assert from "node:assert/strict";

import {
  checkNpmPackage,
  checkPypiPackage,
  checkPackages,
  detectScopeCreep,
  extractPackageReferencesFromPatch,
  extractPackagesFromChangedFiles,
  findAuthDrops,
  formatComment,
  findRepeatOffenders,
  parseDeniedLicenses,
  parseRepeatOffenderMemory,
  run,
  synthesizeRisk,
  updateRepeatOffenderMemory,
} from "../slopguard.js";

function response(status, body = {}) {
  return {
    status,
    ok: status >= 200 && status < 300,
    json: async () => body,
  };
}

test("extracts npm package roots from added JS/TS import and require lines only", () => {
  const patch = [
    "@@ -1,3 +1,6 @@",
    " import fs from 'node:fs';",
    "+import thing from '@acme/widget/client';",
    "+const lodash = require('lodash/fp');",
    "-const old = require('removed-package');",
    "+const local = require('./local');",
  ].join("\n");

  assert.deepEqual(extractPackageReferencesFromPatch("src/example.ts", patch), [
    { ecosystem: "npm", name: "@acme/widget", file: "src/example.ts", line: 2 },
    { ecosystem: "npm", name: "lodash", file: "src/example.ts", line: 3 },
  ]);
});

test("extracts third-party Python imports while excluding the standard library", () => {
  const patch = [
    "@@ -10,0 +11,5 @@",
    "+import requests as http, yaml",
    "+from fastapi.security import OAuth2PasswordBearer",
    "+import json",
    "+from .local import helper",
  ].join("\n");

  assert.deepEqual(extractPackageReferencesFromPatch("api/service.py", patch), [
    { ecosystem: "pypi", name: "requests", file: "api/service.py", line: 11 },
    { ecosystem: "pypi", name: "PyYAML", file: "api/service.py", line: 11 },
    { ecosystem: "pypi", name: "fastapi", file: "api/service.py", line: 12 },
  ]);
});

test("deduplicates imported packages but retains every location", () => {
  const packages = extractPackagesFromChangedFiles([
    { filename: "one.js", patch: "@@ -0,0 +1 @@\n+import x from 'real-package';" },
    { filename: "two.js", patch: "@@ -0,0 +1 @@\n+const x = require('real-package/subpath');" },
  ]);

  assert.deepEqual(packages, [{
    ecosystem: "npm",
    name: "real-package",
    locations: [{ file: "one.js", line: 1 }, { file: "two.js", line: 1 }],
  }]);
});

test("npm registry checks distinguish missing packages from failures and calculate age", async () => {
  const fixedNow = Date.parse("2026-07-16T00:00:00.000Z");
  const fetchImpl = async (url) => {
    if (url.includes("real-package")) {
      return response(200, {
        time: { created: "2020-07-16T00:00:00.000Z" },
        license: "MIT",
        versions: { "1.0.0": {}, "2.0.0": {} },
        repository: { url: "https://github.com/acme/real-package.git" },
        maintainers: [{ name: "Acme" }],
      });
    }
    if (url.includes("missing-package")) return response(404);
    return response(503);
  };

  assert.deepEqual(await checkNpmPackage("real-package", { fetchImpl, now: fixedNow }), {
    ecosystem: "npm",
    name: "real-package",
    exists: true,
    registrationAgeDays: 2191,
    license: "MIT",
    releaseCount: 2,
    repositoryUrl: "https://github.com/acme/real-package.git",
    maintainerCount: 1,
    error: null,
  });
  assert.equal((await checkNpmPackage("missing-package", { fetchImpl })).exists, false);
  assert.equal((await checkNpmPackage("unavailable-package", { fetchImpl })).exists, null);
});

test("PyPI checks normalize names, calculate registration age, and treat failures as unverified", async () => {
  const seen = [];
  const fixedNow = Date.parse("2026-07-16T00:00:00.000Z");
  const found = await checkPypiPackage("My_Package", {
    fetchImpl: async (url) => {
      seen.push(url);
      return response(200, {
        info: {
          name: "my-package",
          license_expression: "MIT OR Apache-2.0",
          project_urls: { Source: "https://github.com/acme/my-package" },
          maintainer: "Acme",
        },
        releases: {
          "1.0.0": [{ upload_time_iso_8601: "2020-07-16T00:00:00.000Z" }],
          "2.0.0": [{ upload_time_iso_8601: "2025-07-16T00:00:00.000Z" }],
        },
      });
    },
    now: fixedNow,
  });
  const unavailable = await checkPypiPackage("network-down", {
    fetchImpl: async () => { throw new Error("offline"); },
  });

  assert.match(seen[0], /pypi\/my-package\/json$/);
  assert.equal(found.exists, true);
  assert.equal(found.registrationAgeDays, 2191);
  assert.equal(found.license, "MIT OR Apache-2.0");
  assert.equal(found.releaseCount, 2);
  assert.equal(found.repositoryUrl, "https://github.com/acme/my-package");
  assert.equal(found.maintainerCount, 1);
  assert.equal(unavailable.exists, null);
  assert.match(unavailable.error, /offline/);
});

test("license policy flags only explicitly denied SPDX identifiers", () => {
  const signals = {
    packages: [{ ecosystem: "npm", name: "copyleft-package", locations: [{ file: "src/x.js", line: 1 }] }],
    registryChecks: [{
      ecosystem: "npm",
      name: "copyleft-package",
      exists: true,
      registrationAgeDays: 1000,
      license: "MIT OR GPL-3.0-only",
    }],
    deniedLicenses: parseDeniedLicenses("GPL-3.0-only, AGPL-3.0-only"),
    scopeCreep: detectScopeCreep("", 1),
    authDrops: [],
  };
  const body = formatComment(signals, {
    source: "fallback",
    score: 15,
    level: "low",
    explanation: "A package license needs review.",
    flags: [],
    packages_to_verify: [],
  });

  assert.match(body, /matches the denied license 'GPL-3.0-ONLY'/);
  assert.match(body, /declares denied license GPL-3.0-ONLY/);
  assert.doesNotMatch(body, /AGPL-3.0-ONLY/);
});

test("repeat-offender memory ignores a rerun of the same PR and cites earlier PRs", () => {
  const firstMemory = updateRepeatOffenderMemory([], [{
    ecosystem: "npm",
    name: "repeated-fake-package",
    exists: false,
  }], 12);
  const serialized = JSON.stringify({ version: 1, entries: firstMemory });
  const parsed = parseRepeatOffenderMe
[truncated — 10058 more characters]
```

### .github/workflows/slopguard.yml

```yaml
name: SlopGuard

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  actions: write
  contents: read
  pull-requests: write

concurrency:
  group: slopguard-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  dependency-review:
    # This complements SlopGuard's import heuristics with GitHub's manifest
    # and lockfile dependency diff. Keep it advisory to match SlopGuard.
    if: ${{ github.event.pull_request.head.repo.fork == false }}
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Check out pull request
        uses: actions/checkout@v4
      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        continue-on-error: true
        with:
          deny-licenses: ${{ vars.SLOPGUARD_DENY_LICENSES }}
  review:
    # GitHub withholds secrets and gives a read-only token to forked PRs.
    # Keep this pull_request workflow rather than exposing secrets via pull_request_target.
    if: ${{ github.event.pull_request.head.repo.fork == false }}
    runs-on: ubuntu-latest
    steps:
      - name: Check out trusted action code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}
      - name: Run SlopGuard
        uses: ./
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          pr-number: ${{ github.event.pull_request.number }}
          repo-owner: ${{ github.repository_owner }}
          repo-name: ${{ github.event.repository.name }}
          deny-licenses: ${{ vars.SLOPGUARD_DENY_LICENSES }}

```