@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,205 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
|
|
8
|
+
import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
|
|
9
|
+
import { validateEvaluationReport } from './evaluation-report.js';
|
|
10
|
+
import { compareText } from './portable.js';
|
|
11
|
+
|
|
12
|
+
export const METRICS_SCHEMA_VERSION = '1.0.0';
|
|
13
|
+
export const DEFAULT_METRICS_PATH = 'metrics.json';
|
|
14
|
+
|
|
15
|
+
const schema = JSON.parse(await readFile(
|
|
16
|
+
new URL('../schemas/metrics.schema.json', import.meta.url),
|
|
17
|
+
'utf8'
|
|
18
|
+
));
|
|
19
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
20
|
+
addFormats(ajv);
|
|
21
|
+
const validateSchema = ajv.compile(schema);
|
|
22
|
+
|
|
23
|
+
function ratio(numerator, denominator, emptyValue = 1) {
|
|
24
|
+
return denominator === 0 ? emptyValue : numerator / denominator;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function metric(passed, total) {
|
|
28
|
+
return { value: ratio(passed, total), passed, total };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function boolCheck(caseResult, name) {
|
|
32
|
+
return caseResult.checks?.[name]?.passed === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sortedUnique(values = []) {
|
|
36
|
+
return [...new Set(values)].sort(compareText);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flattenRefs(refs = {}) {
|
|
40
|
+
return sortedUnique([
|
|
41
|
+
...(refs.summary ?? []),
|
|
42
|
+
...(refs.risk ?? []),
|
|
43
|
+
...(refs.findings ?? [])
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function evidenceRefCounts(caseResult) {
|
|
48
|
+
const expected = flattenRefs(caseResult.checks?.evidenceReferences?.expected);
|
|
49
|
+
const actual = flattenRefs(caseResult.checks?.evidenceReferences?.actual);
|
|
50
|
+
const actualSet = new Set(actual);
|
|
51
|
+
return {
|
|
52
|
+
expected: expected.length,
|
|
53
|
+
actual: actual.length,
|
|
54
|
+
matched: expected.filter((ref) => actualSet.has(ref)).length
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validationWarningCodes(caseResult) {
|
|
59
|
+
return sortedUnique(caseResult.checks?.validationState?.actual?.warningCodes ?? []);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function unsupportedClaimCount(caseResult) {
|
|
63
|
+
return validationWarningCodes(caseResult).includes('CLAIMS_DROPPED') ? 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function actualFindingCount(caseResult) {
|
|
67
|
+
return Array.isArray(caseResult.checks?.findings?.actual)
|
|
68
|
+
? caseResult.checks.findings.actual.length
|
|
69
|
+
: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deterministicPassed(caseResult) {
|
|
73
|
+
return boolCheck(caseResult, 'schema')
|
|
74
|
+
&& boolCheck(caseResult, 'evidenceReferences')
|
|
75
|
+
&& boolCheck(caseResult, 'validationState')
|
|
76
|
+
&& boolCheck(caseResult, 'findings');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildInput(report) {
|
|
80
|
+
return {
|
|
81
|
+
evaluationReport: {
|
|
82
|
+
schemaVersion: report.schemaVersion,
|
|
83
|
+
generatedAt: report.generatedAt,
|
|
84
|
+
datasetPath: report.evaluation.datasetPath,
|
|
85
|
+
datasetVersion: report.evaluation.datasetVersion,
|
|
86
|
+
promptVersion: report.evaluation.promptVersion,
|
|
87
|
+
model: structuredClone(report.model),
|
|
88
|
+
caseCount: report.evaluation.caseCount
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildCaseMetrics(report) {
|
|
94
|
+
return report.cases
|
|
95
|
+
.map((caseResult) => ({
|
|
96
|
+
id: caseResult.id,
|
|
97
|
+
category: caseResult.category,
|
|
98
|
+
ecosystem: caseResult.ecosystem,
|
|
99
|
+
passed: caseResult.passed,
|
|
100
|
+
riskClassificationPassed: boolCheck(caseResult, 'riskLevel'),
|
|
101
|
+
humanReviewPassed: boolCheck(caseResult, 'humanReview'),
|
|
102
|
+
evidenceReferencesPassed: boolCheck(caseResult, 'evidenceReferences'),
|
|
103
|
+
validationPassed: boolCheck(caseResult, 'schema') && boolCheck(caseResult, 'validationState'),
|
|
104
|
+
deterministicPassed: deterministicPassed(caseResult),
|
|
105
|
+
unsupportedClaimCount: unsupportedClaimCount(caseResult),
|
|
106
|
+
actualFindingCount: actualFindingCount(caseResult)
|
|
107
|
+
}))
|
|
108
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function metricsFromReport(report, cases) {
|
|
112
|
+
const total = report.cases.length;
|
|
113
|
+
const refCounts = report.cases.map(evidenceRefCounts);
|
|
114
|
+
const matchedReferenceCount = refCounts.reduce((sum, item) => sum + item.matched, 0);
|
|
115
|
+
const expectedReferenceCount = refCounts.reduce((sum, item) => sum + item.expected, 0);
|
|
116
|
+
const unsupportedClaimCountTotal = cases.reduce((sum, item) => sum + item.unsupportedClaimCount, 0);
|
|
117
|
+
const totalFindingCount = cases.reduce((sum, item) => sum + item.actualFindingCount, 0);
|
|
118
|
+
const unsupportedDenominator = totalFindingCount + unsupportedClaimCountTotal;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
casePassRate: metric(report.summary.passed, total),
|
|
122
|
+
riskClassificationAccuracy: metric(report.cases.filter((item) => boolCheck(item, 'riskLevel')).length, total),
|
|
123
|
+
humanReviewAccuracy: metric(report.cases.filter((item) => boolCheck(item, 'humanReview')).length, total),
|
|
124
|
+
humanReviewReasonAccuracy: metric(report.cases.filter((item) => boolCheck(item, 'humanReviewReasons')).length, total),
|
|
125
|
+
evidenceReferenceAccuracy: metric(report.cases.filter((item) => boolCheck(item, 'evidenceReferences')).length, total),
|
|
126
|
+
evidenceCoverageAccuracy: metric(report.cases.filter((item) => boolCheck(item, 'evidenceCoverage')).length, total),
|
|
127
|
+
evidenceReferenceCoverage: {
|
|
128
|
+
value: ratio(matchedReferenceCount, expectedReferenceCount),
|
|
129
|
+
matchedReferenceCount,
|
|
130
|
+
expectedReferenceCount
|
|
131
|
+
},
|
|
132
|
+
unsupportedClaimRate: {
|
|
133
|
+
value: ratio(unsupportedClaimCountTotal, unsupportedDenominator, 0),
|
|
134
|
+
unsupportedClaimCount: unsupportedClaimCountTotal,
|
|
135
|
+
totalFindingCount,
|
|
136
|
+
denominator: unsupportedDenominator
|
|
137
|
+
},
|
|
138
|
+
validationPassRate: metric(cases.filter((item) => item.validationPassed).length, total),
|
|
139
|
+
deterministicPassRate: metric(cases.filter((item) => item.deterministicPassed).length, total)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildMetrics(report, { generatedAt = new Date() } = {}) {
|
|
144
|
+
const validated = validateEvaluationReport(report);
|
|
145
|
+
const cases = buildCaseMetrics(validated);
|
|
146
|
+
const metrics = {
|
|
147
|
+
schemaVersion: METRICS_SCHEMA_VERSION,
|
|
148
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
149
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
150
|
+
input: buildInput(validated),
|
|
151
|
+
summary: structuredClone(validated.summary),
|
|
152
|
+
metrics: metricsFromReport(validated, cases),
|
|
153
|
+
cases
|
|
154
|
+
};
|
|
155
|
+
return validateMetrics(metrics);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function validateMetrics(metrics) {
|
|
159
|
+
if (metrics?.schemaVersion !== METRICS_SCHEMA_VERSION) {
|
|
160
|
+
throw new Error(`Metrics validation error: unsupported schema version; expected ${METRICS_SCHEMA_VERSION}.`);
|
|
161
|
+
}
|
|
162
|
+
if (!validateSchema(metrics)) {
|
|
163
|
+
throw new Error(`Metrics validation error: schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
164
|
+
}
|
|
165
|
+
return metrics;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function serializeMetrics(metrics) {
|
|
169
|
+
validateMetrics(metrics);
|
|
170
|
+
return `${JSON.stringify(metrics, null, 2)}\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function metricsDigest(metrics) {
|
|
174
|
+
return `sha256:${createHash('sha256').update(serializeMetrics(metrics)).digest('hex')}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function loadEvaluationReportForMetrics(reportPath) {
|
|
178
|
+
try {
|
|
179
|
+
return validateEvaluationReport(JSON.parse(await readFile(reportPath, 'utf8')));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof SyntaxError) throw new Error(`Evaluation Report is not valid JSON: ${reportPath}.`);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function writeMetrics(outputPath, metrics) {
|
|
187
|
+
const target = path.resolve(outputPath);
|
|
188
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
189
|
+
const contents = serializeMetrics(metrics);
|
|
190
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
191
|
+
let handle;
|
|
192
|
+
try {
|
|
193
|
+
handle = await open(temporary, 'w', 0o600);
|
|
194
|
+
await handle.writeFile(contents, 'utf8');
|
|
195
|
+
await handle.sync();
|
|
196
|
+
await handle.close();
|
|
197
|
+
handle = null;
|
|
198
|
+
await rename(temporary, target);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await handle?.close().catch(() => {});
|
|
201
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
return target;
|
|
205
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
2
|
+
|
|
3
|
+
import { compareText } from '../portable.js';
|
|
4
|
+
import { validateMigrationChecklistInstructionContent } from './grounding-policy.js';
|
|
5
|
+
|
|
6
|
+
export const MIGRATION_CANDIDATE_STATUSES = Object.freeze(['ACTIONABLE', 'ABSTAIN']);
|
|
7
|
+
export const MIGRATION_CANDIDATE_ABSTENTION_REASONS = Object.freeze([
|
|
8
|
+
'NO_EXPLICIT_ACTION',
|
|
9
|
+
'AMBIGUOUS_EVIDENCE',
|
|
10
|
+
'VERSION_SCOPE_UNCLEAR',
|
|
11
|
+
'CONFLICTING_EVIDENCE'
|
|
12
|
+
]);
|
|
13
|
+
export const MIGRATION_CHECKLIST_CANDIDATE_ERROR_CODES = Object.freeze([
|
|
14
|
+
'OUTPUT_JSON_INVALID',
|
|
15
|
+
'OUTPUT_SCHEMA_INVALID',
|
|
16
|
+
'OUTPUT_SEMANTICS_INVALID'
|
|
17
|
+
]);
|
|
18
|
+
export const MIGRATION_CHECKLIST_TRUST_ERROR_CODES = Object.freeze([
|
|
19
|
+
'EVIDENCE_REFERENCE_INVALID',
|
|
20
|
+
'SUPPORTING_EXCERPT_INVALID',
|
|
21
|
+
'PROHIBITED_CAPABILITY',
|
|
22
|
+
'UNSUPPORTED_IDENTIFIER',
|
|
23
|
+
'DUPLICATE_ITEM'
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const DIGEST_PATTERN = '^sha256:[a-f0-9]{64}$';
|
|
27
|
+
const MAX_ITEMS = 4;
|
|
28
|
+
const MAX_REFS = 6;
|
|
29
|
+
const MAX_INSTRUCTION_CHARACTERS = 800;
|
|
30
|
+
const MAX_EXCERPT_CHARACTERS = 500;
|
|
31
|
+
|
|
32
|
+
export const MIGRATION_CHECKLIST_CANDIDATE_SCHEMA = deepFreeze({
|
|
33
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
34
|
+
type: 'object',
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
required: ['status', 'items', 'abstentionReason'],
|
|
37
|
+
properties: {
|
|
38
|
+
status: { enum: [...MIGRATION_CANDIDATE_STATUSES] },
|
|
39
|
+
items: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
maxItems: MAX_ITEMS,
|
|
42
|
+
items: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
required: ['instruction', 'evidenceRefs', 'supportingExcerpts'],
|
|
46
|
+
properties: {
|
|
47
|
+
instruction: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
minLength: 1,
|
|
50
|
+
maxLength: MAX_INSTRUCTION_CHARACTERS
|
|
51
|
+
},
|
|
52
|
+
evidenceRefs: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
minItems: 1,
|
|
55
|
+
maxItems: MAX_REFS,
|
|
56
|
+
uniqueItems: true,
|
|
57
|
+
items: { type: 'string', pattern: DIGEST_PATTERN }
|
|
58
|
+
},
|
|
59
|
+
supportingExcerpts: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
minItems: 1,
|
|
62
|
+
maxItems: MAX_REFS,
|
|
63
|
+
items: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
required: ['evidenceRef', 'text'],
|
|
67
|
+
properties: {
|
|
68
|
+
evidenceRef: { type: 'string', pattern: DIGEST_PATTERN },
|
|
69
|
+
text: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
minLength: 1,
|
|
72
|
+
maxLength: MAX_EXCERPT_CHARACTERS
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
abstentionReason: {
|
|
81
|
+
enum: [null, ...MIGRATION_CANDIDATE_ABSTENTION_REASONS]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
87
|
+
const validateSchema = ajv.compile(MIGRATION_CHECKLIST_CANDIDATE_SCHEMA);
|
|
88
|
+
|
|
89
|
+
export class MigrationChecklistCandidateError extends Error {
|
|
90
|
+
constructor(code, message) {
|
|
91
|
+
if (!MIGRATION_CHECKLIST_CANDIDATE_ERROR_CODES.includes(code)) {
|
|
92
|
+
throw new TypeError('Unknown Migration Checklist candidate error code.');
|
|
93
|
+
}
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = 'MigrationChecklistCandidateError';
|
|
96
|
+
this.code = code;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class MigrationChecklistTrustError extends Error {
|
|
101
|
+
constructor(code, message, { detailCode } = {}) {
|
|
102
|
+
if (!MIGRATION_CHECKLIST_TRUST_ERROR_CODES.includes(code)) {
|
|
103
|
+
throw new TypeError('Unknown Migration Checklist trust error code.');
|
|
104
|
+
}
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'MigrationChecklistTrustError';
|
|
107
|
+
this.code = code;
|
|
108
|
+
if (typeof detailCode === 'string') this.detailCode = detailCode;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isMigrationChecklistCandidateError(error) {
|
|
113
|
+
return error instanceof MigrationChecklistCandidateError;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isMigrationChecklistTrustError(error) {
|
|
117
|
+
return error instanceof MigrationChecklistTrustError;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function deepFreeze(value) {
|
|
121
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
122
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
123
|
+
return Object.freeze(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeLineEndings(value) {
|
|
127
|
+
return value.replace(/\r\n?/g, '\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseCandidate(output) {
|
|
131
|
+
if (typeof output !== 'string') return output;
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(output);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new MigrationChecklistCandidateError(
|
|
136
|
+
'OUTPUT_JSON_INVALID',
|
|
137
|
+
'Migration Checklist candidate is not valid JSON.'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Parse and validate the strict model-owned candidate contract. */
|
|
143
|
+
export function validateMigrationChecklistCandidate(output) {
|
|
144
|
+
const candidate = parseCandidate(output);
|
|
145
|
+
if (!validateSchema(candidate)) {
|
|
146
|
+
throw new MigrationChecklistCandidateError(
|
|
147
|
+
'OUTPUT_SCHEMA_INVALID',
|
|
148
|
+
`Migration Checklist candidate schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (candidate.status === 'ACTIONABLE'
|
|
152
|
+
&& (candidate.items.length === 0 || candidate.abstentionReason !== null)) {
|
|
153
|
+
throw new MigrationChecklistCandidateError(
|
|
154
|
+
'OUTPUT_SEMANTICS_INVALID',
|
|
155
|
+
'ACTIONABLE candidates require items and a null abstentionReason.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (candidate.status === 'ABSTAIN'
|
|
159
|
+
&& (candidate.items.length !== 0 || candidate.abstentionReason === null)) {
|
|
160
|
+
throw new MigrationChecklistCandidateError(
|
|
161
|
+
'OUTPUT_SEMANTICS_INVALID',
|
|
162
|
+
'ABSTAIN candidates require no items and a constrained abstentionReason.'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return deepFreeze(structuredClone(candidate));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const PROHIBITED_PATTERNS = Object.freeze([
|
|
169
|
+
['ROLLBACK_PLAN', /\b(?:rollback|roll back|revert)\b/i],
|
|
170
|
+
['EFFORT_ESTIMATE', /\b(?:(?:low|medium|high) effort|story points?|\d+(?:\.\d+)?\s*(?:minutes?|hours?|days?|weeks?)\b)/i],
|
|
171
|
+
['NUMERIC_CONFIDENCE', /\b(?:confidence(?: score)?\s*(?:is|:|=)?\s*\d+(?:\.\d+)?\s*%?|\d+(?:\.\d+)?%\s+confiden)/i],
|
|
172
|
+
['DEPENDENCY_ORDERING', /\b(?:before|after|first|then|next)\b[^.\n]{0,100}\b(?:install|migrate|update|upgrade)\b|\b(?:install|migrate|update|upgrade)\b[^.\n]{0,100}\b(?:before|after|first|then|next)\b|\b(?:prerequisite|required before|depends on)\b/i],
|
|
173
|
+
['REPOSITORY_LOCATION', /(?:^|[\s("'])\.{0,2}\/[^\s)"']+|\b(?:src|lib|app|test|tests)\/[A-Za-z0-9_./-]+|\b[A-Za-z0-9_.-]+\.(?:jsx?|tsx?|py|java|go|rs|json|ya?ml|toml)\b/i],
|
|
174
|
+
['SAFETY_OR_COMPLETION_CLAIM', /\b(?:safe to (?:upgrade|migrate)|ready to (?:upgrade|migrate|deploy)|(?:is|are|will be|now) (?:safe|ready|verified|complete)|upgrade is safe)\b/i],
|
|
175
|
+
['COMMAND_OR_CODE', /(?:^|\n)\s*(?:\$|>|#)\s*\S|\b(?:npm|pnpm|yarn|npx|pip3?|poetry|uv|cargo|mvn|gradle|go|bash|zsh|curl|wget|git|rm|cp|mv|sed|awk)\s+\S+/i]
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const IDENTIFIER_PATTERNS = Object.freeze([
|
|
179
|
+
/`([^`\n]{1,80})`/g,
|
|
180
|
+
/\b--[a-z0-9][a-z0-9-]*\b/gi,
|
|
181
|
+
/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b/g,
|
|
182
|
+
/\b[a-z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*\b/g,
|
|
183
|
+
/\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b/g,
|
|
184
|
+
/\b[A-Za-z_$][\w$]*\(\)/g
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const IDENTIFIER_STOPLIST = new Set([
|
|
188
|
+
'AI', 'API', 'CLI', 'CSS', 'HTML', 'HTTP', 'HTTPS', 'JSON', 'SDK', 'SQL', 'URL', 'XML'
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
function technicalIdentifiers(instruction) {
|
|
192
|
+
const identifiers = new Set();
|
|
193
|
+
for (const expression of IDENTIFIER_PATTERNS) {
|
|
194
|
+
expression.lastIndex = 0;
|
|
195
|
+
for (const match of instruction.matchAll(expression)) {
|
|
196
|
+
const token = match[1] ?? match[0];
|
|
197
|
+
const normalized = token.endsWith('()') ? token.slice(0, -2) : token;
|
|
198
|
+
if (!IDENTIFIER_STOPLIST.has(normalized)) identifiers.add(normalized);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...identifiers].sort(compareText);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function trustError(code, message, detailCode) {
|
|
205
|
+
return new MigrationChecklistTrustError(code, message, { detailCode });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function validateMigrationChecklistInstructionTrust(instruction, verifiedText) {
|
|
209
|
+
const inheritedViolations = validateMigrationChecklistInstructionContent(instruction);
|
|
210
|
+
if (inheritedViolations.length > 0) {
|
|
211
|
+
const violation = inheritedViolations[0];
|
|
212
|
+
throw trustError('PROHIBITED_CAPABILITY', violation.message, violation.code);
|
|
213
|
+
}
|
|
214
|
+
for (const [code, expression] of PROHIBITED_PATTERNS) {
|
|
215
|
+
if (expression.test(instruction)) {
|
|
216
|
+
throw trustError(
|
|
217
|
+
'PROHIBITED_CAPABILITY',
|
|
218
|
+
`Migration instruction contains prohibited capability ${code}.`,
|
|
219
|
+
code
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const identifier of technicalIdentifiers(instruction)) {
|
|
224
|
+
if (!verifiedText.includes(identifier)) {
|
|
225
|
+
throw trustError(
|
|
226
|
+
'UNSUPPORTED_IDENTIFIER',
|
|
227
|
+
'Migration instruction contains an identifier absent from its verified supporting excerpts.',
|
|
228
|
+
'IDENTIFIER_NOT_IN_EXCERPT'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compareExcerpt(left, right) {
|
|
235
|
+
return compareText(left.evidenceRef, right.evidenceRef) || compareText(left.text, right.text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function compareItem(left, right) {
|
|
239
|
+
return compareText(left.instruction, right.instruction)
|
|
240
|
+
|| compareText(JSON.stringify(left.evidenceRefs), JSON.stringify(right.evidenceRefs));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fail-closed trust validation. One invalid item rejects the entire candidate;
|
|
245
|
+
* exact excerpt matching normalizes line endings only.
|
|
246
|
+
*/
|
|
247
|
+
export function trustValidateMigrationChecklistCandidate(candidate, context) {
|
|
248
|
+
const allowed = new Set(context.evidenceAllowlist);
|
|
249
|
+
const evidenceById = new Map(context.evidence.map((evidence) => [evidence.id, evidence]));
|
|
250
|
+
const normalizedItems = [];
|
|
251
|
+
|
|
252
|
+
for (const item of candidate.items) {
|
|
253
|
+
const refs = [...item.evidenceRefs].sort(compareText);
|
|
254
|
+
const refSet = new Set(refs);
|
|
255
|
+
for (const ref of refs) {
|
|
256
|
+
if (!allowed.has(ref) || !evidenceById.has(ref)) {
|
|
257
|
+
throw trustError(
|
|
258
|
+
'EVIDENCE_REFERENCE_INVALID',
|
|
259
|
+
'Migration instruction references evidence outside its exact context allowlist.',
|
|
260
|
+
'UNKNOWN_EVIDENCE_REFERENCE'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const excerptRefs = new Set();
|
|
266
|
+
const excerpts = [...item.supportingExcerpts].sort(compareExcerpt);
|
|
267
|
+
for (const excerpt of excerpts) {
|
|
268
|
+
if (!refSet.has(excerpt.evidenceRef)
|
|
269
|
+
|| !allowed.has(excerpt.evidenceRef)
|
|
270
|
+
|| !evidenceById.has(excerpt.evidenceRef)
|
|
271
|
+
|| excerptRefs.has(excerpt.evidenceRef)) {
|
|
272
|
+
throw trustError(
|
|
273
|
+
'EVIDENCE_REFERENCE_INVALID',
|
|
274
|
+
'Each evidence reference must have exactly one excerpt from that same selected record.',
|
|
275
|
+
'EXCERPT_REFERENCE_MISMATCH'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
excerptRefs.add(excerpt.evidenceRef);
|
|
279
|
+
const content = normalizeLineEndings(evidenceById.get(excerpt.evidenceRef).content);
|
|
280
|
+
const text = normalizeLineEndings(excerpt.text);
|
|
281
|
+
if (!content.includes(text)) {
|
|
282
|
+
throw trustError(
|
|
283
|
+
'SUPPORTING_EXCERPT_INVALID',
|
|
284
|
+
'Supporting excerpt is not an exact substring of its selected evidence record.',
|
|
285
|
+
'EXCERPT_NOT_EXACT'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (excerptRefs.size !== refSet.size || refs.some((ref) => !excerptRefs.has(ref))) {
|
|
290
|
+
throw trustError(
|
|
291
|
+
'EVIDENCE_REFERENCE_INVALID',
|
|
292
|
+
'Every evidence reference must have exactly one supporting excerpt.',
|
|
293
|
+
'MISSING_SUPPORTING_EXCERPT'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const verifiedText = excerpts.map((excerpt) => normalizeLineEndings(excerpt.text)).join('\n');
|
|
298
|
+
validateMigrationChecklistInstructionTrust(item.instruction, verifiedText);
|
|
299
|
+
normalizedItems.push({
|
|
300
|
+
instruction: item.instruction,
|
|
301
|
+
evidenceRefs: refs,
|
|
302
|
+
supportingExcerpts: structuredClone(excerpts)
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const itemKeys = normalizedItems.map((item) => JSON.stringify(item));
|
|
307
|
+
if (new Set(itemKeys).size !== itemKeys.length) {
|
|
308
|
+
throw trustError(
|
|
309
|
+
'DUPLICATE_ITEM',
|
|
310
|
+
'Migration candidate contains duplicate instructions.',
|
|
311
|
+
'DUPLICATE_INSTRUCTION'
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return deepFreeze({
|
|
316
|
+
status: candidate.status,
|
|
317
|
+
items: normalizedItems.sort(compareItem),
|
|
318
|
+
abstentionReason: candidate.abstentionReason
|
|
319
|
+
});
|
|
320
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
2
|
+
import { buildMigrationChecklist, validateMigrationChecklist } from './migration-checklist.js';
|
|
3
|
+
|
|
4
|
+
function inputError(message) {
|
|
5
|
+
return new TypeError(`Migration Checklist assembly error: ${message}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Assemble normalized contexts plus versioned generator output through the MP-01 builder. */
|
|
9
|
+
export function assembleMigrationChecklist({
|
|
10
|
+
prepared,
|
|
11
|
+
generation,
|
|
12
|
+
qualification,
|
|
13
|
+
generatedAt
|
|
14
|
+
}) {
|
|
15
|
+
if (!prepared?.input || !Array.isArray(prepared.eligibleContexts)
|
|
16
|
+
|| !Array.isArray(prepared.fallbackRecords)) {
|
|
17
|
+
throw inputError('prepared MP-02 contexts are required.');
|
|
18
|
+
}
|
|
19
|
+
if (!generation?.input || !Array.isArray(generation.records)
|
|
20
|
+
|| !Array.isArray(generation.warnings)) {
|
|
21
|
+
throw inputError('normalized MP-03 generation output is required.');
|
|
22
|
+
}
|
|
23
|
+
if (canonicalJson(prepared.input) !== canonicalJson(generation.input)) {
|
|
24
|
+
throw inputError('MP-02 and MP-03 lineage inputs do not match.');
|
|
25
|
+
}
|
|
26
|
+
if (!qualification || !Array.isArray(qualification.limitations)) {
|
|
27
|
+
throw inputError('a qualification guard result is required.');
|
|
28
|
+
}
|
|
29
|
+
const checklist = buildMigrationChecklist({
|
|
30
|
+
input: prepared.input,
|
|
31
|
+
repository: prepared.input.projectManifest.repository,
|
|
32
|
+
dependencies: generation.records,
|
|
33
|
+
limitations: qualification.limitations,
|
|
34
|
+
generatedAt
|
|
35
|
+
});
|
|
36
|
+
return validateMigrationChecklist(checklist);
|
|
37
|
+
}
|