@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,199 @@
|
|
|
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
|
+
REPOSITORY_IMPACT_SCHEMA_VERSION,
|
|
9
|
+
VERSION
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { compareText, isSorted } from '../portable.js';
|
|
12
|
+
import { coverageForProject } from '../usage/coverage.js';
|
|
13
|
+
import {
|
|
14
|
+
REPOSITORY_IMPACT_REASON_CODES,
|
|
15
|
+
REPOSITORY_IMPACT_STATUSES,
|
|
16
|
+
classifyDependencyImpact,
|
|
17
|
+
classifyFindingImpact
|
|
18
|
+
} from './status.js';
|
|
19
|
+
|
|
20
|
+
const schema = JSON.parse(await readFile(
|
|
21
|
+
new URL('../../schemas/repository-impact.schema.json', import.meta.url),
|
|
22
|
+
'utf8'
|
|
23
|
+
));
|
|
24
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
25
|
+
addFormats(ajv);
|
|
26
|
+
const validateSchema = ajv.compile(schema);
|
|
27
|
+
|
|
28
|
+
const compareDependencies = (left, right) => (
|
|
29
|
+
compareText(left.projectId, right.projectId)
|
|
30
|
+
|| compareText(left.packageId, right.packageId)
|
|
31
|
+
|| compareText(left.analysisResultId, right.analysisResultId)
|
|
32
|
+
);
|
|
33
|
+
const compareFindings = (left, right) => compareText(left.id, right.id);
|
|
34
|
+
const compareMatches = (left, right) => compareText(left.symbol, right.symbol);
|
|
35
|
+
|
|
36
|
+
function buildDependencies(versionAnalysis, usageIndex, matcher) {
|
|
37
|
+
const usages = new Map(usageIndex.dependencies.map((usage) => (
|
|
38
|
+
[`${usage.projectId}\0${usage.packageId}`, usage]
|
|
39
|
+
)));
|
|
40
|
+
return versionAnalysis.results.map((result) => {
|
|
41
|
+
const key = `${result.dependency.projectId}\0${result.dependency.packageId}`;
|
|
42
|
+
const usage = usages.get(key) ?? null;
|
|
43
|
+
const coverage = coverageForProject(
|
|
44
|
+
usageIndex,
|
|
45
|
+
result.dependency.projectId,
|
|
46
|
+
result.dependency.ecosystem ?? null
|
|
47
|
+
);
|
|
48
|
+
const versionStatus = result.status ?? 'analyzed';
|
|
49
|
+
const findings = result.findings
|
|
50
|
+
.filter((finding) => finding.kind === 'breakingChange')
|
|
51
|
+
.map((finding) => {
|
|
52
|
+
const matches = matcher.match(finding, usage);
|
|
53
|
+
const state = classifyFindingImpact({ versionStatus, coverage, usage, matches });
|
|
54
|
+
return {
|
|
55
|
+
id: finding.id,
|
|
56
|
+
kind: finding.kind,
|
|
57
|
+
summary: finding.summary,
|
|
58
|
+
impacted: matches.length > 0,
|
|
59
|
+
...state,
|
|
60
|
+
matches
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
.sort(compareFindings);
|
|
64
|
+
const state = classifyDependencyImpact({ versionStatus, coverage, usage, findings });
|
|
65
|
+
return {
|
|
66
|
+
analysisResultId: result.id,
|
|
67
|
+
projectId: result.dependency.projectId,
|
|
68
|
+
packageId: result.dependency.packageId,
|
|
69
|
+
name: result.dependency.declaredName,
|
|
70
|
+
impacted: findings.some((finding) => finding.impacted),
|
|
71
|
+
...state,
|
|
72
|
+
coverage: structuredClone(coverage),
|
|
73
|
+
findings
|
|
74
|
+
};
|
|
75
|
+
}).sort(compareDependencies);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildSummary(dependencies) {
|
|
79
|
+
const findings = dependencies.flatMap((dependency) => dependency.findings);
|
|
80
|
+
const matches = findings.flatMap((finding) => finding.matches);
|
|
81
|
+
const affectedFiles = new Set(matches.flatMap((match) => match.files));
|
|
82
|
+
return {
|
|
83
|
+
impacted: dependencies.some((dependency) => dependency.impacted),
|
|
84
|
+
dependencyCount: dependencies.length,
|
|
85
|
+
impactedDependencyCount: dependencies.filter((dependency) => dependency.impacted).length,
|
|
86
|
+
findingCount: findings.length,
|
|
87
|
+
impactedFindingCount: findings.filter((finding) => finding.impacted).length,
|
|
88
|
+
matchCount: matches.length,
|
|
89
|
+
affectedFileCount: affectedFiles.size,
|
|
90
|
+
notImpactedDependencyCount: dependencies.filter((item) => item.status === 'NOT_IMPACTED').length,
|
|
91
|
+
usageNotFoundDependencyCount: dependencies.filter((item) => item.status === 'USAGE_NOT_FOUND').length,
|
|
92
|
+
coverageUnavailableDependencyCount: dependencies.filter((item) => item.status === 'COVERAGE_UNAVAILABLE').length,
|
|
93
|
+
notAnalyzedDependencyCount: dependencies.filter((item) => item.status === 'NOT_ANALYZED').length
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateRepositoryImpactInvariants(impact) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
if (!isSorted(impact.dependencies, compareDependencies)) errors.push('dependencies must be sorted.');
|
|
100
|
+
if (impact.analysis.resultCount !== impact.dependencies.length) {
|
|
101
|
+
errors.push('analysis.resultCount must equal dependencies length.');
|
|
102
|
+
}
|
|
103
|
+
const resultIds = new Set();
|
|
104
|
+
for (const dependency of impact.dependencies) {
|
|
105
|
+
if (resultIds.has(dependency.analysisResultId)) {
|
|
106
|
+
errors.push(`duplicate analysis result ${dependency.analysisResultId}.`);
|
|
107
|
+
}
|
|
108
|
+
resultIds.add(dependency.analysisResultId);
|
|
109
|
+
if (!isSorted(dependency.findings, compareFindings)) {
|
|
110
|
+
errors.push(`findings for ${dependency.analysisResultId} must be sorted.`);
|
|
111
|
+
}
|
|
112
|
+
if (new Set(dependency.findings.map((finding) => finding.id)).size !== dependency.findings.length) {
|
|
113
|
+
errors.push(`findings for ${dependency.analysisResultId} must be unique.`);
|
|
114
|
+
}
|
|
115
|
+
const expectedDependencyImpact = dependency.findings.some((finding) => finding.impacted);
|
|
116
|
+
if (dependency.impacted !== expectedDependencyImpact) {
|
|
117
|
+
errors.push(`dependency ${dependency.analysisResultId} impacted is inconsistent.`);
|
|
118
|
+
}
|
|
119
|
+
if (dependency.status !== undefined) {
|
|
120
|
+
if (!REPOSITORY_IMPACT_STATUSES.includes(dependency.status)) {
|
|
121
|
+
errors.push(`dependency ${dependency.analysisResultId} status is unsupported.`);
|
|
122
|
+
}
|
|
123
|
+
if (!REPOSITORY_IMPACT_REASON_CODES.includes(dependency.reasonCode)) {
|
|
124
|
+
errors.push(`dependency ${dependency.analysisResultId} reasonCode is unsupported.`);
|
|
125
|
+
}
|
|
126
|
+
if ((dependency.status === 'IMPACTED') !== dependency.impacted) {
|
|
127
|
+
errors.push(`dependency ${dependency.analysisResultId} status is inconsistent with impacted.`);
|
|
128
|
+
}
|
|
129
|
+
if (!dependency.coverage) {
|
|
130
|
+
errors.push(`dependency ${dependency.analysisResultId} status has no coverage.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const finding of dependency.findings) {
|
|
134
|
+
if (!isSorted(finding.matches, compareMatches)) errors.push(`matches for finding ${finding.id} must be sorted.`);
|
|
135
|
+
if (new Set(finding.matches.map((match) => match.symbol)).size !== finding.matches.length) {
|
|
136
|
+
errors.push(`matches for finding ${finding.id} must be unique.`);
|
|
137
|
+
}
|
|
138
|
+
if (finding.impacted !== (finding.matches.length > 0)) {
|
|
139
|
+
errors.push(`finding ${finding.id} impacted is inconsistent.`);
|
|
140
|
+
}
|
|
141
|
+
if (finding.status !== undefined) {
|
|
142
|
+
if (!REPOSITORY_IMPACT_STATUSES.includes(finding.status)) {
|
|
143
|
+
errors.push(`finding ${finding.id} status is unsupported.`);
|
|
144
|
+
}
|
|
145
|
+
if (!REPOSITORY_IMPACT_REASON_CODES.includes(finding.reasonCode)) {
|
|
146
|
+
errors.push(`finding ${finding.id} reasonCode is unsupported.`);
|
|
147
|
+
}
|
|
148
|
+
if ((finding.status === 'IMPACTED') !== finding.impacted) {
|
|
149
|
+
errors.push(`finding ${finding.id} status is inconsistent with impacted.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const match of finding.matches) {
|
|
153
|
+
if (!isSorted(match.files, compareText)) errors.push(`files for match ${match.symbol} must be sorted.`);
|
|
154
|
+
if (new Set(match.files).size !== match.files.length) errors.push(`files for match ${match.symbol} must be unique.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const expected = buildSummary(impact.dependencies);
|
|
159
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
160
|
+
const statusAware = impact.dependencies.some((dependency) => dependency.status !== undefined);
|
|
161
|
+
if (statusAware && impact.summary[field] === undefined) {
|
|
162
|
+
errors.push(`summary.${field} is required for status-aware impact.`);
|
|
163
|
+
} else if (impact.summary[field] !== undefined && impact.summary[field] !== value) {
|
|
164
|
+
errors.push(`summary.${field} is inconsistent.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return errors.sort(compareText);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function validateRepositoryImpact(impact) {
|
|
171
|
+
if (!validateSchema(impact)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Repository Impact validation error: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const errors = validateRepositoryImpactInvariants(impact);
|
|
177
|
+
if (errors.length > 0) throw new Error(`Repository Impact invariant error: ${errors.join(' ')}`);
|
|
178
|
+
return impact;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildRepositoryImpact({ input, versionAnalysis, usageIndex, matcher, generatedAt = new Date() }) {
|
|
182
|
+
if (!matcher || typeof matcher.id !== 'string' || typeof matcher.version !== 'string'
|
|
183
|
+
|| typeof matcher.match !== 'function') {
|
|
184
|
+
throw new Error('Repository Impact input error: matcher must expose id, version, and match().');
|
|
185
|
+
}
|
|
186
|
+
const dependencies = buildDependencies(versionAnalysis, usageIndex, matcher);
|
|
187
|
+
return validateRepositoryImpact({
|
|
188
|
+
schemaVersion: REPOSITORY_IMPACT_SCHEMA_VERSION,
|
|
189
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
190
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
191
|
+
input: structuredClone(input),
|
|
192
|
+
analysis: {
|
|
193
|
+
matcher: { id: matcher.id, version: matcher.version },
|
|
194
|
+
resultCount: dependencies.length
|
|
195
|
+
},
|
|
196
|
+
summary: buildSummary(dependencies),
|
|
197
|
+
dependencies
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { loadImpactAnalysisInputs } from './input-loader.js';
|
|
2
|
+
import { createExactSymbolImpactMatcher } from './matcher.js';
|
|
3
|
+
import { buildRepositoryImpact } from './repository-impact.js';
|
|
4
|
+
|
|
5
|
+
export function analyzeRepositoryImpact({
|
|
6
|
+
versionAnalysis,
|
|
7
|
+
usageIndex,
|
|
8
|
+
input,
|
|
9
|
+
matcher = createExactSymbolImpactMatcher(),
|
|
10
|
+
clock
|
|
11
|
+
}) {
|
|
12
|
+
return buildRepositoryImpact({
|
|
13
|
+
input,
|
|
14
|
+
versionAnalysis,
|
|
15
|
+
usageIndex,
|
|
16
|
+
matcher,
|
|
17
|
+
generatedAt: clock?.() ?? new Date()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runImpactAnalysis({ sources, ...options }) {
|
|
22
|
+
const artifacts = await loadImpactAnalysisInputs(sources, options);
|
|
23
|
+
return analyzeRepositoryImpact({ ...artifacts, ...options });
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isMatchableUsageSymbol } from './matcher.js';
|
|
2
|
+
|
|
3
|
+
export const REPOSITORY_IMPACT_STATUSES = Object.freeze([
|
|
4
|
+
'IMPACTED',
|
|
5
|
+
'NOT_IMPACTED',
|
|
6
|
+
'USAGE_NOT_FOUND',
|
|
7
|
+
'COVERAGE_UNAVAILABLE',
|
|
8
|
+
'NOT_ANALYZED'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const REPOSITORY_IMPACT_REASON_CODES = Object.freeze([
|
|
12
|
+
'EXACT_SYMBOL_USAGE_FOUND',
|
|
13
|
+
'NO_EXACT_SYMBOL_USAGE_FOUND',
|
|
14
|
+
'NO_MATCHABLE_SYMBOL_FOUND',
|
|
15
|
+
'USAGE_NOT_FOUND',
|
|
16
|
+
'VERSION_ANALYSIS_SKIPPED',
|
|
17
|
+
'VERSION_ANALYSIS_FAILED',
|
|
18
|
+
'COVERAGE_COMPLETE',
|
|
19
|
+
'ANALYZER_UNAVAILABLE',
|
|
20
|
+
'ANALYZER_AMBIGUOUS',
|
|
21
|
+
'SOURCE_SCAN_INCOMPLETE',
|
|
22
|
+
'SOURCE_PARSE_FAILED',
|
|
23
|
+
'FILE_UNREADABLE',
|
|
24
|
+
'ANALYZER_FAILED',
|
|
25
|
+
'MULTIPLE_ANALYSIS_FAILURES',
|
|
26
|
+
'COVERAGE_METADATA_MISSING'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function notAnalyzedReason(status) {
|
|
30
|
+
return status === 'failed' ? 'VERSION_ANALYSIS_FAILED' : 'VERSION_ANALYSIS_SKIPPED';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function classifyFindingImpact({ versionStatus = 'analyzed', coverage, usage, matches }) {
|
|
34
|
+
if (versionStatus !== 'analyzed') {
|
|
35
|
+
return { status: 'NOT_ANALYZED', reasonCode: notAnalyzedReason(versionStatus) };
|
|
36
|
+
}
|
|
37
|
+
if (matches.length > 0) {
|
|
38
|
+
return { status: 'IMPACTED', reasonCode: 'EXACT_SYMBOL_USAGE_FOUND' };
|
|
39
|
+
}
|
|
40
|
+
if (coverage.status !== 'complete') {
|
|
41
|
+
return { status: 'COVERAGE_UNAVAILABLE', reasonCode: coverage.reasonCode };
|
|
42
|
+
}
|
|
43
|
+
if (!usage) return { status: 'USAGE_NOT_FOUND', reasonCode: 'USAGE_NOT_FOUND' };
|
|
44
|
+
if (!usage.symbols.some((symbol) => isMatchableUsageSymbol(symbol.name))) {
|
|
45
|
+
return { status: 'NOT_IMPACTED', reasonCode: 'NO_MATCHABLE_SYMBOL_FOUND' };
|
|
46
|
+
}
|
|
47
|
+
return { status: 'NOT_IMPACTED', reasonCode: 'NO_EXACT_SYMBOL_USAGE_FOUND' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function classifyDependencyImpact({ versionStatus = 'analyzed', coverage, usage, findings }) {
|
|
51
|
+
if (versionStatus !== 'analyzed') {
|
|
52
|
+
return { status: 'NOT_ANALYZED', reasonCode: notAnalyzedReason(versionStatus) };
|
|
53
|
+
}
|
|
54
|
+
if (findings.some((finding) => finding.status === 'IMPACTED')) {
|
|
55
|
+
return { status: 'IMPACTED', reasonCode: 'EXACT_SYMBOL_USAGE_FOUND' };
|
|
56
|
+
}
|
|
57
|
+
if (coverage.status !== 'complete') {
|
|
58
|
+
return { status: 'COVERAGE_UNAVAILABLE', reasonCode: coverage.reasonCode };
|
|
59
|
+
}
|
|
60
|
+
if (!usage) return { status: 'USAGE_NOT_FOUND', reasonCode: 'USAGE_NOT_FOUND' };
|
|
61
|
+
return { status: 'NOT_IMPACTED', reasonCode: 'NO_EXACT_SYMBOL_USAGE_FOUND' };
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateRepositoryImpact } from './repository-impact.js';
|
|
5
|
+
|
|
6
|
+
export function serializeRepositoryImpact(impact) {
|
|
7
|
+
validateRepositoryImpact(impact);
|
|
8
|
+
return `${JSON.stringify(impact, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function writeRepositoryImpact(outputPath, impact) {
|
|
12
|
+
const target = path.resolve(outputPath);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
const contents = serializeRepositoryImpact(impact);
|
|
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
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_REPOSITORY_IMPACT_PATH,
|
|
6
|
+
REPOSITORY_IMPACT_SCHEMA_VERSION
|
|
7
|
+
} from '../constants.js';
|
|
8
|
+
import { ImpactAnalysisInputError, loadImpactAnalysisInputs } from '../impact/input-loader.js';
|
|
9
|
+
import {
|
|
10
|
+
EXACT_SYMBOL_MATCHER_ID,
|
|
11
|
+
EXACT_SYMBOL_MATCHER_VERSION,
|
|
12
|
+
matchFindingToUsage
|
|
13
|
+
} from '../impact/matcher.js';
|
|
14
|
+
import { classifyFindingImpact } from '../impact/status.js';
|
|
15
|
+
import { validateRepositoryImpact } from '../impact/repository-impact.js';
|
|
16
|
+
import { isPortableRelativePath } from '../portable.js';
|
|
17
|
+
import { coverageForProject } from '../usage/coverage.js';
|
|
18
|
+
|
|
19
|
+
export class ImpactEvidenceInputError extends Error {
|
|
20
|
+
constructor(message, code = 'IMPACT_EVIDENCE_INPUT_INVALID') {
|
|
21
|
+
super(`Impact Evidence input error: ${message}`);
|
|
22
|
+
this.name = 'ImpactEvidenceInputError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function digest(bytes) {
|
|
28
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readBytes(source) {
|
|
32
|
+
if (typeof source === 'string' || source instanceof URL) return readFile(source);
|
|
33
|
+
if (source && typeof source === 'object' && 'bytes' in source && source.bytes instanceof Uint8Array) {
|
|
34
|
+
return Buffer.from(source.bytes);
|
|
35
|
+
}
|
|
36
|
+
throw new ImpactEvidenceInputError(
|
|
37
|
+
'Repository Impact source must be a file path or { bytes, artifact }.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function artifactFor(source, configured) {
|
|
42
|
+
const artifact = source && typeof source === 'object' && 'bytes' in source
|
|
43
|
+
? source.artifact
|
|
44
|
+
: configured ?? DEFAULT_REPOSITORY_IMPACT_PATH;
|
|
45
|
+
if (!isPortableRelativePath(artifact)) {
|
|
46
|
+
throw new ImpactEvidenceInputError(
|
|
47
|
+
'Repository Impact artifact must be a portable repository-relative path.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return artifact;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readField(value, field) {
|
|
54
|
+
return field.split('.').reduce((current, segment) => current?.[segment], value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateLineage(baseInput, repositoryImpact) {
|
|
58
|
+
const groups = [
|
|
59
|
+
['Project Manifest', repositoryImpact.input.projectManifest, baseInput.projectManifest,
|
|
60
|
+
['schemaVersion', 'artifact', 'artifactDigest', 'repository.name', 'repository.root']],
|
|
61
|
+
['Version Analysis', repositoryImpact.input.versionAnalysis, baseInput.versionAnalysis,
|
|
62
|
+
['schemaVersion', 'artifact', 'artifactDigest']],
|
|
63
|
+
['Usage Index', repositoryImpact.input.usageIndex, baseInput.usageIndex,
|
|
64
|
+
['schemaVersion', 'artifact', 'artifactDigest']]
|
|
65
|
+
];
|
|
66
|
+
const mismatches = groups.flatMap(([label, expected, actual, fields]) => (
|
|
67
|
+
fields
|
|
68
|
+
.filter((field) => readField(expected, field) !== readField(actual, field))
|
|
69
|
+
.map((field) => `${label} ${field}`)
|
|
70
|
+
));
|
|
71
|
+
if (mismatches.length > 0) {
|
|
72
|
+
throw new ImpactEvidenceInputError(
|
|
73
|
+
`Repository Impact lineage mismatch: ${mismatches.join(', ')}.`,
|
|
74
|
+
'LINEAGE_MISMATCH'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sameValue(left, right) {
|
|
80
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateReferences(versionAnalysis, usageIndex, repositoryImpact) {
|
|
84
|
+
if (repositoryImpact.analysis.matcher.id !== EXACT_SYMBOL_MATCHER_ID
|
|
85
|
+
|| repositoryImpact.analysis.matcher.version !== EXACT_SYMBOL_MATCHER_VERSION) {
|
|
86
|
+
throw new ImpactEvidenceInputError(
|
|
87
|
+
`unsupported impact matcher; expected ${EXACT_SYMBOL_MATCHER_ID}@${EXACT_SYMBOL_MATCHER_VERSION}.`,
|
|
88
|
+
'MATCHER_UNSUPPORTED'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const results = new Map(versionAnalysis.results.map((result) => [result.id, result]));
|
|
92
|
+
const usages = new Map(usageIndex.dependencies.map((usage) => (
|
|
93
|
+
[`${usage.projectId}\0${usage.packageId}`, usage]
|
|
94
|
+
)));
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const dependency of repositoryImpact.dependencies) {
|
|
97
|
+
const result = results.get(dependency.analysisResultId);
|
|
98
|
+
if (!result) {
|
|
99
|
+
errors.push(`unknown Version Analysis result ${dependency.analysisResultId}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (dependency.projectId !== result.dependency.projectId
|
|
103
|
+
|| dependency.packageId !== result.dependency.packageId
|
|
104
|
+
|| dependency.name !== result.dependency.declaredName) {
|
|
105
|
+
errors.push(`dependency identity mismatch for ${dependency.analysisResultId}`);
|
|
106
|
+
}
|
|
107
|
+
const versionFindings = result.findings.filter((finding) => finding.kind === 'breakingChange');
|
|
108
|
+
if (versionFindings.length !== dependency.findings.length) {
|
|
109
|
+
errors.push(`finding count mismatch for ${dependency.analysisResultId}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const findings = new Map(versionFindings.map((finding) => [finding.id, finding]));
|
|
113
|
+
const usage = usages.get(`${dependency.projectId}\0${dependency.packageId}`) ?? null;
|
|
114
|
+
const coverage = dependency.coverage
|
|
115
|
+
?? coverageForProject(usageIndex, dependency.projectId, result.dependency.ecosystem);
|
|
116
|
+
for (const impactFinding of dependency.findings) {
|
|
117
|
+
const sourceFinding = findings.get(impactFinding.id);
|
|
118
|
+
if (!sourceFinding) {
|
|
119
|
+
errors.push(`unknown finding ${impactFinding.id} for ${dependency.analysisResultId}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (impactFinding.summary !== sourceFinding.summary) {
|
|
123
|
+
errors.push(`finding summary mismatch for ${dependency.analysisResultId}/${impactFinding.id}`);
|
|
124
|
+
}
|
|
125
|
+
const expectedMatches = matchFindingToUsage(sourceFinding, usage);
|
|
126
|
+
if (!sameValue(impactFinding.matches, expectedMatches)) {
|
|
127
|
+
errors.push(`usage match mismatch for ${dependency.analysisResultId}/${impactFinding.id}`);
|
|
128
|
+
}
|
|
129
|
+
if (impactFinding.status !== undefined) {
|
|
130
|
+
const expectedState = classifyFindingImpact({
|
|
131
|
+
versionStatus: result.status,
|
|
132
|
+
coverage,
|
|
133
|
+
usage,
|
|
134
|
+
matches: expectedMatches
|
|
135
|
+
});
|
|
136
|
+
if (impactFinding.status !== expectedState.status
|
|
137
|
+
|| impactFinding.reasonCode !== expectedState.reasonCode) {
|
|
138
|
+
errors.push(`impact state mismatch for ${dependency.analysisResultId}/${impactFinding.id}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (repositoryImpact.dependencies.length !== versionAnalysis.results.length) {
|
|
144
|
+
errors.push('Repository Impact dependency count does not match Version Analysis results');
|
|
145
|
+
}
|
|
146
|
+
if (errors.length > 0) {
|
|
147
|
+
throw new ImpactEvidenceInputError(`invalid artifact references: ${errors.sort().join('; ')}.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function loadImpactEvidenceInputs(sources, options = {}) {
|
|
152
|
+
if (!sources?.projectManifest || !sources?.versionAnalysis || !sources?.usageIndex
|
|
153
|
+
|| !sources?.repositoryImpact) {
|
|
154
|
+
throw new ImpactEvidenceInputError(
|
|
155
|
+
'projectManifest, versionAnalysis, usageIndex, and repositoryImpact sources are required.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
let base;
|
|
159
|
+
try {
|
|
160
|
+
base = await loadImpactAnalysisInputs(sources, options);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof ImpactAnalysisInputError) {
|
|
163
|
+
throw new ImpactEvidenceInputError(error.message, error.code);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
const artifact = artifactFor(sources.repositoryImpact, options.repositoryImpactArtifact);
|
|
168
|
+
const bytes = await readBytes(sources.repositoryImpact);
|
|
169
|
+
let repositoryImpact;
|
|
170
|
+
try {
|
|
171
|
+
repositoryImpact = JSON.parse(bytes.toString('utf8'));
|
|
172
|
+
} catch {
|
|
173
|
+
throw new ImpactEvidenceInputError('Repository Impact bytes are not valid JSON.');
|
|
174
|
+
}
|
|
175
|
+
if (repositoryImpact?.schemaVersion !== REPOSITORY_IMPACT_SCHEMA_VERSION) {
|
|
176
|
+
throw new ImpactEvidenceInputError(
|
|
177
|
+
`unsupported Repository Impact schema version; expected ${REPOSITORY_IMPACT_SCHEMA_VERSION}.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
validateRepositoryImpact(repositoryImpact);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new ImpactEvidenceInputError(error.message);
|
|
184
|
+
}
|
|
185
|
+
validateLineage(base.input, repositoryImpact);
|
|
186
|
+
validateReferences(base.versionAnalysis, base.usageIndex, repositoryImpact);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
projectManifest: base.projectManifest,
|
|
190
|
+
versionAnalysis: base.versionAnalysis,
|
|
191
|
+
usageIndex: base.usageIndex,
|
|
192
|
+
repositoryImpact,
|
|
193
|
+
input: {
|
|
194
|
+
...base.input,
|
|
195
|
+
repositoryImpact: {
|
|
196
|
+
schemaVersion: repositoryImpact.schemaVersion,
|
|
197
|
+
artifact,
|
|
198
|
+
artifactDigest: digest(bytes)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|