codemetrics-ai 0.4.0 → 0.6.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -5
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +11 -5
- package/dist/cli.js +7 -3
- package/dist/dependency-evidence.d.ts +5 -0
- package/dist/dependency-evidence.js +30 -0
- package/dist/dependency-inventory.d.ts +67 -0
- package/dist/dependency-inventory.js +203 -0
- package/dist/dependency-metadata.d.ts +65 -0
- package/dist/dependency-metadata.js +125 -0
- package/dist/dependency-pnpm.d.ts +4 -0
- package/dist/dependency-pnpm.js +237 -0
- package/dist/dependency-spec.d.ts +13 -0
- package/dist/dependency-spec.js +30 -0
- package/dist/error-handling-scoring.d.ts +1 -0
- package/dist/error-handling-scoring.js +15 -0
- package/dist/error-handling.d.ts +4 -1
- package/dist/error-handling.js +50 -17
- package/dist/evidence-reader.js +3 -1
- package/dist/evidence-tools.js +4 -0
- package/dist/failure-contracts.d.ts +19 -0
- package/dist/failure-contracts.js +261 -0
- package/dist/function-scoring.d.ts +2 -1
- package/dist/function-scoring.js +13 -24
- package/dist/handler-disposition.d.ts +9 -0
- package/dist/handler-disposition.js +156 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/python/ASYNC_ADAPTERS.md +38 -0
- package/dist/python/ASYNC_CONTEXT.md +58 -0
- package/dist/python/ASYNC_SCORING.md +174 -0
- package/dist/python/ASYNC_USAGE.md +62 -0
- package/dist/python/BEHAVIOR_REVIEW.md +84 -0
- package/dist/python/COVERAGE.md +96 -0
- package/dist/python/DECOMPOSITION.md +46 -0
- package/dist/python/DUPLICATION.md +36 -0
- package/dist/python/ERROR_HANDLING.md +45 -0
- package/dist/python/ERROR_HANDLING_CONTEXT.md +61 -0
- package/dist/python/ERROR_HANDLING_OPERATIONS.md +54 -0
- package/dist/python/ERROR_HANDLING_TRANSFERS.md +64 -0
- package/dist/python/METHOD_COMPLEXITY.md +46 -0
- package/dist/python/README.md +162 -0
- package/dist/python/analysis.py +54 -0
- package/dist/python/async_adapters.py +243 -0
- package/dist/python/async_bindings.py +180 -0
- package/dist/python/async_context.py +143 -0
- package/dist/python/async_helper_paths.py +186 -0
- package/dist/python/async_provenance.py +129 -0
- package/dist/python/async_scoring.py +203 -0
- package/dist/python/async_usage.py +244 -0
- package/dist/python/coverage_collect.py +121 -0
- package/dist/python/coverage_provenance.py +77 -0
- package/dist/python/coverage_review.py +273 -0
- package/dist/python/coverage_worker.py +46 -0
- package/dist/python/decomposition.py +199 -0
- package/dist/python/duplication.py +177 -0
- package/dist/python/error_handling.py +249 -0
- package/dist/python/error_handling_context.py +294 -0
- package/dist/python/error_handling_operations.py +228 -0
- package/dist/python/error_handling_transfers.py +211 -0
- package/dist/python/review_cli.py +36 -0
- package/dist/python/source_facts.py +293 -0
- package/dist/python/source_selection.py +157 -0
- package/dist/python-analyzer.d.ts +21 -0
- package/dist/python-analyzer.js +123 -0
- package/dist/python-async-adapters.d.ts +67 -0
- package/dist/python-async-adapters.js +197 -0
- package/dist/python-async-context.d.ts +73 -0
- package/dist/python-async-context.js +181 -0
- package/dist/python-async-scoring.d.ts +89 -0
- package/dist/python-async-scoring.js +249 -0
- package/dist/python-async-usage.d.ts +56 -0
- package/dist/python-async-usage.js +143 -0
- package/dist/python-cli.d.ts +2 -0
- package/dist/python-cli.js +53 -0
- package/dist/python-collect-cli.d.ts +2 -0
- package/dist/python-collect-cli.js +28 -0
- package/dist/python-complexity.d.ts +18 -0
- package/dist/python-complexity.js +36 -0
- package/dist/python-coverage.d.ts +63 -0
- package/dist/python-coverage.js +173 -0
- package/dist/python-decomposition.d.ts +56 -0
- package/dist/python-decomposition.js +102 -0
- package/dist/python-duplication.d.ts +65 -0
- package/dist/python-duplication.js +89 -0
- package/dist/python-error-context.d.ts +84 -0
- package/dist/python-error-context.js +165 -0
- package/dist/python-error-handling.d.ts +56 -0
- package/dist/python-error-handling.js +120 -0
- package/dist/python-error-operations.d.ts +44 -0
- package/dist/python-error-operations.js +155 -0
- package/dist/python-error-transfers.d.ts +38 -0
- package/dist/python-error-transfers.js +125 -0
- package/dist/python-evidence.d.ts +23 -0
- package/dist/python-evidence.js +255 -0
- package/dist/score-arithmetic.d.ts +1 -12
- package/dist/score-arithmetic.js +1 -40
- package/dist/shared-source-facts/arithmetic.d.mts +9 -0
- package/dist/shared-source-facts/arithmetic.mjs +32 -0
- package/dist/shared-source-facts/contract.d.mts +35 -0
- package/dist/shared-source-facts/contract.mjs +95 -0
- package/dist/shared-source-facts/population.d.mts +12 -0
- package/dist/shared-source-facts/population.mjs +41 -0
- package/dist/source-facts.d.ts +4 -0
- package/dist/source-facts.js +16 -0
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# codemetrics-ai
|
|
2
2
|
|
|
3
|
-
Deterministic source analysis for JavaScript, TypeScript, JSX and TSX
|
|
3
|
+
Deterministic source analysis for JavaScript, TypeScript, JSX and TSX, with Python support in preview 0.6.0-preview.1. The package implements source metrics and React hook/effect checks and emits schema-v3 evidence, including executed scoring decisions. Scores are **uncalibrated across ecosystems**; compare them only with compatible runs of this analyzer.
|
|
4
4
|
|
|
5
5
|
Each scored dimension includes `scoringDecision`: policy inputs, executed steps, rounding and finding attribution. These are aggregate policy decisions, not independent finding deductions. See [the decision contract](../../shared/scorecard-schema/scoring-decisions.md).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Version 0.5.0 uses `javascript-typescript-2026-10-09-handler-contracts` and adds
|
|
8
|
+
contextual Error Handling scoring. Adopting it requires a fresh baseline. See the
|
|
9
|
+
[0.5.0 release notes](../../docs/releases/javascript-typescript-0.5.0.md).
|
|
10
|
+
|
|
11
|
+
The earlier 0.4.0 ruleset `javascript-typescript-2026-10-04-local-handlers` replaces the published 0.3.0 maximum-CC/median-MI ladders with owned-function population scores and extends React-only checks with bounded promise-usage analysis. It also corrects erased TypeScript syntax in owned-body measurements, scores async usage by affected eligible owners, and adds unscored module dependency, decomposition, and error-handler evidence. Generate new baselines when upgrading from 0.3.0. See the [0.4.0 release and migration notes](https://github.com/sean-m-cooper/CodeMetrics.AI/blob/master/docs/releases/javascript-typescript-0.4.0.md).
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
14
|
npx codemetrics-ai --project package.json
|
|
@@ -16,13 +20,18 @@ Default output paths are `.scorecard/javascript-typescript/metrics.csv` and `.sc
|
|
|
16
20
|
|
|
17
21
|
## Implemented scope
|
|
18
22
|
|
|
23
|
+
Preview 0.6.0-preview.1 adds unscored npm/pnpm dependency inventory and optional
|
|
24
|
+
registry observations under ruleset `javascript-typescript-2026-10-09-dependency-evidence`.
|
|
25
|
+
It requires fresh baselines. Stable 0.5.0 does not include this extension. Install the preview explicitly; the npm `latest` tag stays on the stable release.
|
|
26
|
+
|
|
19
27
|
| Dimension | Implementation |
|
|
20
28
|
|---|---|
|
|
21
29
|
| codeQuality (Method Complexity) | 40% one worst function score + 60% mean remaining; owned CC findings above 10. Decomposition has descriptive size/distribution evidence and no score. |
|
|
22
30
|
| maintainability | 40% mean weakest ceil(N/5) function scores + 60% mean remaining |
|
|
23
31
|
| performanceAsync (Async/Blocking Usage) | React hook/effect checks, standard Promise async executors, and informational async Array.forEach callbacks |
|
|
24
32
|
| architecture | Module dependencies, explicit type-only separation, internal cycle groups and fan-in/out rankings; score remains unmeasured |
|
|
25
|
-
| errorHandling |
|
|
33
|
+
| errorHandling | Contextual failure dispositions for distinct catch/Promise handler bodies; scores unexplained swallowing only with a fully assessed population |
|
|
34
|
+
| dependencyManagement (preview) | npm lockfile v2/v3 and pnpm v9 inventory, workspace uses, optional public-registry deprecation/update/advisory observations; no score |
|
|
26
35
|
| Other dimensions | Explicitly skipped |
|
|
27
36
|
|
|
28
37
|
React checks run only when a source file imports React. Aliased named imports and namespace imports are recognized; local functions shadowing imports are excluded. Missing dependency arrays are advisory because running after every render can be intentional. The implementation does not claim exhaustive Rules of Hooks, dependency-array correctness, or general concurrency analysis. Source syntax errors make the run incomplete and source dimensions unscored. Semantic type errors are outside this AST-focused version's completeness check.
|
|
@@ -44,7 +53,9 @@ This is bounded source analysis, not promise-flow verification. Referenced execu
|
|
|
44
53
|
|
|
45
54
|
`codeQuality.componentDetails.decomposition` version 2 uses owned executable statements as its primary measure. It ranks functions/modules by statements and reports statement totals, median/p90, and module concentration. Lines and initializer/literal/declaration/re-export observations remain context. Nested bodies are counted separately. Size is not responsibility: data tables, barrels, and orchestration roles receive no inferred defect or deduction. Code Quality remains a method-complexity score.
|
|
46
55
|
|
|
47
|
-
`errorHandling.handlerEvidence`
|
|
56
|
+
`errorHandling.handlerEvidence` preserves empty/documented/nonempty syntax measurements and links each distinct implementation to its uses. Referenced Promise callbacks resolve through function declarations, const aliases, and imports within selected source. The new `dispositionEvidence` separates explicit outcomes, recognized fallback, documented intent, unexplained swallowing, and unknown paths. Nonempty code alone does not establish handling. Attached intent is honored without judging the business rationale.
|
|
57
|
+
|
|
58
|
+
When every observed body is assessed and no callback uses are unresolved, Error Handling scores `10 - 10 * unexplainedSwallowing / assessedHandlers`, capped at 9 if any issue exists, rounded half up to one decimal. Shared bodies count once. Unknown dispositions, unresolved callbacks, and absent populations yield no score. Scores describe bounded handling signals, not proven recovery or bugs. See the [policy and recognition limits](../../shared/scorecard-schema/javascript-typescript-error-handling-policy.md).
|
|
48
59
|
|
|
49
60
|
## Module architecture evidence
|
|
50
61
|
|
|
@@ -58,6 +69,23 @@ Cycles are strongly connected groups of resolved internal value/mixed dependenci
|
|
|
58
69
|
|
|
59
70
|
Architecture retains `status: skipped` and no score until calibration. Other measured dimensions remain usable when this diagnostic graph has resolution gaps. Source parse failures retain partial graph evidence with `status: failed` and the existing incomplete-analysis exit. Schema v3 is unchanged; the new scope/ruleset requires fresh baselines. See the [graph contract](../../shared/scorecard-schema/javascript-typescript-module-graph.md).
|
|
60
71
|
|
|
72
|
+
## Dependency observations (development)
|
|
73
|
+
|
|
74
|
+
Inventory supports npm lockfiles v2/v3 and pnpm v9, including pnpm 12 environment/project documents, catalogs, workspace links, aliases, and peer snapshots. The project graph is selected; environment tooling is explicitly excluded. Peer variants retain locations without multiplying package/version counts. Older pnpm formats and file/git/custom resolutions remain gaps. Integrity-only records do not establish public-registry provenance.
|
|
75
|
+
|
|
76
|
+
Inventory is local by default. `--dependency-metadata` explicitly enables bounded
|
|
77
|
+
public npm registry metadata and bulk advisory queries. It never installs target
|
|
78
|
+
dependencies or runs their scripts. Unsupported package managers, stale locks,
|
|
79
|
+
private/unknown sources, and failed queries are exposed as coverage gaps.
|
|
80
|
+
Optional peers and locked range differences remain contextual notices. Duplicate
|
|
81
|
+
installations retain their locations without creating independent penalties.
|
|
82
|
+
|
|
83
|
+
Deprecation, `wanted` updates within direct declared ranges, `latest` releases,
|
|
84
|
+
and advisories are separate observations. Exact pins and newer major versions do
|
|
85
|
+
not establish neglect; advisory presence does not establish runtime reachability.
|
|
86
|
+
The dimension remains unscored. See the [dependency policy](../../shared/scorecard-schema/javascript-typescript-dependency-policy.md)
|
|
87
|
+
for counting, limits, availability, and calibration boundaries.
|
|
88
|
+
|
|
61
89
|
## Discovery
|
|
62
90
|
|
|
63
91
|
The CLI discovers files from each package's tsconfig when present, otherwise scans source extensions. Positive npm workspace globs (including object-form `workspaces.packages`) are supported. Each child package owns its files and exclusions; the root cannot re-include a child's excluded files. External tsconfig references are not recursively analyzed as separate packages: include them in the workspace manifest. Explicit missing manifests/configs and unsupported negative workspace patterns are errors.
|
|
@@ -120,7 +148,7 @@ evidence. Overall dependencies and cycle findings remain intact, including mixed
|
|
|
120
148
|
files and cycles crossing the views. Architecture is still unscored. See the
|
|
121
149
|
[dependency view contract](../../shared/scorecard-schema/javascript-typescript-module-graph.md#implementation-reference-and-public-re-export-views).
|
|
122
150
|
|
|
123
|
-
`metrics.ts` owns source traversal, raw member metrics and finding identities. `function-measurements.ts` collects the separate owned-body measurements; `function-scoring.ts` applies the two population policies with exact interpolation/rounding from `score-arithmetic.ts`. `react-probe.ts` checks calls owned by each function for React hook/effect observations. `async-probe.ts` checks each visited node once for bounded standard-library promise patterns, including module-level operations. Neither probe traverses nested bodies or calculates metrics. `async-population.ts` assigns distinct eligible owners; `async-scoring.ts` applies the population ladder. `decomposition-evidence.ts` records function size and module distribution without deductions. `handler-resolution.ts` performs bounded declaration lookup; `error-handling.ts`
|
|
151
|
+
`metrics.ts` owns source traversal, raw member metrics and finding identities. `function-measurements.ts` collects the separate owned-body measurements; `function-scoring.ts` applies the two population policies with exact interpolation/rounding from `score-arithmetic.ts`. `react-probe.ts` checks calls owned by each function for React hook/effect observations. `async-probe.ts` checks each visited node once for bounded standard-library promise patterns, including module-level operations. Neither probe traverses nested bodies or calculates metrics. `async-population.ts` assigns distinct eligible owners; `async-scoring.ts` applies the population ladder. `decomposition-evidence.ts` records function size and module distribution without deductions. `handler-resolution.ts` performs bounded declaration lookup; `handler-disposition.ts` classifies bounded failure paths; `failure-contracts.ts` resolves local logging, callback, and guarded-result contracts; `error-handling.ts` aggregates bodies, uses, context, and findings; `error-handling-scoring.ts` applies the population formula. Regression fixtures preserve raw metrics and source identities; the handler policy and ruleset change is intentional.
|
|
124
152
|
|
|
125
153
|
```sh
|
|
126
154
|
npm ci
|
|
@@ -160,3 +188,21 @@ records results and limitations. Executable statements are the accepted primary
|
|
|
160
188
|
and weights remain experimental; Code Quality continues to score method complexity
|
|
161
189
|
only. The [statement calibration report](../../shared/scorecard-schema/calibration-runs/javascript-typescript-statements-2026-10-04.md)
|
|
162
190
|
records the updated preview and counting contract.
|
|
191
|
+
|
|
192
|
+
The [completed context calibration](../../shared/scorecard-schema/javascript-typescript-context-policy-decision.md)
|
|
193
|
+
retained decomposition and error handling as unscored evidence at that stage. Across 4,062 functions,
|
|
194
|
+
the large-statement candidates overlapped existing CC/MI review groups; source review
|
|
195
|
+
also found explicit fallback and declared-intent counterexamples to body-only handler
|
|
196
|
+
deductions. The released workflow was verified on all six selections and a compatible
|
|
197
|
+
Uptime Kuma rerun. Numerical experiments remain outside the analyzer and are not
|
|
198
|
+
consumer scoring policies. The subsequent [contextual handler policy](../../shared/scorecard-schema/javascript-typescript-error-handling-policy.md)
|
|
199
|
+
enables the agreed population formula only where bounded classification establishes
|
|
200
|
+
an assessable population. Decomposition remains unscored.
|
|
201
|
+
|
|
202
|
+
## Experimental Python command in development builds
|
|
203
|
+
|
|
204
|
+
Local development archives bundle `codemetrics-python` and its Python 3.12 AST adapter, version `0.15.0`. It parses source without importing the target and produces evidence v3 with a production Method Complexity score, owned statements, block nesting, duplication, optional coverage/CRAP review evidence, and unscored exception-handler observations at `dimensions.errorHandling.review`, plus bounded lexical context and conservative patterns at `dimensions.errorHandling.contextReview`, with owned raise/cause syntax at `dimensions.errorHandling.transferReview` and owned calls/returns plus unresolved-suite inventory at `dimensions.errorHandling.operationReview`, and async-owned call evidence at `dimensions.performanceAsync.review`. `--coverage-json` imports observations; `--collection-manifest` checks their source binding. Adapter 0.15.0 also scores Async/Blocking Usage from production-owner contributions at `performanceAsync.asyncScoring`, with bounded paths at `performanceAsync.helperReview`. The other seven Python dimensions remain unavailable; decomposition, duplication, coverage/CRAP, and handler observations remain unscored. Install `codemetrics-ai@0.6.0-preview.1` explicitly to use these commands; stable npm 0.5.0 does not include them. For example: `npm exec --yes --package=codemetrics-ai@0.6.0-preview.1 -- codemetrics-python --project ./pyproject.toml --scorecard-output ./python-evidence.json`. The installed package includes its Python guide at `dist/python/README.md` and scoring policy at `dist/python/ASYNC_SCORING.md`. Ordinary analysis/import needs only Node.js 20+ and Python 3.12. The separate `codemetrics-python-collect` command explicitly executes tests and requires a caller-supplied environment containing coverage.py and the project's test dependencies.
|
|
205
|
+
|
|
206
|
+
Python adapter 0.13.0 also embeds unscored async call context at `dimensions.performanceAsync.contextReview`, including unresolved reason/shape inventories and bounded local callback/stored-wrapper links. The package ships `dist/python/ASYNC_CONTEXT.md`; historical 0.12.0 reports remain valid without this extension.
|
|
207
|
+
|
|
208
|
+
Python adapter 0.14.0 additionally embeds unscored partial construction/reference context and narrow `sync_to_async` decorator links at `dimensions.performanceAsync.adapterReview`. The package ships `dist/python/ASYNC_ADAPTERS.md`; historical 0.13.0 evidence remains valid without this report. This adapter report itself remains unscored; 0.15.0 adds the separate async scoring contract. Error Handling remains unscored.
|
package/dist/analyzer.d.ts
CHANGED
package/dist/analyzer.js
CHANGED
|
@@ -11,6 +11,9 @@ import { asyncPopulation } from "./async-population.js";
|
|
|
11
11
|
import { decompositionModule, decompositionEvidence } from "./decomposition-evidence.js";
|
|
12
12
|
import { HandlerResolver } from "./handler-resolution.js";
|
|
13
13
|
import { inspectErrorHandling, errorHandlingEvidence } from "./error-handling.js";
|
|
14
|
+
import { FailureContracts } from "./failure-contracts.js";
|
|
15
|
+
import { dependencyInventory } from "./dependency-inventory.js";
|
|
16
|
+
import { dependencyEvidence } from "./dependency-evidence.js";
|
|
14
17
|
export function analyze(options, version) {
|
|
15
18
|
const identity = invocationIds(options.runId, options.auditId);
|
|
16
19
|
const discovery = discover(options.project, options.tsconfig);
|
|
@@ -30,6 +33,7 @@ export function analyze(options, version) {
|
|
|
30
33
|
const handlerSources = pkg.files.map(filename => program.getSourceFile(filename))
|
|
31
34
|
.filter((source) => !!source && !program.getSyntacticDiagnostics(source).length);
|
|
32
35
|
const handlerResolver = new HandlerResolver(checker, handlerSources, discovery.repositoryRoot, pkg.name);
|
|
36
|
+
const failureContracts = new FailureContracts(checker, program.getSourceFiles(), handlerSources, discovery.repositoryRoot);
|
|
33
37
|
const collectModules = moduleGraph.forPackage(pkg, program);
|
|
34
38
|
for (const filename of pkg.files) {
|
|
35
39
|
const source = program.getSourceFile(filename);
|
|
@@ -48,7 +52,7 @@ export function analyze(options, version) {
|
|
|
48
52
|
const file = path.relative(discovery.repositoryRoot, filename).replaceAll("\\", "/");
|
|
49
53
|
asyncOwners.push(...asyncPopulation(source, checker, file, pkg.name, result.functions, result.findings.filter(item => item.dimension === "performanceAsync").map(item => item.finding)));
|
|
50
54
|
decompositionModules.push(decompositionModule(source, file, pkg.name, result.functions));
|
|
51
|
-
errorHandling.push(inspectErrorHandling(source, checker, file, pkg.name, handlerResolver));
|
|
55
|
+
errorHandling.push(inspectErrorHandling(source, checker, file, pkg.name, handlerResolver, failureContracts));
|
|
52
56
|
metrics.push(...result.metrics);
|
|
53
57
|
collected.push(...result.findings);
|
|
54
58
|
functions.push(...result.functions);
|
|
@@ -67,6 +71,8 @@ export function analyze(options, version) {
|
|
|
67
71
|
dimensions[key] = { status: "failed", basis: "Incomplete source analysis; partial findings are unscored.", findings: dimensions[key].findings, scope: dimensions[key].scope };
|
|
68
72
|
dimensions.architecture = moduleGraph.finish(diagnostics.length > 0);
|
|
69
73
|
dimensions.errorHandling = errorHandlingEvidence(errorHandling, diagnostics.length > 0);
|
|
74
|
+
const dependencies = dependencyInventory(discovery.packages, discovery.repositoryRoot);
|
|
75
|
+
dimensions.dependencyManagement = dependencyEvidence(dependencies.inventory, undefined, options.dependencyMetadata ? "public-registry" : "offline");
|
|
70
76
|
dimensions.codeQuality.componentDetails = { ...(dimensions.codeQuality.componentDetails ?? {}),
|
|
71
77
|
decomposition: decompositionEvidence(decompositionModules, diagnostics.length > 0) };
|
|
72
78
|
const evidence = {
|
|
@@ -75,11 +81,11 @@ export function analyze(options, version) {
|
|
|
75
81
|
filters: { totalUnits: discovery.packages.reduce((sum, pkg) => sum + pkg.files.length, 0) + discovery.skipped.length,
|
|
76
82
|
analyzedUnits: analyzedFiles, skipped: discovery.skipped },
|
|
77
83
|
population: { types: new Set(metrics.map(metric => `${metric.project}|${metric.file}|${metric.type}`)).size, members: metrics.length },
|
|
78
|
-
dimensions, analysis: { ...identity, status: diagnostics.length ? "incomplete" : "complete", ruleset: "javascript-typescript-2026-10-
|
|
79
|
-
calibration: "uncalibrated", configurationFingerprint: hash(JSON.stringify(canonicalConfiguration(discovery.packages.map(pkg => ({ name: pkg.name,
|
|
80
|
-
|
|
84
|
+
dimensions, analysis: { ...identity, status: diagnostics.length ? "incomplete" : "complete", ruleset: "javascript-typescript-2026-10-09-dependency-evidence",
|
|
85
|
+
calibration: "uncalibrated", configurationFingerprint: hash(JSON.stringify(canonicalConfiguration({ packages: discovery.packages.map(pkg => ({ name: pkg.name,
|
|
86
|
+
options: pkg.options, selection: pkg.selection })), dependencyMetadata: options.dependencyMetadata ? "public-registry" : "offline" }, discovery.repositoryRoot))), diagnostics, suppressions: [] }
|
|
81
87
|
};
|
|
82
|
-
return { evidence, metrics, csv: toCsv(metrics), inputs: discovery.packages.flatMap(pkg => [pkg.entryPoint, ...pkg.files]) };
|
|
88
|
+
return { evidence, metrics, csv: toCsv(metrics), inputs: [...dependencies.inputs, ...discovery.packages.flatMap(pkg => [pkg.entryPoint, ...pkg.files])] };
|
|
83
89
|
}
|
|
84
90
|
function canonicalConfiguration(value, root) {
|
|
85
91
|
if (typeof value === "string")
|
package/dist/cli.js
CHANGED
|
@@ -5,18 +5,20 @@ import { analyze } from "./analyzer.js";
|
|
|
5
5
|
import { defaultEvidencePath, defaultMetricsPath } from "./scorecard-contract.js";
|
|
6
6
|
import { compare, gate, readEvidence, sarif, validateEvidence } from "./evidence-tools.js";
|
|
7
7
|
import { distinctOutputs, write } from "./io.js";
|
|
8
|
+
import { enrichDependencies } from "./dependency-evidence.js";
|
|
8
9
|
try {
|
|
9
10
|
const { values } = parseArgs({ options: {
|
|
10
11
|
project: { type: "string" }, tsconfig: { type: "string" }, "run-id": { type: "string" }, "audit-id": { type: "string" }, output: { type: "string", default: defaultMetricsPath },
|
|
11
12
|
"scorecard-output": { type: "string", default: defaultEvidencePath }, baseline: { type: "string" },
|
|
12
13
|
"comparison-output": { type: "string" }, sarif: { type: "string" }, "fail-on-new": { type: "string" },
|
|
13
|
-
"max-score-drop": { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" }
|
|
14
|
+
"max-score-drop": { type: "string" }, "dependency-metadata": { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" }
|
|
14
15
|
} });
|
|
15
16
|
const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
16
17
|
if (values.help)
|
|
17
18
|
console.log([
|
|
18
19
|
"codemetrics-ai — JavaScript, TypeScript and React source analysis",
|
|
19
20
|
"--project <package.json> --tsconfig <tsconfig.json>",
|
|
21
|
+
"--dependency-metadata (opt in to bounded public npm registry observations; inventory is always local)",
|
|
20
22
|
"--run-id <uuid> --audit-id <uuid> (generated when omitted)",
|
|
21
23
|
"--output <metrics.csv> --scorecard-output <evidence.json>",
|
|
22
24
|
"--baseline <evidence.json> --comparison-output <comparison.json>",
|
|
@@ -30,9 +32,11 @@ try {
|
|
|
30
32
|
if ((values["fail-on-new"] || values["max-score-drop"] || values["comparison-output"]) && !values.baseline)
|
|
31
33
|
throw new Error("Comparison and quality gates require --baseline.");
|
|
32
34
|
distinctOutputs([values.project ?? "package.json", values.tsconfig, values.baseline], [values.output, values["scorecard-output"], values["comparison-output"], values.sarif]);
|
|
33
|
-
const result = analyze({ ...values, runId: values["run-id"], auditId: values["audit-id"] }, version);
|
|
34
|
-
validateEvidence(result.evidence);
|
|
35
|
+
const result = analyze({ ...values, runId: values["run-id"], auditId: values["audit-id"], dependencyMetadata: values["dependency-metadata"] }, version);
|
|
35
36
|
distinctOutputs(result.inputs, [values.output, values["scorecard-output"], values["comparison-output"], values.sarif]);
|
|
37
|
+
if (values["dependency-metadata"])
|
|
38
|
+
await enrichDependencies(result.evidence);
|
|
39
|
+
validateEvidence(result.evidence);
|
|
36
40
|
const comparison = values.baseline ? compare(result.evidence, readEvidence(values.baseline)) : undefined;
|
|
37
41
|
const failed = comparison ? gate(comparison, values["fail-on-new"], values["max-score-drop"] === undefined ? undefined : Number(values["max-score-drop"])) : false;
|
|
38
42
|
write(values.output, result.csv);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Dimension, type Evidence } from "./evidence.js";
|
|
2
|
+
import type { DependencyInventory } from "./dependency-inventory.js";
|
|
3
|
+
import { type DependencyMetadata, type RegistryReader } from "./dependency-metadata.js";
|
|
4
|
+
export declare function dependencyEvidence(inventory: DependencyInventory, metadata?: DependencyMetadata, metadataMode?: "offline" | "public-registry"): Dimension;
|
|
5
|
+
export declare function enrichDependencies(evidence: Evidence, reader?: RegistryReader): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { hash } from "./evidence.js";
|
|
2
|
+
import { dependencyMetadata, noMetadata } from "./dependency-metadata.js";
|
|
3
|
+
export function dependencyEvidence(inventory, metadata = noMetadata(), metadataMode = metadata.status === "notRequested" ? "offline" : "public-registry") {
|
|
4
|
+
const findings = [];
|
|
5
|
+
const add = (id, name, category, message, observations) => findings.push({
|
|
6
|
+
category, ruleId: `javascript-typescript/dependencyManagement/${category}`, fingerprint: hash(`${id}|${category}`),
|
|
7
|
+
severity: "info", confidence: "high", package: name, message, observations: { ...observations, scoreDisposition: "excludedUnscored" }
|
|
8
|
+
});
|
|
9
|
+
for (const row of metadata.observations) {
|
|
10
|
+
if (row.deprecated)
|
|
11
|
+
add(row.packageId, row.name, "deprecatedPackage", `${row.name} ${row.version} is deprecated: ${row.deprecated}`, { packageId: row.packageId, version: row.version });
|
|
12
|
+
if (row.upgrades?.some(upgrade => upgrade.newerWithinRange))
|
|
13
|
+
add(row.packageId, row.name, "updateWithinDeclaredRange", `${row.name} has an update within a declared version range; compatibility and intent require review.`, { packageId: row.packageId, upgrades: row.upgrades });
|
|
14
|
+
for (const advisory of row.vulnerabilities.advisories ?? [])
|
|
15
|
+
add(`${row.packageId}|${advisory.id}`, row.name, "reportedVulnerability", `${row.name} ${row.version}: ${advisory.title}`, { packageId: row.packageId, advisory, reachability: "notAssessed" });
|
|
16
|
+
}
|
|
17
|
+
return { status: "skipped", basis: "Dependency inventory and registry observations only; numerical scoring is not calibrated. Missing evidence is not a code defect.",
|
|
18
|
+
scope: { id: "javascript-typescript/dependencyManagement/lock-observations-v1", coverage: "partial",
|
|
19
|
+
includes: ["npm-lockfile-v2-v3", "pnpm-lockfile-v9", "selected-workspace-reachability", "public-registry-metadata-if-requested"],
|
|
20
|
+
excludes: ["dependency-scoring", "runtime-reachability", "upgrade-safety", "private-registry-authentication", "yarn-bun-lockfiles", "pnpm-file-git-custom-resolutions", "pnpm-environment-tooling-document", "implicit-registry-provenance"] },
|
|
21
|
+
findings, dependencyInventory: inventory, dependencyMetadata: metadata, metadataMode };
|
|
22
|
+
}
|
|
23
|
+
export async function enrichDependencies(evidence, reader) {
|
|
24
|
+
if (evidence.dimensions.dependencyManagement.metadataMode !== "public-registry")
|
|
25
|
+
throw new Error("Analyze with dependencyMetadata: true before requesting registry observations");
|
|
26
|
+
const inventory = evidence.dimensions.dependencyManagement.dependencyInventory;
|
|
27
|
+
if (!inventory)
|
|
28
|
+
throw new Error("Dependency inventory is required before metadata acquisition");
|
|
29
|
+
evidence.dimensions.dependencyManagement = dependencyEvidence(inventory, await dependencyMetadata(inventory, reader));
|
|
30
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PackageInput } from "./discovery.js";
|
|
2
|
+
import { declarations, type DependencyKind } from "./dependency-spec.js";
|
|
3
|
+
export { declarations, registrySpec, type DependencyKind } from "./dependency-spec.js";
|
|
4
|
+
export interface DependencyUse {
|
|
5
|
+
project: string;
|
|
6
|
+
manifest: string;
|
|
7
|
+
from: string;
|
|
8
|
+
requested: string;
|
|
9
|
+
range: string;
|
|
10
|
+
resolvedRange?: string;
|
|
11
|
+
kind: DependencyKind;
|
|
12
|
+
direct: boolean;
|
|
13
|
+
optional: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface DependencyPackage {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
source: "publicRegistry" | "otherSource" | "workspace";
|
|
20
|
+
locations: string[];
|
|
21
|
+
uses: DependencyUse[];
|
|
22
|
+
}
|
|
23
|
+
export interface DependencyGap {
|
|
24
|
+
manifest: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
dependency?: string;
|
|
27
|
+
from?: string;
|
|
28
|
+
range?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DependencyInventory {
|
|
31
|
+
version: 1;
|
|
32
|
+
status: "complete" | "gaps";
|
|
33
|
+
countingUnit: "packageNameVersionSource";
|
|
34
|
+
packages: DependencyPackage[];
|
|
35
|
+
gaps: DependencyGap[];
|
|
36
|
+
notices: {
|
|
37
|
+
manifest: string;
|
|
38
|
+
from: string;
|
|
39
|
+
dependency: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
range: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
}[];
|
|
44
|
+
manifests: {
|
|
45
|
+
file: string;
|
|
46
|
+
lockfile?: string;
|
|
47
|
+
manager?: string;
|
|
48
|
+
declarations: ReturnType<typeof declarations>;
|
|
49
|
+
overrides?: unknown;
|
|
50
|
+
}[];
|
|
51
|
+
lockfiles: {
|
|
52
|
+
file: string;
|
|
53
|
+
version: number;
|
|
54
|
+
sha256: string;
|
|
55
|
+
projectDocument?: number;
|
|
56
|
+
environmentDocument?: "excluded";
|
|
57
|
+
}[];
|
|
58
|
+
configurationInputs?: {
|
|
59
|
+
file: string;
|
|
60
|
+
sha256: string;
|
|
61
|
+
}[];
|
|
62
|
+
}
|
|
63
|
+
/** Reads only selected manifests and reachable lockfile entries; never installs or executes packages. */
|
|
64
|
+
export declare function dependencyInventory(selected: Pick<PackageInput, "entryPoint" | "root" | "name">[], root: string): {
|
|
65
|
+
inventory: DependencyInventory;
|
|
66
|
+
inputs: string[];
|
|
67
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import { hash } from "./evidence.js";
|
|
5
|
+
import { pnpmInventory } from "./dependency-pnpm.js";
|
|
6
|
+
import { declarations, registrySpec } from "./dependency-spec.js";
|
|
7
|
+
export { declarations, registrySpec } from "./dependency-spec.js";
|
|
8
|
+
const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const slash = (value) => value.replaceAll("\\", "/");
|
|
10
|
+
const within = (root, file) => { const r = path.relative(root, file); return !path.isAbsolute(r) && r !== ".." && !r.startsWith(".." + path.sep); };
|
|
11
|
+
function read(file) {
|
|
12
|
+
if (fs.statSync(file).size > 20 * 1024 * 1024)
|
|
13
|
+
throw new Error("Dependency input exceeds 20 MiB");
|
|
14
|
+
const text = fs.readFileSync(file, "utf8"), data = JSON.parse(text);
|
|
15
|
+
if (!object(data))
|
|
16
|
+
throw new Error("Dependency input must be an object");
|
|
17
|
+
return { data, sha256: hash(text) };
|
|
18
|
+
}
|
|
19
|
+
function locate(start, root, inputs) {
|
|
20
|
+
const directories = [];
|
|
21
|
+
for (let dir = start; within(root, dir); dir = path.dirname(dir)) {
|
|
22
|
+
directories.push(dir);
|
|
23
|
+
if (dir === root)
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
let manager;
|
|
27
|
+
for (const dir of directories) {
|
|
28
|
+
const manifest = path.join(dir, "package.json");
|
|
29
|
+
if (fs.existsSync(manifest))
|
|
30
|
+
inputs.add(manifest);
|
|
31
|
+
const declared = fs.existsSync(manifest) ? read(manifest).data.packageManager : undefined;
|
|
32
|
+
if (!manager && typeof declared === "string")
|
|
33
|
+
manager = declared;
|
|
34
|
+
}
|
|
35
|
+
if (manager && !/^(npm|pnpm)@/.test(manager))
|
|
36
|
+
return { manager };
|
|
37
|
+
for (const dir of directories) {
|
|
38
|
+
const candidates = manager?.startsWith("pnpm@") ? ["pnpm-lock.yaml"] : manager?.startsWith("npm@") ? ["npm-shrinkwrap.json", "package-lock.json"] : ["pnpm-lock.yaml", "npm-shrinkwrap.json", "package-lock.json"];
|
|
39
|
+
if (!manager && fs.existsSync(path.join(dir, "pnpm-lock.yaml")) && ["npm-shrinkwrap.json", "package-lock.json", "yarn.lock", "bun.lock"].some(name => fs.existsSync(path.join(dir, name))))
|
|
40
|
+
return { manager: "ambiguous lockfiles without packageManager" };
|
|
41
|
+
for (const name of candidates) {
|
|
42
|
+
const file = path.join(dir, name);
|
|
43
|
+
if (fs.existsSync(file))
|
|
44
|
+
return { file, manager: name === "pnpm-lock.yaml" ? "pnpm" : "npm" };
|
|
45
|
+
}
|
|
46
|
+
const unsupported = ["yarn.lock", "bun.lock", "bun.lockb"].find(name => fs.existsSync(path.join(dir, name)));
|
|
47
|
+
if (unsupported) {
|
|
48
|
+
inputs.add(path.join(dir, unsupported));
|
|
49
|
+
return { manager: unsupported };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { manager };
|
|
53
|
+
}
|
|
54
|
+
function validLocation(location) {
|
|
55
|
+
return !location.includes("\\") && !path.posix.isAbsolute(location) && !location.split("/").includes("..") && !location.includes(":");
|
|
56
|
+
}
|
|
57
|
+
function resolve(packages, from, name) {
|
|
58
|
+
if (!/^(?:@[^/]+\/)?[^/]+$/.test(name) || name.includes("\\") || name.includes(".."))
|
|
59
|
+
return;
|
|
60
|
+
for (let dir = from;; dir = path.posix.dirname(dir)) {
|
|
61
|
+
const at = path.posix.join(dir, "node_modules", name);
|
|
62
|
+
if (object(packages[at]))
|
|
63
|
+
return at;
|
|
64
|
+
if (!dir || dir === ".")
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function packageSource(entry) {
|
|
69
|
+
if (typeof entry.resolved !== "string")
|
|
70
|
+
return "otherSource";
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(entry.resolved);
|
|
73
|
+
return url.protocol === "https:" && url.hostname === "registry.npmjs.org" && !url.username && !url.password ? "publicRegistry" : "otherSource";
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "otherSource";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Reads only selected manifests and reachable lockfile entries; never installs or executes packages. */
|
|
80
|
+
export function dependencyInventory(selected, root) {
|
|
81
|
+
const inventory = { version: 1, status: "complete", countingUnit: "packageNameVersionSource", packages: [], gaps: [], notices: [], manifests: [], lockfiles: [] };
|
|
82
|
+
const inputs = new Set(selected.map(pkg => pkg.entryPoint)), rows = new Map();
|
|
83
|
+
const locks = new Map();
|
|
84
|
+
const relative = (file) => slash(path.relative(root, file));
|
|
85
|
+
for (const pkg of selected) {
|
|
86
|
+
const manifest = relative(pkg.entryPoint);
|
|
87
|
+
const gap = (reason, dependency, from, range) => inventory.gaps.push({ manifest, reason, ...(dependency ? { dependency } : {}), ...(from !== undefined ? { from } : {}), ...(range ? { range } : {}) });
|
|
88
|
+
try {
|
|
89
|
+
const data = read(pkg.entryPoint).data, declared = declarations(data), located = locate(pkg.root, root, inputs);
|
|
90
|
+
inventory.manifests.push({ file: manifest, declarations: declared, ...(data.overrides ? { overrides: data.overrides } : {}), ...(located.file ? { lockfile: relative(located.file) } : {}), ...(located.manager ? { manager: located.manager } : {}) });
|
|
91
|
+
if (!located.file) {
|
|
92
|
+
gap(located.manager && !/^(npm|pnpm)@/.test(located.manager) ? `Unsupported package manager/lockfile: ${located.manager}` : `No ${located.manager?.split("@")[0] ?? "npm or pnpm"} lockfile found`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
inputs.add(located.file);
|
|
96
|
+
if (!within(root, fs.realpathSync(located.file))) {
|
|
97
|
+
gap("Lockfile is outside repository");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (located.manager === "pnpm") {
|
|
101
|
+
pnpmInventory(pkg, root, located.file, inventory, inputs, rows);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!locks.has(located.file))
|
|
105
|
+
locks.set(located.file, read(located.file));
|
|
106
|
+
const lock = locks.get(located.file), packages = lock.data.packages;
|
|
107
|
+
if (![2, 3].includes(Number(lock.data.lockfileVersion)) || !object(packages)) {
|
|
108
|
+
gap("Only npm lockfile versions 2 and 3 with a packages map are supported");
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!inventory.lockfiles.some(row => row.file === relative(located.file)))
|
|
112
|
+
inventory.lockfiles.push({ file: relative(located.file), version: Number(lock.data.lockfileVersion), sha256: lock.sha256 });
|
|
113
|
+
if (Object.keys(packages).some(location => !validLocation(location))) {
|
|
114
|
+
gap("Unsafe or malformed lockfile location");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const start = slash(path.relative(path.dirname(located.file), pkg.root)), locked = packages[start];
|
|
118
|
+
if (!object(locked)) {
|
|
119
|
+
gap("Selected manifest has no lockfile package entry");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (JSON.stringify(declared) !== JSON.stringify(declarations(locked))) {
|
|
123
|
+
gap("Manifest and lockfile declarations differ; resolved inventory is withheld");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const visited = new Set();
|
|
127
|
+
const queue = declared.map(edge => ({ from: start, ...edge, mayBeAbsent: edge.optional, role: edge.kind, direct: true }));
|
|
128
|
+
for (let index = 0; index < queue.length; index++) {
|
|
129
|
+
if (index >= 100000) {
|
|
130
|
+
gap("Dependency traversal limit exceeded");
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
const edge = queue[index], position = resolve(packages, edge.from, edge.requested);
|
|
134
|
+
if (!position) {
|
|
135
|
+
if (edge.mayBeAbsent)
|
|
136
|
+
inventory.notices.push({ manifest, from: edge.from, dependency: edge.requested, range: edge.range, reason: "Optional dependency/peer is absent from the lockfile; no missing-package deduction" });
|
|
137
|
+
else
|
|
138
|
+
gap(`Unresolved ${edge.kind} dependency in lockfile`, edge.requested, edge.from, edge.range);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let location = position, entry = packages[location];
|
|
142
|
+
const linked = entry.link === true;
|
|
143
|
+
if (linked) {
|
|
144
|
+
if (typeof entry.resolved !== "string" || !validLocation(entry.resolved) || !object(packages[entry.resolved])) {
|
|
145
|
+
gap("Unresolved workspace/link target", edge.requested);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
location = entry.resolved;
|
|
149
|
+
entry = packages[location];
|
|
150
|
+
const target = path.resolve(path.dirname(located.file), location, "package.json");
|
|
151
|
+
if (!within(root, target) || !fs.existsSync(target) || !within(root, fs.realpathSync(target))) {
|
|
152
|
+
gap("Workspace manifest unavailable or outside repository", edge.requested);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
inputs.add(target);
|
|
156
|
+
if (JSON.stringify(declarations(read(target).data)) !== JSON.stringify(declarations(entry))) {
|
|
157
|
+
gap("Workspace manifest and lockfile differ", edge.requested);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const spec = registrySpec(edge.requested, edge.range);
|
|
162
|
+
const name = typeof entry.name === "string" ? entry.name : spec?.name ?? edge.requested;
|
|
163
|
+
if (typeof entry.version !== "string" || !semver.valid(entry.version)) {
|
|
164
|
+
gap("Missing/non-semver resolved version", edge.requested);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (spec && semver.validRange(spec.range) && !semver.satisfies(entry.version, spec.range))
|
|
168
|
+
inventory.notices.push({ manifest, from: edge.from, dependency: edge.requested, range: edge.range, version: entry.version,
|
|
169
|
+
reason: "Locked version differs from requested range; overrides or peer-resolution policy may explain this. Installation validity is not assessed" });
|
|
170
|
+
if (spec && spec.name !== name)
|
|
171
|
+
inventory.notices.push({ manifest, from: edge.from, dependency: edge.requested, range: edge.range, version: entry.version,
|
|
172
|
+
reason: `Locked identity is ${name}; an alias/override may replace the requested package. Installation policy is not assessed` });
|
|
173
|
+
const source = linked ? "workspace" : packageSource(entry), id = hash(`${name}|${entry.version}|${source}`);
|
|
174
|
+
const row = rows.get(id) ?? { id, name, version: entry.version, source, locations: [], uses: [] };
|
|
175
|
+
rows.set(id, row);
|
|
176
|
+
const install = `${relative(located.file)}#${position}`;
|
|
177
|
+
if (!row.locations.includes(install))
|
|
178
|
+
row.locations.push(install);
|
|
179
|
+
const use = { project: pkg.name, manifest, from: edge.from, requested: edge.requested, range: edge.range, kind: edge.role, direct: edge.direct, optional: edge.optional };
|
|
180
|
+
if (!row.uses.some(existing => JSON.stringify(existing) === JSON.stringify(use)))
|
|
181
|
+
row.uses.push(use);
|
|
182
|
+
const key = `${location}|${edge.role}|${edge.optional}`;
|
|
183
|
+
if (visited.has(key))
|
|
184
|
+
continue;
|
|
185
|
+
visited.add(key);
|
|
186
|
+
for (const child of declarations(entry, false))
|
|
187
|
+
queue.push({ from: location, ...child, mayBeAbsent: child.optional, optional: edge.optional || child.optional, role: edge.role, direct: false });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
gap(error instanceof Error ? error.message : "Dependency inventory unavailable");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
inventory.packages = [...rows.values()].sort((a, b) => a.name.localeCompare(b.name, "en") || a.version.localeCompare(b.version, "en") || a.source.localeCompare(b.source, "en"));
|
|
195
|
+
for (const row of inventory.packages) {
|
|
196
|
+
row.locations.sort();
|
|
197
|
+
row.uses.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en"));
|
|
198
|
+
}
|
|
199
|
+
inventory.gaps = [...new Map(inventory.gaps.map(gap => [JSON.stringify(gap), gap])).values()];
|
|
200
|
+
inventory.notices = [...new Map(inventory.notices.map(notice => [JSON.stringify(notice), notice])).values()];
|
|
201
|
+
inventory.status = inventory.gaps.length ? "gaps" : "complete";
|
|
202
|
+
return { inventory, inputs: [...inputs] };
|
|
203
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type DependencyInventory } from "./dependency-inventory.js";
|
|
2
|
+
export interface MetadataResult {
|
|
3
|
+
status: "available" | "unavailable";
|
|
4
|
+
data?: unknown;
|
|
5
|
+
sha256?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RegistryReader {
|
|
9
|
+
package(name: string): Promise<MetadataResult>;
|
|
10
|
+
advisories(packages: Record<string, string[]>): Promise<MetadataResult>;
|
|
11
|
+
}
|
|
12
|
+
export interface PackageObservation {
|
|
13
|
+
packageId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
metadata: {
|
|
17
|
+
status: "available" | "unavailable";
|
|
18
|
+
reason?: string;
|
|
19
|
+
sha256?: string;
|
|
20
|
+
};
|
|
21
|
+
deprecated?: string | null;
|
|
22
|
+
latest?: string | null;
|
|
23
|
+
upgrades?: {
|
|
24
|
+
manifest: string;
|
|
25
|
+
range: string;
|
|
26
|
+
wanted: string | null;
|
|
27
|
+
newerWithinRange: boolean;
|
|
28
|
+
latestOutsideRange: boolean | null;
|
|
29
|
+
reason?: string;
|
|
30
|
+
}[];
|
|
31
|
+
nodeEngine?: string | null;
|
|
32
|
+
vulnerabilities: {
|
|
33
|
+
status: "available" | "unavailable";
|
|
34
|
+
reason?: string;
|
|
35
|
+
advisories?: {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
url: string;
|
|
39
|
+
severity: string;
|
|
40
|
+
range: string;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface DependencyMetadata {
|
|
45
|
+
version: 1;
|
|
46
|
+
status: "notRequested" | "available" | "partial" | "unavailable";
|
|
47
|
+
source: "https://registry.npmjs.org";
|
|
48
|
+
checkedAtUtc?: string;
|
|
49
|
+
scope: "public-registry-lockfile-packages";
|
|
50
|
+
observations: PackageObservation[];
|
|
51
|
+
availablePackages: number;
|
|
52
|
+
unavailablePackages: number;
|
|
53
|
+
advisoryResponseSha256?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare const noMetadata: () => DependencyMetadata;
|
|
56
|
+
/** Explicit opt-in, bounded public-registry reads. No credentials, redirects, installs or target scripts. */
|
|
57
|
+
export declare function publicRegistryReader(options?: {
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
maxBytes?: number;
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
}): RegistryReader;
|
|
62
|
+
export declare function dependencyMetadata(inventory: DependencyInventory, reader?: RegistryReader, options?: {
|
|
63
|
+
maxPackages?: number;
|
|
64
|
+
deadlineMs?: number;
|
|
65
|
+
}): Promise<DependencyMetadata>;
|