@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,225 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ mkdir,
4
+ open,
5
+ readFile,
6
+ rename,
7
+ rm
8
+ } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+
11
+ import Ajv2020 from 'ajv/dist/2020.js';
12
+ import addFormats from 'ajv-formats';
13
+
14
+ import { canonicalJsonBytes } from '../canonical-json.js';
15
+ import {
16
+ DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH
17
+ } from '../constants.js';
18
+ import { compareText, isPortableRelativePath, isSorted } from '../portable.js';
19
+
20
+ const schema = JSON.parse(await readFile(
21
+ new URL('../../schemas/migration-planning-qualification-record.schema.json', import.meta.url),
22
+ 'utf8'
23
+ ));
24
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
25
+ addFormats(ajv);
26
+ const validateSchema = ajv.compile(schema);
27
+
28
+ export const MIGRATION_PLANNING_QUALIFICATION_RECORD_SCHEMA_VERSION = '1.0.0';
29
+
30
+ export class MigrationQualificationStoreError extends Error {
31
+ constructor(code, message) {
32
+ super(message);
33
+ this.name = 'MigrationQualificationStoreError';
34
+ this.code = code;
35
+ }
36
+ }
37
+
38
+ function deepFreeze(value) {
39
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
40
+ for (const child of Object.values(value)) deepFreeze(child);
41
+ return Object.freeze(value);
42
+ }
43
+
44
+ function digest(value) {
45
+ return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
46
+ }
47
+
48
+ function recordMaterial(record) {
49
+ return {
50
+ schemaVersion: record.schemaVersion,
51
+ qualification: record.qualification
52
+ };
53
+ }
54
+
55
+ function duplicate(values) {
56
+ return values.find((value, index) => values.indexOf(value) !== index) ?? null;
57
+ }
58
+
59
+ function containsSensitiveValue(value) {
60
+ if (typeof value === 'string') {
61
+ return /(?:\bBearer\s+\S+|\bsk-[A-Za-z0-9_-]{8,}|(?:api[_ -]?key|access[_ -]?token|authorization|client[_ -]?secret)\s*[:=])/i.test(value)
62
+ || /https?:\/\/[^/\s:@]+:[^/\s@]+@/i.test(value);
63
+ }
64
+ if (Array.isArray(value)) return value.some(containsSensitiveValue);
65
+ if (value && typeof value === 'object') return Object.values(value).some(containsSensitiveValue);
66
+ return false;
67
+ }
68
+
69
+ function invariantErrors(record) {
70
+ const errors = [];
71
+ const qualification = record.qualification;
72
+ if (qualification.qualificationId !== digest(qualification.identity)) {
73
+ errors.push('qualificationId does not match the exact qualification identity.');
74
+ }
75
+ if (record.recordDigest !== digest(recordMaterial(record))) {
76
+ errors.push('recordDigest does not match the canonical persisted qualification material.');
77
+ }
78
+ const duplicateMetric = duplicate(qualification.thresholdResults.map((item) => item.metric));
79
+ if (duplicateMetric) errors.push(`duplicate threshold metric ${duplicateMetric}.`);
80
+ const duplicateGate = duplicate(qualification.criticalGates.map((item) => item.id));
81
+ if (duplicateGate) errors.push(`duplicate critical gate ${duplicateGate}.`);
82
+ if (!isSorted(qualification.limitations, (left, right) => (
83
+ compareText(left.code, right.code) || compareText(left.message, right.message)
84
+ ))) {
85
+ errors.push('limitations must use deterministic code/message ordering.');
86
+ }
87
+ for (const field of ['observedProviders', 'observedModels']) {
88
+ if (!isSorted(qualification.identity.runtime[field], compareText)) {
89
+ errors.push(`runtime.${field} must be sorted.`);
90
+ }
91
+ }
92
+ const criticalFailure = qualification.criticalGates.some((item) => !item.passed)
93
+ || qualification.thresholdResults.some((item) => item.critical && !item.passed);
94
+ if (qualification.verdict === 'NOT_QUALIFIED' && !criticalFailure) {
95
+ errors.push('NOT_QUALIFIED requires a failed critical gate or threshold.');
96
+ }
97
+ if (['QUALIFIED', 'QUALIFIED_WITH_LIMITATIONS', 'INSUFFICIENT_EVIDENCE'].includes(
98
+ qualification.verdict
99
+ ) && criticalFailure) {
100
+ errors.push(`${qualification.verdict} cannot contain a failed critical gate or threshold.`);
101
+ }
102
+ if (qualification.verdict === 'QUALIFIED' && qualification.limitations.length > 0) {
103
+ errors.push('QUALIFIED cannot contain qualification limitations.');
104
+ }
105
+ if (qualification.verdict === 'QUALIFIED_WITH_LIMITATIONS'
106
+ && qualification.limitations.length === 0) {
107
+ errors.push('QUALIFIED_WITH_LIMITATIONS requires at least one limitation.');
108
+ }
109
+ if (containsSensitiveValue(record)) {
110
+ errors.push('qualification record contains a secret-like value.');
111
+ }
112
+ return errors;
113
+ }
114
+
115
+ export function validateMigrationPlanningQualificationRecordSchema(record) {
116
+ if (!validateSchema(record)) {
117
+ throw new MigrationQualificationStoreError(
118
+ 'MIGRATION_QUALIFICATION_RECORD_SCHEMA_INVALID',
119
+ 'Migration Planning qualification record failed strict schema validation.'
120
+ );
121
+ }
122
+ return record;
123
+ }
124
+
125
+ export function validateMigrationPlanningQualificationRecord(record) {
126
+ validateMigrationPlanningQualificationRecordSchema(record);
127
+ const errors = invariantErrors(record);
128
+ if (errors.length > 0) {
129
+ const code = errors.some((item) => item.includes('secret-like'))
130
+ ? 'MIGRATION_QUALIFICATION_RECORD_SENSITIVE'
131
+ : 'MIGRATION_QUALIFICATION_RECORD_INTEGRITY_INVALID';
132
+ throw new MigrationQualificationStoreError(
133
+ code,
134
+ 'Migration Planning qualification record failed integrity validation.'
135
+ );
136
+ }
137
+ return record;
138
+ }
139
+
140
+ export function buildMigrationPlanningQualificationRecord(qualification) {
141
+ const record = {
142
+ schemaVersion: MIGRATION_PLANNING_QUALIFICATION_RECORD_SCHEMA_VERSION,
143
+ recordDigest: `sha256:${'0'.repeat(64)}`,
144
+ qualification: structuredClone(qualification)
145
+ };
146
+ record.recordDigest = digest(recordMaterial(record));
147
+ return deepFreeze(validateMigrationPlanningQualificationRecord(record));
148
+ }
149
+
150
+ export function serializeMigrationPlanningQualificationRecord(record) {
151
+ validateMigrationPlanningQualificationRecord(record);
152
+ return `${JSON.stringify(record, null, 2)}\n`;
153
+ }
154
+
155
+ function artifactTarget(repositoryRoot, artifactPath) {
156
+ if (!isPortableRelativePath(artifactPath)) {
157
+ throw new MigrationQualificationStoreError(
158
+ 'MIGRATION_QUALIFICATION_PATH_INVALID',
159
+ 'Migration Planning qualification path must be portable and relative to the repository root.'
160
+ );
161
+ }
162
+ return path.resolve(repositoryRoot, artifactPath);
163
+ }
164
+
165
+ export async function loadMigrationPlanningQualificationRecord(repositoryRoot, {
166
+ artifactPath = DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH,
167
+ readArtifact = readFile
168
+ } = {}) {
169
+ const target = artifactTarget(repositoryRoot, artifactPath);
170
+ let bytes;
171
+ try {
172
+ bytes = await readArtifact(target);
173
+ } catch (error) {
174
+ if (error?.code === 'ENOENT') {
175
+ throw new MigrationQualificationStoreError(
176
+ 'MIGRATION_QUALIFICATION_RECORD_MISSING',
177
+ `Migration Planning qualification record was not found at ${artifactPath}.`
178
+ );
179
+ }
180
+ throw new MigrationQualificationStoreError(
181
+ 'MIGRATION_QUALIFICATION_RECORD_UNREADABLE',
182
+ `Migration Planning qualification record could not be read at ${artifactPath}.`
183
+ );
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(Buffer.from(bytes).toString('utf8'));
188
+ } catch {
189
+ throw new MigrationQualificationStoreError(
190
+ 'MIGRATION_QUALIFICATION_RECORD_INVALID_JSON',
191
+ 'Migration Planning qualification record is not valid JSON.'
192
+ );
193
+ }
194
+ return deepFreeze(validateMigrationPlanningQualificationRecord(parsed));
195
+ }
196
+
197
+ export async function writeMigrationPlanningQualificationRecord(
198
+ repositoryRoot,
199
+ qualification,
200
+ {
201
+ artifactPath = DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH
202
+ } = {}
203
+ ) {
204
+ const record = buildMigrationPlanningQualificationRecord(qualification);
205
+ const target = artifactTarget(repositoryRoot, artifactPath);
206
+ const temporary = `${target}.${process.pid}.tmp`;
207
+ await mkdir(path.dirname(target), { recursive: true });
208
+ let handle;
209
+ try {
210
+ handle = await open(temporary, 'w', 0o600);
211
+ await handle.writeFile(serializeMigrationPlanningQualificationRecord(record), 'utf8');
212
+ await handle.sync();
213
+ await handle.close();
214
+ handle = null;
215
+ await rename(temporary, target);
216
+ } catch {
217
+ await handle?.close().catch(() => {});
218
+ await rm(temporary, { force: true }).catch(() => {});
219
+ throw new MigrationQualificationStoreError(
220
+ 'MIGRATION_QUALIFICATION_RECORD_WRITE_FAILED',
221
+ `Migration Planning qualification record could not be written at ${artifactPath}.`
222
+ );
223
+ }
224
+ return artifactPath;
225
+ }
@@ -0,0 +1,205 @@
1
+ import { DEFAULT_MIGRATION_CHECKLIST_PATH } from '../constants.js';
2
+ import { assembleMigrationChecklist } from './assembler.js';
3
+ import { prepareMigrationChecklistContexts } from './context-runtime.js';
4
+ import { generateMigrationExtractiveChecklistDrafts } from './generator.js';
5
+ import { buildMigrationChecklistViewModel } from './presentation.js';
6
+ import {
7
+ decideMigrationQualification,
8
+ migrationQualificationErrorForDecision
9
+ } from './qualification-guard.js';
10
+ import { writeMigrationChecklist } from './writer.js';
11
+
12
+ export const MIGRATION_CHECKLIST_STAGE_ID = 'migrationChecklist';
13
+ export const MIGRATION_CHECKLIST_STAGE_LABEL = 'Migration Checklist';
14
+
15
+ function deepFreeze(value) {
16
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
17
+ for (const child of Object.values(value)) deepFreeze(child);
18
+ return Object.freeze(value);
19
+ }
20
+
21
+ function emit(listener, event) {
22
+ if (!listener) return;
23
+ try {
24
+ if (typeof listener === 'function') listener(deepFreeze(structuredClone(event)));
25
+ else listener.handle?.(deepFreeze(structuredClone(event)));
26
+ } catch {
27
+ // Operational presentation cannot affect business output.
28
+ }
29
+ }
30
+
31
+ function failureCode(error) {
32
+ if (typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code)) return error.code;
33
+ if (/lineage/i.test(error?.message ?? '')) return 'LINEAGE_INVALID';
34
+ if (/schema|validation/i.test(error?.message ?? '')) return 'ARTIFACT_VALIDATION_FAILED';
35
+ return 'MIGRATION_CHECKLIST_STAGE_FAILED';
36
+ }
37
+
38
+ function eventTypeForResult(event) {
39
+ if (event.outcome === 'generated') return 'migration:context-complete';
40
+ if (event.outcome === 'abstained') return 'migration:abstained';
41
+ if (event.reasonCode === 'TRUST_VALIDATION_REJECTED') return 'migration:trust-rejected';
42
+ return 'migration:fallback';
43
+ }
44
+
45
+ function throwIfCancelled(signal) {
46
+ if (!signal?.aborted) return;
47
+ const error = new Error('Migration Checklist stage was cancelled.', { cause: signal.reason });
48
+ error.code = 'ANALYSIS_CANCELLED';
49
+ throw error;
50
+ }
51
+
52
+ /**
53
+ * Experimental application stage: validated artifacts + Upgrade Decision -> MP-02 ->
54
+ * v2 guard -> extractive generator -> MP-01 assembly -> atomic artifact -> presentation view model.
55
+ */
56
+ export async function runMigrationChecklistStage({
57
+ repositoryRoot,
58
+ aiRuntime,
59
+ createAiRuntime,
60
+ runtimeMetadata,
61
+ qualification = null,
62
+ qualificationDecision = null,
63
+ allowExperimental = false,
64
+ generatedAt,
65
+ artifactPath = DEFAULT_MIGRATION_CHECKLIST_PATH,
66
+ onCompatibilityDiagnostic,
67
+ onEvent,
68
+ signal,
69
+ prepareContexts = prepareMigrationChecklistContexts,
70
+ generateDrafts = generateMigrationExtractiveChecklistDrafts,
71
+ assemble = assembleMigrationChecklist,
72
+ writeArtifact = writeMigrationChecklist
73
+ }) {
74
+ let total = 0;
75
+ let processed = 0;
76
+ let qualificationResult;
77
+ try {
78
+ throwIfCancelled(signal);
79
+ qualificationResult = qualificationDecision ?? await decideMigrationQualification({
80
+ qualification,
81
+ runtimeMetadata,
82
+ allowExperimental,
83
+ sourceKind: qualification ? 'injected' : 'none'
84
+ });
85
+ if (!qualificationResult.executionAllowed) {
86
+ throw migrationQualificationErrorForDecision(qualificationResult);
87
+ }
88
+ throwIfCancelled(signal);
89
+ const prepared = await prepareContexts(repositoryRoot, {
90
+ onCompatibilityDiagnostic
91
+ });
92
+ throwIfCancelled(signal);
93
+ total = prepared.eligibleContexts.length;
94
+ emit(onEvent, {
95
+ type: 'stage:start',
96
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
97
+ total,
98
+ qualificationStatus: qualificationResult.status,
99
+ qualificationId: qualificationResult.qualificationId,
100
+ experimentalOverrideUsed: qualificationResult.experimentalOverrideUsed
101
+ });
102
+ emit(onEvent, {
103
+ type: 'stage:progress',
104
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
105
+ processed,
106
+ total,
107
+ qualificationStatus: qualificationResult.status,
108
+ qualificationId: qualificationResult.qualificationId,
109
+ experimentalOverrideUsed: qualificationResult.experimentalOverrideUsed
110
+ });
111
+ const activeRuntime = aiRuntime ?? (total > 0 ? createAiRuntime?.() : {
112
+ async generateStructured() {
113
+ throw new Error('No eligible migration context should invoke the AI runtime.');
114
+ }
115
+ });
116
+ const generation = await generateDrafts(prepared, {
117
+ aiRuntime: activeRuntime,
118
+ signal,
119
+ onContextEvent(event) {
120
+ if (event.phase === 'start') {
121
+ emit(onEvent, {
122
+ type: 'migration:context-start',
123
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
124
+ packageName: event.packageName,
125
+ processed,
126
+ total
127
+ });
128
+ return;
129
+ }
130
+ processed += 1;
131
+ emit(onEvent, {
132
+ type: eventTypeForResult(event),
133
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
134
+ packageName: event.packageName,
135
+ processed,
136
+ total,
137
+ outcome: event.outcome,
138
+ reasonCode: event.reasonCode,
139
+ detailCode: event.detailCode
140
+ });
141
+ emit(onEvent, {
142
+ type: 'stage:progress',
143
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
144
+ processed,
145
+ total,
146
+ qualificationStatus: qualificationResult.status,
147
+ qualificationId: qualificationResult.qualificationId,
148
+ experimentalOverrideUsed: qualificationResult.experimentalOverrideUsed
149
+ });
150
+ }
151
+ });
152
+ throwIfCancelled(signal);
153
+ const checklist = assemble({
154
+ prepared,
155
+ generation,
156
+ qualification: qualificationResult,
157
+ generatedAt
158
+ });
159
+ throwIfCancelled(signal);
160
+ const outputPath = await writeArtifact(repositoryRoot, checklist, { artifactPath });
161
+ emit(onEvent, {
162
+ type: 'migration:artifact-written',
163
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
164
+ artifactPath: outputPath
165
+ });
166
+ const viewModel = buildMigrationChecklistViewModel(checklist, {
167
+ qualificationDecision: qualificationResult
168
+ });
169
+ emit(onEvent, {
170
+ type: 'stage:complete',
171
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
172
+ processed,
173
+ total,
174
+ generated: generation.summary.generated,
175
+ abstained: generation.summary.abstained,
176
+ rejected: generation.summary.rejected,
177
+ failed: generation.summary.failed,
178
+ limitationCount: checklist.summary.limitationCount,
179
+ qualificationStatus: qualificationResult.status,
180
+ qualificationId: qualificationResult.qualificationId,
181
+ experimentalOverrideUsed: qualificationResult.experimentalOverrideUsed
182
+ });
183
+ return deepFreeze({
184
+ artifactPath: outputPath,
185
+ checklist,
186
+ viewModel,
187
+ prepared,
188
+ generation,
189
+ qualification: qualificationResult
190
+ });
191
+ } catch (error) {
192
+ qualificationResult ??= error?.decision ?? null;
193
+ emit(onEvent, {
194
+ type: signal?.aborted ? 'stage:cancelled' : 'stage:failed',
195
+ stageId: MIGRATION_CHECKLIST_STAGE_ID,
196
+ processed,
197
+ total,
198
+ reasonCode: signal?.aborted ? 'USER_CANCELLED' : failureCode(error),
199
+ qualificationStatus: qualificationResult?.status ?? 'UNKNOWN',
200
+ qualificationId: qualificationResult?.qualificationId ?? null,
201
+ experimentalOverrideUsed: qualificationResult?.experimentalOverrideUsed ?? false
202
+ });
203
+ throw error;
204
+ }
205
+ }
@@ -0,0 +1,134 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import { canonicalJsonBytes } from '../canonical-json.js';
6
+ import { compareText, isPortableRelativePath } from '../portable.js';
7
+
8
+ export const MIGRATION_VERIFICATION_STATUSES = Object.freeze([
9
+ 'AVAILABLE',
10
+ 'VERIFICATION_COMMAND_UNAVAILABLE',
11
+ 'NOT_APPLICABLE'
12
+ ]);
13
+
14
+ const SCRIPT_ROLES = Object.freeze({
15
+ build: 'BUILD',
16
+ check: 'CHECK',
17
+ lint: 'LINT',
18
+ test: 'TEST',
19
+ typecheck: 'TYPECHECK'
20
+ });
21
+ const PACKAGE_MANAGER_COMMANDS = Object.freeze({
22
+ bun: (name) => `bun run ${name}`,
23
+ npm: (name) => `npm run ${name}`,
24
+ pnpm: (name) => `pnpm run ${name}`,
25
+ yarn: (name) => `yarn run ${name}`
26
+ });
27
+ const SAFE_SCRIPT_NAME = /^[A-Za-z0-9:_-]+$/;
28
+
29
+ function digest(bytes) {
30
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
31
+ }
32
+
33
+ function commandId(value) {
34
+ return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
35
+ }
36
+
37
+ function unavailable() {
38
+ return {
39
+ status: 'VERIFICATION_COMMAND_UNAVAILABLE',
40
+ commands: [],
41
+ limitation: {
42
+ code: 'VERIFICATION_COMMAND_UNAVAILABLE',
43
+ message: 'No supported project-derived verification command was found.'
44
+ }
45
+ };
46
+ }
47
+
48
+ function manifestPaths(project) {
49
+ return [...new Set([
50
+ ...project.manifests,
51
+ ...project.dependencies.map((dependency) => dependency.manifest)
52
+ ])].filter((value) => value.endsWith('package.json')).sort(compareText);
53
+ }
54
+
55
+ function commandRecords(project, sourcePath, sourceDigest, manifest) {
56
+ const packageManager = project.packageManager?.name;
57
+ const renderCommand = PACKAGE_MANAGER_COMMANDS[packageManager];
58
+ if (!renderCommand || !manifest.scripts || typeof manifest.scripts !== 'object'
59
+ || Array.isArray(manifest.scripts)) {
60
+ return [];
61
+ }
62
+ const workingDirectory = project.path;
63
+ return Object.entries(SCRIPT_ROLES)
64
+ .filter(([scriptName]) => (
65
+ SAFE_SCRIPT_NAME.test(scriptName)
66
+ && typeof manifest.scripts[scriptName] === 'string'
67
+ && manifest.scripts[scriptName].trim().length > 0
68
+ ))
69
+ .map(([scriptName, role]) => {
70
+ const core = {
71
+ role,
72
+ command: renderCommand(scriptName),
73
+ workingDirectory,
74
+ source: {
75
+ path: sourcePath,
76
+ contentDigest: sourceDigest,
77
+ scriptName,
78
+ packageManager
79
+ }
80
+ };
81
+ return { id: commandId(core), ...core };
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Read only Project Manifest-declared Node package manifests and project safe
87
+ * script keys. Script bodies are neither parsed nor copied into the handoff.
88
+ */
89
+ export async function extractProjectVerification(repositoryRoot, projectManifest, {
90
+ readArtifact = readFile
91
+ } = {}) {
92
+ const byProject = new Map();
93
+ for (const project of projectManifest.projects) {
94
+ const commands = [];
95
+ if (project.ecosystem === 'node' && typeof repositoryRoot === 'string') {
96
+ for (const sourcePath of manifestPaths(project)) {
97
+ if (!isPortableRelativePath(sourcePath)) continue;
98
+ let bytes;
99
+ try {
100
+ bytes = await readArtifact(path.resolve(repositoryRoot, sourcePath));
101
+ } catch {
102
+ continue;
103
+ }
104
+ let manifest;
105
+ try {
106
+ manifest = JSON.parse(Buffer.from(bytes).toString('utf8'));
107
+ } catch {
108
+ continue;
109
+ }
110
+ commands.push(...commandRecords(
111
+ project,
112
+ sourcePath,
113
+ digest(Buffer.from(bytes)),
114
+ manifest
115
+ ));
116
+ }
117
+ }
118
+ const unique = new Map(commands.map((command) => [command.id, command]));
119
+ const normalized = [...unique.values()].sort((left, right) => (
120
+ compareText(left.role, right.role)
121
+ || compareText(left.workingDirectory, right.workingDirectory)
122
+ || compareText(left.command, right.command)
123
+ || compareText(left.id, right.id)
124
+ ));
125
+ byProject.set(project.id, normalized.length > 0
126
+ ? { status: 'AVAILABLE', commands: normalized, limitation: null }
127
+ : unavailable());
128
+ }
129
+ return byProject;
130
+ }
131
+
132
+ export function unavailableProjectVerification() {
133
+ return unavailable();
134
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+
3
+ import { DEFAULT_MIGRATION_CHECKLIST_PATH } from '../constants.js';
4
+ import { writeTextArtifact } from '../orchestration/text-writer.js';
5
+ import { isPortableRelativePath } from '../portable.js';
6
+ import { serializeMigrationChecklist, validateMigrationChecklist } from './migration-checklist.js';
7
+
8
+ export class MigrationChecklistWriterError extends Error {
9
+ constructor(message, cause) {
10
+ super(message, { cause });
11
+ this.name = 'MigrationChecklistWriterError';
12
+ this.code = 'MIGRATION_CHECKLIST_WRITE_FAILED';
13
+ }
14
+ }
15
+
16
+ /** Validate first, then atomically publish deterministic bytes and return a portable path. */
17
+ export async function writeMigrationChecklist(repositoryRoot, checklist, {
18
+ artifactPath = DEFAULT_MIGRATION_CHECKLIST_PATH,
19
+ writeArtifact = writeTextArtifact
20
+ } = {}) {
21
+ if (!isPortableRelativePath(artifactPath)) {
22
+ throw new TypeError('Migration Checklist artifact path must be portable and repository-relative.');
23
+ }
24
+ validateMigrationChecklist(checklist);
25
+ const contents = serializeMigrationChecklist(checklist);
26
+ try {
27
+ await writeArtifact(path.resolve(repositoryRoot, artifactPath), contents);
28
+ } catch (cause) {
29
+ throw new MigrationChecklistWriterError(
30
+ `Unable to write Migration Checklist artifact ${artifactPath}.`,
31
+ cause
32
+ );
33
+ }
34
+ return artifactPath.split(path.sep).join('/');
35
+ }