@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,256 @@
|
|
|
1
|
+
import { compareText } from '../portable.js';
|
|
2
|
+
|
|
3
|
+
const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
4
|
+
const PRIVATE_KEYS = new Set([
|
|
5
|
+
'authorization', 'proxyauthorization', 'cookie', 'setcookie', 'etag',
|
|
6
|
+
'lastmodified', 'headers', 'cachekey', 'stack', 'password', 'credential',
|
|
7
|
+
'credentials', 'secret', 'token', 'accesstoken', 'apikey', 'clientsecret', 'privatekey'
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export class NpmPackumentError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'NpmPackumentError';
|
|
14
|
+
this.code = 'NPM_PACKUMENT_INVALID';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function plainObject(value) {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
20
|
+
const prototype = Object.getPrototypeOf(value);
|
|
21
|
+
return prototype === Object.prototype || prototype === null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nullableString(value) {
|
|
25
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizedHttpsUrl(value) {
|
|
29
|
+
if (typeof value !== 'string' || value.trim() === '') return null;
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(value.trim());
|
|
32
|
+
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
33
|
+
url.search = '';
|
|
34
|
+
url.hash = '';
|
|
35
|
+
if (url.pathname === '/') return `${url.protocol}//${url.host}`;
|
|
36
|
+
return url.toString();
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function repositoryUrl(value) {
|
|
43
|
+
const raw = typeof value === 'string' ? value : plainObject(value) ? value.url : null;
|
|
44
|
+
if (typeof raw !== 'string') return null;
|
|
45
|
+
const candidate = raw.startsWith('git+') ? raw.slice(4) : raw;
|
|
46
|
+
const normalized = normalizedHttpsUrl(candidate);
|
|
47
|
+
if (!normalized) return null;
|
|
48
|
+
const url = new URL(normalized);
|
|
49
|
+
if (url.hostname.toLowerCase() === 'github.com') {
|
|
50
|
+
url.pathname = url.pathname.replace(/\.git$/i, '');
|
|
51
|
+
return url.pathname === '/' ? `${url.protocol}//${url.host}` : url.toString();
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function issueUrl(value) {
|
|
57
|
+
const raw = typeof value === 'string' ? value : plainObject(value) ? value.url : null;
|
|
58
|
+
return normalizedHttpsUrl(raw);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function publishedAt(value) {
|
|
62
|
+
if (typeof value !== 'string') return null;
|
|
63
|
+
const date = new Date(value);
|
|
64
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function explicitDeprecated(value) {
|
|
68
|
+
if (typeof value === 'string') return value.trim() === '' ? false : true;
|
|
69
|
+
if (typeof value === 'boolean') return value;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function npmPageUrl(name) {
|
|
74
|
+
return `https://www.npmjs.com/package/${name}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function npmReleaseUrl(name, version) {
|
|
78
|
+
return `${npmPageUrl(name)}/v/${encodeURIComponent(version)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sortedStrings(values) {
|
|
82
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))]
|
|
83
|
+
.sort(compareText);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function privateKey(key) {
|
|
87
|
+
return PRIVATE_KEYS.has(key.toLowerCase().replace(/[-_]/g, ''));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function unsafeStoredString(value) {
|
|
91
|
+
return value.startsWith('/')
|
|
92
|
+
|| /^[A-Za-z]:[\\/]/.test(value)
|
|
93
|
+
|| /[a-z][a-z0-9+.-]*:\/\/[^/?#]*@/i.test(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Keep cache envelopes portable even when publisher-controlled metadata
|
|
98
|
+
* contains a credentialed URL or request-shaped property.
|
|
99
|
+
*/
|
|
100
|
+
export function sanitizeNpmPackumentForCache(value) {
|
|
101
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
|
|
102
|
+
if (typeof value === 'string') return unsafeStoredString(value) ? null : value;
|
|
103
|
+
if (Array.isArray(value)) return value.map(sanitizeNpmPackumentForCache);
|
|
104
|
+
if (!plainObject(value)) return null;
|
|
105
|
+
return Object.fromEntries(Object.keys(value).sort(compareText).flatMap((key) => {
|
|
106
|
+
if (privateKey(key)) return [];
|
|
107
|
+
return [[key, sanitizeNpmPackumentForCache(value[key])]];
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function packageMetadata(packument) {
|
|
112
|
+
const deprecated = packument.deprecated;
|
|
113
|
+
const deprecationMessage = nullableString(deprecated);
|
|
114
|
+
return {
|
|
115
|
+
description: nullableString(packument.description),
|
|
116
|
+
license: nullableString(packument.license) ?? nullableString(packument.license?.type),
|
|
117
|
+
homepageUrl: normalizedHttpsUrl(packument.homepage),
|
|
118
|
+
documentationUrl: null,
|
|
119
|
+
repositoryUrl: repositoryUrl(packument.repository),
|
|
120
|
+
issueUrl: issueUrl(packument.bugs),
|
|
121
|
+
deprecationMessage,
|
|
122
|
+
projectStatus: deprecated === undefined || deprecated === false ? null : 'deprecated'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function normalizeNpmRegistryBaseUrl(value) {
|
|
127
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
128
|
+
throw new NpmPackumentError('npm Registry base URL must be a non-empty HTTPS URL.');
|
|
129
|
+
}
|
|
130
|
+
let url;
|
|
131
|
+
try {
|
|
132
|
+
url = new URL(value);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new NpmPackumentError('npm Registry base URL must be a valid HTTPS URL.');
|
|
135
|
+
}
|
|
136
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
|
137
|
+
throw new NpmPackumentError('npm Registry base URL must be HTTPS and must not contain credentials or query data.');
|
|
138
|
+
}
|
|
139
|
+
url.pathname = url.pathname.replace(/\/$/, '');
|
|
140
|
+
return url.toString().replace(/\/$/, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function validateNpmResearchPackage(researchPackage) {
|
|
144
|
+
if (!plainObject(researchPackage)) {
|
|
145
|
+
throw new NpmPackumentError('npm research package must be an object.');
|
|
146
|
+
}
|
|
147
|
+
const name = researchPackage.normalizedName;
|
|
148
|
+
if (typeof name !== 'string' || !NPM_NAME.test(name)
|
|
149
|
+
|| researchPackage.id !== `npm:${name}`
|
|
150
|
+
|| researchPackage.registry !== 'npm'
|
|
151
|
+
|| researchPackage.ecosystem !== 'node') {
|
|
152
|
+
throw new NpmPackumentError('Research package is not a valid npm registry identity.');
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
id: researchPackage.id,
|
|
156
|
+
registry: 'npm',
|
|
157
|
+
ecosystem: 'node',
|
|
158
|
+
normalizedName: name,
|
|
159
|
+
observedDeclaredNames: sortedStrings(researchPackage.observedDeclaredNames ?? [name]),
|
|
160
|
+
occurrences: structuredClone(researchPackage.occurrences ?? [])
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function npmRegistryUrls(registryBaseUrl, normalizedName) {
|
|
165
|
+
const base = normalizeNpmRegistryBaseUrl(registryBaseUrl);
|
|
166
|
+
return {
|
|
167
|
+
registryBaseUrl: base,
|
|
168
|
+
packageUrl: npmPageUrl(normalizedName),
|
|
169
|
+
apiUrl: `${base}/${encodeURIComponent(normalizedName)}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Validate the minimum packument shape before the caller writes it to cache. */
|
|
174
|
+
export function validateNpmPackument(packument, normalizedName) {
|
|
175
|
+
if (!plainObject(packument)
|
|
176
|
+
|| packument.name !== normalizedName
|
|
177
|
+
|| !plainObject(packument['dist-tags'])
|
|
178
|
+
|| !plainObject(packument.versions)
|
|
179
|
+
|| !plainObject(packument.time)) {
|
|
180
|
+
throw new NpmPackumentError('npm Registry package metadata has an unsupported shape.');
|
|
181
|
+
}
|
|
182
|
+
if (Object.hasOwn(packument['dist-tags'], 'latest')
|
|
183
|
+
&& typeof packument['dist-tags'].latest !== 'string') {
|
|
184
|
+
throw new NpmPackumentError('npm Registry latest distribution tag must be a string.');
|
|
185
|
+
}
|
|
186
|
+
return packument;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Translate a valid full npm packument into records that match the package and
|
|
191
|
+
* source shapes used by the Knowledge Manifest, without assembling a manifest.
|
|
192
|
+
*/
|
|
193
|
+
export function normalizeNpmPackument(researchPackage, packument, { snapshot, registryBaseUrl }) {
|
|
194
|
+
const input = validateNpmResearchPackage(researchPackage);
|
|
195
|
+
validateNpmPackument(packument, input.normalizedName);
|
|
196
|
+
const urls = npmRegistryUrls(registryBaseUrl, input.normalizedName);
|
|
197
|
+
const sourceId = `${input.id}:registry`;
|
|
198
|
+
const versions = packument.versions;
|
|
199
|
+
const time = packument.time;
|
|
200
|
+
const releases = Object.keys(versions).sort(compareText).map((version) => ({
|
|
201
|
+
version,
|
|
202
|
+
publishedAt: publishedAt(time[version]),
|
|
203
|
+
url: npmReleaseUrl(input.normalizedName, version),
|
|
204
|
+
prerelease: null,
|
|
205
|
+
yanked: null,
|
|
206
|
+
deprecated: explicitDeprecated(versions[version]?.deprecated),
|
|
207
|
+
sourceIds: [sourceId]
|
|
208
|
+
}));
|
|
209
|
+
const latestVersion = packument['dist-tags'].latest;
|
|
210
|
+
const latestRecord = typeof latestVersion === 'string' && Object.hasOwn(versions, latestVersion)
|
|
211
|
+
? {
|
|
212
|
+
version: latestVersion,
|
|
213
|
+
selection: 'dist-tag:latest',
|
|
214
|
+
publishedAt: publishedAt(time[latestVersion]),
|
|
215
|
+
releaseUrl: npmReleaseUrl(input.normalizedName, latestVersion),
|
|
216
|
+
prerelease: null,
|
|
217
|
+
yanked: null,
|
|
218
|
+
deprecated: explicitDeprecated(versions[latestVersion]?.deprecated),
|
|
219
|
+
sourceId
|
|
220
|
+
}
|
|
221
|
+
: null;
|
|
222
|
+
const status = latestRecord ? 'resolved' : 'partial';
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
package: {
|
|
226
|
+
id: input.id,
|
|
227
|
+
ecosystem: 'node',
|
|
228
|
+
status,
|
|
229
|
+
identity: {
|
|
230
|
+
observedDeclaredNames: input.observedDeclaredNames,
|
|
231
|
+
normalizedName: input.normalizedName,
|
|
232
|
+
registry: 'npm',
|
|
233
|
+
...urls
|
|
234
|
+
},
|
|
235
|
+
occurrences: input.occurrences,
|
|
236
|
+
metadata: packageMetadata(packument),
|
|
237
|
+
latest: latestRecord,
|
|
238
|
+
releaseIndex: releases,
|
|
239
|
+
sourceIds: [sourceId],
|
|
240
|
+
warningCodes: []
|
|
241
|
+
},
|
|
242
|
+
source: {
|
|
243
|
+
id: sourceId,
|
|
244
|
+
kind: 'registry',
|
|
245
|
+
authority: 'registryAuthoritative',
|
|
246
|
+
trust: 'publisher',
|
|
247
|
+
url: urls.packageUrl,
|
|
248
|
+
apiUrl: urls.apiUrl,
|
|
249
|
+
status: 'available',
|
|
250
|
+
supports: ['identity', 'latest', 'metadata', 'releases'],
|
|
251
|
+
discoveredFrom: null,
|
|
252
|
+
trustEvidenceSourceIds: [],
|
|
253
|
+
snapshot
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchNpmJson,
|
|
3
|
+
BoundedFetchError,
|
|
4
|
+
validateRegistryResponseLimit
|
|
5
|
+
} from '../http/bounded-fetch.js';
|
|
6
|
+
import { USER_AGENT } from '../constants.js';
|
|
7
|
+
import { createCacheIdentity, createKnowledgeCache } from '../knowledge-cache.js';
|
|
8
|
+
import {
|
|
9
|
+
normalizeNpmPackument,
|
|
10
|
+
normalizeNpmRegistryBaseUrl,
|
|
11
|
+
npmRegistryUrls,
|
|
12
|
+
sanitizeNpmPackumentForCache,
|
|
13
|
+
validateNpmPackument,
|
|
14
|
+
validateNpmResearchPackage
|
|
15
|
+
} from './npm-packument.js';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_NPM_REGISTRY_BASE_URL = 'https://registry.npmjs.org';
|
|
18
|
+
export const DEFAULT_NPM_METADATA_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
19
|
+
export const DEFAULT_NPM_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
function currentDate(clock) {
|
|
22
|
+
const value = clock();
|
|
23
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
24
|
+
if (Number.isNaN(date.getTime())) throw new Error('npm Registry adapter clock returned an invalid time.');
|
|
25
|
+
return date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cacheIdentityFor(input) {
|
|
29
|
+
return createCacheIdentity({
|
|
30
|
+
adapter: 'npm',
|
|
31
|
+
resourceKind: 'registry-package',
|
|
32
|
+
packageId: input.id,
|
|
33
|
+
resourceVariant: 'full-packument',
|
|
34
|
+
adapterVersion: '1'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snapshot(bodyDigest, retrievedAt) {
|
|
39
|
+
return {
|
|
40
|
+
contentDigest: bodyDigest,
|
|
41
|
+
mediaType: 'application/json',
|
|
42
|
+
retrievedAt,
|
|
43
|
+
freshness: 'fresh'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cacheOutcome(readResult) {
|
|
48
|
+
if (readResult.status === 'missing') return { outcome: 'miss' };
|
|
49
|
+
if (readResult.status === 'expired') return { outcome: 'expired' };
|
|
50
|
+
if (readResult.status === 'corrupted') {
|
|
51
|
+
return { outcome: 'corrupted', reason: readResult.reason };
|
|
52
|
+
}
|
|
53
|
+
return { outcome: 'hit' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unavailableResult(input, registryBaseUrl, cache, {
|
|
57
|
+
status = 'unavailable',
|
|
58
|
+
warningCode,
|
|
59
|
+
message,
|
|
60
|
+
retryable,
|
|
61
|
+
errorCode
|
|
62
|
+
}) {
|
|
63
|
+
const urls = npmRegistryUrls(registryBaseUrl, input.normalizedName);
|
|
64
|
+
const sourceId = `${input.id}:registry`;
|
|
65
|
+
const warning = {
|
|
66
|
+
code: warningCode,
|
|
67
|
+
packageId: input.id,
|
|
68
|
+
sourceId,
|
|
69
|
+
message,
|
|
70
|
+
retryable
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
package: {
|
|
74
|
+
id: input.id,
|
|
75
|
+
ecosystem: 'node',
|
|
76
|
+
status,
|
|
77
|
+
identity: {
|
|
78
|
+
observedDeclaredNames: input.observedDeclaredNames,
|
|
79
|
+
normalizedName: input.normalizedName,
|
|
80
|
+
registry: 'npm',
|
|
81
|
+
...urls
|
|
82
|
+
},
|
|
83
|
+
occurrences: input.occurrences,
|
|
84
|
+
metadata: {},
|
|
85
|
+
latest: null,
|
|
86
|
+
releaseIndex: [],
|
|
87
|
+
sourceIds: [sourceId],
|
|
88
|
+
warningCodes: [warningCode]
|
|
89
|
+
},
|
|
90
|
+
source: {
|
|
91
|
+
id: sourceId,
|
|
92
|
+
kind: 'registry',
|
|
93
|
+
authority: 'registryAuthoritative',
|
|
94
|
+
trust: 'publisher',
|
|
95
|
+
url: urls.packageUrl,
|
|
96
|
+
apiUrl: urls.apiUrl,
|
|
97
|
+
status: status === 'notFound' ? 'notFound' : 'unavailable',
|
|
98
|
+
supports: ['identity'],
|
|
99
|
+
discoveredFrom: null,
|
|
100
|
+
trustEvidenceSourceIds: [],
|
|
101
|
+
snapshot: null
|
|
102
|
+
},
|
|
103
|
+
cache,
|
|
104
|
+
warnings: [warning],
|
|
105
|
+
errorCode
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizedResult(researchPackage, body, registryBaseUrl, cache, cacheRecord) {
|
|
110
|
+
const normalized = normalizeNpmPackument(researchPackage, body, {
|
|
111
|
+
registryBaseUrl,
|
|
112
|
+
snapshot: snapshot(cacheRecord.bodyDigest, cacheRecord.storedAt)
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
...normalized,
|
|
116
|
+
cache,
|
|
117
|
+
warnings: [],
|
|
118
|
+
errorCode: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create the private npm-compatible Registry adapter. It resolves `npm:`
|
|
124
|
+
* identities only; npm, Yarn, pnpm, and Bun are intentionally not inputs to
|
|
125
|
+
* its request or cache behavior.
|
|
126
|
+
*/
|
|
127
|
+
export function createNpmRegistryAdapter({
|
|
128
|
+
registryBaseUrl = DEFAULT_NPM_REGISTRY_BASE_URL,
|
|
129
|
+
fetch: fetchImplementation = globalThis.fetch,
|
|
130
|
+
cache,
|
|
131
|
+
clock = () => new Date(),
|
|
132
|
+
timeoutMs = 10_000,
|
|
133
|
+
maxResponseBytes = DEFAULT_NPM_MAX_RESPONSE_BYTES,
|
|
134
|
+
ttlMs = DEFAULT_NPM_METADATA_TTL_MS,
|
|
135
|
+
offline = false,
|
|
136
|
+
userAgent = USER_AGENT
|
|
137
|
+
} = {}) {
|
|
138
|
+
const normalizedRegistryBaseUrl = normalizeNpmRegistryBaseUrl(registryBaseUrl);
|
|
139
|
+
validateRegistryResponseLimit(maxResponseBytes, { errorPrefix: 'NPM' });
|
|
140
|
+
const knowledgeCache = cache ?? createKnowledgeCache({ clock });
|
|
141
|
+
if (!knowledgeCache || typeof knowledgeCache.read !== 'function' || typeof knowledgeCache.write !== 'function') {
|
|
142
|
+
throw new Error('npm Registry adapter requires a Knowledge Store cache.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function requestFor(researchPackage) {
|
|
146
|
+
const input = validateNpmResearchPackage(researchPackage);
|
|
147
|
+
return npmRegistryUrls(normalizedRegistryBaseUrl, input.normalizedName).apiUrl;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
requestFor,
|
|
152
|
+
cacheIdentityFor(researchPackage) {
|
|
153
|
+
return cacheIdentityFor(validateNpmResearchPackage(researchPackage));
|
|
154
|
+
},
|
|
155
|
+
async researchPackage(researchPackage) {
|
|
156
|
+
const input = validateNpmResearchPackage(researchPackage);
|
|
157
|
+
const identity = cacheIdentityFor(input);
|
|
158
|
+
const cached = await knowledgeCache.read(identity);
|
|
159
|
+
|
|
160
|
+
if (cached.status === 'fresh') {
|
|
161
|
+
try {
|
|
162
|
+
return normalizedResult(input, cached.body, normalizedRegistryBaseUrl, cacheOutcome(cached), cached);
|
|
163
|
+
} catch {
|
|
164
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, cacheOutcome(cached), {
|
|
165
|
+
warningCode: 'REGISTRY_RESPONSE_INVALID',
|
|
166
|
+
message: 'npm Registry package metadata is invalid.',
|
|
167
|
+
retryable: true,
|
|
168
|
+
errorCode: 'NPM_PACKUMENT_INVALID'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const previousCacheOutcome = cacheOutcome(cached);
|
|
174
|
+
if (offline) {
|
|
175
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
176
|
+
warningCode: 'OFFLINE_CACHE_MISS',
|
|
177
|
+
message: 'Offline mode has no fresh npm Registry package metadata in cache.',
|
|
178
|
+
retryable: false,
|
|
179
|
+
errorCode: 'NPM_OFFLINE_CACHE_MISS'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
let response;
|
|
183
|
+
try {
|
|
184
|
+
response = await fetchNpmJson(requestFor(input), {
|
|
185
|
+
fetchImplementation,
|
|
186
|
+
timeoutMs,
|
|
187
|
+
maxResponseBytes,
|
|
188
|
+
userAgent
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error instanceof BoundedFetchError
|
|
192
|
+
&& ['NPM_RESPONSE_INVALID', 'NPM_RESPONSE_TOO_LARGE'].includes(error.code)) {
|
|
193
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
194
|
+
warningCode: 'REGISTRY_RESPONSE_INVALID',
|
|
195
|
+
message: 'npm Registry returned an invalid package metadata response.',
|
|
196
|
+
retryable: true,
|
|
197
|
+
errorCode: error.code
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
201
|
+
warningCode: 'REGISTRY_UNAVAILABLE',
|
|
202
|
+
message: 'npm Registry package metadata is unavailable.',
|
|
203
|
+
retryable: true,
|
|
204
|
+
errorCode: error instanceof BoundedFetchError ? error.code : 'NPM_TRANSPORT_FAILED'
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (response.status === 404) {
|
|
209
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
210
|
+
status: 'notFound',
|
|
211
|
+
warningCode: 'PACKAGE_NOT_FOUND',
|
|
212
|
+
message: 'Package was not found in the npm Registry.',
|
|
213
|
+
retryable: false,
|
|
214
|
+
errorCode: 'NPM_HTTP_404'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (response.status === 429) {
|
|
218
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
219
|
+
warningCode: 'REGISTRY_RATE_LIMITED',
|
|
220
|
+
message: 'npm Registry rate limited package metadata retrieval.',
|
|
221
|
+
retryable: true,
|
|
222
|
+
errorCode: 'NPM_HTTP_429'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (response.status !== 200 && response.status >= 200 && response.status < 300) {
|
|
226
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
227
|
+
warningCode: 'REGISTRY_RESPONSE_INVALID',
|
|
228
|
+
message: 'npm Registry returned an invalid package metadata response.',
|
|
229
|
+
retryable: true,
|
|
230
|
+
errorCode: `NPM_HTTP_${response.status}`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (response.status !== 200) {
|
|
234
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
235
|
+
warningCode: 'REGISTRY_UNAVAILABLE',
|
|
236
|
+
message: 'npm Registry package metadata is unavailable.',
|
|
237
|
+
retryable: response.status >= 500,
|
|
238
|
+
errorCode: `NPM_HTTP_${response.status}`
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
validateNpmPackument(response.body, input.normalizedName);
|
|
244
|
+
} catch {
|
|
245
|
+
return unavailableResult(input, normalizedRegistryBaseUrl, previousCacheOutcome, {
|
|
246
|
+
warningCode: 'REGISTRY_RESPONSE_INVALID',
|
|
247
|
+
message: 'npm Registry returned an invalid package metadata response.',
|
|
248
|
+
retryable: true,
|
|
249
|
+
errorCode: 'NPM_PACKUMENT_INVALID'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const cacheBody = sanitizeNpmPackumentForCache(response.body);
|
|
254
|
+
const stored = await knowledgeCache.write(identity, cacheBody, { ttlMs });
|
|
255
|
+
const fetchedCache = {
|
|
256
|
+
outcome: cached.status === 'expired' ? 'revalidated' : cached.status === 'corrupted' ? 'corrupted-replaced' : 'miss'
|
|
257
|
+
};
|
|
258
|
+
if (cached.status === 'corrupted') fetchedCache.reason = cached.reason;
|
|
259
|
+
return normalizedResult(input, cacheBody, normalizedRegistryBaseUrl, fetchedCache, stored);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|