{"_id":"@cyberstamp/cdx-npm-enrich","_rev":"7-b6da75d04e70f11d5959ea899f62bb4e","name":"@cyberstamp/cdx-npm-enrich","dist-tags":{"latest":"0.1.6"},"versions":{"0.1.0":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.0","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.0","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"8dc59493184c2643dfa7ea7ee5c5703cc45d139a","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.0.tgz","fileCount":4,"integrity":"sha512-NomyPGK8nzBIrfT2iqHZms0fRaOjLfxbWIp5fFN3YPGx1Z0c7cMhF/8D6WT6w6wqcPcdL6Z2bObTjO7C0f5NEA==","signatures":[{"sig":"MEYCIQDkJHXExSuXnFpqqVybqlaZcX1O/C/tpWMzPlIcBVWwAgIhANTtbyA33FUJIcpXjlpHAKOYT5MRNrZQKedtLmHAaE+b","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30768},"type":"module","engines":{"node":">=18"},"gitHead":"d51f6415a93ab049ef366e8273bc5254e615d77d","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.0_1785785417533_0.049787026414021884","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.1","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.1","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"f3fc5a2d1e079b38bb7ef8dc3d9a11c2e3d4cfb9","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.1.tgz","fileCount":4,"integrity":"sha512-GFWMOfwGGtZnei0mIpyAF4iNEnW+LSuxWTy0ZQIPffykHvAcnF2cb17cLfJoBN2/MPY5wW31TY5q3P82mBU08Q==","signatures":[{"sig":"MEUCIHKxX+t0ffXQaTcLaqnCOSMEWzsfSUQ625kFw926JKIVAiEA5za1zXCQozkpmfymBgkRVX0ksLcWf/DU7/Zwbzudwzo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30952},"type":"module","engines":{"node":">=18"},"gitHead":"66dc7297f0c9fd18716568bd52cc88fb104d7735","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.1_1785786056101_0.7957368540158261","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.2","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.2","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"820bef5a21ba239bb699fe25ec29e83e7baf9abf","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.2.tgz","fileCount":4,"integrity":"sha512-QaxQ6YpbJLlnJPZvWfzL2W8RJ73H0KkuT97m8BYUZEvzWlxFAMiscyBo3bipx9A+lWIyrBa+y8DqHUscADYWPg==","signatures":[{"sig":"MEQCIHYt95fLR0SrOt8aSNvHnU4Ks2RUJgaK+jjT5gohVqXgAiBlZ0PoXc+E8btU8kUGDAD2C6ovUgTTVem4gJ3KJdpIjA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":31603},"type":"module","engines":{"node":">=18"},"gitHead":"50f1c95ba1ec67067fa9c5e1faa77ac30ed20c61","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.2_1785787757705_0.9202387550346711","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.3","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.3","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"0b77a1ddf5a8c12360acac214504134bfb533c7e","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.3.tgz","fileCount":4,"integrity":"sha512-x6Aqz/qPJHvKjj1df8wKTrqr6te4GLW5YCF67l/6MohwqWKuvc8I7qlgE3eCT7fUL6UU70QFV52sgN3rOakf5A==","signatures":[{"sig":"MEUCIQD8UMvuvvzUxaJfMSQ3IrlJdPWm9WvF7MgUSdF2i8YgDgIgNqZ7D3EluktjBfmWGrZWaWyA2mTTOaRo4zwgyMh/CUA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":38198},"type":"module","engines":{"node":">=18"},"gitHead":"6674959a671670930d10c88e40cd8e530b960025","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.3_1785789282382_0.5957327039582128","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.4","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.4","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"a2cf13712ad075c559e871210227ad33e037cbd9","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.4.tgz","fileCount":4,"integrity":"sha512-775FmJ5PiqASS5MIogFXstFywV8lV0m6bjVbzQe23O+x/SsA0wJP2esdqhoix13rZPYujmBd5GYZ8o/EiAp5Nw==","signatures":[{"sig":"MEYCIQCh+h8whF418d9Eb7Pbr9Ms+5Qk0zX8z6TbGMXv7HnqIwIhALYrQ8/ZzC5J9RaueE68Uudp4BxbFz24/r9Csyq2IGhG","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39552},"type":"module","engines":{"node":">=18"},"gitHead":"3e0111b15810bad4886d6a1d63300d8e0a1d5522","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.4_1785871421114_0.6810314937170723","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.5","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"license":"Apache-2.0","_id":"@cyberstamp/cdx-npm-enrich@0.1.5","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"bin":{"cdx-npm-enrich":"index.mjs"},"dist":{"shasum":"b106ff347f84506436b8086eb89bad8250076518","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.5.tgz","fileCount":4,"integrity":"sha512-Hy/bb6jWOvERhmV0Mp7RClLncN887YIVwpIJPas9BvTy2O0DYF0OLMGkQ5PmnnJ9RwqUpSGKGRC2G2TzVCIt1Q==","signatures":[{"sig":"MEUCIQDgLKfGFJ/HhnrIEINj1af1KSTo28uMHPZ8fCasaPUy5QIgGxMEAPa3thVjGnHcZ2CDome+Kgqdjxzoc0BUcb6Bw9k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":51490},"type":"module","engines":{"node":">=18"},"gitHead":"866e9e80b376efa33e5632849bf02325c5db6880","scripts":{"test":"node --test test/*.test.mjs"},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"repository":{"url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git","type":"git"},"_npmVersion":"10.9.7","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","directories":{},"_nodeVersion":"22.22.2","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/cdx-npm-enrich_0.1.5_1786546768949_0.5033278713878062","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"@cyberstamp/cdx-npm-enrich","version":"0.1.6","description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","type":"module","bin":{"cdx-npm-enrich":"index.mjs"},"scripts":{"test":"node --test test/*.test.mjs"},"license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git"},"engines":{"node":">=18"},"keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"_id":"@cyberstamp/cdx-npm-enrich@0.1.6","gitHead":"687d97993f9425ca1689babf2d7e3db42b2434d1","bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-NYxCSYaXHIEDQOp+sv6gY1SPuD32iFNABSCVyJUHJ7Boxn4Prkk9m9L+MkfzVblH+dWitH/xZIiZFQBymGbqtA==","shasum":"41cfe076a9b2931accc92930b02ccfdaa26d62b8","tarball":"https://registry.npmjs.org/@cyberstamp/cdx-npm-enrich/-/cdx-npm-enrich-0.1.6.tgz","fileCount":4,"unpackedSize":52791,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGEMRCPxv7gl8VR1ARvjH25Dnbh1S919OGEWnuPu2fqhAiEAuQlfQbwlgP34Rb3wjLkT6NtXQYwP8nLmuI4z5bnR6oc="}]},"_npmUser":{"name":"aloubyansky","email":"alexey@cyberstamp.dev"},"directories":{},"maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cdx-npm-enrich_0.1.6_1787685194442_0.21958365134538682"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-03T19:30:17.352Z","modified":"2026-08-25T19:13:14.723Z","0.1.0":"2026-08-03T19:30:17.678Z","0.1.1":"2026-08-03T19:40:56.225Z","0.1.2":"2026-08-03T20:09:17.843Z","0.1.3":"2026-08-03T20:34:42.528Z","0.1.4":"2026-08-04T19:23:41.247Z","0.1.5":"2026-08-12T14:59:29.087Z","0.1.6":"2026-08-25T19:13:14.560Z"},"bugs":{"url":"https://github.com/cyberstamp/cdx-npm-enrich/issues"},"license":"Apache-2.0","homepage":"https://github.com/cyberstamp/cdx-npm-enrich#readme","keywords":["cyclonedx","sbom","npm","yarn","pnpm","scope","license","enrich"],"repository":{"type":"git","url":"git+https://github.com/cyberstamp/cdx-npm-enrich.git"},"description":"Enrich CycloneDX npm SBOMs with dev/prod scope and missing licenses","maintainers":[{"name":"aloubyansky","email":"alexey@cyberstamp.dev"}],"readme":"# @cyberstamp/cdx-npm-enrich\n\nA post-processing utility that enriches [CycloneDX](https://cyclonedx.org/) SBOMs produced by Node.js SBOM generators. It is not an SBOM generator — it takes an existing SBOM as input, cross-references it against `package.json` dependency declarations and `node_modules` contents (not the lockfile), and improves scope classification, license coverage, and hash placement.\n\n> **NOTE:** In an ideal world this utility shouldn't exist. Hopefully, SBOM generators for Node.js will align on a common and standard approach to manifest prod and non-prod dependencies soon.\n\n\n## Why\n\nCycloneDX SBOM generators for Node.js produce varying levels of scope and license coverage. Some omit scope entirely, others use lockfile heuristics or AST analysis that can misclassify dependencies. License metadata is often incomplete.\n\nThis tool post-processes a generator's output to ensure:\n\n- **Scope classification**: dev-only dependencies are marked `scope: \"excluded\"` (not reachable at runtime per the [CycloneDX spec](https://cyclonedx.org/docs/1.6/json/#components_items_scope)). Production dependencies reachable only through `optionalDependencies` edges retain `scope: \"optional\"`. All other production dependencies have their scope cleared (implied `\"required\"`)\n- **Complete licenses**: components missing license metadata are enriched from `node_modules/*/package.json`\n- **Hashes**: if a component's `externalReferences[type=distribution]` entry has no hashes, a SHA-512 checksum from the lockfile (`yarn.lock`, `pnpm-lock.yaml`, or `package-lock.json`) is added. Hashes are placed on the distribution external reference per the CycloneDX spec (the hash is of the registry tarball, not the component content itself). Existing distribution hashes are preserved\n- **Evidence**: production components receive CycloneDX `evidence.identity` confirming their PURL was verified via `manifest-analysis` (reading `package.json` from `node_modules`). Confidence is set to 0.6 — the top of the [CycloneDX-recommended range](https://github.com/CycloneDX/guides/blob/main/SBOM/en/0x60-Evidence.md) for manifest analysis. This is more conservative than cdxgen's 1.0: a lockfile or `package.json` confirms a package was declared and installed, but does not verify that its contents match a known-good artifact (e.g., via content hash). Existing `evidence.identity` from the upstream SBOM generator is preserved\n\nProduction vs dev classification builds the dependency graph reachable from workspace `dependencies` (following `dependencies`, `peerDependencies`, and `optionalDependencies` — never `devDependencies`), then classifies each package based on the edge types through which it was reached. Peer dependencies are gated by the consumer workspace's declaration — see [Peer dependency classification](#peer-dependency-classification) below.\n\n### Generators tested (August 2026)\n\n| Generator | Scope | Licenses | Hashes | Evidence | Issues addressed |\n|-----------|-------|----------|--------|----------|-----------------|\n| [cdxgen](https://github.com/CycloneDX/cdxgen) 12.x | Most components marked `optional`; some `excluded` for type-only imports. `--required-only` can strip non-prod components. Neither mode marks dev deps as `excluded`. | Can resolve licenses by querying public registries (`FETCH_LICENSE=true`); disabled by default due to performance. Does not read from `node_modules/*/package.json`. | SHA-512 from lockfile on `component.hashes` (spec-incorrect — tarball hash placed at top level) | `manifest-analysis` / lockfile name, confidence 1.0 | Scope reclassified; missing licenses enriched; hashes relocated from `component.hashes` to `externalReferences[type=distribution]` |\n| [@cyclonedx/yarn-plugin-cyclonedx](https://github.com/CycloneDX/cyclonedx-node-yarn) 3.3 | No scope set. `--prod` can strip dev deps. No option to keep all and mark dev deps as `excluded`. | Resolved from `package.json` in `node_modules` (same approach as this tool). | Not produced | Not produced | Scope added; hashes enriched from lockfile; evidence added |\n| [pnpm sbom](https://pnpm.io/cli/sbom) 11.x | Dev deps marked `excluded` with `cdx:npm:package:development` property; prod deps have no scope set (implied `required`). `--prod` strips dev deps, `--no-optional` strips optional deps. | Resolved from `package.json` in `node_modules` (same approach as this tool). | SHA-512 on `externalReferences[type=distribution]` (spec-correct placement) | Not produced | Evidence added (minimal value-add — pnpm sbom already handles scope, licenses, and hashes correctly) |\n\n## Install\n\n```bash\nnpm install -g @cyberstamp/cdx-npm-enrich\n```\n\nOr run directly with npx:\n\n```bash\nnpx @cyberstamp/cdx-npm-enrich bom.cdx.json\n```\n\n## Usage\n\n```\ncdx-npm-enrich [options] <bom.cdx.json>\n```\n\nThe SBOM file is modified in-place.\n\n### Options\n\n| Option | Description |\n|--------|-------------|\n| `--project-dir <dir>` | Project root containing `package.json` and `node_modules`. Defaults to the current working directory. |\n| `-o, --output <file>` | Write to a new file instead of modifying the input in-place. |\n| `--prod-only` | Strip dev-only components and their dependency entries instead of marking them `excluded`. |\n\n### Examples\n\nEnrich an SBOM (mark dev deps as excluded, keep everything):\n\n```bash\ncdx-npm-enrich bom.cdx.json\n```\n\nEnrich an SBOM for a project in a different directory:\n\n```bash\ncdx-npm-enrich --project-dir /path/to/project bom.cdx.json\n```\n\nStrip dev dependencies entirely:\n\n```bash\ncdx-npm-enrich --prod-only bom.cdx.json\n```\n\n## Workspace support\n\nThe tool auto-detects the workspace configuration:\n\n- **npm/yarn**: reads the `workspaces` field from `package.json`\n- **pnpm**: reads `pnpm-workspace.yaml`\n\nDependencies are resolved from per-workspace `node_modules` directories, including pnpm's content-addressable `.pnpm` store.\n\n## How it works\n\n1. **Workspace discovery**: finds all workspace packages from `package.json` (`workspaces` field) or `pnpm-workspace.yaml`\n2. **Graph construction**: collects direct production dependencies (`dependencies`, not `devDependencies`) from each workspace, skipping `workspace:` protocol references. Transitively walks reachable packages through `node_modules`, following `dependencies`, `peerDependencies`, and `optionalDependencies` at each level — `devDependencies` are never followed. Symlinks are resolved so that pnpm's `.pnpm` store siblings are reachable. For pnpm virtual packages (store entries with `_` in the directory name), sibling entries are scanned to discover implicit peer bindings\n3. **Prod/dev classification**: propagates production status through the graph. Regular dependencies and optional dependencies are always production. Packages reachable exclusively through `optionalDependencies` edges (including their transitive children) are marked `optional`. A package reachable through both a required and an optional path is classified as required. Peer dependencies are classified based on how the consuming workspace declared them (see below)\n4. **Lockfile parsing**: reads SHA-512 checksums from `yarn.lock`, `pnpm-lock.yaml`, or `package-lock.json`\n5. **Enrichment**: adds missing license data, hashes (on `externalReferences[type=distribution]`), and `evidence.identity` to production components. Removes `component.hashes` entries that duplicate distribution tarball hashes\n6. **Output**: required production components have their scope cleared (implied `\"required\"` per CycloneDX spec). Optional-only production components get `scope: \"optional\"`. Dev-only components get `scope: \"excluded\"` (or are removed in `--prod-only` mode)\n\n### Peer dependency classification\n\nA `peerDependencies` declaration is neutral — it says \"the host must provide this\" but doesn't indicate whether the package is needed at runtime or only during development. The **consumer's** declaration determines the classification:\n\n- If a workspace has package A in `dependencies` and A declares a peer dep on B:\n  - B in the workspace's `dependencies` → **production** (explicitly declared as runtime)\n  - B in the workspace's `devDependencies` (not in `dependencies`) → **dev-only** (consumer signaled it's not needed at runtime)\n  - B not declared by the workspace at all → **production** (safe default — the consumer didn't signal dev-only, and A needs B at runtime)\n\n- If A is a **transitive** dependency (not directly in any workspace's `dependencies`), no workspace is the direct consumer of A's peer dep. In this case the peer dep is always followed — the workspace's own `devDependencies` are unrelated to A's runtime needs\n\n- A package reached as dev-only through a peer dep edge can still be production if it's reachable through a regular `dependencies` edge from another production package. Classification never downgrades from prod to dev\n\n## Limitations\n\n- The tool classifies dependencies by walking `package.json` fields, not by analyzing actual code usage. A declared production dependency that is never imported at runtime will still be classified as production\n- Peer dependencies and optional dependencies that are not installed are silently skipped — the package manager already warns about missing required peers during install\n- For pnpm, implicit peer bindings are discovered by scanning sibling entries in virtual store directories. This relies on pnpm's internal directory naming convention (`_` separator for peer variants)\n\n## License\n\n[Apache-2.0](LICENSE)\n","readmeFilename":"README.md"}