@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,158 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
|
|
8
|
+
import { canonicalJsonBytes } from './canonical-json.js';
|
|
9
|
+
import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
|
|
10
|
+
import { compareText } from './portable.js';
|
|
11
|
+
import { RUNTIME_CONFORMANCE_CAPABILITIES } from './runtime-conformance.js';
|
|
12
|
+
|
|
13
|
+
export const CONFORMANCE_REPORT_SCHEMA_VERSION = '1.0.0';
|
|
14
|
+
|
|
15
|
+
const schema = JSON.parse(await readFile(
|
|
16
|
+
new URL('../schemas/conformance-report.schema.json', import.meta.url),
|
|
17
|
+
'utf8'
|
|
18
|
+
));
|
|
19
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
20
|
+
addFormats(ajv);
|
|
21
|
+
const validateSchema = ajv.compile(schema);
|
|
22
|
+
|
|
23
|
+
function capabilitySummary(caseResults) {
|
|
24
|
+
return Object.fromEntries(RUNTIME_CONFORMANCE_CAPABILITIES.map((capability) => {
|
|
25
|
+
const relevant = caseResults.filter((result) => result.capabilities.includes(capability));
|
|
26
|
+
if (relevant.some((result) => result.status === 'FAIL')) return [capability, 'FAIL'];
|
|
27
|
+
if (relevant.length > 0 && relevant.every((result) => result.capabilityStatus === 'NOT_SUPPORTED')) {
|
|
28
|
+
return [capability, 'NOT_SUPPORTED'];
|
|
29
|
+
}
|
|
30
|
+
return [capability, 'PASS'];
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recommendationFor(caseResults) {
|
|
35
|
+
if (caseResults.some((result) => result.required && result.status === 'FAIL')) return 'NON_CONFORMANT';
|
|
36
|
+
if (caseResults.some((result) => result.status === 'FAIL' || result.status === 'SKIP')) {
|
|
37
|
+
return 'PARTIALLY_CONFORMANT';
|
|
38
|
+
}
|
|
39
|
+
return 'CONFORMANT';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildConformanceReport({
|
|
43
|
+
runtime,
|
|
44
|
+
caseResults,
|
|
45
|
+
deploymentProfileDigest,
|
|
46
|
+
capabilityProfileDigest,
|
|
47
|
+
conformanceScope = 'offline-runtime-protocol',
|
|
48
|
+
generatedAt = new Date()
|
|
49
|
+
}) {
|
|
50
|
+
const sortedResults = [...caseResults].sort((left, right) => compareText(left.id, right.id));
|
|
51
|
+
const cases = sortedResults.map((result) => ({
|
|
52
|
+
id: result.id,
|
|
53
|
+
capability: result.capability,
|
|
54
|
+
required: result.required,
|
|
55
|
+
status: result.status,
|
|
56
|
+
expected: result.expected,
|
|
57
|
+
observed: result.observed
|
|
58
|
+
}));
|
|
59
|
+
const failedResults = sortedResults.filter((result) => result.status === 'FAIL');
|
|
60
|
+
const report = {
|
|
61
|
+
schemaVersion: CONFORMANCE_REPORT_SCHEMA_VERSION,
|
|
62
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
63
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
64
|
+
suite: {
|
|
65
|
+
mode: 'offlineFixture',
|
|
66
|
+
protocol: 'openai-compatible-chat-completions',
|
|
67
|
+
scope: conformanceScope,
|
|
68
|
+
caseCount: cases.length
|
|
69
|
+
},
|
|
70
|
+
runtime: {
|
|
71
|
+
provider: runtime.provider,
|
|
72
|
+
model: runtime.model
|
|
73
|
+
},
|
|
74
|
+
deploymentProfileDigest,
|
|
75
|
+
capabilityProfileDigest,
|
|
76
|
+
conformanceScope,
|
|
77
|
+
summary: {
|
|
78
|
+
total: cases.length,
|
|
79
|
+
passed: cases.filter((result) => result.status === 'PASS').length,
|
|
80
|
+
failed: failedResults.length,
|
|
81
|
+
skipped: cases.filter((result) => result.status === 'SKIP').length
|
|
82
|
+
},
|
|
83
|
+
capabilities: capabilitySummary(sortedResults),
|
|
84
|
+
cases,
|
|
85
|
+
failures: failedResults.map((result) => ({
|
|
86
|
+
caseId: result.id,
|
|
87
|
+
code: result.observed,
|
|
88
|
+
message: 'Observed runtime behavior did not match the conformance contract.'
|
|
89
|
+
})),
|
|
90
|
+
recommendation: recommendationFor(sortedResults)
|
|
91
|
+
};
|
|
92
|
+
return validateConformanceReport(report);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateConformanceReport(report) {
|
|
96
|
+
if (report?.schemaVersion !== CONFORMANCE_REPORT_SCHEMA_VERSION) {
|
|
97
|
+
throw new Error(`Conformance Report validation error: unsupported schema version; expected ${CONFORMANCE_REPORT_SCHEMA_VERSION}.`);
|
|
98
|
+
}
|
|
99
|
+
if (!validateSchema(report)) {
|
|
100
|
+
throw new Error(`Conformance Report validation error: schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
101
|
+
}
|
|
102
|
+
if (report.suite.caseCount !== report.cases.length || report.summary.total !== report.cases.length) {
|
|
103
|
+
throw new Error('Conformance Report validation error: case totals do not match the case list.');
|
|
104
|
+
}
|
|
105
|
+
if (report.suite.scope !== report.conformanceScope) {
|
|
106
|
+
throw new Error('Conformance Report validation error: suite scope does not match conformanceScope.');
|
|
107
|
+
}
|
|
108
|
+
const counted = report.summary.passed + report.summary.failed + report.summary.skipped;
|
|
109
|
+
if (counted !== report.summary.total) {
|
|
110
|
+
throw new Error('Conformance Report validation error: summary counts do not add up to the total.');
|
|
111
|
+
}
|
|
112
|
+
const caseIds = new Set();
|
|
113
|
+
for (const result of report.cases) {
|
|
114
|
+
if (caseIds.has(result.id)) throw new Error(`Conformance Report validation error: duplicate case ${result.id}.`);
|
|
115
|
+
caseIds.add(result.id);
|
|
116
|
+
}
|
|
117
|
+
for (const failure of report.failures) {
|
|
118
|
+
const result = report.cases.find((entry) => entry.id === failure.caseId);
|
|
119
|
+
if (!result || result.status !== 'FAIL') {
|
|
120
|
+
throw new Error(`Conformance Report validation error: failure references non-failed case ${failure.caseId}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (report.failures.length !== report.summary.failed) {
|
|
124
|
+
throw new Error('Conformance Report validation error: failure count does not match the summary.');
|
|
125
|
+
}
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function serializeConformanceReport(report) {
|
|
130
|
+
validateConformanceReport(report);
|
|
131
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function conformanceReportDigest(report) {
|
|
135
|
+
validateConformanceReport(report);
|
|
136
|
+
return `sha256:${createHash('sha256').update(canonicalJsonBytes(report)).digest('hex')}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function writeConformanceReport(outputPath, report) {
|
|
140
|
+
const target = path.resolve(outputPath);
|
|
141
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
142
|
+
const contents = serializeConformanceReport(report);
|
|
143
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
144
|
+
let handle;
|
|
145
|
+
try {
|
|
146
|
+
handle = await open(temporary, 'w', 0o600);
|
|
147
|
+
await handle.writeFile(contents, 'utf8');
|
|
148
|
+
await handle.sync();
|
|
149
|
+
await handle.close();
|
|
150
|
+
handle = null;
|
|
151
|
+
await rename(temporary, target);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await handle?.close().catch(() => {});
|
|
154
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return target;
|
|
158
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { buildConformanceReport } from './conformance-report.js';
|
|
2
|
+
import { createOpenAiCompatibleProvider } from './openai-compatible-provider.js';
|
|
3
|
+
import {
|
|
4
|
+
OFFLINE_CONFORMANCE_ENDPOINT,
|
|
5
|
+
capabilityProfileDigest as digestCapabilityProfile,
|
|
6
|
+
createDefaultGovernanceArtifacts,
|
|
7
|
+
deploymentProfileDigest as digestDeploymentProfile
|
|
8
|
+
} from './governance-metadata.js';
|
|
9
|
+
import {
|
|
10
|
+
RUNTIME_CONFORMANCE_CASES,
|
|
11
|
+
RUNTIME_CONFORMANCE_OUTPUT_SCHEMA,
|
|
12
|
+
RuntimeConformanceValidationError,
|
|
13
|
+
normalizeConformanceError,
|
|
14
|
+
validateRuntimeConformanceOutput
|
|
15
|
+
} from './runtime-conformance.js';
|
|
16
|
+
|
|
17
|
+
const FIXTURE_ENDPOINT = OFFLINE_CONFORMANCE_ENDPOINT;
|
|
18
|
+
const FIXTURE_USAGE = Object.freeze({
|
|
19
|
+
prompt_tokens: 17,
|
|
20
|
+
completion_tokens: 9,
|
|
21
|
+
total_tokens: 26
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function fixtureSuccessBody(model, transport) {
|
|
25
|
+
const content = transport.content
|
|
26
|
+
?? JSON.stringify({ status: 'ok', protocol: 'chat-completions' });
|
|
27
|
+
const choice = {
|
|
28
|
+
index: 0,
|
|
29
|
+
message: { role: 'assistant', content },
|
|
30
|
+
finish_reason: 'stop'
|
|
31
|
+
};
|
|
32
|
+
const body = {
|
|
33
|
+
id: 'offline-conformance-response',
|
|
34
|
+
model: transport.returnedModel ?? model,
|
|
35
|
+
usage: { ...FIXTURE_USAGE }
|
|
36
|
+
};
|
|
37
|
+
if (!transport.omitChoices) {
|
|
38
|
+
body.choices = transport.choiceCount === 2
|
|
39
|
+
? [choice, { ...choice, index: 1 }]
|
|
40
|
+
: [choice];
|
|
41
|
+
}
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function jsonResponse(body, { status = 200, contentType = 'application/json' } = {}) {
|
|
46
|
+
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
|
|
47
|
+
status,
|
|
48
|
+
headers: { 'content-type': contentType }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function waitForAbort(signal) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const rejectAbort = () => reject(new DOMException('Fixture request aborted.', 'AbortError'));
|
|
55
|
+
if (signal.aborted) rejectAbort();
|
|
56
|
+
else signal.addEventListener('abort', rejectAbort, { once: true });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createFixtureFetch(caseDefinition, runtime, observations) {
|
|
61
|
+
return async (_url, init) => {
|
|
62
|
+
observations.authorization = init.headers.authorization ?? null;
|
|
63
|
+
observations.redirect = init.redirect;
|
|
64
|
+
observations.requestBody = JSON.parse(init.body);
|
|
65
|
+
const transport = caseDefinition.transport;
|
|
66
|
+
if (transport.type === 'networkError') throw new TypeError('Offline fixture network failure.');
|
|
67
|
+
if (transport.type === 'waitForAbort') return waitForAbort(init.signal);
|
|
68
|
+
if (transport.type === 'redirect') throw new TypeError('Offline fixture redirect rejected.');
|
|
69
|
+
if (transport.type === 'malformedJson') return jsonResponse('{malformed');
|
|
70
|
+
if (transport.type === 'oversized') {
|
|
71
|
+
return new Response('x'.repeat(512), {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: {
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
'content-length': '512'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (transport.type === 'httpError') {
|
|
80
|
+
return jsonResponse({
|
|
81
|
+
error: {
|
|
82
|
+
message: transport.descriptor ?? `offline fixture HTTP ${transport.status}`
|
|
83
|
+
}
|
|
84
|
+
}, { status: transport.status });
|
|
85
|
+
}
|
|
86
|
+
return jsonResponse(fixtureSuccessBody(runtime.model, transport), {
|
|
87
|
+
contentType: transport.contentType
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runtimeRequest(caseDefinition, signal) {
|
|
93
|
+
return {
|
|
94
|
+
contractVersion: '1.0.0',
|
|
95
|
+
runId: `conformance:${caseDefinition.id}`,
|
|
96
|
+
contextId: 'offline-runtime-conformance',
|
|
97
|
+
task: 'runtime-conformance.v1',
|
|
98
|
+
promptVersion: '1.0.0',
|
|
99
|
+
systemPrompt: 'Return the fixed runtime conformance object.',
|
|
100
|
+
userPrompt: 'Return status ok and protocol chat-completions.',
|
|
101
|
+
structuredOutput: {
|
|
102
|
+
mode: caseDefinition.mode,
|
|
103
|
+
name: 'upgradelens_runtime_conformance',
|
|
104
|
+
schema: RUNTIME_CONFORMANCE_OUTPUT_SCHEMA
|
|
105
|
+
},
|
|
106
|
+
...(signal ? { signal } : {})
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertionError(code, message) {
|
|
111
|
+
throw new RuntimeConformanceValidationError(code, message);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateAssertions(caseDefinition, result, observations, runtime) {
|
|
115
|
+
for (const assertion of caseDefinition.assertions) {
|
|
116
|
+
if (assertion === 'outputSchema') validateRuntimeConformanceOutput(result.output);
|
|
117
|
+
else if (assertion === 'noAuthorization' && observations.authorization !== null) {
|
|
118
|
+
assertionError('AUTH_MAPPING_INVALID', 'No-auth request unexpectedly sent authorization.');
|
|
119
|
+
} else if (assertion === 'bearerAuthorization'
|
|
120
|
+
&& observations.authorization !== caseDefinition.authorization) {
|
|
121
|
+
assertionError('AUTH_MAPPING_INVALID', 'Bearer authorization was not forwarded exactly.');
|
|
122
|
+
} else if (assertion === 'structuredRequest') {
|
|
123
|
+
const format = observations.requestBody?.response_format;
|
|
124
|
+
if (format?.type !== 'json_schema'
|
|
125
|
+
|| format.json_schema?.strict !== true
|
|
126
|
+
|| JSON.stringify(format.json_schema.schema) !== JSON.stringify(RUNTIME_CONFORMANCE_OUTPUT_SCHEMA)
|
|
127
|
+
|| observations.requestBody.stream !== false) {
|
|
128
|
+
assertionError('REQUEST_MAPPING_INVALID', 'JSON Schema request mapping is not exact.');
|
|
129
|
+
}
|
|
130
|
+
} else if (assertion === 'usage') {
|
|
131
|
+
if (result.usage.inputTokens !== FIXTURE_USAGE.prompt_tokens
|
|
132
|
+
|| result.usage.outputTokens !== FIXTURE_USAGE.completion_tokens
|
|
133
|
+
|| result.usage.totalTokens !== FIXTURE_USAGE.total_tokens) {
|
|
134
|
+
assertionError('USAGE_MAPPING_INVALID', 'Token usage was not mapped exactly.');
|
|
135
|
+
}
|
|
136
|
+
} else if (assertion === 'identity') {
|
|
137
|
+
if (result.requestedModel !== runtime.model
|
|
138
|
+
|| result.model !== runtime.model
|
|
139
|
+
|| result.actualModel !== runtime.model) {
|
|
140
|
+
assertionError('IDENTITY_MAPPING_INVALID', 'Configured and returned model identities do not match.');
|
|
141
|
+
}
|
|
142
|
+
} else if (assertion === 'redirectErrorMode' && observations.redirect !== 'error') {
|
|
143
|
+
assertionError('REDIRECT_PROTECTION_INVALID', 'Fetch redirect mode was not set to error.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createOfflineConformanceExecutor({
|
|
149
|
+
providerFactory = createOpenAiCompatibleProvider
|
|
150
|
+
} = {}) {
|
|
151
|
+
return async function executeConformanceCase(caseDefinition, runtime) {
|
|
152
|
+
if (caseDefinition.transport.type === 'unknownError') {
|
|
153
|
+
try {
|
|
154
|
+
throw new Error('Offline fixture unknown error.');
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { outcome: normalizeConformanceError(error) };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const observations = {};
|
|
161
|
+
const provider = providerFactory({
|
|
162
|
+
endpoint: FIXTURE_ENDPOINT,
|
|
163
|
+
model: runtime.model,
|
|
164
|
+
provider: runtime.provider,
|
|
165
|
+
authorization: caseDefinition.authorization,
|
|
166
|
+
fetchImplementation: createFixtureFetch(caseDefinition, runtime, observations),
|
|
167
|
+
timeoutMs: caseDefinition.id === 'timeout-classification' ? 10 : 1_000,
|
|
168
|
+
maxResponseBytes: caseDefinition.transport.type === 'oversized' ? 64 : 2_048,
|
|
169
|
+
requireExactModelIdentity: caseDefinition.requireExactModelIdentity
|
|
170
|
+
});
|
|
171
|
+
const callerController = caseDefinition.assertions.includes('callerAbort')
|
|
172
|
+
? new AbortController()
|
|
173
|
+
: null;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const pending = provider.generateStructured(runtimeRequest(caseDefinition, callerController?.signal));
|
|
177
|
+
if (callerController) callerController.abort();
|
|
178
|
+
const result = await pending;
|
|
179
|
+
validateAssertions(caseDefinition, result, observations, runtime);
|
|
180
|
+
return { outcome: 'SUCCESS' };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
try {
|
|
183
|
+
if (caseDefinition.assertions.includes('redirectErrorMode') && observations.redirect !== 'error') {
|
|
184
|
+
assertionError('REDIRECT_PROTECTION_INVALID', 'Fetch redirect mode was not set to error.');
|
|
185
|
+
}
|
|
186
|
+
} catch (assertionFailure) {
|
|
187
|
+
return { outcome: normalizeConformanceError(assertionFailure) };
|
|
188
|
+
}
|
|
189
|
+
return { outcome: normalizeConformanceError(error) };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateRuntimeProfile(runtime) {
|
|
195
|
+
if (!runtime || typeof runtime !== 'object' || Array.isArray(runtime)) {
|
|
196
|
+
throw new TypeError('Conformance runtime profile must be an object.');
|
|
197
|
+
}
|
|
198
|
+
if (typeof runtime.provider !== 'string' || runtime.provider.trim().length === 0) {
|
|
199
|
+
throw new TypeError('Conformance runtime provider is required.');
|
|
200
|
+
}
|
|
201
|
+
if (typeof runtime.model !== 'string' || runtime.model.trim().length === 0) {
|
|
202
|
+
throw new TypeError('Conformance runtime model is required.');
|
|
203
|
+
}
|
|
204
|
+
return { provider: runtime.provider, model: runtime.model };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function runConformance({
|
|
208
|
+
runtime = { provider: 'openai-compatible', model: 'offline-fixture' },
|
|
209
|
+
cases = RUNTIME_CONFORMANCE_CASES,
|
|
210
|
+
capabilityProfileDigest,
|
|
211
|
+
deploymentProfileDigest,
|
|
212
|
+
conformanceScope = 'offline-runtime-protocol',
|
|
213
|
+
generatedAt = new Date()
|
|
214
|
+
} = {}, {
|
|
215
|
+
executeCase = createOfflineConformanceExecutor()
|
|
216
|
+
} = {}) {
|
|
217
|
+
const profile = validateRuntimeProfile(runtime);
|
|
218
|
+
const governance = createDefaultGovernanceArtifacts({
|
|
219
|
+
provider: profile.provider,
|
|
220
|
+
endpoint: FIXTURE_ENDPOINT,
|
|
221
|
+
model: profile.model
|
|
222
|
+
});
|
|
223
|
+
const caseResults = [];
|
|
224
|
+
for (const caseDefinition of cases) {
|
|
225
|
+
let observed;
|
|
226
|
+
try {
|
|
227
|
+
const result = await executeCase(caseDefinition, profile);
|
|
228
|
+
observed = typeof result?.outcome === 'string' ? result.outcome : 'UNKNOWN';
|
|
229
|
+
} catch (error) {
|
|
230
|
+
observed = normalizeConformanceError(error);
|
|
231
|
+
}
|
|
232
|
+
caseResults.push({
|
|
233
|
+
id: caseDefinition.id,
|
|
234
|
+
capability: caseDefinition.capability,
|
|
235
|
+
capabilities: caseDefinition.capabilities,
|
|
236
|
+
required: caseDefinition.required,
|
|
237
|
+
capabilityStatus: caseDefinition.capabilityStatusByOutcome[observed] ?? 'PASS',
|
|
238
|
+
status: caseDefinition.accepted.includes(observed) ? 'PASS' : 'FAIL',
|
|
239
|
+
expected: caseDefinition.expected,
|
|
240
|
+
observed
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return buildConformanceReport({
|
|
244
|
+
runtime: profile,
|
|
245
|
+
caseResults,
|
|
246
|
+
deploymentProfileDigest: deploymentProfileDigest
|
|
247
|
+
?? digestDeploymentProfile(governance.deploymentProfile),
|
|
248
|
+
capabilityProfileDigest: capabilityProfileDigest
|
|
249
|
+
?? digestCapabilityProfile(governance.capabilityProfile),
|
|
250
|
+
conformanceScope,
|
|
251
|
+
generatedAt
|
|
252
|
+
});
|
|
253
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const packageMetadata = require('../package.json');
|
|
5
|
+
|
|
6
|
+
export const PRODUCT_NAME = 'DepVerdict';
|
|
7
|
+
export const PACKAGE_NAME = '@thomasminh1995/depverdict';
|
|
8
|
+
export const CLI_NAME = 'depverdict';
|
|
9
|
+
export const ARTIFACT_GENERATOR_NAME = 'UpgradeLens';
|
|
10
|
+
export const VERSION = packageMetadata.version;
|
|
11
|
+
export const USER_AGENT = `${PRODUCT_NAME}/${VERSION}`;
|
|
12
|
+
export const MANIFEST_SCHEMA_VERSION = '2.0.0';
|
|
13
|
+
export const DEFAULT_OUTPUT_DIRECTORY = '.depverdict';
|
|
14
|
+
export const DEFAULT_MANIFEST_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/project-manifest.json`;
|
|
15
|
+
export const KNOWLEDGE_MANIFEST_SCHEMA_VERSION = '1.0.0';
|
|
16
|
+
export const DEFAULT_KNOWLEDGE_MANIFEST_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/knowledge-manifest.json`;
|
|
17
|
+
export const VERSION_ANALYSIS_SCHEMA_VERSION = '1.0.0';
|
|
18
|
+
export const DEFAULT_VERSION_ANALYSIS_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/version-analysis.json`;
|
|
19
|
+
export const USAGE_INDEX_SCHEMA_VERSION = '1.0.0';
|
|
20
|
+
export const DEFAULT_USAGE_INDEX_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/usage-index.json`;
|
|
21
|
+
export const REPOSITORY_IMPACT_SCHEMA_VERSION = '1.0.0';
|
|
22
|
+
export const DEFAULT_REPOSITORY_IMPACT_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/repository-impact.json`;
|
|
23
|
+
export const REPOSITORY_IMPACT_EVIDENCE_SCHEMA_VERSION = '1.0.0';
|
|
24
|
+
export const DEFAULT_REPOSITORY_IMPACT_EVIDENCE_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/repository-impact-evidence.json`;
|
|
25
|
+
export const UPGRADE_DECISION_SCHEMA_VERSION = '1.0.0';
|
|
26
|
+
export const DEFAULT_UPGRADE_DECISION_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/upgrade-decision.json`;
|
|
27
|
+
export const MIGRATION_CHECKLIST_SCHEMA_VERSION = '2.0.0';
|
|
28
|
+
export const DEFAULT_MIGRATION_CHECKLIST_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/migration-checklist.json`;
|
|
29
|
+
export const DEFAULT_MIGRATION_PLANNING_QUALIFICATION_PATH =
|
|
30
|
+
`${DEFAULT_OUTPUT_DIRECTORY}/migration-planning-qualification.json`;
|
|
31
|
+
export const DEFAULT_REPOSITORY_IMPACT_REPORT_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/repository-impact.md`;
|
|
32
|
+
export const DEFAULT_ANALYSIS_LOG_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/logs/analyze.log`;
|
|
33
|
+
export const DEFAULT_EVALUATION_REPORT_PATH = 'evaluation-report.json';
|
|
34
|
+
export const DEFAULT_METRICS_PATH = 'metrics.json';
|
|
35
|
+
export const DEFAULT_AI_SCORECARD_PATH = 'ai-scorecard.json';
|
|
36
|
+
export const DEFAULT_BENCHMARK_CONFIG_PATH = 'benchmark.json';
|
|
37
|
+
export const DEFAULT_BENCHMARK_REPORT_PATH = 'benchmark-report.json';
|
|
38
|
+
export const DEFAULT_CONFORMANCE_REPORT_PATH = `${DEFAULT_OUTPUT_DIRECTORY}/conformance-report.json`;
|
|
39
|
+
export const DEFAULT_GOVERNANCE_DIRECTORY = DEFAULT_OUTPUT_DIRECTORY;
|
|
40
|
+
export const CAPABILITY_PROFILE_FILENAME = 'capability-profile.json';
|
|
41
|
+
export const DEPLOYMENT_PROFILE_FILENAME = 'deployment-profile.json';
|
|
42
|
+
export const QUALIFICATION_RECORD_FILENAME = 'qualification-record.json';
|
|
43
|
+
export const DEFAULT_CAPABILITY_PROFILE_PATH = `${DEFAULT_GOVERNANCE_DIRECTORY}/${CAPABILITY_PROFILE_FILENAME}`;
|
|
44
|
+
export const DEFAULT_DEPLOYMENT_PROFILE_PATH = `${DEFAULT_GOVERNANCE_DIRECTORY}/${DEPLOYMENT_PROFILE_FILENAME}`;
|
|
45
|
+
export const DEFAULT_QUALIFICATION_RECORD_PATH = `${DEFAULT_GOVERNANCE_DIRECTORY}/${QUALIFICATION_RECORD_FILENAME}`;
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_IGNORED_DIRECTORIES = new Set([
|
|
48
|
+
'.git',
|
|
49
|
+
'.cache',
|
|
50
|
+
'.depverdict',
|
|
51
|
+
'.gradle',
|
|
52
|
+
'.hg',
|
|
53
|
+
'.idea',
|
|
54
|
+
'.mypy_cache',
|
|
55
|
+
'.next',
|
|
56
|
+
'.nuxt',
|
|
57
|
+
'.pytest_cache',
|
|
58
|
+
'.svn',
|
|
59
|
+
'.tox',
|
|
60
|
+
'.turbo',
|
|
61
|
+
'.upgradelens',
|
|
62
|
+
'.venv',
|
|
63
|
+
'.yarn',
|
|
64
|
+
'__pycache__',
|
|
65
|
+
'bower_components',
|
|
66
|
+
'build',
|
|
67
|
+
'coverage',
|
|
68
|
+
'dist',
|
|
69
|
+
'node_modules',
|
|
70
|
+
'out',
|
|
71
|
+
'target',
|
|
72
|
+
'vendor'
|
|
73
|
+
]);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_COOPERATIVE_BATCH_SIZE = 64;
|
|
4
|
+
export const DEFAULT_COOPERATIVE_MAX_INTERVAL_MS = 50;
|
|
5
|
+
|
|
6
|
+
function yieldToEventLoop() {
|
|
7
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cancellationError(signal) {
|
|
11
|
+
const error = new Error('Repository scan was cancelled.', { cause: signal?.reason });
|
|
12
|
+
error.name = 'AbortError';
|
|
13
|
+
error.code = 'ANALYSIS_CANCELLED';
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requireMonotonicTime(clock) {
|
|
18
|
+
const value = clock();
|
|
19
|
+
if (!Number.isFinite(value)) {
|
|
20
|
+
throw new TypeError('Cooperative scheduler monotonic clock must return a finite number.');
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates operation-local cooperative scheduling state for sequential scanners.
|
|
27
|
+
* A boundary represents one fully completed unit; it never exposes partial work.
|
|
28
|
+
*/
|
|
29
|
+
export function createCooperativeScheduler({
|
|
30
|
+
signal,
|
|
31
|
+
enabled = true,
|
|
32
|
+
batchSize = DEFAULT_COOPERATIVE_BATCH_SIZE,
|
|
33
|
+
maxIntervalMs = DEFAULT_COOPERATIVE_MAX_INTERVAL_MS,
|
|
34
|
+
monotonicClock = () => performance.now(),
|
|
35
|
+
yieldImplementation = yieldToEventLoop
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (typeof enabled !== 'boolean') throw new TypeError('Cooperative scheduler enabled must be boolean.');
|
|
38
|
+
if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
|
|
39
|
+
throw new TypeError('Cooperative scheduler batchSize must be a positive integer.');
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isFinite(maxIntervalMs) || maxIntervalMs <= 0) {
|
|
42
|
+
throw new TypeError('Cooperative scheduler maxIntervalMs must be positive.');
|
|
43
|
+
}
|
|
44
|
+
if (typeof monotonicClock !== 'function' || typeof yieldImplementation !== 'function') {
|
|
45
|
+
throw new TypeError('Cooperative scheduler requires clock and yield functions.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let completedUnits = 0;
|
|
49
|
+
let unitsSinceYield = 0;
|
|
50
|
+
let yieldCount = 0;
|
|
51
|
+
let lastYieldAt = requireMonotonicTime(monotonicClock);
|
|
52
|
+
|
|
53
|
+
function throwIfAborted() {
|
|
54
|
+
if (signal?.aborted) throw cancellationError(signal);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
checkpoint: throwIfAborted,
|
|
59
|
+
async boundary() {
|
|
60
|
+
throwIfAborted();
|
|
61
|
+
completedUnits += 1;
|
|
62
|
+
unitsSinceYield += 1;
|
|
63
|
+
if (!enabled) return false;
|
|
64
|
+
|
|
65
|
+
const now = requireMonotonicTime(monotonicClock);
|
|
66
|
+
if (unitsSinceYield < batchSize && now - lastYieldAt < maxIntervalMs) return false;
|
|
67
|
+
|
|
68
|
+
await yieldImplementation();
|
|
69
|
+
yieldCount += 1;
|
|
70
|
+
unitsSinceYield = 0;
|
|
71
|
+
lastYieldAt = requireMonotonicTime(monotonicClock);
|
|
72
|
+
throwIfAborted();
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
snapshot() {
|
|
76
|
+
return Object.freeze({ completedUnits, yieldCount, unitsSinceYield });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function compare(left, right) {
|
|
2
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function sortDependencies(dependencies) {
|
|
6
|
+
return [...dependencies].sort((left, right) =>
|
|
7
|
+
compare(left.normalizedName, right.normalizedName)
|
|
8
|
+
|| compare(left.type, right.type)
|
|
9
|
+
|| compare(left.name, right.name)
|
|
10
|
+
|| compare(left.declaredVersion ?? '', right.declaredVersion ?? '')
|
|
11
|
+
|| compare(left.manifest ?? '', right.manifest ?? '')
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function summarizeDependencies(dependencies, byType) {
|
|
16
|
+
const declarationCount = dependencies.length;
|
|
17
|
+
const uniqueCount = new Set(dependencies.map((dependency) => dependency.normalizedName)).size;
|
|
18
|
+
return {
|
|
19
|
+
status: 'parsed',
|
|
20
|
+
declarationCount,
|
|
21
|
+
uniqueCount,
|
|
22
|
+
duplicateCount: declarationCount - uniqueCount,
|
|
23
|
+
...(byType ? { byType } : {})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function duplicateDependencyNames(dependencies) {
|
|
28
|
+
const counts = new Map();
|
|
29
|
+
for (const dependency of dependencies) {
|
|
30
|
+
counts.set(dependency.normalizedName, (counts.get(dependency.normalizedName) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
return [...counts]
|
|
33
|
+
.filter(([, count]) => count > 1)
|
|
34
|
+
.map(([name]) => name)
|
|
35
|
+
.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function duplicateDependencyWarnings(dependencies, manifest) {
|
|
39
|
+
return duplicateDependencyNames(dependencies).map((name) => ({
|
|
40
|
+
code: 'DUPLICATE_DEPENDENCY_DECLARATION',
|
|
41
|
+
path: manifest,
|
|
42
|
+
message: `Dependency ${name} is declared multiple times.`
|
|
43
|
+
}));
|
|
44
|
+
}
|