@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,455 @@
|
|
|
1
|
+
import { normalizePythonPackageName } from './python-requirements.js';
|
|
2
|
+
import { compareText, isPortableRelativePath, isSorted } from './portable.js';
|
|
3
|
+
|
|
4
|
+
const PLAN_VERSION = '1';
|
|
5
|
+
const SUPPORTED_ECOSYSTEMS = new Map([
|
|
6
|
+
['node', 'npm'],
|
|
7
|
+
['python', 'pypi']
|
|
8
|
+
]);
|
|
9
|
+
const INVALID_REASONS = new Set([
|
|
10
|
+
'empty-name',
|
|
11
|
+
'invalid-npm-name',
|
|
12
|
+
'invalid-pypi-name',
|
|
13
|
+
'unnamed-direct-reference',
|
|
14
|
+
'local-path-reference',
|
|
15
|
+
'unsupported-reference'
|
|
16
|
+
]);
|
|
17
|
+
const INVALID_WARNING = 'INVALID_PACKAGE_REFERENCE';
|
|
18
|
+
const UNSUPPORTED_WARNING = 'UNSUPPORTED_RESEARCH_ECOSYSTEM';
|
|
19
|
+
|
|
20
|
+
function compareBy(...selectors) {
|
|
21
|
+
return (left, right) => {
|
|
22
|
+
for (const selector of selectors) {
|
|
23
|
+
const result = compareText(selector(left), selector(right));
|
|
24
|
+
if (result !== 0) return result;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const compareOccurrences = compareBy(
|
|
31
|
+
(item) => item.projectId,
|
|
32
|
+
(item) => item.manifest,
|
|
33
|
+
(item) => item.dependencyType,
|
|
34
|
+
(item) => item.declaredName,
|
|
35
|
+
(item) => item.declaredVersion ?? ''
|
|
36
|
+
);
|
|
37
|
+
const compareInvalidOccurrences = compareBy(
|
|
38
|
+
(item) => item.projectId,
|
|
39
|
+
(item) => item.manifest,
|
|
40
|
+
(item) => item.dependencyType,
|
|
41
|
+
(item) => item.declaredName,
|
|
42
|
+
(item) => item.declaredVersion ?? '',
|
|
43
|
+
(item) => item.reason
|
|
44
|
+
);
|
|
45
|
+
const compareWarnings = compareBy(
|
|
46
|
+
(item) => item.ecosystem ?? '',
|
|
47
|
+
(item) => item.projectId ?? '',
|
|
48
|
+
(item) => item.manifest ?? '',
|
|
49
|
+
(item) => item.dependencyType ?? '',
|
|
50
|
+
(item) => item.declaredName ?? '',
|
|
51
|
+
(item) => item.code,
|
|
52
|
+
(item) => item.message
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function isNpmPublicName(name) {
|
|
56
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPyPIName(name) {
|
|
60
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isLocalPathReference(value) {
|
|
64
|
+
return typeof value === 'string'
|
|
65
|
+
&& /^(?:file:)?(?:\.{1,2}(?:[\\/]|$)|[\\/]|~[\\/])/i.test(value.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRemoteReference(value) {
|
|
69
|
+
return typeof value === 'string'
|
|
70
|
+
&& /^(?:git\+|git:|https?:|ssh:|github:|gitlab:|bitbucket:)/i.test(value.trim());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isNodeUnresearchableReference(value) {
|
|
74
|
+
return typeof value === 'string'
|
|
75
|
+
&& /^(?:workspace:|file:|link:|npm:|git\+|git:|https?:|ssh:|github:|gitlab:|bitbucket:|\.{1,2}[\\/]|[\\/]|~[\\/])/i.test(value.trim());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeReference(value) {
|
|
79
|
+
if (typeof value !== 'string') return value;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
if (isLocalPathReference(trimmed)) return '<local-path-reference>';
|
|
82
|
+
|
|
83
|
+
const prefix = trimmed.startsWith('git+') ? 'git+' : '';
|
|
84
|
+
const candidate = prefix ? trimmed.slice(prefix.length) : trimmed;
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(candidate);
|
|
87
|
+
if (!['http:', 'https:', 'ssh:', 'git:'].includes(url.protocol)) return trimmed;
|
|
88
|
+
url.username = '';
|
|
89
|
+
url.password = '';
|
|
90
|
+
url.search = '';
|
|
91
|
+
url.hash = '';
|
|
92
|
+
return `${prefix}${url.toString()}`;
|
|
93
|
+
} catch {
|
|
94
|
+
return trimmed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sanitizedOccurrence(project, dependency) {
|
|
99
|
+
return {
|
|
100
|
+
projectId: project.id,
|
|
101
|
+
projectPath: project.path,
|
|
102
|
+
manifest: dependency.manifest,
|
|
103
|
+
ecosystem: project.ecosystem,
|
|
104
|
+
dependencyType: dependency.type,
|
|
105
|
+
declaredName: sanitizeReference(dependency.name),
|
|
106
|
+
normalizedName: sanitizeReference(dependency.normalizedName),
|
|
107
|
+
declaredVersion: dependency.declaredVersion === null
|
|
108
|
+
? null
|
|
109
|
+
: sanitizeReference(dependency.declaredVersion)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function nodeEligibility(dependency) {
|
|
114
|
+
const normalizedName = dependency.normalizedName.toLowerCase();
|
|
115
|
+
if (!normalizedName) return { outcome: 'invalid', reason: 'empty-name' };
|
|
116
|
+
if (!isNpmPublicName(normalizedName)) return { outcome: 'invalid', reason: 'invalid-npm-name' };
|
|
117
|
+
if (isLocalPathReference(dependency.declaredVersion)) {
|
|
118
|
+
return { outcome: 'invalid', reason: 'local-path-reference' };
|
|
119
|
+
}
|
|
120
|
+
if (isNodeUnresearchableReference(dependency.declaredVersion)) {
|
|
121
|
+
return { outcome: 'invalid', reason: 'unsupported-reference' };
|
|
122
|
+
}
|
|
123
|
+
return { outcome: 'researchable', registry: 'npm', normalizedName };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pythonEligibility(dependency) {
|
|
127
|
+
const originalName = dependency.name ?? '';
|
|
128
|
+
const normalizedName = normalizePythonPackageName(dependency.normalizedName ?? '');
|
|
129
|
+
if (!normalizedName) return { outcome: 'invalid', reason: 'empty-name' };
|
|
130
|
+
if (isLocalPathReference(originalName)) {
|
|
131
|
+
return { outcome: 'invalid', reason: 'local-path-reference' };
|
|
132
|
+
}
|
|
133
|
+
if (isRemoteReference(originalName)) {
|
|
134
|
+
return { outcome: 'invalid', reason: 'unnamed-direct-reference' };
|
|
135
|
+
}
|
|
136
|
+
if (!isPyPIName(normalizedName)) return { outcome: 'invalid', reason: 'invalid-pypi-name' };
|
|
137
|
+
return { outcome: 'researchable', registry: 'pypi', normalizedName };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function eligibility(project, dependency) {
|
|
141
|
+
if (project.ecosystem === 'node') return nodeEligibility(dependency);
|
|
142
|
+
if (project.ecosystem === 'python') return pythonEligibility(dependency);
|
|
143
|
+
return { outcome: 'unsupported' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function invalidWarning(occurrence) {
|
|
147
|
+
return {
|
|
148
|
+
code: INVALID_WARNING,
|
|
149
|
+
projectId: occurrence.projectId,
|
|
150
|
+
manifest: occurrence.manifest,
|
|
151
|
+
dependencyType: occurrence.dependencyType,
|
|
152
|
+
declaredName: occurrence.declaredName,
|
|
153
|
+
declaredVersion: occurrence.declaredVersion,
|
|
154
|
+
message: `Dependency reference is not a supported public ${occurrence.ecosystem} package identity.`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unsupportedWarning(unsupported) {
|
|
159
|
+
return {
|
|
160
|
+
code: UNSUPPORTED_WARNING,
|
|
161
|
+
ecosystem: unsupported.ecosystem,
|
|
162
|
+
message: `Dependencies in ${unsupported.ecosystem} projects are not supported by MVP-02 research planning.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function unsupportedRecords(records) {
|
|
167
|
+
return [...records.values()]
|
|
168
|
+
.map((record) => ({
|
|
169
|
+
ecosystem: record.ecosystem,
|
|
170
|
+
projectIds: [...record.projectIds].sort(compareText),
|
|
171
|
+
occurrenceCount: record.occurrenceCount
|
|
172
|
+
}))
|
|
173
|
+
.sort((left, right) => compareText(left.ecosystem, right.ecosystem));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function packageRecords(records) {
|
|
177
|
+
return [...records.values()]
|
|
178
|
+
.map((record) => ({
|
|
179
|
+
id: `${record.registry}:${record.normalizedName}`,
|
|
180
|
+
registry: record.registry,
|
|
181
|
+
ecosystem: record.ecosystem,
|
|
182
|
+
normalizedName: record.normalizedName,
|
|
183
|
+
observedDeclaredNames: [...record.observedDeclaredNames].sort(compareText),
|
|
184
|
+
occurrences: record.occurrences.sort(compareOccurrences)
|
|
185
|
+
}))
|
|
186
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasCredentials(value) {
|
|
190
|
+
return typeof value === 'string' && /[a-z][a-z0-9+.-]*:\/\/[^/?#]*@/i.test(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function plainObject(value) {
|
|
194
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
195
|
+
const prototype = Object.getPrototypeOf(value);
|
|
196
|
+
return prototype === Object.prototype || prototype === null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkOnlyKeys(errors, value, allowed, field) {
|
|
200
|
+
if (!plainObject(value)) {
|
|
201
|
+
errors.push(`${field} must be an object.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const key of Object.keys(value)) {
|
|
205
|
+
if (!allowed.has(key)) errors.push(`${field} contains unsupported field ${key}.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function planError(errors) {
|
|
210
|
+
throw new Error(`Research Plan invariant violation: ${errors.join(' ')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function occurrenceKey(occurrence) {
|
|
214
|
+
return [
|
|
215
|
+
occurrence.projectId,
|
|
216
|
+
occurrence.manifest,
|
|
217
|
+
occurrence.dependencyType,
|
|
218
|
+
occurrence.declaredName,
|
|
219
|
+
occurrence.declaredVersion ?? ''
|
|
220
|
+
].join('\0');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate a Research Plan's internal contract. It throws on implementation
|
|
225
|
+
* bugs instead of converting invariant failures into repository warnings.
|
|
226
|
+
*/
|
|
227
|
+
export function validateResearchPlan(plan) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
if (!plainObject(plan)) {
|
|
230
|
+
throw new Error('Research Plan invariant violation: plan must be an object.');
|
|
231
|
+
}
|
|
232
|
+
checkOnlyKeys(errors, plan, new Set([
|
|
233
|
+
'planVersion', 'input', 'summary', 'packages', 'invalidOccurrences', 'unsupported', 'warnings'
|
|
234
|
+
]), 'Research Plan');
|
|
235
|
+
if (plan.planVersion !== PLAN_VERSION) errors.push(`planVersion must be ${PLAN_VERSION}.`);
|
|
236
|
+
const lineage = plan.input?.projectManifest;
|
|
237
|
+
checkOnlyKeys(errors, plan.input, new Set(['projectManifest']), 'Research Plan input');
|
|
238
|
+
checkOnlyKeys(errors, lineage, new Set(['schemaVersion', 'artifact', 'artifactDigest', 'repository']), 'Project Manifest lineage');
|
|
239
|
+
checkOnlyKeys(errors, lineage?.repository, new Set(['name', 'root']), 'Project Manifest lineage repository');
|
|
240
|
+
checkOnlyKeys(errors, plan.summary, new Set([
|
|
241
|
+
'inputProjectCount', 'inputOccurrenceCount', 'researchableOccurrenceCount', 'uniqueResearchPackageCount',
|
|
242
|
+
'invalidOccurrenceCount', 'unsupportedOccurrenceCount'
|
|
243
|
+
]), 'Research Plan summary');
|
|
244
|
+
if (!lineage || lineage.schemaVersion !== '2.0.0') errors.push('input Project Manifest schemaVersion must be 2.0.0.');
|
|
245
|
+
if (!lineage || !isPortableRelativePath(lineage.artifact)) errors.push('input artifact must be portable.');
|
|
246
|
+
if (!lineage || !/^sha256:[a-f0-9]{64}$/.test(lineage.artifactDigest)) errors.push('input artifact digest is invalid.');
|
|
247
|
+
if (!lineage || lineage.repository?.root !== '.') errors.push('input repository root must be .');
|
|
248
|
+
|
|
249
|
+
const packages = plan.packages ?? [];
|
|
250
|
+
const invalidOccurrences = plan.invalidOccurrences ?? [];
|
|
251
|
+
const unsupported = plan.unsupported ?? [];
|
|
252
|
+
const warnings = plan.warnings ?? [];
|
|
253
|
+
const summary = plan.summary ?? {};
|
|
254
|
+
|
|
255
|
+
if (!isSorted(packages, (left, right) => compareText(left.id, right.id))) {
|
|
256
|
+
errors.push('packages must be sorted by id.');
|
|
257
|
+
}
|
|
258
|
+
if (!isSorted(invalidOccurrences, compareInvalidOccurrences)) {
|
|
259
|
+
errors.push('invalidOccurrences must be canonically sorted.');
|
|
260
|
+
}
|
|
261
|
+
if (!isSorted(unsupported, (left, right) => compareText(left.ecosystem, right.ecosystem))) {
|
|
262
|
+
errors.push('unsupported must be sorted by ecosystem.');
|
|
263
|
+
}
|
|
264
|
+
if (!isSorted(warnings, compareWarnings)) errors.push('warnings must be canonically sorted.');
|
|
265
|
+
|
|
266
|
+
const packageIds = new Set();
|
|
267
|
+
let researchableOccurrenceCount = 0;
|
|
268
|
+
for (const packageRecord of packages) {
|
|
269
|
+
checkOnlyKeys(errors, packageRecord, new Set([
|
|
270
|
+
'id', 'registry', 'ecosystem', 'normalizedName', 'observedDeclaredNames', 'occurrences'
|
|
271
|
+
]), 'Research Plan package');
|
|
272
|
+
if (packageIds.has(packageRecord.id)) errors.push(`Duplicate research package ${packageRecord.id}.`);
|
|
273
|
+
packageIds.add(packageRecord.id);
|
|
274
|
+
const expectedId = `${packageRecord.registry}:${packageRecord.normalizedName}`;
|
|
275
|
+
if (packageRecord.id !== expectedId) errors.push(`Package ${packageRecord.id} does not match its identity.`);
|
|
276
|
+
if (!SUPPORTED_ECOSYSTEMS.has(packageRecord.ecosystem)) {
|
|
277
|
+
errors.push(`Package ${packageRecord.id} has an unsupported ecosystem.`);
|
|
278
|
+
}
|
|
279
|
+
if (SUPPORTED_ECOSYSTEMS.get(packageRecord.ecosystem) !== packageRecord.registry) {
|
|
280
|
+
errors.push(`Package ${packageRecord.id} registry does not match its ecosystem.`);
|
|
281
|
+
}
|
|
282
|
+
if (!isSorted(packageRecord.observedDeclaredNames, compareText)) {
|
|
283
|
+
errors.push(`Package ${packageRecord.id} observedDeclaredNames must be sorted.`);
|
|
284
|
+
}
|
|
285
|
+
if (!isSorted(packageRecord.occurrences, compareOccurrences)) {
|
|
286
|
+
errors.push(`Package ${packageRecord.id} occurrences must be canonically sorted.`);
|
|
287
|
+
}
|
|
288
|
+
for (const occurrence of packageRecord.occurrences) {
|
|
289
|
+
checkOnlyKeys(errors, occurrence, new Set([
|
|
290
|
+
'projectId', 'projectPath', 'manifest', 'ecosystem', 'dependencyType', 'declaredName', 'normalizedName', 'declaredVersion'
|
|
291
|
+
]), 'Research Plan occurrence');
|
|
292
|
+
researchableOccurrenceCount += 1;
|
|
293
|
+
if (occurrence.ecosystem !== packageRecord.ecosystem) {
|
|
294
|
+
errors.push(`Package ${packageRecord.id} has an occurrence from a different ecosystem.`);
|
|
295
|
+
}
|
|
296
|
+
if (occurrence.normalizedName !== packageRecord.normalizedName) {
|
|
297
|
+
errors.push(`Package ${packageRecord.id} has an occurrence with a different normalized name.`);
|
|
298
|
+
}
|
|
299
|
+
if (!isPortableRelativePath(occurrence.projectPath) || !isPortableRelativePath(occurrence.manifest)) {
|
|
300
|
+
errors.push(`Package ${packageRecord.id} has a non-portable occurrence path.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const occurrence of invalidOccurrences) {
|
|
306
|
+
checkOnlyKeys(errors, occurrence, new Set([
|
|
307
|
+
'projectId', 'projectPath', 'manifest', 'ecosystem', 'dependencyType', 'declaredName', 'normalizedName', 'declaredVersion', 'reason'
|
|
308
|
+
]), 'Invalid occurrence');
|
|
309
|
+
if (!INVALID_REASONS.has(occurrence.reason)) errors.push(`Invalid occurrence has an unknown reason ${occurrence.reason}.`);
|
|
310
|
+
if (!SUPPORTED_ECOSYSTEMS.has(occurrence.ecosystem)) {
|
|
311
|
+
errors.push('Invalid occurrence must come from a supported ecosystem.');
|
|
312
|
+
}
|
|
313
|
+
if (!isPortableRelativePath(occurrence.projectPath) || !isPortableRelativePath(occurrence.manifest)) {
|
|
314
|
+
errors.push('Invalid occurrence has a non-portable path.');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let unsupportedOccurrenceCount = 0;
|
|
319
|
+
for (const record of unsupported) {
|
|
320
|
+
checkOnlyKeys(errors, record, new Set(['ecosystem', 'projectIds', 'occurrenceCount']), 'Unsupported record');
|
|
321
|
+
unsupportedOccurrenceCount += record.occurrenceCount;
|
|
322
|
+
if (SUPPORTED_ECOSYSTEMS.has(record.ecosystem)) {
|
|
323
|
+
errors.push(`Unsupported record ${record.ecosystem} is a supported ecosystem.`);
|
|
324
|
+
}
|
|
325
|
+
if (!isSorted(record.projectIds, compareText)) {
|
|
326
|
+
errors.push(`Unsupported record ${record.ecosystem} projectIds must be sorted.`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const invalidKeys = new Set(invalidOccurrences.map(occurrenceKey));
|
|
331
|
+
const unsupportedEcosystems = new Set(unsupported.map((record) => record.ecosystem));
|
|
332
|
+
for (const warning of warnings) {
|
|
333
|
+
checkOnlyKeys(errors, warning, warning.code === INVALID_WARNING
|
|
334
|
+
? new Set(['code', 'projectId', 'manifest', 'dependencyType', 'declaredName', 'declaredVersion', 'message'])
|
|
335
|
+
: new Set(['code', 'ecosystem', 'message']), 'Research Plan warning');
|
|
336
|
+
if (warning.code === INVALID_WARNING) {
|
|
337
|
+
const key = occurrenceKey(warning);
|
|
338
|
+
if (!invalidKeys.has(key)) errors.push('Invalid package warning does not reference an invalid occurrence.');
|
|
339
|
+
} else if (warning.code === UNSUPPORTED_WARNING) {
|
|
340
|
+
if (!unsupportedEcosystems.has(warning.ecosystem)) {
|
|
341
|
+
errors.push('Unsupported ecosystem warning does not reference an unsupported record.');
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
errors.push(`Unknown planning warning code ${warning.code}.`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const expectedWarningCount = invalidOccurrences.length + unsupported.length;
|
|
349
|
+
if (warnings.length !== expectedWarningCount) errors.push('warnings do not match invalid and unsupported planning outcomes.');
|
|
350
|
+
if (summary.uniqueResearchPackageCount !== packages.length) errors.push('summary uniqueResearchPackageCount is inconsistent.');
|
|
351
|
+
if (summary.researchableOccurrenceCount !== researchableOccurrenceCount) {
|
|
352
|
+
errors.push('summary researchableOccurrenceCount is inconsistent.');
|
|
353
|
+
}
|
|
354
|
+
if (summary.invalidOccurrenceCount !== invalidOccurrences.length) {
|
|
355
|
+
errors.push('summary invalidOccurrenceCount is inconsistent.');
|
|
356
|
+
}
|
|
357
|
+
if (summary.unsupportedOccurrenceCount !== unsupportedOccurrenceCount) {
|
|
358
|
+
errors.push('summary unsupportedOccurrenceCount is inconsistent.');
|
|
359
|
+
}
|
|
360
|
+
const plannedOccurrences = researchableOccurrenceCount + invalidOccurrences.length + unsupportedOccurrenceCount;
|
|
361
|
+
if (summary.inputOccurrenceCount !== plannedOccurrences) {
|
|
362
|
+
errors.push('summary inputOccurrenceCount is inconsistent.');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const value of JSON.stringify(plan).match(/"(?:[^"\\]|\\.)*"/g) ?? []) {
|
|
366
|
+
const decoded = JSON.parse(value);
|
|
367
|
+
if (hasCredentials(decoded)) errors.push('Research Plan must not contain URL credentials.');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (errors.length > 0) planError([...new Set(errors)].sort(compareText));
|
|
371
|
+
return plan;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Convert a validated Project Manifest input into a deterministic, internal
|
|
376
|
+
* Research Plan. Call loadProjectManifestInput before this function.
|
|
377
|
+
*/
|
|
378
|
+
export function createResearchPlan(loadedInput) {
|
|
379
|
+
if (!loadedInput?.manifest || !loadedInput?.input?.projectManifest) {
|
|
380
|
+
throw new Error('Research Plan requires a loaded Project Manifest input.');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const packageMap = new Map();
|
|
384
|
+
const invalidOccurrences = [];
|
|
385
|
+
const unsupportedMap = new Map();
|
|
386
|
+
const inputProjectCount = loadedInput.manifest.projects.length;
|
|
387
|
+
|
|
388
|
+
for (const project of loadedInput.manifest.projects) {
|
|
389
|
+
for (const dependency of project.dependencies) {
|
|
390
|
+
const occurrence = sanitizedOccurrence(project, dependency);
|
|
391
|
+
const result = eligibility(project, dependency);
|
|
392
|
+
|
|
393
|
+
if (result.outcome === 'unsupported') {
|
|
394
|
+
const record = unsupportedMap.get(project.ecosystem) ?? {
|
|
395
|
+
ecosystem: project.ecosystem,
|
|
396
|
+
projectIds: new Set(),
|
|
397
|
+
occurrenceCount: 0
|
|
398
|
+
};
|
|
399
|
+
record.projectIds.add(project.id);
|
|
400
|
+
record.occurrenceCount += 1;
|
|
401
|
+
unsupportedMap.set(project.ecosystem, record);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (result.outcome === 'invalid') {
|
|
406
|
+
invalidOccurrences.push({ ...occurrence, reason: result.reason });
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const key = `${result.registry}:${result.normalizedName}`;
|
|
411
|
+
const record = packageMap.get(key) ?? {
|
|
412
|
+
registry: result.registry,
|
|
413
|
+
ecosystem: project.ecosystem,
|
|
414
|
+
normalizedName: result.normalizedName,
|
|
415
|
+
observedDeclaredNames: new Set(),
|
|
416
|
+
occurrences: []
|
|
417
|
+
};
|
|
418
|
+
record.observedDeclaredNames.add(occurrence.declaredName);
|
|
419
|
+
record.occurrences.push({ ...occurrence, normalizedName: result.normalizedName });
|
|
420
|
+
packageMap.set(key, record);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const packages = packageRecords(packageMap);
|
|
425
|
+
const sortedInvalidOccurrences = invalidOccurrences.sort(compareInvalidOccurrences);
|
|
426
|
+
const unsupported = unsupportedRecords(unsupportedMap);
|
|
427
|
+
const warnings = [
|
|
428
|
+
...sortedInvalidOccurrences.map(invalidWarning),
|
|
429
|
+
...unsupported.map(unsupportedWarning)
|
|
430
|
+
].sort(compareWarnings);
|
|
431
|
+
const researchableOccurrenceCount = packages.reduce(
|
|
432
|
+
(count, packageRecord) => count + packageRecord.occurrences.length,
|
|
433
|
+
0
|
|
434
|
+
);
|
|
435
|
+
const unsupportedOccurrenceCount = unsupported.reduce((count, record) => count + record.occurrenceCount, 0);
|
|
436
|
+
|
|
437
|
+
const plan = {
|
|
438
|
+
planVersion: PLAN_VERSION,
|
|
439
|
+
input: structuredClone(loadedInput.input),
|
|
440
|
+
summary: {
|
|
441
|
+
inputProjectCount,
|
|
442
|
+
inputOccurrenceCount: researchableOccurrenceCount + sortedInvalidOccurrences.length + unsupportedOccurrenceCount,
|
|
443
|
+
researchableOccurrenceCount,
|
|
444
|
+
uniqueResearchPackageCount: packages.length,
|
|
445
|
+
invalidOccurrenceCount: sortedInvalidOccurrences.length,
|
|
446
|
+
unsupportedOccurrenceCount
|
|
447
|
+
},
|
|
448
|
+
packages,
|
|
449
|
+
invalidOccurrences: sortedInvalidOccurrences,
|
|
450
|
+
unsupported,
|
|
451
|
+
warnings
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return validateResearchPlan(plan);
|
|
455
|
+
}
|