@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.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/bin/depverdict.js +7 -0
- package/bin/upgradelens.js +7 -0
- package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
- package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
- package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
- package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
- package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
- package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
- package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
- package/docs/IA-04-CLI-Orchestration.md +235 -0
- package/docs/IA-05-Real-Provider-Validation.md +235 -0
- package/docs/IA-05-VinGrade-Validation.md +339 -0
- package/docs/MVP-01.md +48 -0
- package/docs/MVP-02-Architecture.md +536 -0
- package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
- package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
- package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
- package/docs/MVP-02-Knowledge-Manifest.md +279 -0
- package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
- package/docs/MVP-02-Knowledge-Store.md +87 -0
- package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
- package/docs/MVP-02-Research-Planning.md +196 -0
- package/docs/MVP-02-Source-Provenance.md +81 -0
- package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
- package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
- package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
- package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
- package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
- package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
- package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
- package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
- package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
- package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
- package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
- package/docs/VinGrade-MVP-02-Validation.md +480 -0
- package/docs/VinGrade-RC02-Live-Validation.md +273 -0
- package/docs/ai-capability-discovery.md +573 -0
- package/docs/ai-engineering-production-readiness.md +390 -0
- package/docs/ai-engineering-review.md +251 -0
- package/docs/ai-runtime-governance-discovery.md +754 -0
- package/docs/architecture-overview.md +78 -0
- package/docs/cli-progress.md +111 -0
- package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
- package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
- package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
- package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
- package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
- package/docs/gateway-runtime-discovery.md +614 -0
- package/docs/live-ai-validation.md +299 -0
- package/docs/migration-planning-qualification-resolution.md +99 -0
- package/docs/migrations/upgradelens-to-depverdict.md +97 -0
- package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
- package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
- package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
- package/docs/mvp-05-deterministic-context-runtime.md +127 -0
- package/docs/mvp-05-migration-checklist-contract.md +166 -0
- package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
- package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
- package/docs/mvp-05-provider-neutral-generator.md +79 -0
- package/docs/ollama-local-smoke-validation.md +156 -0
- package/docs/openai-compatible-runtime-discovery.md +789 -0
- package/docs/openrouter-one-dependency-validation.md +205 -0
- package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
- package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
- package/docs/package-content-policy.md +98 -0
- package/docs/releases/v0.5.0-technical-preview.md +114 -0
- package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
- package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
- package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
- package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
- package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
- package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
- package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
- package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
- package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
- package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
- package/docs/reviews/oss-03-community-scaffolding.md +378 -0
- package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
- package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
- package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
- package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
- package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
- package/docs/runtime-contract-discovery.md +521 -0
- package/docs/structured-output-compatibility-report.md +100 -0
- package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
- package/docs/version-analysis-architecture.md +827 -0
- package/eval/README.md +86 -0
- package/eval/datasets/generic/declared-constraint.json +59 -0
- package/eval/datasets/generic/evidence-conflict.json +73 -0
- package/eval/datasets/generic/major-breaking-release.json +66 -0
- package/eval/datasets/generic/missing-evidence.json +46 -0
- package/eval/datasets/generic/patch-release-low.json +59 -0
- package/eval/datasets/node/axios-patch-low.json +59 -0
- package/eval/datasets/node/react-major-breaking.json +66 -0
- package/eval/datasets/node/react-minor-compatibility.json +66 -0
- package/eval/datasets/python/fastapi-deprecation.json +66 -0
- package/eval/datasets/python/pydantic-major-breaking.json +66 -0
- package/eval/migration-planning/golden-dataset-v2.json +306 -0
- package/eval/migration-planning/golden-dataset.json +214 -0
- package/eval/schemas/expected-result.schema.json +88 -0
- package/eval/schemas/golden-case.schema.json +181 -0
- package/package.json +57 -0
- package/schemas/ai-scorecard.schema.json +132 -0
- package/schemas/benchmark-report.schema.json +189 -0
- package/schemas/benchmark.schema.json +55 -0
- package/schemas/capability-profile.schema.json +44 -0
- package/schemas/conformance-report.schema.json +150 -0
- package/schemas/deployment-profile.schema.json +64 -0
- package/schemas/evaluation-report.schema.json +158 -0
- package/schemas/knowledge-evidence-bundle.schema.json +150 -0
- package/schemas/knowledge-manifest.schema.json +548 -0
- package/schemas/metrics.schema.json +178 -0
- package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
- package/schemas/migration-checklist.schema.json +706 -0
- package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
- package/schemas/migration-evaluation-dataset.schema.json +204 -0
- package/schemas/migration-planning-qualification-record.schema.json +234 -0
- package/schemas/project-manifest.schema.json +308 -0
- package/schemas/qualification-record.schema.json +56 -0
- package/schemas/repository-impact-evidence.schema.json +232 -0
- package/schemas/repository-impact.schema.json +202 -0
- package/schemas/upgrade-decision.schema.json +273 -0
- package/schemas/usage-index.schema.json +179 -0
- package/schemas/version-analysis.schema.json +449 -0
- package/src/ai-runtime-debug.js +325 -0
- package/src/ai-runtime-error.js +42 -0
- package/src/ai-runtime.js +174 -0
- package/src/ai-scorecard.js +204 -0
- package/src/ai-version-analysis.js +484 -0
- package/src/artifact-root-compatibility.js +91 -0
- package/src/benchmark-report.js +111 -0
- package/src/benchmark-runner.js +191 -0
- package/src/canonical-json.js +69 -0
- package/src/cli.js +1299 -0
- package/src/conformance-report.js +158 -0
- package/src/conformance-runner.js +253 -0
- package/src/constants.js +73 -0
- package/src/cooperative-scheduler.js +79 -0
- package/src/dependencies.js +44 -0
- package/src/dependency-ai-context.js +625 -0
- package/src/detectors.js +253 -0
- package/src/discovery.js +234 -0
- package/src/ecosystem-version-adapter.js +294 -0
- package/src/environment-compatibility.js +77 -0
- package/src/evaluation-comparator.js +158 -0
- package/src/evaluation-report.js +76 -0
- package/src/evaluation-runner.js +248 -0
- package/src/evidence-source-adapter.js +472 -0
- package/src/files.js +89 -0
- package/src/governance-diagnostics.js +64 -0
- package/src/governance-loader.js +63 -0
- package/src/governance-metadata.js +346 -0
- package/src/governance-validator.js +360 -0
- package/src/http/bounded-fetch.js +278 -0
- package/src/http/cli-http-runtime.js +44 -0
- package/src/impact/input-loader.js +157 -0
- package/src/impact/matcher.js +40 -0
- package/src/impact/repository-impact.js +199 -0
- package/src/impact/runtime.js +24 -0
- package/src/impact/status.js +62 -0
- package/src/impact/writer.js +30 -0
- package/src/impact-evidence/input-loader.js +202 -0
- package/src/impact-evidence/repository-impact-evidence.js +234 -0
- package/src/impact-evidence/runtime.js +16 -0
- package/src/impact-evidence/writer.js +30 -0
- package/src/index.js +563 -0
- package/src/installed-version-baseline.js +196 -0
- package/src/knowledge-cache.js +324 -0
- package/src/knowledge-evidence-bundle.js +101 -0
- package/src/knowledge-evidence-producer.js +233 -0
- package/src/knowledge-manifest-builder.js +188 -0
- package/src/knowledge-manifest-writer.js +32 -0
- package/src/knowledge-manifest.js +255 -0
- package/src/knowledge-research.js +615 -0
- package/src/metrics-engine.js +205 -0
- package/src/migration-checklist/ai-candidate.js +320 -0
- package/src/migration-checklist/assembler.js +37 -0
- package/src/migration-checklist/context-runtime.js +828 -0
- package/src/migration-checklist/evaluation/action-criteria.js +244 -0
- package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
- package/src/migration-checklist/evaluation/comparator.js +279 -0
- package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
- package/src/migration-checklist/evaluation/dataset.js +336 -0
- package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
- package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
- package/src/migration-checklist/evaluation/metrics.js +158 -0
- package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
- package/src/migration-checklist/evaluation/qualification.js +239 -0
- package/src/migration-checklist/evaluation/runner-v2.js +294 -0
- package/src/migration-checklist/evaluation/runner.js +194 -0
- package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
- package/src/migration-checklist/evaluation/scorecard.js +86 -0
- package/src/migration-checklist/extractive-candidate.js +166 -0
- package/src/migration-checklist/extractive-prompt.js +62 -0
- package/src/migration-checklist/generator.js +702 -0
- package/src/migration-checklist/grounding-policy.js +117 -0
- package/src/migration-checklist/input-loader.js +613 -0
- package/src/migration-checklist/migration-checklist.js +635 -0
- package/src/migration-checklist/presentation.js +292 -0
- package/src/migration-checklist/progress.js +96 -0
- package/src/migration-checklist/prompt.js +83 -0
- package/src/migration-checklist/qualification-guard.js +462 -0
- package/src/migration-checklist/qualification-resolution.js +122 -0
- package/src/migration-checklist/qualification-store.js +225 -0
- package/src/migration-checklist/runtime.js +205 -0
- package/src/migration-checklist/verification.js +134 -0
- package/src/migration-checklist/writer.js +35 -0
- package/src/openai-compatible-provider.js +451 -0
- package/src/orchestration/failure-log.js +32 -0
- package/src/orchestration/pipeline.js +200 -0
- package/src/orchestration/progress-events.js +337 -0
- package/src/orchestration/progress-reporter.js +131 -0
- package/src/orchestration/text-writer.js +22 -0
- package/src/portable.js +13 -0
- package/src/product-completion.js +249 -0
- package/src/project-manifest-input.js +90 -0
- package/src/project-manifest.js +141 -0
- package/src/python-requirements.js +137 -0
- package/src/registry/npm-packument.js +256 -0
- package/src/registry/npm-registry-adapter.js +262 -0
- package/src/registry/pypi-project.js +300 -0
- package/src/registry/pypi-registry-adapter.js +235 -0
- package/src/registry/sanitize-registry-body.js +51 -0
- package/src/renderers/console.js +160 -0
- package/src/renderers/impact-presentation.js +278 -0
- package/src/renderers/markdown.js +172 -0
- package/src/research-plan.js +455 -0
- package/src/runtime-conformance.js +275 -0
- package/src/source-provenance.js +393 -0
- package/src/source-url.js +62 -0
- package/src/structured-output-schema.js +66 -0
- package/src/target-selector.js +306 -0
- package/src/upgrade-decision/input-loader.js +107 -0
- package/src/upgrade-decision/presentation.js +43 -0
- package/src/upgrade-decision/runtime.js +21 -0
- package/src/upgrade-decision/upgrade-decision.js +626 -0
- package/src/upgrade-decision/writer.js +30 -0
- package/src/usage/analyzer-registry.js +63 -0
- package/src/usage/coverage.js +116 -0
- package/src/usage/input-loader.js +139 -0
- package/src/usage/js/analyzer.js +240 -0
- package/src/usage/js/parser.js +21 -0
- package/src/usage/runtime.js +187 -0
- package/src/usage/scope.js +44 -0
- package/src/usage/source-files.js +50 -0
- package/src/usage/usage-index.js +217 -0
- package/src/usage/writer.js +31 -0
- package/src/version-analysis-loader.js +203 -0
- package/src/version-analysis-manifest.js +314 -0
- 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
|
+
}
|