# Dependencies

Each dependency is code that people outside this project can change, and each one brings its own dependencies. We add a package only when it gives more than it costs in that trust. [ADR 0072](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0072-few-dependencies-owned-slivers-kept-foundations.md) records the decision, the incidents behind it, and the rejected alternatives. This page gives the current rules. [Supply-chain security in CI](/next/contributor/supply-chain-security.md) lists the controls that CI runs.

## The constraint

Each package in a lockfile is one more maintainer account or publish token that an attacker can take over. Thus, the number of packages is a cost, in addition to the size of the code. [ADR 0072](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0072-few-dependencies-owned-slivers-kept-foundations.md#context) gives the incidents that show it.

## The bar for a new dependency

A new package needs a reason in its pull request. Answer each item in the pull request body:

1. **What we use.** Name the functions or components that we import. If it is one or two small functions, read [When to own code instead](#when-to-own-code-instead).
2. **Transitive cost.** Give the change that `gate:dependency-baseline` reports. The section [The dependency baseline](#the-dependency-baseline) explains it.
3. **Install scripts.** The package and its dependencies run no install script. If one must run, add an `allowBuilds` entry in `pnpm-workspace.yaml` and give the reason.
4. **Provenance.** The version has npm provenance from a trusted publisher, or the maintainers publish it the same way for each release.
5. **Maintainers.** An organization or more than one active maintainer releases the package. A release in the last 12 months shows that it is maintained.
6. **Bundle bytes.** For a webapp or extension runtime package, give the gzip bytes that it adds to the production build.
7. **License.** The license permits use in an MIT project. Copyleft and unknown licenses need a maintainer decision.

A Gradle dependency answers items 1, 5 and 7. Its transitive cost is the change to the Gradle lockfiles. For its provenance, Gradle dependency verification checks the checksum of each artifact against `server/gradle/verification-metadata.xml`. [Updating Java dependencies](/next/contributor/local-development.md#updating-java-dependencies) describes that review. The dependency baseline, install scripts and bundle bytes apply only to npm packages.

A platform feature comes before a package. Examples are a Node built-in such as `util.parseArgs`, `util.styleText`, `fs.glob` or `fetch`, a browser API, or a feature of a framework that we already use.

In `webapp/` and `server/`, ask a maintainer before you add a dependency. Their package guides say so.

### Exceptions to the bar

Only a maintainer can accept a package that fails an item. Each accepted exception has a row here, with the condition that makes us examine it again.

| Package                                                  | Item           | Reason                                                                                                                                                                                                                                                                                                                                            | Examine again when                                                          |
| -------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `@isaacs/nonrecursive-glob-match@1.0.3`                  | 5. Maintainers | `minimatch` 11 needs it, and only `minimatch` 11 fixes GHSA-g3hv-75wg-m249. The single maintainer of `minimatch` released this code from the `minimatch` tree. We already ship `minimatch` with the same single maintainer. Thus, the trust boundary stays the same. The package has no dependencies, no install script and a permissive license. | `minimatch` gets an organization or a second maintainer, or npm provenance. |
| `@hylimo/cli@3.0.0-alpha.2` and its `@hylimo/*` packages | 5. Maintainers | One npm account publishes the packages, with npm provenance. The maintainer chose HyLiMo for the system diagrams. It runs only in `scripts/render-diagrams.ts`, at development time. The rendered SVGs are committed, so the docs build and the site do not run it. It has no install script and an MIT license.                                  | A stable 3.x release ships, or the project releases nothing for 12 months.  |

### Held versions

An update that breaks a budget or a suite leaves its batch, and stays out until a newer release fixes it. Each held version has a row, with the condition that makes us try it again.

| Package                                                   | Held at       | Reason                                                                                                                                                                                                                                             | Try again when                                                                     |
| --------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `react`, `react-dom`                                      | 19.2.8        | 19.3 adds 29,236 bytes to the entry, which every page loads. `react-dom` is part of the entry, so no route split can move it.                                                                                                                      | The entry budget has room for it.                                                  |
| `lucide-react`                                            | 1.41.0        | 1.64.0 adds 2,687 gzip bytes to the entry. The bundle budget in `webapp/tools/bundle-budget` allows 1,024.                                                                                                                                         | A release does not grow the entry.                                                 |
| `@base-ui/react`                                          | 1.8.0         | 1.9.0 adds 2,088 gzip bytes to the entry. The bundle budget allows 1,024. 1.9 also renders a disabled accordion trigger with the native `disabled` attribute instead of `aria-disabled`, so `ui/accordion.tsx` and the catalog story move with it. | A release does not grow the entry.                                                 |
| `@tanstack/react-query`, `@tanstack/react-query-devtools` | 5.102.8       | From 5.103, the devtools stop bundling their UI and depend on 12 packages, among them the Solid UI framework and its component library. The devtools are a runtime dependency, so these become production packages.                                | The devtools leave the production dependencies, or a release bundles its UI again. |
| `recharts`                                                | 3.8.0         | 3.9 deprecates the legend's `verticalAlign`, which `ChartLegendContent` in the vendored `ui/chart.tsx` reads, so `no-deprecated` fails the type-aware lint.                                                                                        | The shadcn registry's `chart` moves to `position`.                                 |
| `monaco-editor`                                           | 0.56.0        | 0.57.0 adds about 30,000 gzip bytes to each page with the code editor. No feature that we use needs 0.57.                                                                                                                                          | We need a feature of a newer release, or a release does not grow these pages.      |
| `msw`, `msw-storybook-addon`                              | 2.15.0, 2.0.7 | Thirteen route tests fail on 3.0.3. A handler sees a request's `credentials` as `same-origin` when the app sends `omit`. A test of a response body in transit times out.                                                                           | A release keeps the request as the app sent it.                                    |
| `motion`                                                  | 13.5.1        | 14.8.0 adds 4,612 gzip bytes to the landing page. The bundle budget in `webapp/tools/bundle-budget` allows 1,024. No feature that we use needs 14.                                                                                                 | A release does not grow the page, or we need a feature of 14.                      |
| `entities`                                                | 8.1.0         | 9.0.0 adds a second copy to the tree for one lint rule. Its synchronous decoding, which we use, does not change.                                                                                                                                   | Other packages in the tree move to 9.                                              |
| `com.graphql-java:graphql-java`                           | 25.0          | Every release shades Guava 32.1.2-jre. `security/vulnerability-policy.json` has a reviewed CVE-2026-102554 decision for the 25.0 jar only. The 25.1 jar that Spring Boot 4.1.2 manages has not been reviewed.                                      | A release shades a fixed Guava, or a newer jar gets its own reviewed decision.     |

## When to own code instead

Own the code when all of these are true:

* We use a small part of the package, such as one or two functions.
* That part is small enough to review in one sitting, typically less than 200 lines with its tests.
* That part is stable. It implements a fixed format or a simple algorithm, not a moving specification.
* No platform feature does the same work.

Keep the package when it is a foundation. Foundations include React, TanStack, Base UI, Spring and parsers for complex formats. Code that a security property depends on is also a foundation, such as an HTML sanitizer or a cryptographic library. Keep the large, well-run foundations, but remove the optional add-ons that we do not use.

Owned code has a cost that a package does not have. Renovate does not update it, and an advisory against the upstream package does not reach us automatically. Thus, own only a part that has no security history and that changes rarely.

## How owned code lives

* **One place.** Owned code is one module in the tree that uses it, such as `webapp/src/lib/` or `scripts/lib/`. Do not copy it into a second tree. Import it from the first.
* **A header.** Each file of owned code starts with a comment that gives its source. A tool reads it, so the format is exact. [The owned-code header](#the-owned-code-header) gives it.
* **The license notice.** MIT, ISC and BSD licenses require that the copyright line and the notice stay with the code. Put them in the header.
* **Tests.** Port the upstream tests for the part that we keep. Add tests for our callers. A module without tests does not replace a package.
* **The SBOM entry.** Each owned module in shipped code needs a component in the release SBOM. Its upstream package is the pedigree ancestor of that component. Thus, the license and the provenance stay true. Do not merge owned code into a shipped tree before the release SBOM lists it. [Release management](/next/contributor/release-management.md#supply-chain-evidence) describes the SBOM.
* **A patch is an owned fork.** A `patchedDependencies` patch changes a package that we install, so it is owned code. A directory notice named NOTICE.md beside the patch records its source, its changes and their reasons, and its tests. It also names who makes the patch again for a new version. [`docker/agents/pi/patches/NOTICE.md`](https://github.com/hephaestus-build/Hephaestus/blob/main/docker/agents/pi/patches/NOTICE.md) is the example.

## The owned-code header

The header is the first comment in the file: `/** … */` in TypeScript and JavaScript, `/* … */` in CSS. It has four fields, each on its own line, in this order and once each. A line can start with `*` and spaces.

| Field         | Value                                                                                                                                                           |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Owned from:` | An `https` URL of the upstream source at a tag or a commit, not at a branch                                                                                     |
| `Package:`    | The canonical [package URL](https://github.com/package-url/purl-spec) of the upstream release, such as `pkg:npm/%40gitlab/svgs@3.164.0`. A scope's `@` is `%40` |
| `License:`    | The upstream license as an expression of [SPDX license identifiers](https://spdx.org/licenses/), such as `MIT`                                                  |
| `Changes:`    | One line: what we kept, removed or changed                                                                                                                      |

After the fields, an empty line and the upstream copyright line and license notice follow.

```
/**

 * Owned from: https://github.com/<owner>/<repository>/blob/<tag>/<path>

 * Package: pkg:npm/<name>@<version>

 * License: MIT

 * Changes: <what we kept, removed or changed>

 *

 * <the upstream copyright line and license notice>

 */
```

Shipped code is each file under `webapp/src/`, `extension/src/`, `docker/` and `server/application/src/main/`, except the package patches that [Owned static assets](#owned-static-assets) covers. A symbolic link in shipped code must point inside these directories. Test files, story files, and files in `test/` and `__tests__/` directories are not shipped code. In shipped code, a file with an `Owned from:` line in its first 40 lines is owned code. Such a file needs a complete header and a component in the release SBOM. Run `vp run gate:owned-code` to check these headers before merge.

The gate checks the header structure and npm coordinates. Reviewers verify the SPDX expression and the fixed upstream tag or commit.

## Owned static assets

Static assets under `webapp/public/` and `extension/public/`, and package patches under `docker/agents/pi/patches/`, use a directory notice. A directory enters this inventory when its notice contains an `Owned from:` line. Keep the asset bytes unchanged. Put the four fields from [the owned-code header](#the-owned-code-header) in a directory notice named NOTICE.md. For example, `webapp/public/brand/ai/NOTICE.md` can declare the brand marks.

Each upstream has its own block. Add a fifth field, `Files:`, after `Changes:`. Its value is a JSON array of explicit sibling basenames on one line:

```
Owned from: https://github.com/<owner>/<repository>/tree/<tag>

Package: pkg:npm/<name>@<version>

License: MIT

Changes: Keep unmodified marks.

Files: ["first.svg", "second.svg"]
```

A non-npm upstream can use a versioned package URL such as `pkg:github/ls1intum/edutelligence@<full-commit>`. Keep directories with ownership blocks flat. Use no paths or globs in `Files:`. Each asset must exist and belong to exactly one block. The gate rejects missing files, undeclared assets, duplicate declarations and symbolic links. Only the notice itself and license filenames that match `^LICENSE(?:-[A-Z0-9-]+)?$` are excluded from asset coverage.

The release records each declared file's raw SHA-256 and the same license and pedigree fields as owned code.

## The dependency baseline

`security/dependency-baseline.json` records, for each lockfile and workspace, the number of direct and transitive packages. It also records the packages of pnpm's env document: the package manager and the config dependencies, which run during an install. `gate:dependency-baseline` compares it with the lockfiles and fails when a count rises above the baseline. Thus, an addition that takes a count above the baseline raises the baseline in its pull request. The review sees the cost beside the change.

A count below the baseline passes, and the gate names the lower value. Any later pull request can lower the baseline in passing. Thus, removals, and updates that stay within the baseline, merge in parallel.

To raise the baseline with an addition, or to lower it after a removal, run:

```
node scripts/check-dependency-baseline.ts --write
```

## How to remove a dependency

1. Find each import site with `git grep -n "<package>"`. Include configuration files, `package.json` scripts and CSS `@import` and `@plugin` lines.
2. Replace each use with a platform feature or owned code, or delete the use.
3. Remove the package from its `package.json`.
4. Run `pnpm install`.
5. Find each `pnpm-workspace.yaml` entry that names the package: `allowBuilds`, `overrides`, `minimumReleaseAgeExclude`, `packageExtensions` and `patchedDependencies`.
6. For each entry, run `pnpm why -r <package>` in each lockfile directory, so that every workspace importer is searched. Remove the entry only if no package in any lockfile still needs it. A security override for a package that stays as a transitive dependency must stay.
7. If you removed an entry, run `pnpm install` again. The lockfile records the workspace settings.
8. Update the dependency baseline.
9. Run the tests of each tree that used the package.
10. If the package was in shipped code, add a changeset.

## Install hardening

`pnpm-workspace.yaml` owns the install policy, such as the release age and the packages that can run install scripts. Its comments give the reason for each setting. pnpm reads the nearest `pnpm-workspace.yaml`. Thus, the two agent images resolve with their own files in `docker/agents/pi/` and `docker/agents/practice-scripts/`. [Supply-chain security in CI](/next/contributor/supply-chain-security.md) describes how CI and Renovate apply the policy.

## Next steps

* [Supply-chain security in CI](/next/contributor/supply-chain-security.md)
* [Vulnerability remediation](/next/contributor/vulnerability-remediation.md)
* [Release management](/next/contributor/release-management.md#supply-chain-evidence)
