@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,249 @@
1
+ import { compareText } from './portable.js';
2
+
3
+ export const PRODUCT_COMPLETION_SCHEMA_VERSION = '1.0.0';
4
+ export const PRODUCT_COMPLETION_STATUSES = Object.freeze([
5
+ 'COMPLETED',
6
+ 'COMPLETED_WITH_REVIEW',
7
+ 'PARTIAL',
8
+ 'INSUFFICIENT_DATA',
9
+ 'FAILED',
10
+ 'CANCELLED'
11
+ ]);
12
+
13
+ const HANDOFF_STATUSES = Object.freeze([
14
+ 'NO_VERSION_CHANGE_REQUIRED',
15
+ 'ACTIONABLE_WITH_REVIEW',
16
+ 'INVESTIGATION_REQUIRED',
17
+ 'INSUFFICIENT_EVIDENCE',
18
+ 'NOT_ANALYZED',
19
+ 'ACTION_GENERATION_FAILED',
20
+ 'NO_GROUNDED_ACTION'
21
+ ]);
22
+
23
+ function deepFreeze(value) {
24
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
25
+ for (const child of Object.values(value)) deepFreeze(child);
26
+ return Object.freeze(value);
27
+ }
28
+
29
+ function handoffByDecision(migrationChecklistViewModel) {
30
+ return new Map((migrationChecklistViewModel?.dependencies ?? []).map((dependency) => [
31
+ dependency.decisionId,
32
+ dependency
33
+ ]));
34
+ }
35
+
36
+ function analysisById(versionAnalysis) {
37
+ return new Map((versionAnalysis?.results ?? []).map((result) => [result.id, result]));
38
+ }
39
+
40
+ function failureCodes(record, analysis) {
41
+ return [...new Set([
42
+ ...record.reasonCodes,
43
+ ...(analysis?.humanReviewReasons ?? []),
44
+ ...(analysis?.limitations ?? []).map((item) => item.code)
45
+ ])].sort(compareText);
46
+ }
47
+
48
+ function failureRecovery(codes) {
49
+ if (codes.includes('INSUFFICIENT_CREDIT')) {
50
+ return 'Add credit or change the configured provider, then rerun the failed dependency analysis.';
51
+ }
52
+ if (codes.some((code) => code.includes('RATE_LIMIT'))) {
53
+ return 'Wait for the configured provider rate limit to clear, then rerun the failed dependency analysis.';
54
+ }
55
+ if (codes.some((code) => /OUTPUT|SCHEMA|REJECT/.test(code))) {
56
+ return 'The provider output was rejected and was not published; review or rerun the failed dependency analysis.';
57
+ }
58
+ if (codes.some((code) => /TIMEOUT|PROVIDER|RUNTIME/.test(code))) {
59
+ return 'Resolve the configured provider/runtime failure, then rerun the failed dependency analysis.';
60
+ }
61
+ return 'Review the failed Version Analysis occurrence and rerun it after resolving the recorded limitation.';
62
+ }
63
+
64
+ function nextStepFor(record, handoff) {
65
+ if (handoff?.nextStep?.message) return handoff.nextStep.message;
66
+ if (record.decision === 'KEEP_CURRENT') return 'No version change is required for this target.';
67
+ if (record.decision === 'PLAN_UPGRADE' || record.decision === 'UPGRADE_NOW') {
68
+ return 'Review the evidence-bounded migration handoff before changing source or dependency declarations.';
69
+ }
70
+ if (record.decision === 'INVESTIGATE') {
71
+ if (record.primaryReasonCode === 'UPGRADE_AVAILABLE_NO_RECOMMENDATION_DRIVER') {
72
+ return 'A newer version is available, but no upgrade target has been selected; use --target to select one.';
73
+ }
74
+ return 'Complete the stated human investigation before selecting migration work.';
75
+ }
76
+ if (record.primaryReasonCode === 'INSTALLED_VERSION_UNAVAILABLE') {
77
+ return 'Resolve the installed version baseline and rerun analysis.';
78
+ }
79
+ if (record.primaryReasonCode === 'TARGET_VERSION_UNAVAILABLE') {
80
+ return 'Refresh research when external verification is available or select a validated explicit target.';
81
+ }
82
+ if (record.decision === 'INSUFFICIENT_EVIDENCE') {
83
+ return 'Collect valid target-scoped evidence and rerun analysis.';
84
+ }
85
+ if (record.primaryReasonCode === 'VERSION_ANALYSIS_SKIPPED') {
86
+ return 'Resolve the missing baseline, target, or evidence and rerun Version Analysis.';
87
+ }
88
+ return 'Resolve the failed Version Analysis occurrence and rerun it.';
89
+ }
90
+
91
+ function coverageLabel(record) {
92
+ const coverage = record.impact.coverage.status;
93
+ const values = {
94
+ complete: record.impact.status === 'IMPACTED' ? 'Impacted; complete coverage' : 'Complete coverage',
95
+ partial: 'Partial coverage',
96
+ unavailable: 'Coverage unavailable',
97
+ failed: 'Coverage failed',
98
+ missing: 'Coverage unavailable'
99
+ };
100
+ return values[coverage] ?? 'Coverage unknown';
101
+ }
102
+
103
+ function compareDecisions(left, right) {
104
+ return compareText(left.projectId, right.projectId)
105
+ || compareText(left.manifest, right.manifest)
106
+ || compareText(left.packageId, right.packageId)
107
+ || compareText(left.dependencyType, right.dependencyType)
108
+ || compareText(left.id, right.id);
109
+ }
110
+
111
+ function overallNextStep(status, decisions) {
112
+ if (status === 'PARTIAL') {
113
+ return 'Resolve the listed provider/output/runtime failures and rerun the failed occurrences.';
114
+ }
115
+ if (status === 'INSUFFICIENT_DATA') {
116
+ return 'Resolve missing installed baselines or target-scoped evidence, then rerun analysis.';
117
+ }
118
+ if (status === 'COMPLETED_WITH_REVIEW') {
119
+ if (decisions.some((item) => item.handoffStatus === 'ACTIONABLE_WITH_REVIEW')) {
120
+ return 'Review and approve the evidence-bounded migration handoff before source changes.';
121
+ }
122
+ if (decisions.some((item) => (
123
+ item.decision === 'INVESTIGATE'
124
+ && item.nextStep.includes('--target')
125
+ ))) {
126
+ return 'Review investigation results; use --target for any dependency you choose to plan.';
127
+ }
128
+ return 'Review the listed dependency decisions and coverage limitations.';
129
+ }
130
+ return 'No additional version change is required for the evaluated targets.';
131
+ }
132
+
133
+ export function buildProductCompletion({
134
+ upgradeDecision,
135
+ versionAnalysis = null,
136
+ migrationChecklistViewModel = null,
137
+ artifactPaths = {}
138
+ }) {
139
+ if (!upgradeDecision?.summary || !Array.isArray(upgradeDecision.decisions)) {
140
+ throw new Error('Product completion requires a persisted Upgrade Decision artifact.');
141
+ }
142
+ const handoffs = handoffByDecision(migrationChecklistViewModel);
143
+ const analyses = analysisById(versionAnalysis);
144
+ const decisions = upgradeDecision.decisions.map((record) => {
145
+ const handoff = handoffs.get(record.id) ?? null;
146
+ return {
147
+ id: record.id,
148
+ analysisResultId: record.analysisResultId,
149
+ projectId: record.occurrence.projectId,
150
+ manifest: record.occurrence.manifest,
151
+ dependencyType: record.occurrence.dependencyType,
152
+ packageId: record.occurrence.packageId,
153
+ dependency: record.occurrence.declaredName,
154
+ installedVersion: record.versions.installedVersion,
155
+ targetVersion: record.versions.targetVersion,
156
+ targetOrigin: record.versions.targetPolicy,
157
+ decision: record.decision,
158
+ explanation: record.summary,
159
+ coverage: coverageLabel(record),
160
+ handoffStatus: handoff?.handoffStatus ?? null,
161
+ nextStep: nextStepFor(record, handoff),
162
+ requiresHumanReview: record.requiresHumanReview
163
+ };
164
+ }).sort(compareDecisions);
165
+ const failedOccurrences = upgradeDecision.decisions.flatMap((record) => {
166
+ const handoff = handoffs.get(record.id);
167
+ const versionFailed = record.reasonCodes.includes('VERSION_ANALYSIS_FAILED');
168
+ const actionFailed = handoff?.handoffStatus === 'ACTION_GENERATION_FAILED';
169
+ if (!versionFailed && !actionFailed) return [];
170
+ const analysis = analyses.get(record.analysisResultId);
171
+ const codes = failureCodes(record, analysis);
172
+ return [{
173
+ decisionId: record.id,
174
+ projectId: record.occurrence.projectId,
175
+ manifest: record.occurrence.manifest,
176
+ dependencyType: record.occurrence.dependencyType,
177
+ packageId: record.occurrence.packageId,
178
+ dependency: record.occurrence.declaredName,
179
+ stage: actionFailed ? 'Migration Checklist' : 'Version Analysis',
180
+ failureType: actionFailed ? 'ACTION_GENERATION_FAILED' : 'VERSION_ANALYSIS_FAILED',
181
+ recovery: actionFailed
182
+ ? 'Review the rejected or failed migration action manually, then rerun Migration Checklist.'
183
+ : failureRecovery(codes)
184
+ }];
185
+ }).sort(compareDecisions);
186
+ const explicitInsufficient = upgradeDecision.decisions.some((record) => (
187
+ record.versions.targetPolicy === 'explicit'
188
+ && record.decision === 'INSUFFICIENT_EVIDENCE'
189
+ ));
190
+ const unansweredCount = upgradeDecision.decisions.filter((record) => (
191
+ record.decision === 'INSUFFICIENT_EVIDENCE'
192
+ || (record.decision === 'NOT_ANALYZED'
193
+ && !record.reasonCodes.includes('VERSION_ANALYSIS_FAILED'))
194
+ )).length;
195
+ const majorityUnanswered = upgradeDecision.decisions.length > 0
196
+ && unansweredCount * 2 > upgradeDecision.decisions.length;
197
+ const hasReview = upgradeDecision.decisions.some((record) => (
198
+ record.requiresHumanReview
199
+ || ['PLAN_UPGRADE', 'UPGRADE_NOW', 'INVESTIGATE'].includes(record.decision)
200
+ || record.impact.coverage.status !== 'complete'
201
+ ));
202
+ const actionGenerationFailed = [...handoffs.values()].some((handoff) => (
203
+ handoff.handoffStatus === 'ACTION_GENERATION_FAILED'
204
+ ));
205
+ const status = failedOccurrences.length > 0 || actionGenerationFailed
206
+ ? 'PARTIAL'
207
+ : explicitInsufficient || majorityUnanswered
208
+ ? 'INSUFFICIENT_DATA'
209
+ : hasReview
210
+ ? 'COMPLETED_WITH_REVIEW'
211
+ : 'COMPLETED';
212
+ const handoffCounts = Object.fromEntries(HANDOFF_STATUSES.map((value) => [
213
+ value,
214
+ migrationChecklistViewModel?.summary?.handoffStatusCounts?.[value] ?? 0
215
+ ]));
216
+ const output = {
217
+ schemaVersion: PRODUCT_COMPLETION_SCHEMA_VERSION,
218
+ status,
219
+ nextStep: overallNextStep(status, decisions),
220
+ decisionCounts: {
221
+ KEEP_CURRENT: upgradeDecision.summary.KEEP_CURRENT,
222
+ UPGRADE_NOW: upgradeDecision.summary.UPGRADE_NOW,
223
+ PLAN_UPGRADE: upgradeDecision.summary.PLAN_UPGRADE,
224
+ INVESTIGATE: upgradeDecision.summary.INVESTIGATE,
225
+ INSUFFICIENT_EVIDENCE: upgradeDecision.summary.INSUFFICIENT_EVIDENCE,
226
+ NOT_ANALYZED: upgradeDecision.summary.NOT_ANALYZED
227
+ },
228
+ handoffCounts,
229
+ reviewRequiredCount: upgradeDecision.summary.requiresHumanReviewCount,
230
+ failedOccurrences,
231
+ decisions,
232
+ artifactPaths: {
233
+ report: artifactPaths.report ?? null,
234
+ upgradeDecision: artifactPaths.upgradeDecision ?? null,
235
+ migrationChecklist: artifactPaths.migrationChecklist ?? null
236
+ }
237
+ };
238
+ return deepFreeze(output);
239
+ }
240
+
241
+ export function productCompletionExitCode(completion, { strict = false } = {}) {
242
+ if (completion.status === 'FAILED') return 1;
243
+ if (completion.status === 'CANCELLED') return 130;
244
+ if (completion.status === 'PARTIAL') return 2;
245
+ if (strict && ['COMPLETED_WITH_REVIEW', 'INSUFFICIENT_DATA'].includes(completion.status)) {
246
+ return 2;
247
+ }
248
+ return 0;
249
+ }
@@ -0,0 +1,90 @@
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 { DEFAULT_MANIFEST_PATH, MANIFEST_SCHEMA_VERSION } from './constants.js';
8
+ import { isPortableRelativePath } from './portable.js';
9
+ import { validateProjectManifestInvariants } from './project-manifest.js';
10
+
11
+ const schema = JSON.parse(await readFile(
12
+ new URL('../schemas/project-manifest.schema.json', import.meta.url),
13
+ 'utf8'
14
+ ));
15
+ const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
16
+ addFormats(ajv);
17
+ const validateSchema = ajv.compile(schema);
18
+
19
+ function inputError(message) {
20
+ return new Error(`Project Manifest input error: ${message}`);
21
+ }
22
+
23
+ function digest(bytes) {
24
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
25
+ }
26
+
27
+ function parseManifest(bytes) {
28
+ try {
29
+ return JSON.parse(Buffer.from(bytes).toString('utf8'));
30
+ } catch {
31
+ throw inputError('manifest bytes are not valid JSON.');
32
+ }
33
+ }
34
+
35
+ function artifactFor(source, options) {
36
+ const artifact = typeof source === 'object' && source !== null && 'bytes' in source
37
+ ? source.artifact
38
+ : options.artifact ?? DEFAULT_MANIFEST_PATH;
39
+ if (!isPortableRelativePath(artifact)) {
40
+ throw inputError('artifact must be a portable repository-relative path.');
41
+ }
42
+ return artifact;
43
+ }
44
+
45
+ async function readInputBytes(source) {
46
+ if (typeof source === 'string' || source instanceof URL) return readFile(source);
47
+ if (source && typeof source === 'object' && 'bytes' in source) {
48
+ if (!(source.bytes instanceof Uint8Array)) {
49
+ throw inputError('bytes must be a Uint8Array or Buffer.');
50
+ }
51
+ return Buffer.from(source.bytes);
52
+ }
53
+ throw inputError('source must be a manifest file path or { bytes, artifact }.');
54
+ }
55
+
56
+ /**
57
+ * Read one exact Project Manifest byte sequence, validate it, and return its
58
+ * parsed value together with portable lineage for research planning.
59
+ */
60
+ export async function loadProjectManifestInput(source, options = {}) {
61
+ const artifact = artifactFor(source, options);
62
+ const bytes = await readInputBytes(source);
63
+ const manifest = parseManifest(bytes);
64
+
65
+ if (manifest?.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
66
+ throw inputError(`unsupported schema version; expected ${MANIFEST_SCHEMA_VERSION}.`);
67
+ }
68
+ if (!validateSchema(manifest)) {
69
+ throw inputError(`schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
70
+ }
71
+ const invariantErrors = validateProjectManifestInvariants(manifest);
72
+ if (invariantErrors.length > 0) {
73
+ throw inputError(`runtime invariants failed: ${invariantErrors.join(' ')}`);
74
+ }
75
+
76
+ return {
77
+ manifest,
78
+ input: {
79
+ projectManifest: {
80
+ schemaVersion: manifest.schemaVersion,
81
+ artifact,
82
+ artifactDigest: digest(bytes),
83
+ repository: {
84
+ name: manifest.repository.name,
85
+ root: manifest.repository.root
86
+ }
87
+ }
88
+ }
89
+ };
90
+ }
@@ -0,0 +1,141 @@
1
+ import { compareText, isPortableRelativePath } from './portable.js';
2
+
3
+ const NODE_DEPENDENCY_TYPES = {
4
+ dependency: 'dependencies',
5
+ devDependency: 'devDependencies',
6
+ optionalDependency: 'optionalDependencies',
7
+ peerDependency: 'peerDependencies'
8
+ };
9
+
10
+ function addMismatch(errors, field, actual, expected) {
11
+ if (actual !== expected) errors.push(`${field} is ${actual}; expected ${expected}.`);
12
+ }
13
+
14
+ /**
15
+ * Validate relationships in a Project Manifest that JSON Schema cannot compare.
16
+ * Input ordering is intentionally not required here because research planning
17
+ * canonicalizes its own output independently of manifest array order.
18
+ */
19
+ export function validateProjectManifestInvariants(manifest) {
20
+ const errors = [];
21
+ const projects = manifest.projects ?? [];
22
+ const ecosystems = {};
23
+ const projectIds = new Set();
24
+
25
+ for (const project of projects) {
26
+ if (projectIds.has(project.id)) errors.push(`Duplicate project id ${project.id}.`);
27
+ projectIds.add(project.id);
28
+ ecosystems[project.ecosystem] = (ecosystems[project.ecosystem] ?? 0) + 1;
29
+ if (!isPortableRelativePath(project.path)) {
30
+ errors.push(`Project ${project.id} has a non-portable path.`);
31
+ }
32
+ for (const manifestPath of project.manifests) {
33
+ if (!isPortableRelativePath(manifestPath)) {
34
+ errors.push(`Project ${project.id} has a non-portable manifest path.`);
35
+ }
36
+ }
37
+ if (project.workspace && !isPortableRelativePath(project.workspace.root)) {
38
+ errors.push(`Project ${project.id} has a non-portable workspace root.`);
39
+ }
40
+
41
+ const dependencies = project.dependencies;
42
+ for (const dependency of dependencies) {
43
+ if (!isPortableRelativePath(dependency.manifest)) {
44
+ errors.push(`Project ${project.id} has a dependency with a non-portable manifest path.`);
45
+ }
46
+ const baselineFields = [
47
+ 'installedVersion',
48
+ 'installedVersionStatus',
49
+ 'installedVersionSource',
50
+ 'installedVersionReason'
51
+ ];
52
+ const baselineFieldCount = baselineFields.filter((field) => field in dependency).length;
53
+ if (baselineFieldCount !== 0 && baselineFieldCount !== baselineFields.length) {
54
+ errors.push(`Project ${project.id} dependency ${dependency.name} has an incomplete installed-version baseline.`);
55
+ }
56
+ if (baselineFieldCount === baselineFields.length) {
57
+ if (dependency.installedVersionStatus === 'resolved') {
58
+ if (
59
+ typeof dependency.installedVersion !== 'string'
60
+ || dependency.installedVersionSource === null
61
+ || dependency.installedVersionReason !== null
62
+ ) {
63
+ errors.push(`Project ${project.id} dependency ${dependency.name} has inconsistent resolved baseline fields.`);
64
+ }
65
+ } else if (
66
+ dependency.installedVersionStatus === 'unresolved'
67
+ && (
68
+ dependency.installedVersion !== null
69
+ || dependency.installedVersionSource !== null
70
+ || dependency.installedVersionReason === null
71
+ )
72
+ ) {
73
+ errors.push(`Project ${project.id} dependency ${dependency.name} has inconsistent unresolved baseline fields.`);
74
+ }
75
+ if (
76
+ dependency.installedVersionSource
77
+ && (
78
+ !isPortableRelativePath(dependency.installedVersionSource.path)
79
+ || !isPortableRelativePath(dependency.installedVersionSource.packagePath)
80
+ )
81
+ ) {
82
+ errors.push(`Project ${project.id} dependency ${dependency.name} has non-portable installed-version provenance.`);
83
+ }
84
+ }
85
+ }
86
+
87
+ if (project.dependencySummary.status !== 'parsed') {
88
+ if (dependencies.length !== 0) {
89
+ errors.push(`Project ${project.id} has dependencies with a non-parsed dependency summary.`);
90
+ }
91
+ continue;
92
+ }
93
+
94
+ const summary = project.dependencySummary;
95
+ const uniqueCount = new Set(dependencies.map((dependency) => dependency.normalizedName)).size;
96
+ addMismatch(errors, `${project.id}.dependencySummary.declarationCount`, summary.declarationCount, dependencies.length);
97
+ addMismatch(errors, `${project.id}.dependencySummary.uniqueCount`, summary.uniqueCount, uniqueCount);
98
+ addMismatch(
99
+ errors,
100
+ `${project.id}.dependencySummary.duplicateCount`,
101
+ summary.duplicateCount,
102
+ dependencies.length - uniqueCount
103
+ );
104
+
105
+ if (summary.byType) {
106
+ const byType = {
107
+ dependencies: 0,
108
+ devDependencies: 0,
109
+ peerDependencies: 0,
110
+ optionalDependencies: 0
111
+ };
112
+ for (const dependency of dependencies) {
113
+ const key = NODE_DEPENDENCY_TYPES[dependency.type];
114
+ if (key) byType[key] += 1;
115
+ }
116
+ for (const [type, count] of Object.entries(byType)) {
117
+ addMismatch(errors, `${project.id}.dependencySummary.byType.${type}`, summary.byType[type], count);
118
+ }
119
+ }
120
+ }
121
+
122
+ addMismatch(errors, 'summary.projectCount', manifest.summary.projectCount, projects.length);
123
+ addMismatch(
124
+ errors,
125
+ 'summary.workspaceCount',
126
+ manifest.summary.workspaceCount,
127
+ projects.filter((project) => project.workspace?.role === 'root').length
128
+ );
129
+
130
+ const actualEcosystemKeys = Object.keys(ecosystems).sort(compareText);
131
+ const summaryEcosystemKeys = Object.keys(manifest.summary.ecosystems ?? []).sort(compareText);
132
+ if (actualEcosystemKeys.join('\0') !== summaryEcosystemKeys.join('\0')) {
133
+ errors.push('summary.ecosystems does not match project ecosystems.');
134
+ } else {
135
+ for (const ecosystem of actualEcosystemKeys) {
136
+ addMismatch(errors, `summary.ecosystems.${ecosystem}`, manifest.summary.ecosystems[ecosystem], ecosystems[ecosystem]);
137
+ }
138
+ }
139
+
140
+ return errors.sort(compareText);
141
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ duplicateDependencyNames,
3
+ sortDependencies,
4
+ summarizeDependencies
5
+ } from './dependencies.js';
6
+
7
+ const PACKAGE_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?/;
8
+ const EXTRAS = /^\[([A-Za-z0-9._-]+(?:\s*,\s*[A-Za-z0-9._-]+)*)\]/;
9
+ const VERSION_SPECIFIERS = /^(?:===|==|~=|!=|>=|<=|>|<)\s*[^\s,]+(?:\s*,\s*(?:===|==|~=|!=|>=|<=|>|<)\s*[^\s,]+)*$/;
10
+
11
+ export function normalizePythonPackageName(name) {
12
+ return name.toLowerCase().replace(/[-_.]+/g, '-');
13
+ }
14
+
15
+ function stripInlineComment(line) {
16
+ for (let index = 0; index < line.length; index += 1) {
17
+ if (line[index] === '#' && (index === 0 || /\s/.test(line[index - 1]))) {
18
+ return line.slice(0, index).trim();
19
+ }
20
+ }
21
+ return line.trim();
22
+ }
23
+
24
+ function packageNameFromEgg(reference) {
25
+ const encoded = reference.match(/[#&]egg=([^&\s]+)/i)?.[1];
26
+ if (!encoded) return undefined;
27
+ let egg;
28
+ try {
29
+ egg = decodeURIComponent(encoded);
30
+ } catch {
31
+ egg = encoded;
32
+ }
33
+ return egg.match(PACKAGE_NAME)?.[0];
34
+ }
35
+
36
+ function isUnnamedReference(value) {
37
+ return /^(?:[A-Za-z][A-Za-z0-9+.-]*:|git\+|\.\.?[/\\]|[/\\])/.test(value);
38
+ }
39
+
40
+ function dependency(name, declaredVersion, type, normalizeName = true) {
41
+ return {
42
+ name,
43
+ normalizedName: normalizeName ? normalizePythonPackageName(name) : name,
44
+ declaredVersion,
45
+ type
46
+ };
47
+ }
48
+
49
+ function parseNamedRequirement(value) {
50
+ const nameMatch = value.match(PACKAGE_NAME);
51
+ if (!nameMatch) return undefined;
52
+ const name = nameMatch[0];
53
+ let remainder = value.slice(name.length);
54
+
55
+ if (remainder.startsWith('[')) {
56
+ const extrasMatch = remainder.match(EXTRAS);
57
+ if (!extrasMatch) return undefined;
58
+ remainder = remainder.slice(extrasMatch[0].length);
59
+ }
60
+
61
+ remainder = remainder.trim();
62
+ const markerIndex = remainder.indexOf(';');
63
+ if (markerIndex >= 0) {
64
+ const marker = remainder.slice(markerIndex + 1).trim();
65
+ if (!marker) return undefined;
66
+ remainder = remainder.slice(0, markerIndex).trim();
67
+ }
68
+
69
+ if (remainder.startsWith('@')) {
70
+ const reference = remainder.slice(1).trim();
71
+ return reference ? dependency(name, reference, 'directReference') : undefined;
72
+ }
73
+ if (!remainder || VERSION_SPECIFIERS.test(remainder)) {
74
+ return dependency(name, remainder || null, 'runtime');
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ function parseRequirementLine(value) {
80
+ if (/^(?:-r|--requirement)(?:\s+|=)(\S.*)$/.test(value)) return { kind: 'directive' };
81
+ if (/^(?:-c|--constraint)(?:\s+|=)(\S.*)$/.test(value)) return { kind: 'directive' };
82
+ if (/^(?:-r|--requirement|-c|--constraint)$/.test(value)) {
83
+ return { kind: 'invalid', reason: 'include or constraint directive is missing a path' };
84
+ }
85
+
86
+ const editable = value.match(/^(?:-e|--editable)(?:\s+|=)(\S.*)$/);
87
+ if (editable) {
88
+ const target = editable[1];
89
+ const named = packageNameFromEgg(target) ?? parseNamedRequirement(target)?.name;
90
+ return {
91
+ kind: 'dependency',
92
+ dependency: dependency(named ?? target, target, 'editable', named !== undefined)
93
+ };
94
+ }
95
+ if (/^(?:-e|--editable)$/.test(value)) {
96
+ return { kind: 'invalid', reason: 'editable directive is missing a target' };
97
+ }
98
+
99
+ if (/^--(?:index-url|extra-index-url)(?:\s+|=)/.test(value)) return { kind: 'option' };
100
+ if (/^--[A-Za-z0-9-]+(?:\s|=|$)/.test(value)) return { kind: 'option' };
101
+ if (/^-[A-Za-z]/.test(value)) return { kind: 'option' };
102
+
103
+ if (isUnnamedReference(value)) {
104
+ const name = packageNameFromEgg(value);
105
+ return {
106
+ kind: 'dependency',
107
+ dependency: dependency(name ?? value, value, 'directReference', name !== undefined)
108
+ };
109
+ }
110
+
111
+ const parsed = parseNamedRequirement(value);
112
+ return parsed
113
+ ? { kind: 'dependency', dependency: parsed }
114
+ : { kind: 'invalid', reason: 'unsupported or malformed requirement' };
115
+ }
116
+
117
+ export function parseRequirementsTxt(contents) {
118
+ const dependencies = [];
119
+ const issues = [];
120
+ const lines = contents.split(/\r?\n/);
121
+
122
+ for (let index = 0; index < lines.length; index += 1) {
123
+ const value = stripInlineComment(lines[index]);
124
+ if (!value) continue;
125
+ const result = parseRequirementLine(value);
126
+ if (result.kind === 'dependency') dependencies.push(result.dependency);
127
+ else if (result.kind === 'invalid') issues.push({ line: index + 1, reason: result.reason });
128
+ }
129
+
130
+ const sortedDependencies = sortDependencies(dependencies);
131
+ return {
132
+ dependencies: sortedDependencies,
133
+ dependencySummary: summarizeDependencies(sortedDependencies),
134
+ duplicateNames: duplicateDependencyNames(sortedDependencies),
135
+ issues
136
+ };
137
+ }