Hedronite Lesson · Polyglot-Dev / TypeScript · Wed 2026-09-23

Supply-chain / lockfile discipline

Commit the lockfile. Install exactly. Type third-party code at the edge.

Lesson Class: Duha (TypeScript Primary track)
Focus: lockfile contract · npm ci / frozen-lockfile · overrides/resolutions · declare module boundary · audit
Code Blocks: clean blocks, explanation in prose
Done-criteria: lockfile as contract + one override + typed third-party boundary
Grounding: Cherny Ch.11 typed/untyped edge · prior Web lockfile integrity · Bun lockfile runtime cite
The contract
Ranges live in package.json; the lockfile pins the tree.
The pin
overrides / resolutions force a transitive when you must.
The edge
declare module + wrapper; never import third-party as any.
Commit the lockfile. Install exactly. Type third-party code at the edge.

<!-- hal:authoritative:yaml -->

*Commit the lockfile. Install exactly what it pins. Override when you must. Type third-party code at the edge, never as any.*

§I - Frame

Duha Primary TypeScript session 8. Topics #15: supply-chain / lockfile discipline. Topics #14 typed env shipped 2026-09-22. Bun-centric rows 5-8 and syllabus #11-13 stay deferred. This is tooling and trust depth, not a Bun survey and not a re-ship of typed env.

Done-criteria: Can explain why a committed lockfile is the install contract, name one override/resolution mechanism, and sketch a typed boundary for a third-party dependency.

Primary cite: Cherny Programming TypeScript Ch.11 on the typed/untyped edge. Secondary: prior Web lesson (2026-07-01 lockfile integrity) + Handbook modules / unknown. Bun lockfile docs only as a runtime cite when the project uses Bun.

Home: .

§II - Lockfile as install contract

package.json names ranges. The lockfile names exact trees: resolved versions, registry URLs, and integrity hashes for every node. Commit it. Without that file in source control, every machine and every CI run may resolve a different graph from the same ranges.

The install command makes the contract binding:

# npm: install exactly from the lockfile, or fail
npm ci

# Bun: frozen install from bun.lock (runtime cite)
bun install --frozen-lockfile
# or: bun ci

npm install may rewrite the lockfile when it resolves something new. npm ci refuses that: it deletes node_modules, installs from the lockfile, and errors if package.json and the lockfile disagree. In CI, the strict command is the contract. The lockfile is not a hint; it is the pin.

Integrity fields (for example sha512-… in npm) mean the next install recomputes the tarball hash and aborts on mismatch. That is reproducibility: same inputs, same tree, or a hard fail.

§III - Overrides and resolutions

Sometimes a transitive dependency must be forced to a known-safe version while you wait for upstream. Package managers expose a pin for that:

{
  "name": "app",
  "dependencies": {
    "left-pad-wrapper": "^2.0.0"
  },
  "overrides": {
    "vulnerable-lib": "1.2.3"
  }
}

npm uses overrides. Yarn Classic uses resolutions. Bun accepts an overrides field in package.json with the same intent: force one version (or a nested path) across the tree. After you change an override, regenerate and commit the lockfile, then re-run the frozen install. An override without a committed lockfile update is unfinished work.

Use overrides sparingly. Prefer upstream fixes. When you do override, treat the entry as temporary debt with a tracked reason.

§IV - Typed boundary for a third-party dependency

Cherny treats the seam between typed and untyped JavaScript as the place third-party imports go wrong: an untyped module enters as any, and any spreads into every value derived from it. The fix is a single typed edge the rest of the app imports instead of the raw package.

// types/crowd-metrics.d.ts  -  ambient shape you verified
declare module "crowd-metrics" {
  export function track(
    event: string,
    payload: Record<string, unknown>,
  ): void
}

// src/metrics.ts  -  local boundary
import { track as rawTrack } from "crowd-metrics"

export type MetricEvent = {
  readonly name: string
  readonly fields: Record<string, string | number>
}

export function track(event: MetricEvent): void {
  rawTrack(event.name, event.fields)
}

Application code imports track from src/metrics.ts, never from "crowd-metrics" directly. The compiler patrols the shape. If the runtime package later changes, the break surfaces at this one file.

Prefer unknown over any when a value arrives from an untrusted parse (JSON, config blob). Narrow before use:

function asMetricName(raw: unknown): string {
  if (typeof raw === "string" && raw.length > 0) return raw
  throw new Error("metric name must be a non-empty string")
}

That pairs with session 7's fail-fast config habit: refuse bad input at the edge.

§V - Audit and install reproducibility

After a clean frozen install, check the resolved tree against known advisories:

npm audit --audit-level=high
# or the equivalent Bun/npm audit path your CI already uses

Fail the build on high severity when policy says so. Combine with install-script posture from the prior Web lesson: prefer ignoring dependency lifecycle scripts by default (ignore-scripts / Bun trustedDependencies allowlist) so a new package cannot run code at install time unless you named it.

Checklist for a Primary-depth proof:

  1. Lockfile committed; CI uses npm ci or bun install --frozen-lockfile.
  2. One overrides (or Yarn resolutions) example explained.
  3. One declare module + wrapper for an untyped dependency.
  4. Audit runs in CI; install scripts gated.

§VI - Closing

The lockfile is the install contract. Strict CI install makes it binding. Overrides pin a transitive when you must. Cherny's typed/untyped edge becomes a declare module wrapper so third-party code never crosses as any. Bun rows 5-8 and #11-13 stay deferred. Next Primary TypeScript topic follows the syllabus after #15 once a teammate seats it.

Done-criteria: committed lockfile as contract + one override/resolution mechanism + typed third-party boundary sketch.

Related