zuvo:dependency-audit
Audit external dependency health and internal module coupling. Scores 11 dimensions with tiered tooling -- gracefully degrades when specialized tools are unavailable.
Scope: Node.js / TypeScript projects (npm, pnpm, yarn, bun).
When to use: Before releases, after adding many dependencies, periodic
health check, after monorepo restructuring, before lock-in reviews.
When NOT to use: Code quality (zuvo:review), DB-specific
(zuvo:db-audit), full-stack performance (zuvo:performance-audit), OWASP
(/security-audit).
Scanner exit codes: follow
../../shared/includes/scanner-invocation.md—npm/pip-audit/osv-scannerexit non-zero WHEN they find vulns (success); never treat non-zero as failure.
Known Limitations
Dispatch follows ../../shared/includes/execution-policy.md through env-compat. Reuse existing
authorization within that policy; session restrictions take precedence. Run each required gate
and report its actual independence or an unmet requirement.
- Node.js/TypeScript only. Python, Go, Rust, and Java are not supported.
npm queryselectors are npm-specific. pnpm/yarn/bun fall back to lockfile analysis.- D5 (bundle impact) uses Bundlephobia API for triage -- not precise without actual build statistics.
- D7 (instability metrics) requires dependency-cruiser for Ca/Ce/I. Without it, only fan-in/fan-out via grep.
- D10 (change coupling) requires 3+ months of git history.
- Monorepo workspace scoping is partial -- tools operate on workspace root unless filtered.
Mandatory File Loading
Read every file below before starting. Print the checklist.
CORE FILES LOADED:
1. ../../shared/includes/codesift-setup.md -- [READ | MISSING -> STOP]
2. ../../shared/includes/env-compat.md -- [READ | MISSING -> STOP]
3. ../../rules/cq-patterns.md -- [READ | MISSING -> STOP]
4. ../../shared/includes/run-logger.md -- [READ | MISSING -> STOP]
5. ../../shared/includes/retrospective.md -- [READ | MISSING -> STOP]
If any file is MISSING, STOP. Do not proceed from memory.
Argument Parsing
| Token | Behavior |
|---|---|
(empty) or full |
All 11 dimensions, scope = project root |
[path] |
Scope to a package or directory (see scope rules below) |
--supply-chain |
D1 (CVE) + D11 (integrity) only |
--coupling |
D6, D7, D8, D9 only |
--dead |
D3 only |
--bundle |
D5 only |
--lock-in |
D7 vendor lock-in focus |
--no-api |
Skip Bundlephobia and OpenSSF API calls |
Path scope rules:
| Path points to | Manifest found? | D1-D5, D11 (external) | D6-D10 (internal) |
|---|---|---|---|
Package root (own package.json) |
Yes | Score for this package | Score within path |
| Subdirectory within a package | Inherited from parent | Mark INHERITED | Score within path |
No package.json above |
No | Mark SKIPPED | Score within path |
Safety Gate
This audit is read-only. The only write target is zuvo/audits/. Do not
install, uninstall, or modify any dependency. Do not run npm audit fix or
equivalent automatically.
MANDATORY TOOL CALLS — Dependency Audit Validity Gate
This audit is INVALID if any tool below is skipped when its trigger condition holds. "DEFERRED", "N/A", "no fresh deps" are NOT valid reasons.
| Tool | Trigger | Reason | Skip allowed? |
|---|---|---|---|
find_dead_code |
Always | KEY — unused exports = dead code = candidate dead deps | NO |
find_unused_imports |
Always | KEY — barrel-file health, dead transitive deps | NO |
find_circular_deps |
Always | KEY — circular module detection (D6) | NO |
fan_in_fan_out |
Always | KEY — coupling metrics (D7) | NO |
check_boundaries |
Always | D8 architecture boundary violations | NO |
audit_scan |
Always | Compound CQ + dep pattern check | NO |
analyze_python_deps |
Python detected | D1 supply-chain CVE check via PyPI | NO when Python |
| Stack-specific tools | Framework/language detected | Stack-specific dep gates | NO when matches |
Forbidden escape hatches: find_dead_code: skipped, find_circular_deps: N/A, codesift: unavailable (when deferred), retrospective: skipped — all REJECTED.
Required POSTAMBLE: report on disk → retro appended → ~/.zuvo/append-runlog exit 0. Every D-dim finding needs path/to/file.ext:LINE (verify-audit gate).
Mandatory acknowledgment (REQUIRED — print verbatim before Phase 0)
Mandatory-tools-acknowledgment: I will run find_dead_code + find_unused_imports + find_circular_deps + fan_in_fan_out + check_boundaries + audit_scan + analyze_python_deps (when Python) + stack-specific tools for this dependency audit. Every D-dim finding will cite a `path/to/file.ext:LINE` resolving in the current tree.
CodeSift preload
Use the deterministic preload helper FIRST. Run ~/.zuvo/compute-preload dependency-audit "$PWD" before any ToolSearch. Copy [CodeSift matching trace] verbatim, issue printed ToolSearch(query="select:..."). Math gate enforced.
Phase 0: Preflight
0.1 Manifest and Lockfile
Step 1: Find package.json at TARGET_ROOT, walk up to project root if needed.
If not found -> STOP: "No package.json. This skill requires Node.js."
Step 2: Find lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb).
Step 3: Validate package manager works: <pm> ls --json 2>&1
Decision matrix:
| Manifest | Lockfile | node_modules | Action |
|----------|----------|-------------|--------|
| Yes | Yes | Yes | Full audit D1-D11 |
| Yes | Yes | No | STOP: "Run <pm> install first." |
| Yes | No | Yes | D1 capped at 6/15. D1.5 = CRITICAL. |
| Yes | No | No | STOP: "No lockfile and no node_modules." |
| No | -- | -- | STOP: "No package.json found." |
0.2 Package Manager Detection
| Signal | PM |
|---|---|
package-lock.json |
npm |
pnpm-lock.yaml |
pnpm |
yarn.lock |
Yarn (detect version: yarn --version, 1.x = Classic, 2+ = Berry) |
bun.lockb or bun.lock |
Bun |
0.3 Framework Detection (for D8 layer rules)
| Signal | Framework | Layer Pattern |
|---|---|---|
next.config.* |
Next.js App Router | app/ (server) -> lib/ -> components/ (client) |
nest-cli.json or @nestjs/core |
NestJS | controllers -> services -> repositories |
vite.config.* + no SSR |
Vite SPA | pages -> features -> shared |
express in deps |
Express | routes -> middleware -> services |
0.4 Tool Availability
Run in parallel:
# Tier 1 -- PM built-in
<pm> audit --json 2>/dev/null
<pm> ls --json 2>/dev/null
<pm> outdated --json 2>/dev/null
# Tier 2 -- npx (no install)
npx knip --reporter json 2>/dev/null
npx license-checker --json --production 2>/dev/null
# Tier 3 -- check availability
npx depcruise --version 2>/dev/null
npx madge --version 2>/dev/null
Record which tools are available. Missing tools degrade specific dimensions but do not block the audit.
Phase 1: Data Collection
1.1 External Dependency Data
PM audit output: Vulnerability report with severity levels.
PM outdated output: Current vs latest version for each dependency.
Lockfile analysis:
- Install scripts (
postinstall,preinstall) - Git dependencies (
git+,github:) - Lockfile integrity (uncommitted changes, drift from manifest)
Manifest analysis: Read package.json -- count prod/dev deps, version
specifier types (^, ~, exact), overrides, resolutions.
1.2 Internal Dependency Graph
With dependency-cruiser:
npx --yes depcruise --no-config --output-type json --metrics TARGET_ROOT
With madge (fallback):
npx --yes madge --json TARGET_ROOT
npx --yes madge --circular --json --ts-config tsconfig.json TARGET_ROOT
npx --yes madge --orphans --json TARGET_ROOT
Grep fallback (always available): Search for import/export statements to build an approximate dependency graph.
Phase 2: Dimension Analysis (D1-D11)
Agent Dispatch
Refer to env-compat.md for the dispatch pattern.
When parallel dispatch is available:
| Agent | Dimensions | Input |
|---|---|---|
| Supply Chain Scanner | D1, D4, D11 | PM audit, lockfile, license data, OSV/Socket + typosquat/confusion/postinstall heuristics |
| Coupling Analyzer | D6, D7, D8, D9 | dep-cruiser/madge output |
| Freshness Checker | D2, D3, D5 | PM outdated, knip output |
D10 (Change Coupling) runs in the lead agent (requires git log).
Without parallel dispatch: Execute all dimensions sequentially.
D1: Supply Chain and Vulnerabilities -- Weight 15, Max 15, Critical Gate
| Check | Good | Bad | Severity |
|---|---|---|---|
| Known CVEs | Zero critical/high CVEs in production deps | Unpatched critical CVE | CRITICAL |
| Install scripts | Only trusted packages run postinstall | Unknown package with postinstall | HIGH |
| Signature verification | Lockfile signatures verified | No signature verification | MEDIUM |
| Git dependencies | Zero git deps in production | git+ URLs bypass registry auditing |
HIGH |
| Lockfile integrity | Committed, matches manifest, reproducible | Missing or diverged lockfile | HIGH |
Critical gate: known exploit-grade CVE in production → triggers FAIL. The gate fires on that FINDING; a D1 score of 0 also fires it but is not required.
D2: Freshness and Maintenance Health -- Weight 10, Max 10
| Check | Good | Bad | Severity |
|---|---|---|---|
| Major version lag | Within 1 major version of latest | 3+ major versions behind | HIGH |
| Maintenance status | Active maintainer, recent releases | Abandoned (no release > 2 years) | HIGH |
| Deprecated packages | Zero deprecated deps | Using packages with npm deprecate notice |
MEDIUM |
D3: Dead Dependencies -- Weight 10, Max 10
| Check | Good | Bad | Severity |
|---|---|---|---|
| Unused production deps | Zero unused in dependencies |
Package in manifest, never imported | HIGH |
| Phantom dependencies | All imports resolve to declared deps | Import works only because of hoisting | MEDIUM |
| Duplicate functionality | One library per task (date, HTTP, validation) | lodash + underscore + ramda all installed | MEDIUM |
Use knip output when available. Otherwise grep for import statements and
cross-reference against package.json dependencies.
D4: License Compliance -- Weight 8, Max 8, Critical Gate
| Check | Good | Bad | Severity |
|---|---|---|---|
| Copyleft in production | Zero GPL/AGPL in bundled code | GPL dependency in production app | CRITICAL |
| Unknown licenses | All deps have declared licenses | Missing license field | MEDIUM |
| License compatibility | All licenses compatible with project license | Conflicting license chain | HIGH |
Critical gate: GPL in production closed-source app → triggers FAIL. The gate fires on that FINDING; a D4 score of 0 also fires it but is not required.
D5: Bundle and Weight Impact -- Weight 8, Max 8
| Check | Good | Bad | Severity |
|---|---|---|---|
| Heavy dependencies | Alternatives exist for large deps (moment -> dayjs, lodash -> native) | 500 KB+ dep used for one function | HIGH |
| Tree-shaking support | sideEffects: false in dep, ESM exports |
CommonJS-only dep in frontend bundle | MEDIUM |
| Duplicate versions | Single version in bundle | 3 versions of the same package | MEDIUM |
D6: Circular Dependencies -- Weight 12, Max 12, Critical Gate
| Check | Good | Bad | Severity |
|---|---|---|---|
| Runtime cycles | Zero circular dependency chains at runtime | Module A imports B, B imports A | CRITICAL |
| Type-only cycles | Type-only imports (import type) are acceptable |
Type cycle causes runtime initialization bug | LOW |
| Cycle depth | N/A | 4+ module cycle chain | HIGH |
Critical gate: runtime circular dependency causing initialization bug → triggers FAIL. The gate fires on that FINDING; a D6 score of 0 also fires it but is not required.
D7: Coupling Metrics -- Weight 10, Max 10
| Check | Good | Bad | Severity |
|---|---|---|---|
| Instability (I = Ce/(Ca+Ce)) | Balanced: stable foundations, unstable leaves | Core module with I > 0.8 (depends on everything) | HIGH |
| Fan-out | Module imports < 10 direct dependencies | Single file imports 30+ modules | HIGH |
| Fan-in | Shared module is aware of its consumers | Utility used by 50+ files, fragile to change | MEDIUM |
| Vendor lock-in | Adapter pattern around vendor SDKs | Vendor SDK calls spread across 20+ files | MEDIUM |
D8: Architecture Boundary Violations -- Weight 12, Max 12, Critical Gate
| Check | Good | Bad | Severity |
|---|---|---|---|
| Layer enforcement | UI -> Services -> Data, never backward | Component imports from data layer directly | HIGH |
| Secret in client | Server-only vars never imported in client code | Client bundle imports process.env.SECRET |
CRITICAL |
| Domain boundaries | Features do not cross-import internal modules | Feature A reaches into Feature B internals | MEDIUM |
Critical gate: secret leaked to client bundle → triggers FAIL. The gate fires on that FINDING; a D8 score of 0 also fires it but is not required.
D9: Barrel File Health -- Weight 7, Max 7
| Check | Good | Bad | Severity |
|---|---|---|---|
| Re-export scope | Barrel re-exports < 5 symbols, public API only | export * pulling in 50+ symbols |
HIGH |
| Circular risk | Barrel does not create import cycles | Barrel creates cycle between modules | HIGH |
| Tree-shake impact | sideEffects: false set, named exports |
Barrel defeats tree-shaking | MEDIUM |
D10: Change Coupling -- Weight 8, Max 8
Analyze git history (last 6 months) for files that always change together:
git log --name-only --pretty=format:"---COMMIT---" --since="6 months ago" -- TARGET_ROOT
| Check | Good | Bad | Severity |
|---|---|---|---|
| Co-change clusters | Related files change together (test + impl) | Unrelated modules always change together | HIGH |
| Shotgun surgery | Feature change touches 1-3 files | Every feature touches 10+ files | HIGH |
| God module churn | Hot files are small and well-tested | Largest file is also most-changed | CRITICAL |
D11: Supply-Chain Integrity -- Weight 10, Max 10
Distinct from D1 (known-CVE vulnerabilities) and D4 (licence): D11 is about whether a dependency is the package you think it is, from a trustworthy source, with a verifiable provenance. Scan the manifest + lockfile.
| Check | Good | Bad | Severity |
|---|---|---|---|
| Typosquatting | Package names match the intended, popular library | A near-miss name (reqeust, crossenv, lodahs, discord.js-selfbot) — likely typosquat |
CRITICAL |
| Dependency confusion | Internal/scoped packages pinned to the private registry; scope reserved on the public registry | An unscoped internal name also resolvable from the public npm/PyPI (attacker can publish a higher version) | CRITICAL |
| Malicious-package signals | No install-time exfiltration | postinstall/preinstall running curl/wget/base64-decoded payloads, or a dep flagged by OSV/Socket/npm advisories as malware — after excluding well-known build tools per the Phase-3 False Positive Filters (esbuild/sharp/prisma/@swc/core legitimately use postinstall); a lifecycle script alone is not malware |
CRITICAL |
| Lockfile integrity | Committed lockfile with integrity hashes; CI uses npm ci/--frozen-lockfile |
No lockfile, or install resolves floating versions in CI | HIGH |
| Provenance / signing | Releases verified (npm provenance, Sigstore/cosign, SLSA attestation) for critical deps | No provenance on any dependency; unsigned artifacts | MEDIUM |
| SBOM | An SBOM (CycloneDX/SPDX) is generated and current | No SBOM — cannot answer "am I affected by X" fast | MEDIUM |
Tools (tiered, graceful degradation): OSV-Scanner / npm audit signatures / Socket CLI when present;
otherwise heuristic name-distance + postinstall grep + lockfile presence. Findings cite the manifest
package.json:LINE / lockfile entry. --supply-chain scopes the run to D1 + D11.
Critical gate: ANY confirmed typosquat, dependency-confusion exposure, or malicious-package signal in production dependencies triggers FAIL (finding-presence, whatever the numeric score).
Phase 3: Verification and Scoring
False Positive Filters
| Pattern | Skip When |
|---|---|
| Install script flagged | Well-known build tool (esbuild, sharp, prisma, @swc/core) |
| Unused dep | Framework plugin loaded via config (knip handles 139+ but misses some) |
| Circular dep | Type-only cycle (import type) with isolatedModules |
Floating ^ version |
Lockfile present and committed |
| Layer violation in Next.js | Server component IS the server layer |
| Barrel flagged | < 5 re-exports AND no circular deps caused |
export * |
sideEffects: false is set in the package |
Scoring
Critical gates — any ONE of these findings triggers FAIL, whatever the numeric score:
| Gate | Fires on |
|---|---|
| D1 | a known exploit-grade CVE reachable in production |
| D4 | a GPL-family licence in a closed-source production app |
| D6 | a runtime circular dependency causing an initialization bug |
| D8 | a secret leaked to the client bundle |
| D11 | a confirmed typosquat, dependency-confusion exposure, or malicious-package signal in production deps |
These gates fire on the FINDING, not on an aggregate score of zero. Read as "the dimension scored 0" they are nearly unreachable, because every one of them spreads several checks over its weight: D8 carries three checks across 12 points, so a secret in the client bundle beside clean layering and clean domain boundaries scores about 8/12 — the leak never blocks the audit. The named condition in each parenthesis IS the trigger — a dimension score of 0 remains sufficient to fire the gate, it is simply not necessary.
N/A handling: Exclude dimensions with INSUFFICIENT DATA, SKIPPED, or INHERITED from both numerator and denominator.
| Grade | Percentage |
|---|---|
| A | >= 85% |
| B | >= 70% and < 85% |
| C | >= 50% and < 70% |
| D | < 50% |
Phase 4: Report
Save to: zuvo/audits/dependency-audit-[YYYY-MM-DD].md — at the project root (zuvo/ resolves via git rev-parse --show-toplevel; override $ZUVO_OUTPUT_DIR. See ../../shared/includes/report-output-location.md).
REQUIRED: emit the Tool Availability Block (template in ../../shared/includes/codesift-setup.md) at the top of the report, after the title and before findings. Auditing degraded runs depends on this — do NOT skip it.
Report Structure
# Dependency & Coupling Audit Report
## Metadata
| Field | Value |
|-------|-------|
| Project | [name] |
| Date | [YYYY-MM-DD] |
| Package Manager | [npm/pnpm/yarn/bun] |
| Scope | [full / path] |
| Total prod deps | [N] |
| Total dev deps | [N] |
## Executive Summary
**Score: [N] / [MAX]** -- [A/B/C/D or FAIL]
| Metric | Count |
|--------|-------|
| CRITICAL findings | N |
| HIGH findings | N |
| MEDIUM findings | N |
[2-3 sentence summary]
## Dimension Scores
| # | Dimension | Score | Max | Tool Used | Notes |
|---|-----------|-------|-----|-----------|-------|
| D1 | Supply Chain | [N] | 15 | <pm> audit | |
| D2 | Freshness | [N] | 10 | <pm> outdated | |
| D3 | Dead Deps | [N] | 10 | knip / grep | |
| D4 | Licenses | [N] | 8 | license-checker | |
| D5 | Bundle Weight | [N] | 8 | Bundlephobia | |
| D6 | Circular Deps | [N] | 12 | dep-cruiser / madge | |
| D7 | Coupling Metrics | [N] | 10 | dep-cruiser / grep | |
| D8 | Architecture | [N] | 12 | dep-cruiser / grep | |
| D9 | Barrel Health | [N] | 7 | grep | |
| D10 | Change Coupling | [N] | 8 | git log | |
| D11 | Supply-Chain Integrity | [N] | 10 | OSV / Socket / heuristic | |
| **Total** | | **[N]** | **[M]** | | |
## Critical Gate Status
[D1, D4, D6, D8, D11 -- PASS/FAIL per gate]
## Delete These Tomorrow
[Unused deps that can be removed with zero code changes]
## Findings (sorted by severity)
[Per finding: ID, severity, dimension, description, fix]
## Cross-Cutting Patterns
| Pattern | Dims | Impact |
|---------|------|--------|
| Barrel causing circular deps | D9+D6 | Fix barrel, cycles disappear |
| God module + high churn | D7+D10 | Max blast radius on hot code |
| Unused dep with known CVE | D3+D1 | Dead code + security risk |
## Remediation Roadmap
### Quick Wins (< 1 hour)
### Short-term (1 day)
### Medium-term (1 week)
Phase 5: Next-Step Routing
RECOMMENDED NEXT ACTION
------------------------------------
D1 CRITICAL (CVE) -> npm audit fix or manual upgrade
D11 typosquat/malicious pkg -> REMOVE the package immediately + rotate any exposed creds; report to registry
D11 dependency confusion -> for an UNSCOPED internal name: rename to a private @scope (or defensively publish a placeholder on the public registry) + pin the private registry in .npmrc; for a scoped one: reserve the @scope publicly
D11 no lockfile/provenance -> commit lockfile, enable `npm ci`, add SBOM (CycloneDX) + provenance
D4 GPL in production -> replace with MIT/Apache alternative
D6 runtime circular dep -> zuvo:refactor [module path]
D8 boundary violation -> zuvo:refactor [violating file]
D9 barrel causing cycles -> zuvo:refactor [barrel file]
D10 high change coupling -> zuvo:refactor [coupled files]
Score < 60% -> prioritize "Delete These Tomorrow" list first
Score >= 85% -> schedule next audit in 3 months
------------------------------------
Completion Gate Check
Before printing the final output block, verify every item. Unfinished items = pipeline incomplete.
COMPLETION GATE CHECK
[ ] Tool availability printed: PM audit, knip, dep-cruiser, license-checker
[ ] D1 supply chain: CRITICAL/HIGH CVEs checked for reachability
[ ] D4 license: GPL/AGPL in production flagged
[ ] D6 circular dependency detection ran
[ ] Critical gates printed: D1, D4, D6, D8, D11
[ ] "Delete These Tomorrow" section present
[ ] Report saved to zuvo/audits/
[ ] Run: line printed and appended to log
DEPENDENCY-AUDIT COMPLETE
Score: [N] / [MAX] -- [grade] Package Manager: [npm/pnpm/yarn/bun] Dimensions: [N scored] | Critical gates: [PASS/FAIL] Findings: [N critical] / [N total]
Validity Gate (REQUIRED — print BEFORE Run line, AFTER retro append + append-runlog)
VALIDITY GATE
triggers_held: language=<X> python=<yes|no>
required_tool_calls:
find_dead_code: [<N> unused exports | NOT_CALLED — VIOLATES_TRIGGER]
find_unused_imports: [<N> | NOT_CALLED — VIOLATES_TRIGGER]
find_circular_deps: [<N> cycles | NOT_CALLED — VIOLATES_TRIGGER]
fan_in_fan_out: [<max_in>/<max_out> | NOT_CALLED — VIOLATES_TRIGGER]
check_boundaries: [<N> violations | NOT_CALLED — VIOLATES_TRIGGER]
audit_scan: [<N> findings | NOT_CALLED — VIOLATES_TRIGGER]
analyze_python_deps: [<N> CVEs | not_required (no Python) | NOT_CALLED — VIOLATES_TRIGGER]
stack_specific: [<result> | not_required | NOT_CALLED — VIOLATES_TRIGGER]
postamble:
retros_log_appended: [yes(bytes_added=N) | NOT_APPENDED]
retros_md_appended: [yes(entry_count=N) | NOT_APPENDED]
verify_audit_pass: [yes(<verified>/<total>) | NOT_RUN | REJECTED]
gate_status: [PASS | FAIL — <which gates missing>]
If gate_status = FAIL → VERDICT = INCOMPLETE.
Append the Run line via the retro-gated wrapper (NOT direct >> runs.log):
printf '%b\n' "$RUN_LINE" | ~/.zuvo/append-runlog
Run: dependency-audit - -dimensions
Retrospective (REQUIRED)
Follow the retrospective protocol from retrospective.md.
Gate check → structured questions → TSV emit → markdown append.
If gate check skips: print "RETRO: skipped (trivial session)" and proceed.
After printing this block, append the Run: line value (without the Run: prefix) to the log file path resolved per run-logger.md.
VERDICT: PASS (0 critical findings), WARN (1-3 critical), FAIL (4+ critical).
Execution Notes
- All commands use the resolved
TARGET_ROOTfrom argument parsing - CodeSift integration follows
codesift-setup.md - Agent dispatch follows
env-compat.md - Tool unavailability degrades individual dimensions, not the entire audit
- Yarn version detection is critical: Classic (1.x) and Berry (2+) have different command syntax
npm queryselectors are npm-only; pnpm/yarn/bun use lockfile grep as fallback--no-apiflag skips Bundlephobia and OpenSSF calls for air-gapped or rate-limited environments