@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
package/src/detectors.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
duplicateDependencyWarnings,
|
|
5
|
+
sortDependencies,
|
|
6
|
+
summarizeDependencies
|
|
7
|
+
} from './dependencies.js';
|
|
8
|
+
import { readJson, readText, relativePath } from './files.js';
|
|
9
|
+
import { parseRequirementsTxt } from './python-requirements.js';
|
|
10
|
+
|
|
11
|
+
const DEFINITIONS = [
|
|
12
|
+
{ ecosystem: 'node', languages: ['JavaScript', 'TypeScript'], names: ['package.json'] },
|
|
13
|
+
{ ecosystem: 'python', languages: ['Python'], names: ['pyproject.toml', 'requirements.txt'] },
|
|
14
|
+
{ ecosystem: 'java', languages: ['Java', 'Kotlin'], names: ['pom.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts'] },
|
|
15
|
+
{ ecosystem: 'dotnet', languages: ['C#', 'F#', 'Visual Basic'], extensions: ['.csproj', '.fsproj', '.vbproj', '.sln'] },
|
|
16
|
+
{ ecosystem: 'go', languages: ['Go'], names: ['go.mod'] },
|
|
17
|
+
{ ecosystem: 'rust', languages: ['Rust'], names: ['Cargo.toml'] },
|
|
18
|
+
{ ecosystem: 'ruby', languages: ['Ruby'], names: ['Gemfile'] },
|
|
19
|
+
{ ecosystem: 'php', languages: ['PHP'], names: ['composer.json'] },
|
|
20
|
+
{ ecosystem: 'al', languages: ['AL'], names: ['app.json'] }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function definitionForFile(filePath) {
|
|
24
|
+
const name = path.basename(filePath);
|
|
25
|
+
return DEFINITIONS.find((definition) =>
|
|
26
|
+
definition.names?.includes(name) || definition.extensions?.some((extension) => name.endsWith(extension))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function firstMatch(text, expressions) {
|
|
31
|
+
for (const expression of expressions) {
|
|
32
|
+
const value = text.match(expression)?.[1]?.trim();
|
|
33
|
+
if (value) return value;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function packageManagerFromNode(data, fileNames) {
|
|
39
|
+
if (typeof data.packageManager === 'string') {
|
|
40
|
+
const [name, ...versionParts] = data.packageManager.split('@');
|
|
41
|
+
return { name, ...(versionParts.length ? { version: versionParts.join('@') } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (fileNames.has('pnpm-lock.yaml')) return { name: 'pnpm' };
|
|
44
|
+
if (fileNames.has('yarn.lock')) return { name: 'yarn' };
|
|
45
|
+
if (fileNames.has('bun.lock') || fileNames.has('bun.lockb')) return { name: 'bun' };
|
|
46
|
+
if (fileNames.has('package-lock.json') || fileNames.has('npm-shrinkwrap.json')) return { name: 'npm' };
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const NODE_DEPENDENCY_SECTIONS = [
|
|
51
|
+
['dependencies', 'dependency'],
|
|
52
|
+
['devDependencies', 'devDependency'],
|
|
53
|
+
['peerDependencies', 'peerDependency'],
|
|
54
|
+
['optionalDependencies', 'optionalDependency']
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function parseNodeDependencies(data, manifest) {
|
|
58
|
+
const dependencies = [];
|
|
59
|
+
const byType = {
|
|
60
|
+
dependencies: 0,
|
|
61
|
+
devDependencies: 0,
|
|
62
|
+
peerDependencies: 0,
|
|
63
|
+
optionalDependencies: 0
|
|
64
|
+
};
|
|
65
|
+
let invalid = false;
|
|
66
|
+
|
|
67
|
+
for (const [section, type] of NODE_DEPENDENCY_SECTIONS) {
|
|
68
|
+
const declarations = data[section];
|
|
69
|
+
if (declarations === undefined) continue;
|
|
70
|
+
if (!declarations || typeof declarations !== 'object' || Array.isArray(declarations)) {
|
|
71
|
+
invalid = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const [name, declaredVersion] of Object.entries(declarations)) {
|
|
75
|
+
if (typeof declaredVersion !== 'string') {
|
|
76
|
+
invalid = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
byType[section] += 1;
|
|
80
|
+
dependencies.push({
|
|
81
|
+
name,
|
|
82
|
+
normalizedName: name.toLowerCase(),
|
|
83
|
+
declaredVersion,
|
|
84
|
+
type,
|
|
85
|
+
manifest
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (invalid) {
|
|
91
|
+
return {
|
|
92
|
+
dependencySummary: { status: 'failed' },
|
|
93
|
+
dependencies: [],
|
|
94
|
+
warnings: [{
|
|
95
|
+
code: 'DEPENDENCY_PARSE_FAILED',
|
|
96
|
+
path: manifest,
|
|
97
|
+
message: 'Unable to parse dependency declarations in package.json.'
|
|
98
|
+
}]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sortedDependencies = sortDependencies(dependencies);
|
|
103
|
+
return {
|
|
104
|
+
dependencySummary: summarizeDependencies(sortedDependencies, byType),
|
|
105
|
+
dependencies: sortedDependencies,
|
|
106
|
+
warnings: duplicateDependencyWarnings(sortedDependencies, manifest)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function nodeMetadata(root, files, directoryEntries) {
|
|
111
|
+
const packageFile = files.find((file) => path.basename(file) === 'package.json');
|
|
112
|
+
if (!packageFile) return {};
|
|
113
|
+
const data = await readJson(packageFile);
|
|
114
|
+
const workspaces = Array.isArray(data.workspaces) ? data.workspaces : data.workspaces?.packages;
|
|
115
|
+
const dependencyMetadata = parseNodeDependencies(data, relativePath(root, packageFile));
|
|
116
|
+
return {
|
|
117
|
+
name: typeof data.name === 'string' ? data.name : undefined,
|
|
118
|
+
version: typeof data.version === 'string' ? data.version : undefined,
|
|
119
|
+
private: typeof data.private === 'boolean' ? data.private : undefined,
|
|
120
|
+
packageManager: packageManagerFromNode(data, directoryEntries),
|
|
121
|
+
...dependencyMetadata,
|
|
122
|
+
workspacePatterns: Array.isArray(workspaces) ? workspaces.filter((item) => typeof item === 'string') : []
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function pythonMetadata(root, files) {
|
|
127
|
+
const fallbackName = path.basename(path.dirname(files[0]));
|
|
128
|
+
const pyprojectFile = files.find((file) => path.basename(file) === 'pyproject.toml');
|
|
129
|
+
const requirementsFile = files.find((file) => path.basename(file) === 'requirements.txt');
|
|
130
|
+
const warnings = [];
|
|
131
|
+
let name = fallbackName;
|
|
132
|
+
|
|
133
|
+
if (pyprojectFile) {
|
|
134
|
+
try {
|
|
135
|
+
const contents = await readText(pyprojectFile);
|
|
136
|
+
name = firstMatch(contents, [/^\s*name\s*=\s*["']([^"']+)["']/m]) ?? fallbackName;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
warnings.push({
|
|
139
|
+
code: 'MANIFEST_UNREADABLE',
|
|
140
|
+
path: relativePath(root, pyprojectFile),
|
|
141
|
+
message: `Unable to read manifest (${error.code ?? 'unknown error'})`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!requirementsFile) {
|
|
147
|
+
return { name, dependencySummary: { status: 'unsupported' }, dependencies: [], warnings };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const parsed = parseRequirementsTxt(await readText(requirementsFile));
|
|
152
|
+
if (parsed.issues.length > 0) {
|
|
153
|
+
const lines = parsed.issues.map((issue) => issue.line).join(', ');
|
|
154
|
+
warnings.push({
|
|
155
|
+
code: 'DEPENDENCY_PARSE_FAILED',
|
|
156
|
+
path: relativePath(root, requirementsFile),
|
|
157
|
+
message: `Unable to parse ${parsed.issues.length} requirement line(s): ${lines}`
|
|
158
|
+
});
|
|
159
|
+
return { name, dependencySummary: { status: 'failed' }, dependencies: [], warnings };
|
|
160
|
+
}
|
|
161
|
+
const manifest = relativePath(root, requirementsFile);
|
|
162
|
+
const dependencies = sortDependencies(parsed.dependencies.map((dependencyRecord) => ({
|
|
163
|
+
...dependencyRecord,
|
|
164
|
+
manifest
|
|
165
|
+
})));
|
|
166
|
+
warnings.push(...duplicateDependencyWarnings(dependencies, manifest));
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
dependencySummary: summarizeDependencies(dependencies),
|
|
170
|
+
dependencies,
|
|
171
|
+
warnings
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
warnings.push({
|
|
175
|
+
code: 'DEPENDENCY_PARSE_FAILED',
|
|
176
|
+
path: relativePath(root, requirementsFile),
|
|
177
|
+
message: `Unable to read or parse requirements (${error.code ?? 'unknown error'})`
|
|
178
|
+
});
|
|
179
|
+
return { name, dependencySummary: { status: 'failed' }, dependencies: [], warnings };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function alMetadata(files) {
|
|
184
|
+
const data = await readJson(files[0]);
|
|
185
|
+
const looksLikeAl = typeof data.id === 'string' && typeof data.name === 'string' && typeof data.publisher === 'string';
|
|
186
|
+
if (!looksLikeAl) return { skip: true };
|
|
187
|
+
return {
|
|
188
|
+
name: data.name,
|
|
189
|
+
version: typeof data.version === 'string' ? data.version : undefined
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function jsonMetadata(files, ecosystem) {
|
|
194
|
+
const data = await readJson(files[0]);
|
|
195
|
+
if (ecosystem === 'php') {
|
|
196
|
+
return {
|
|
197
|
+
name: typeof data.name === 'string' ? data.name : undefined,
|
|
198
|
+
version: typeof data.version === 'string' ? data.version : undefined
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function textMetadata(files, ecosystem) {
|
|
205
|
+
const contents = await Promise.all(files.map(readText));
|
|
206
|
+
const text = contents.join('\n');
|
|
207
|
+
const fallbackName = path.basename(path.dirname(files[0]));
|
|
208
|
+
|
|
209
|
+
switch (ecosystem) {
|
|
210
|
+
case 'java':
|
|
211
|
+
return { name: firstMatch(text, [/<artifactId>([^<]+)<\/artifactId>/, /rootProject\.name\s*=\s*["']([^"']+)["']/]) ?? fallbackName };
|
|
212
|
+
case 'dotnet':
|
|
213
|
+
return { name: firstMatch(text, [/<AssemblyName>([^<]+)<\/AssemblyName>/, /<RootNamespace>([^<]+)<\/RootNamespace>/]) ?? path.parse(files[0]).name };
|
|
214
|
+
case 'go':
|
|
215
|
+
return { name: firstMatch(text, [/^module\s+(.+)$/m]) ?? fallbackName };
|
|
216
|
+
case 'rust':
|
|
217
|
+
return { name: firstMatch(text, [/^\s*name\s*=\s*["']([^"']+)["']/m]), version: firstMatch(text, [/^\s*version\s*=\s*["']([^"']+)["']/m]) };
|
|
218
|
+
default:
|
|
219
|
+
return { name: fallbackName };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function inspectProjectGroup(root, group, directoryEntries) {
|
|
224
|
+
const { definition, directory, files } = group;
|
|
225
|
+
let metadata;
|
|
226
|
+
|
|
227
|
+
if (definition.ecosystem === 'node') metadata = await nodeMetadata(root, files, directoryEntries);
|
|
228
|
+
else if (definition.ecosystem === 'python') metadata = await pythonMetadata(root, files);
|
|
229
|
+
else if (definition.ecosystem === 'al') metadata = await alMetadata(files);
|
|
230
|
+
else if (definition.ecosystem === 'php') metadata = await jsonMetadata(files, definition.ecosystem);
|
|
231
|
+
else metadata = await textMetadata(files, definition.ecosystem);
|
|
232
|
+
|
|
233
|
+
if (metadata.skip) return undefined;
|
|
234
|
+
const projectPath = relativePath(root, directory);
|
|
235
|
+
const manifests = files.map((file) => relativePath(root, file)).sort();
|
|
236
|
+
const name = metadata.name || path.basename(directory);
|
|
237
|
+
|
|
238
|
+
const project = {
|
|
239
|
+
id: `${definition.ecosystem}:${projectPath}`,
|
|
240
|
+
name,
|
|
241
|
+
path: projectPath,
|
|
242
|
+
ecosystem: definition.ecosystem,
|
|
243
|
+
languages: definition.languages,
|
|
244
|
+
manifests,
|
|
245
|
+
...(metadata.version ? { version: metadata.version } : {}),
|
|
246
|
+
...(metadata.private !== undefined ? { private: metadata.private } : {}),
|
|
247
|
+
...(metadata.packageManager ? { packageManager: metadata.packageManager } : {}),
|
|
248
|
+
dependencySummary: metadata.dependencySummary ?? { status: 'unsupported' },
|
|
249
|
+
dependencies: metadata.dependencies ?? [],
|
|
250
|
+
_workspacePatterns: metadata.workspacePatterns ?? []
|
|
251
|
+
};
|
|
252
|
+
return { project, warnings: metadata.warnings ?? [] };
|
|
253
|
+
}
|
package/src/discovery.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MANIFEST_SCHEMA_VERSION,
|
|
6
|
+
ARTIFACT_GENERATOR_NAME,
|
|
7
|
+
VERSION
|
|
8
|
+
} from './constants.js';
|
|
9
|
+
import { createCooperativeScheduler } from './cooperative-scheduler.js';
|
|
10
|
+
import { definitionForFile, inspectProjectGroup } from './detectors.js';
|
|
11
|
+
import { collectCandidateFiles, relativePath } from './files.js';
|
|
12
|
+
import { attachInstalledVersionBaselines } from './installed-version-baseline.js';
|
|
13
|
+
|
|
14
|
+
async function assertDirectory(inputPath) {
|
|
15
|
+
const absolutePath = path.resolve(inputPath);
|
|
16
|
+
let details;
|
|
17
|
+
try {
|
|
18
|
+
details = await stat(absolutePath);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new Error(`Cannot access project root "${inputPath}": ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
if (!details.isDirectory()) throw new Error(`Project root is not a directory: ${inputPath}`);
|
|
23
|
+
return realpath(absolutePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function directoryEntryNames(directory) {
|
|
27
|
+
try {
|
|
28
|
+
return new Set(await readdir(directory));
|
|
29
|
+
} catch {
|
|
30
|
+
return new Set();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function groupCandidateFiles(files) {
|
|
35
|
+
const groups = new Map();
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const definition = definitionForFile(file);
|
|
38
|
+
if (!definition) continue;
|
|
39
|
+
const directory = path.dirname(file);
|
|
40
|
+
const key = `${definition.ecosystem}\0${directory}`;
|
|
41
|
+
if (!groups.has(key)) groups.set(key, { definition, directory, files: [] });
|
|
42
|
+
groups.get(key).files.push(file);
|
|
43
|
+
}
|
|
44
|
+
return [...groups.values()].map((group) => ({ ...group, files: group.files.sort() }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function globToRegExp(pattern) {
|
|
48
|
+
const normalized = pattern.replace(/^\.\//, '').replace(/\/$/, '');
|
|
49
|
+
let expression = '^';
|
|
50
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
51
|
+
const character = normalized[index];
|
|
52
|
+
if (character === '*' && normalized[index + 1] === '*') {
|
|
53
|
+
expression += '.*';
|
|
54
|
+
index += 1;
|
|
55
|
+
} else if (character === '*') expression += '[^/]*';
|
|
56
|
+
else if (character === '?') expression += '[^/]';
|
|
57
|
+
else expression += character.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
58
|
+
}
|
|
59
|
+
return new RegExp(`${expression}$`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function matchesWorkspace(patterns, projectPath) {
|
|
63
|
+
const positive = patterns.filter((pattern) => !pattern.startsWith('!'));
|
|
64
|
+
const negative = patterns.filter((pattern) => pattern.startsWith('!')).map((pattern) => pattern.slice(1));
|
|
65
|
+
return positive.some((pattern) => globToRegExp(pattern).test(projectPath))
|
|
66
|
+
&& !negative.some((pattern) => globToRegExp(pattern).test(projectPath));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parsePnpmWorkspace(contents) {
|
|
70
|
+
const patterns = [];
|
|
71
|
+
let inPackages = false;
|
|
72
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
73
|
+
if (/^packages:\s*(?:#.*)?$/.test(line)) {
|
|
74
|
+
inPackages = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (inPackages && /^\S/.test(line)) break;
|
|
78
|
+
if (!inPackages) continue;
|
|
79
|
+
const pattern = line.match(/^\s*-\s*(['"]?)(.+?)\1\s*(?:#.*)?$/)?.[2]?.trim();
|
|
80
|
+
if (pattern) patterns.push(pattern);
|
|
81
|
+
}
|
|
82
|
+
return patterns;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function addPnpmWorkspacePatterns(root, files, projects, warnings, cooperativeScheduler) {
|
|
86
|
+
for (const file of files.filter((candidate) => path.basename(candidate) === 'pnpm-workspace.yaml')) {
|
|
87
|
+
try {
|
|
88
|
+
const projectPath = relativePath(root, path.dirname(file));
|
|
89
|
+
const project = projects.find((candidate) => candidate.ecosystem === 'node' && candidate.path === projectPath);
|
|
90
|
+
if (!project) continue;
|
|
91
|
+
try {
|
|
92
|
+
const patterns = parsePnpmWorkspace(await readFile(file, 'utf8'));
|
|
93
|
+
project._workspacePatterns = [...new Set([...project._workspacePatterns, ...patterns])];
|
|
94
|
+
} catch (error) {
|
|
95
|
+
warnings.push({
|
|
96
|
+
code: 'MANIFEST_UNREADABLE',
|
|
97
|
+
path: relativePath(root, file),
|
|
98
|
+
message: `Unable to read workspace manifest (${error.code ?? 'unknown error'})`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
await cooperativeScheduler.boundary();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function addWorkspaceRelationships(projects, cooperativeScheduler) {
|
|
108
|
+
const roots = projects
|
|
109
|
+
.filter((project) => project.ecosystem === 'node' && project._workspacePatterns.some((pattern) => !pattern.startsWith('!')))
|
|
110
|
+
.sort((left, right) => right.path.length - left.path.length);
|
|
111
|
+
|
|
112
|
+
for (const project of projects) {
|
|
113
|
+
try {
|
|
114
|
+
if (project.ecosystem !== 'node') continue;
|
|
115
|
+
const workspaceRoot = roots.find((candidate) => {
|
|
116
|
+
if (candidate.id === project.id) return false;
|
|
117
|
+
const relative = path.posix.relative(candidate.path === '.' ? '' : candidate.path, project.path);
|
|
118
|
+
return matchesWorkspace(candidate._workspacePatterns, relative);
|
|
119
|
+
});
|
|
120
|
+
if (workspaceRoot) {
|
|
121
|
+
project.workspace = { root: workspaceRoot.path, role: 'member' };
|
|
122
|
+
if (!project.packageManager && workspaceRoot.packageManager) {
|
|
123
|
+
project.packageManager = { ...workspaceRoot.packageManager };
|
|
124
|
+
}
|
|
125
|
+
} else if (project._workspacePatterns.some((pattern) => !pattern.startsWith('!'))) {
|
|
126
|
+
project.workspace = { root: project.path, role: 'root' };
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
await cooperativeScheduler.boundary();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const project of projects) {
|
|
134
|
+
delete project._workspacePatterns;
|
|
135
|
+
await cooperativeScheduler.boundary();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function gitMetadata(root) {
|
|
140
|
+
const gitPath = path.join(root, '.git');
|
|
141
|
+
let gitDirectory = gitPath;
|
|
142
|
+
try {
|
|
143
|
+
const details = await stat(gitPath);
|
|
144
|
+
if (details.isFile()) {
|
|
145
|
+
const pointer = await readFile(gitPath, 'utf8');
|
|
146
|
+
const target = pointer.match(/^gitdir:\s*(.+)$/m)?.[1];
|
|
147
|
+
if (!target) return undefined;
|
|
148
|
+
gitDirectory = path.resolve(root, target);
|
|
149
|
+
}
|
|
150
|
+
const head = (await readFile(path.join(gitDirectory, 'HEAD'), 'utf8')).trim();
|
|
151
|
+
const branch = head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : undefined;
|
|
152
|
+
return { type: 'git', ...(branch ? { branch } : {}) };
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function summarize(projects) {
|
|
159
|
+
const ecosystems = {};
|
|
160
|
+
for (const project of projects) ecosystems[project.ecosystem] = (ecosystems[project.ecosystem] ?? 0) + 1;
|
|
161
|
+
return {
|
|
162
|
+
projectCount: projects.length,
|
|
163
|
+
ecosystems: Object.fromEntries(Object.entries(ecosystems).sort(([left], [right]) => left.localeCompare(right))),
|
|
164
|
+
workspaceCount: projects.filter((project) => project.workspace?.role === 'root').length
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function warningFor(error, root, group) {
|
|
169
|
+
const firstFile = group.files[0];
|
|
170
|
+
const isSyntax = error instanceof SyntaxError;
|
|
171
|
+
return {
|
|
172
|
+
code: isSyntax ? 'MANIFEST_INVALID' : 'MANIFEST_UNREADABLE',
|
|
173
|
+
path: relativePath(root, firstFile),
|
|
174
|
+
message: isSyntax ? error.message : `Unable to read manifest (${error.code ?? 'unknown error'})`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function discoverProject(inputPath = '.', options = {}) {
|
|
179
|
+
const cooperativeScheduler = options.cooperativeScheduler ?? createCooperativeScheduler({
|
|
180
|
+
signal: options.signal,
|
|
181
|
+
enabled: options.cooperativeScheduling !== false,
|
|
182
|
+
batchSize: options.cooperativeBatchSize,
|
|
183
|
+
maxIntervalMs: options.cooperativeMaxIntervalMs,
|
|
184
|
+
monotonicClock: options.monotonicClock
|
|
185
|
+
});
|
|
186
|
+
cooperativeScheduler.checkpoint();
|
|
187
|
+
const root = await assertDirectory(inputPath);
|
|
188
|
+
const { files, warnings } = await collectCandidateFiles(root, {
|
|
189
|
+
maxDepth: options.maxDepth,
|
|
190
|
+
cooperativeScheduler
|
|
191
|
+
});
|
|
192
|
+
const projects = [];
|
|
193
|
+
|
|
194
|
+
for (const group of groupCandidateFiles(files)) {
|
|
195
|
+
try {
|
|
196
|
+
const entries = await directoryEntryNames(group.directory);
|
|
197
|
+
const result = await inspectProjectGroup(root, group, entries);
|
|
198
|
+
if (result) {
|
|
199
|
+
projects.push(result.project);
|
|
200
|
+
warnings.push(...result.warnings);
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
warnings.push(warningFor(error, root, group));
|
|
204
|
+
} finally {
|
|
205
|
+
await cooperativeScheduler.boundary();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
projects.sort((left, right) => left.id.localeCompare(right.id));
|
|
210
|
+
await addPnpmWorkspacePatterns(root, files, projects, warnings, cooperativeScheduler);
|
|
211
|
+
await addWorkspaceRelationships(projects, cooperativeScheduler);
|
|
212
|
+
await attachInstalledVersionBaselines(root, projects, {
|
|
213
|
+
...options,
|
|
214
|
+
cooperativeScheduler
|
|
215
|
+
});
|
|
216
|
+
warnings.sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code));
|
|
217
|
+
|
|
218
|
+
cooperativeScheduler.checkpoint();
|
|
219
|
+
const now = options.clock?.() ?? new Date();
|
|
220
|
+
const vcs = await gitMetadata(root);
|
|
221
|
+
return {
|
|
222
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
223
|
+
generatedAt: now.toISOString(),
|
|
224
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
225
|
+
repository: {
|
|
226
|
+
name: path.basename(root),
|
|
227
|
+
root: '.',
|
|
228
|
+
...(vcs ? { vcs } : {})
|
|
229
|
+
},
|
|
230
|
+
summary: summarize(projects),
|
|
231
|
+
projects,
|
|
232
|
+
warnings
|
|
233
|
+
};
|
|
234
|
+
}
|