Skip to content

ADR 0136: Third-party notices derive from the compile graph

Status: accepted — replaces the hand-maintained notices manifest (launch task 1.2, never ADR'd). Applies the generated-artifact discipline of ADR 0026 and the forcing-function guard pattern of ADR 0051 to license attribution.

Context

§

Distributing the compiled binary means reproducing the copyright notice and license text of every third-party component it embeds — MIT, ISC, and BSD all require it. The first cut was a hand-maintained manifest: a TypeScript array of packages, versions, and copyright lines, plus one shared body per license family. Tests tied it to deno.lock and the src/ imports.

It had two defects, one structural and one factual.

Structural: the manifest was a second source of truth. The resolver already knows the component set, and the packages already ship their own license texts. A hand-copied mirror of both can only drift, and every dependency bump demanded a parallel edit under test duress.

Factual: the manifest excluded the MCP SDK's HTTP/SSE transport dependencies (express, hono, cors, jose, and their closures). The stated reasoning: they are "not reachable from the stdio entry points discern imports." That is file-level, ESM-shaped reasoning — but deno compile embeds npm dependencies at package granularity. There is no tree-shaking inside npm packages: the whole dependency closure of an imported package ships. Strings in the released binary confirmed it: express-rate-limit's versioned snapshot key, hono source, and Express's verbatim copyright line were all embedded, none credited.

The failure modes are asymmetric. Crediting a package the binary does not embed is legally harmless. Omitting one it does embed violates the license. Any approximation must therefore err toward inclusion — the manifest's hand-pruning erred the other way.

Decision

§

The graph decides. deno task codegen derives the component set from deno info --json src/main.ts — the account the resolver itself gives of what deno compile embeds — and reproduces each package's own LICENSE file verbatim. No human judgment sits between the dependency graph and the notices.

  1. JSR components are the packages behind the graph's https://jsr.io/… modules. npm components are the dependency closure of the graph's npm roots, walked over the resolution snapshot at package granularity — the same set the compiler materializes. The walk drops packages in the snapshot but outside the closure (other workspace roots). Type-only roots stay in, because over-inclusion is the safe direction.
  2. License texts ship verbatim, from the packages themselves. npm texts come from the extracted package store, with the package.json license field as the label. Codegen fetches a JSR text once from jsr.io — which requires every package to publish a LICENSE — into a committed cache, so regeneration and the gate stay offline. A package with no resolvable license fails generation. LICENSE_OVERRIDES is the deliberate, per-version escape hatch (empty today), never a second manifest.
  3. The binary embeds the committed notices. discern licenses prints them from a generated bundle module compiled into the binary, so an install's notices always match its build. Codegen compresses the bundle — license text repeats the same few bodies — so the notices hold the binary_size ceiling rather than raising it. The uncompressed JSON bytes are canonical; codegen preserves any valid committed compressed stream that represents those bytes, because compression metadata and output vary across Deno versions and hosts. A drift test pins that payload to the readable THIRD_PARTY_NOTICES.
  4. The notices credit the Deno runtime. The binary also embeds the runtime. A fixed section credits the Deno authors (MIT) and points to Deno's own published license and notices rather than enumerating its Rust-crate closure.
  5. Guards hold the tie (tests/third_party_notices_test.ts): offline regeneration parity (a dependency change without deno task codegen fails the gate); closure-closedness with deno.lock as an independent oracle (a generator regression that drops part of the embedded closure — this ADR's originating defect class — fails even though regeneration is self-consistent); and direct-import coverage via the import map.

Consequences

§
  • Adding, removing, or bumping a dependency auto-enrols it in the notices; the gate fails until codegen runs. There is no per-dependency attribution work and no copyright line to transcribe.
  • The notices list the full embedded closure (about 114 components today, express and hono included). Long attribution files are the norm, not a defect.
  • Codegen now shells out to deno info and, only when a new JSR dependency appears, fetches its LICENSE from jsr.io; the committed cache keeps every other run — including the drift guard — offline and deterministic.
  • Re-running codegen under a different Deno compressor or operating system leaves an equivalent committed bundle byte-for-byte untouched. Invalid compressed data or changed canonical JSON still regenerates it.
  • A future dependency under a copyleft or otherwise surprising license surfaces in the regenerated diff at review time instead of slipping past unseen.
choose openEsc close