Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/adapters/benchmark-manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
} from '../domain/benchmark.js'
import { InvalidUsageError, StorageFailureError } from '../domain/errors.js'

/** Default benchmark manifest location used by the benchmark CLI flow. */
export const DEFAULT_BENCHMARK_MANIFEST_PATH = resolve(
process.cwd(),
'.internal/benchmarks/benchmark-manifest.json',
)

/** JSON-backed benchmark manifest loader. */
export class JsonBenchmarkManifestLoader implements BenchmarkManifestLoader {
constructor(private readonly manifestPath = DEFAULT_BENCHMARK_MANIFEST_PATH) {}

Expand Down
1 change: 1 addition & 0 deletions src/adapters/heuristic-risk-scorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
const MATURE_PACKAGE_VERSION_THRESHOLD = 100
const MATURE_PACKAGE_DOWNLOAD_THRESHOLD = 100_000

/** Default additive heuristic scorer for package metadata signals. */
export class HeuristicRiskScorer implements RiskScorer {
constructor(private readonly now: () => Date = () => new Date()) {}

Expand Down
7 changes: 7 additions & 0 deletions src/adapters/jsonl-scan-review-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface JsonlScanReviewStorePaths {
reviewEventsPath: string
}

/** JSONL-backed implementation of the scan review store. */
export class JsonlScanReviewStore implements ScanReviewStore {
constructor(private readonly paths: JsonlScanReviewStorePaths) {}

Expand Down Expand Up @@ -88,6 +89,12 @@ export class JsonlScanReviewStore implements ScanReviewStore {
}
}

/**
* Returns the default repo-local JSONL persistence paths.
*
* @param workingDirectory Working directory used to anchor `.depgraph`.
* @returns Paths for scan and review history files.
*/
export function defaultScanReviewStorePaths(workingDirectory: string): JsonlScanReviewStorePaths {
// Keep append-only history repo-local and inspectable instead of hiding mutable state in a user-global cache.
return {
Expand Down
10 changes: 10 additions & 0 deletions src/adapters/lockfile-dependency-traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from '../domain/ports.js'
import { packageKey } from '../domain/value-objects.js'

/** Normalized dependency entry extracted from a lockfile snapshot. */
export interface NormalizedLockfileEntry {
entry_id: string
name: string
Expand All @@ -16,6 +17,7 @@ export interface NormalizedLockfileEntry {
integrity: string | null
}

/** Shared lockfile traversal interface consumed by lockfile adapters. */
export interface NormalizedLockfileProject {
root_package: PackageMetadata['package']
root_dependencies: Record<string, string>
Expand All @@ -38,6 +40,14 @@ interface QueueItem {
// converts the root into an explicit project-root PackageNode with nullable package metadata.
const SYNTHETIC_ROOT_PUBLISHED_AT = '1970-01-01T00:00:00.000Z'

/**
* Traverses a normalized lockfile project into the shared traversed-graph shape.
*
* @param project Normalized lockfile project.
* @param metadataSource Metadata source for exact package enrichment.
* @param maxDepth Maximum dependency depth to traverse.
* @returns Traversed dependency graph.
*/
export async function traverseNormalizedLockfileProject(
project: NormalizedLockfileProject,
metadataSource: PackageMetadataSource,
Expand Down
1 change: 1 addition & 0 deletions src/adapters/npm-package-metadata-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface WeeklyDownloadsResult {
lookup_failed: boolean
}

/** npm-registry-backed package metadata source. */
export class NpmPackageMetadataSource implements PackageMetadataSource {
constructor(private readonly fetcher: typeof fetch = fetch) {}

Expand Down
1 change: 1 addition & 0 deletions src/adapters/package-lock-dependency-traverser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface IndexedPackageEntry {
integrity: string | null
}

/** `package-lock.json` traverser that projects resolved dependencies into the shared graph shape. */
export class PackageLockDependencyTraverser implements PackageLockDependencyTraverserPort {
constructor(private readonly metadataSource: PackageMetadataSource) {}

Expand Down
7 changes: 7 additions & 0 deletions src/adapters/parsers/pnpm-lockfile-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ type PnpmDependencyReference =
specifier?: string
}

/**
* Parses a pnpm lockfile into the normalized project traversal interface.
*
* @param pnpmLockPath Path to `pnpm-lock.yaml`.
* @param projectRoot Project root whose importer should be scanned.
* @returns Normalized lockfile project.
*/
export function parsePnpmLockfile(
pnpmLockPath: string,
projectRoot: string,
Expand Down
1 change: 1 addition & 0 deletions src/adapters/pnpm-lock-dependency-traverser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import { parsePnpmLockfile } from './parsers/pnpm-lockfile-parser.js'
import { traverseNormalizedLockfileProject } from './lockfile-dependency-traversal.js'

/** `pnpm-lock.yaml` traverser that projects importer dependencies into the shared graph shape. */
export class PnpmLockDependencyTraverser implements PnpmLockDependencyTraverserPort {
constructor(private readonly metadataSource: PackageMetadataSource) {}

Expand Down
16 changes: 16 additions & 0 deletions src/adapters/project-scan-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import { dirname, join, resolve } from 'node:path'

import { InvalidUsageError } from '../domain/errors.js'

/** Resolved project scan request for `package-lock.json`. */
export interface PackageLockScanResolution {
scan_mode: 'package_lock'
project_root: string
package_lock_path: string
}

/** Resolved project scan request for `pnpm-lock.yaml`. */
export interface PnpmLockScanResolution {
scan_mode: 'pnpm_lock'
project_root: string
pnpm_lock_path: string
}

/** Any supported resolved project scan request. */
export type ProjectScanResolution = PackageLockScanResolution | PnpmLockScanResolution

/** Filesystem-based resolver for local project scan inputs. */
export class NodeProjectScanResolver {
async resolve(projectPath: string): Promise<ProjectScanResolution> {
const projectRoot = resolve(projectPath)
Expand Down Expand Up @@ -55,6 +59,12 @@ export class NodeProjectScanResolver {
}
}

/**
* Creates an explicit package-lock scan resolution from a file path.
*
* @param packageLockPath Path to `package-lock.json`.
* @returns Resolved package-lock scan input.
*/
export function resolvePackageLockScan(packageLockPath: string): PackageLockScanResolution {
const resolvedPackageLockPath = resolve(packageLockPath)

Expand All @@ -65,6 +75,12 @@ export function resolvePackageLockScan(packageLockPath: string): PackageLockScan
}
}

/**
* Creates an explicit pnpm-lock scan resolution from a file path.
*
* @param pnpmLockPath Path to `pnpm-lock.yaml`.
* @returns Resolved pnpm-lock scan input.
*/
export function resolvePnpmLockScan(pnpmLockPath: string): PnpmLockScanResolution {
const resolvedPnpmLockPath = resolve(pnpmLockPath)

Expand Down
1 change: 1 addition & 0 deletions src/adapters/registry-dependency-traverser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface QueueItem {
path_packages: TraversedPackageNode['path']['packages']
}

/** Breadth-first registry dependency traverser using resolved package metadata. */
export class RegistryDependencyTraverser implements RegistryDependencyTraverserPort {
constructor(private readonly metadataSource: PackageMetadataSource) {}

Expand Down
1 change: 1 addition & 0 deletions src/adapters/scan-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resolve } from 'node:path'
import type { BenchmarkScanRunner } from '../domain/benchmark.js'
import type { ScanResult } from '../domain/entities.js'

/** Benchmark scan runner that shells out to the built CLI. */
export class CliBenchmarkScanRunner implements BenchmarkScanRunner {
constructor(
private readonly cliEntryPoint = resolve(process.cwd(), 'dist/cli/index.js'),
Expand Down
27 changes: 27 additions & 0 deletions src/application/evaluate-benchmark-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import type {
} from '../domain/benchmark.js'
import type { ScanResult } from '../domain/entities.js'

/** Dependencies required to evaluate one benchmark case. */
export interface EvaluateBenchmarkCaseDependencies {
scanRunner: BenchmarkScanRunner
}

/**
* Evaluates one benchmark case by running a scan and comparing expected outcomes.
*
* @param benchmarkCase Benchmark case to evaluate.
* @param dependencies Runtime dependency for executing scans.
* @returns Benchmark result describing pass, fail, or skipped status.
*/
export async function evaluateBenchmarkCase(
benchmarkCase: BenchmarkCase,
dependencies: EvaluateBenchmarkCaseDependencies,
Expand Down Expand Up @@ -77,6 +85,12 @@ export async function evaluateBenchmarkCase(
}
}

/**
* Maps a scan result to the benchmark priority vocabulary.
*
* @param scanResult Completed scan result.
* @returns Expected-priority equivalent for benchmark assertions.
*/
export function mapPriorityFromScan(scanResult: ScanResult): ExpectedPriority {
if (scanResult.root.risk_score >= scanResult.threshold) {
return 'high_priority_review'
Expand All @@ -89,6 +103,12 @@ export function mapPriorityFromScan(scanResult: ScanResult): ExpectedPriority {
return 'safe'
}

/**
* Extracts stable actual signal identifiers from a scan result.
*
* @param scanResult Completed scan result.
* @returns Sorted unique signal identifiers from root signals and warnings.
*/
export function extractActualSignals(scanResult: ScanResult): string[] {
const signals = new Set<string>()

Expand All @@ -103,6 +123,13 @@ export function extractActualSignals(scanResult: ScanResult): string[] {
return [...signals].sort((left, right) => left.localeCompare(right))
}

/**
* Computes which expected benchmark signals were missing from the actual scan output.
*
* @param expectedSignals Expected signal identifiers from the benchmark case.
* @param actualSignals Actual signal identifiers extracted from a scan result.
* @returns Missing expected signals.
*/
export function findMissingSignals(expectedSignals: string[], actualSignals: string[]): string[] {
const actualSignalSet = new Set(actualSignals)

Expand Down
6 changes: 6 additions & 0 deletions src/application/evaluate-scans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ interface EvaluateScansDependencies {
resolveReviewStateIndex: () => Promise<ReadonlyMap<string, ResolvedReviewTargetState>>
}

/**
* Creates the evaluation use case for persisted scans and review history.
*
* @param dependencies Runtime dependencies for scan records, review events, and resolved review state.
* @returns Use case that aggregates evaluation summary output.
*/
export function createEvaluateScansUseCase({
scanRecordSource,
rawReviewEventSource,
Expand Down
13 changes: 11 additions & 2 deletions src/application/evaluation-readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import type {
SignalFrequency,
} from '../domain/contracts.js'
import type { PackageNode, RiskSignal } from '../domain/entities.js'
import {
getPackageNodeMetadataFieldState,
isObservedMetadataField,
} from '../domain/metadata-field-state.js'
import { isSecurityRelatedDeprecation } from '../domain/security-deprecation.js'

// Export-readiness exclusions are single-reason buckets. This precedence keeps
Expand Down Expand Up @@ -200,6 +204,8 @@ export function buildEvaluationDatasetSummary(scanRecords: ScanReviewRecord[]):
// they are structural roots rather than real published packages.
for (const node of flattenMetadataNodes(record.root)) {
totalNodes += 1
const dependentsCountState = getPackageNodeMetadataFieldState(node, 'dependents_count')
const advisoriesState = getPackageNodeMetadataFieldState(node, 'has_advisories')

if (node.weekly_downloads === null) {
nodesMissingWeeklyDownloads += 1
Expand All @@ -208,15 +214,18 @@ export function buildEvaluationDatasetSummary(scanRecords: ScanReviewRecord[]):
collectSignals(node.signals, knownDownloadsSignalCounts)
}

if (node.dependents_count === null) {
if (!isObservedMetadataField(dependentsCountState)) {
nodesMissingDependentsCount += 1
dependentsCountUnavailableCount += 1
collectSignals(node.signals, missingDependentsSignalCounts)
} else {
collectSignals(node.signals, knownDependentsSignalCounts)
}

if (node.has_advisories === false) {
if (
advisoriesState.observation === 'unavailable'
&& advisoriesState.reason === 'not_collected_yet'
) {
hasAdvisoriesPlaceholderCount += 1
}

Expand Down
7 changes: 7 additions & 0 deletions src/application/resolve-review-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export function buildResolvedReviewStateIndex(
return resolvedStates
}

/**
* Returns the resolved review state for a target, defaulting to an unreviewed state.
*
* @param reviewTarget Review target to resolve.
* @param resolvedReviewStateIndex Precomputed resolved review-state index.
* @returns Resolved review state for the target.
*/
export function getResolvedReviewState(
reviewTarget: ReviewTarget,
resolvedReviewStateIndex: ReadonlyMap<string, ResolvedReviewTargetState>,
Expand Down
6 changes: 6 additions & 0 deletions src/application/review-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ interface ReviewScanDependencies {
now?: () => Date
}

/**
* Creates the review append use case for stored scan records.
*
* @param dependencies Runtime dependencies for persistence and time.
* @returns Use case that appends a review event.
*/
export function createReviewScanUseCase({
reviewStore,
now = () => new Date(),
Expand Down
7 changes: 7 additions & 0 deletions src/application/run-benchmark-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import type {
} from '../domain/benchmark.js'
import { evaluateBenchmarkCase } from './evaluate-benchmark-case.js'

/** Dependencies required to run the benchmark suite. */
export interface RunBenchmarkSuiteDependencies {
manifestLoader: BenchmarkManifestLoader
scanRunner: BenchmarkScanRunner
}

/**
* Runs the full benchmark suite and summarizes the results.
*
* @param dependencies Runtime dependencies for loading cases and executing scans.
* @returns Benchmark suite result with per-case output and aggregate counts.
*/
export async function runBenchmarkSuite(
dependencies: RunBenchmarkSuiteDependencies,
): Promise<BenchmarkSuiteResult> {
Expand Down
Loading
Loading