@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,626 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
|
|
7
|
+
import { canonicalJsonBytes } from '../canonical-json.js';
|
|
8
|
+
import {
|
|
9
|
+
ARTIFACT_GENERATOR_NAME,
|
|
10
|
+
UPGRADE_DECISION_SCHEMA_VERSION,
|
|
11
|
+
VERSION
|
|
12
|
+
} from '../constants.js';
|
|
13
|
+
import {
|
|
14
|
+
EcosystemVersionError,
|
|
15
|
+
getEcosystemVersionAdapter
|
|
16
|
+
} from '../ecosystem-version-adapter.js';
|
|
17
|
+
import { compareText, isSorted } from '../portable.js';
|
|
18
|
+
|
|
19
|
+
export const UPGRADE_DECISION_POLICY_ID = 'deterministic-upgrade-decision';
|
|
20
|
+
export const UPGRADE_DECISION_POLICY_VERSION = '1.1.0';
|
|
21
|
+
export const UPGRADE_DECISIONS = Object.freeze([
|
|
22
|
+
'KEEP_CURRENT',
|
|
23
|
+
'UPGRADE_NOW',
|
|
24
|
+
'PLAN_UPGRADE',
|
|
25
|
+
'INVESTIGATE',
|
|
26
|
+
'INSUFFICIENT_EVIDENCE',
|
|
27
|
+
'NOT_ANALYZED'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const schema = JSON.parse(await readFile(
|
|
31
|
+
new URL('../../schemas/upgrade-decision.schema.json', import.meta.url),
|
|
32
|
+
'utf8'
|
|
33
|
+
));
|
|
34
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
35
|
+
addFormats(ajv);
|
|
36
|
+
const validateSchema = ajv.compile(schema);
|
|
37
|
+
|
|
38
|
+
const compareDecisions = (left, right) => (
|
|
39
|
+
compareText(left.occurrence.projectId, right.occurrence.projectId)
|
|
40
|
+
|| compareText(left.occurrence.manifest, right.occurrence.manifest)
|
|
41
|
+
|| compareText(left.occurrence.dependencyType, right.occurrence.dependencyType)
|
|
42
|
+
|| compareText(left.occurrence.packageId, right.occurrence.packageId)
|
|
43
|
+
|| compareText(left.analysisResultId ?? '', right.analysisResultId ?? '')
|
|
44
|
+
);
|
|
45
|
+
const compareLimitations = (left, right) => (
|
|
46
|
+
compareText(left.code, right.code) || compareText(left.message, right.message)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function digest(value) {
|
|
50
|
+
return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decisionId(input, occurrence, analysisResultId) {
|
|
54
|
+
return digest({
|
|
55
|
+
inputDigests: Object.fromEntries(
|
|
56
|
+
Object.entries(input).map(([key, value]) => [key, value.artifactDigest])
|
|
57
|
+
),
|
|
58
|
+
occurrence,
|
|
59
|
+
analysisResultId,
|
|
60
|
+
policy: `${UPGRADE_DECISION_POLICY_ID}@${UPGRADE_DECISION_POLICY_VERSION}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sortedUniqueText(values) {
|
|
65
|
+
return [...new Set(values)].sort(compareText);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sortedUniqueLimitations(values) {
|
|
69
|
+
const unique = new Map(values.map((value) => [`${value.code}\0${value.message}`, value]));
|
|
70
|
+
return [...unique.values()].sort(compareLimitations);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function limitation(code, message) {
|
|
74
|
+
return { code, message };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function adapterMetadata(adapter) {
|
|
78
|
+
return adapter ? { ecosystem: adapter.ecosystem, contractVersion: '1' } : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function impactFor(impactEvidence, result) {
|
|
82
|
+
const impact = impactEvidence.get(result.id);
|
|
83
|
+
const breakingFindingCount = result.findings.filter((finding) => finding.kind === 'breakingChange').length;
|
|
84
|
+
return {
|
|
85
|
+
status: impact?.status ?? 'NOT_ANALYZED',
|
|
86
|
+
reasonCode: impact?.reasonCode ?? null,
|
|
87
|
+
repositorySensitiveFindingCount: breakingFindingCount,
|
|
88
|
+
coverage: {
|
|
89
|
+
status: impact?.coverage?.status ?? 'missing',
|
|
90
|
+
reasonCode: impact?.coverage?.reasonCode ?? null
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function referencedEvidenceIds(result) {
|
|
96
|
+
return sortedUniqueText([
|
|
97
|
+
...result.summaryEvidenceRefs,
|
|
98
|
+
...result.riskEvidenceRefs,
|
|
99
|
+
...result.findings.flatMap((finding) => finding.evidenceRefs)
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function evidenceFor(artifacts, result) {
|
|
104
|
+
const bundleById = new Map(artifacts.knowledgeEvidenceBundle.evidence.map((item) => [item.id, item]));
|
|
105
|
+
const sourceById = new Map(artifacts.knowledgeManifest.sources.map((source) => [source.id, source]));
|
|
106
|
+
const warningMatches = (code, source) => artifacts.knowledgeManifest.warnings.some((warning) => (
|
|
107
|
+
warning.code === code
|
|
108
|
+
&& (!warning.packageId || warning.packageId === result.dependency.packageId)
|
|
109
|
+
&& (!warning.sourceId || warning.sourceId === source.id)
|
|
110
|
+
));
|
|
111
|
+
const refs = referencedEvidenceIds(result);
|
|
112
|
+
let conflicted = result.validation.warningCodes.includes('SOURCE_CONFLICT')
|
|
113
|
+
|| result.limitations.some((item) => item.code === 'SOURCE_CONFLICT');
|
|
114
|
+
let stale = result.validation.warningCodes.includes('CACHE_EXPIRED')
|
|
115
|
+
|| result.limitations.some((item) => item.code === 'CACHE_EXPIRED');
|
|
116
|
+
const targetScopedRefs = [];
|
|
117
|
+
for (const ref of refs) {
|
|
118
|
+
const item = bundleById.get(ref);
|
|
119
|
+
const source = item ? sourceById.get(item.sourceId) : null;
|
|
120
|
+
if (!item || !source) continue;
|
|
121
|
+
conflicted ||= (source.conflictsWith?.length ?? 0) > 0 || warningMatches('SOURCE_CONFLICT', source);
|
|
122
|
+
stale ||= source.status === 'stale'
|
|
123
|
+
|| source.snapshot?.freshness === 'stale'
|
|
124
|
+
|| warningMatches('CACHE_EXPIRED', source);
|
|
125
|
+
const official = ['official', 'publisher'].includes(source.trust)
|
|
126
|
+
&& ['officialProject', 'publisherProvided'].includes(source.authority)
|
|
127
|
+
&& !['registry', 'community'].includes(source.kind);
|
|
128
|
+
if (official
|
|
129
|
+
&& source.status === 'available'
|
|
130
|
+
&& result.versions.targetVersion !== null
|
|
131
|
+
&& item.releaseVersions.includes(result.versions.targetVersion)) {
|
|
132
|
+
targetScopedRefs.push(ref);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const sufficient = result.evidenceCoverage === 'sufficient'
|
|
136
|
+
&& targetScopedRefs.length > 0
|
|
137
|
+
&& !conflicted
|
|
138
|
+
&& !stale;
|
|
139
|
+
return {
|
|
140
|
+
status: conflicted ? 'conflicted' : sufficient ? 'sufficient' : 'insufficient',
|
|
141
|
+
targetScopedRefs: sortedUniqueText(targetScopedRefs),
|
|
142
|
+
hasReferencedEvidence: refs.length > 0,
|
|
143
|
+
conflicted,
|
|
144
|
+
stale
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function comparisonFor(result, adapters) {
|
|
149
|
+
const versions = result.versions;
|
|
150
|
+
const installedVersionStatus = versions.installedVersionStatus
|
|
151
|
+
?? (Object.hasOwn(versions, 'installedVersion') ? 'unresolved' : 'legacyMissing');
|
|
152
|
+
const base = {
|
|
153
|
+
declaredVersion: versions.declaredVersion,
|
|
154
|
+
installedVersion: versions.installedVersion ?? null,
|
|
155
|
+
installedVersionStatus,
|
|
156
|
+
targetVersion: versions.targetVersion,
|
|
157
|
+
targetPolicy: versions.targetPolicy,
|
|
158
|
+
comparison: 'unknown',
|
|
159
|
+
adapter: null
|
|
160
|
+
};
|
|
161
|
+
if (installedVersionStatus !== 'resolved' || !versions.installedVersion || !versions.targetVersion) {
|
|
162
|
+
return { versions: base, adapter: null };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const adapter = getEcosystemVersionAdapter(result.dependency.ecosystem, adapters);
|
|
166
|
+
const compared = adapter.compareVersions(versions.installedVersion, versions.targetVersion);
|
|
167
|
+
const comparison = compared.direction === 'same'
|
|
168
|
+
? 'equal'
|
|
169
|
+
: compared.direction === 'upgrade'
|
|
170
|
+
? 'targetNewer'
|
|
171
|
+
: compared.direction === 'downgrade'
|
|
172
|
+
? 'installedNewer'
|
|
173
|
+
: 'incomparable';
|
|
174
|
+
return {
|
|
175
|
+
versions: { ...base, comparison, adapter: adapterMetadata(adapter) },
|
|
176
|
+
adapter
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (!(error instanceof EcosystemVersionError)) throw error;
|
|
180
|
+
return { versions: { ...base, comparison: 'incomparable' }, adapter: null };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function coverageReason(impact) {
|
|
185
|
+
if (impact.coverage.status === 'partial') return 'USAGE_COVERAGE_PARTIAL';
|
|
186
|
+
if (['unavailable', 'failed', 'missing'].includes(impact.coverage.status)) {
|
|
187
|
+
return 'USAGE_COVERAGE_UNAVAILABLE';
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function providerRejected(result) {
|
|
193
|
+
return [...result.humanReviewReasons, ...result.limitations.map((item) => item.code)]
|
|
194
|
+
.some((value) => /PROVIDER|REJECT/i.test(value));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recommendationDriverFor(result) {
|
|
198
|
+
return result.versions.targetPolicy === 'explicit' ? 'USER_SELECTED_TARGET' : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function decide(result, comparison, evidence, impact) {
|
|
202
|
+
const { versions } = comparison;
|
|
203
|
+
const limitations = [];
|
|
204
|
+
const coverageCode = coverageReason(impact);
|
|
205
|
+
if (coverageCode) {
|
|
206
|
+
limitations.push(limitation(
|
|
207
|
+
coverageCode,
|
|
208
|
+
'Repository usage coverage is incomplete; repository-sensitive conclusions remain bounded.'
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
if (versions.targetPolicy === 'registryLatest') {
|
|
212
|
+
limitations.push(limitation(
|
|
213
|
+
'REGISTRY_LATEST_IS_NOT_RECOMMENDATION',
|
|
214
|
+
'Registry latest is a candidate fact and is not independently an upgrade recommendation.'
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.status !== 'analyzed') {
|
|
219
|
+
const failed = result.status === 'failed';
|
|
220
|
+
const reasons = [failed ? 'VERSION_ANALYSIS_FAILED' : 'VERSION_ANALYSIS_SKIPPED'];
|
|
221
|
+
if (failed && providerRejected(result)) reasons.push('PROVIDER_REJECTED');
|
|
222
|
+
return {
|
|
223
|
+
decision: 'NOT_ANALYZED',
|
|
224
|
+
primaryReasonCode: reasons[0],
|
|
225
|
+
reasonCodes: reasons,
|
|
226
|
+
summary: 'No upgrade decision was evaluated because Version Analysis did not complete.',
|
|
227
|
+
limitations
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (result.versions.installedVersionReason === 'NON_REGISTRY_DEPENDENCY') {
|
|
231
|
+
return {
|
|
232
|
+
decision: 'INVESTIGATE',
|
|
233
|
+
primaryReasonCode: 'NON_REGISTRY_DEPENDENCY',
|
|
234
|
+
reasonCodes: ['NON_REGISTRY_DEPENDENCY'],
|
|
235
|
+
summary: 'Manual investigation is required for a non-registry dependency baseline.',
|
|
236
|
+
limitations
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (versions.installedVersionStatus !== 'resolved' || versions.installedVersion === null) {
|
|
240
|
+
return {
|
|
241
|
+
decision: 'INSUFFICIENT_EVIDENCE',
|
|
242
|
+
primaryReasonCode: 'INSTALLED_VERSION_UNAVAILABLE',
|
|
243
|
+
reasonCodes: ['INSTALLED_VERSION_UNAVAILABLE'],
|
|
244
|
+
summary: 'The installed version baseline is unavailable.',
|
|
245
|
+
limitations
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (versions.targetVersion === null) {
|
|
249
|
+
return {
|
|
250
|
+
decision: 'INSUFFICIENT_EVIDENCE',
|
|
251
|
+
primaryReasonCode: 'TARGET_VERSION_UNAVAILABLE',
|
|
252
|
+
reasonCodes: ['TARGET_VERSION_UNAVAILABLE'],
|
|
253
|
+
summary: 'No evaluated target version is available.',
|
|
254
|
+
limitations
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (versions.adapter === null || versions.comparison === 'incomparable') {
|
|
258
|
+
const reason = comparison.adapter === null
|
|
259
|
+
&& !['node', 'python'].includes(result.dependency.ecosystem)
|
|
260
|
+
? 'UNSUPPORTED_ECOSYSTEM'
|
|
261
|
+
: 'VERSION_INCOMPARABLE';
|
|
262
|
+
return {
|
|
263
|
+
decision: 'INVESTIGATE',
|
|
264
|
+
primaryReasonCode: reason,
|
|
265
|
+
reasonCodes: [reason],
|
|
266
|
+
summary: 'Installed and target versions cannot be compared by the ecosystem adapter.',
|
|
267
|
+
limitations
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (versions.comparison === 'equal') {
|
|
271
|
+
const reasons = [
|
|
272
|
+
'ALREADY_AT_TARGET',
|
|
273
|
+
...(coverageCode ? [coverageCode] : []),
|
|
274
|
+
...(evidence.conflicted ? ['EVIDENCE_CONFLICT', 'SOURCE_CONFLICT'] : []),
|
|
275
|
+
...(evidence.stale ? ['STALE_EVIDENCE'] : [])
|
|
276
|
+
];
|
|
277
|
+
if (evidence.conflicted || evidence.stale) {
|
|
278
|
+
limitations.push(limitation(
|
|
279
|
+
evidence.conflicted ? 'EVIDENCE_CONFLICT' : 'STALE_EVIDENCE',
|
|
280
|
+
'The no-version-change conclusion is exact, but upstream evidence still requires review.'
|
|
281
|
+
));
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
decision: 'KEEP_CURRENT',
|
|
285
|
+
primaryReasonCode: 'ALREADY_AT_TARGET',
|
|
286
|
+
reasonCodes: reasons,
|
|
287
|
+
summary: `Keep the installed version; it already equals target ${versions.targetVersion}.`,
|
|
288
|
+
limitations
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (versions.comparison === 'installedNewer') {
|
|
292
|
+
limitations.push(limitation(
|
|
293
|
+
'INSTALLED_NEWER_THAN_TARGET',
|
|
294
|
+
'The installed version is newer than the evaluated target; this does not establish future safety.'
|
|
295
|
+
));
|
|
296
|
+
return {
|
|
297
|
+
decision: 'KEEP_CURRENT',
|
|
298
|
+
primaryReasonCode: 'INSTALLED_NEWER_THAN_TARGET',
|
|
299
|
+
reasonCodes: [
|
|
300
|
+
'INSTALLED_NEWER_THAN_TARGET',
|
|
301
|
+
...(coverageCode ? [coverageCode] : []),
|
|
302
|
+
...(evidence.conflicted ? ['EVIDENCE_CONFLICT', 'SOURCE_CONFLICT'] : []),
|
|
303
|
+
...(evidence.stale ? ['STALE_EVIDENCE'] : [])
|
|
304
|
+
],
|
|
305
|
+
summary: 'Keep the installed version because it is newer than the evaluated target.',
|
|
306
|
+
limitations
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (evidence.conflicted) {
|
|
310
|
+
return {
|
|
311
|
+
decision: 'INVESTIGATE',
|
|
312
|
+
primaryReasonCode: 'EVIDENCE_CONFLICT',
|
|
313
|
+
reasonCodes: ['EVIDENCE_CONFLICT', 'SOURCE_CONFLICT'],
|
|
314
|
+
summary: 'Manual investigation is required because target evidence is conflicted.',
|
|
315
|
+
limitations
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (evidence.status === 'insufficient' && evidence.hasReferencedEvidence) {
|
|
319
|
+
const reasons = ['EVIDENCE_INSUFFICIENT', ...(evidence.stale ? ['STALE_EVIDENCE'] : [])];
|
|
320
|
+
return {
|
|
321
|
+
decision: 'INSUFFICIENT_EVIDENCE',
|
|
322
|
+
primaryReasonCode: 'EVIDENCE_INSUFFICIENT',
|
|
323
|
+
reasonCodes: reasons,
|
|
324
|
+
summary: 'Referenced evidence is invalid or insufficient for an upgrade plan.',
|
|
325
|
+
limitations
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const recommendationDriver = recommendationDriverFor(result);
|
|
329
|
+
if (recommendationDriver === null) {
|
|
330
|
+
limitations.push(limitation(
|
|
331
|
+
'UPGRADE_AVAILABLE_NO_RECOMMENDATION_DRIVER',
|
|
332
|
+
'A newer target is available, but no verified reason to recommend upgrading was provided.'
|
|
333
|
+
));
|
|
334
|
+
return {
|
|
335
|
+
decision: 'INVESTIGATE',
|
|
336
|
+
primaryReasonCode: 'UPGRADE_AVAILABLE_NO_RECOMMENDATION_DRIVER',
|
|
337
|
+
reasonCodes: ['UPGRADE_AVAILABLE_NO_RECOMMENDATION_DRIVER'],
|
|
338
|
+
summary: 'A newer target is available, but no structured recommendation driver is present.',
|
|
339
|
+
limitations
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (evidence.status !== 'sufficient') {
|
|
343
|
+
const reasons = ['EVIDENCE_INSUFFICIENT', ...(evidence.stale ? ['STALE_EVIDENCE'] : [])];
|
|
344
|
+
return {
|
|
345
|
+
decision: 'INSUFFICIENT_EVIDENCE',
|
|
346
|
+
primaryReasonCode: 'EVIDENCE_INSUFFICIENT',
|
|
347
|
+
reasonCodes: reasons,
|
|
348
|
+
summary: 'Target-scoped official evidence is insufficient for an upgrade plan.',
|
|
349
|
+
limitations
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (impact.repositorySensitiveFindingCount > 0 && coverageCode) {
|
|
353
|
+
return {
|
|
354
|
+
decision: 'INVESTIGATE',
|
|
355
|
+
primaryReasonCode: coverageCode,
|
|
356
|
+
reasonCodes: [coverageCode],
|
|
357
|
+
summary: 'Manual investigation is required because repository-sensitive findings lack complete coverage.',
|
|
358
|
+
limitations
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
decision: 'PLAN_UPGRADE',
|
|
363
|
+
primaryReasonCode: recommendationDriver,
|
|
364
|
+
reasonCodes: [recommendationDriver, 'TARGET_NEWER_EVIDENCE_AVAILABLE'],
|
|
365
|
+
summary: `Plan the caller-selected, evidence-bounded upgrade to target ${versions.targetVersion}.`,
|
|
366
|
+
limitations
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function decisionRecord(artifacts, result, impactEvidence, adapters) {
|
|
371
|
+
const comparison = comparisonFor(result, adapters);
|
|
372
|
+
const evidence = result.status === 'analyzed'
|
|
373
|
+
? evidenceFor(artifacts, result)
|
|
374
|
+
: {
|
|
375
|
+
status: 'notEvaluated',
|
|
376
|
+
targetScopedRefs: [],
|
|
377
|
+
hasReferencedEvidence: false,
|
|
378
|
+
conflicted: false,
|
|
379
|
+
stale: false
|
|
380
|
+
};
|
|
381
|
+
const impact = impactFor(impactEvidence, result);
|
|
382
|
+
const outcome = decide(result, comparison, evidence, impact);
|
|
383
|
+
const occurrence = structuredClone(result.dependency);
|
|
384
|
+
const provenance = {
|
|
385
|
+
policyId: UPGRADE_DECISION_POLICY_ID,
|
|
386
|
+
policyVersion: UPGRADE_DECISION_POLICY_VERSION,
|
|
387
|
+
versionAdapter: adapterMetadata(comparison.adapter),
|
|
388
|
+
analysisResultId: result.id
|
|
389
|
+
};
|
|
390
|
+
const id = decisionId(artifacts.input, occurrence, result.id);
|
|
391
|
+
return {
|
|
392
|
+
id,
|
|
393
|
+
analysisResultId: result.id,
|
|
394
|
+
occurrence,
|
|
395
|
+
decision: outcome.decision,
|
|
396
|
+
summary: outcome.summary,
|
|
397
|
+
primaryReasonCode: outcome.primaryReasonCode,
|
|
398
|
+
reasonCodes: sortedUniqueText(outcome.reasonCodes),
|
|
399
|
+
versions: comparison.versions,
|
|
400
|
+
evidence: {
|
|
401
|
+
status: evidence.status,
|
|
402
|
+
targetScopedRefs: evidence.targetScopedRefs
|
|
403
|
+
},
|
|
404
|
+
impact,
|
|
405
|
+
requiresHumanReview: !(outcome.decision === 'KEEP_CURRENT'
|
|
406
|
+
&& comparison.versions.comparison === 'equal')
|
|
407
|
+
|| evidence.conflicted
|
|
408
|
+
|| result.validation.status === 'invalid',
|
|
409
|
+
provenance,
|
|
410
|
+
limitations: sortedUniqueLimitations(outcome.limitations)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function occurrenceKey(value) {
|
|
415
|
+
return [
|
|
416
|
+
value.projectId,
|
|
417
|
+
value.manifest,
|
|
418
|
+
value.dependencyType ?? value.type,
|
|
419
|
+
value.declaredName ?? value.name,
|
|
420
|
+
value.normalizedName,
|
|
421
|
+
value.declaredVersion ?? value.versions?.declaredVersion ?? ''
|
|
422
|
+
].join('\0');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function canonicalOccurrences(artifacts) {
|
|
426
|
+
if (!Array.isArray(artifacts.knowledgeManifest.packages)) {
|
|
427
|
+
return artifacts.versionAnalysis.results.map((result) => ({
|
|
428
|
+
occurrence: result.dependency,
|
|
429
|
+
declaredVersion: result.versions.declaredVersion
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
const projects = new Map(artifacts.projectManifest.projects.map((project) => [project.id, project]));
|
|
433
|
+
return artifacts.knowledgeManifest.packages.flatMap((packageRecord) => (
|
|
434
|
+
packageRecord.occurrences.map((item) => {
|
|
435
|
+
const project = projects.get(item.projectId);
|
|
436
|
+
const dependency = project.dependencies.find((candidate) => (
|
|
437
|
+
candidate.manifest === item.manifest
|
|
438
|
+
&& candidate.type === item.dependencyType
|
|
439
|
+
&& candidate.name === item.declaredName
|
|
440
|
+
&& candidate.normalizedName === packageRecord.identity.normalizedName
|
|
441
|
+
&& (candidate.declaredVersion ?? null) === (item.declaredVersion ?? null)
|
|
442
|
+
));
|
|
443
|
+
return {
|
|
444
|
+
occurrence: {
|
|
445
|
+
projectId: item.projectId,
|
|
446
|
+
packageId: packageRecord.id,
|
|
447
|
+
declaredName: item.declaredName,
|
|
448
|
+
normalizedName: packageRecord.identity.normalizedName,
|
|
449
|
+
ecosystem: packageRecord.ecosystem,
|
|
450
|
+
registry: packageRecord.identity.registry,
|
|
451
|
+
packageManager: project.packageManager?.name ?? null,
|
|
452
|
+
dependencyType: item.dependencyType,
|
|
453
|
+
manifest: item.manifest
|
|
454
|
+
},
|
|
455
|
+
declaredVersion: item.declaredVersion ?? null,
|
|
456
|
+
installedVersion: dependency.installedVersion ?? null,
|
|
457
|
+
installedVersionStatus: dependency.installedVersionStatus ?? 'legacyMissing'
|
|
458
|
+
};
|
|
459
|
+
})
|
|
460
|
+
));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function missingAnalysisDecision(artifacts, item) {
|
|
464
|
+
const occurrence = structuredClone(item.occurrence);
|
|
465
|
+
const versions = {
|
|
466
|
+
declaredVersion: item.declaredVersion,
|
|
467
|
+
installedVersion: item.installedVersion ?? null,
|
|
468
|
+
installedVersionStatus: item.installedVersionStatus ?? 'legacyMissing',
|
|
469
|
+
targetVersion: null,
|
|
470
|
+
targetPolicy: 'registryLatest',
|
|
471
|
+
comparison: 'unknown',
|
|
472
|
+
adapter: null
|
|
473
|
+
};
|
|
474
|
+
const provenance = {
|
|
475
|
+
policyId: UPGRADE_DECISION_POLICY_ID,
|
|
476
|
+
policyVersion: UPGRADE_DECISION_POLICY_VERSION,
|
|
477
|
+
versionAdapter: null,
|
|
478
|
+
analysisResultId: null
|
|
479
|
+
};
|
|
480
|
+
return {
|
|
481
|
+
id: decisionId(artifacts.input, occurrence, null),
|
|
482
|
+
analysisResultId: null,
|
|
483
|
+
occurrence,
|
|
484
|
+
decision: 'NOT_ANALYZED',
|
|
485
|
+
summary: 'No Version Analysis result exists for this validated dependency occurrence.',
|
|
486
|
+
primaryReasonCode: 'VERSION_ANALYSIS_MISSING',
|
|
487
|
+
reasonCodes: ['VERSION_ANALYSIS_MISSING'],
|
|
488
|
+
versions,
|
|
489
|
+
evidence: { status: 'notEvaluated', targetScopedRefs: [] },
|
|
490
|
+
impact: {
|
|
491
|
+
status: 'NOT_ANALYZED',
|
|
492
|
+
reasonCode: null,
|
|
493
|
+
repositorySensitiveFindingCount: 0,
|
|
494
|
+
coverage: { status: 'missing', reasonCode: null }
|
|
495
|
+
},
|
|
496
|
+
requiresHumanReview: true,
|
|
497
|
+
provenance,
|
|
498
|
+
limitations: [limitation(
|
|
499
|
+
'VERSION_ANALYSIS_MISSING',
|
|
500
|
+
'The occurrence has no Version Analysis result and cannot produce an upgrade action.'
|
|
501
|
+
)]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function buildSummary(decisions) {
|
|
506
|
+
const summary = Object.fromEntries(UPGRADE_DECISIONS.map((decision) => [decision, 0]));
|
|
507
|
+
for (const record of decisions) summary[record.decision] += 1;
|
|
508
|
+
return {
|
|
509
|
+
dependencyCount: decisions.length,
|
|
510
|
+
...summary,
|
|
511
|
+
requiresHumanReviewCount: decisions.filter((record) => record.requiresHumanReview).length
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function validateUpgradeDecisionInvariants(artifact) {
|
|
516
|
+
const errors = [];
|
|
517
|
+
if (!isSorted(artifact.decisions, compareDecisions)) errors.push('decisions must be sorted.');
|
|
518
|
+
if (new Set(artifact.decisions.map((record) => record.id)).size !== artifact.decisions.length) {
|
|
519
|
+
errors.push('decision ids must be unique.');
|
|
520
|
+
}
|
|
521
|
+
const resultIds = artifact.decisions
|
|
522
|
+
.map((record) => record.analysisResultId)
|
|
523
|
+
.filter((value) => value !== null);
|
|
524
|
+
if (new Set(resultIds).size !== resultIds.length) {
|
|
525
|
+
errors.push('analysisResultIds must be unique.');
|
|
526
|
+
}
|
|
527
|
+
const expectedSummary = buildSummary(artifact.decisions);
|
|
528
|
+
if (JSON.stringify(artifact.summary) !== JSON.stringify(expectedSummary)) {
|
|
529
|
+
errors.push('summary is inconsistent.');
|
|
530
|
+
}
|
|
531
|
+
for (const record of artifact.decisions) {
|
|
532
|
+
if (!isSorted(record.reasonCodes, compareText)) {
|
|
533
|
+
errors.push(`reasonCodes for ${record.id} must be sorted.`);
|
|
534
|
+
}
|
|
535
|
+
if (record.decision === 'UPGRADE_NOW') {
|
|
536
|
+
errors.push('UPGRADE_NOW is unavailable without a structured urgency contract.');
|
|
537
|
+
}
|
|
538
|
+
if (record.id !== decisionId(artifact.input, record.occurrence, record.analysisResultId)) {
|
|
539
|
+
errors.push(`decision ${record.id} has an unstable id.`);
|
|
540
|
+
}
|
|
541
|
+
if (!record.reasonCodes.includes(record.primaryReasonCode)) {
|
|
542
|
+
errors.push(`decision ${record.id} primaryReasonCode is not present in reasonCodes.`);
|
|
543
|
+
}
|
|
544
|
+
if (!isSorted(record.evidence.targetScopedRefs, compareText)) {
|
|
545
|
+
errors.push(`evidence refs for ${record.id} must be sorted.`);
|
|
546
|
+
}
|
|
547
|
+
if (!isSorted(record.limitations, compareLimitations)) {
|
|
548
|
+
errors.push(`limitations for ${record.id} must be sorted.`);
|
|
549
|
+
}
|
|
550
|
+
if (record.decision === 'KEEP_CURRENT'
|
|
551
|
+
&& !['equal', 'installedNewer'].includes(record.versions.comparison)) {
|
|
552
|
+
errors.push(`KEEP_CURRENT ${record.id} has an invalid comparison.`);
|
|
553
|
+
}
|
|
554
|
+
if (record.decision !== 'KEEP_CURRENT' && !record.requiresHumanReview) {
|
|
555
|
+
errors.push(`decision ${record.id} must require human review.`);
|
|
556
|
+
}
|
|
557
|
+
if (record.decision === 'PLAN_UPGRADE'
|
|
558
|
+
&& (record.versions.targetPolicy !== 'explicit'
|
|
559
|
+
|| record.versions.comparison !== 'targetNewer'
|
|
560
|
+
|| record.evidence.status !== 'sufficient'
|
|
561
|
+
|| record.primaryReasonCode !== 'USER_SELECTED_TARGET'
|
|
562
|
+
|| !record.reasonCodes.includes('USER_SELECTED_TARGET'))) {
|
|
563
|
+
errors.push(`PLAN_UPGRADE ${record.id} lacks a structured user-selected target driver.`);
|
|
564
|
+
}
|
|
565
|
+
if (record.decision === 'KEEP_CURRENT'
|
|
566
|
+
&& record.versions.comparison === 'installedNewer'
|
|
567
|
+
&& !record.requiresHumanReview) {
|
|
568
|
+
errors.push(`installed-newer decision ${record.id} must require human review.`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return errors.sort(compareText);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function validateUpgradeDecision(artifact) {
|
|
575
|
+
if (artifact?.schemaVersion !== UPGRADE_DECISION_SCHEMA_VERSION) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Upgrade Decision validation error: unsupported schema version; expected ${UPGRADE_DECISION_SCHEMA_VERSION}.`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (!validateSchema(artifact)) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Upgrade Decision validation error: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const errors = validateUpgradeDecisionInvariants(artifact);
|
|
586
|
+
if (errors.length > 0) {
|
|
587
|
+
throw new Error(`Upgrade Decision validation error: ${errors.join(' ')}`);
|
|
588
|
+
}
|
|
589
|
+
return artifact;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function buildUpgradeDecision(artifacts, options = {}) {
|
|
593
|
+
const impactEvidence = new Map(artifacts.repositoryImpactEvidence.dependencies.map((dependency) => [
|
|
594
|
+
dependency.analysisResultId,
|
|
595
|
+
dependency
|
|
596
|
+
]));
|
|
597
|
+
const resultsByOccurrence = new Map(artifacts.versionAnalysis.results.map((result) => [
|
|
598
|
+
occurrenceKey({ ...result.dependency, declaredVersion: result.versions.declaredVersion }),
|
|
599
|
+
result
|
|
600
|
+
]));
|
|
601
|
+
const decisions = canonicalOccurrences(artifacts)
|
|
602
|
+
.map((item) => {
|
|
603
|
+
const result = resultsByOccurrence.get(occurrenceKey({
|
|
604
|
+
...item.occurrence,
|
|
605
|
+
declaredVersion: item.declaredVersion
|
|
606
|
+
}));
|
|
607
|
+
return result
|
|
608
|
+
? decisionRecord(artifacts, result, impactEvidence, options.adapters)
|
|
609
|
+
: missingAnalysisDecision(artifacts, item);
|
|
610
|
+
})
|
|
611
|
+
.sort(compareDecisions);
|
|
612
|
+
const artifact = {
|
|
613
|
+
schemaVersion: UPGRADE_DECISION_SCHEMA_VERSION,
|
|
614
|
+
generatedAt: artifacts.repositoryImpactEvidence.generatedAt,
|
|
615
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
616
|
+
input: structuredClone(artifacts.input),
|
|
617
|
+
policy: {
|
|
618
|
+
id: UPGRADE_DECISION_POLICY_ID,
|
|
619
|
+
version: UPGRADE_DECISION_POLICY_VERSION,
|
|
620
|
+
urgencyContract: 'unavailable'
|
|
621
|
+
},
|
|
622
|
+
summary: buildSummary(decisions),
|
|
623
|
+
decisions
|
|
624
|
+
};
|
|
625
|
+
return validateUpgradeDecision(artifact);
|
|
626
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateUpgradeDecision } from './upgrade-decision.js';
|
|
5
|
+
|
|
6
|
+
export function serializeUpgradeDecision(artifact) {
|
|
7
|
+
validateUpgradeDecision(artifact);
|
|
8
|
+
return `${JSON.stringify(artifact, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function writeUpgradeDecision(outputPath, artifact) {
|
|
12
|
+
const target = path.resolve(outputPath);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
const contents = serializeUpgradeDecision(artifact);
|
|
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
|
+
}
|