@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,462 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { canonicalJson, canonicalJsonBytes } from '../canonical-json.js';
4
+ import { compareText } from '../portable.js';
5
+ import {
6
+ loadMigrationEvaluationDatasetV2
7
+ } from './evaluation/dataset-v2.js';
8
+ import {
9
+ migrationActionEvaluationCriteriaDigest,
10
+ migrationActionEvaluationCriteriaIdentity
11
+ } from './evaluation/action-criteria.js';
12
+ import {
13
+ MIGRATION_EXTRACTIVE_GENERATOR_TRUST_SOURCE_IDENTITY,
14
+ MIGRATION_EXTRACTIVE_QUALIFICATION_POLICY_V2_VERSION,
15
+ migrationExtractiveQualificationPolicyV2Digest
16
+ } from './evaluation/qualification-v2.js';
17
+ import {
18
+ MIGRATION_EXTRACTIVE_PRESENTATION,
19
+ migrationExtractiveCandidateSchemaDigest
20
+ } from './extractive-candidate.js';
21
+ import {
22
+ MIGRATION_EXTRACTIVE_PLANNING_TASK,
23
+ MIGRATION_EXTRACTIVE_PROMPT_VERSION,
24
+ migrationExtractivePromptDigest
25
+ } from './extractive-prompt.js';
26
+ import {
27
+ buildMigrationPlanningQualificationRecord
28
+ } from './qualification-store.js';
29
+
30
+ export const MIGRATION_QUALIFICATION_STATES = Object.freeze([
31
+ 'QUALIFIED',
32
+ 'QUALIFIED_WITH_LIMITATIONS',
33
+ 'NOT_QUALIFIED',
34
+ 'MISSING',
35
+ 'IDENTITY_MISMATCH',
36
+ 'CORRUPTED',
37
+ 'INSUFFICIENT_EVIDENCE'
38
+ ]);
39
+
40
+ export class MigrationQualificationError extends Error {
41
+ constructor(code, message, decision = null) {
42
+ super(message);
43
+ this.name = 'MigrationQualificationError';
44
+ this.code = code;
45
+ this.decision = decision;
46
+ }
47
+ }
48
+
49
+ function deepFreeze(value) {
50
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
51
+ for (const child of Object.values(value)) deepFreeze(child);
52
+ return Object.freeze(value);
53
+ }
54
+
55
+ function limitation(code, message) {
56
+ return { code, message };
57
+ }
58
+
59
+ function sortedUniqueLimitations(values) {
60
+ const unique = new Map();
61
+ for (const item of values) unique.set(`${item.code}\0${item.message}`, structuredClone(item));
62
+ return [...unique.values()].sort((left, right) => (
63
+ compareText(left.code, right.code) || compareText(left.message, right.message)
64
+ ));
65
+ }
66
+
67
+ export function normalizedMigrationRuntimeMetadata(runtimeMetadata = {}) {
68
+ return {
69
+ provider: typeof runtimeMetadata.provider === 'string' && runtimeMetadata.provider.length > 0
70
+ ? runtimeMetadata.provider : 'unknown',
71
+ model: typeof runtimeMetadata.model === 'string' && runtimeMetadata.model.length > 0
72
+ ? runtimeMetadata.model : 'unknown',
73
+ adapter: typeof runtimeMetadata.adapter === 'string' && runtimeMetadata.adapter.length > 0
74
+ ? runtimeMetadata.adapter : 'unknown'
75
+ };
76
+ }
77
+
78
+ function identityMatches(actual, expected) {
79
+ if (!actual) return false;
80
+ for (const field of [
81
+ 'task', 'datasetId', 'datasetVersion', 'datasetDigest',
82
+ 'evaluationCriteriaId', 'evaluationCriteriaVersion', 'evaluationCriteriaDigest',
83
+ 'comparatorVersion', 'normalizationVersion',
84
+ 'policyVersion', 'policyDigest', 'promptVersion', 'promptDigest',
85
+ 'candidateSchemaDigest', 'deterministicPresentationIdentity'
86
+ ]) {
87
+ if (actual[field] !== expected[field]) return false;
88
+ }
89
+ if (canonicalJson(actual.generatorTrustSourceIdentity) !== canonicalJson(expected.generatorTrustSourceIdentity)) {
90
+ return false;
91
+ }
92
+ return actual.runtime?.mode === 'real'
93
+ && actual.runtime.provider === expected.runtime.provider
94
+ && actual.runtime.model === expected.runtime.model
95
+ && actual.runtime.adapter === expected.runtime.adapter
96
+ && canonicalJson(actual.runtime.observedProviders)
97
+ === canonicalJson(expected.runtime.observedProviders)
98
+ && canonicalJson(actual.runtime.observedModels)
99
+ === canonicalJson(expected.runtime.observedModels);
100
+ }
101
+
102
+ export function migrationQualificationIdentityDigest(identity) {
103
+ return `sha256:${createHash('sha256').update(canonicalJsonBytes(identity)).digest('hex')}`;
104
+ }
105
+
106
+ function experimentalBaseLimitations() {
107
+ return [
108
+ limitation(
109
+ 'EXPERIMENTAL_MIGRATION_CHECKLIST',
110
+ 'Migration Checklist is experimental and every generated instruction requires human review.'
111
+ ),
112
+ limitation(
113
+ 'EXTRACTIVE_SEMANTIC_APPLICABILITY_NOT_VERIFIED',
114
+ 'Exact selected guidance proves provenance and structural safety, not semantic applicability to this repository.'
115
+ )
116
+ ];
117
+ }
118
+
119
+ async function expectedQualificationIdentity(runtimeMetadata) {
120
+ const runtime = normalizedMigrationRuntimeMetadata(runtimeMetadata);
121
+ const dataset = await loadMigrationEvaluationDatasetV2();
122
+ const criteria = migrationActionEvaluationCriteriaIdentity();
123
+ return {
124
+ task: MIGRATION_EXTRACTIVE_PLANNING_TASK,
125
+ datasetId: dataset.datasetId,
126
+ datasetVersion: dataset.schemaVersion,
127
+ datasetDigest: dataset.datasetDigest,
128
+ evaluationCriteriaId: criteria.evaluationCriteriaId,
129
+ evaluationCriteriaVersion: criteria.evaluationCriteriaVersion,
130
+ evaluationCriteriaDigest: migrationActionEvaluationCriteriaDigest(),
131
+ comparatorVersion: criteria.comparatorVersion,
132
+ normalizationVersion: criteria.normalizationVersion,
133
+ policyVersion: MIGRATION_EXTRACTIVE_QUALIFICATION_POLICY_V2_VERSION,
134
+ policyDigest: migrationExtractiveQualificationPolicyV2Digest(),
135
+ promptVersion: MIGRATION_EXTRACTIVE_PROMPT_VERSION,
136
+ promptDigest: migrationExtractivePromptDigest(),
137
+ candidateSchemaDigest: migrationExtractiveCandidateSchemaDigest(),
138
+ generatorTrustSourceIdentity: MIGRATION_EXTRACTIVE_GENERATOR_TRUST_SOURCE_IDENTITY,
139
+ deterministicPresentationIdentity: MIGRATION_EXTRACTIVE_PRESENTATION,
140
+ runtime: {
141
+ mode: 'real',
142
+ ...runtime,
143
+ observedProviders: [runtime.provider],
144
+ observedModels: [runtime.model]
145
+ }
146
+ };
147
+ }
148
+
149
+ function qualificationDecision({
150
+ status,
151
+ reasonCode,
152
+ executionAllowed,
153
+ experimentalOverrideUsed,
154
+ qualificationId,
155
+ identity,
156
+ runtimeIdentity,
157
+ recordRuntimeIdentity = null,
158
+ sourceKind,
159
+ sourcePath = null,
160
+ limitations,
161
+ nextAction
162
+ }) {
163
+ return deepFreeze({
164
+ status,
165
+ state: status,
166
+ reasonCode,
167
+ executionAllowed,
168
+ experimentalOverrideUsed,
169
+ qualificationId,
170
+ task: MIGRATION_EXTRACTIVE_PLANNING_TASK,
171
+ identity,
172
+ runtimeIdentity: structuredClone(runtimeIdentity),
173
+ recordRuntimeIdentity: recordRuntimeIdentity
174
+ ? {
175
+ provider: recordRuntimeIdentity.provider,
176
+ model: recordRuntimeIdentity.model,
177
+ adapter: recordRuntimeIdentity.adapter
178
+ }
179
+ : null,
180
+ sourceKind,
181
+ sourcePath,
182
+ limitations: sortedUniqueLimitations(limitations),
183
+ nextAction
184
+ });
185
+ }
186
+
187
+ function blockedMessage(decision) {
188
+ if (decision.status === 'NOT_QUALIFIED') {
189
+ return 'The matching provider/model failed a critical migration-planning.v2 qualification gate.';
190
+ }
191
+ if (decision.status === 'IDENTITY_MISMATCH') {
192
+ return 'The persisted Migration Planning qualification does not match the current runtime identity.';
193
+ }
194
+ if (decision.status === 'CORRUPTED') {
195
+ return 'The selected Migration Planning qualification source failed schema or integrity validation.';
196
+ }
197
+ if (decision.status === 'INSUFFICIENT_EVIDENCE') {
198
+ return 'The matching Migration Planning qualification has insufficient evidence for execution.';
199
+ }
200
+ return 'A matching real-provider qualification is required for migration-planning.v2.';
201
+ }
202
+
203
+ export function migrationQualificationErrorForDecision(decision) {
204
+ return new MigrationQualificationError(
205
+ decision.reasonCode,
206
+ blockedMessage(decision),
207
+ decision
208
+ );
209
+ }
210
+
211
+ export async function createMigrationQualificationSourceFailureDecision({
212
+ status,
213
+ reasonCode,
214
+ runtimeMetadata,
215
+ sourceKind,
216
+ sourcePath = null,
217
+ limitationCode,
218
+ limitationMessage,
219
+ nextAction
220
+ }) {
221
+ const identity = await expectedQualificationIdentity(runtimeMetadata);
222
+ return qualificationDecision({
223
+ status,
224
+ reasonCode,
225
+ executionAllowed: false,
226
+ experimentalOverrideUsed: false,
227
+ qualificationId: null,
228
+ identity,
229
+ runtimeIdentity: identity.runtime,
230
+ sourceKind,
231
+ sourcePath,
232
+ limitations: [
233
+ ...experimentalBaseLimitations(),
234
+ limitation(limitationCode, limitationMessage)
235
+ ],
236
+ nextAction
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Produce one immutable source-of-truth decision for the exact migration task/runtime identity.
242
+ * Only a missing record may use the explicit experimental execution exception.
243
+ */
244
+ export async function decideMigrationQualification({
245
+ qualification = null,
246
+ runtimeMetadata,
247
+ allowExperimental = false,
248
+ sourceKind = qualification ? 'injected' : 'none',
249
+ sourcePath = null
250
+ } = {}) {
251
+ const expectedIdentity = await expectedQualificationIdentity(runtimeMetadata);
252
+ const runtime = expectedIdentity.runtime;
253
+
254
+ let identityDigestValid = !qualification;
255
+ if (qualification) {
256
+ try {
257
+ identityDigestValid = qualification.qualificationId
258
+ === migrationQualificationIdentityDigest(qualification.identity);
259
+ } catch {
260
+ identityDigestValid = false;
261
+ }
262
+ }
263
+ if (!identityDigestValid) {
264
+ return qualificationDecision({
265
+ status: 'CORRUPTED',
266
+ reasonCode: 'MIGRATION_QUALIFICATION_IDENTITY_CORRUPT',
267
+ executionAllowed: false,
268
+ experimentalOverrideUsed: false,
269
+ qualificationId: null,
270
+ identity: expectedIdentity,
271
+ runtimeIdentity: runtime,
272
+ recordRuntimeIdentity: qualification.identity?.runtime,
273
+ sourceKind,
274
+ sourcePath,
275
+ limitations: [
276
+ ...experimentalBaseLimitations(),
277
+ limitation(
278
+ 'MIGRATION_QUALIFICATION_IDENTITY_CORRUPT',
279
+ 'The Migration Planning qualification identity digest is invalid.'
280
+ )
281
+ ],
282
+ nextAction: 'REPLACE_QUALIFICATION_RECORD'
283
+ });
284
+ }
285
+ if (qualification) {
286
+ try {
287
+ qualification = buildMigrationPlanningQualificationRecord(qualification).qualification;
288
+ } catch (error) {
289
+ return qualificationDecision({
290
+ status: 'CORRUPTED',
291
+ reasonCode: error?.code ?? 'MIGRATION_QUALIFICATION_RECORD_INVALID',
292
+ executionAllowed: false,
293
+ experimentalOverrideUsed: false,
294
+ qualificationId: null,
295
+ identity: expectedIdentity,
296
+ runtimeIdentity: runtime,
297
+ recordRuntimeIdentity: qualification.identity?.runtime,
298
+ sourceKind,
299
+ sourcePath,
300
+ limitations: [
301
+ ...experimentalBaseLimitations(),
302
+ limitation(
303
+ error?.code ?? 'MIGRATION_QUALIFICATION_RECORD_INVALID',
304
+ 'The Migration Planning qualification failed strict schema or invariant validation.'
305
+ )
306
+ ],
307
+ nextAction: 'REPLACE_QUALIFICATION_RECORD'
308
+ });
309
+ }
310
+ }
311
+
312
+ const matches = identityMatches(qualification?.identity, expectedIdentity);
313
+ if (matches && qualification?.verdict === 'NOT_QUALIFIED') {
314
+ return qualificationDecision({
315
+ status: 'NOT_QUALIFIED',
316
+ reasonCode: 'MIGRATION_RUNTIME_NOT_QUALIFIED',
317
+ executionAllowed: false,
318
+ experimentalOverrideUsed: false,
319
+ qualificationId: qualification.qualificationId,
320
+ identity: expectedIdentity,
321
+ runtimeIdentity: runtime,
322
+ recordRuntimeIdentity: qualification.identity.runtime,
323
+ sourceKind,
324
+ sourcePath,
325
+ limitations: [
326
+ ...experimentalBaseLimitations(),
327
+ ...(qualification.limitations ?? []).map(({ code, message }) => ({ code, message })),
328
+ limitation(
329
+ 'MIGRATION_RUNTIME_NOT_QUALIFIED',
330
+ 'The matching provider/model failed a critical migration-planning.v2 qualification gate.'
331
+ )
332
+ ],
333
+ nextAction: 'REQUALIFY_RUNTIME'
334
+ });
335
+ }
336
+ if (matches && qualification?.verdict === 'INSUFFICIENT_EVIDENCE') {
337
+ return qualificationDecision({
338
+ status: 'INSUFFICIENT_EVIDENCE',
339
+ reasonCode: 'MIGRATION_QUALIFICATION_INSUFFICIENT',
340
+ executionAllowed: false,
341
+ experimentalOverrideUsed: false,
342
+ qualificationId: qualification.qualificationId,
343
+ identity: expectedIdentity,
344
+ runtimeIdentity: runtime,
345
+ recordRuntimeIdentity: qualification.identity.runtime,
346
+ sourceKind,
347
+ sourcePath,
348
+ limitations: [
349
+ ...experimentalBaseLimitations(),
350
+ ...(qualification.limitations ?? []).map(({ code, message }) => ({ code, message })),
351
+ limitation(
352
+ 'MIGRATION_QUALIFICATION_INSUFFICIENT',
353
+ 'The matching qualification has insufficient real-provider evidence.'
354
+ )
355
+ ],
356
+ nextAction: 'REQUALIFY_RUNTIME'
357
+ });
358
+ }
359
+
360
+ const realQualified = matches && ['QUALIFIED', 'QUALIFIED_WITH_LIMITATIONS'].includes(
361
+ qualification?.verdict
362
+ );
363
+ if (realQualified) {
364
+ return qualificationDecision({
365
+ status: qualification.verdict,
366
+ reasonCode: 'MIGRATION_QUALIFICATION_MATCHED',
367
+ executionAllowed: true,
368
+ experimentalOverrideUsed: false,
369
+ qualificationId: qualification.qualificationId,
370
+ identity: expectedIdentity,
371
+ runtimeIdentity: runtime,
372
+ recordRuntimeIdentity: qualification.identity.runtime,
373
+ sourceKind,
374
+ sourcePath,
375
+ limitations: sortedUniqueLimitations([
376
+ ...experimentalBaseLimitations(),
377
+ ...(qualification.limitations ?? []).map(({ code, message }) => ({ code, message }))
378
+ ]),
379
+ nextAction: 'NONE'
380
+ });
381
+ }
382
+
383
+ const limitations = experimentalBaseLimitations();
384
+ if (!qualification) {
385
+ limitations.push(limitation(
386
+ 'MIGRATION_PROVIDER_NOT_QUALIFIED',
387
+ 'The configured provider/model has not been qualified for migration-planning.v2.'
388
+ ));
389
+ if (Object.values(normalizedMigrationRuntimeMetadata(runtimeMetadata)).includes('unknown')) {
390
+ limitations.push(limitation(
391
+ 'MIGRATION_RUNTIME_IDENTITY_INCOMPLETE',
392
+ 'Provider, model, or runtime adapter metadata is incomplete; this run cannot establish qualification identity.'
393
+ ));
394
+ }
395
+ return qualificationDecision({
396
+ status: 'MISSING',
397
+ reasonCode: allowExperimental
398
+ ? 'MIGRATION_QUALIFICATION_MISSING_EXPERIMENTAL_OVERRIDE'
399
+ : 'MIGRATION_QUALIFICATION_REQUIRED',
400
+ executionAllowed: allowExperimental,
401
+ experimentalOverrideUsed: allowExperimental,
402
+ qualificationId: null,
403
+ identity: expectedIdentity,
404
+ runtimeIdentity: runtime,
405
+ sourceKind,
406
+ sourcePath,
407
+ limitations,
408
+ nextAction: allowExperimental
409
+ ? 'INSTALL_QUALIFICATION_RECORD_OR_REVIEW_EXPERIMENTAL_OUTPUT'
410
+ : 'INSTALL_QUALIFICATION_RECORD'
411
+ });
412
+ }
413
+
414
+ if (qualification.identity?.runtime?.mode === 'fake') {
415
+ limitations.push(limitation(
416
+ 'FAKE_QUALIFICATION_NOT_REAL_PROVIDER',
417
+ 'Fake-runtime qualification does not qualify the configured real provider/model.'
418
+ ));
419
+ return qualificationDecision({
420
+ status: 'IDENTITY_MISMATCH',
421
+ reasonCode: 'MIGRATION_FAKE_QUALIFICATION_FOR_REAL_RUNTIME',
422
+ executionAllowed: false,
423
+ experimentalOverrideUsed: false,
424
+ qualificationId: qualification.qualificationId ?? null,
425
+ identity: expectedIdentity,
426
+ runtimeIdentity: runtime,
427
+ recordRuntimeIdentity: qualification.identity.runtime,
428
+ sourceKind,
429
+ sourcePath,
430
+ limitations,
431
+ nextAction: 'INSTALL_MATCHING_REAL_QUALIFICATION_RECORD'
432
+ });
433
+ }
434
+
435
+ limitations.push(limitation(
436
+ 'MIGRATION_QUALIFICATION_IDENTITY_MISMATCH',
437
+ 'The qualification does not match the current task, provider, model, adapter, dataset, prompt, schema, or policy identity.'
438
+ ));
439
+ return qualificationDecision({
440
+ status: 'IDENTITY_MISMATCH',
441
+ reasonCode: 'MIGRATION_QUALIFICATION_IDENTITY_MISMATCH',
442
+ executionAllowed: false,
443
+ experimentalOverrideUsed: false,
444
+ qualificationId: qualification.qualificationId ?? null,
445
+ identity: expectedIdentity,
446
+ runtimeIdentity: runtime,
447
+ recordRuntimeIdentity: qualification.identity?.runtime,
448
+ sourceKind,
449
+ sourcePath,
450
+ limitations,
451
+ nextAction: 'INSTALL_MATCHING_QUALIFICATION_RECORD'
452
+ });
453
+ }
454
+
455
+ /**
456
+ * Backward-compatible guard API: allowed decisions are returned; blocked decisions remain errors.
457
+ */
458
+ export async function evaluateMigrationQualification(options = {}) {
459
+ const decision = await decideMigrationQualification(options);
460
+ if (!decision.executionAllowed) throw migrationQualificationErrorForDecision(decision);
461
+ return decision;
462
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH
3
+ } from '../constants.js';
4
+ import { resolveArtifactRootChain } from '../artifact-root-compatibility.js';
5
+ import {
6
+ createMigrationQualificationSourceFailureDecision,
7
+ decideMigrationQualification
8
+ } from './qualification-guard.js';
9
+ import {
10
+ buildMigrationPlanningQualificationRecord,
11
+ loadMigrationPlanningQualificationRecord
12
+ } from './qualification-store.js';
13
+
14
+ function sourceErrorDetails(error, explicitPath) {
15
+ if (error?.code === 'MIGRATION_QUALIFICATION_RECORD_MISSING') {
16
+ return {
17
+ status: 'MISSING',
18
+ reasonCode: explicitPath
19
+ ? 'MIGRATION_QUALIFICATION_EXPLICIT_SOURCE_MISSING'
20
+ : 'MIGRATION_QUALIFICATION_RECORD_MISSING',
21
+ limitationCode: explicitPath
22
+ ? 'MIGRATION_QUALIFICATION_EXPLICIT_SOURCE_MISSING'
23
+ : 'MIGRATION_PROVIDER_NOT_QUALIFIED',
24
+ limitationMessage: explicitPath
25
+ ? 'The explicitly selected Migration Planning qualification record was not found.'
26
+ : 'The default Migration Planning qualification record was not found.',
27
+ nextAction: 'INSTALL_QUALIFICATION_RECORD'
28
+ };
29
+ }
30
+ return {
31
+ status: 'CORRUPTED',
32
+ reasonCode: error?.code ?? 'MIGRATION_QUALIFICATION_RECORD_INVALID',
33
+ limitationCode: error?.code ?? 'MIGRATION_QUALIFICATION_RECORD_INVALID',
34
+ limitationMessage: 'The selected Migration Planning qualification record failed strict validation.',
35
+ nextAction: 'REPLACE_QUALIFICATION_RECORD'
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Deterministic precedence: injected qualification > explicit path > project-local default > missing.
41
+ * Exactly one source is selected and validated; invalid explicit sources never fall back.
42
+ */
43
+ export async function resolveMigrationQualification({
44
+ repositoryRoot,
45
+ runtimeMetadata,
46
+ allowExperimental = false,
47
+ qualification,
48
+ qualificationPath,
49
+ onCompatibilityDiagnostic,
50
+ loadRecord = loadMigrationPlanningQualificationRecord
51
+ }) {
52
+ if (qualification !== undefined) {
53
+ try {
54
+ const record = buildMigrationPlanningQualificationRecord(qualification);
55
+ return decideMigrationQualification({
56
+ qualification: record.qualification,
57
+ runtimeMetadata,
58
+ allowExperimental,
59
+ sourceKind: 'injected',
60
+ sourcePath: null
61
+ });
62
+ } catch (error) {
63
+ const details = sourceErrorDetails(error, true);
64
+ return createMigrationQualificationSourceFailureDecision({
65
+ ...details,
66
+ runtimeMetadata,
67
+ sourceKind: 'injected',
68
+ sourcePath: null
69
+ });
70
+ }
71
+ }
72
+
73
+ const explicitPath = qualificationPath !== undefined;
74
+ let artifactPath = qualificationPath ?? DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH;
75
+ if (!explicitPath) {
76
+ try {
77
+ const selection = await resolveArtifactRootChain(
78
+ repositoryRoot,
79
+ [DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH],
80
+ { onDiagnostic: onCompatibilityDiagnostic }
81
+ );
82
+ [artifactPath] = selection.artifacts;
83
+ } catch (error) {
84
+ const details = sourceErrorDetails(error, false);
85
+ return createMigrationQualificationSourceFailureDecision({
86
+ ...details,
87
+ runtimeMetadata,
88
+ sourceKind: 'defaultPath',
89
+ sourcePath: DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH
90
+ });
91
+ }
92
+ }
93
+ let record;
94
+ try {
95
+ record = await loadRecord(repositoryRoot, { artifactPath });
96
+ } catch (error) {
97
+ if (!explicitPath && error?.code === 'MIGRATION_QUALIFICATION_RECORD_MISSING') {
98
+ return decideMigrationQualification({
99
+ qualification: null,
100
+ runtimeMetadata,
101
+ allowExperimental,
102
+ sourceKind: 'defaultPath',
103
+ sourcePath: artifactPath
104
+ });
105
+ }
106
+ const details = sourceErrorDetails(error, explicitPath);
107
+ return createMigrationQualificationSourceFailureDecision({
108
+ ...details,
109
+ runtimeMetadata,
110
+ sourceKind: explicitPath ? 'explicitPath' : 'defaultPath',
111
+ sourcePath: artifactPath
112
+ });
113
+ }
114
+
115
+ return decideMigrationQualification({
116
+ qualification: record.qualification,
117
+ runtimeMetadata,
118
+ allowExperimental,
119
+ sourceKind: explicitPath ? 'explicitPath' : 'defaultPath',
120
+ sourcePath: artifactPath
121
+ });
122
+ }