@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,62 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
|
|
3
|
+
function unsafeIpLiteral(hostname) {
|
|
4
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
5
|
+
const type = isIP(host);
|
|
6
|
+
if (type === 4) {
|
|
7
|
+
const [first, second] = host.split('.').map(Number);
|
|
8
|
+
return first === 0 || first === 10 || first === 127
|
|
9
|
+
|| (first === 169 && second === 254)
|
|
10
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
11
|
+
|| (first === 192 && second === 168);
|
|
12
|
+
}
|
|
13
|
+
if (type === 6) {
|
|
14
|
+
return host === '::1' || host.startsWith('fc') || host.startsWith('fd')
|
|
15
|
+
|| /^fe[89ab]/.test(host) || host.startsWith('::ffff:');
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function publicHostname(hostname) {
|
|
21
|
+
const host = hostname.toLowerCase().replace(/\.$/, '');
|
|
22
|
+
return host !== 'localhost' && !host.endsWith('.localhost') && !unsafeIpLiteral(host);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizedPath(url) {
|
|
26
|
+
let pathname = url.pathname.replace(/\/+$/g, '') || '/';
|
|
27
|
+
if (url.hostname.toLowerCase() === 'github.com') {
|
|
28
|
+
pathname = pathname.replace(/\.git$/i, '').replace(/\/+$/g, '') || '/';
|
|
29
|
+
}
|
|
30
|
+
return pathname;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert a publisher-provided public URL into its portable form. This helper
|
|
35
|
+
* performs no DNS lookup, redirect handling, or network access.
|
|
36
|
+
*/
|
|
37
|
+
export function canonicalizeSourceUrl(value, { role = null } = {}) {
|
|
38
|
+
if (typeof value !== 'string' || value.trim() === '') return null;
|
|
39
|
+
const raw = value.trim();
|
|
40
|
+
const gitHttps = /^git\+https:/i.test(raw);
|
|
41
|
+
if (gitHttps && role !== 'repository') return null;
|
|
42
|
+
const candidate = gitHttps ? raw.replace(/^git\+/i, '') : raw;
|
|
43
|
+
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(candidate);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || !publicHostname(url.hostname)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
url.hostname = url.hostname.toLowerCase().replace(/\.$/, '');
|
|
55
|
+
url.hash = '';
|
|
56
|
+
const pathname = normalizedPath(url);
|
|
57
|
+
return `${url.protocol}//${url.host}${pathname === '/' ? '' : pathname}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isCanonicalPublicSourceUrl(value) {
|
|
61
|
+
return canonicalizeSourceUrl(value) === value;
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const PROVIDER_UNSUPPORTED_GENERATION_KEYWORDS = new Set(['uniqueItems']);
|
|
2
|
+
const MAX_SCHEMA_DEPTH = 256;
|
|
3
|
+
const MAX_SCHEMA_NODES = 100_000;
|
|
4
|
+
|
|
5
|
+
function projectionError(message) {
|
|
6
|
+
return new TypeError(`Provider-facing structured output schema projection failed: ${message}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider structured-output implementations may support only a JSON Schema
|
|
11
|
+
* subset. Real conformance probes rejected uniqueItems, so generation omits it
|
|
12
|
+
* while the unchanged exact local schema remains the validation authority.
|
|
13
|
+
*/
|
|
14
|
+
export function projectStructuredOutputSchemaForProvider(schema) {
|
|
15
|
+
const active = new WeakSet();
|
|
16
|
+
let visitedNodes = 0;
|
|
17
|
+
|
|
18
|
+
function project(value, depth) {
|
|
19
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
20
|
+
if (typeof value === 'number') {
|
|
21
|
+
if (!Number.isFinite(value)) throw projectionError('numbers must be finite JSON values.');
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
if (['undefined', 'bigint', 'function', 'symbol'].includes(typeof value)) {
|
|
25
|
+
throw projectionError('schema must contain only JSON values.');
|
|
26
|
+
}
|
|
27
|
+
if (depth > MAX_SCHEMA_DEPTH) throw projectionError('schema nesting is too deep.');
|
|
28
|
+
if (active.has(value)) throw projectionError('circular schema input is not supported.');
|
|
29
|
+
visitedNodes += 1;
|
|
30
|
+
if (visitedNodes > MAX_SCHEMA_NODES) throw projectionError('schema is too large.');
|
|
31
|
+
|
|
32
|
+
active.add(value);
|
|
33
|
+
try {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const output = [];
|
|
36
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
37
|
+
if (!Object.hasOwn(value, index)) throw projectionError('sparse arrays are not JSON-safe.');
|
|
38
|
+
output.push(project(value[index], depth + 1));
|
|
39
|
+
}
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
45
|
+
throw projectionError('schema objects must be plain JSON objects.');
|
|
46
|
+
}
|
|
47
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
48
|
+
throw projectionError('symbol properties are not JSON-safe.');
|
|
49
|
+
}
|
|
50
|
+
const entries = [];
|
|
51
|
+
for (const key of Object.keys(value)) {
|
|
52
|
+
if (PROVIDER_UNSUPPORTED_GENERATION_KEYWORDS.has(key)) continue;
|
|
53
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
54
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
55
|
+
throw projectionError('schema accessors are not supported.');
|
|
56
|
+
}
|
|
57
|
+
entries.push([key, project(descriptor.value, depth + 1)]);
|
|
58
|
+
}
|
|
59
|
+
return Object.fromEntries(entries);
|
|
60
|
+
} finally {
|
|
61
|
+
active.delete(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return project(schema, 0);
|
|
66
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { canonicalJsonBytes } from './canonical-json.js';
|
|
4
|
+
import {
|
|
5
|
+
createDefaultEcosystemVersionAdapterRegistry,
|
|
6
|
+
getEcosystemVersionAdapter
|
|
7
|
+
} from './ecosystem-version-adapter.js';
|
|
8
|
+
import { compareText, isPortableRelativePath } from './portable.js';
|
|
9
|
+
|
|
10
|
+
const REQUIRED_KEYS = Object.freeze(['package', 'target']);
|
|
11
|
+
const OPTIONAL_KEYS = Object.freeze(['project', 'manifest', 'type', 'occurrence']);
|
|
12
|
+
const ALLOWED_KEYS = new Set([...REQUIRED_KEYS, ...OPTIONAL_KEYS]);
|
|
13
|
+
const OCCURRENCE_ID_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
14
|
+
const OCCURRENCE_ID_VERSION = 'target-occurrence.v1';
|
|
15
|
+
|
|
16
|
+
export class TargetSelectorError extends Error {
|
|
17
|
+
constructor(code, message, candidates = []) {
|
|
18
|
+
super(`${code}: ${message}`);
|
|
19
|
+
this.name = 'TargetSelectorError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.candidates = Object.freeze([...candidates]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function selectorError(code, message, candidates) {
|
|
26
|
+
throw new TargetSelectorError(code, message, candidates);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireText(value, field) {
|
|
30
|
+
if (typeof value !== 'string' || value.trim().length === 0 || /[\r\n\0]/.test(value)) {
|
|
31
|
+
selectorError('TARGET_SELECTOR_INVALID', `${field} must be a non-empty single-line value.`);
|
|
32
|
+
}
|
|
33
|
+
return value.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canonicalPackageId(value) {
|
|
37
|
+
const text = requireText(value, 'package');
|
|
38
|
+
const separator = text.indexOf(':');
|
|
39
|
+
if (separator <= 0 || separator === text.length - 1
|
|
40
|
+
|| !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(text.slice(0, separator))
|
|
41
|
+
|| /\s/.test(text.slice(separator + 1))) {
|
|
42
|
+
selectorError(
|
|
43
|
+
'TARGET_SELECTOR_INVALID',
|
|
44
|
+
'package must be an exact canonical ID such as npm:react, npm:@scope/package, or pypi:langsmith.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseTargetSelector(value) {
|
|
51
|
+
const text = requireText(value, '--target');
|
|
52
|
+
const fields = {};
|
|
53
|
+
for (const segment of text.split(',')) {
|
|
54
|
+
const separator = segment.indexOf('=');
|
|
55
|
+
if (separator <= 0) {
|
|
56
|
+
selectorError(
|
|
57
|
+
'TARGET_SELECTOR_INVALID',
|
|
58
|
+
'each --target field must use key=value syntax.'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const key = segment.slice(0, separator).trim();
|
|
62
|
+
const fieldValue = requireText(segment.slice(separator + 1), key || '--target field');
|
|
63
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
64
|
+
selectorError('TARGET_SELECTOR_INVALID', `unsupported --target field ${key}.`);
|
|
65
|
+
}
|
|
66
|
+
if (Object.hasOwn(fields, key)) {
|
|
67
|
+
selectorError('TARGET_SELECTOR_INVALID', `duplicate --target field ${key}.`);
|
|
68
|
+
}
|
|
69
|
+
fields[key] = fieldValue;
|
|
70
|
+
}
|
|
71
|
+
for (const key of REQUIRED_KEYS) {
|
|
72
|
+
if (!Object.hasOwn(fields, key)) {
|
|
73
|
+
selectorError('TARGET_SELECTOR_INVALID', `--target requires ${key}=<value>.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const selector = {
|
|
77
|
+
packageId: canonicalPackageId(fields.package),
|
|
78
|
+
targetVersion: fields.target
|
|
79
|
+
};
|
|
80
|
+
if (fields.project) selector.projectId = fields.project;
|
|
81
|
+
if (fields.manifest) {
|
|
82
|
+
if (!isPortableRelativePath(fields.manifest)) {
|
|
83
|
+
selectorError('TARGET_SELECTOR_INVALID', 'manifest must be a portable repository-relative path.');
|
|
84
|
+
}
|
|
85
|
+
selector.manifest = fields.manifest;
|
|
86
|
+
}
|
|
87
|
+
if (fields.type) selector.dependencyType = fields.type;
|
|
88
|
+
if (fields.occurrence) {
|
|
89
|
+
if (!OCCURRENCE_ID_PATTERN.test(fields.occurrence)) {
|
|
90
|
+
selectorError(
|
|
91
|
+
'TARGET_SELECTOR_INVALID',
|
|
92
|
+
'occurrence must be a stable sha256:<64 lowercase hexadecimal characters> identifier.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
selector.occurrenceId = fields.occurrence;
|
|
96
|
+
}
|
|
97
|
+
return Object.freeze(selector);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function occurrenceIdentity(input) {
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: OCCURRENCE_ID_VERSION,
|
|
103
|
+
projectId: input.project.id,
|
|
104
|
+
packageId: input.packageRecord.id,
|
|
105
|
+
manifest: input.dependency.manifest,
|
|
106
|
+
dependencyType: input.dependency.type,
|
|
107
|
+
declaredName: input.dependency.name,
|
|
108
|
+
declaredVersion: input.dependency.declaredVersion ?? null
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function targetOccurrenceId(input) {
|
|
113
|
+
const digest = createHash('sha256')
|
|
114
|
+
.update(canonicalJsonBytes(occurrenceIdentity(input)))
|
|
115
|
+
.digest('hex');
|
|
116
|
+
return `sha256:${digest}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function targetOccurrenceKey(input) {
|
|
120
|
+
return [
|
|
121
|
+
input.project.id,
|
|
122
|
+
input.packageRecord.id,
|
|
123
|
+
input.dependency.manifest,
|
|
124
|
+
input.dependency.type,
|
|
125
|
+
input.dependency.name,
|
|
126
|
+
input.dependency.declaredVersion ?? ''
|
|
127
|
+
].join('\0');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matches(input, selector) {
|
|
131
|
+
return input.packageRecord.id === selector.packageId
|
|
132
|
+
&& (!selector.projectId || input.project.id === selector.projectId)
|
|
133
|
+
&& (!selector.manifest || input.dependency.manifest === selector.manifest)
|
|
134
|
+
&& (!selector.dependencyType || input.dependency.type === selector.dependencyType);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function exactSelector(input, targetVersion) {
|
|
138
|
+
return [
|
|
139
|
+
`package=${input.packageRecord.id}`,
|
|
140
|
+
`target=${targetVersion}`,
|
|
141
|
+
`project=${input.project.id}`,
|
|
142
|
+
`manifest=${input.dependency.manifest}`,
|
|
143
|
+
`type=${input.dependency.type}`,
|
|
144
|
+
`occurrence=${targetOccurrenceId(input)}`
|
|
145
|
+
].join(',');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function safeDeclaredVersion(value) {
|
|
149
|
+
if (value === null || value === undefined || value === '') return '<unversioned>';
|
|
150
|
+
const text = String(value).trim();
|
|
151
|
+
if (
|
|
152
|
+
text.length === 0
|
|
153
|
+
|| text.length > 240
|
|
154
|
+
|| /[\u0000-\u001f\u007f]/.test(text)
|
|
155
|
+
|| /(?:authorization|credential|password|secret|token)\s*=/i.test(text)
|
|
156
|
+
) {
|
|
157
|
+
return '<redacted-declaration>';
|
|
158
|
+
}
|
|
159
|
+
if (/^(?:file:|link:|\.{1,2}[\\/]|[\\/]|~[\\/])/i.test(text)) {
|
|
160
|
+
return '<local-path-reference>';
|
|
161
|
+
}
|
|
162
|
+
const prefix = text.startsWith('git+') ? 'git+' : '';
|
|
163
|
+
const candidate = prefix ? text.slice(prefix.length) : text;
|
|
164
|
+
try {
|
|
165
|
+
const url = new URL(candidate);
|
|
166
|
+
if (!['http:', 'https:', 'ssh:', 'git:'].includes(url.protocol)) return text;
|
|
167
|
+
url.username = '';
|
|
168
|
+
url.password = '';
|
|
169
|
+
url.search = '';
|
|
170
|
+
url.hash = '';
|
|
171
|
+
return `${prefix}${url.toString()}`;
|
|
172
|
+
} catch {
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function exactCandidate(input, targetVersion) {
|
|
178
|
+
return {
|
|
179
|
+
selector: exactSelector(input, targetVersion),
|
|
180
|
+
projectId: input.project.id,
|
|
181
|
+
manifest: input.dependency.manifest,
|
|
182
|
+
dependencyType: input.dependency.type,
|
|
183
|
+
declaredName: safeDeclaredVersion(input.dependency.name),
|
|
184
|
+
declaredVersion: safeDeclaredVersion(input.dependency.declaredVersion)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function candidateMessage(candidate) {
|
|
189
|
+
return [
|
|
190
|
+
` --target '${candidate.selector}'`,
|
|
191
|
+
` project: ${candidate.projectId}`,
|
|
192
|
+
` manifest: ${candidate.manifest}`,
|
|
193
|
+
` type: ${candidate.dependencyType}`,
|
|
194
|
+
` declared name: ${candidate.declaredName}`,
|
|
195
|
+
` declared: ${candidate.declaredVersion}`
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function occurrenceIndex(inputs) {
|
|
200
|
+
const indexed = new Map();
|
|
201
|
+
for (const input of inputs) {
|
|
202
|
+
const id = targetOccurrenceId(input);
|
|
203
|
+
if (!indexed.has(id)) indexed.set(id, []);
|
|
204
|
+
indexed.get(id).push(input);
|
|
205
|
+
}
|
|
206
|
+
for (const [id, matchesForId] of indexed) {
|
|
207
|
+
if (matchesForId.length > 1) {
|
|
208
|
+
selectorError(
|
|
209
|
+
'TARGET_SELECTOR_CONFLICT',
|
|
210
|
+
`dependency occurrence identity ${id} is duplicated; exact target selection is unsafe and no provider call was made.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return indexed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function matchesForSelector(inputs, selector, indexed) {
|
|
218
|
+
if (!selector.occurrenceId) return inputs.filter((input) => matches(input, selector));
|
|
219
|
+
const selected = indexed.get(selector.occurrenceId);
|
|
220
|
+
if (!selected) {
|
|
221
|
+
selectorError(
|
|
222
|
+
'TARGET_SELECTOR_NOT_FOUND',
|
|
223
|
+
`occurrence ${selector.occurrenceId} is stale or unknown; no provider call was made.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const input = selected[0];
|
|
227
|
+
if (!matches(input, selector)) {
|
|
228
|
+
selectorError(
|
|
229
|
+
'TARGET_SELECTOR_CONFLICT',
|
|
230
|
+
`occurrence ${selector.occurrenceId} conflicts with the supplied package/project/manifest/type fields; no provider call was made.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return selected;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeTarget(input, selector, adapterRegistry) {
|
|
237
|
+
let adapter;
|
|
238
|
+
try {
|
|
239
|
+
adapter = getEcosystemVersionAdapter(input.project.ecosystem, adapterRegistry);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
selectorError(
|
|
242
|
+
'TARGET_VERSION_INVALID',
|
|
243
|
+
`target ${selector.targetVersion} cannot be validated for ecosystem ${input.project.ecosystem}: ${error.message}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const normalized = adapter.normalizeVersion(selector.targetVersion);
|
|
247
|
+
if (!normalized.ok) {
|
|
248
|
+
selectorError(
|
|
249
|
+
'TARGET_VERSION_INVALID',
|
|
250
|
+
`target ${selector.targetVersion} is invalid for ${input.packageRecord.id}: ${normalized.reason}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return normalized.value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function resolveTargetSelectors(inputs, selectors, {
|
|
257
|
+
adapterRegistry = createDefaultEcosystemVersionAdapterRegistry()
|
|
258
|
+
} = {}) {
|
|
259
|
+
const parsed = selectors.map((selector) => (
|
|
260
|
+
typeof selector === 'string' ? parseTargetSelector(selector) : selector
|
|
261
|
+
));
|
|
262
|
+
const indexed = occurrenceIndex(inputs);
|
|
263
|
+
const selected = new Map();
|
|
264
|
+
for (const selector of parsed) {
|
|
265
|
+
const matchingInputs = matchesForSelector(inputs, selector, indexed);
|
|
266
|
+
if (matchingInputs.length === 0) {
|
|
267
|
+
selectorError(
|
|
268
|
+
'TARGET_SELECTOR_NOT_FOUND',
|
|
269
|
+
`no dependency occurrence matches ${selector.packageId}; no provider call was made.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (matchingInputs.length > 1) {
|
|
273
|
+
const exactCandidates = [...matchingInputs]
|
|
274
|
+
.sort((left, right) => compareText(targetOccurrenceKey(left), targetOccurrenceKey(right)))
|
|
275
|
+
.map((input) => exactCandidate(input, selector.targetVersion));
|
|
276
|
+
const candidates = exactCandidates.map((candidate) => candidate.selector);
|
|
277
|
+
selectorError(
|
|
278
|
+
'TARGET_SELECTOR_AMBIGUOUS',
|
|
279
|
+
`Target selector for ${selector.packageId} matches ${matchingInputs.length} dependency occurrences.\n`
|
|
280
|
+
+ 'Choose one of the following exact selectors:\n'
|
|
281
|
+
+ exactCandidates.map(candidateMessage).join('\n\n'),
|
|
282
|
+
candidates
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const input = matchingInputs[0];
|
|
286
|
+
const key = targetOccurrenceKey(input);
|
|
287
|
+
const targetVersion = normalizeTarget(input, selector, adapterRegistry);
|
|
288
|
+
if (selected.has(key)) {
|
|
289
|
+
const prior = selected.get(key);
|
|
290
|
+
selectorError(
|
|
291
|
+
'TARGET_SELECTOR_CONFLICT',
|
|
292
|
+
`dependency occurrence ${input.project.id} / ${input.dependency.manifest} / `
|
|
293
|
+
+ `${input.dependency.type} / ${input.packageRecord.id} was selected more than once `
|
|
294
|
+
+ `(${prior.targetVersion}, ${targetVersion}).`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
selected.set(key, Object.freeze({
|
|
298
|
+
targetVersion,
|
|
299
|
+
target: Object.freeze({ policy: 'explicit', version: targetVersion }),
|
|
300
|
+
selector: Object.freeze({ ...selector })
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
return Object.freeze(new Map(
|
|
304
|
+
[...selected.entries()].sort(([left], [right]) => compareText(left, right))
|
|
305
|
+
));
|
|
306
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_UPGRADE_DECISION_PATH } from '../constants.js';
|
|
6
|
+
import { isPortableRelativePath } from '../portable.js';
|
|
7
|
+
import { buildUpgradeDecision, validateUpgradeDecision } from './upgrade-decision.js';
|
|
8
|
+
|
|
9
|
+
function digest(bytes) {
|
|
10
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sameJson(left, right) {
|
|
14
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sourceFor(input, options) {
|
|
18
|
+
const explicit = options.upgradeDecision ?? input?.upgradeDecision ?? input?.sources?.upgradeDecision;
|
|
19
|
+
if (explicit !== undefined) return { source: explicit, required: true };
|
|
20
|
+
if (typeof input === 'string' || input === undefined || input === null) {
|
|
21
|
+
return {
|
|
22
|
+
source: path.resolve(input ?? '.', options.upgradeDecisionArtifact ?? DEFAULT_UPGRADE_DECISION_PATH),
|
|
23
|
+
required: false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (input instanceof URL) {
|
|
27
|
+
return {
|
|
28
|
+
source: new URL(options.upgradeDecisionArtifact ?? DEFAULT_UPGRADE_DECISION_PATH, input),
|
|
29
|
+
required: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (typeof input.repositoryRoot === 'string') {
|
|
33
|
+
return {
|
|
34
|
+
source: path.resolve(
|
|
35
|
+
input.repositoryRoot,
|
|
36
|
+
options.upgradeDecisionArtifact ?? DEFAULT_UPGRADE_DECISION_PATH
|
|
37
|
+
),
|
|
38
|
+
required: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { source: undefined, required: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function artifactFor(source, options) {
|
|
45
|
+
const explicit = source && typeof source === 'object' && !(source instanceof URL)
|
|
46
|
+
? source.artifact
|
|
47
|
+
: undefined;
|
|
48
|
+
const artifact = explicit ?? options.upgradeDecisionArtifact ?? DEFAULT_UPGRADE_DECISION_PATH;
|
|
49
|
+
if (!isPortableRelativePath(artifact)) {
|
|
50
|
+
throw new Error('Upgrade Decision input error: artifact path must be repository-relative.');
|
|
51
|
+
}
|
|
52
|
+
return artifact;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readBytes(source) {
|
|
56
|
+
if (typeof source === 'string' || source instanceof URL) return readFile(source);
|
|
57
|
+
if (source?.bytes instanceof Uint8Array) return Buffer.from(source.bytes);
|
|
58
|
+
if (typeof source?.path === 'string' || source?.path instanceof URL) return readFile(source.path);
|
|
59
|
+
throw new Error('Upgrade Decision input error: unsupported artifact source.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function loadPersistedUpgradeDecision(input, artifacts, options = {}) {
|
|
63
|
+
const selected = sourceFor(input, options);
|
|
64
|
+
if (selected.source === undefined) return artifacts;
|
|
65
|
+
let bytes;
|
|
66
|
+
try {
|
|
67
|
+
bytes = await readBytes(selected.source);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!selected.required && error?.code === 'ENOENT') return artifacts;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
let decision;
|
|
73
|
+
try {
|
|
74
|
+
decision = JSON.parse(bytes.toString('utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error('Upgrade Decision input error: artifact is not valid JSON.');
|
|
77
|
+
}
|
|
78
|
+
validateUpgradeDecision(decision);
|
|
79
|
+
if (!sameJson(decision.input, artifacts.input)) {
|
|
80
|
+
throw new Error('Upgrade Decision input error: upstream artifact lineage mismatch.');
|
|
81
|
+
}
|
|
82
|
+
const resultIds = artifacts.versionAnalysis.results.map((result) => result.id).sort();
|
|
83
|
+
const decisionIds = decision.decisions
|
|
84
|
+
.map((record) => record.analysisResultId)
|
|
85
|
+
.filter((value) => value !== null)
|
|
86
|
+
.sort();
|
|
87
|
+
if (!sameJson(resultIds, decisionIds)) {
|
|
88
|
+
throw new Error('Upgrade Decision input error: dependency occurrence set mismatch.');
|
|
89
|
+
}
|
|
90
|
+
const expected = buildUpgradeDecision(artifacts);
|
|
91
|
+
if (!sameJson(decision, expected)) {
|
|
92
|
+
throw new Error('Upgrade Decision input error: artifact does not match deterministic policy output.');
|
|
93
|
+
}
|
|
94
|
+
const artifact = artifactFor(selected.source, options);
|
|
95
|
+
return {
|
|
96
|
+
...artifacts,
|
|
97
|
+
upgradeDecision: decision,
|
|
98
|
+
input: {
|
|
99
|
+
...artifacts.input,
|
|
100
|
+
upgradeDecision: {
|
|
101
|
+
schemaVersion: decision.schemaVersion,
|
|
102
|
+
artifact,
|
|
103
|
+
artifactDigest: digest(bytes)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function inlineCode(value) {
|
|
2
|
+
return `\`${String(value).replaceAll('`', '\\`')}\``;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function renderUpgradeDecisionConsole({ artifact, artifactPath }) {
|
|
6
|
+
const lines = [
|
|
7
|
+
'Upgrade Decisions',
|
|
8
|
+
'',
|
|
9
|
+
`Keep current: ${artifact.summary.KEEP_CURRENT}`,
|
|
10
|
+
`Upgrade now: ${artifact.summary.UPGRADE_NOW}`,
|
|
11
|
+
`Plan upgrade: ${artifact.summary.PLAN_UPGRADE}`,
|
|
12
|
+
`Investigate: ${artifact.summary.INVESTIGATE}`,
|
|
13
|
+
`Insufficient evidence: ${artifact.summary.INSUFFICIENT_EVIDENCE}`,
|
|
14
|
+
`Not analyzed: ${artifact.summary.NOT_ANALYZED}`,
|
|
15
|
+
`Artifact: ${artifactPath}`,
|
|
16
|
+
''
|
|
17
|
+
];
|
|
18
|
+
for (const record of artifact.decisions) {
|
|
19
|
+
lines.push(
|
|
20
|
+
`${record.occurrence.declaredName}: ${record.decision} (${record.primaryReasonCode})`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return `${lines.join('\n')}\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderUpgradeDecisionMarkdownSection(artifact) {
|
|
27
|
+
const lines = [
|
|
28
|
+
'## Upgrade Decisions',
|
|
29
|
+
'',
|
|
30
|
+
'| Dependency occurrence | Installed | Target | Decision | Reason |',
|
|
31
|
+
'| --- | --- | --- | --- | --- |'
|
|
32
|
+
];
|
|
33
|
+
for (const record of artifact.decisions) {
|
|
34
|
+
const occurrence = `${record.occurrence.declaredName} — ${record.occurrence.manifest} (${record.occurrence.dependencyType})`;
|
|
35
|
+
lines.push(
|
|
36
|
+
`| ${inlineCode(occurrence)} | ${inlineCode(record.versions.installedVersion ?? 'unavailable')} | `
|
|
37
|
+
+ `${inlineCode(record.versions.targetVersion ?? 'unavailable')} | ${inlineCode(record.decision)} | `
|
|
38
|
+
+ `${inlineCode(record.primaryReasonCode)} |`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
lines.push('');
|
|
42
|
+
return `${lines.join('\n')}\n`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_UPGRADE_DECISION_PATH } from '../constants.js';
|
|
4
|
+
import { loadMigrationChecklistInputs } from '../migration-checklist/input-loader.js';
|
|
5
|
+
import { buildUpgradeDecision } from './upgrade-decision.js';
|
|
6
|
+
import { writeUpgradeDecision } from './writer.js';
|
|
7
|
+
|
|
8
|
+
export async function runUpgradeDecisionStage({
|
|
9
|
+
repositoryRoot,
|
|
10
|
+
artifactPath = DEFAULT_UPGRADE_DECISION_PATH,
|
|
11
|
+
adapters,
|
|
12
|
+
onCompatibilityDiagnostic,
|
|
13
|
+
writeArtifact = writeUpgradeDecision
|
|
14
|
+
}) {
|
|
15
|
+
const artifacts = await loadMigrationChecklistInputs(repositoryRoot, {
|
|
16
|
+
onCompatibilityDiagnostic
|
|
17
|
+
});
|
|
18
|
+
const decision = buildUpgradeDecision(artifacts, { adapters });
|
|
19
|
+
await writeArtifact(path.resolve(repositoryRoot, artifactPath), decision);
|
|
20
|
+
return decision;
|
|
21
|
+
}
|