@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,204 @@
1
+ import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import Ajv2020 from 'ajv/dist/2020.js';
5
+ import addFormats from 'ajv-formats';
6
+
7
+ import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
8
+ import { compareText } from './portable.js';
9
+ import { validateMetrics } from './metrics-engine.js';
10
+
11
+ export const AI_SCORECARD_SCHEMA_VERSION = '1.0.0';
12
+ export const DEFAULT_AI_SCORECARD_PATH = 'ai-scorecard.json';
13
+
14
+ const schema = JSON.parse(await readFile(
15
+ new URL('../schemas/ai-scorecard.schema.json', import.meta.url),
16
+ 'utf8'
17
+ ));
18
+ const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
19
+ addFormats(ajv);
20
+ const validateSchema = ajv.compile(schema);
21
+
22
+ const SEVERITY_ORDER = new Map([
23
+ ['high', 0],
24
+ ['medium', 1],
25
+ ['info', 2]
26
+ ]);
27
+
28
+ function score(value) {
29
+ return Math.max(0, Math.min(100, Math.round(value * 100)));
30
+ }
31
+
32
+ function average(values) {
33
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
34
+ }
35
+
36
+ function metricValues(metrics) {
37
+ return {
38
+ casePassRate: metrics.metrics.casePassRate.value,
39
+ riskClassificationAccuracy: metrics.metrics.riskClassificationAccuracy.value,
40
+ humanReviewAccuracy: metrics.metrics.humanReviewAccuracy.value,
41
+ humanReviewReasonAccuracy: metrics.metrics.humanReviewReasonAccuracy.value,
42
+ evidenceReferenceAccuracy: metrics.metrics.evidenceReferenceAccuracy.value,
43
+ evidenceCoverageAccuracy: metrics.metrics.evidenceCoverageAccuracy.value,
44
+ evidenceReferenceCoverage: metrics.metrics.evidenceReferenceCoverage.value,
45
+ unsupportedClaimRate: metrics.metrics.unsupportedClaimRate.value,
46
+ validationPassRate: metrics.metrics.validationPassRate.value,
47
+ deterministicPassRate: metrics.metrics.deterministicPassRate.value
48
+ };
49
+ }
50
+
51
+ function categoryScores(values) {
52
+ const unsupportedClaimScore = 1 - values.unsupportedClaimRate;
53
+ return {
54
+ riskAnalysis: score(values.riskClassificationAccuracy),
55
+ humanReview: score(average([
56
+ values.humanReviewAccuracy,
57
+ values.humanReviewReasonAccuracy
58
+ ])),
59
+ evidenceQuality: score(average([
60
+ values.evidenceReferenceAccuracy,
61
+ values.evidenceCoverageAccuracy,
62
+ values.evidenceReferenceCoverage,
63
+ unsupportedClaimScore
64
+ ])),
65
+ trustLayer: score(average([
66
+ values.validationPassRate,
67
+ unsupportedClaimScore,
68
+ values.humanReviewAccuracy
69
+ ])),
70
+ deterministicQuality: score(values.deterministicPassRate)
71
+ };
72
+ }
73
+
74
+ function recommendation(severity, category, code, message) {
75
+ return { severity, category, code, message };
76
+ }
77
+
78
+ function buildRecommendations(values, categories) {
79
+ const recommendations = [];
80
+ if (values.riskClassificationAccuracy < 0.9) {
81
+ recommendations.push(recommendation(
82
+ 'high',
83
+ 'Risk Analysis',
84
+ 'RISK_CLASSIFICATION_LOW',
85
+ 'Review risk prompt guidance and golden evidence coverage for risk classification misses.'
86
+ ));
87
+ }
88
+ if (values.humanReviewAccuracy < 0.95 || values.humanReviewReasonAccuracy < 0.95) {
89
+ recommendations.push(recommendation(
90
+ 'high',
91
+ 'Human Review',
92
+ 'HUMAN_REVIEW_POLICY_LOW',
93
+ 'Review deterministic human-review policy and expected review reasons.'
94
+ ));
95
+ }
96
+ if (values.evidenceReferenceAccuracy < 0.95 || values.evidenceReferenceCoverage < 0.95) {
97
+ recommendations.push(recommendation(
98
+ 'high',
99
+ 'Evidence Quality',
100
+ 'EVIDENCE_REFERENCE_LOW',
101
+ 'Improve evidence selection and reference preservation before comparing models.'
102
+ ));
103
+ }
104
+ if (values.unsupportedClaimRate > 0.05) {
105
+ recommendations.push(recommendation(
106
+ 'medium',
107
+ 'Evidence Quality',
108
+ 'UNSUPPORTED_CLAIMS_DETECTED',
109
+ 'Improve grounding and evidence selection to reduce unsupported claims.'
110
+ ));
111
+ }
112
+ if (values.validationPassRate < 0.98) {
113
+ recommendations.push(recommendation(
114
+ 'high',
115
+ 'Trust Layer',
116
+ 'VALIDATION_PASS_RATE_LOW',
117
+ 'Review structured output validation and trust validation failure modes.'
118
+ ));
119
+ }
120
+ if (values.deterministicPassRate < 1) {
121
+ recommendations.push(recommendation(
122
+ 'high',
123
+ 'Deterministic Quality',
124
+ 'DETERMINISTIC_PASS_RATE_LOW',
125
+ 'Investigate deterministic rule violations or lost evidence references.'
126
+ ));
127
+ }
128
+ if (Object.values(categories).every((value) => value >= 95)) {
129
+ recommendations.push(recommendation(
130
+ 'info',
131
+ 'General',
132
+ 'READY_FOR_BENCHMARK',
133
+ 'Quality gates are high enough to proceed to AE-04 benchmark design.'
134
+ ));
135
+ }
136
+ return recommendations.sort((left, right) =>
137
+ (SEVERITY_ORDER.get(left.severity) ?? 99) - (SEVERITY_ORDER.get(right.severity) ?? 99)
138
+ || compareText(left.category, right.category)
139
+ || compareText(left.code, right.code)
140
+ );
141
+ }
142
+
143
+ export function buildAiScorecard(metrics, { generatedAt = new Date() } = {}) {
144
+ const validated = validateMetrics(metrics);
145
+ const values = metricValues(validated);
146
+ const categories = categoryScores(values);
147
+ const overallScore = Math.round(average(Object.values(categories)));
148
+ const scorecard = {
149
+ schemaVersion: AI_SCORECARD_SCHEMA_VERSION,
150
+ generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
151
+ generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
152
+ input: {
153
+ metrics: {
154
+ schemaVersion: validated.schemaVersion,
155
+ generatedAt: validated.generatedAt,
156
+ datasetVersion: validated.input.evaluationReport.datasetVersion,
157
+ promptVersion: validated.input.evaluationReport.promptVersion,
158
+ model: structuredClone(validated.input.evaluationReport.model),
159
+ caseCount: validated.summary.totalCases
160
+ }
161
+ },
162
+ overallScore,
163
+ categoryScores: categories,
164
+ metricDetails: values,
165
+ recommendations: buildRecommendations(values, categories)
166
+ };
167
+ return validateAiScorecard(scorecard);
168
+ }
169
+
170
+ export function validateAiScorecard(scorecard) {
171
+ if (scorecard?.schemaVersion !== AI_SCORECARD_SCHEMA_VERSION) {
172
+ throw new Error(`AI Scorecard validation error: unsupported schema version; expected ${AI_SCORECARD_SCHEMA_VERSION}.`);
173
+ }
174
+ if (!validateSchema(scorecard)) {
175
+ throw new Error(`AI Scorecard validation error: schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
176
+ }
177
+ return scorecard;
178
+ }
179
+
180
+ export function serializeAiScorecard(scorecard) {
181
+ validateAiScorecard(scorecard);
182
+ return `${JSON.stringify(scorecard, null, 2)}\n`;
183
+ }
184
+
185
+ export async function writeAiScorecard(outputPath, scorecard) {
186
+ const target = path.resolve(outputPath);
187
+ const temporary = `${target}.${process.pid}.tmp`;
188
+ const contents = serializeAiScorecard(scorecard);
189
+ await mkdir(path.dirname(target), { recursive: true });
190
+ let handle;
191
+ try {
192
+ handle = await open(temporary, 'w', 0o600);
193
+ await handle.writeFile(contents, 'utf8');
194
+ await handle.sync();
195
+ await handle.close();
196
+ handle = null;
197
+ await rename(temporary, target);
198
+ } catch (error) {
199
+ await handle?.close().catch(() => {});
200
+ await rm(temporary, { force: true }).catch(() => {});
201
+ throw error;
202
+ }
203
+ return target;
204
+ }
@@ -0,0 +1,484 @@
1
+ import Ajv2020 from 'ajv/dist/2020.js';
2
+
3
+ import { AI_RUNTIME_CONTRACT_VERSION, validateAiRuntime } from './ai-runtime.js';
4
+ import { isAiRuntimeError } from './ai-runtime-error.js';
5
+ import {
6
+ BASELINE_UNSUPPORTED_WARNING_CODE,
7
+ MISSING_TARGET_WARNING_CODE
8
+ } from './dependency-ai-context.js';
9
+ import { compareText } from './portable.js';
10
+
11
+ export const AI_VERSION_ANALYSIS_RESULT_VERSION = '1';
12
+ export const VERSION_ANALYSIS_PROMPT_VERSION = '1';
13
+ export const VERSION_ANALYSIS_TASK = 'version-analysis.v1';
14
+ export const VERSION_ANALYSIS_SCHEMA_NAME = 'upgradelens_version_analysis';
15
+
16
+ const SAFE_RUNTIME_FAILURE_SUMMARIES = Object.freeze({
17
+ CONFIGURATION_ERROR: 'AI runtime configuration is invalid.',
18
+ AUTH_ERROR: 'AI provider rejected authentication.',
19
+ ACCESS_DENIED: 'AI provider denied access to the requested resource.',
20
+ INSUFFICIENT_CREDIT: 'AI provider reported insufficient credit.',
21
+ INVALID_REQUEST: 'AI provider rejected the request parameters.',
22
+ MODEL_NOT_FOUND: 'Configured AI model was not found.',
23
+ NETWORK_ERROR: 'AI provider network request failed.',
24
+ TIMEOUT: 'AI provider request timed out.',
25
+ CANCELLED: 'AI provider request was cancelled.',
26
+ RATE_LIMITED: 'AI provider rate limit was reached.',
27
+ PROVIDER_UNAVAILABLE: 'AI provider is temporarily unavailable.',
28
+ SCHEMA_REJECTED: 'AI provider rejected the required output schema.',
29
+ STRUCTURED_OUTPUT_UNSUPPORTED: 'AI provider does not support the required structured output mode.',
30
+ INVALID_RESPONSE: 'AI provider returned an invalid response.',
31
+ OUTPUT_TRUNCATED: 'AI provider output was truncated.',
32
+ CONTENT_REFUSED: 'AI provider refused the requested content.',
33
+ RESPONSE_TOO_LARGE: 'AI provider response exceeded the configured size limit.',
34
+ PROVIDER_ERROR: 'AI provider returned an unsuccessful response.',
35
+ IDENTITY_MISMATCH: 'AI provider returned an unexpected model identity.',
36
+ UNKNOWN: 'AI runtime returned an unknown failure.'
37
+ });
38
+
39
+ export const AI_VERSION_ANALYSIS_CANDIDATE_SCHEMA = {
40
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
41
+ type: 'object',
42
+ additionalProperties: false,
43
+ required: ['summary', 'summaryEvidenceRefs', 'riskLevel', 'riskEvidenceRefs', 'findings'],
44
+ properties: {
45
+ summary: { type: 'string', minLength: 1 },
46
+ summaryEvidenceRefs: {
47
+ type: 'array',
48
+ uniqueItems: true,
49
+ items: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' }
50
+ },
51
+ riskLevel: { enum: ['low', 'medium', 'high', 'unknown'] },
52
+ riskEvidenceRefs: {
53
+ type: 'array',
54
+ uniqueItems: true,
55
+ items: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' }
56
+ },
57
+ findings: {
58
+ type: 'array',
59
+ items: {
60
+ type: 'object',
61
+ additionalProperties: false,
62
+ required: ['id', 'kind', 'summary', 'appliesToVersions', 'evidenceRefs'],
63
+ properties: {
64
+ id: { type: 'string', minLength: 1 },
65
+ kind: { enum: ['breakingChange', 'deprecation', 'compatibility'] },
66
+ summary: { type: 'string', minLength: 1 },
67
+ appliesToVersions: {
68
+ type: 'array',
69
+ uniqueItems: true,
70
+ items: { type: 'string', minLength: 1 }
71
+ },
72
+ evidenceRefs: {
73
+ type: 'array',
74
+ minItems: 1,
75
+ uniqueItems: true,
76
+ items: { type: 'string', pattern: '^sha256:[a-f0-9]{64}$' }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ };
83
+
84
+ const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
85
+ const validateCandidateSchema = ajv.compile(AI_VERSION_ANALYSIS_CANDIDATE_SCHEMA);
86
+
87
+ const REVIEW_REASON_ORDER = [
88
+ 'HIGH_RISK',
89
+ 'UNKNOWN_RISK',
90
+ 'EVIDENCE_NONE',
91
+ 'EVIDENCE_PARTIAL',
92
+ 'SOURCE_STALE',
93
+ 'SOURCE_CONFLICT',
94
+ 'VERSION_UNCERTAIN',
95
+ 'CLAIMS_DROPPED',
96
+ 'ANALYSIS_FAILED'
97
+ ];
98
+
99
+ function sortedUnique(values) {
100
+ return [...new Set(values)].sort((left, right) => {
101
+ const leftIndex = REVIEW_REASON_ORDER.indexOf(left);
102
+ const rightIndex = REVIEW_REASON_ORDER.indexOf(right);
103
+ if (leftIndex !== -1 || rightIndex !== -1) {
104
+ return (leftIndex === -1 ? 999 : leftIndex) - (rightIndex === -1 ? 999 : rightIndex);
105
+ }
106
+ return compareText(left, right);
107
+ });
108
+ }
109
+
110
+ function selectedEvidenceIds(context) {
111
+ return new Set(context.metadata?.selectedEvidenceIds ?? context.knowledge.evidence.map((item) => item.id));
112
+ }
113
+
114
+ function selectedSourceUrls(context) {
115
+ return new Set(context.knowledge.evidence.map((item) => item.sourceUrl).filter(Boolean));
116
+ }
117
+
118
+ function materialUrls(value) {
119
+ if (typeof value !== 'string') return [];
120
+ return value.match(/https?:\/\/[^\s)"']+/g) ?? [];
121
+ }
122
+
123
+ function filterRefs(refs, allowed) {
124
+ const valid = [];
125
+ const invalid = [];
126
+ for (const ref of refs ?? []) {
127
+ if (allowed.has(ref)) valid.push(ref);
128
+ else invalid.push(ref);
129
+ }
130
+ return {
131
+ valid: [...new Set(valid)].sort(compareText),
132
+ invalid: [...new Set(invalid)].sort(compareText)
133
+ };
134
+ }
135
+
136
+ function unsupportedUrlFound(candidate, context) {
137
+ const allowedUrls = selectedSourceUrls(context);
138
+ const urls = [
139
+ ...materialUrls(candidate.summary),
140
+ ...candidate.findings.flatMap((finding) => materialUrls(finding.summary))
141
+ ];
142
+ return urls.some((url) => !allowedUrls.has(url));
143
+ }
144
+
145
+ function contextHasWarning(context, code) {
146
+ return (context.metadata?.warnings ?? []).some((warning) => warning.code === code);
147
+ }
148
+
149
+ function contextValidationWarningCodes(context) {
150
+ const codes = [];
151
+ if (contextHasWarning(context, 'SOURCE_CONFLICT')) codes.push('SOURCE_CONFLICT');
152
+ return codes;
153
+ }
154
+
155
+ function addLimitation(limitations, code, message) {
156
+ if (!limitations.some((item) => item.code === code)) {
157
+ limitations.push({ code, message });
158
+ }
159
+ }
160
+
161
+ function emptyClaims(context, status, validationStatus, warningCodes, limitationCode, message) {
162
+ const riskLevel = 'unknown';
163
+ const validationWarningCodes = sortedUnique([
164
+ ...warningCodes,
165
+ ...contextValidationWarningCodes(context)
166
+ ]);
167
+ const humanReview = humanReviewPolicy({
168
+ context,
169
+ riskLevel,
170
+ evidenceCoverage: context.knowledge.evidence.length === 0 ? 'none' : 'partial',
171
+ validationWarningCodes,
172
+ status
173
+ });
174
+ return {
175
+ resultVersion: AI_VERSION_ANALYSIS_RESULT_VERSION,
176
+ status,
177
+ contextId: context.contextId,
178
+ dependency: structuredClone(context.dependency),
179
+ versions: structuredClone(context.versions),
180
+ summary: message,
181
+ summaryEvidenceRefs: [],
182
+ riskLevel,
183
+ riskEvidenceRefs: [],
184
+ findings: [],
185
+ evidenceCoverage: context.knowledge.evidence.length === 0 ? 'none' : 'partial',
186
+ validation: {
187
+ status: validationStatus,
188
+ warningCodes: validationWarningCodes
189
+ },
190
+ requiresHumanReview: humanReview.requiresHumanReview,
191
+ humanReviewReasons: humanReview.humanReviewReasons,
192
+ nextAction: status === 'skipped' && validationWarningCodes.includes(BASELINE_UNSUPPORTED_WARNING_CODE)
193
+ ? 'resolveCurrentVersion'
194
+ : status === 'skipped' && validationWarningCodes.includes(MISSING_TARGET_WARNING_CODE)
195
+ ? 'provideExplicitTarget'
196
+ : status === 'skipped'
197
+ ? 'collectEvidence'
198
+ : 'retryAnalysis',
199
+ limitations: [{ code: limitationCode, message }]
200
+ };
201
+ }
202
+
203
+ function parseRuntimeOutput(output) {
204
+ if (typeof output !== 'string') return output;
205
+ try {
206
+ return JSON.parse(output);
207
+ } catch {
208
+ throw new Error('INVALID_JSON');
209
+ }
210
+ }
211
+
212
+ function validateStructuredOutput(output) {
213
+ if (!validateCandidateSchema(output)) {
214
+ throw new Error(ajv.errorsText(validateCandidateSchema.errors, { separator: '; ' }));
215
+ }
216
+ return output;
217
+ }
218
+
219
+ export function buildVersionAnalysisPrompt({
220
+ context,
221
+ outputSchema = AI_VERSION_ANALYSIS_CANDIDATE_SCHEMA,
222
+ promptVersion = VERSION_ANALYSIS_PROMPT_VERSION
223
+ }) {
224
+ return {
225
+ promptVersion,
226
+ system: [
227
+ 'You are UpgradeLens AI Version Analysis.',
228
+ 'Analyze dependency release-level risk using only the supplied Dependency AI Context.',
229
+ 'Do not infer source-code impact, migration plans, installed versions, URLs, or evidence.',
230
+ 'Return only JSON that satisfies the structured output schema.'
231
+ ].join('\n'),
232
+ user: [
233
+ 'Rules:',
234
+ '- Use only evidence items present in context.knowledge.evidence.',
235
+ '- Cite evidence only by existing evidence id.',
236
+ '- Do not create URLs, source ids, evidence ids, versions, or package facts.',
237
+ '- Do not guess currentVersion; if it is null, treat the baseline as unresolved.',
238
+ '- Do not analyze source code and do not propose a migration plan.',
239
+ '- Keep the analysis generic across package ecosystems.',
240
+ '',
241
+ 'Structured output schema:',
242
+ JSON.stringify(outputSchema),
243
+ '',
244
+ 'Dependency AI Context:',
245
+ JSON.stringify(context)
246
+ ].join('\n')
247
+ };
248
+ }
249
+
250
+ export function trustValidateAiVersionAnalysisCandidate(candidate, context) {
251
+ const allowed = selectedEvidenceIds(context);
252
+ const warningCodes = [];
253
+ const limitations = [];
254
+ let summary = candidate.summary;
255
+ let riskLevel = candidate.riskLevel;
256
+
257
+ const summaryRefs = filterRefs(candidate.summaryEvidenceRefs, allowed);
258
+ const riskRefs = filterRefs(candidate.riskEvidenceRefs, allowed);
259
+ let findings = candidate.findings.map((finding) => {
260
+ const refs = filterRefs(finding.evidenceRefs, allowed);
261
+ if (refs.invalid.length > 0) warningCodes.push('EVIDENCE_REFERENCE_INVALID');
262
+ if (refs.valid.length === 0) {
263
+ warningCodes.push('CLAIMS_DROPPED');
264
+ return null;
265
+ }
266
+ return {
267
+ id: finding.id,
268
+ kind: finding.kind,
269
+ summary: finding.summary,
270
+ appliesToVersions: [...finding.appliesToVersions].sort(compareText),
271
+ evidenceRefs: refs.valid
272
+ };
273
+ }).filter(Boolean);
274
+
275
+ if (summaryRefs.invalid.length > 0 || riskRefs.invalid.length > 0) {
276
+ warningCodes.push('EVIDENCE_REFERENCE_INVALID');
277
+ }
278
+ if (summaryRefs.valid.length === 0) {
279
+ summary = 'AI summary was removed because it was not supported by selected evidence.';
280
+ warningCodes.push('CLAIMS_DROPPED');
281
+ }
282
+ if (riskLevel !== 'unknown' && riskRefs.valid.length === 0) {
283
+ riskLevel = 'unknown';
284
+ warningCodes.push('CLAIMS_DROPPED');
285
+ }
286
+ if (unsupportedUrlFound(candidate, context)) {
287
+ summary = 'AI claims were limited because the candidate introduced a URL outside selected evidence.';
288
+ riskLevel = 'unknown';
289
+ findings = [];
290
+ warningCodes.push('INVENTED_URL', 'CLAIMS_DROPPED');
291
+ }
292
+ if (contextHasWarning(context, 'SOURCE_CONFLICT')) {
293
+ if (riskLevel !== 'unknown') riskLevel = 'unknown';
294
+ warningCodes.push('SOURCE_CONFLICT');
295
+ addLimitation(
296
+ limitations,
297
+ 'SOURCE_CONFLICT',
298
+ 'Risk was downgraded because selected evidence has unresolved source conflicts.'
299
+ );
300
+ }
301
+ if (warningCodes.includes('CLAIMS_DROPPED')) {
302
+ addLimitation(
303
+ limitations,
304
+ 'CLAIMS_DROPPED',
305
+ 'One or more AI claims were removed or downgraded because selected evidence did not support them.'
306
+ );
307
+ }
308
+ if (warningCodes.includes('EVIDENCE_REFERENCE_INVALID')) {
309
+ addLimitation(
310
+ limitations,
311
+ 'EVIDENCE_REFERENCE_INVALID',
312
+ 'The AI candidate referenced evidence outside the selected context.'
313
+ );
314
+ }
315
+ if (warningCodes.includes('INVENTED_URL')) {
316
+ addLimitation(
317
+ limitations,
318
+ 'INVENTED_URL',
319
+ 'The AI candidate introduced a URL that was not present in selected evidence.'
320
+ );
321
+ }
322
+
323
+ const evidenceCoverage = context.knowledge.evidence.length === 0
324
+ ? 'none'
325
+ : warningCodes.length > 0 || context.metadata.warnings.length > 0
326
+ ? 'partial'
327
+ : 'sufficient';
328
+ const validationWarningCodes = sortedUnique(warningCodes);
329
+ const humanReview = humanReviewPolicy({
330
+ context,
331
+ riskLevel,
332
+ evidenceCoverage,
333
+ validationWarningCodes,
334
+ status: 'analyzed'
335
+ });
336
+
337
+ return {
338
+ resultVersion: AI_VERSION_ANALYSIS_RESULT_VERSION,
339
+ status: 'analyzed',
340
+ contextId: context.contextId,
341
+ dependency: structuredClone(context.dependency),
342
+ versions: structuredClone(context.versions),
343
+ summary,
344
+ summaryEvidenceRefs: summaryRefs.valid,
345
+ riskLevel,
346
+ riskEvidenceRefs: riskLevel === 'unknown' ? [] : riskRefs.valid,
347
+ findings,
348
+ evidenceCoverage,
349
+ validation: {
350
+ status: validationWarningCodes.length > 0 ? 'validWithWarnings' : 'valid',
351
+ warningCodes: validationWarningCodes
352
+ },
353
+ requiresHumanReview: humanReview.requiresHumanReview,
354
+ humanReviewReasons: humanReview.humanReviewReasons,
355
+ nextAction: humanReview.requiresHumanReview ? 'reviewBeforeImpactAnalysis' : 'proceedToImpactAnalysis',
356
+ limitations
357
+ };
358
+ }
359
+
360
+ export function humanReviewPolicy({
361
+ context,
362
+ riskLevel,
363
+ evidenceCoverage,
364
+ validationWarningCodes = [],
365
+ status = 'analyzed'
366
+ }) {
367
+ const reasons = [];
368
+ if (status !== 'analyzed') reasons.push('ANALYSIS_FAILED');
369
+ if (riskLevel === 'high') reasons.push('HIGH_RISK');
370
+ if (riskLevel === 'unknown') reasons.push('UNKNOWN_RISK');
371
+ if (evidenceCoverage === 'none') reasons.push('EVIDENCE_NONE');
372
+ if (evidenceCoverage === 'partial') reasons.push('EVIDENCE_PARTIAL');
373
+ if (['declaredConstraint', 'unsupportedBaseline'].includes(context.versions.analysisMode)) {
374
+ reasons.push('VERSION_UNCERTAIN');
375
+ }
376
+ for (const warning of context.metadata.warnings ?? []) {
377
+ if (warning.code === 'SOURCE_STALE') reasons.push('SOURCE_STALE');
378
+ if (warning.code === 'SOURCE_CONFLICT') reasons.push('SOURCE_CONFLICT');
379
+ if (warning.code === 'EVIDENCE_MISSING') reasons.push('EVIDENCE_NONE');
380
+ if (warning.code === BASELINE_UNSUPPORTED_WARNING_CODE) reasons.push('VERSION_UNCERTAIN');
381
+ }
382
+ if (validationWarningCodes.some((code) =>
383
+ ['CLAIMS_DROPPED', 'EVIDENCE_REFERENCE_INVALID', 'INVENTED_URL'].includes(code)
384
+ )) {
385
+ reasons.push('CLAIMS_DROPPED');
386
+ }
387
+ return {
388
+ requiresHumanReview: reasons.length > 0,
389
+ humanReviewReasons: sortedUnique(reasons)
390
+ };
391
+ }
392
+
393
+ export async function analyzeDependencyAiContext(context, {
394
+ runtime,
395
+ signal,
396
+ runId = `run:${context.contextId}`,
397
+ promptVersion = VERSION_ANALYSIS_PROMPT_VERSION,
398
+ outputSchema = AI_VERSION_ANALYSIS_CANDIDATE_SCHEMA
399
+ } = {}) {
400
+ if ((context.metadata?.missingInformation ?? []).includes('baseline')
401
+ || contextHasWarning(context, BASELINE_UNSUPPORTED_WARNING_CODE)) {
402
+ return emptyClaims(
403
+ context,
404
+ 'skipped',
405
+ 'validWithWarnings',
406
+ [BASELINE_UNSUPPORTED_WARNING_CODE],
407
+ BASELINE_UNSUPPORTED_WARNING_CODE,
408
+ 'AI analysis was skipped because no supported current-version baseline was available.'
409
+ );
410
+ }
411
+ if ((context.metadata?.missingInformation ?? []).includes('targetVersion')
412
+ || contextHasWarning(context, MISSING_TARGET_WARNING_CODE)) {
413
+ return emptyClaims(
414
+ context,
415
+ 'skipped',
416
+ 'validWithWarnings',
417
+ [MISSING_TARGET_WARNING_CODE],
418
+ MISSING_TARGET_WARNING_CODE,
419
+ 'AI analysis was skipped because no target version was available. Provide an explicit target or collect target evidence.'
420
+ );
421
+ }
422
+ if (context.knowledge.evidence.length === 0) {
423
+ return emptyClaims(
424
+ context,
425
+ 'skipped',
426
+ 'validWithWarnings',
427
+ ['EVIDENCE_MISSING'],
428
+ 'EVIDENCE_MISSING',
429
+ 'AI analysis was skipped because no selected evidence was available.'
430
+ );
431
+ }
432
+ validateAiRuntime(runtime);
433
+
434
+ const prompt = buildVersionAnalysisPrompt({ context, outputSchema, promptVersion });
435
+ let runtimeResult;
436
+ try {
437
+ runtimeResult = await runtime.generateStructured({
438
+ contractVersion: AI_RUNTIME_CONTRACT_VERSION,
439
+ runId,
440
+ contextId: context.contextId,
441
+ task: VERSION_ANALYSIS_TASK,
442
+ promptVersion,
443
+ systemPrompt: prompt.system,
444
+ userPrompt: prompt.user,
445
+ structuredOutput: {
446
+ mode: 'jsonSchema',
447
+ name: VERSION_ANALYSIS_SCHEMA_NAME,
448
+ schema: outputSchema
449
+ },
450
+ ...(signal ? { signal } : {})
451
+ });
452
+ } catch (error) {
453
+ const code = isAiRuntimeError(error) ? error.code : 'UNKNOWN';
454
+ const status = isAiRuntimeError(error) && Number.isInteger(error.status)
455
+ ? ` (HTTP ${error.status})`
456
+ : '';
457
+ const summary = SAFE_RUNTIME_FAILURE_SUMMARIES[code] ?? SAFE_RUNTIME_FAILURE_SUMMARIES.UNKNOWN;
458
+ return emptyClaims(
459
+ context,
460
+ 'failed',
461
+ 'invalid',
462
+ [code],
463
+ code,
464
+ `AI runtime failed with ${code}${status}: ${summary}`
465
+ );
466
+ }
467
+
468
+ try {
469
+ const candidate = validateStructuredOutput(parseRuntimeOutput(runtimeResult.output));
470
+ return trustValidateAiVersionAnalysisCandidate(candidate, context);
471
+ } catch (error) {
472
+ const code = error.message === 'INVALID_JSON' ? 'OUTPUT_JSON_INVALID' : 'OUTPUT_SCHEMA_INVALID';
473
+ return emptyClaims(
474
+ context,
475
+ 'failed',
476
+ 'invalid',
477
+ [code],
478
+ code,
479
+ code === 'OUTPUT_JSON_INVALID'
480
+ ? 'AI runtime returned invalid JSON.'
481
+ : `AI runtime output failed structured validation: ${error.message}`
482
+ );
483
+ }
484
+ }