@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,278 @@
|
|
|
1
|
+
import { USER_AGENT } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
const JSON_MEDIA_TYPE = /^(?:application\/(?:json|[a-z0-9.+-]+\+json))(?:\s*;|$)/i;
|
|
4
|
+
const EVIDENCE_MEDIA_TYPE = /^(?:text\/(?:plain|markdown|html)|application\/(?:json|[a-z0-9.+-]+\+json|xhtml\+xml))(?:\s*;|$)/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adapter configuration is intentionally bounded even though the transport
|
|
8
|
+
* helper itself can enforce any positive safe integer supplied by a caller.
|
|
9
|
+
*/
|
|
10
|
+
export const MAX_REGISTRY_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
export class BoundedFetchError extends Error {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'BoundedFetchError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function headerValue(headers, name) {
|
|
21
|
+
if (!headers || typeof headers.get !== 'function') return null;
|
|
22
|
+
return headers.get(name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validLimit(value, name, errorPrefix) {
|
|
26
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
27
|
+
throw new BoundedFetchError(`${errorPrefix}_REQUEST_INVALID`, `${name} must be a positive integer.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Validate an adapter-owned response limit without imposing a shared default. */
|
|
32
|
+
export function validateRegistryResponseLimit(value, {
|
|
33
|
+
errorPrefix = 'HTTP',
|
|
34
|
+
maximum = MAX_REGISTRY_RESPONSE_BYTES
|
|
35
|
+
} = {}) {
|
|
36
|
+
validLimit(value, 'maxResponseBytes', errorPrefix);
|
|
37
|
+
if (value > maximum) {
|
|
38
|
+
throw new BoundedFetchError(
|
|
39
|
+
`${errorPrefix}_REQUEST_INVALID`,
|
|
40
|
+
`maxResponseBytes must not exceed ${maximum} bytes.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function discardResponseBody(response) {
|
|
47
|
+
try {
|
|
48
|
+
await response?.body?.cancel?.();
|
|
49
|
+
} catch {
|
|
50
|
+
// Cleanup cannot change a sanitized registry result.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readBoundedText(response, maxResponseBytes, { errorPrefix, serviceName }) {
|
|
55
|
+
const declaredLength = headerValue(response.headers, 'content-length');
|
|
56
|
+
if (declaredLength !== null && /^\d+$/.test(declaredLength) && Number(declaredLength) > maxResponseBytes) {
|
|
57
|
+
await discardResponseBody(response);
|
|
58
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_TOO_LARGE`, `${serviceName} response exceeds the configured size limit.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reader = response.body?.getReader?.();
|
|
62
|
+
if (!reader) {
|
|
63
|
+
return readFallbackText(response, maxResponseBytes, { errorPrefix, serviceName });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let byteCount = 0;
|
|
68
|
+
let completed = false;
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const { done, value } = await reader.read();
|
|
72
|
+
if (done) {
|
|
73
|
+
completed = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const chunk = Buffer.from(value);
|
|
77
|
+
byteCount += chunk.length;
|
|
78
|
+
if (byteCount > maxResponseBytes) {
|
|
79
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_TOO_LARGE`, `${serviceName} response exceeds the configured size limit.`);
|
|
80
|
+
}
|
|
81
|
+
chunks.push(chunk);
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
if (!completed) {
|
|
85
|
+
try {
|
|
86
|
+
await reader.cancel?.();
|
|
87
|
+
} catch {
|
|
88
|
+
// Cleanup cannot change a sanitized registry result.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
reader.releaseLock?.();
|
|
93
|
+
} catch {
|
|
94
|
+
// Cleanup cannot change a sanitized registry result.
|
|
95
|
+
}
|
|
96
|
+
if (!completed) await discardResponseBody(response);
|
|
97
|
+
}
|
|
98
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readFallbackText(response, maxResponseBytes, { errorPrefix, serviceName }) {
|
|
102
|
+
const read = typeof response?.text === 'function'
|
|
103
|
+
? () => response.text()
|
|
104
|
+
: typeof response?.body?.text === 'function'
|
|
105
|
+
? () => response.body.text()
|
|
106
|
+
: null;
|
|
107
|
+
if (!read) {
|
|
108
|
+
await discardResponseBody(response);
|
|
109
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_INVALID`, `${serviceName} response body is not readable.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let completed = false;
|
|
113
|
+
try {
|
|
114
|
+
const text = await read();
|
|
115
|
+
if (Buffer.byteLength(String(text), 'utf8') > maxResponseBytes) {
|
|
116
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_TOO_LARGE`, `${serviceName} response exceeds the configured size limit.`);
|
|
117
|
+
}
|
|
118
|
+
completed = true;
|
|
119
|
+
return String(text);
|
|
120
|
+
} finally {
|
|
121
|
+
if (!completed) await discardResponseBody(response);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch one registry JSON representation without allowing an unbounded body
|
|
127
|
+
* into memory. Redirects are explicitly disabled in this small adapter.
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchRegistryJson(url, {
|
|
130
|
+
fetchImplementation = globalThis.fetch,
|
|
131
|
+
timeoutMs = 10_000,
|
|
132
|
+
maxResponseBytes,
|
|
133
|
+
userAgent = USER_AGENT,
|
|
134
|
+
errorPrefix = 'NPM',
|
|
135
|
+
serviceName = 'npm Registry',
|
|
136
|
+
setTimeoutImplementation = setTimeout,
|
|
137
|
+
clearTimeoutImplementation = clearTimeout
|
|
138
|
+
} = {}) {
|
|
139
|
+
if (typeof fetchImplementation !== 'function') {
|
|
140
|
+
throw new BoundedFetchError(`${errorPrefix}_REQUEST_INVALID`, 'A WHATWG-compatible fetch implementation is required.');
|
|
141
|
+
}
|
|
142
|
+
validLimit(timeoutMs, 'timeoutMs', errorPrefix);
|
|
143
|
+
validLimit(maxResponseBytes, 'maxResponseBytes', errorPrefix);
|
|
144
|
+
if (typeof setTimeoutImplementation !== 'function' || typeof clearTimeoutImplementation !== 'function') {
|
|
145
|
+
throw new BoundedFetchError(`${errorPrefix}_REQUEST_INVALID`, 'Timer implementations must be functions.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timer = setTimeoutImplementation(() => controller.abort(), timeoutMs);
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
try {
|
|
153
|
+
response = await fetchImplementation(url, {
|
|
154
|
+
method: 'GET',
|
|
155
|
+
headers: {
|
|
156
|
+
Accept: 'application/json',
|
|
157
|
+
'User-Agent': userAgent
|
|
158
|
+
},
|
|
159
|
+
signal: controller.signal,
|
|
160
|
+
credentials: 'omit',
|
|
161
|
+
redirect: 'error'
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (controller.signal.aborted || error?.name === 'AbortError') {
|
|
165
|
+
throw new BoundedFetchError(`${errorPrefix}_REQUEST_TIMEOUT`, `${serviceName} request timed out.`);
|
|
166
|
+
}
|
|
167
|
+
throw new BoundedFetchError(`${errorPrefix}_TRANSPORT_FAILED`, `${serviceName} request failed.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!response || !Number.isInteger(response.status)) {
|
|
171
|
+
await discardResponseBody(response);
|
|
172
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_INVALID`, `${serviceName} returned an invalid HTTP response.`);
|
|
173
|
+
}
|
|
174
|
+
if (response.status !== 200) {
|
|
175
|
+
await discardResponseBody(response);
|
|
176
|
+
return { status: response.status, body: null };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const contentType = headerValue(response.headers, 'content-type');
|
|
180
|
+
if (!contentType || !JSON_MEDIA_TYPE.test(contentType)) {
|
|
181
|
+
await discardResponseBody(response);
|
|
182
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_INVALID`, `${serviceName} response must use a JSON media type.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let text;
|
|
186
|
+
try {
|
|
187
|
+
text = await readBoundedText(response, maxResponseBytes, { errorPrefix, serviceName });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof BoundedFetchError) throw error;
|
|
190
|
+
if (controller.signal.aborted || error?.name === 'AbortError') {
|
|
191
|
+
throw new BoundedFetchError(`${errorPrefix}_REQUEST_TIMEOUT`, `${serviceName} request timed out.`);
|
|
192
|
+
}
|
|
193
|
+
throw new BoundedFetchError(`${errorPrefix}_TRANSPORT_FAILED`, `${serviceName} response could not be read.`);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
return { status: 200, body: JSON.parse(text) };
|
|
197
|
+
} catch {
|
|
198
|
+
throw new BoundedFetchError(`${errorPrefix}_RESPONSE_INVALID`, `${serviceName} response is not valid JSON.`);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeoutImplementation(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function fetchNpmJson(url, options = {}) {
|
|
206
|
+
return fetchRegistryJson(url, { ...options, errorPrefix: 'NPM', serviceName: 'npm Registry' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fetch one bounded public evidence document. This deliberately shares the
|
|
211
|
+
* registry transport policy: GET only, no credentials, no redirects, a hard
|
|
212
|
+
* deadline, a hard body limit, and sanitized failures.
|
|
213
|
+
*/
|
|
214
|
+
export async function fetchEvidenceDocument(url, {
|
|
215
|
+
fetchImplementation = globalThis.fetch,
|
|
216
|
+
timeoutMs = 10_000,
|
|
217
|
+
maxResponseBytes = 512 * 1024,
|
|
218
|
+
userAgent = USER_AGENT,
|
|
219
|
+
setTimeoutImplementation = setTimeout,
|
|
220
|
+
clearTimeoutImplementation = clearTimeout
|
|
221
|
+
} = {}) {
|
|
222
|
+
if (typeof fetchImplementation !== 'function') {
|
|
223
|
+
throw new BoundedFetchError('EVIDENCE_REQUEST_INVALID', 'A WHATWG-compatible fetch implementation is required.');
|
|
224
|
+
}
|
|
225
|
+
validLimit(timeoutMs, 'timeoutMs', 'EVIDENCE');
|
|
226
|
+
validLimit(maxResponseBytes, 'maxResponseBytes', 'EVIDENCE');
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const timer = setTimeoutImplementation(() => controller.abort(), timeoutMs);
|
|
229
|
+
let response;
|
|
230
|
+
try {
|
|
231
|
+
try {
|
|
232
|
+
response = await fetchImplementation(url, {
|
|
233
|
+
method: 'GET',
|
|
234
|
+
headers: {
|
|
235
|
+
Accept: 'application/json, text/markdown, text/plain, text/html;q=0.8',
|
|
236
|
+
'User-Agent': userAgent
|
|
237
|
+
},
|
|
238
|
+
signal: controller.signal,
|
|
239
|
+
credentials: 'omit',
|
|
240
|
+
redirect: 'error'
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (controller.signal.aborted || error?.name === 'AbortError') {
|
|
244
|
+
throw new BoundedFetchError('EVIDENCE_REQUEST_TIMEOUT', 'Evidence source request timed out.');
|
|
245
|
+
}
|
|
246
|
+
throw new BoundedFetchError('EVIDENCE_TRANSPORT_FAILED', 'Evidence source request failed.');
|
|
247
|
+
}
|
|
248
|
+
if (!response || !Number.isInteger(response.status)) {
|
|
249
|
+
await discardResponseBody(response);
|
|
250
|
+
throw new BoundedFetchError('EVIDENCE_RESPONSE_INVALID', 'Evidence source returned an invalid HTTP response.');
|
|
251
|
+
}
|
|
252
|
+
if (response.status !== 200) {
|
|
253
|
+
await discardResponseBody(response);
|
|
254
|
+
return { status: response.status, mediaType: null, text: null };
|
|
255
|
+
}
|
|
256
|
+
const contentType = headerValue(response.headers, 'content-type');
|
|
257
|
+
if (!contentType || !EVIDENCE_MEDIA_TYPE.test(contentType)) {
|
|
258
|
+
await discardResponseBody(response);
|
|
259
|
+
throw new BoundedFetchError('EVIDENCE_RESPONSE_INVALID', 'Evidence source returned an unsupported media type.');
|
|
260
|
+
}
|
|
261
|
+
let text;
|
|
262
|
+
try {
|
|
263
|
+
text = await readBoundedText(response, maxResponseBytes, {
|
|
264
|
+
errorPrefix: 'EVIDENCE',
|
|
265
|
+
serviceName: 'Evidence source'
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof BoundedFetchError) throw error;
|
|
269
|
+
if (controller.signal.aborted || error?.name === 'AbortError') {
|
|
270
|
+
throw new BoundedFetchError('EVIDENCE_REQUEST_TIMEOUT', 'Evidence source request timed out.');
|
|
271
|
+
}
|
|
272
|
+
throw new BoundedFetchError('EVIDENCE_TRANSPORT_FAILED', 'Evidence source response could not be read.');
|
|
273
|
+
}
|
|
274
|
+
return { status: 200, mediaType: contentType.split(';', 1)[0].trim().toLowerCase(), text };
|
|
275
|
+
} finally {
|
|
276
|
+
clearTimeoutImplementation(timer);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Agent, fetch as undiciFetch } from 'undici';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_AGENT_OPTIONS = Object.freeze({
|
|
4
|
+
connections: 4,
|
|
5
|
+
keepAliveTimeout: 1_000,
|
|
6
|
+
keepAliveMaxTimeout: 1_000
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export class CliHttpRuntimeError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'CliHttpRuntimeError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the short-lived HTTP runtime owned by one online CLI research run.
|
|
18
|
+
* It deliberately does not mutate Undici's process-wide global dispatcher.
|
|
19
|
+
*/
|
|
20
|
+
export function createCliHttpRuntime({
|
|
21
|
+
createAgent = (options) => new Agent(options),
|
|
22
|
+
fetchImplementation = undiciFetch,
|
|
23
|
+
agentOptions = {}
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (typeof createAgent !== 'function' || typeof fetchImplementation !== 'function') {
|
|
26
|
+
throw new CliHttpRuntimeError('Unable to create the CLI HTTP runtime.');
|
|
27
|
+
}
|
|
28
|
+
const dispatcher = createAgent({ ...DEFAULT_AGENT_OPTIONS, ...agentOptions });
|
|
29
|
+
if (!dispatcher || typeof dispatcher.close !== 'function') {
|
|
30
|
+
throw new CliHttpRuntimeError('Unable to create the CLI HTTP runtime.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let closePromise = null;
|
|
34
|
+
const close = async () => {
|
|
35
|
+
if (!closePromise) closePromise = Promise.resolve().then(() => dispatcher.close());
|
|
36
|
+
return closePromise;
|
|
37
|
+
};
|
|
38
|
+
const fetch = async (input, init = {}) => {
|
|
39
|
+
if (closePromise) throw new CliHttpRuntimeError('The CLI HTTP runtime is closed.');
|
|
40
|
+
return fetchImplementation(input, { ...init, dispatcher });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return Object.freeze({ fetch, close });
|
|
44
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_USAGE_INDEX_PATH, USAGE_INDEX_SCHEMA_VERSION } from '../constants.js';
|
|
5
|
+
import { isPortableRelativePath } from '../portable.js';
|
|
6
|
+
import { UsageDiscoveryInputError, loadUsageDiscoveryInputs } from '../usage/input-loader.js';
|
|
7
|
+
import { validateUsageIndex } from '../usage/usage-index.js';
|
|
8
|
+
|
|
9
|
+
export class ImpactAnalysisInputError extends Error {
|
|
10
|
+
constructor(message, code = 'IMPACT_ANALYSIS_INPUT_INVALID') {
|
|
11
|
+
super(`Impact Analysis input error: ${message}`);
|
|
12
|
+
this.name = 'ImpactAnalysisInputError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function digest(bytes) {
|
|
18
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readBytes(source) {
|
|
22
|
+
if (typeof source === 'string' || source instanceof URL) return readFile(source);
|
|
23
|
+
if (source && typeof source === 'object' && 'bytes' in source && source.bytes instanceof Uint8Array) {
|
|
24
|
+
return Buffer.from(source.bytes);
|
|
25
|
+
}
|
|
26
|
+
throw new ImpactAnalysisInputError('Usage Index source must be a file path or { bytes, artifact }.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function artifactFor(source, configured) {
|
|
30
|
+
const artifact = source && typeof source === 'object' && 'bytes' in source
|
|
31
|
+
? source.artifact
|
|
32
|
+
: configured ?? DEFAULT_USAGE_INDEX_PATH;
|
|
33
|
+
if (!isPortableRelativePath(artifact)) {
|
|
34
|
+
throw new ImpactAnalysisInputError('Usage Index artifact must be a portable repository-relative path.');
|
|
35
|
+
}
|
|
36
|
+
return artifact;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function lineageMismatches(expected, actual, fields) {
|
|
40
|
+
return fields.filter((field) => {
|
|
41
|
+
const path = field.split('.');
|
|
42
|
+
const read = (value) => path.reduce((current, segment) => current?.[segment], value);
|
|
43
|
+
return read(expected) !== read(actual);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateLineage(baseInput, usageIndex) {
|
|
48
|
+
const projectMismatches = lineageMismatches(
|
|
49
|
+
usageIndex.input.projectManifest,
|
|
50
|
+
baseInput.projectManifest,
|
|
51
|
+
['schemaVersion', 'artifact', 'artifactDigest', 'repository.name', 'repository.root']
|
|
52
|
+
);
|
|
53
|
+
const versionMismatches = lineageMismatches(
|
|
54
|
+
usageIndex.input.versionAnalysis,
|
|
55
|
+
baseInput.versionAnalysis,
|
|
56
|
+
['schemaVersion', 'artifact', 'artifactDigest']
|
|
57
|
+
);
|
|
58
|
+
const mismatches = [
|
|
59
|
+
...projectMismatches.map((field) => `Project Manifest ${field}`),
|
|
60
|
+
...versionMismatches.map((field) => `Version Analysis ${field}`)
|
|
61
|
+
];
|
|
62
|
+
if (mismatches.length > 0) {
|
|
63
|
+
throw new ImpactAnalysisInputError(
|
|
64
|
+
`Usage Index lineage mismatch: ${mismatches.join(', ')}.`,
|
|
65
|
+
'LINEAGE_MISMATCH'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateUsageReferences(versionAnalysis, usageIndex) {
|
|
71
|
+
const results = new Map();
|
|
72
|
+
for (const result of versionAnalysis.results) {
|
|
73
|
+
const key = `${result.dependency.projectId}\0${result.dependency.packageId}`;
|
|
74
|
+
if (!results.has(key)) results.set(key, []);
|
|
75
|
+
results.get(key).push(result);
|
|
76
|
+
}
|
|
77
|
+
const errors = [];
|
|
78
|
+
for (const usage of usageIndex.dependencies) {
|
|
79
|
+
const key = `${usage.projectId}\0${usage.packageId}`;
|
|
80
|
+
const candidates = results.get(key) ?? [];
|
|
81
|
+
if (candidates.length === 0) {
|
|
82
|
+
errors.push(`${usage.packageId} in ${usage.projectId} has no Version Analysis result`);
|
|
83
|
+
} else if (!candidates.some((result) => result.dependency.declaredName === usage.name)) {
|
|
84
|
+
errors.push(`${usage.packageId} in ${usage.projectId} has a dependency-name mismatch`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
throw new ImpactAnalysisInputError(`invalid Usage Index references: ${errors.sort().join('; ')}.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateCoverageReferences(projectManifest, usageIndex) {
|
|
93
|
+
if (!usageIndex.analysis.coverage) return;
|
|
94
|
+
const projects = new Map(projectManifest.projects.map((project) => [project.id, project]));
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const coverage of usageIndex.analysis.coverage) {
|
|
97
|
+
const project = projects.get(coverage.projectId);
|
|
98
|
+
if (!project) {
|
|
99
|
+
errors.push(`${coverage.projectId} coverage references an unknown project`);
|
|
100
|
+
} else if (coverage.projectPath !== project.path || coverage.ecosystem !== project.ecosystem) {
|
|
101
|
+
errors.push(`${coverage.projectId} coverage project identity does not match Project Manifest`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (errors.length > 0) {
|
|
105
|
+
throw new ImpactAnalysisInputError(`invalid Usage Index coverage references: ${errors.sort().join('; ')}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function loadImpactAnalysisInputs(sources, options = {}) {
|
|
110
|
+
if (!sources?.projectManifest || !sources?.versionAnalysis || !sources?.usageIndex) {
|
|
111
|
+
throw new ImpactAnalysisInputError('projectManifest, versionAnalysis, and usageIndex sources are required.');
|
|
112
|
+
}
|
|
113
|
+
let base;
|
|
114
|
+
try {
|
|
115
|
+
base = await loadUsageDiscoveryInputs(sources, options);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof UsageDiscoveryInputError) {
|
|
118
|
+
throw new ImpactAnalysisInputError(error.message, error.code);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const artifact = artifactFor(sources.usageIndex, options.usageIndexArtifact);
|
|
123
|
+
const bytes = await readBytes(sources.usageIndex);
|
|
124
|
+
let usageIndex;
|
|
125
|
+
try {
|
|
126
|
+
usageIndex = JSON.parse(bytes.toString('utf8'));
|
|
127
|
+
} catch {
|
|
128
|
+
throw new ImpactAnalysisInputError('Usage Index bytes are not valid JSON.');
|
|
129
|
+
}
|
|
130
|
+
if (usageIndex?.schemaVersion !== USAGE_INDEX_SCHEMA_VERSION) {
|
|
131
|
+
throw new ImpactAnalysisInputError(
|
|
132
|
+
`unsupported Usage Index schema version; expected ${USAGE_INDEX_SCHEMA_VERSION}.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
validateUsageIndex(usageIndex);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new ImpactAnalysisInputError(error.message);
|
|
139
|
+
}
|
|
140
|
+
validateLineage(base.input, usageIndex);
|
|
141
|
+
validateUsageReferences(base.versionAnalysis, usageIndex);
|
|
142
|
+
validateCoverageReferences(base.projectManifest, usageIndex);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
projectManifest: base.projectManifest,
|
|
146
|
+
versionAnalysis: base.versionAnalysis,
|
|
147
|
+
usageIndex,
|
|
148
|
+
input: {
|
|
149
|
+
...base.input,
|
|
150
|
+
usageIndex: {
|
|
151
|
+
schemaVersion: usageIndex.schemaVersion,
|
|
152
|
+
artifact,
|
|
153
|
+
artifactDigest: digest(bytes)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { compareText } from '../portable.js';
|
|
2
|
+
|
|
3
|
+
export const EXACT_SYMBOL_MATCHER_ID = 'exact-symbol';
|
|
4
|
+
export const EXACT_SYMBOL_MATCHER_VERSION = '1.0.0';
|
|
5
|
+
|
|
6
|
+
const SYNTHETIC_SYMBOLS = new Set(['*', 'default']);
|
|
7
|
+
const IDENTIFIER_CHARACTER = '\\p{L}\\p{N}\\p{M}\\p{Pc}$\\u200C\\u200D';
|
|
8
|
+
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isMatchableUsageSymbol(symbol) {
|
|
14
|
+
return typeof symbol === 'string' && symbol.length > 0 && !SYNTHETIC_SYMBOLS.has(symbol);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function summaryContainsExactSymbol(summary, symbol) {
|
|
18
|
+
if (typeof summary !== 'string' || !isMatchableUsageSymbol(symbol)) return false;
|
|
19
|
+
const expression = new RegExp(
|
|
20
|
+
`(?:^|[^${IDENTIFIER_CHARACTER}])${escapeRegExp(symbol)}(?=$|[^${IDENTIFIER_CHARACTER}])`,
|
|
21
|
+
'u'
|
|
22
|
+
);
|
|
23
|
+
return expression.test(summary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function matchFindingToUsage(finding, dependencyUsage) {
|
|
27
|
+
if (finding?.kind !== 'breakingChange' || !dependencyUsage) return [];
|
|
28
|
+
return dependencyUsage.symbols
|
|
29
|
+
.filter((usage) => summaryContainsExactSymbol(finding.summary, usage.name))
|
|
30
|
+
.map((usage) => ({ symbol: usage.name, files: [...usage.files].sort(compareText) }))
|
|
31
|
+
.sort((left, right) => compareText(left.symbol, right.symbol));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createExactSymbolImpactMatcher() {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
id: EXACT_SYMBOL_MATCHER_ID,
|
|
37
|
+
version: EXACT_SYMBOL_MATCHER_VERSION,
|
|
38
|
+
match: matchFindingToUsage
|
|
39
|
+
});
|
|
40
|
+
}
|