diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d8c8664e..a9ccd17d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,8 +23,13 @@ name: pr # `frontend`, `smoke-test`, `docker` (api buildx), `docker-ui` — each carry # if: ${{ vars.SKIP_HEAVY_CI != 'true' }} # so they can be skipped en masse to conserve GitHub Actions minutes during a -# budget crunch. The 4 sub-minute jobs (backend fast-lane unit tests, DCO, -# secrets guard, gitleaks) always run, retaining baseline signal. +# budget crunch. The sub-minute jobs (backend fast-lane unit tests, DCO, +# secrets guard, gitleaks) always run, retaining baseline signal. The two +# legal-compliance gates — `license-headers` (SPDX) and `license-inventory` +# (Apache-2.0 dependency check) — also ALWAYS run regardless of this switch: +# they're cheap (no service containers, no test suite) and must not let a +# missing header or a new GPL/AGPL runtime dep slip onto main while heavy jobs +# are off. # # Activate (skip heavy jobs): gh variable set SKIP_HEAVY_CI --body true # Restore (run everything): gh variable delete SKIP_HEAVY_CI @@ -94,6 +99,145 @@ jobs: - name: pytest backend/tests/unit/ run: uv run pytest backend/tests/unit/ --no-cov -q + # ------------------------------------------------------------------------- + # License headers — REUSE (https://reuse.software/) SPDX compliance. + # ALWAYS runs (not gated by SKIP_HEAVY_CI): a legal-compliance gate must hold + # even during a CI-budget crunch. Cheap — no service containers, just resolve + # the `reuse` dev pin and lint every tracked file for an SPDX header (inline + # or via REUSE.toml). Fix locally with `uv run reuse annotate` + # (see CONTRIBUTING.md "License headers (SPDX)"). + # ------------------------------------------------------------------------- + license-headers: + name: license-headers + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + version: "0.5.7" + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install project deps (frozen) + run: uv sync --frozen + - name: Lint SPDX headers (reuse) + run: uv run reuse lint + + # ------------------------------------------------------------------------- + # Dependency license inventory — Apache-2.0 compatibility gate. ALWAYS runs + # (not gated by SKIP_HEAVY_CI). Regenerates docs/04_security/license-inventory.md + # from the locked closure (uv + pnpm) and fails if it's stale OR if any shipped + # dependency carries a forbidden (GPL/AGPL) or unclassified license. Needs both + # toolchains because the script spans Python (uv tree + pip-licenses) and + # frontend (pnpm licenses). Fix locally: + # `make license-inventory && git add docs/04_security/license-inventory.md`. + # ------------------------------------------------------------------------- + license-inventory: + name: license-inventory + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + version: "0.5.7" + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install Python deps (frozen) + run: uv sync --frozen + - uses: pnpm/action-setup@v6 + with: + version: 9 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: "pnpm" + cache-dependency-path: ui/pnpm-lock.yaml + - name: Install pnpm deps (frozen) + run: pnpm --dir ui install --frozen-lockfile + - name: Check license inventory + run: uv run python scripts/gen_license_inventory.py --check + + # ------------------------------------------------------------------------- + # Static checks — ALWAYS run (not gated by SKIP_HEAVY_CI). Bundles the cheap, + # service-free quality gates that would otherwise be skipped during a CI-budget + # crunch: backend ruff + mypy + the bash drift-guards, and frontend prettier + + # eslint + tsc + vitest. No Postgres/Redis/ES/OpenSearch, no `next build`, no + # pytest — those stay in the heavy `backend` / `frontend` jobs. When the + # kill-switch is OFF these checks also run inside those jobs (minor, accepted + # duplication); when it's ON this job keeps static analysis + the frontend unit + # suite enforced on every PR. + # ------------------------------------------------------------------------- + static-checks: + name: static-checks (lint + types + fe unit, always-run) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + version: "0.5.7" + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install project deps (frozen) + run: uv sync --frozen + - name: Restore mypy + ruff caches + uses: actions/cache@v5 + with: + path: | + .mypy_cache + .ruff_cache + key: mypy-ruff-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + restore-keys: | + mypy-ruff-${{ runner.os }}- + + - uses: pnpm/action-setup@v6 + with: + version: 9 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: "pnpm" + cache-dependency-path: ui/pnpm-lock.yaml + - name: pnpm install + run: pnpm --dir ui install --frozen-lockfile + + - name: ruff check + run: uv run ruff check . + - name: Format check (ruff) + run: uv run ruff format --check . + - name: mypy --strict + run: uv run mypy backend/ + + - name: Verify source-of-truth enum comments + run: bash scripts/ci/verify_enum_source_of_truth.sh + - name: Verify demo-cluster slug parity + run: bash scripts/ci/verify_demo_slug_parity.sh + - name: Verify install.sh builds every buildable Compose service + run: bash scripts/ci/verify_install_builds_all_services.sh + + - name: Format check (prettier) + run: pnpm --dir ui exec prettier --check src package.json tsconfig.json eslint.config.mjs .prettierrc.json + - name: ESLint + run: pnpm --dir ui lint + - name: tsc --noEmit + run: pnpm --dir ui typecheck + - name: vitest + run: pnpm --dir ui test + backend: name: backend (lint + typecheck + tests + coverage) # TEMP CI-budget kill-switch — see the SKIP_HEAVY_CI note at the top of diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbbae02b..4a584853 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,27 @@ and fail if any tracked file is uncovered — fix a new file by annotating it or adding a glob to `REUSE.toml`. Full compliance is reported as "Congratulations! Your project is compliant…". +### Dependency licenses + +RelyLoop ships under Apache-2.0, which is incompatible with **strong copyleft +(GPL / AGPL)** in a *shipped* dependency. Every dependency is inventoried in +[`docs/04_security/license-inventory.md`](docs/04_security/license-inventory.md), +generated from the locked closure (`uv tree` + `pnpm licenses`): + +```bash +make license-inventory # or: uv run python scripts/gen_license_inventory.py +``` + +The `license-inventory` CI job runs the same script with `--check`; it fails if +the committed inventory is stale **or** if any shipped dependency carries a +forbidden (GPL/AGPL) or unclassified license. If you add a dependency: + +1. Run `make license-inventory` and commit the updated file. +2. If it introduces a non-permissive license, add an adjudication to the + `ADJUDICATIONS` map in [`scripts/gen_license_inventory.py`](scripts/gen_license_inventory.py). + Dev-only copyleft (e.g. the `reuse` linter, GPL-3.0) is fine — it never + ships. Strong copyleft in a runtime dependency is not; replace the dep. + ### Verifying the gitleaks hook To confirm secret scanning is wired correctly on your machine: diff --git a/Makefile b/Makefile index f6d05d30..57f3b4db 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ui-fmt ui-lint ui-typecheck ui-test ui-build ui-dev \ pre-commit pre-commit-install \ up down restart logs reset migrate migrate-create seed-clusters seed-es \ - dev dashboard + dev dashboard license-inventory help: ## Show this help message @echo "" @@ -185,3 +185,6 @@ migrate-create: ## Create new migration: make migrate-create name= (runs dashboard: ## Regenerate docs/00_overview/_dashboard.{html,md} from feature folders @python3 scripts/build_mvp1_dashboard.py + +license-inventory: ## Regenerate the dependency license inventory (docs/04_security/license-inventory.md) + uv run python scripts/gen_license_inventory.py diff --git a/docs/04_security/license-inventory.md b/docs/04_security/license-inventory.md new file mode 100644 index 00000000..0b71b14a --- /dev/null +++ b/docs/04_security/license-inventory.md @@ -0,0 +1,817 @@ +# Dependency License Inventory + +> **Generated file — do not edit by hand.** Regenerate with `python scripts/gen_license_inventory.py`. Per-package adjudications and license overrides live in that script (`ADJUDICATIONS` / `PY_LICENSE_OVERRIDES`). + +RelyLoop is distributed under **Apache-2.0**. Apache-2.0 is incompatible with **strong copyleft (GPL / AGPL)** in a *shipped* dependency. This inventory is derived from the locked dependency closure (`uv tree` + `pnpm licenses`), so it is identical in CI and locally regardless of ambient virtualenv state. Versions are omitted on purpose — they live in `uv.lock` / `ui/pnpm-lock.yaml`, and excluding them keeps routine bumps from churning this file. + +The `license-inventory` CI job runs `python scripts/gen_license_inventory.py --check`, which fails if (a) this file is stale, or (b) any **shipped** dependency carries a forbidden or unclassified license. + +## Flagged licenses (non-permissive) + +| Package | Ecosystem | License | Scope | Apache-2.0 compatible? | Decided action | +|---|---|---|---|---|---| +| certifi | Python | Mozilla Public License 2.0 (MPL 2.0) | runtime | Yes — weak copyleft (file-level), flagged | **Accept.** MPL-2.0 is file-level (weak) copyleft and is explicitly compatible with Apache-2.0 for mere aggregation/distribution; we ship certifi unmodified, so no source-disclosure obligation attaches to RelyLoop's own Apache-2.0 code. | +| pathspec | Python | Mozilla Public License 2.0 (MPL 2.0) | dev | Yes — weak copyleft (file-level), flagged | **Accept.** Dev-only (pulled by the `reuse`/pre-commit toolchain). MPL-2.0 file-level copyleft; never shipped in the runtime artifact. | +| psycopg2-binary | Python | GNU Library or Lesser General Public License (LGPL) | runtime | Yes — weak copyleft (file-level), flagged | **Accept.** LGPL-3.0 (with the OpenSSL exception). LGPL's copyleft is library-level: we use psycopg2 as an unmodified, dynamically-imported PostgreSQL driver and never modify its source, so no obligation attaches to RelyLoop's Apache-2.0 code. Shipping an unmodified LGPL library alongside permissive code is the canonical allowed case. | +| python-debian | Python | DFSG approved; GNU General Public License v2 or later (GPLv2+) | dev | **NO — strong copyleft** | **Accept.** Dev-only (transitive dep of the `reuse` SPDX linter). GPL-2.0+, but a build-time tool that is never imported, linked, or bundled into the distributed artifact — its copyleft does not reach shipped code. | +| reuse | Python | Apache Software License; CC0 1.0 Universal (CC0 1.0) Public Domain Dedication; DFSG approved; GNU General Public License v3 or later (GPLv3+); Other/Proprietary License | dev | **NO — strong copyleft** | **Accept.** Dev-only (the SPDX-header linter run by pre-commit + the `license-headers` CI job). GPL-3.0, but `reuse` is never imported, linked, or bundled into the distributed RelyLoop artifact — it is a build-time tool, so its copyleft does not reach distributed code. | +| tqdm | Python | MPL-2.0 AND MIT | runtime | Yes — weak copyleft (file-level), flagged | **Accept.** Dual-licensed MPL-2.0 AND MIT — the MIT grant alone is fully Apache-2.0-compatible, so we take it under MIT. (tqdm is a transitive progress-bar dep; shipped unmodified regardless.) | +| @img/sharp-libvips- | npm | LGPL-3.0-or-later | runtime | Yes — weak copyleft (file-level), flagged | **Accept.** LGPL-3.0 platform binary for `sharp` (image processing, transitively via Next.js). Shipped unmodified as a dynamically-loaded library; LGPL library-level copyleft imposes no obligation on RelyLoop's own Apache-2.0 code. Replaceable if ever needed. | +| axe-core | npm | MPL-2.0 | dev | Yes — weak copyleft (file-level), flagged | **Accept.** Dev-only (accessibility testing, transitive via the test tooling). MPL-2.0 file-level copyleft; never shipped. | +| lightningcss | npm | MPL-2.0 | dev | Yes — weak copyleft (file-level), flagged | **Accept.** Dev-only (CSS build tooling). MPL-2.0 file-level copyleft; never shipped in the runtime artifact. | + +## Full inventory + +| Package | Ecosystem | License | Scope | Apache-2.0 compatible? | +|---|---|---|---|---| +| alembic | Python | MIT | runtime | Yes | +| annotated-doc | Python | MIT | runtime | Yes | +| annotated-types | Python | MIT License | runtime | Yes | +| anyio | Python | MIT | runtime | Yes | +| arq | Python | MIT License | runtime | Yes | +| asgi-lifespan | Python | MIT | dev | Yes | +| ast_serialize | Python | MIT | dev | Yes | +| asyncpg | Python | Apache-2.0 | runtime | Yes | +| attrs | Python | MIT | dev | Yes | +| certifi | Python | Mozilla Public License 2.0 (MPL 2.0) | runtime | Yes — weak copyleft (file-level), flagged | +| cfgv | Python | MIT | dev | Yes | +| charset-normalizer | Python | MIT | dev | Yes | +| click | Python | BSD-3-Clause | runtime | Yes | +| colorlog | Python | MIT License | runtime | Yes | +| contourpy | Python | BSD License | runtime | Yes | +| coverage | Python | Apache-2.0 | dev | Yes | +| cycler | Python | BSD License | runtime | Yes | +| distlib | Python | Python Software Foundation License | dev | Yes | +| distro | Python | Apache Software License | runtime | Yes | +| docker | Python | Apache-2.0 | dev | Yes | +| execnet | Python | MIT | dev | Yes | +| fastapi | Python | MIT | runtime | Yes | +| filelock | Python | MIT | dev | Yes | +| fonttools | Python | MIT | runtime | Yes | +| greenlet | Python | MIT AND PSF-2.0 | runtime | Yes | +| h11 | Python | MIT License | runtime | Yes | +| hiredis | Python | MIT License | runtime | Yes | +| httpcore | Python | BSD-3-Clause | runtime | Yes | +| httptools | Python | MIT | runtime | Yes | +| httpx | Python | BSD License | runtime | Yes | +| identify | Python | MIT | dev | Yes | +| idna | Python | BSD-3-Clause | runtime | Yes | +| iniconfig | Python | MIT | dev | Yes | +| ir_measures | Python | Apache Software License | runtime | Yes | +| Jinja2 | Python | BSD License | runtime | Yes | +| jiter | Python | MIT | runtime | Yes | +| joblib | Python | BSD-3-Clause | runtime | Yes | +| kiwisolver | Python | BSD License | runtime | Yes | +| librt | Python | MIT | dev | Yes | +| license-expression | Python | Apache-2.0 | dev | Yes | +| Mako | Python | MIT License | runtime | Yes | +| MarkupSafe | Python | BSD-3-Clause | runtime | Yes | +| matplotlib | Python | Python Software Foundation License | runtime | Yes | +| mypy | Python | MIT | dev | Yes | +| mypy_extensions | Python | MIT | dev | Yes | +| nodeenv | Python | BSD License | dev | Yes | +| numpy | Python | BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 | runtime | Yes | +| openai | Python | Apache Software License | runtime | Yes | +| optuna | Python | MIT License | runtime | Yes | +| packaging | Python | Apache-2.0 OR BSD-2-Clause | runtime | Yes | +| pathspec | Python | Mozilla Public License 2.0 (MPL 2.0) | dev | Yes — weak copyleft (file-level), flagged | +| pillow | Python | MIT-CMU | runtime | Yes | +| platformdirs | Python | MIT | dev | Yes | +| pluggy | Python | MIT License | dev | Yes | +| pre_commit | Python | MIT | dev | Yes | +| psycopg2-binary | Python | GNU Library or Lesser General Public License (LGPL) | runtime | Yes — weak copyleft (file-level), flagged | +| pydantic | Python | MIT | runtime | Yes | +| pydantic_core | Python | MIT | runtime | Yes | +| pydantic-settings | Python | MIT | runtime | Yes | +| Pygments | Python | BSD-2-Clause | dev | Yes | +| PyJWT | Python | MIT | runtime | Yes | +| pyparsing | Python | MIT | runtime | Yes | +| pytest | Python | MIT | dev | Yes | +| pytest-asyncio | Python | Apache-2.0 | dev | Yes | +| pytest-cov | Python | MIT | dev | Yes | +| pytest-mock | Python | MIT License | dev | Yes | +| pytest-recording | Python | MIT | dev | Yes | +| pytest-xdist | Python | MIT | dev | Yes | +| python-dateutil | Python | Apache Software License; BSD License | runtime | Yes | +| python-debian | Python | DFSG approved; GNU General Public License v2 or later (GPLv2+) | dev | **NO — strong copyleft** | +| python-discovery | Python | MIT License | dev | Yes | +| python-dotenv | Python | BSD-3-Clause | runtime | Yes | +| python-magic | Python | MIT License | dev | Yes | +| pytrec_eval-terrier | Python | MIT License | runtime | Yes | +| PyYAML | Python | MIT License | runtime | Yes | +| redis | Python | MIT License | runtime | Yes | +| relyloop | Python | Apache Software License | runtime | Yes | +| requests | Python | Apache Software License | dev | Yes | +| reuse | Python | Apache Software License; CC0 1.0 Universal (CC0 1.0) Public Domain Dedication; DFSG approved; GNU General Public License v3 or later (GPLv3+); Other/Proprietary License | dev | **NO — strong copyleft** | +| ruff | Python | MIT | dev | Yes | +| scikit-learn | Python | BSD-3-Clause | runtime | Yes | +| scipy | Python | BSD License | runtime | Yes | +| six | Python | MIT License | runtime | Yes | +| sniffio | Python | Apache Software License; MIT License | runtime | Yes | +| SQLAlchemy | Python | MIT | runtime | Yes | +| starlette | Python | BSD-3-Clause | runtime | Yes | +| structlog | Python | MIT OR Apache-2.0 | runtime | Yes | +| testcontainers | Python | Apache-2.0 | dev | Yes | +| threadpoolctl | Python | BSD License | runtime | Yes | +| tomlkit | Python | MIT License | dev | Yes | +| tqdm | Python | MPL-2.0 AND MIT | runtime | Yes — weak copyleft (file-level), flagged | +| types-PyYAML | Python | Apache-2.0 | dev | Yes | +| typing_extensions | Python | PSF-2.0 | runtime | Yes | +| typing-inspection | Python | MIT | runtime | Yes | +| urllib3 | Python | MIT | dev | Yes | +| uuid_utils | Python | BSD-3-Clause | runtime | Yes | +| uvloop | Python | Apache Software License; MIT License | runtime | Yes | +| vcrpy | Python | MIT License | dev | Yes | +| virtualenv | Python | MIT | dev | Yes | +| watchfiles | Python | MIT License | runtime | Yes | +| websockets | Python | BSD-3-Clause | runtime | Yes | +| wrapt | Python | BSD-2-Clause | dev | Yes | +| @adobe/css-tools | npm | MIT | dev | Yes | +| @alloc/quick-lru | npm | MIT | dev | Yes | +| @asamuzakjp/css-color | npm | MIT | dev | Yes | +| @asamuzakjp/dom-selector | npm | MIT | dev | Yes | +| @asamuzakjp/generational-cache | npm | MIT | dev | Yes | +| @asamuzakjp/nwsapi | npm | MIT | dev | Yes | +| @babel/code-frame | npm | MIT | runtime | Yes | +| @babel/compat-data | npm | MIT | runtime | Yes | +| @babel/core | npm | MIT | runtime | Yes | +| @babel/generator | npm | MIT | runtime | Yes | +| @babel/helper-compilation-targets | npm | MIT | runtime | Yes | +| @babel/helper-globals | npm | MIT | runtime | Yes | +| @babel/helper-module-imports | npm | MIT | runtime | Yes | +| @babel/helper-module-transforms | npm | MIT | runtime | Yes | +| @babel/helper-string-parser | npm | MIT | runtime | Yes | +| @babel/helper-validator-identifier | npm | MIT | runtime | Yes | +| @babel/helper-validator-option | npm | MIT | runtime | Yes | +| @babel/helpers | npm | MIT | runtime | Yes | +| @babel/parser | npm | MIT | runtime | Yes | +| @babel/runtime | npm | MIT | dev | Yes | +| @babel/template | npm | MIT | runtime | Yes | +| @babel/traverse | npm | MIT | runtime | Yes | +| @babel/types | npm | MIT | runtime | Yes | +| @bramus/specificity | npm | MIT | dev | Yes | +| @bundled-es-modules/cookie | npm | ISC | dev | Yes | +| @bundled-es-modules/statuses | npm | ISC | dev | Yes | +| @bundled-es-modules/tough-cookie | npm | ISC | dev | Yes | +| @csstools/color-helpers | npm | MIT-0 | dev | Yes | +| @csstools/css-calc | npm | MIT | dev | Yes | +| @csstools/css-color-parser | npm | MIT | dev | Yes | +| @csstools/css-parser-algorithms | npm | MIT | dev | Yes | +| @csstools/css-syntax-patches-for-csstree | npm | MIT-0 | dev | Yes | +| @csstools/css-tokenizer | npm | MIT | dev | Yes | +| @eslint-community/eslint-utils | npm | MIT | dev | Yes | +| @eslint-community/regexpp | npm | MIT | dev | Yes | +| @eslint/config-array | npm | Apache-2.0 | dev | Yes | +| @eslint/config-helpers | npm | Apache-2.0 | dev | Yes | +| @eslint/core | npm | Apache-2.0 | dev | Yes | +| @eslint/eslintrc | npm | MIT | dev | Yes | +| @eslint/js | npm | MIT | dev | Yes | +| @eslint/object-schema | npm | Apache-2.0 | dev | Yes | +| @eslint/plugin-kit | npm | Apache-2.0 | dev | Yes | +| @exodus/bytes | npm | MIT | dev | Yes | +| @floating-ui/core | npm | MIT | runtime | Yes | +| @floating-ui/dom | npm | MIT | runtime | Yes | +| @floating-ui/react-dom | npm | MIT | runtime | Yes | +| @floating-ui/utils | npm | MIT | runtime | Yes | +| @hookform/resolvers | npm | MIT | runtime | Yes | +| @humanfs/core | npm | Apache-2.0 | dev | Yes | +| @humanfs/node | npm | Apache-2.0 | dev | Yes | +| @humanfs/types | npm | Apache-2.0 | dev | Yes | +| @humanwhocodes/module-importer | npm | Apache-2.0 | dev | Yes | +| @humanwhocodes/retry | npm | Apache-2.0 | dev | Yes | +| @img/colour | npm | MIT | runtime | Yes | +| @img/sharp- | npm | Apache-2.0 | runtime | Yes | +| @img/sharp-libvips- | npm | LGPL-3.0-or-later | runtime | Yes — weak copyleft (file-level), flagged | +| @inquirer/ansi | npm | MIT | dev | Yes | +| @inquirer/confirm | npm | MIT | dev | Yes | +| @inquirer/core | npm | MIT | dev | Yes | +| @inquirer/figures | npm | MIT | dev | Yes | +| @inquirer/type | npm | MIT | dev | Yes | +| @jridgewell/gen-mapping | npm | MIT | runtime | Yes | +| @jridgewell/remapping | npm | MIT | runtime | Yes | +| @jridgewell/resolve-uri | npm | MIT | runtime | Yes | +| @jridgewell/sourcemap-codec | npm | MIT | runtime | Yes | +| @jridgewell/trace-mapping | npm | MIT | runtime | Yes | +| @mswjs/interceptors | npm | MIT | dev | Yes | +| @next/env | npm | MIT | runtime | Yes | +| @next/eslint-plugin-next | npm | MIT | dev | Yes | +| @next/swc- | npm | MIT | runtime | Yes | +| @nodelib/fs.scandir | npm | MIT | dev | Yes | +| @nodelib/fs.stat | npm | MIT | dev | Yes | +| @nodelib/fs.walk | npm | MIT | dev | Yes | +| @nolyfill/is-core-module | npm | MIT | dev | Yes | +| @open-draft/deferred-promise | npm | MIT | dev | Yes | +| @open-draft/logger | npm | MIT | dev | Yes | +| @open-draft/until | npm | MIT | dev | Yes | +| @oxc-project/types | npm | MIT | dev | Yes | +| @playwright/test | npm | Apache-2.0 | runtime | Yes | +| @radix-ui/number | npm | MIT | runtime | Yes | +| @radix-ui/primitive | npm | MIT | runtime | Yes | +| @radix-ui/react-alert-dialog | npm | MIT | runtime | Yes | +| @radix-ui/react-arrow | npm | MIT | runtime | Yes | +| @radix-ui/react-collection | npm | MIT | runtime | Yes | +| @radix-ui/react-compose-refs | npm | MIT | runtime | Yes | +| @radix-ui/react-context | npm | MIT | runtime | Yes | +| @radix-ui/react-dialog | npm | MIT | runtime | Yes | +| @radix-ui/react-direction | npm | MIT | runtime | Yes | +| @radix-ui/react-dismissable-layer | npm | MIT | runtime | Yes | +| @radix-ui/react-focus-guards | npm | MIT | runtime | Yes | +| @radix-ui/react-focus-scope | npm | MIT | runtime | Yes | +| @radix-ui/react-id | npm | MIT | runtime | Yes | +| @radix-ui/react-label | npm | MIT | runtime | Yes | +| @radix-ui/react-popover | npm | MIT | runtime | Yes | +| @radix-ui/react-popper | npm | MIT | runtime | Yes | +| @radix-ui/react-portal | npm | MIT | runtime | Yes | +| @radix-ui/react-presence | npm | MIT | runtime | Yes | +| @radix-ui/react-primitive | npm | MIT | runtime | Yes | +| @radix-ui/react-roving-focus | npm | MIT | runtime | Yes | +| @radix-ui/react-select | npm | MIT | runtime | Yes | +| @radix-ui/react-slot | npm | MIT | runtime | Yes | +| @radix-ui/react-tabs | npm | MIT | runtime | Yes | +| @radix-ui/react-tooltip | npm | MIT | runtime | Yes | +| @radix-ui/react-use-callback-ref | npm | MIT | runtime | Yes | +| @radix-ui/react-use-controllable-state | npm | MIT | runtime | Yes | +| @radix-ui/react-use-effect-event | npm | MIT | runtime | Yes | +| @radix-ui/react-use-escape-keydown | npm | MIT | runtime | Yes | +| @radix-ui/react-use-layout-effect | npm | MIT | runtime | Yes | +| @radix-ui/react-use-previous | npm | MIT | runtime | Yes | +| @radix-ui/react-use-rect | npm | MIT | runtime | Yes | +| @radix-ui/react-use-size | npm | MIT | runtime | Yes | +| @radix-ui/react-visually-hidden | npm | MIT | runtime | Yes | +| @radix-ui/rect | npm | MIT | runtime | Yes | +| @redocly/ajv | npm | MIT | dev | Yes | +| @redocly/config | npm | MIT | dev | Yes | +| @redocly/openapi-core | npm | MIT | dev | Yes | +| @reduxjs/toolkit | npm | MIT | runtime | Yes | +| @rolldown/pluginutils | npm | MIT | dev | Yes | +| @rtsao/scc | npm | MIT | dev | Yes | +| @standard-schema/spec | npm | MIT | runtime | Yes | +| @standard-schema/utils | npm | MIT | runtime | Yes | +| @swc/helpers | npm | Apache-2.0 | runtime | Yes | +| @tailwindcss/node | npm | MIT | dev | Yes | +| @tailwindcss/oxide | npm | MIT | dev | Yes | +| @tailwindcss/postcss | npm | MIT | dev | Yes | +| @tailwindcss/typography | npm | MIT | dev | Yes | +| @tanstack/query-core | npm | MIT | runtime | Yes | +| @tanstack/query-devtools | npm | MIT | runtime | Yes | +| @tanstack/react-query | npm | MIT | runtime | Yes | +| @tanstack/react-query-devtools | npm | MIT | runtime | Yes | +| @tanstack/react-table | npm | MIT | runtime | Yes | +| @tanstack/table-core | npm | MIT | runtime | Yes | +| @testing-library/dom | npm | MIT | dev | Yes | +| @testing-library/jest-dom | npm | MIT | dev | Yes | +| @testing-library/react | npm | MIT | dev | Yes | +| @testing-library/user-event | npm | MIT | dev | Yes | +| @types/aria-query | npm | MIT | dev | Yes | +| @types/chai | npm | MIT | dev | Yes | +| @types/cookie | npm | MIT | dev | Yes | +| @types/d3-array | npm | MIT | runtime | Yes | +| @types/d3-color | npm | MIT | runtime | Yes | +| @types/d3-ease | npm | MIT | runtime | Yes | +| @types/d3-interpolate | npm | MIT | runtime | Yes | +| @types/d3-path | npm | MIT | runtime | Yes | +| @types/d3-scale | npm | MIT | runtime | Yes | +| @types/d3-shape | npm | MIT | runtime | Yes | +| @types/d3-time | npm | MIT | runtime | Yes | +| @types/d3-timer | npm | MIT | runtime | Yes | +| @types/debug | npm | MIT | runtime | Yes | +| @types/deep-eql | npm | MIT | dev | Yes | +| @types/estree | npm | MIT | runtime | Yes | +| @types/estree-jsx | npm | MIT | runtime | Yes | +| @types/hast | npm | MIT | runtime | Yes | +| @types/json-schema | npm | MIT | dev | Yes | +| @types/json5 | npm | MIT | dev | Yes | +| @types/mdast | npm | MIT | runtime | Yes | +| @types/ms | npm | MIT | runtime | Yes | +| @types/node | npm | MIT | dev | Yes | +| @types/prismjs | npm | MIT | runtime | Yes | +| @types/react | npm | MIT | runtime | Yes | +| @types/react-dom | npm | MIT | runtime | Yes | +| @types/statuses | npm | MIT | dev | Yes | +| @types/tough-cookie | npm | MIT | dev | Yes | +| @types/unist | npm | MIT | runtime | Yes | +| @types/use-sync-external-store | npm | MIT | runtime | Yes | +| @typescript-eslint/eslint-plugin | npm | MIT | dev | Yes | +| @typescript-eslint/parser | npm | MIT | dev | Yes | +| @typescript-eslint/project-service | npm | MIT | dev | Yes | +| @typescript-eslint/scope-manager | npm | MIT | dev | Yes | +| @typescript-eslint/tsconfig-utils | npm | MIT | dev | Yes | +| @typescript-eslint/type-utils | npm | MIT | dev | Yes | +| @typescript-eslint/types | npm | MIT | dev | Yes | +| @typescript-eslint/typescript-estree | npm | MIT | dev | Yes | +| @typescript-eslint/utils | npm | MIT | dev | Yes | +| @typescript-eslint/visitor-keys | npm | MIT | dev | Yes | +| @ungap/structured-clone | npm | ISC | runtime | Yes | +| @vitejs/plugin-react | npm | MIT | dev | Yes | +| @vitest/expect | npm | MIT | dev | Yes | +| @vitest/mocker | npm | MIT | dev | Yes | +| @vitest/pretty-format | npm | MIT | dev | Yes | +| @vitest/runner | npm | MIT | dev | Yes | +| @vitest/snapshot | npm | MIT | dev | Yes | +| @vitest/spy | npm | MIT | dev | Yes | +| @vitest/utils | npm | MIT | dev | Yes | +| acorn | npm | MIT | dev | Yes | +| acorn-jsx | npm | MIT | dev | Yes | +| agent-base | npm | MIT | dev | Yes | +| ajv | npm | MIT | dev | Yes | +| ansi-colors | npm | MIT | dev | Yes | +| ansi-regex | npm | MIT | dev | Yes | +| ansi-styles | npm | MIT | dev | Yes | +| argparse | npm | Python-2.0 | dev | Yes | +| aria-hidden | npm | MIT | runtime | Yes | +| aria-query | npm | Apache-2.0 | dev | Yes | +| array-buffer-byte-length | npm | MIT | dev | Yes | +| array-includes | npm | MIT | dev | Yes | +| array.prototype.findlast | npm | MIT | dev | Yes | +| array.prototype.findlastindex | npm | MIT | dev | Yes | +| array.prototype.flat | npm | MIT | dev | Yes | +| array.prototype.flatmap | npm | MIT | dev | Yes | +| array.prototype.tosorted | npm | MIT | dev | Yes | +| arraybuffer.prototype.slice | npm | MIT | dev | Yes | +| assertion-error | npm | MIT | dev | Yes | +| ast-types-flow | npm | MIT | dev | Yes | +| async-function | npm | MIT | dev | Yes | +| autoprefixer | npm | MIT | dev | Yes | +| available-typed-arrays | npm | MIT | dev | Yes | +| axe-core | npm | MPL-2.0 | dev | Yes — weak copyleft (file-level), flagged | +| axobject-query | npm | Apache-2.0 | dev | Yes | +| bail | npm | MIT | runtime | Yes | +| balanced-match | npm | MIT | dev | Yes | +| baseline-browser-mapping | npm | Apache-2.0 | runtime | Yes | +| bidi-js | npm | MIT | dev | Yes | +| brace-expansion | npm | MIT | dev | Yes | +| braces | npm | MIT | dev | Yes | +| browserslist | npm | MIT | runtime | Yes | +| call-bind | npm | MIT | dev | Yes | +| call-bind-apply-helpers | npm | MIT | dev | Yes | +| call-bound | npm | MIT | dev | Yes | +| callsites | npm | MIT | dev | Yes | +| caniuse-lite | npm | CC-BY-4.0 | runtime | Yes | +| ccount | npm | MIT | runtime | Yes | +| chai | npm | MIT | dev | Yes | +| chalk | npm | MIT | dev | Yes | +| change-case | npm | MIT | dev | Yes | +| character-entities | npm | MIT | runtime | Yes | +| character-entities-html4 | npm | MIT | runtime | Yes | +| character-entities-legacy | npm | MIT | runtime | Yes | +| character-reference-invalid | npm | MIT | runtime | Yes | +| class-variance-authority | npm | Apache-2.0 | runtime | Yes | +| cli-width | npm | ISC | dev | Yes | +| client-only | npm | MIT | runtime | Yes | +| cliui | npm | ISC | dev | Yes | +| clsx | npm | MIT | runtime | Yes | +| color-convert | npm | MIT | dev | Yes | +| color-name | npm | MIT | dev | Yes | +| colorette | npm | MIT | dev | Yes | +| comma-separated-tokens | npm | MIT | runtime | Yes | +| concat-map | npm | MIT | dev | Yes | +| convert-source-map | npm | MIT | runtime | Yes | +| cookie | npm | MIT | dev | Yes | +| cross-spawn | npm | MIT | dev | Yes | +| css-tree | npm | MIT | dev | Yes | +| css.escape | npm | MIT | dev | Yes | +| cssesc | npm | MIT | dev | Yes | +| csstype | npm | MIT | runtime | Yes | +| d3-array | npm | ISC | runtime | Yes | +| d3-color | npm | ISC | runtime | Yes | +| d3-ease | npm | BSD-3-Clause | runtime | Yes | +| d3-format | npm | ISC | runtime | Yes | +| d3-interpolate | npm | ISC | runtime | Yes | +| d3-path | npm | ISC | runtime | Yes | +| d3-scale | npm | ISC | runtime | Yes | +| d3-shape | npm | ISC | runtime | Yes | +| d3-time | npm | ISC | runtime | Yes | +| d3-time-format | npm | ISC | runtime | Yes | +| d3-timer | npm | ISC | runtime | Yes | +| damerau-levenshtein | npm | BSD-2-Clause | dev | Yes | +| data-urls | npm | MIT | dev | Yes | +| data-view-buffer | npm | MIT | dev | Yes | +| data-view-byte-length | npm | MIT | dev | Yes | +| data-view-byte-offset | npm | MIT | dev | Yes | +| debug | npm | MIT | runtime | Yes | +| decimal.js | npm | MIT | dev | Yes | +| decimal.js-light | npm | MIT | runtime | Yes | +| decode-named-character-reference | npm | MIT | runtime | Yes | +| deep-is | npm | MIT | dev | Yes | +| define-data-property | npm | MIT | dev | Yes | +| define-properties | npm | MIT | dev | Yes | +| dequal | npm | MIT | runtime | Yes | +| detect-libc | npm | Apache-2.0 | runtime | Yes | +| detect-node-es | npm | MIT | runtime | Yes | +| devlop | npm | MIT | runtime | Yes | +| doctrine | npm | Apache-2.0 | dev | Yes | +| dom-accessibility-api | npm | MIT | dev | Yes | +| dunder-proto | npm | MIT | dev | Yes | +| electron-to-chromium | npm | ISC | runtime | Yes | +| emoji-regex | npm | MIT | dev | Yes | +| enhanced-resolve | npm | MIT | dev | Yes | +| entities | npm | BSD-2-Clause | dev | Yes | +| es-abstract | npm | MIT | dev | Yes | +| es-define-property | npm | MIT | dev | Yes | +| es-errors | npm | MIT | dev | Yes | +| es-iterator-helpers | npm | MIT | dev | Yes | +| es-module-lexer | npm | MIT | dev | Yes | +| es-object-atoms | npm | MIT | dev | Yes | +| es-set-tostringtag | npm | MIT | dev | Yes | +| es-shim-unscopables | npm | MIT | dev | Yes | +| es-to-primitive | npm | MIT | dev | Yes | +| es-toolkit | npm | MIT | runtime | Yes | +| escalade | npm | MIT | runtime | Yes | +| escape-string-regexp | npm | MIT | runtime | Yes | +| eslint | npm | MIT | dev | Yes | +| eslint-config-next | npm | MIT | dev | Yes | +| eslint-import-resolver-node | npm | MIT | dev | Yes | +| eslint-import-resolver-typescript | npm | ISC | dev | Yes | +| eslint-module-utils | npm | MIT | dev | Yes | +| eslint-plugin-import | npm | MIT | dev | Yes | +| eslint-plugin-jsx-a11y | npm | MIT | dev | Yes | +| eslint-plugin-react | npm | MIT | dev | Yes | +| eslint-plugin-react-hooks | npm | MIT | dev | Yes | +| eslint-plugin-security | npm | Apache-2.0 | dev | Yes | +| eslint-scope | npm | BSD-2-Clause | dev | Yes | +| eslint-visitor-keys | npm | Apache-2.0 | dev | Yes | +| espree | npm | BSD-2-Clause | dev | Yes | +| esquery | npm | BSD-3-Clause | dev | Yes | +| esrecurse | npm | BSD-2-Clause | dev | Yes | +| estraverse | npm | BSD-2-Clause | dev | Yes | +| estree-util-is-identifier-name | npm | MIT | runtime | Yes | +| estree-walker | npm | MIT | dev | Yes | +| esutils | npm | BSD-2-Clause | dev | Yes | +| eventemitter3 | npm | MIT | runtime | Yes | +| expect-type | npm | Apache-2.0 | dev | Yes | +| extend | npm | MIT | runtime | Yes | +| fast-deep-equal | npm | MIT | dev | Yes | +| fast-glob | npm | MIT | dev | Yes | +| fast-json-stable-stringify | npm | MIT | dev | Yes | +| fast-levenshtein | npm | MIT | dev | Yes | +| fastq | npm | ISC | dev | Yes | +| fdir | npm | MIT | dev | Yes | +| file-entry-cache | npm | MIT | dev | Yes | +| fill-range | npm | MIT | dev | Yes | +| find-up | npm | MIT | dev | Yes | +| flat-cache | npm | MIT | dev | Yes | +| flatted | npm | ISC | dev | Yes | +| for-each | npm | MIT | dev | Yes | +| fraction.js | npm | MIT | dev | Yes | +| fsevents | npm | MIT | runtime | Yes | +| function-bind | npm | MIT | dev | Yes | +| function.prototype.name | npm | MIT | dev | Yes | +| functions-have-names | npm | MIT | dev | Yes | +| generator-function | npm | MIT | dev | Yes | +| gensync | npm | MIT | runtime | Yes | +| get-caller-file | npm | ISC | dev | Yes | +| get-intrinsic | npm | MIT | dev | Yes | +| get-nonce | npm | MIT | runtime | Yes | +| get-proto | npm | MIT | dev | Yes | +| get-symbol-description | npm | MIT | dev | Yes | +| get-tsconfig | npm | MIT | dev | Yes | +| glob-parent | npm | ISC | dev | Yes | +| globals | npm | MIT | dev | Yes | +| globalthis | npm | MIT | dev | Yes | +| gopd | npm | MIT | dev | Yes | +| graceful-fs | npm | ISC | dev | Yes | +| graphql | npm | MIT | dev | Yes | +| has-bigints | npm | MIT | dev | Yes | +| has-flag | npm | MIT | dev | Yes | +| has-property-descriptors | npm | MIT | dev | Yes | +| has-proto | npm | MIT | dev | Yes | +| has-symbols | npm | MIT | dev | Yes | +| has-tostringtag | npm | MIT | dev | Yes | +| hasown | npm | MIT | dev | Yes | +| hast-util-to-jsx-runtime | npm | MIT | runtime | Yes | +| hast-util-whitespace | npm | MIT | runtime | Yes | +| headers-polyfill | npm | MIT | dev | Yes | +| hermes-estree | npm | MIT | dev | Yes | +| hermes-parser | npm | MIT | dev | Yes | +| html-encoding-sniffer | npm | MIT | dev | Yes | +| html-url-attributes | npm | MIT | runtime | Yes | +| https-proxy-agent | npm | MIT | dev | Yes | +| ignore | npm | MIT | dev | Yes | +| immer | npm | MIT | runtime | Yes | +| import-fresh | npm | MIT | dev | Yes | +| imurmurhash | npm | MIT | dev | Yes | +| indent-string | npm | MIT | dev | Yes | +| index-to-position | npm | MIT | dev | Yes | +| inline-style-parser | npm | MIT | runtime | Yes | +| internal-slot | npm | MIT | dev | Yes | +| internmap | npm | ISC | runtime | Yes | +| is-alphabetical | npm | MIT | runtime | Yes | +| is-alphanumerical | npm | MIT | runtime | Yes | +| is-array-buffer | npm | MIT | dev | Yes | +| is-async-function | npm | MIT | dev | Yes | +| is-bigint | npm | MIT | dev | Yes | +| is-boolean-object | npm | MIT | dev | Yes | +| is-bun-module | npm | MIT | dev | Yes | +| is-callable | npm | MIT | dev | Yes | +| is-core-module | npm | MIT | dev | Yes | +| is-data-view | npm | MIT | dev | Yes | +| is-date-object | npm | MIT | dev | Yes | +| is-decimal | npm | MIT | runtime | Yes | +| is-extglob | npm | MIT | dev | Yes | +| is-finalizationregistry | npm | MIT | dev | Yes | +| is-fullwidth-code-point | npm | MIT | dev | Yes | +| is-generator-function | npm | MIT | dev | Yes | +| is-glob | npm | MIT | dev | Yes | +| is-hexadecimal | npm | MIT | runtime | Yes | +| is-map | npm | MIT | dev | Yes | +| is-negative-zero | npm | MIT | dev | Yes | +| is-node-process | npm | MIT | dev | Yes | +| is-number | npm | MIT | dev | Yes | +| is-number-object | npm | MIT | dev | Yes | +| is-plain-obj | npm | MIT | runtime | Yes | +| is-potential-custom-element-name | npm | MIT | dev | Yes | +| is-regex | npm | MIT | dev | Yes | +| is-set | npm | MIT | dev | Yes | +| is-shared-array-buffer | npm | MIT | dev | Yes | +| is-string | npm | MIT | dev | Yes | +| is-symbol | npm | MIT | dev | Yes | +| is-typed-array | npm | MIT | dev | Yes | +| is-weakmap | npm | MIT | dev | Yes | +| is-weakref | npm | MIT | dev | Yes | +| is-weakset | npm | MIT | dev | Yes | +| isarray | npm | MIT | dev | Yes | +| isexe | npm | ISC | dev | Yes | +| iterator.prototype | npm | MIT | dev | Yes | +| jiti | npm | MIT | dev | Yes | +| js-levenshtein | npm | MIT | dev | Yes | +| js-tokens | npm | MIT | runtime | Yes | +| js-yaml | npm | MIT | dev | Yes | +| jsdom | npm | MIT | dev | Yes | +| jsesc | npm | MIT | runtime | Yes | +| json-buffer | npm | MIT | dev | Yes | +| json-schema-traverse | npm | MIT | dev | Yes | +| json-stable-stringify-without-jsonify | npm | MIT | dev | Yes | +| json5 | npm | MIT | runtime | Yes | +| jsx-ast-utils | npm | MIT | dev | Yes | +| keyv | npm | MIT | dev | Yes | +| language-subtag-registry | npm | CC0-1.0 | dev | Yes | +| language-tags | npm | MIT | dev | Yes | +| levn | npm | MIT | dev | Yes | +| lightningcss | npm | MPL-2.0 | dev | Yes — weak copyleft (file-level), flagged | +| locate-path | npm | MIT | dev | Yes | +| lodash.merge | npm | MIT | dev | Yes | +| longest-streak | npm | MIT | runtime | Yes | +| loose-envify | npm | MIT | dev | Yes | +| lru-cache | npm | ISC | runtime | Yes | +| lucide-react | npm | ISC | runtime | Yes | +| lz-string | npm | MIT | dev | Yes | +| magic-string | npm | MIT | dev | Yes | +| markdown-table | npm | MIT | runtime | Yes | +| math-intrinsics | npm | MIT | dev | Yes | +| mdast-util-find-and-replace | npm | MIT | runtime | Yes | +| mdast-util-from-markdown | npm | MIT | runtime | Yes | +| mdast-util-gfm | npm | MIT | runtime | Yes | +| mdast-util-gfm-autolink-literal | npm | MIT | runtime | Yes | +| mdast-util-gfm-footnote | npm | MIT | runtime | Yes | +| mdast-util-gfm-strikethrough | npm | MIT | runtime | Yes | +| mdast-util-gfm-table | npm | MIT | runtime | Yes | +| mdast-util-gfm-task-list-item | npm | MIT | runtime | Yes | +| mdast-util-mdx-expression | npm | MIT | runtime | Yes | +| mdast-util-mdx-jsx | npm | MIT | runtime | Yes | +| mdast-util-mdxjs-esm | npm | MIT | runtime | Yes | +| mdast-util-phrasing | npm | MIT | runtime | Yes | +| mdast-util-to-hast | npm | MIT | runtime | Yes | +| mdast-util-to-markdown | npm | MIT | runtime | Yes | +| mdast-util-to-string | npm | MIT | runtime | Yes | +| mdn-data | npm | CC0-1.0 | dev | Yes | +| merge2 | npm | MIT | dev | Yes | +| micromark | npm | MIT | runtime | Yes | +| micromark-core-commonmark | npm | MIT | runtime | Yes | +| micromark-extension-gfm | npm | MIT | runtime | Yes | +| micromark-extension-gfm-autolink-literal | npm | MIT | runtime | Yes | +| micromark-extension-gfm-footnote | npm | MIT | runtime | Yes | +| micromark-extension-gfm-strikethrough | npm | MIT | runtime | Yes | +| micromark-extension-gfm-table | npm | MIT | runtime | Yes | +| micromark-extension-gfm-tagfilter | npm | MIT | runtime | Yes | +| micromark-extension-gfm-task-list-item | npm | MIT | runtime | Yes | +| micromark-factory-destination | npm | MIT | runtime | Yes | +| micromark-factory-label | npm | MIT | runtime | Yes | +| micromark-factory-space | npm | MIT | runtime | Yes | +| micromark-factory-title | npm | MIT | runtime | Yes | +| micromark-factory-whitespace | npm | MIT | runtime | Yes | +| micromark-util-character | npm | MIT | runtime | Yes | +| micromark-util-chunked | npm | MIT | runtime | Yes | +| micromark-util-classify-character | npm | MIT | runtime | Yes | +| micromark-util-combine-extensions | npm | MIT | runtime | Yes | +| micromark-util-decode-numeric-character-reference | npm | MIT | runtime | Yes | +| micromark-util-decode-string | npm | MIT | runtime | Yes | +| micromark-util-encode | npm | MIT | runtime | Yes | +| micromark-util-html-tag-name | npm | MIT | runtime | Yes | +| micromark-util-normalize-identifier | npm | MIT | runtime | Yes | +| micromark-util-resolve-all | npm | MIT | runtime | Yes | +| micromark-util-sanitize-uri | npm | MIT | runtime | Yes | +| micromark-util-subtokenize | npm | MIT | runtime | Yes | +| micromark-util-symbol | npm | MIT | runtime | Yes | +| micromark-util-types | npm | MIT | runtime | Yes | +| micromatch | npm | MIT | dev | Yes | +| min-indent | npm | MIT | dev | Yes | +| minimatch | npm | BlueOak-1.0.0 | dev | Yes | +| minimist | npm | MIT | dev | Yes | +| ms | npm | MIT | runtime | Yes | +| msw | npm | MIT | dev | Yes | +| mute-stream | npm | ISC | dev | Yes | +| nanoid | npm | MIT | runtime | Yes | +| napi-postinstall | npm | MIT | dev | Yes | +| natural-compare | npm | MIT | dev | Yes | +| next | npm | MIT | runtime | Yes | +| next-themes | npm | MIT | runtime | Yes | +| node-exports-info | npm | MIT | dev | Yes | +| node-releases | npm | MIT | runtime | Yes | +| object-assign | npm | MIT | dev | Yes | +| object-inspect | npm | MIT | dev | Yes | +| object-keys | npm | MIT | dev | Yes | +| object.assign | npm | MIT | dev | Yes | +| object.entries | npm | MIT | dev | Yes | +| object.fromentries | npm | MIT | dev | Yes | +| object.groupby | npm | MIT | dev | Yes | +| object.values | npm | MIT | dev | Yes | +| obug | npm | MIT | dev | Yes | +| openapi-typescript | npm | MIT | dev | Yes | +| optionator | npm | MIT | dev | Yes | +| outvariant | npm | MIT | dev | Yes | +| own-keys | npm | MIT | dev | Yes | +| p-limit | npm | MIT | dev | Yes | +| p-locate | npm | MIT | dev | Yes | +| parent-module | npm | MIT | dev | Yes | +| parse-entities | npm | MIT | runtime | Yes | +| parse-json | npm | MIT | dev | Yes | +| parse5 | npm | MIT | dev | Yes | +| path-exists | npm | MIT | dev | Yes | +| path-key | npm | MIT | dev | Yes | +| path-parse | npm | MIT | dev | Yes | +| path-to-regexp | npm | MIT | dev | Yes | +| pathe | npm | MIT | dev | Yes | +| picocolors | npm | ISC | runtime | Yes | +| picomatch | npm | MIT | dev | Yes | +| playwright | npm | Apache-2.0 | runtime | Yes | +| playwright-core | npm | Apache-2.0 | runtime | Yes | +| pluralize | npm | MIT | dev | Yes | +| possible-typed-array-names | npm | MIT | dev | Yes | +| postcss | npm | MIT | runtime | Yes | +| postcss-selector-parser | npm | MIT | dev | Yes | +| postcss-value-parser | npm | MIT | dev | Yes | +| prelude-ls | npm | MIT | dev | Yes | +| prettier | npm | MIT | dev | Yes | +| pretty-format | npm | MIT | dev | Yes | +| prism-react-renderer | npm | MIT | runtime | Yes | +| prop-types | npm | MIT | dev | Yes | +| property-information | npm | MIT | runtime | Yes | +| psl | npm | MIT | dev | Yes | +| punycode | npm | MIT | dev | Yes | +| querystringify | npm | MIT | dev | Yes | +| queue-microtask | npm | MIT | dev | Yes | +| react | npm | MIT | runtime | Yes | +| react-dom | npm | MIT | runtime | Yes | +| react-hook-form | npm | MIT | runtime | Yes | +| react-is | npm | MIT | runtime | Yes | +| react-markdown | npm | MIT | runtime | Yes | +| react-redux | npm | MIT | runtime | Yes | +| react-remove-scroll | npm | MIT | runtime | Yes | +| react-remove-scroll-bar | npm | MIT | runtime | Yes | +| react-style-singleton | npm | MIT | runtime | Yes | +| recharts | npm | MIT | runtime | Yes | +| redent | npm | MIT | dev | Yes | +| redux | npm | MIT | runtime | Yes | +| redux-thunk | npm | MIT | runtime | Yes | +| reflect.getprototypeof | npm | MIT | dev | Yes | +| regexp-tree | npm | MIT | dev | Yes | +| regexp.prototype.flags | npm | MIT | dev | Yes | +| remark-gfm | npm | MIT | runtime | Yes | +| remark-parse | npm | MIT | runtime | Yes | +| remark-rehype | npm | MIT | runtime | Yes | +| remark-stringify | npm | MIT | runtime | Yes | +| require-directory | npm | MIT | dev | Yes | +| require-from-string | npm | MIT | dev | Yes | +| requires-port | npm | MIT | dev | Yes | +| reselect | npm | MIT | runtime | Yes | +| resolve | npm | MIT | dev | Yes | +| resolve-from | npm | MIT | dev | Yes | +| resolve-pkg-maps | npm | MIT | dev | Yes | +| reusify | npm | MIT | dev | Yes | +| rolldown | npm | MIT | dev | Yes | +| run-parallel | npm | MIT | dev | Yes | +| safe-array-concat | npm | MIT | dev | Yes | +| safe-push-apply | npm | MIT | dev | Yes | +| safe-regex | npm | MIT | dev | Yes | +| safe-regex-test | npm | MIT | dev | Yes | +| saxes | npm | ISC | dev | Yes | +| scheduler | npm | MIT | runtime | Yes | +| semver | npm | ISC | runtime | Yes | +| set-function-length | npm | MIT | dev | Yes | +| set-function-name | npm | MIT | dev | Yes | +| set-proto | npm | MIT | dev | Yes | +| sharp | npm | Apache-2.0 | runtime | Yes | +| shebang-command | npm | MIT | dev | Yes | +| shebang-regex | npm | MIT | dev | Yes | +| side-channel | npm | MIT | dev | Yes | +| side-channel-list | npm | MIT | dev | Yes | +| side-channel-map | npm | MIT | dev | Yes | +| side-channel-weakmap | npm | MIT | dev | Yes | +| siginfo | npm | ISC | dev | Yes | +| signal-exit | npm | ISC | dev | Yes | +| sonner | npm | MIT | runtime | Yes | +| source-map-js | npm | BSD-3-Clause | runtime | Yes | +| space-separated-tokens | npm | MIT | runtime | Yes | +| stable-hash | npm | MIT | dev | Yes | +| stackback | npm | MIT | dev | Yes | +| statuses | npm | MIT | dev | Yes | +| std-env | npm | MIT | dev | Yes | +| stop-iteration-iterator | npm | MIT | dev | Yes | +| strict-event-emitter | npm | MIT | dev | Yes | +| string-width | npm | MIT | dev | Yes | +| string.prototype.includes | npm | MIT | dev | Yes | +| string.prototype.matchall | npm | MIT | dev | Yes | +| string.prototype.repeat | npm | MIT | dev | Yes | +| string.prototype.trim | npm | MIT | dev | Yes | +| string.prototype.trimend | npm | MIT | dev | Yes | +| string.prototype.trimstart | npm | MIT | dev | Yes | +| stringify-entities | npm | MIT | runtime | Yes | +| strip-ansi | npm | MIT | dev | Yes | +| strip-bom | npm | MIT | dev | Yes | +| strip-indent | npm | MIT | dev | Yes | +| strip-json-comments | npm | MIT | dev | Yes | +| style-to-js | npm | MIT | runtime | Yes | +| style-to-object | npm | MIT | runtime | Yes | +| styled-jsx | npm | MIT | runtime | Yes | +| supports-color | npm | MIT | runtime | Yes | +| supports-preserve-symlinks-flag | npm | MIT | dev | Yes | +| symbol-tree | npm | MIT | dev | Yes | +| tailwind-merge | npm | MIT | runtime | Yes | +| tailwindcss | npm | MIT | dev | Yes | +| tapable | npm | MIT | dev | Yes | +| tiny-invariant | npm | MIT | runtime | Yes | +| tinybench | npm | MIT | dev | Yes | +| tinyexec | npm | MIT | dev | Yes | +| tinyglobby | npm | MIT | dev | Yes | +| tinyrainbow | npm | MIT | dev | Yes | +| tldts | npm | MIT | dev | Yes | +| tldts-core | npm | MIT | dev | Yes | +| to-regex-range | npm | MIT | dev | Yes | +| tough-cookie | npm | BSD-3-Clause | dev | Yes | +| tr46 | npm | MIT | dev | Yes | +| trim-lines | npm | MIT | runtime | Yes | +| trough | npm | MIT | runtime | Yes | +| ts-api-utils | npm | MIT | dev | Yes | +| tsconfig-paths | npm | MIT | dev | Yes | +| tslib | npm | 0BSD | runtime | Yes | +| type-check | npm | MIT | dev | Yes | +| type-fest | npm | (MIT OR CC0-1.0) | dev | Yes | +| typed-array-buffer | npm | MIT | dev | Yes | +| typed-array-byte-length | npm | MIT | dev | Yes | +| typed-array-byte-offset | npm | MIT | dev | Yes | +| typed-array-length | npm | MIT | dev | Yes | +| typescript | npm | Apache-2.0 | dev | Yes | +| typescript-eslint | npm | MIT | dev | Yes | +| unbox-primitive | npm | MIT | dev | Yes | +| undici | npm | MIT | dev | Yes | +| undici-types | npm | MIT | dev | Yes | +| unified | npm | MIT | runtime | Yes | +| unist-util-is | npm | MIT | runtime | Yes | +| unist-util-position | npm | MIT | runtime | Yes | +| unist-util-stringify-position | npm | MIT | runtime | Yes | +| unist-util-visit | npm | MIT | runtime | Yes | +| unist-util-visit-parents | npm | MIT | runtime | Yes | +| universalify | npm | MIT | dev | Yes | +| unrs-resolver | npm | MIT | dev | Yes | +| update-browserslist-db | npm | MIT | runtime | Yes | +| uri-js | npm | BSD-2-Clause | dev | Yes | +| uri-js-replace | npm | MIT | dev | Yes | +| url-parse | npm | MIT | dev | Yes | +| use-callback-ref | npm | MIT | runtime | Yes | +| use-sidecar | npm | MIT | runtime | Yes | +| use-sync-external-store | npm | MIT | runtime | Yes | +| util-deprecate | npm | MIT | dev | Yes | +| vfile | npm | MIT | runtime | Yes | +| vfile-message | npm | MIT | runtime | Yes | +| victory-vendor | npm | MIT AND ISC | runtime | Yes | +| vite | npm | MIT | dev | Yes | +| vitest | npm | MIT | dev | Yes | +| w3c-xmlserializer | npm | MIT | dev | Yes | +| webidl-conversions | npm | BSD-2-Clause | dev | Yes | +| whatwg-mimetype | npm | MIT | dev | Yes | +| whatwg-url | npm | MIT | dev | Yes | +| which | npm | ISC | dev | Yes | +| which-boxed-primitive | npm | MIT | dev | Yes | +| which-builtin-type | npm | MIT | dev | Yes | +| which-collection | npm | MIT | dev | Yes | +| which-typed-array | npm | MIT | dev | Yes | +| why-is-node-running | npm | MIT | dev | Yes | +| word-wrap | npm | MIT | dev | Yes | +| wrap-ansi | npm | MIT | dev | Yes | +| xml-name-validator | npm | Apache-2.0 | dev | Yes | +| xmlchars | npm | MIT | dev | Yes | +| y18n | npm | ISC | dev | Yes | +| yallist | npm | ISC | runtime | Yes | +| yaml-ast-parser | npm | Apache-2.0 | dev | Yes | +| yargs | npm | MIT | dev | Yes | +| yargs-parser | npm | ISC | dev | Yes | +| yocto-queue | npm | MIT | dev | Yes | +| yoctocolors-cjs | npm | MIT | dev | Yes | +| zod | npm | MIT | runtime | Yes | +| zod-validation-error | npm | MIT | dev | Yes | +| zwitch | npm | MIT | runtime | Yes | + +## Summary + +- Total dependencies in locked closure: **786** (324 shipped, 462 dev-only). +- Non-permissive licenses: **9** (all adjudicated above). diff --git a/pyproject.toml b/pyproject.toml index 69d07251..879e9fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,9 @@ dev = [ # pre-commit and CI to enforce that every new source file carries an SPDX # header; `reuse annotate` adds them. See REUSE.toml for the coverage map. "reuse>=4.0.0", + # pip-licenses — feeds scripts/gen_license_inventory.py (the dependency + # license-compatibility audit). Used by the `license-inventory` CI gate. + "pip-licenses>=5.0.0", ] [build-system] diff --git a/scripts/gen_license_inventory.py b/scripts/gen_license_inventory.py new file mode 100644 index 00000000..b4ad68ce --- /dev/null +++ b/scripts/gen_license_inventory.py @@ -0,0 +1,397 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +"""Generate the dependency license inventory (docs/04_security/license-inventory.md). + +RelyLoop is distributed under Apache-2.0, which is incompatible with strong +copyleft (GPL / AGPL) in a *shipped* dependency. This script inventories every +dependency in the locked closure, classifies each license against Apache-2.0, +and records the adjudication for any flagged license. + +Determinism is the whole point: the inventory is derived from the LOCKED +dependency closure (``uv tree`` + ``pnpm``), never from whatever happens to be +installed in the ambient virtualenv. A developer's polluted ``.venv`` (stale +packages from a previous branch) therefore can't change the output — CI's clean +``uv sync --frozen`` env and a local run produce byte-identical files. Versions +are deliberately excluded from the table so routine dependency bumps don't churn +the committed file; only a *new* dependency, a *removed* one, or a *changed +license* moves it. + +Usage:: + + python scripts/gen_license_inventory.py # rewrite the inventory + python scripts/gen_license_inventory.py --check # CI gate (see below) + +``--check`` does two things and exits non-zero on either failure: + +1. Regenerates the inventory into memory and diffs it against the committed + ``docs/04_security/license-inventory.md``. Drift (new/removed dep, changed + license) fails — fix by running the script without ``--check`` and committing. +2. Hard-fails if any *shipped* dependency (Python runtime closure or frontend + prod deps) carries a forbidden copyleft license (GPL / AGPL) or an + unclassifiable license with no adjudication. Dev-only copyleft is allowed + (it never ships) but is still listed. +""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +OUTPUT = REPO_ROOT / "docs" / "04_security" / "license-inventory.md" +UI_DIR = REPO_ROOT / "ui" + +# Licenses pip-licenses cannot read from package metadata ("UNKNOWN"). Each was +# verified by hand against the package's own LICENSE file / PyPI classifiers. +# A package that newly reports UNKNOWN and is NOT in this map is rendered as +# "UNKNOWN" and fails the --check gate if it ships, forcing classification. +PY_LICENSE_OVERRIDES = { + "certifi": "MPL-2.0", + "idna": "BSD-3-Clause", + "typing-extensions": "PSF-2.0", + "ply": "BSD-3-Clause", +} + +# Per-package adjudications for any license that is not plainly permissive. +# Keyed by normalized package name. This is the single source of truth for the +# "Decided action" column — editing it here is what updates the inventory. +ADJUDICATIONS = { + "reuse": ( + "**Accept.** Dev-only (the SPDX-header linter run by pre-commit + the " + "`license-headers` CI job). GPL-3.0, but `reuse` is never imported, " + "linked, or bundled into the distributed RelyLoop artifact — it is a " + "build-time tool, so its copyleft does not reach distributed code." + ), + "certifi": ( + "**Accept.** MPL-2.0 is file-level (weak) copyleft and is explicitly " + "compatible with Apache-2.0 for mere aggregation/distribution; we ship " + "certifi unmodified, so no source-disclosure obligation attaches to " + "RelyLoop's own Apache-2.0 code." + ), + "psycopg2-binary": ( + "**Accept.** LGPL-3.0 (with the OpenSSL exception). LGPL's copyleft is " + "library-level: we use psycopg2 as an unmodified, dynamically-imported " + "PostgreSQL driver and never modify its source, so no obligation " + "attaches to RelyLoop's Apache-2.0 code. Shipping an unmodified LGPL " + "library alongside permissive code is the canonical allowed case." + ), + "tqdm": ( + "**Accept.** Dual-licensed MPL-2.0 AND MIT — the MIT grant alone is " + "fully Apache-2.0-compatible, so we take it under MIT. (tqdm is a " + "transitive progress-bar dep; shipped unmodified regardless.)" + ), + "pathspec": ( + "**Accept.** Dev-only (pulled by the `reuse`/pre-commit toolchain). " + "MPL-2.0 file-level copyleft; never shipped in the runtime artifact." + ), + "python-debian": ( + "**Accept.** Dev-only (transitive dep of the `reuse` SPDX linter). " + "GPL-2.0+, but a build-time tool that is never imported, linked, or " + "bundled into the distributed artifact — its copyleft does not reach " + "shipped code." + ), + "@img/sharp-libvips-": ( + "**Accept.** LGPL-3.0 platform binary for `sharp` (image processing, " + "transitively via Next.js). Shipped unmodified as a dynamically-loaded " + "library; LGPL library-level copyleft imposes no obligation on " + "RelyLoop's own Apache-2.0 code. Replaceable if ever needed." + ), + "@img/sharp-": ( + "**Accept.** Apache-2.0 platform binary for `sharp` (the LGPL part is " + "the separate libvips binary, adjudicated above). Listed because the " + "platform-suffix canonicalization groups it; fully permissive." + ), + "axe-core": ( + "**Accept.** Dev-only (accessibility testing, transitive via the test " + "tooling). MPL-2.0 file-level copyleft; never shipped." + ), + "lightningcss": ( + "**Accept.** Dev-only (CSS build tooling). MPL-2.0 file-level " + "copyleft; never shipped in the runtime artifact." + ), +} + +# License classification. Order matters: copyleft checks run before permissive. +FORBIDDEN_PATTERNS = [ # strong copyleft — must NOT appear in a shipped dep + r"\bAGPL", + r"AFFERO", + r"\bGPL", # GPLv2/v3; LGPL is excluded below before this runs + r"GENERAL PUBLIC LICENSE", +] +WEAK_COPYLEFT_PATTERNS = [ # allowed but flagged for review + r"\bLGPL", + r"LESSER GENERAL PUBLIC", + r"\bMPL", + r"MOZILLA PUBLIC", + r"\bEPL", + r"ECLIPSE PUBLIC", + r"\bCDDL", +] +PERMISSIVE_PATTERNS = [ + r"\bMIT\b", + r"\bMIT\*", + r"\bBSD\b", + r"BSD-[0-9]", + r"\b0BSD\b", # BSD Zero Clause (tslib) + r"\bISC\b", + r"APACHE", + r"\bPSF\b", + r"PYTHON SOFTWARE FOUNDATION", + r"PYTHON-2", + r"\bCC0", + r"CC-BY-[0-9]", # Creative Commons Attribution (caniuse-lite data set) + r"UNLICENSE", + r"\bZLIB\b", + r"HPND", + r"BLUE ?OAK", # "Blue Oak" or "BlueOak-1.0.0" + r"BLUEOAK", +] + + +def _norm(name: str) -> str: + return name.strip().lower().replace("_", "-") + + +# pnpm reports only the OPTIONAL platform binaries for the *current* host +# (e.g. @img/sharp-libvips-darwin-arm64 on macOS, -linux-x64 on CI's runner). +# Left raw, the inventory would differ per platform and the --check gate would +# fail on every CI run. Collapse each platform variant to a single canonical +# "-" entry so the output is host-independent. +_PLATFORM_SUFFIX = re.compile( + r"-(darwin|linux|linuxmusl|win32|freebsd|wasm32)" + r"(-(x64|arm64|arm|ia32|s390x|ppc64|riscv64))?$" +) + + +def _canonical_npm(name: str) -> str: + return _PLATFORM_SUFFIX.sub("-", name) + + +def _run(cmd: list[str], cwd: Path | None = None) -> str: + # noqa: S603 — cmd is always a hardcoded literal arg list (uv / pnpm / + # pip-licenses), never user input. No shell=True; this is a dev/CI tool. + return subprocess.run( # noqa: S603 + cmd, cwd=cwd, capture_output=True, text=True, check=True + ).stdout + + +def _tree_names(extra_args: list[str]) -> set[str]: + """Package names in a ``uv tree`` closure (deduped, normalized).""" + out = _run(["uv", "tree", "--frozen", *extra_args], cwd=REPO_ROOT) + names: set[str] = set() + for line in out.splitlines(): + m = re.search(r"([a-zA-Z0-9_.-]+)\s+v[0-9]", line) + if m: + names.add(_norm(m.group(1))) + return names + + +def classify(license_str: str) -> str: + """Return one of: forbidden | weak-copyleft | permissive | unknown.""" + up = license_str.upper() + # Exclude LGPL from the GPL forbidden check first. + is_lgpl = bool(re.search(r"\bLGPL", up) or "LESSER GENERAL PUBLIC" in up) + if not is_lgpl and any(re.search(p, up) for p in FORBIDDEN_PATTERNS): + return "forbidden" + if any(re.search(p, up) for p in WEAK_COPYLEFT_PATTERNS): + return "weak-copyleft" + if any(re.search(p, up) for p in PERMISSIVE_PATTERNS): + return "permissive" + return "unknown" + + +def _compat_label(bucket: str) -> str: + return { + "permissive": "Yes", + "weak-copyleft": "Yes — weak copyleft (file-level), flagged", + "forbidden": "**NO — strong copyleft**", + "unknown": "**Unclassified — needs review**", + }[bucket] + + +def collect_python() -> list[dict]: + runtime = _tree_names(["--no-dev"]) + full = _tree_names([]) + raw = json.loads(_run(["uv", "run", "pip-licenses", "--format=json"], cwd=REPO_ROOT)) + rows: list[dict] = [] + for entry in raw: + norm = _norm(entry["Name"]) + if norm not in full: + continue # ambient-venv pollution; not in the locked closure + lic = entry.get("License", "UNKNOWN") + if lic in ("UNKNOWN", "", None): + lic = PY_LICENSE_OVERRIDES.get(norm, "UNKNOWN") + rows.append( + { + "ecosystem": "Python", + "name": entry["Name"], + "norm": norm, + "license": lic, + "scope": "runtime" if norm in runtime else "dev", + } + ) + return rows + + +def collect_frontend() -> list[dict]: + prod = json.loads(_run(["pnpm", "licenses", "list", "--prod", "--json"], cwd=UI_DIR)) + dev = json.loads(_run(["pnpm", "licenses", "list", "--dev", "--json"], cwd=UI_DIR)) + prod_names = {_norm(_canonical_npm(item["name"])) for items in prod.values() for item in items} + seen: dict[str, dict] = {} + for source, scope in ((prod, "runtime"), (dev, "dev")): + for lic, items in source.items(): + for item in items: + canon = _canonical_npm(item["name"]) + norm = _norm(canon) + # prod wins: a package used in both ships, so it's runtime. + eff_scope = "runtime" if norm in prod_names else scope + # Keep the first (or upgrade dev->runtime if seen later). + if norm in seen and seen[norm]["scope"] == "runtime": + continue + seen[norm] = { + "ecosystem": "npm", + "name": canon, + "norm": norm, + "license": lic, + "scope": eff_scope, + } + return list(seen.values()) + + +def render(rows: list[dict]) -> str: + rows = sorted(rows, key=lambda r: (r["ecosystem"], r["norm"])) + flagged = [r for r in rows if classify(r["license"]) != "permissive"] + + lines: list[str] = [] + lines.append("# Dependency License Inventory") + lines.append("") + lines.append( + "> **Generated file — do not edit by hand.** Regenerate with " + "`python scripts/gen_license_inventory.py`. Per-package adjudications " + "and license overrides live in that script " + "(`ADJUDICATIONS` / `PY_LICENSE_OVERRIDES`)." + ) + lines.append("") + lines.append( + "RelyLoop is distributed under **Apache-2.0**. Apache-2.0 is " + "incompatible with **strong copyleft (GPL / AGPL)** in a *shipped* " + "dependency. This inventory is derived from the locked dependency " + "closure (`uv tree` + `pnpm licenses`), so it is identical in CI and " + "locally regardless of ambient virtualenv state. Versions are omitted " + "on purpose — they live in `uv.lock` / `ui/pnpm-lock.yaml`, and " + "excluding them keeps routine bumps from churning this file." + ) + lines.append("") + lines.append( + "The `license-inventory` CI job runs " + "`python scripts/gen_license_inventory.py --check`, which fails if " + "(a) this file is stale, or (b) any **shipped** dependency carries a " + "forbidden or unclassified license." + ) + lines.append("") + + # --- Flagged section (the part humans actually care about) --------------- + lines.append("## Flagged licenses (non-permissive)") + lines.append("") + if not flagged: + lines.append("_None — every dependency is permissively licensed._") + else: + lines.append( + "| Package | Ecosystem | License | Scope | Apache-2.0 compatible? | Decided action |" + ) + lines.append("|---|---|---|---|---|---|") + for r in flagged: + action = ADJUDICATIONS.get( + r["norm"], "**Needs adjudication** — add to `ADJUDICATIONS`." + ) + lines.append( + f"| {r['name']} | {r['ecosystem']} | {r['license']} | " + f"{r['scope']} | {_compat_label(classify(r['license']))} | {action} |" + ) + lines.append("") + + # --- Full table ---------------------------------------------------------- + lines.append("## Full inventory") + lines.append("") + lines.append("| Package | Ecosystem | License | Scope | Apache-2.0 compatible? |") + lines.append("|---|---|---|---|---|") + for r in rows: + lines.append( + f"| {r['name']} | {r['ecosystem']} | {r['license']} | " + f"{r['scope']} | {_compat_label(classify(r['license']))} |" + ) + lines.append("") + + # --- Summary ------------------------------------------------------------- + n_runtime = sum(1 for r in rows if r["scope"] == "runtime") + n_dev = len(rows) - n_runtime + lines.append("## Summary") + lines.append("") + lines.append( + f"- Total dependencies in locked closure: **{len(rows)}** " + f"({n_runtime} shipped, {n_dev} dev-only)." + ) + lines.append(f"- Non-permissive licenses: **{len(flagged)}** (all adjudicated above).") + # Exactly one trailing newline so the end-of-file-fixer pre-commit hook + # doesn't rewrite the generated file and break --check determinism. + return "\n".join(lines) + "\n" + + +def violations(rows: list[dict]) -> list[str]: + """Shipped deps with a forbidden or unadjudicated-unknown license.""" + out: list[str] = [] + for r in rows: + if r["scope"] != "runtime": + continue + bucket = classify(r["license"]) + if bucket == "forbidden": + out.append( + f"{r['name']} ({r['ecosystem']}): {r['license']} — " + "strong copyleft in a shipped dependency" + ) + elif bucket == "unknown" and r["norm"] not in ADJUDICATIONS: + out.append(f"{r['name']} ({r['ecosystem']}): UNKNOWN license, no adjudication") + return out + + +def main() -> int: + check = "--check" in sys.argv[1:] + rows = collect_python() + collect_frontend() + content = render(rows) + + viol = violations(rows) + + if check: + current = OUTPUT.read_text() if OUTPUT.exists() else "" + drift = current != content + if drift: + print("ERROR: license-inventory.md is stale.", file=sys.stderr) + print( + "Run: python scripts/gen_license_inventory.py && git add " + "docs/04_security/license-inventory.md", + file=sys.stderr, + ) + if viol: + print("ERROR: forbidden/unclassified license in a shipped dependency:", file=sys.stderr) + for v in viol: + print(f" - {v}", file=sys.stderr) + return 1 if (drift or viol) else 0 + + OUTPUT.write_text(content) + print( + f"Wrote {OUTPUT.relative_to(REPO_ROOT)} ({len(rows)} dependencies, {len(viol)} violations)." + ) + if viol: + for v in viol: + print(f" VIOLATION: {v}") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uv.lock b/uv.lock index 09bf8637..67f92cbe 100644 --- a/uv.lock +++ b/uv.lock @@ -1215,6 +1215,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] +[[package]] +name = "pip-licenses" +version = "5.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prettytable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/18/ddd93af610a04f56a51a27095ddfe55238e1ec236f6758730a0d2c0b49f2/pip_licenses-5.5.5.tar.gz", hash = "sha256:60750c006adf7a0910347b726e8ee9fee3bc8d2e7c8307a5c4ec0776c8e2a276", size = 54955, upload-time = "2026-03-28T22:12:56.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9a/6acfdb8d463eac7cdae7534d35d72237eca63f5fbafe797289d8a5fae447/pip_licenses-5.5.5-py3-none-any.whl", hash = "sha256:f4c4c6d9e6a03612cf59f29f19dc8ab54904d82e055b8e191498f2279a224e14", size = 23247, upload-time = "2026-03-28T22:12:54.89Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" @@ -1249,6 +1261,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.12" @@ -1645,6 +1669,7 @@ dependencies = [ dev = [ { name = "asgi-lifespan" }, { name = "mypy" }, + { name = "pip-licenses" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1686,6 +1711,7 @@ requires-dist = [ dev = [ { name = "asgi-lifespan", specifier = ">=2.1" }, { name = "mypy", specifier = ">=1.13" }, + { name = "pip-licenses", specifier = ">=5.0.0" }, { name = "pre-commit", specifier = ">=4.6.0" }, { name = "pytest", specifier = ">=8.3" }, { name = "pytest-asyncio", specifier = ">=0.24" }, @@ -2210,6 +2236,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] + [[package]] name = "websockets" version = "16.0"