@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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/bin/depverdict.js +7 -0
  4. package/bin/upgradelens.js +7 -0
  5. package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
  6. package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
  7. package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
  8. package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
  9. package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
  10. package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
  11. package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
  12. package/docs/IA-04-CLI-Orchestration.md +235 -0
  13. package/docs/IA-05-Real-Provider-Validation.md +235 -0
  14. package/docs/IA-05-VinGrade-Validation.md +339 -0
  15. package/docs/MVP-01.md +48 -0
  16. package/docs/MVP-02-Architecture.md +536 -0
  17. package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
  18. package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
  19. package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
  20. package/docs/MVP-02-Knowledge-Manifest.md +279 -0
  21. package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
  22. package/docs/MVP-02-Knowledge-Store.md +87 -0
  23. package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
  24. package/docs/MVP-02-Research-Planning.md +196 -0
  25. package/docs/MVP-02-Source-Provenance.md +81 -0
  26. package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
  27. package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
  28. package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
  29. package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
  30. package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
  31. package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
  32. package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
  33. package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
  34. package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
  35. package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
  36. package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
  37. package/docs/VinGrade-MVP-02-Validation.md +480 -0
  38. package/docs/VinGrade-RC02-Live-Validation.md +273 -0
  39. package/docs/ai-capability-discovery.md +573 -0
  40. package/docs/ai-engineering-production-readiness.md +390 -0
  41. package/docs/ai-engineering-review.md +251 -0
  42. package/docs/ai-runtime-governance-discovery.md +754 -0
  43. package/docs/architecture-overview.md +78 -0
  44. package/docs/cli-progress.md +111 -0
  45. package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
  46. package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
  47. package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
  48. package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
  49. package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
  50. package/docs/gateway-runtime-discovery.md +614 -0
  51. package/docs/live-ai-validation.md +299 -0
  52. package/docs/migration-planning-qualification-resolution.md +99 -0
  53. package/docs/migrations/upgradelens-to-depverdict.md +97 -0
  54. package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
  55. package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
  56. package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
  57. package/docs/mvp-05-deterministic-context-runtime.md +127 -0
  58. package/docs/mvp-05-migration-checklist-contract.md +166 -0
  59. package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
  60. package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
  61. package/docs/mvp-05-provider-neutral-generator.md +79 -0
  62. package/docs/ollama-local-smoke-validation.md +156 -0
  63. package/docs/openai-compatible-runtime-discovery.md +789 -0
  64. package/docs/openrouter-one-dependency-validation.md +205 -0
  65. package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
  66. package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
  67. package/docs/package-content-policy.md +98 -0
  68. package/docs/releases/v0.5.0-technical-preview.md +114 -0
  69. package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
  70. package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
  71. package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
  72. package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
  73. package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
  74. package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
  75. package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
  76. package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
  77. package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
  78. package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
  79. package/docs/reviews/oss-03-community-scaffolding.md +378 -0
  80. package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
  81. package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
  82. package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
  83. package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
  84. package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
  85. package/docs/runtime-contract-discovery.md +521 -0
  86. package/docs/structured-output-compatibility-report.md +100 -0
  87. package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
  88. package/docs/version-analysis-architecture.md +827 -0
  89. package/eval/README.md +86 -0
  90. package/eval/datasets/generic/declared-constraint.json +59 -0
  91. package/eval/datasets/generic/evidence-conflict.json +73 -0
  92. package/eval/datasets/generic/major-breaking-release.json +66 -0
  93. package/eval/datasets/generic/missing-evidence.json +46 -0
  94. package/eval/datasets/generic/patch-release-low.json +59 -0
  95. package/eval/datasets/node/axios-patch-low.json +59 -0
  96. package/eval/datasets/node/react-major-breaking.json +66 -0
  97. package/eval/datasets/node/react-minor-compatibility.json +66 -0
  98. package/eval/datasets/python/fastapi-deprecation.json +66 -0
  99. package/eval/datasets/python/pydantic-major-breaking.json +66 -0
  100. package/eval/migration-planning/golden-dataset-v2.json +306 -0
  101. package/eval/migration-planning/golden-dataset.json +214 -0
  102. package/eval/schemas/expected-result.schema.json +88 -0
  103. package/eval/schemas/golden-case.schema.json +181 -0
  104. package/package.json +57 -0
  105. package/schemas/ai-scorecard.schema.json +132 -0
  106. package/schemas/benchmark-report.schema.json +189 -0
  107. package/schemas/benchmark.schema.json +55 -0
  108. package/schemas/capability-profile.schema.json +44 -0
  109. package/schemas/conformance-report.schema.json +150 -0
  110. package/schemas/deployment-profile.schema.json +64 -0
  111. package/schemas/evaluation-report.schema.json +158 -0
  112. package/schemas/knowledge-evidence-bundle.schema.json +150 -0
  113. package/schemas/knowledge-manifest.schema.json +548 -0
  114. package/schemas/metrics.schema.json +178 -0
  115. package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
  116. package/schemas/migration-checklist.schema.json +706 -0
  117. package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
  118. package/schemas/migration-evaluation-dataset.schema.json +204 -0
  119. package/schemas/migration-planning-qualification-record.schema.json +234 -0
  120. package/schemas/project-manifest.schema.json +308 -0
  121. package/schemas/qualification-record.schema.json +56 -0
  122. package/schemas/repository-impact-evidence.schema.json +232 -0
  123. package/schemas/repository-impact.schema.json +202 -0
  124. package/schemas/upgrade-decision.schema.json +273 -0
  125. package/schemas/usage-index.schema.json +179 -0
  126. package/schemas/version-analysis.schema.json +449 -0
  127. package/src/ai-runtime-debug.js +325 -0
  128. package/src/ai-runtime-error.js +42 -0
  129. package/src/ai-runtime.js +174 -0
  130. package/src/ai-scorecard.js +204 -0
  131. package/src/ai-version-analysis.js +484 -0
  132. package/src/artifact-root-compatibility.js +91 -0
  133. package/src/benchmark-report.js +111 -0
  134. package/src/benchmark-runner.js +191 -0
  135. package/src/canonical-json.js +69 -0
  136. package/src/cli.js +1299 -0
  137. package/src/conformance-report.js +158 -0
  138. package/src/conformance-runner.js +253 -0
  139. package/src/constants.js +73 -0
  140. package/src/cooperative-scheduler.js +79 -0
  141. package/src/dependencies.js +44 -0
  142. package/src/dependency-ai-context.js +625 -0
  143. package/src/detectors.js +253 -0
  144. package/src/discovery.js +234 -0
  145. package/src/ecosystem-version-adapter.js +294 -0
  146. package/src/environment-compatibility.js +77 -0
  147. package/src/evaluation-comparator.js +158 -0
  148. package/src/evaluation-report.js +76 -0
  149. package/src/evaluation-runner.js +248 -0
  150. package/src/evidence-source-adapter.js +472 -0
  151. package/src/files.js +89 -0
  152. package/src/governance-diagnostics.js +64 -0
  153. package/src/governance-loader.js +63 -0
  154. package/src/governance-metadata.js +346 -0
  155. package/src/governance-validator.js +360 -0
  156. package/src/http/bounded-fetch.js +278 -0
  157. package/src/http/cli-http-runtime.js +44 -0
  158. package/src/impact/input-loader.js +157 -0
  159. package/src/impact/matcher.js +40 -0
  160. package/src/impact/repository-impact.js +199 -0
  161. package/src/impact/runtime.js +24 -0
  162. package/src/impact/status.js +62 -0
  163. package/src/impact/writer.js +30 -0
  164. package/src/impact-evidence/input-loader.js +202 -0
  165. package/src/impact-evidence/repository-impact-evidence.js +234 -0
  166. package/src/impact-evidence/runtime.js +16 -0
  167. package/src/impact-evidence/writer.js +30 -0
  168. package/src/index.js +563 -0
  169. package/src/installed-version-baseline.js +196 -0
  170. package/src/knowledge-cache.js +324 -0
  171. package/src/knowledge-evidence-bundle.js +101 -0
  172. package/src/knowledge-evidence-producer.js +233 -0
  173. package/src/knowledge-manifest-builder.js +188 -0
  174. package/src/knowledge-manifest-writer.js +32 -0
  175. package/src/knowledge-manifest.js +255 -0
  176. package/src/knowledge-research.js +615 -0
  177. package/src/metrics-engine.js +205 -0
  178. package/src/migration-checklist/ai-candidate.js +320 -0
  179. package/src/migration-checklist/assembler.js +37 -0
  180. package/src/migration-checklist/context-runtime.js +828 -0
  181. package/src/migration-checklist/evaluation/action-criteria.js +244 -0
  182. package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
  183. package/src/migration-checklist/evaluation/comparator.js +279 -0
  184. package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
  185. package/src/migration-checklist/evaluation/dataset.js +336 -0
  186. package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
  187. package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
  188. package/src/migration-checklist/evaluation/metrics.js +158 -0
  189. package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
  190. package/src/migration-checklist/evaluation/qualification.js +239 -0
  191. package/src/migration-checklist/evaluation/runner-v2.js +294 -0
  192. package/src/migration-checklist/evaluation/runner.js +194 -0
  193. package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
  194. package/src/migration-checklist/evaluation/scorecard.js +86 -0
  195. package/src/migration-checklist/extractive-candidate.js +166 -0
  196. package/src/migration-checklist/extractive-prompt.js +62 -0
  197. package/src/migration-checklist/generator.js +702 -0
  198. package/src/migration-checklist/grounding-policy.js +117 -0
  199. package/src/migration-checklist/input-loader.js +613 -0
  200. package/src/migration-checklist/migration-checklist.js +635 -0
  201. package/src/migration-checklist/presentation.js +292 -0
  202. package/src/migration-checklist/progress.js +96 -0
  203. package/src/migration-checklist/prompt.js +83 -0
  204. package/src/migration-checklist/qualification-guard.js +462 -0
  205. package/src/migration-checklist/qualification-resolution.js +122 -0
  206. package/src/migration-checklist/qualification-store.js +225 -0
  207. package/src/migration-checklist/runtime.js +205 -0
  208. package/src/migration-checklist/verification.js +134 -0
  209. package/src/migration-checklist/writer.js +35 -0
  210. package/src/openai-compatible-provider.js +451 -0
  211. package/src/orchestration/failure-log.js +32 -0
  212. package/src/orchestration/pipeline.js +200 -0
  213. package/src/orchestration/progress-events.js +337 -0
  214. package/src/orchestration/progress-reporter.js +131 -0
  215. package/src/orchestration/text-writer.js +22 -0
  216. package/src/portable.js +13 -0
  217. package/src/product-completion.js +249 -0
  218. package/src/project-manifest-input.js +90 -0
  219. package/src/project-manifest.js +141 -0
  220. package/src/python-requirements.js +137 -0
  221. package/src/registry/npm-packument.js +256 -0
  222. package/src/registry/npm-registry-adapter.js +262 -0
  223. package/src/registry/pypi-project.js +300 -0
  224. package/src/registry/pypi-registry-adapter.js +235 -0
  225. package/src/registry/sanitize-registry-body.js +51 -0
  226. package/src/renderers/console.js +160 -0
  227. package/src/renderers/impact-presentation.js +278 -0
  228. package/src/renderers/markdown.js +172 -0
  229. package/src/research-plan.js +455 -0
  230. package/src/runtime-conformance.js +275 -0
  231. package/src/source-provenance.js +393 -0
  232. package/src/source-url.js +62 -0
  233. package/src/structured-output-schema.js +66 -0
  234. package/src/target-selector.js +306 -0
  235. package/src/upgrade-decision/input-loader.js +107 -0
  236. package/src/upgrade-decision/presentation.js +43 -0
  237. package/src/upgrade-decision/runtime.js +21 -0
  238. package/src/upgrade-decision/upgrade-decision.js +626 -0
  239. package/src/upgrade-decision/writer.js +30 -0
  240. package/src/usage/analyzer-registry.js +63 -0
  241. package/src/usage/coverage.js +116 -0
  242. package/src/usage/input-loader.js +139 -0
  243. package/src/usage/js/analyzer.js +240 -0
  244. package/src/usage/js/parser.js +21 -0
  245. package/src/usage/runtime.js +187 -0
  246. package/src/usage/scope.js +44 -0
  247. package/src/usage/source-files.js +50 -0
  248. package/src/usage/usage-index.js +217 -0
  249. package/src/usage/writer.js +31 -0
  250. package/src/version-analysis-loader.js +203 -0
  251. package/src/version-analysis-manifest.js +314 -0
  252. 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
+ }