@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,702 @@
|
|
|
1
|
+
import { AI_RUNTIME_CONTRACT_VERSION, validateAiRuntime } from '../ai-runtime.js';
|
|
2
|
+
import { isAiRuntimeError } from '../ai-runtime-error.js';
|
|
3
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
4
|
+
import { compareText } from '../portable.js';
|
|
5
|
+
import {
|
|
6
|
+
isMigrationChecklistCandidateError,
|
|
7
|
+
isMigrationChecklistTrustError,
|
|
8
|
+
MIGRATION_CHECKLIST_CANDIDATE_SCHEMA,
|
|
9
|
+
trustValidateMigrationChecklistCandidate,
|
|
10
|
+
validateMigrationChecklistCandidate
|
|
11
|
+
} from './ai-candidate.js';
|
|
12
|
+
import {
|
|
13
|
+
isMigrationExtractiveCandidateError,
|
|
14
|
+
MIGRATION_EXTRACTIVE_CANDIDATE_SCHEMA,
|
|
15
|
+
trustValidateMigrationExtractiveCandidate,
|
|
16
|
+
validateMigrationExtractiveCandidate
|
|
17
|
+
} from './extractive-candidate.js';
|
|
18
|
+
import {
|
|
19
|
+
buildMigrationExtractivePrompt,
|
|
20
|
+
MIGRATION_EXTRACTIVE_PLANNING_TASK,
|
|
21
|
+
MIGRATION_EXTRACTIVE_PROMPT_VERSION,
|
|
22
|
+
MIGRATION_EXTRACTIVE_SCHEMA_NAME
|
|
23
|
+
} from './extractive-prompt.js';
|
|
24
|
+
import {
|
|
25
|
+
MIGRATION_LOCATION_REASON_CODES,
|
|
26
|
+
MIGRATION_TASK_CONTEXT_VERSION
|
|
27
|
+
} from './context-runtime.js';
|
|
28
|
+
import { migrationChecklistItemId } from './migration-checklist.js';
|
|
29
|
+
import {
|
|
30
|
+
buildMigrationChecklistPrompt,
|
|
31
|
+
MIGRATION_PLANNING_PROMPT_VERSION,
|
|
32
|
+
MIGRATION_PLANNING_SCHEMA_NAME,
|
|
33
|
+
MIGRATION_PLANNING_TASK
|
|
34
|
+
} from './prompt.js';
|
|
35
|
+
|
|
36
|
+
export const MIGRATION_GENERATION_RESULT_VERSION = '1';
|
|
37
|
+
export const MIGRATION_EXTRACTIVE_GENERATION_RESULT_VERSION = '2';
|
|
38
|
+
export const MIGRATION_GENERATION_WARNING_CODES = Object.freeze([
|
|
39
|
+
'MODEL_ABSTAINED',
|
|
40
|
+
'OUTPUT_JSON_INVALID',
|
|
41
|
+
'OUTPUT_SCHEMA_INVALID',
|
|
42
|
+
'OUTPUT_SEMANTICS_INVALID',
|
|
43
|
+
'TRUST_VALIDATION_REJECTED',
|
|
44
|
+
'AI_RUNTIME_FAILED'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const DIGEST_EXPRESSION = /^sha256:[a-f0-9]{64}$/;
|
|
48
|
+
const ACTION_EVIDENCE_KINDS = new Set([
|
|
49
|
+
'migrationGuide', 'breakingChanges', 'deprecations', 'releaseNotes', 'changelog'
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const compareLimitations = (left, right) => (
|
|
53
|
+
compareText(left.code, right.code) || compareText(left.message, right.message)
|
|
54
|
+
);
|
|
55
|
+
const compareLocations = (left, right) => (
|
|
56
|
+
compareText(left.impactEvidenceId, right.impactEvidenceId)
|
|
57
|
+
|| compareText(left.symbol, right.symbol)
|
|
58
|
+
|| compareText(left.file, right.file)
|
|
59
|
+
);
|
|
60
|
+
const compareRecords = (left, right) => (
|
|
61
|
+
compareText(left.dependency.projectId, right.dependency.projectId)
|
|
62
|
+
|| compareText(left.dependency.manifest, right.dependency.manifest)
|
|
63
|
+
|| compareText(left.dependency.dependencyType, right.dependency.dependencyType)
|
|
64
|
+
|| compareText(left.dependency.packageId, right.dependency.packageId)
|
|
65
|
+
|| compareText(left.analysisResultId ?? '', right.analysisResultId ?? '')
|
|
66
|
+
|| compareText(left.decisionId ?? '', right.decisionId ?? '')
|
|
67
|
+
);
|
|
68
|
+
const compareWarnings = (left, right) => (
|
|
69
|
+
compareText(left.contextId, right.contextId)
|
|
70
|
+
|| compareText(left.code, right.code)
|
|
71
|
+
|| compareText(left.detailCode, right.detailCode)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
function deepFreeze(value) {
|
|
75
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
76
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
77
|
+
return Object.freeze(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function notifyContext(listener, event) {
|
|
81
|
+
if (typeof listener !== 'function') return;
|
|
82
|
+
try {
|
|
83
|
+
listener(deepFreeze(structuredClone(event)));
|
|
84
|
+
} catch {
|
|
85
|
+
// Progress instrumentation must never alter checklist generation semantics.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function throwIfCancelled(signal) {
|
|
90
|
+
if (!signal?.aborted) return;
|
|
91
|
+
const error = new Error('Migration Checklist generation was cancelled.', { cause: signal.reason });
|
|
92
|
+
error.code = 'ANALYSIS_CANCELLED';
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sortedUniqueText(values = []) {
|
|
97
|
+
return [...new Set(values)].sort(compareText);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sortedUniqueLimitations(values = []) {
|
|
101
|
+
const unique = new Map();
|
|
102
|
+
for (const value of values) unique.set(`${value.code}\0${value.message}`, structuredClone(value));
|
|
103
|
+
return [...unique.values()].sort(compareLimitations);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inputError(message) {
|
|
107
|
+
return new TypeError(`Migration Checklist Generator input error: ${message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertString(value, field) {
|
|
111
|
+
if (typeof value !== 'string' || value.length === 0) throw inputError(`${field} must be a non-empty string.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateEligibleContext(context) {
|
|
115
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
116
|
+
throw inputError('eligible context must be an object.');
|
|
117
|
+
}
|
|
118
|
+
if (context.contextVersion !== MIGRATION_TASK_CONTEXT_VERSION) {
|
|
119
|
+
throw inputError(`contextVersion must be ${MIGRATION_TASK_CONTEXT_VERSION}.`);
|
|
120
|
+
}
|
|
121
|
+
if (!DIGEST_EXPRESSION.test(context.contextId)) throw inputError('contextId must be a SHA-256 digest.');
|
|
122
|
+
if (!context.dependency || !context.versions || !context.finding) {
|
|
123
|
+
throw inputError(`context ${context.contextId} is missing dependency, versions, or finding identity.`);
|
|
124
|
+
}
|
|
125
|
+
assertString(context.analysisResultId, 'analysisResultId');
|
|
126
|
+
if (!DIGEST_EXPRESSION.test(context.analysisResultId)) {
|
|
127
|
+
throw inputError('analysisResultId must be a SHA-256 digest.');
|
|
128
|
+
}
|
|
129
|
+
for (const field of [
|
|
130
|
+
'projectId', 'packageId', 'declaredName', 'normalizedName', 'ecosystem', 'registry',
|
|
131
|
+
'dependencyType', 'manifest'
|
|
132
|
+
]) {
|
|
133
|
+
assertString(context.dependency[field], `dependency.${field}`);
|
|
134
|
+
}
|
|
135
|
+
if (context.versions.targetVersion === null) throw inputError('eligible targetVersion cannot be null.');
|
|
136
|
+
assertString(context.versions.targetVersion, 'versions.targetVersion');
|
|
137
|
+
assertString(context.finding.id, 'finding.id');
|
|
138
|
+
assertString(context.finding.summary, 'finding.summary');
|
|
139
|
+
if (context.finding.kind !== 'breakingChange') throw inputError('finding.kind must be breakingChange.');
|
|
140
|
+
if (!Array.isArray(context.finding.appliesToVersions)) {
|
|
141
|
+
throw inputError('finding.appliesToVersions must be an array.');
|
|
142
|
+
}
|
|
143
|
+
if (context.eligibility?.status !== 'ELIGIBLE' || context.eligibility?.reasonCode !== 'ELIGIBLE') {
|
|
144
|
+
throw inputError(`context ${context.contextId} is not eligible.`);
|
|
145
|
+
}
|
|
146
|
+
if (context.requiresHumanReview !== true) throw inputError('requiresHumanReview must be true.');
|
|
147
|
+
if (context.decisionId !== undefined) {
|
|
148
|
+
if (!DIGEST_EXPRESSION.test(context.decisionId)
|
|
149
|
+
|| !['PLAN_UPGRADE', 'UPGRADE_NOW'].includes(context.decision?.status)
|
|
150
|
+
|| context.decision?.recommendationDriver === null
|
|
151
|
+
|| context.humanReviewRequired !== true) {
|
|
152
|
+
throw inputError(`context ${context.contextId} has an invalid actionable Upgrade Decision projection.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(context.evidence) || !Array.isArray(context.evidenceAllowlist)
|
|
156
|
+
|| !Array.isArray(context.finding.evidenceRefs) || context.evidence.length === 0) {
|
|
157
|
+
throw inputError(`context ${context.contextId} must contain selected evidence and allowlists.`);
|
|
158
|
+
}
|
|
159
|
+
const evidenceIds = context.evidence.map((item) => item.id);
|
|
160
|
+
if (evidenceIds.some((id) => !DIGEST_EXPRESSION.test(id))
|
|
161
|
+
|| evidenceIds.some((id, index) => evidenceIds.indexOf(id) !== index)
|
|
162
|
+
|| context.evidence.some((item) => (
|
|
163
|
+
typeof item.content !== 'string' || item.content.length === 0
|
|
164
|
+
|| typeof item.sourceId !== 'string' || item.sourceId.length === 0
|
|
165
|
+
|| typeof item.kind !== 'string' || item.kind.length === 0
|
|
166
|
+
|| typeof item.authority !== 'string' || item.authority.length === 0
|
|
167
|
+
|| typeof item.trust !== 'string' || item.trust.length === 0
|
|
168
|
+
|| !DIGEST_EXPRESSION.test(item.contentDigest)
|
|
169
|
+
|| !Array.isArray(item.releaseVersions)
|
|
170
|
+
|| !item.releaseVersions.includes(context.versions.targetVersion)
|
|
171
|
+
|| !['officialProject', 'publisherProvided'].includes(item.authority)
|
|
172
|
+
|| !['official', 'publisher'].includes(item.trust)
|
|
173
|
+
|| !ACTION_EVIDENCE_KINDS.has(item.kind)
|
|
174
|
+
))) {
|
|
175
|
+
throw inputError(`context ${context.contextId} contains invalid or duplicate selected evidence.`);
|
|
176
|
+
}
|
|
177
|
+
const expectedRefs = sortedUniqueText(evidenceIds);
|
|
178
|
+
if (JSON.stringify(context.evidenceAllowlist) !== JSON.stringify(expectedRefs)
|
|
179
|
+
|| JSON.stringify(context.finding.evidenceRefs) !== JSON.stringify(expectedRefs)) {
|
|
180
|
+
throw inputError(`context ${context.contextId} evidence allowlists are inconsistent.`);
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(context.positiveCandidateLocations)
|
|
183
|
+
|| !Array.isArray(context.humanReviewReasons)
|
|
184
|
+
|| !Array.isArray(context.limitations)
|
|
185
|
+
|| !context.locationEligibility) {
|
|
186
|
+
throw inputError(`context ${context.contextId} is not a normalized MP-02 context.`);
|
|
187
|
+
}
|
|
188
|
+
if (!MIGRATION_LOCATION_REASON_CODES.includes(context.locationEligibility.reasonCode)
|
|
189
|
+
|| !['ELIGIBLE', 'REVIEW_REQUIRED'].includes(context.locationEligibility.status)) {
|
|
190
|
+
throw inputError(`context ${context.contextId} has invalid location eligibility.`);
|
|
191
|
+
}
|
|
192
|
+
for (const location of context.positiveCandidateLocations) {
|
|
193
|
+
if (!DIGEST_EXPRESSION.test(location.impactEvidenceId)) {
|
|
194
|
+
throw inputError(`context ${context.contextId} contains an invalid impact evidence id.`);
|
|
195
|
+
}
|
|
196
|
+
assertString(location.symbol, 'candidate location symbol');
|
|
197
|
+
assertString(location.file, 'candidate location file');
|
|
198
|
+
}
|
|
199
|
+
const locationKeys = context.positiveCandidateLocations.map((location) => (
|
|
200
|
+
`${location.impactEvidenceId}\0${location.symbol}\0${location.file}`
|
|
201
|
+
));
|
|
202
|
+
if (new Set(locationKeys).size !== locationKeys.length) {
|
|
203
|
+
throw inputError(`context ${context.contextId} contains duplicate candidate locations.`);
|
|
204
|
+
}
|
|
205
|
+
if ((context.positiveCandidateLocations.length > 0)
|
|
206
|
+
!== (context.locationEligibility.reasonCode === 'POSITIVE_USAGE_MATCH')) {
|
|
207
|
+
throw inputError(`context ${context.contextId} location eligibility is inconsistent.`);
|
|
208
|
+
}
|
|
209
|
+
return context;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sameIdentity(left, right) {
|
|
213
|
+
return canonicalJson(left.dependency) === canonicalJson(right.dependency)
|
|
214
|
+
&& canonicalJson(left.versions) === canonicalJson(right.versions)
|
|
215
|
+
&& left.analysisStatus === right.analysisStatus
|
|
216
|
+
&& (left.decisionId ?? null) === (right.decisionId ?? null);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const HANDOFF_BASIS_FIELDS = Object.freeze([
|
|
220
|
+
'decisionId',
|
|
221
|
+
'decision',
|
|
222
|
+
'affectedAreas',
|
|
223
|
+
'coverage',
|
|
224
|
+
'verification',
|
|
225
|
+
'officialEvidence',
|
|
226
|
+
'preconditions',
|
|
227
|
+
'recovery',
|
|
228
|
+
'reviewQuestions',
|
|
229
|
+
'missingInformation',
|
|
230
|
+
'nextStep',
|
|
231
|
+
'humanReviewRequired'
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
function handoffBasis(context) {
|
|
235
|
+
return Object.fromEntries(HANDOFF_BASIS_FIELDS
|
|
236
|
+
.filter((field) => Object.hasOwn(context, field))
|
|
237
|
+
.map((field) => [field, structuredClone(context[field])]));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function baseRecord(context) {
|
|
241
|
+
return {
|
|
242
|
+
analysisResultId: context.analysisResultId,
|
|
243
|
+
...handoffBasis(context),
|
|
244
|
+
dependency: structuredClone(context.dependency),
|
|
245
|
+
versions: structuredClone(context.versions),
|
|
246
|
+
analysisStatus: 'analyzed',
|
|
247
|
+
selectedEvidenceRefs: [...context.evidenceAllowlist],
|
|
248
|
+
findings: [],
|
|
249
|
+
limitations: structuredClone(context.limitations)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function checklistItem(analysisResultId, findingId, item) {
|
|
254
|
+
return {
|
|
255
|
+
id: migrationChecklistItemId(analysisResultId, findingId, item),
|
|
256
|
+
...item
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function locationItems(context) {
|
|
261
|
+
return [...context.positiveCandidateLocations]
|
|
262
|
+
.sort(compareLocations)
|
|
263
|
+
.map((location) => checklistItem(context.analysisResultId, context.finding.id, {
|
|
264
|
+
kind: 'REVIEW_CANDIDATE_USAGE',
|
|
265
|
+
basis: 'DETERMINISTIC',
|
|
266
|
+
instruction: `Review the validated usage candidate for symbol "${location.symbol}" in the referenced repository file.`,
|
|
267
|
+
findingId: context.finding.id,
|
|
268
|
+
evidenceRefs: [...context.evidenceAllowlist],
|
|
269
|
+
candidateLocations: [structuredClone(location)],
|
|
270
|
+
requiresHumanReview: true
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function actionableRecord(context, trustedCandidate) {
|
|
275
|
+
const items = trustedCandidate.items.map((candidateItem) => checklistItem(
|
|
276
|
+
context.analysisResultId,
|
|
277
|
+
context.finding.id,
|
|
278
|
+
{
|
|
279
|
+
kind: 'REVIEW_MIGRATION_INSTRUCTION',
|
|
280
|
+
basis: 'AI_AUTHORED',
|
|
281
|
+
instruction: candidateItem.instruction,
|
|
282
|
+
findingId: context.finding.id,
|
|
283
|
+
evidenceRefs: [...candidateItem.evidenceRefs],
|
|
284
|
+
candidateLocations: [],
|
|
285
|
+
requiresHumanReview: true
|
|
286
|
+
}
|
|
287
|
+
));
|
|
288
|
+
items.push(...locationItems(context));
|
|
289
|
+
items.sort((left, right) => compareText(left.id, right.id));
|
|
290
|
+
const record = baseRecord(context);
|
|
291
|
+
record.findings.push({
|
|
292
|
+
id: context.finding.id,
|
|
293
|
+
kind: context.finding.kind,
|
|
294
|
+
summary: context.finding.summary,
|
|
295
|
+
eligibilityReasonCode: 'ELIGIBLE',
|
|
296
|
+
evidenceRefs: [...context.evidenceAllowlist],
|
|
297
|
+
positiveImpactLocations: structuredClone(context.positiveCandidateLocations).sort(compareLocations),
|
|
298
|
+
items
|
|
299
|
+
});
|
|
300
|
+
return record;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const FALLBACK_DETAILS = Object.freeze({
|
|
304
|
+
NO_GROUNDED_ACTION: Object.freeze({
|
|
305
|
+
instruction: 'Manual review is required because the selected evidence did not yield an explicit migration action.',
|
|
306
|
+
limitationMessage: 'The model abstained because selected evidence did not provide a sufficiently explicit migration action.'
|
|
307
|
+
}),
|
|
308
|
+
MANUAL_REVIEW_REQUIRED: Object.freeze({
|
|
309
|
+
instruction: 'Manual review is required because the generated migration draft did not pass validation.',
|
|
310
|
+
limitationMessage: 'The generated migration draft was not published because generation or trust validation failed.'
|
|
311
|
+
})
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
function fallbackRecord(context, reasonCode, limitationCode, limitationMessage) {
|
|
315
|
+
const details = FALLBACK_DETAILS[reasonCode];
|
|
316
|
+
const record = baseRecord(context);
|
|
317
|
+
record.limitations = sortedUniqueLimitations([
|
|
318
|
+
...record.limitations,
|
|
319
|
+
{ code: limitationCode, message: limitationMessage ?? details.limitationMessage }
|
|
320
|
+
]);
|
|
321
|
+
const manualItem = checklistItem(context.analysisResultId, context.finding.id, {
|
|
322
|
+
kind: 'MANUAL_REVIEW_REQUIRED',
|
|
323
|
+
basis: 'DETERMINISTIC',
|
|
324
|
+
instruction: details.instruction,
|
|
325
|
+
findingId: context.finding.id,
|
|
326
|
+
evidenceRefs: [...context.evidenceAllowlist],
|
|
327
|
+
candidateLocations: [],
|
|
328
|
+
requiresHumanReview: true
|
|
329
|
+
});
|
|
330
|
+
record.findings.push({
|
|
331
|
+
id: context.finding.id,
|
|
332
|
+
kind: context.finding.kind,
|
|
333
|
+
summary: context.finding.summary,
|
|
334
|
+
eligibilityReasonCode: reasonCode,
|
|
335
|
+
evidenceRefs: [...context.evidenceAllowlist],
|
|
336
|
+
positiveImpactLocations: structuredClone(context.positiveCandidateLocations).sort(compareLocations),
|
|
337
|
+
items: [manualItem]
|
|
338
|
+
});
|
|
339
|
+
return record;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function warning(context, code, detailCode, message) {
|
|
343
|
+
return {
|
|
344
|
+
contextId: context.contextId,
|
|
345
|
+
analysisResultId: context.analysisResultId,
|
|
346
|
+
findingId: context.finding.id,
|
|
347
|
+
code,
|
|
348
|
+
detailCode,
|
|
349
|
+
message
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function runtimeFailure(context, error) {
|
|
354
|
+
const detailCode = isAiRuntimeError(error) ? error.code : 'UNKNOWN';
|
|
355
|
+
return {
|
|
356
|
+
outcome: 'failed',
|
|
357
|
+
record: fallbackRecord(context, 'MANUAL_REVIEW_REQUIRED', 'AI_RUNTIME_FAILED'),
|
|
358
|
+
warning: warning(
|
|
359
|
+
context,
|
|
360
|
+
'AI_RUNTIME_FAILED',
|
|
361
|
+
detailCode,
|
|
362
|
+
'AI runtime failed for this migration context; deterministic manual review is required.'
|
|
363
|
+
)
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function candidateFailure(context, error) {
|
|
368
|
+
if (isMigrationChecklistCandidateError(error) || isMigrationExtractiveCandidateError(error)) {
|
|
369
|
+
return {
|
|
370
|
+
outcome: 'rejected',
|
|
371
|
+
record: fallbackRecord(context, 'MANUAL_REVIEW_REQUIRED', error.code),
|
|
372
|
+
warning: warning(
|
|
373
|
+
context,
|
|
374
|
+
error.code,
|
|
375
|
+
error.code,
|
|
376
|
+
'AI output did not satisfy the Migration Checklist candidate contract; deterministic manual review is required.'
|
|
377
|
+
)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (isMigrationChecklistTrustError(error)) {
|
|
381
|
+
return {
|
|
382
|
+
outcome: 'rejected',
|
|
383
|
+
record: fallbackRecord(context, 'MANUAL_REVIEW_REQUIRED', 'TRUST_VALIDATION_REJECTED'),
|
|
384
|
+
warning: warning(
|
|
385
|
+
context,
|
|
386
|
+
'TRUST_VALIDATION_REJECTED',
|
|
387
|
+
error.detailCode ?? error.code,
|
|
388
|
+
'AI output failed evidence or capability trust validation; deterministic manual review is required.'
|
|
389
|
+
)
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function generateMigrationChecklistForContextWithContract(context, {
|
|
396
|
+
aiRuntime,
|
|
397
|
+
runId,
|
|
398
|
+
promptVersion,
|
|
399
|
+
signal
|
|
400
|
+
}, contract) {
|
|
401
|
+
throwIfCancelled(signal);
|
|
402
|
+
validateEligibleContext(context);
|
|
403
|
+
validateAiRuntime(aiRuntime);
|
|
404
|
+
const prompt = contract.buildPrompt({
|
|
405
|
+
context,
|
|
406
|
+
outputSchema: contract.candidateSchema,
|
|
407
|
+
promptVersion
|
|
408
|
+
});
|
|
409
|
+
let runtimeResult;
|
|
410
|
+
try {
|
|
411
|
+
runtimeResult = await aiRuntime.generateStructured({
|
|
412
|
+
contractVersion: AI_RUNTIME_CONTRACT_VERSION,
|
|
413
|
+
runId,
|
|
414
|
+
contextId: context.contextId,
|
|
415
|
+
task: contract.task,
|
|
416
|
+
promptVersion,
|
|
417
|
+
systemPrompt: prompt.system,
|
|
418
|
+
userPrompt: prompt.user,
|
|
419
|
+
structuredOutput: {
|
|
420
|
+
mode: 'jsonSchema',
|
|
421
|
+
name: contract.schemaName,
|
|
422
|
+
schema: contract.candidateSchema
|
|
423
|
+
},
|
|
424
|
+
...(signal ? { signal } : {})
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return deepFreeze(runtimeFailure(context, error));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const candidate = contract.validateCandidate(runtimeResult?.output);
|
|
432
|
+
if (candidate.status === 'ABSTAIN') {
|
|
433
|
+
return deepFreeze({
|
|
434
|
+
outcome: 'abstained',
|
|
435
|
+
record: fallbackRecord(
|
|
436
|
+
context,
|
|
437
|
+
'NO_GROUNDED_ACTION',
|
|
438
|
+
'MODEL_ABSTAINED',
|
|
439
|
+
`The model abstained with constrained reason ${candidate.abstentionReason}; no AI-authored action was created.`
|
|
440
|
+
),
|
|
441
|
+
warning: warning(
|
|
442
|
+
context,
|
|
443
|
+
'MODEL_ABSTAINED',
|
|
444
|
+
candidate.abstentionReason,
|
|
445
|
+
'The model validly abstained; deterministic manual review is required.'
|
|
446
|
+
)
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const trusted = contract.trustCandidate(candidate, context);
|
|
450
|
+
return deepFreeze({
|
|
451
|
+
outcome: 'generated',
|
|
452
|
+
record: actionableRecord(context, trusted),
|
|
453
|
+
warning: null
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return deepFreeze(candidateFailure(context, error));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const FREE_FORM_CONTRACT = Object.freeze({
|
|
461
|
+
task: MIGRATION_PLANNING_TASK,
|
|
462
|
+
candidateSchema: MIGRATION_CHECKLIST_CANDIDATE_SCHEMA,
|
|
463
|
+
schemaName: MIGRATION_PLANNING_SCHEMA_NAME,
|
|
464
|
+
buildPrompt: buildMigrationChecklistPrompt,
|
|
465
|
+
validateCandidate: validateMigrationChecklistCandidate,
|
|
466
|
+
trustCandidate: trustValidateMigrationChecklistCandidate
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const EXTRACTIVE_CONTRACT = Object.freeze({
|
|
470
|
+
task: MIGRATION_EXTRACTIVE_PLANNING_TASK,
|
|
471
|
+
candidateSchema: MIGRATION_EXTRACTIVE_CANDIDATE_SCHEMA,
|
|
472
|
+
schemaName: MIGRATION_EXTRACTIVE_SCHEMA_NAME,
|
|
473
|
+
buildPrompt: buildMigrationExtractivePrompt,
|
|
474
|
+
validateCandidate: validateMigrationExtractiveCandidate,
|
|
475
|
+
trustCandidate: trustValidateMigrationExtractiveCandidate
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
/** Historical free-form v1 generation retained for evaluation reproducibility. */
|
|
479
|
+
export async function generateMigrationChecklistForContext(context, {
|
|
480
|
+
aiRuntime,
|
|
481
|
+
signal,
|
|
482
|
+
runId = `migration:${context.contextId}`,
|
|
483
|
+
promptVersion = MIGRATION_PLANNING_PROMPT_VERSION
|
|
484
|
+
} = {}) {
|
|
485
|
+
return generateMigrationChecklistForContextWithContract(context, {
|
|
486
|
+
aiRuntime, runId, promptVersion, signal
|
|
487
|
+
}, FREE_FORM_CONTRACT);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Production extractive v2 generation for new experimental checklist runs. */
|
|
491
|
+
export async function generateMigrationExtractiveChecklistForContext(context, {
|
|
492
|
+
aiRuntime,
|
|
493
|
+
signal,
|
|
494
|
+
runId = `migration-extractive:${context.contextId}`,
|
|
495
|
+
promptVersion = MIGRATION_EXTRACTIVE_PROMPT_VERSION
|
|
496
|
+
} = {}) {
|
|
497
|
+
return generateMigrationChecklistForContextWithContract(context, {
|
|
498
|
+
aiRuntime, runId, promptVersion, signal
|
|
499
|
+
}, EXTRACTIVE_CONTRACT);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function validatePrepared(prepared) {
|
|
503
|
+
if (!prepared || typeof prepared !== 'object' || Array.isArray(prepared)) {
|
|
504
|
+
throw inputError('prepared contexts must be an object.');
|
|
505
|
+
}
|
|
506
|
+
if (prepared.contextVersion !== MIGRATION_TASK_CONTEXT_VERSION
|
|
507
|
+
|| !prepared.input
|
|
508
|
+
|| !Array.isArray(prepared.eligibleContexts)
|
|
509
|
+
|| !Array.isArray(prepared.fallbackRecords)) {
|
|
510
|
+
throw inputError('prepared contexts must be normalized MP-02 output.');
|
|
511
|
+
}
|
|
512
|
+
const contextIds = new Set();
|
|
513
|
+
const findingKeys = new Set();
|
|
514
|
+
for (const context of prepared.eligibleContexts) {
|
|
515
|
+
validateEligibleContext(context);
|
|
516
|
+
if (contextIds.has(context.contextId)) throw inputError(`duplicate contextId ${context.contextId}.`);
|
|
517
|
+
contextIds.add(context.contextId);
|
|
518
|
+
const key = `${context.analysisResultId}\0${context.finding.id}`;
|
|
519
|
+
if (findingKeys.has(key)) throw inputError('duplicate eligible analysis result and finding identity.');
|
|
520
|
+
findingKeys.add(key);
|
|
521
|
+
}
|
|
522
|
+
const fallbackIds = new Set();
|
|
523
|
+
for (const record of prepared.fallbackRecords) {
|
|
524
|
+
validateFallbackRecord(record);
|
|
525
|
+
const fallbackId = record.analysisResultId ?? record.decisionId;
|
|
526
|
+
if (fallbackIds.has(fallbackId)) {
|
|
527
|
+
throw inputError(`duplicate fallback identity ${fallbackId}.`);
|
|
528
|
+
}
|
|
529
|
+
fallbackIds.add(fallbackId);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function validateFallbackRecord(record) {
|
|
534
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
535
|
+
throw inputError('fallback record must be an object.');
|
|
536
|
+
}
|
|
537
|
+
if (record.analysisResultId !== null) {
|
|
538
|
+
assertString(record.analysisResultId, 'fallback analysisResultId');
|
|
539
|
+
}
|
|
540
|
+
if (!record.dependency || !record.versions || typeof record.analysisStatus !== 'string') {
|
|
541
|
+
throw inputError(`fallback ${record.analysisResultId} is missing normalized identity.`);
|
|
542
|
+
}
|
|
543
|
+
if (record.decisionId !== undefined && !DIGEST_EXPRESSION.test(record.decisionId)) {
|
|
544
|
+
throw inputError(`fallback ${record.analysisResultId ?? record.decisionId} has invalid decision identity.`);
|
|
545
|
+
}
|
|
546
|
+
for (const field of [
|
|
547
|
+
'projectId', 'packageId', 'declaredName', 'normalizedName', 'ecosystem', 'registry',
|
|
548
|
+
'dependencyType', 'manifest'
|
|
549
|
+
]) {
|
|
550
|
+
assertString(record.dependency[field], `fallback dependency.${field}`);
|
|
551
|
+
}
|
|
552
|
+
if (!Array.isArray(record.selectedEvidenceRefs)
|
|
553
|
+
|| !Array.isArray(record.findings)
|
|
554
|
+
|| !Array.isArray(record.limitations)) {
|
|
555
|
+
throw inputError(`fallback ${record.analysisResultId} must contain normalized arrays.`);
|
|
556
|
+
}
|
|
557
|
+
if (record.selectedEvidenceRefs.some((ref) => !DIGEST_EXPRESSION.test(ref))) {
|
|
558
|
+
throw inputError(`fallback ${record.analysisResultId} contains an invalid evidence ref.`);
|
|
559
|
+
}
|
|
560
|
+
const findingIds = new Set();
|
|
561
|
+
for (const finding of record.findings) {
|
|
562
|
+
assertString(finding?.id, 'fallback finding id');
|
|
563
|
+
if (findingIds.has(finding.id)) {
|
|
564
|
+
throw inputError(`fallback ${record.analysisResultId} contains duplicate finding ${finding.id}.`);
|
|
565
|
+
}
|
|
566
|
+
findingIds.add(finding.id);
|
|
567
|
+
if (!Array.isArray(finding.evidenceRefs)
|
|
568
|
+
|| !Array.isArray(finding.positiveImpactLocations)
|
|
569
|
+
|| !Array.isArray(finding.items)) {
|
|
570
|
+
throw inputError(`fallback ${record.analysisResultId}/${finding.id} is not normalized.`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function mergeRecord(records, incoming) {
|
|
576
|
+
const recordKey = incoming.analysisResultId ?? incoming.decisionId;
|
|
577
|
+
const existing = records.get(recordKey);
|
|
578
|
+
if (!existing) {
|
|
579
|
+
records.set(recordKey, structuredClone(incoming));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!sameIdentity(existing, incoming)) {
|
|
583
|
+
throw inputError(`conflicting identity for analysisResultId ${incoming.analysisResultId}.`);
|
|
584
|
+
}
|
|
585
|
+
const findingIds = new Set(existing.findings.map((finding) => finding.id));
|
|
586
|
+
for (const finding of incoming.findings) {
|
|
587
|
+
if (findingIds.has(finding.id)) {
|
|
588
|
+
throw inputError(`duplicate finding ${incoming.analysisResultId}/${finding.id} during record merge.`);
|
|
589
|
+
}
|
|
590
|
+
existing.findings.push(structuredClone(finding));
|
|
591
|
+
findingIds.add(finding.id);
|
|
592
|
+
}
|
|
593
|
+
existing.findings.sort((left, right) => compareText(left.id, right.id));
|
|
594
|
+
existing.selectedEvidenceRefs = sortedUniqueText([
|
|
595
|
+
...existing.selectedEvidenceRefs,
|
|
596
|
+
...incoming.selectedEvidenceRefs
|
|
597
|
+
]);
|
|
598
|
+
existing.limitations = sortedUniqueLimitations([
|
|
599
|
+
...existing.limitations,
|
|
600
|
+
...incoming.limitations
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function generateMigrationChecklistDraftsWithContract(prepared, {
|
|
605
|
+
aiRuntime,
|
|
606
|
+
runIdPrefix = 'migration',
|
|
607
|
+
promptVersion,
|
|
608
|
+
onContextEvent,
|
|
609
|
+
signal
|
|
610
|
+
}, {
|
|
611
|
+
generateContext,
|
|
612
|
+
resultVersion
|
|
613
|
+
}) {
|
|
614
|
+
validatePrepared(prepared);
|
|
615
|
+
validateAiRuntime(aiRuntime);
|
|
616
|
+
assertString(runIdPrefix, 'runIdPrefix');
|
|
617
|
+
assertString(promptVersion, 'promptVersion');
|
|
618
|
+
const records = new Map();
|
|
619
|
+
for (const record of prepared.fallbackRecords) mergeRecord(records, record);
|
|
620
|
+
const warnings = [];
|
|
621
|
+
const summary = {
|
|
622
|
+
attempted: prepared.eligibleContexts.length,
|
|
623
|
+
generated: 0,
|
|
624
|
+
abstained: 0,
|
|
625
|
+
rejected: 0,
|
|
626
|
+
failed: 0,
|
|
627
|
+
preservedFallbackRecordCount: prepared.fallbackRecords.length,
|
|
628
|
+
recordCount: 0
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const contexts = [...prepared.eligibleContexts].sort((left, right) => (
|
|
632
|
+
compareText(left.dependency.projectId, right.dependency.projectId)
|
|
633
|
+
|| compareText(left.dependency.manifest, right.dependency.manifest)
|
|
634
|
+
|| compareText(left.dependency.packageId, right.dependency.packageId)
|
|
635
|
+
|| compareText(left.finding.id, right.finding.id)
|
|
636
|
+
|| compareText(left.contextId, right.contextId)
|
|
637
|
+
));
|
|
638
|
+
for (const context of contexts) {
|
|
639
|
+
throwIfCancelled(signal);
|
|
640
|
+
notifyContext(onContextEvent, {
|
|
641
|
+
phase: 'start',
|
|
642
|
+
contextId: context.contextId,
|
|
643
|
+
analysisResultId: context.analysisResultId,
|
|
644
|
+
findingId: context.finding.id,
|
|
645
|
+
packageName: context.dependency.declaredName
|
|
646
|
+
});
|
|
647
|
+
const result = await generateContext(context, {
|
|
648
|
+
aiRuntime,
|
|
649
|
+
runId: `${runIdPrefix}:${context.contextId}`,
|
|
650
|
+
promptVersion,
|
|
651
|
+
signal
|
|
652
|
+
});
|
|
653
|
+
throwIfCancelled(signal);
|
|
654
|
+
summary[result.outcome] += 1;
|
|
655
|
+
mergeRecord(records, result.record);
|
|
656
|
+
if (result.warning) warnings.push(structuredClone(result.warning));
|
|
657
|
+
notifyContext(onContextEvent, {
|
|
658
|
+
phase: 'complete',
|
|
659
|
+
contextId: context.contextId,
|
|
660
|
+
analysisResultId: context.analysisResultId,
|
|
661
|
+
findingId: context.finding.id,
|
|
662
|
+
packageName: context.dependency.declaredName,
|
|
663
|
+
outcome: result.outcome,
|
|
664
|
+
reasonCode: result.warning?.code ?? null,
|
|
665
|
+
detailCode: result.warning?.detailCode ?? null
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const normalizedRecords = [...records.values()].sort(compareRecords);
|
|
670
|
+
summary.recordCount = normalizedRecords.length;
|
|
671
|
+
const output = {
|
|
672
|
+
resultVersion,
|
|
673
|
+
input: structuredClone(prepared.input),
|
|
674
|
+
records: normalizedRecords,
|
|
675
|
+
warnings: warnings.sort(compareWarnings),
|
|
676
|
+
summary
|
|
677
|
+
};
|
|
678
|
+
return deepFreeze(structuredClone(output));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Historical free-form v1 multi-context generation. */
|
|
682
|
+
export async function generateMigrationChecklistDrafts(prepared, options = {}) {
|
|
683
|
+
return generateMigrationChecklistDraftsWithContract(prepared, {
|
|
684
|
+
...options,
|
|
685
|
+
promptVersion: options.promptVersion ?? MIGRATION_PLANNING_PROMPT_VERSION
|
|
686
|
+
}, {
|
|
687
|
+
generateContext: generateMigrationChecklistForContext,
|
|
688
|
+
resultVersion: MIGRATION_GENERATION_RESULT_VERSION
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Production extractive v2 multi-context generation. */
|
|
693
|
+
export async function generateMigrationExtractiveChecklistDrafts(prepared, options = {}) {
|
|
694
|
+
return generateMigrationChecklistDraftsWithContract(prepared, {
|
|
695
|
+
...options,
|
|
696
|
+
runIdPrefix: options.runIdPrefix ?? 'migration-extractive',
|
|
697
|
+
promptVersion: options.promptVersion ?? MIGRATION_EXTRACTIVE_PROMPT_VERSION
|
|
698
|
+
}, {
|
|
699
|
+
generateContext: generateMigrationExtractiveChecklistForContext,
|
|
700
|
+
resultVersion: MIGRATION_EXTRACTIVE_GENERATION_RESULT_VERSION
|
|
701
|
+
});
|
|
702
|
+
}
|