@thomasminh1995/depverdict 0.6.0-alpha.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/LICENSE +21 -0
- package/README.md +518 -0
- package/bin/depverdict.js +7 -0
- package/bin/upgradelens.js +7 -0
- package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
- package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
- package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
- package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
- package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
- package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
- package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
- package/docs/IA-04-CLI-Orchestration.md +235 -0
- package/docs/IA-05-Real-Provider-Validation.md +235 -0
- package/docs/IA-05-VinGrade-Validation.md +339 -0
- package/docs/MVP-01.md +48 -0
- package/docs/MVP-02-Architecture.md +536 -0
- package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
- package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
- package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
- package/docs/MVP-02-Knowledge-Manifest.md +279 -0
- package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
- package/docs/MVP-02-Knowledge-Store.md +87 -0
- package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
- package/docs/MVP-02-Research-Planning.md +196 -0
- package/docs/MVP-02-Source-Provenance.md +81 -0
- package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
- package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
- package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
- package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
- package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
- package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
- package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
- package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
- package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
- package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
- package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
- package/docs/VinGrade-MVP-02-Validation.md +480 -0
- package/docs/VinGrade-RC02-Live-Validation.md +273 -0
- package/docs/ai-capability-discovery.md +573 -0
- package/docs/ai-engineering-production-readiness.md +390 -0
- package/docs/ai-engineering-review.md +251 -0
- package/docs/ai-runtime-governance-discovery.md +754 -0
- package/docs/architecture-overview.md +78 -0
- package/docs/cli-progress.md +111 -0
- package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
- package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
- package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
- package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
- package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
- package/docs/gateway-runtime-discovery.md +614 -0
- package/docs/live-ai-validation.md +299 -0
- package/docs/migration-planning-qualification-resolution.md +99 -0
- package/docs/migrations/upgradelens-to-depverdict.md +97 -0
- package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
- package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
- package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
- package/docs/mvp-05-deterministic-context-runtime.md +127 -0
- package/docs/mvp-05-migration-checklist-contract.md +166 -0
- package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
- package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
- package/docs/mvp-05-provider-neutral-generator.md +79 -0
- package/docs/ollama-local-smoke-validation.md +156 -0
- package/docs/openai-compatible-runtime-discovery.md +789 -0
- package/docs/openrouter-one-dependency-validation.md +205 -0
- package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
- package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
- package/docs/package-content-policy.md +98 -0
- package/docs/releases/v0.5.0-technical-preview.md +114 -0
- package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
- package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
- package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
- package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
- package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
- package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
- package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
- package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
- package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
- package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
- package/docs/reviews/oss-03-community-scaffolding.md +378 -0
- package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
- package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
- package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
- package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
- package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
- package/docs/runtime-contract-discovery.md +521 -0
- package/docs/structured-output-compatibility-report.md +100 -0
- package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
- package/docs/version-analysis-architecture.md +827 -0
- package/eval/README.md +86 -0
- package/eval/datasets/generic/declared-constraint.json +59 -0
- package/eval/datasets/generic/evidence-conflict.json +73 -0
- package/eval/datasets/generic/major-breaking-release.json +66 -0
- package/eval/datasets/generic/missing-evidence.json +46 -0
- package/eval/datasets/generic/patch-release-low.json +59 -0
- package/eval/datasets/node/axios-patch-low.json +59 -0
- package/eval/datasets/node/react-major-breaking.json +66 -0
- package/eval/datasets/node/react-minor-compatibility.json +66 -0
- package/eval/datasets/python/fastapi-deprecation.json +66 -0
- package/eval/datasets/python/pydantic-major-breaking.json +66 -0
- package/eval/migration-planning/golden-dataset-v2.json +306 -0
- package/eval/migration-planning/golden-dataset.json +214 -0
- package/eval/schemas/expected-result.schema.json +88 -0
- package/eval/schemas/golden-case.schema.json +181 -0
- package/package.json +57 -0
- package/schemas/ai-scorecard.schema.json +132 -0
- package/schemas/benchmark-report.schema.json +189 -0
- package/schemas/benchmark.schema.json +55 -0
- package/schemas/capability-profile.schema.json +44 -0
- package/schemas/conformance-report.schema.json +150 -0
- package/schemas/deployment-profile.schema.json +64 -0
- package/schemas/evaluation-report.schema.json +158 -0
- package/schemas/knowledge-evidence-bundle.schema.json +150 -0
- package/schemas/knowledge-manifest.schema.json +548 -0
- package/schemas/metrics.schema.json +178 -0
- package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
- package/schemas/migration-checklist.schema.json +706 -0
- package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
- package/schemas/migration-evaluation-dataset.schema.json +204 -0
- package/schemas/migration-planning-qualification-record.schema.json +234 -0
- package/schemas/project-manifest.schema.json +308 -0
- package/schemas/qualification-record.schema.json +56 -0
- package/schemas/repository-impact-evidence.schema.json +232 -0
- package/schemas/repository-impact.schema.json +202 -0
- package/schemas/upgrade-decision.schema.json +273 -0
- package/schemas/usage-index.schema.json +179 -0
- package/schemas/version-analysis.schema.json +449 -0
- package/src/ai-runtime-debug.js +325 -0
- package/src/ai-runtime-error.js +42 -0
- package/src/ai-runtime.js +174 -0
- package/src/ai-scorecard.js +204 -0
- package/src/ai-version-analysis.js +484 -0
- package/src/artifact-root-compatibility.js +91 -0
- package/src/benchmark-report.js +111 -0
- package/src/benchmark-runner.js +191 -0
- package/src/canonical-json.js +69 -0
- package/src/cli.js +1299 -0
- package/src/conformance-report.js +158 -0
- package/src/conformance-runner.js +253 -0
- package/src/constants.js +73 -0
- package/src/cooperative-scheduler.js +79 -0
- package/src/dependencies.js +44 -0
- package/src/dependency-ai-context.js +625 -0
- package/src/detectors.js +253 -0
- package/src/discovery.js +234 -0
- package/src/ecosystem-version-adapter.js +294 -0
- package/src/environment-compatibility.js +77 -0
- package/src/evaluation-comparator.js +158 -0
- package/src/evaluation-report.js +76 -0
- package/src/evaluation-runner.js +248 -0
- package/src/evidence-source-adapter.js +472 -0
- package/src/files.js +89 -0
- package/src/governance-diagnostics.js +64 -0
- package/src/governance-loader.js +63 -0
- package/src/governance-metadata.js +346 -0
- package/src/governance-validator.js +360 -0
- package/src/http/bounded-fetch.js +278 -0
- package/src/http/cli-http-runtime.js +44 -0
- package/src/impact/input-loader.js +157 -0
- package/src/impact/matcher.js +40 -0
- package/src/impact/repository-impact.js +199 -0
- package/src/impact/runtime.js +24 -0
- package/src/impact/status.js +62 -0
- package/src/impact/writer.js +30 -0
- package/src/impact-evidence/input-loader.js +202 -0
- package/src/impact-evidence/repository-impact-evidence.js +234 -0
- package/src/impact-evidence/runtime.js +16 -0
- package/src/impact-evidence/writer.js +30 -0
- package/src/index.js +563 -0
- package/src/installed-version-baseline.js +196 -0
- package/src/knowledge-cache.js +324 -0
- package/src/knowledge-evidence-bundle.js +101 -0
- package/src/knowledge-evidence-producer.js +233 -0
- package/src/knowledge-manifest-builder.js +188 -0
- package/src/knowledge-manifest-writer.js +32 -0
- package/src/knowledge-manifest.js +255 -0
- package/src/knowledge-research.js +615 -0
- package/src/metrics-engine.js +205 -0
- package/src/migration-checklist/ai-candidate.js +320 -0
- package/src/migration-checklist/assembler.js +37 -0
- package/src/migration-checklist/context-runtime.js +828 -0
- package/src/migration-checklist/evaluation/action-criteria.js +244 -0
- package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
- package/src/migration-checklist/evaluation/comparator.js +279 -0
- package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
- package/src/migration-checklist/evaluation/dataset.js +336 -0
- package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
- package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
- package/src/migration-checklist/evaluation/metrics.js +158 -0
- package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
- package/src/migration-checklist/evaluation/qualification.js +239 -0
- package/src/migration-checklist/evaluation/runner-v2.js +294 -0
- package/src/migration-checklist/evaluation/runner.js +194 -0
- package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
- package/src/migration-checklist/evaluation/scorecard.js +86 -0
- package/src/migration-checklist/extractive-candidate.js +166 -0
- package/src/migration-checklist/extractive-prompt.js +62 -0
- package/src/migration-checklist/generator.js +702 -0
- package/src/migration-checklist/grounding-policy.js +117 -0
- package/src/migration-checklist/input-loader.js +613 -0
- package/src/migration-checklist/migration-checklist.js +635 -0
- package/src/migration-checklist/presentation.js +292 -0
- package/src/migration-checklist/progress.js +96 -0
- package/src/migration-checklist/prompt.js +83 -0
- package/src/migration-checklist/qualification-guard.js +462 -0
- package/src/migration-checklist/qualification-resolution.js +122 -0
- package/src/migration-checklist/qualification-store.js +225 -0
- package/src/migration-checklist/runtime.js +205 -0
- package/src/migration-checklist/verification.js +134 -0
- package/src/migration-checklist/writer.js +35 -0
- package/src/openai-compatible-provider.js +451 -0
- package/src/orchestration/failure-log.js +32 -0
- package/src/orchestration/pipeline.js +200 -0
- package/src/orchestration/progress-events.js +337 -0
- package/src/orchestration/progress-reporter.js +131 -0
- package/src/orchestration/text-writer.js +22 -0
- package/src/portable.js +13 -0
- package/src/product-completion.js +249 -0
- package/src/project-manifest-input.js +90 -0
- package/src/project-manifest.js +141 -0
- package/src/python-requirements.js +137 -0
- package/src/registry/npm-packument.js +256 -0
- package/src/registry/npm-registry-adapter.js +262 -0
- package/src/registry/pypi-project.js +300 -0
- package/src/registry/pypi-registry-adapter.js +235 -0
- package/src/registry/sanitize-registry-body.js +51 -0
- package/src/renderers/console.js +160 -0
- package/src/renderers/impact-presentation.js +278 -0
- package/src/renderers/markdown.js +172 -0
- package/src/research-plan.js +455 -0
- package/src/runtime-conformance.js +275 -0
- package/src/source-provenance.js +393 -0
- package/src/source-url.js +62 -0
- package/src/structured-output-schema.js +66 -0
- package/src/target-selector.js +306 -0
- package/src/upgrade-decision/input-loader.js +107 -0
- package/src/upgrade-decision/presentation.js +43 -0
- package/src/upgrade-decision/runtime.js +21 -0
- package/src/upgrade-decision/upgrade-decision.js +626 -0
- package/src/upgrade-decision/writer.js +30 -0
- package/src/usage/analyzer-registry.js +63 -0
- package/src/usage/coverage.js +116 -0
- package/src/usage/input-loader.js +139 -0
- package/src/usage/js/analyzer.js +240 -0
- package/src/usage/js/parser.js +21 -0
- package/src/usage/runtime.js +187 -0
- package/src/usage/scope.js +44 -0
- package/src/usage/source-files.js +50 -0
- package/src/usage/usage-index.js +217 -0
- package/src/usage/writer.js +31 -0
- package/src/version-analysis-loader.js +203 -0
- package/src/version-analysis-manifest.js +314 -0
- package/src/version-analysis-writer.js +30 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_MANIFEST_PATH, DEFAULT_VERSION_ANALYSIS_PATH } from '../constants.js';
|
|
5
|
+
import { createCooperativeScheduler } from '../cooperative-scheduler.js';
|
|
6
|
+
import { relativePath } from '../files.js';
|
|
7
|
+
import { createUsageAnalyzerRegistry } from './analyzer-registry.js';
|
|
8
|
+
import { buildProjectUsageCoverage } from './coverage.js';
|
|
9
|
+
import { loadUsageDiscoveryInputs } from './input-loader.js';
|
|
10
|
+
import { createJavaScriptUsageAnalyzer } from './js/analyzer.js';
|
|
11
|
+
import { createUsageDiscoveryScope } from './scope.js';
|
|
12
|
+
import { collectUsageSourceFiles } from './source-files.js';
|
|
13
|
+
import { buildUsageIndex } from './usage-index.js';
|
|
14
|
+
|
|
15
|
+
async function repositoryDirectory(inputPath) {
|
|
16
|
+
const resolved = path.resolve(inputPath);
|
|
17
|
+
let details;
|
|
18
|
+
try {
|
|
19
|
+
details = await stat(resolved);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`Cannot access repository root "${inputPath}": ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
if (!details.isDirectory()) throw new Error(`Repository root is not a directory: ${inputPath}`);
|
|
24
|
+
return realpath(resolved);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pathBelongsToProject(file, projectPath) {
|
|
28
|
+
if (projectPath === '.') return true;
|
|
29
|
+
return file === projectPath || file.startsWith(`${projectPath}/`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function owningProject(file, scopes, registry) {
|
|
33
|
+
const candidates = scopes.filter((scope) => (
|
|
34
|
+
pathBelongsToProject(file, scope.projectPath) && registry.find(scope.ecosystem, file)
|
|
35
|
+
));
|
|
36
|
+
return candidates.sort((left, right) => (
|
|
37
|
+
right.projectPath.split('/').length - left.projectPath.split('/').length
|
|
38
|
+
|| left.projectId.localeCompare(right.projectId)
|
|
39
|
+
))[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function coverageOwner(warningPath, projects) {
|
|
43
|
+
return projects
|
|
44
|
+
.filter((project) => pathBelongsToProject(warningPath, project.projectPath))
|
|
45
|
+
.sort((left, right) => (
|
|
46
|
+
right.projectPath.split('/').length - left.projectPath.split('/').length
|
|
47
|
+
|| left.projectId.localeCompare(right.projectId)
|
|
48
|
+
))[0] ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createDefaultUsageAnalyzerRegistry() {
|
|
52
|
+
return createUsageAnalyzerRegistry([createJavaScriptUsageAnalyzer()]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function discoverRepositoryUsage({
|
|
56
|
+
repositoryRoot,
|
|
57
|
+
projectManifest,
|
|
58
|
+
versionAnalysis,
|
|
59
|
+
input,
|
|
60
|
+
registry = createDefaultUsageAnalyzerRegistry(),
|
|
61
|
+
clock,
|
|
62
|
+
maxDepth,
|
|
63
|
+
signal,
|
|
64
|
+
cooperativeScheduler: injectedCooperativeScheduler,
|
|
65
|
+
cooperativeScheduling = true,
|
|
66
|
+
cooperativeBatchSize,
|
|
67
|
+
cooperativeMaxIntervalMs,
|
|
68
|
+
monotonicClock
|
|
69
|
+
}) {
|
|
70
|
+
const cooperativeScheduler = injectedCooperativeScheduler ?? createCooperativeScheduler({
|
|
71
|
+
signal,
|
|
72
|
+
enabled: cooperativeScheduling,
|
|
73
|
+
batchSize: cooperativeBatchSize,
|
|
74
|
+
maxIntervalMs: cooperativeMaxIntervalMs,
|
|
75
|
+
monotonicClock
|
|
76
|
+
});
|
|
77
|
+
cooperativeScheduler.checkpoint();
|
|
78
|
+
const root = await repositoryDirectory(repositoryRoot);
|
|
79
|
+
const scopes = createUsageDiscoveryScope(projectManifest, versionAnalysis);
|
|
80
|
+
const projectScopes = projectManifest.projects.map((project) => ({
|
|
81
|
+
projectId: project.id,
|
|
82
|
+
projectPath: project.path,
|
|
83
|
+
ecosystem: project.ecosystem
|
|
84
|
+
}));
|
|
85
|
+
const dependenciesByProject = new Map();
|
|
86
|
+
for (const scope of scopes) {
|
|
87
|
+
if (!dependenciesByProject.has(scope.projectId)) dependenciesByProject.set(scope.projectId, []);
|
|
88
|
+
dependenciesByProject.get(scope.projectId).push(scope);
|
|
89
|
+
await cooperativeScheduler.boundary();
|
|
90
|
+
}
|
|
91
|
+
const collected = await collectUsageSourceFiles(root, registry.extensions(), {
|
|
92
|
+
maxDepth,
|
|
93
|
+
cooperativeScheduler
|
|
94
|
+
});
|
|
95
|
+
const warnings = [...collected.warnings];
|
|
96
|
+
const usages = [];
|
|
97
|
+
let scannedFileCount = 0;
|
|
98
|
+
let analyzedFileCount = 0;
|
|
99
|
+
const coverageCounts = new Map(projectScopes.map((project) => [project.projectId, {
|
|
100
|
+
scannedFileCount: 0,
|
|
101
|
+
analyzedFileCount: 0,
|
|
102
|
+
parseFailureCount: 0,
|
|
103
|
+
analyzerFailureCount: 0,
|
|
104
|
+
unreadableFileCount: 0,
|
|
105
|
+
scanFailureCount: 0
|
|
106
|
+
}]));
|
|
107
|
+
for (const warning of collected.warnings) {
|
|
108
|
+
const owner = coverageOwner(warning.path, projectScopes);
|
|
109
|
+
if (owner) coverageCounts.get(owner.projectId).scanFailureCount += 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const absoluteFile of collected.files) {
|
|
113
|
+
try {
|
|
114
|
+
const file = relativePath(root, absoluteFile);
|
|
115
|
+
const project = owningProject(file, projectScopes, registry);
|
|
116
|
+
if (!project) continue;
|
|
117
|
+
if (!dependenciesByProject.has(project.projectId)) continue;
|
|
118
|
+
const analyzer = registry.find(project.ecosystem, file);
|
|
119
|
+
if (!analyzer) continue;
|
|
120
|
+
scannedFileCount += 1;
|
|
121
|
+
const counts = coverageCounts.get(project.projectId);
|
|
122
|
+
counts.scannedFileCount += 1;
|
|
123
|
+
let source;
|
|
124
|
+
try {
|
|
125
|
+
source = await readFile(absoluteFile, 'utf8');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
warnings.push({
|
|
128
|
+
code: 'FILE_UNREADABLE',
|
|
129
|
+
path: file,
|
|
130
|
+
message: `Unable to read source file (${error.code ?? 'unknown error'}).`
|
|
131
|
+
});
|
|
132
|
+
counts.unreadableFileCount += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const discovered = await analyzer.analyze({
|
|
137
|
+
source,
|
|
138
|
+
file,
|
|
139
|
+
projectId: project.projectId,
|
|
140
|
+
dependencies: dependenciesByProject.get(project.projectId) ?? []
|
|
141
|
+
});
|
|
142
|
+
usages.push(...discovered.map((usage) => ({ ...usage, projectId: project.projectId })));
|
|
143
|
+
analyzedFileCount += 1;
|
|
144
|
+
counts.analyzedFileCount += 1;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
warnings.push({
|
|
147
|
+
code: error instanceof SyntaxError ? 'SOURCE_PARSE_FAILED' : 'ANALYZER_FAILED',
|
|
148
|
+
path: file,
|
|
149
|
+
message: error instanceof SyntaxError
|
|
150
|
+
? 'Unable to parse source file.'
|
|
151
|
+
: `Analyzer failed (${error.code ?? error.name ?? 'unknown error'}).`
|
|
152
|
+
});
|
|
153
|
+
if (error instanceof SyntaxError) counts.parseFailureCount += 1;
|
|
154
|
+
else counts.analyzerFailureCount += 1;
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
await cooperativeScheduler.boundary();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
cooperativeScheduler.checkpoint();
|
|
162
|
+
const coverage = projectScopes.map((project) => buildProjectUsageCoverage({
|
|
163
|
+
...project,
|
|
164
|
+
analyzers: registry.forEcosystem(project.ecosystem),
|
|
165
|
+
...coverageCounts.get(project.projectId)
|
|
166
|
+
}));
|
|
167
|
+
return buildUsageIndex({
|
|
168
|
+
input,
|
|
169
|
+
usages,
|
|
170
|
+
scannedFileCount,
|
|
171
|
+
analyzedFileCount,
|
|
172
|
+
analyzers: registry.analyzers(),
|
|
173
|
+
coverage,
|
|
174
|
+
warnings,
|
|
175
|
+
generatedAt: clock?.() ?? new Date()
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runUsageDiscovery({ repositoryRoot = '.', sources, ...options } = {}) {
|
|
180
|
+
const root = path.resolve(repositoryRoot);
|
|
181
|
+
const effectiveSources = sources ?? {
|
|
182
|
+
projectManifest: path.join(root, DEFAULT_MANIFEST_PATH),
|
|
183
|
+
versionAnalysis: path.join(root, DEFAULT_VERSION_ANALYSIS_PATH)
|
|
184
|
+
};
|
|
185
|
+
const artifacts = await loadUsageDiscoveryInputs(effectiveSources, options);
|
|
186
|
+
return discoverRepositoryUsage({ repositoryRoot, ...artifacts, ...options });
|
|
187
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { compareText } from '../portable.js';
|
|
2
|
+
|
|
3
|
+
function packageNameFromId(packageId) {
|
|
4
|
+
const separator = packageId.indexOf(':');
|
|
5
|
+
return separator === -1 ? packageId : packageId.slice(separator + 1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createUsageDiscoveryScope(projectManifest, versionAnalysisManifest) {
|
|
9
|
+
const projects = new Map(projectManifest.projects.map((project) => [project.id, project]));
|
|
10
|
+
const scoped = new Map();
|
|
11
|
+
|
|
12
|
+
for (const result of versionAnalysisManifest.results) {
|
|
13
|
+
const project = projects.get(result.dependency.projectId);
|
|
14
|
+
if (!project) {
|
|
15
|
+
throw new Error(`Usage Discovery input error: unknown project ${result.dependency.projectId}.`);
|
|
16
|
+
}
|
|
17
|
+
if (project.ecosystem !== result.dependency.ecosystem) {
|
|
18
|
+
throw new Error(`Usage Discovery input error: ecosystem mismatch for ${project.id}.`);
|
|
19
|
+
}
|
|
20
|
+
const declared = project.dependencies.find((dependency) => (
|
|
21
|
+
dependency.normalizedName === result.dependency.normalizedName
|
|
22
|
+
&& dependency.type === result.dependency.dependencyType
|
|
23
|
+
&& dependency.manifest === result.dependency.manifest
|
|
24
|
+
));
|
|
25
|
+
if (!declared) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Usage Discovery input error: ${result.dependency.packageId} is not declared by ${project.id}.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const key = `${project.id}\0${result.dependency.packageId}`;
|
|
31
|
+
scoped.set(key, {
|
|
32
|
+
projectId: project.id,
|
|
33
|
+
projectPath: project.path,
|
|
34
|
+
ecosystem: project.ecosystem,
|
|
35
|
+
packageId: result.dependency.packageId,
|
|
36
|
+
name: declared.name || packageNameFromId(result.dependency.packageId),
|
|
37
|
+
normalizedName: declared.normalizedName
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return [...scoped.values()].sort((left, right) => (
|
|
42
|
+
compareText(left.projectId, right.projectId) || compareText(left.packageId, right.packageId)
|
|
43
|
+
));
|
|
44
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES } from '../constants.js';
|
|
5
|
+
import { relativePath } from '../files.js';
|
|
6
|
+
|
|
7
|
+
export async function collectUsageSourceFiles(root, extensions, options = {}) {
|
|
8
|
+
const supportedExtensions = new Set(extensions.map((extension) => extension.toLowerCase()));
|
|
9
|
+
const maxDepth = options.maxDepth ?? Number.POSITIVE_INFINITY;
|
|
10
|
+
const cooperativeScheduler = options.cooperativeScheduler;
|
|
11
|
+
const files = [];
|
|
12
|
+
const warnings = [];
|
|
13
|
+
|
|
14
|
+
async function visit(directory, depth) {
|
|
15
|
+
cooperativeScheduler?.checkpoint();
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
warnings.push({
|
|
21
|
+
code: 'DIRECTORY_UNREADABLE',
|
|
22
|
+
path: relativePath(root, directory),
|
|
23
|
+
message: `Unable to read directory (${error.code ?? 'unknown error'}).`
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const entryPath = path.join(directory, entry.name);
|
|
31
|
+
try {
|
|
32
|
+
if (entry.isSymbolicLink()) continue;
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (depth < maxDepth && !DEFAULT_IGNORED_DIRECTORIES.has(entry.name)) {
|
|
35
|
+
await visit(entryPath, depth + 1);
|
|
36
|
+
}
|
|
37
|
+
} else if (entry.isFile() && supportedExtensions.has(path.extname(entry.name).toLowerCase())) {
|
|
38
|
+
files.push(entryPath);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
await cooperativeScheduler?.boundary();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await visit(root, 0);
|
|
47
|
+
return { files, warnings };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const collectRepositorySourceFiles = collectUsageSourceFiles;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ARTIFACT_GENERATOR_NAME,
|
|
8
|
+
USAGE_INDEX_SCHEMA_VERSION,
|
|
9
|
+
VERSION
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { compareText, isSorted } from '../portable.js';
|
|
12
|
+
import {
|
|
13
|
+
USAGE_COVERAGE_REASON_CODES,
|
|
14
|
+
USAGE_COVERAGE_STATUSES,
|
|
15
|
+
compareCoverage
|
|
16
|
+
} from './coverage.js';
|
|
17
|
+
|
|
18
|
+
const schema = JSON.parse(await readFile(
|
|
19
|
+
new URL('../../schemas/usage-index.schema.json', import.meta.url),
|
|
20
|
+
'utf8'
|
|
21
|
+
));
|
|
22
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
23
|
+
addFormats(ajv);
|
|
24
|
+
const validateSchema = ajv.compile(schema);
|
|
25
|
+
|
|
26
|
+
const compareDependencies = (left, right) => (
|
|
27
|
+
compareText(left.projectId, right.projectId) || compareText(left.packageId, right.packageId)
|
|
28
|
+
);
|
|
29
|
+
const compareSymbols = (left, right) => compareText(left.name, right.name);
|
|
30
|
+
const compareWarnings = (left, right) => (
|
|
31
|
+
compareText(left.path, right.path) || compareText(left.code, right.code) || compareText(left.message, right.message)
|
|
32
|
+
);
|
|
33
|
+
const compareAnalyzers = (left, right) => compareText(left.id, right.id) || compareText(left.version, right.version);
|
|
34
|
+
|
|
35
|
+
function indexUsages(usages) {
|
|
36
|
+
const dependencies = new Map();
|
|
37
|
+
for (const usage of usages) {
|
|
38
|
+
const dependencyKey = `${usage.projectId}\0${usage.packageId}`;
|
|
39
|
+
if (!dependencies.has(dependencyKey)) {
|
|
40
|
+
dependencies.set(dependencyKey, {
|
|
41
|
+
projectId: usage.projectId,
|
|
42
|
+
packageId: usage.packageId,
|
|
43
|
+
name: usage.dependency,
|
|
44
|
+
files: new Set(),
|
|
45
|
+
symbols: new Map()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const dependency = dependencies.get(dependencyKey);
|
|
49
|
+
dependency.files.add(usage.file);
|
|
50
|
+
if (usage.symbol === null) continue;
|
|
51
|
+
if (!dependency.symbols.has(usage.symbol)) dependency.symbols.set(usage.symbol, new Set());
|
|
52
|
+
dependency.symbols.get(usage.symbol).add(usage.file);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [...dependencies.values()].map((dependency) => ({
|
|
56
|
+
projectId: dependency.projectId,
|
|
57
|
+
packageId: dependency.packageId,
|
|
58
|
+
name: dependency.name,
|
|
59
|
+
files: [...dependency.files].sort(compareText),
|
|
60
|
+
symbols: [...dependency.symbols].map(([name, files]) => ({
|
|
61
|
+
name,
|
|
62
|
+
files: [...files].sort(compareText)
|
|
63
|
+
})).sort(compareSymbols)
|
|
64
|
+
})).sort(compareDependencies);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function summary(dependencies, warningCount) {
|
|
68
|
+
const files = new Set(dependencies.flatMap((dependency) => dependency.files));
|
|
69
|
+
return {
|
|
70
|
+
dependencyCount: dependencies.length,
|
|
71
|
+
symbolCount: dependencies.reduce((count, dependency) => count + dependency.symbols.length, 0),
|
|
72
|
+
fileCount: files.size,
|
|
73
|
+
warningCount
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function validateUsageIndexInvariants(index) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
if (!isSorted(index.dependencies, compareDependencies)) errors.push('dependencies must be sorted.');
|
|
80
|
+
if (!isSorted(index.analysis.analyzers, compareAnalyzers)) errors.push('analyzers must be sorted.');
|
|
81
|
+
if (index.analysis.coverage && !isSorted(index.analysis.coverage, compareCoverage)) {
|
|
82
|
+
errors.push('analysis.coverage must be sorted.');
|
|
83
|
+
}
|
|
84
|
+
if (new Set(index.analysis.analyzers.map((analyzer) => analyzer.id)).size !== index.analysis.analyzers.length) {
|
|
85
|
+
errors.push('analyzer ids must be unique.');
|
|
86
|
+
}
|
|
87
|
+
if (index.analysis.analyzedFileCount > index.analysis.scannedFileCount) {
|
|
88
|
+
errors.push('analysis.analyzedFileCount cannot exceed scannedFileCount.');
|
|
89
|
+
}
|
|
90
|
+
const coverageProjects = new Set();
|
|
91
|
+
let coverageScannedFileCount = 0;
|
|
92
|
+
let coverageAnalyzedFileCount = 0;
|
|
93
|
+
for (const coverage of index.analysis.coverage ?? []) {
|
|
94
|
+
if (coverageProjects.has(coverage.projectId)) {
|
|
95
|
+
errors.push(`duplicate coverage project ${coverage.projectId}.`);
|
|
96
|
+
}
|
|
97
|
+
coverageProjects.add(coverage.projectId);
|
|
98
|
+
if (!USAGE_COVERAGE_STATUSES.includes(coverage.status)) {
|
|
99
|
+
errors.push(`coverage ${coverage.projectId} has unsupported status.`);
|
|
100
|
+
}
|
|
101
|
+
if (!USAGE_COVERAGE_REASON_CODES.includes(coverage.reasonCode)) {
|
|
102
|
+
errors.push(`coverage ${coverage.projectId} has unsupported reasonCode.`);
|
|
103
|
+
}
|
|
104
|
+
if (coverage.analyzedFileCount > coverage.scannedFileCount) {
|
|
105
|
+
errors.push(`coverage ${coverage.projectId} analyzedFileCount cannot exceed scannedFileCount.`);
|
|
106
|
+
}
|
|
107
|
+
coverageScannedFileCount += coverage.scannedFileCount;
|
|
108
|
+
coverageAnalyzedFileCount += coverage.analyzedFileCount;
|
|
109
|
+
const processedFileCount = coverage.analyzedFileCount + coverage.parseFailureCount
|
|
110
|
+
+ coverage.analyzerFailureCount + coverage.unreadableFileCount;
|
|
111
|
+
if (processedFileCount !== coverage.scannedFileCount) {
|
|
112
|
+
errors.push(`coverage ${coverage.projectId} file counts are inconsistent.`);
|
|
113
|
+
}
|
|
114
|
+
if ((coverage.status === 'unavailable') !== (coverage.analyzer === null)) {
|
|
115
|
+
errors.push(`coverage ${coverage.projectId} analyzer availability is inconsistent.`);
|
|
116
|
+
}
|
|
117
|
+
const failures = coverage.parseFailureCount + coverage.analyzerFailureCount
|
|
118
|
+
+ coverage.unreadableFileCount + coverage.scanFailureCount;
|
|
119
|
+
if (coverage.status === 'complete' && failures !== 0) {
|
|
120
|
+
errors.push(`coverage ${coverage.projectId} complete status has failures.`);
|
|
121
|
+
}
|
|
122
|
+
if (coverage.status === 'complete' && coverage.reasonCode !== 'COVERAGE_COMPLETE') {
|
|
123
|
+
errors.push(`coverage ${coverage.projectId} complete reasonCode is inconsistent.`);
|
|
124
|
+
}
|
|
125
|
+
if (coverage.analyzer
|
|
126
|
+
&& !index.analysis.analyzers.some((analyzer) => (
|
|
127
|
+
analyzer.id === coverage.analyzer.id && analyzer.version === coverage.analyzer.version
|
|
128
|
+
))) {
|
|
129
|
+
errors.push(`coverage ${coverage.projectId} analyzer is not declared by analysis.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ((index.analysis.coverage?.length ?? 0) > 0) {
|
|
133
|
+
if (coverageScannedFileCount !== index.analysis.scannedFileCount) {
|
|
134
|
+
errors.push('analysis.scannedFileCount does not equal project coverage total.');
|
|
135
|
+
}
|
|
136
|
+
if (coverageAnalyzedFileCount !== index.analysis.analyzedFileCount) {
|
|
137
|
+
errors.push('analysis.analyzedFileCount does not equal project coverage total.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!isSorted(index.warnings, compareWarnings)) errors.push('warnings must be sorted.');
|
|
141
|
+
const identities = new Set();
|
|
142
|
+
for (const dependency of index.dependencies) {
|
|
143
|
+
const identity = `${dependency.projectId}\0${dependency.packageId}`;
|
|
144
|
+
if (identities.has(identity)) errors.push(`duplicate dependency ${dependency.packageId} in ${dependency.projectId}.`);
|
|
145
|
+
identities.add(identity);
|
|
146
|
+
if (!isSorted(dependency.files, compareText)) errors.push(`files for ${dependency.packageId} must be sorted.`);
|
|
147
|
+
if (new Set(dependency.files).size !== dependency.files.length) {
|
|
148
|
+
errors.push(`files for ${dependency.packageId} must be unique.`);
|
|
149
|
+
}
|
|
150
|
+
if (!isSorted(dependency.symbols, compareSymbols)) {
|
|
151
|
+
errors.push(`symbols for ${dependency.packageId} must be sorted.`);
|
|
152
|
+
}
|
|
153
|
+
if (new Set(dependency.symbols.map((symbol) => symbol.name)).size !== dependency.symbols.length) {
|
|
154
|
+
errors.push(`symbols for ${dependency.packageId} must be unique.`);
|
|
155
|
+
}
|
|
156
|
+
const dependencyFiles = new Set(dependency.files);
|
|
157
|
+
for (const symbol of dependency.symbols) {
|
|
158
|
+
if (!isSorted(symbol.files, compareText)) {
|
|
159
|
+
errors.push(`files for ${dependency.packageId}/${symbol.name} must be sorted.`);
|
|
160
|
+
}
|
|
161
|
+
if (new Set(symbol.files).size !== symbol.files.length) {
|
|
162
|
+
errors.push(`files for ${dependency.packageId}/${symbol.name} must be unique.`);
|
|
163
|
+
}
|
|
164
|
+
for (const file of symbol.files) {
|
|
165
|
+
if (!dependencyFiles.has(file)) {
|
|
166
|
+
errors.push(`file ${file} for ${dependency.packageId}/${symbol.name} is missing from dependency files.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const expected = summary(index.dependencies, index.warnings.length);
|
|
172
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
173
|
+
if (index.summary[field] !== value) {
|
|
174
|
+
errors.push(`summary.${field} is ${index.summary[field]}; expected ${value}.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return errors.sort(compareText);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function validateUsageIndex(index) {
|
|
181
|
+
if (!validateSchema(index)) {
|
|
182
|
+
throw new Error(`Usage Index validation error: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
183
|
+
}
|
|
184
|
+
const errors = validateUsageIndexInvariants(index);
|
|
185
|
+
if (errors.length > 0) throw new Error(`Usage Index invariant error: ${errors.join(' ')}`);
|
|
186
|
+
return index;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildUsageIndex({
|
|
190
|
+
input,
|
|
191
|
+
usages,
|
|
192
|
+
scannedFileCount,
|
|
193
|
+
analyzedFileCount,
|
|
194
|
+
analyzers,
|
|
195
|
+
coverage = [],
|
|
196
|
+
warnings,
|
|
197
|
+
generatedAt = new Date()
|
|
198
|
+
}) {
|
|
199
|
+
const dependencies = indexUsages(usages);
|
|
200
|
+
const sortedWarnings = structuredClone(warnings).sort(compareWarnings);
|
|
201
|
+
const sortedAnalyzers = analyzers.map(({ id, version }) => ({ id, version })).sort(compareAnalyzers);
|
|
202
|
+
return validateUsageIndex({
|
|
203
|
+
schemaVersion: USAGE_INDEX_SCHEMA_VERSION,
|
|
204
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
205
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
206
|
+
input: structuredClone(input),
|
|
207
|
+
analysis: {
|
|
208
|
+
analyzers: sortedAnalyzers,
|
|
209
|
+
scannedFileCount,
|
|
210
|
+
analyzedFileCount,
|
|
211
|
+
coverage: structuredClone(coverage).sort(compareCoverage)
|
|
212
|
+
},
|
|
213
|
+
summary: summary(dependencies, sortedWarnings.length),
|
|
214
|
+
dependencies,
|
|
215
|
+
warnings: sortedWarnings
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateUsageIndex } from './usage-index.js';
|
|
5
|
+
|
|
6
|
+
export function serializeUsageIndex(index) {
|
|
7
|
+
validateUsageIndex(index);
|
|
8
|
+
return `${JSON.stringify(index, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function writeUsageIndex(outputPath, index) {
|
|
12
|
+
const target = path.resolve(outputPath);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
const contents = serializeUsageIndex(index);
|
|
15
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
16
|
+
let handle;
|
|
17
|
+
try {
|
|
18
|
+
handle = await open(temporary, 'w', 0o600);
|
|
19
|
+
await handle.writeFile(contents, 'utf8');
|
|
20
|
+
await handle.sync();
|
|
21
|
+
await handle.close();
|
|
22
|
+
handle = null;
|
|
23
|
+
await rename(temporary, target);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
await handle?.close().catch(() => {});
|
|
26
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
|