@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,451 @@
|
|
|
1
|
+
import { AiRuntimeError, isAiRuntimeError } from './ai-runtime-error.js';
|
|
2
|
+
import {
|
|
3
|
+
buildErrorDebugRecord,
|
|
4
|
+
buildRequestDebugRecord,
|
|
5
|
+
buildResponseDebugRecord,
|
|
6
|
+
parseProviderErrorDescriptor,
|
|
7
|
+
writeAiRuntimeDebugRecord
|
|
8
|
+
} from './ai-runtime-debug.js';
|
|
9
|
+
import { projectStructuredOutputSchemaForProvider } from './structured-output-schema.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_AI_TIMEOUT_MS = 60_000;
|
|
12
|
+
export const DEFAULT_AI_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
const JSON_MEDIA_TYPE = /^(?:application\/(?:json|[a-z0-9.+-]+\+json))(?:\s*;|$)/i;
|
|
15
|
+
const COMPLETED_FINISH_REASONS = new Set(['stop']);
|
|
16
|
+
const REFUSAL_FINISH_REASONS = new Set(['content_filter', 'safety']);
|
|
17
|
+
const PROVIDER_ERROR_DESCRIPTOR_MAX_CHARACTERS = 1024;
|
|
18
|
+
const ALLOWED_REQUEST_EXTRA_FIELDS = new Set([
|
|
19
|
+
'temperature',
|
|
20
|
+
'top_p',
|
|
21
|
+
'max_tokens',
|
|
22
|
+
'max_completion_tokens',
|
|
23
|
+
'seed'
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function configurationError(message) {
|
|
27
|
+
return new AiRuntimeError('CONFIGURATION_ERROR', message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function positiveInteger(value, name) {
|
|
31
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
32
|
+
throw configurationError(`${name} must be a positive integer.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isLoopbackHostname(hostname) {
|
|
38
|
+
const normalized = hostname.startsWith('[') && hostname.endsWith(']')
|
|
39
|
+
? hostname.slice(1, -1)
|
|
40
|
+
: hostname;
|
|
41
|
+
return normalized === 'localhost'
|
|
42
|
+
|| normalized === '::1'
|
|
43
|
+
|| /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateOpenAiCompatibleEndpoint(endpoint) {
|
|
47
|
+
if (typeof endpoint !== 'string' || endpoint.length === 0) {
|
|
48
|
+
throw configurationError('OpenAI-compatible endpoint is required.');
|
|
49
|
+
}
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(endpoint);
|
|
53
|
+
} catch {
|
|
54
|
+
throw configurationError('OpenAI-compatible endpoint must be a valid URL.');
|
|
55
|
+
}
|
|
56
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
57
|
+
throw configurationError('OpenAI-compatible endpoint must use HTTP or HTTPS.');
|
|
58
|
+
}
|
|
59
|
+
if (url.username || url.password) {
|
|
60
|
+
throw configurationError('OpenAI-compatible endpoint must not contain credentials.');
|
|
61
|
+
}
|
|
62
|
+
if (url.protocol === 'http:' && !isLoopbackHostname(url.hostname)) {
|
|
63
|
+
throw configurationError('Remote OpenAI-compatible endpoints must use HTTPS.');
|
|
64
|
+
}
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function headerValue(headers, name) {
|
|
69
|
+
return headers && typeof headers.get === 'function' ? headers.get(name) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function discardBody(response) {
|
|
73
|
+
try {
|
|
74
|
+
await response?.body?.cancel?.();
|
|
75
|
+
} catch {
|
|
76
|
+
// Cleanup cannot change the sanitized provider result.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readFallbackText(response, maxResponseBytes) {
|
|
81
|
+
if (typeof response?.text !== 'function') {
|
|
82
|
+
await discardBody(response);
|
|
83
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider response body is not readable.');
|
|
84
|
+
}
|
|
85
|
+
const text = String(await response.text());
|
|
86
|
+
if (Buffer.byteLength(text, 'utf8') > maxResponseBytes) {
|
|
87
|
+
throw new AiRuntimeError('RESPONSE_TOO_LARGE', 'AI provider response exceeded the configured size limit.');
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readBoundedText(response, maxResponseBytes) {
|
|
93
|
+
const declaredLength = headerValue(response?.headers, 'content-length');
|
|
94
|
+
if (declaredLength !== null && /^\d+$/.test(declaredLength)
|
|
95
|
+
&& Number(declaredLength) > maxResponseBytes) {
|
|
96
|
+
await discardBody(response);
|
|
97
|
+
throw new AiRuntimeError('RESPONSE_TOO_LARGE', 'AI provider response exceeded the configured size limit.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reader = response?.body?.getReader?.();
|
|
101
|
+
if (!reader) return readFallbackText(response, maxResponseBytes);
|
|
102
|
+
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let byteCount = 0;
|
|
105
|
+
let complete = false;
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) {
|
|
110
|
+
complete = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const chunk = Buffer.from(value);
|
|
114
|
+
byteCount += chunk.length;
|
|
115
|
+
if (byteCount > maxResponseBytes) {
|
|
116
|
+
throw new AiRuntimeError('RESPONSE_TOO_LARGE', 'AI provider response exceeded the configured size limit.');
|
|
117
|
+
}
|
|
118
|
+
chunks.push(chunk);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
if (!complete) {
|
|
122
|
+
try {
|
|
123
|
+
await reader.cancel?.();
|
|
124
|
+
} catch {
|
|
125
|
+
// Cleanup cannot change the sanitized provider result.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
reader.releaseLock?.();
|
|
130
|
+
} catch {
|
|
131
|
+
// Cleanup cannot change the sanitized provider result.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function httpError(status, providerError) {
|
|
138
|
+
const descriptor = Object.values(providerError)
|
|
139
|
+
.join(' ')
|
|
140
|
+
.slice(0, PROVIDER_ERROR_DESCRIPTOR_MAX_CHARACTERS)
|
|
141
|
+
.toLowerCase();
|
|
142
|
+
const mentionsSchema = /json[_ -]?schema|response[_ -]?format|structured[_ -]?output|schema/.test(descriptor);
|
|
143
|
+
const unsupported = /unsupported|not[_ -]?supported|does[_ -]?not[_ -]?support|unknown[_ -]?(?:field|parameter)|unrecognized/.test(descriptor);
|
|
144
|
+
const modelMissing = /model[_ -]?not[_ -]?found|no such model|unknown model|model does not exist/.test(descriptor);
|
|
145
|
+
|
|
146
|
+
if (status === 401) {
|
|
147
|
+
return new AiRuntimeError('AUTH_ERROR', 'AI provider rejected authentication.', { status });
|
|
148
|
+
}
|
|
149
|
+
if (status === 403) {
|
|
150
|
+
return new AiRuntimeError('ACCESS_DENIED', 'AI provider denied access to the requested resource.', { status });
|
|
151
|
+
}
|
|
152
|
+
if (status === 402) {
|
|
153
|
+
return new AiRuntimeError('INSUFFICIENT_CREDIT', 'AI provider reported insufficient credit.', { status });
|
|
154
|
+
}
|
|
155
|
+
if (status === 404 && modelMissing) {
|
|
156
|
+
return new AiRuntimeError('MODEL_NOT_FOUND', 'Configured AI model was not found.', { status });
|
|
157
|
+
}
|
|
158
|
+
if (status === 408 || status === 504) {
|
|
159
|
+
return new AiRuntimeError('TIMEOUT', 'AI provider request timed out.', { status, retryable: true });
|
|
160
|
+
}
|
|
161
|
+
if (status === 429) {
|
|
162
|
+
return new AiRuntimeError('RATE_LIMITED', 'AI provider rate limit was reached.', { status, retryable: true });
|
|
163
|
+
}
|
|
164
|
+
if (status === 502 || status === 503) {
|
|
165
|
+
return new AiRuntimeError('PROVIDER_UNAVAILABLE', 'AI provider is temporarily unavailable.', { status, retryable: true });
|
|
166
|
+
}
|
|
167
|
+
if (status === 400 || status === 422) {
|
|
168
|
+
if (mentionsSchema && unsupported) {
|
|
169
|
+
return new AiRuntimeError(
|
|
170
|
+
'STRUCTURED_OUTPUT_UNSUPPORTED',
|
|
171
|
+
'AI provider does not support the required structured output mode.',
|
|
172
|
+
{ status }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (mentionsSchema) {
|
|
176
|
+
return new AiRuntimeError('SCHEMA_REJECTED', 'AI provider rejected the required output schema.', { status });
|
|
177
|
+
}
|
|
178
|
+
return new AiRuntimeError('INVALID_REQUEST', 'AI provider rejected the request parameters.', { status });
|
|
179
|
+
}
|
|
180
|
+
return new AiRuntimeError('PROVIDER_ERROR', 'AI provider returned an unsuccessful response.', { status });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function nullableToken(value) {
|
|
184
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeUsage(usage) {
|
|
188
|
+
return {
|
|
189
|
+
inputTokens: nullableToken(usage?.prompt_tokens),
|
|
190
|
+
outputTokens: nullableToken(usage?.completion_tokens),
|
|
191
|
+
totalTokens: nullableToken(usage?.total_tokens)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateSuccessEnvelope(body) {
|
|
196
|
+
if (!body || typeof body !== 'object' || !Array.isArray(body.choices) || body.choices.length !== 1) {
|
|
197
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider returned an invalid response envelope.');
|
|
198
|
+
}
|
|
199
|
+
const choice = body.choices[0];
|
|
200
|
+
const message = choice?.message;
|
|
201
|
+
const finishReason = choice?.finish_reason;
|
|
202
|
+
|
|
203
|
+
if (finishReason === 'length') {
|
|
204
|
+
throw new AiRuntimeError('OUTPUT_TRUNCATED', 'AI provider output was truncated.');
|
|
205
|
+
}
|
|
206
|
+
if (REFUSAL_FINISH_REASONS.has(finishReason) || (typeof message?.refusal === 'string' && message.refusal.length > 0)) {
|
|
207
|
+
throw new AiRuntimeError('CONTENT_REFUSED', 'AI provider refused the requested content.');
|
|
208
|
+
}
|
|
209
|
+
if (message?.tool_calls?.length > 0 || message?.function_call || ['tool_calls', 'function_call'].includes(finishReason)) {
|
|
210
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider returned a tool call instead of final content.');
|
|
211
|
+
}
|
|
212
|
+
if (!COMPLETED_FINISH_REASONS.has(finishReason)) {
|
|
213
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider did not return a completed response.');
|
|
214
|
+
}
|
|
215
|
+
if (!message || message.role !== 'assistant'
|
|
216
|
+
|| typeof message.content !== 'string' || message.content.trim().length === 0) {
|
|
217
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider returned no usable assistant content.');
|
|
218
|
+
}
|
|
219
|
+
return message.content;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateStructuredRequest(request) {
|
|
223
|
+
if (typeof request?.systemPrompt !== 'string' || typeof request?.userPrompt !== 'string') {
|
|
224
|
+
throw configurationError('Runtime request requires rendered systemPrompt and userPrompt.');
|
|
225
|
+
}
|
|
226
|
+
const structuredOutput = request.structuredOutput;
|
|
227
|
+
if (structuredOutput?.mode !== 'jsonSchema') {
|
|
228
|
+
throw new AiRuntimeError(
|
|
229
|
+
'STRUCTURED_OUTPUT_UNSUPPORTED',
|
|
230
|
+
'Runtime request requires JSON Schema structured output.'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (!structuredOutput.schema || typeof structuredOutput.schema !== 'object' || Array.isArray(structuredOutput.schema)) {
|
|
234
|
+
throw configurationError('Runtime request requires an exact output schema.');
|
|
235
|
+
}
|
|
236
|
+
if (typeof structuredOutput.name !== 'string'
|
|
237
|
+
|| !/^[A-Za-z0-9_-]{1,64}$/.test(structuredOutput.name)) {
|
|
238
|
+
throw configurationError('Runtime request requires a valid deterministic schema name.');
|
|
239
|
+
}
|
|
240
|
+
return structuredOutput;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createRequestBody(request, model, requestExtras) {
|
|
244
|
+
const structuredOutput = validateStructuredRequest(request);
|
|
245
|
+
const allowedExtras = Object.fromEntries(
|
|
246
|
+
Object.entries(requestExtras).filter(([key]) => ALLOWED_REQUEST_EXTRA_FIELDS.has(key))
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
...structuredClone(allowedExtras),
|
|
250
|
+
model,
|
|
251
|
+
messages: [
|
|
252
|
+
{ role: 'system', content: request.systemPrompt },
|
|
253
|
+
{ role: 'user', content: request.userPrompt }
|
|
254
|
+
],
|
|
255
|
+
response_format: {
|
|
256
|
+
type: 'json_schema',
|
|
257
|
+
json_schema: {
|
|
258
|
+
name: structuredOutput.name,
|
|
259
|
+
strict: true,
|
|
260
|
+
schema: projectStructuredOutputSchemaForProvider(structuredOutput.schema)
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
stream: false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Non-streaming provider for the common OpenAI-compatible Chat Completions
|
|
269
|
+
* wire protocol. It does not parse or validate the task candidate JSON.
|
|
270
|
+
*/
|
|
271
|
+
export function createOpenAiCompatibleProvider({
|
|
272
|
+
endpoint,
|
|
273
|
+
model,
|
|
274
|
+
authorization,
|
|
275
|
+
provider = 'openai-compatible',
|
|
276
|
+
fetchImplementation = globalThis.fetch,
|
|
277
|
+
timeoutMs = DEFAULT_AI_TIMEOUT_MS,
|
|
278
|
+
maxResponseBytes = DEFAULT_AI_MAX_RESPONSE_BYTES,
|
|
279
|
+
requestExtras = {},
|
|
280
|
+
requireExactModelIdentity = false,
|
|
281
|
+
debug = false,
|
|
282
|
+
debugWriter = process.stderr,
|
|
283
|
+
clock = Date
|
|
284
|
+
} = {}) {
|
|
285
|
+
const url = validateOpenAiCompatibleEndpoint(endpoint);
|
|
286
|
+
if (typeof model !== 'string' || model.trim().length === 0) {
|
|
287
|
+
throw configurationError('OpenAI-compatible model is required.');
|
|
288
|
+
}
|
|
289
|
+
if (typeof fetchImplementation !== 'function') {
|
|
290
|
+
throw configurationError('A WHATWG-compatible fetch implementation is required.');
|
|
291
|
+
}
|
|
292
|
+
if (authorization !== undefined && typeof authorization !== 'string') {
|
|
293
|
+
throw configurationError('OpenAI-compatible authorization must be a string when provided.');
|
|
294
|
+
}
|
|
295
|
+
if (!requestExtras || typeof requestExtras !== 'object' || Array.isArray(requestExtras)) {
|
|
296
|
+
throw configurationError('OpenAI-compatible request extras must be an object.');
|
|
297
|
+
}
|
|
298
|
+
if (typeof debug !== 'boolean') {
|
|
299
|
+
throw configurationError('OpenAI-compatible debug must be a boolean.');
|
|
300
|
+
}
|
|
301
|
+
if (debug && typeof debugWriter !== 'function' && typeof debugWriter?.write !== 'function') {
|
|
302
|
+
throw configurationError('OpenAI-compatible debug writer must be a function or writable stream.');
|
|
303
|
+
}
|
|
304
|
+
positiveInteger(timeoutMs, 'timeoutMs');
|
|
305
|
+
positiveInteger(maxResponseBytes, 'maxResponseBytes');
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
name: provider,
|
|
309
|
+
model,
|
|
310
|
+
async generateStructured(request) {
|
|
311
|
+
const startedAt = clock.now();
|
|
312
|
+
let requestBody;
|
|
313
|
+
let serializedBody;
|
|
314
|
+
try {
|
|
315
|
+
requestBody = createRequestBody(request, model, requestExtras);
|
|
316
|
+
serializedBody = JSON.stringify(requestBody);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isAiRuntimeError(error)) throw error;
|
|
319
|
+
throw configurationError('OpenAI-compatible request could not be serialized.');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (debug) {
|
|
323
|
+
writeAiRuntimeDebugRecord(debugWriter, buildRequestDebugRecord({
|
|
324
|
+
provider,
|
|
325
|
+
endpoint: url,
|
|
326
|
+
model,
|
|
327
|
+
requestBody,
|
|
328
|
+
serializedBody,
|
|
329
|
+
structuredOutputMode: request.structuredOutput.mode,
|
|
330
|
+
requestExtraKeys: Object.keys(requestExtras).filter((key) => ALLOWED_REQUEST_EXTRA_FIELDS.has(key))
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
let timedOut = false;
|
|
336
|
+
const onCallerAbort = () => controller.abort();
|
|
337
|
+
if (request?.signal?.aborted) controller.abort();
|
|
338
|
+
else request?.signal?.addEventListener?.('abort', onCallerAbort, { once: true });
|
|
339
|
+
const timer = setTimeout(() => {
|
|
340
|
+
timedOut = true;
|
|
341
|
+
controller.abort();
|
|
342
|
+
}, timeoutMs);
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
let response;
|
|
346
|
+
try {
|
|
347
|
+
response = await fetchImplementation(url, {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: {
|
|
350
|
+
accept: 'application/json',
|
|
351
|
+
'content-type': 'application/json',
|
|
352
|
+
...(authorization ? { authorization } : {})
|
|
353
|
+
},
|
|
354
|
+
body: serializedBody,
|
|
355
|
+
credentials: 'omit',
|
|
356
|
+
redirect: 'error',
|
|
357
|
+
signal: controller.signal
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (isAiRuntimeError(error)) throw error;
|
|
361
|
+
if (timedOut) throw new AiRuntimeError('TIMEOUT', 'AI provider request timed out.', { retryable: true });
|
|
362
|
+
if (request?.signal?.aborted || error?.name === 'AbortError') {
|
|
363
|
+
throw new AiRuntimeError('CANCELLED', 'AI provider request was cancelled.');
|
|
364
|
+
}
|
|
365
|
+
throw new AiRuntimeError('NETWORK_ERROR', 'AI provider network request failed.', { retryable: true });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!response || !Number.isInteger(response.status)) {
|
|
369
|
+
await discardBody(response);
|
|
370
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider returned an invalid HTTP response.');
|
|
371
|
+
}
|
|
372
|
+
let text;
|
|
373
|
+
try {
|
|
374
|
+
text = await readBoundedText(response, maxResponseBytes);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (timedOut && !isAiRuntimeError(error)) {
|
|
377
|
+
throw new AiRuntimeError('TIMEOUT', 'AI provider request timed out.', { retryable: true });
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (response.status < 200 || response.status >= 300) {
|
|
382
|
+
const providerError = parseProviderErrorDescriptor(text);
|
|
383
|
+
const runtimeError = httpError(response.status, providerError);
|
|
384
|
+
if (debug) {
|
|
385
|
+
writeAiRuntimeDebugRecord(debugWriter, buildErrorDebugRecord({
|
|
386
|
+
status: response.status,
|
|
387
|
+
runtimeError,
|
|
388
|
+
providerError,
|
|
389
|
+
responseText: text,
|
|
390
|
+
latencyMs: Math.max(0, clock.now() - startedAt)
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
throw runtimeError;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const contentType = headerValue(response.headers, 'content-type');
|
|
397
|
+
if (!contentType || !JSON_MEDIA_TYPE.test(contentType)) {
|
|
398
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider response must use a JSON media type.');
|
|
399
|
+
}
|
|
400
|
+
let body;
|
|
401
|
+
try {
|
|
402
|
+
body = JSON.parse(text);
|
|
403
|
+
} catch {
|
|
404
|
+
throw new AiRuntimeError('INVALID_RESPONSE', 'AI provider response is not valid JSON.');
|
|
405
|
+
}
|
|
406
|
+
const output = validateSuccessEnvelope(body);
|
|
407
|
+
const actualModel = typeof body.model === 'string' && body.model.length > 0 ? body.model : null;
|
|
408
|
+
if (requireExactModelIdentity && actualModel !== model) {
|
|
409
|
+
throw new AiRuntimeError('IDENTITY_MISMATCH', 'AI provider returned an unexpected model identity.');
|
|
410
|
+
}
|
|
411
|
+
const providerRequestId = typeof body.id === 'string' && /^[A-Za-z0-9._:-]{1,256}$/.test(body.id)
|
|
412
|
+
? body.id
|
|
413
|
+
: null;
|
|
414
|
+
const usage = normalizeUsage(body.usage);
|
|
415
|
+
const latencyMs = Math.max(0, clock.now() - startedAt);
|
|
416
|
+
if (debug) {
|
|
417
|
+
writeAiRuntimeDebugRecord(debugWriter, buildResponseDebugRecord({
|
|
418
|
+
status: response.status,
|
|
419
|
+
contentType,
|
|
420
|
+
responseText: text,
|
|
421
|
+
body,
|
|
422
|
+
assistantContent: output,
|
|
423
|
+
requestedModel: model,
|
|
424
|
+
actualModel,
|
|
425
|
+
usage,
|
|
426
|
+
latencyMs
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
output,
|
|
431
|
+
provider,
|
|
432
|
+
requestedProvider: provider,
|
|
433
|
+
model,
|
|
434
|
+
requestedModel: model,
|
|
435
|
+
actualModel,
|
|
436
|
+
finishReason: 'complete',
|
|
437
|
+
usage,
|
|
438
|
+
latencyMs,
|
|
439
|
+
providerRequestId,
|
|
440
|
+
attemptCount: 1,
|
|
441
|
+
retryCount: 0,
|
|
442
|
+
fallbackCount: 0,
|
|
443
|
+
fallbackOccurred: false
|
|
444
|
+
};
|
|
445
|
+
} finally {
|
|
446
|
+
clearTimeout(timer);
|
|
447
|
+
request?.signal?.removeEventListener?.('abort', onCallerAbort);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_ANALYSIS_LOG_PATH, PRODUCT_NAME } from '../constants.js';
|
|
4
|
+
import { writeTextArtifact } from './text-writer.js';
|
|
5
|
+
|
|
6
|
+
export function renderAnalysisFailureLog(error) {
|
|
7
|
+
const message = error.cause?.message ?? error.message;
|
|
8
|
+
const lines = [
|
|
9
|
+
`${PRODUCT_NAME} analysis failure`,
|
|
10
|
+
`Stage: ${error.stage.label}`,
|
|
11
|
+
`Message: ${message}`
|
|
12
|
+
];
|
|
13
|
+
if (error.cause?.decision) {
|
|
14
|
+
lines.push(
|
|
15
|
+
`Qualification status: ${error.cause.decision.status}`,
|
|
16
|
+
`Reason: ${error.cause.decision.reasonCode}`,
|
|
17
|
+
`Source: ${error.cause.decision.sourceKind}`,
|
|
18
|
+
`Next action: ${error.cause.decision.nextAction}`
|
|
19
|
+
);
|
|
20
|
+
} else if (typeof error.cause?.code === 'string') {
|
|
21
|
+
lines.push(`Reason: ${error.cause.code}`);
|
|
22
|
+
}
|
|
23
|
+
lines.push('');
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function writeAnalysisFailureLog(repositoryRoot, error) {
|
|
28
|
+
return writeTextArtifact(
|
|
29
|
+
path.join(repositoryRoot, DEFAULT_ANALYSIS_LOG_PATH),
|
|
30
|
+
renderAnalysisFailureLog(error)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createProgressEventRuntime } from './progress-events.js';
|
|
2
|
+
|
|
3
|
+
export const ANALYSIS_STAGES = Object.freeze([
|
|
4
|
+
Object.freeze({ id: 'projectDiscovery', label: 'Project Discovery' }),
|
|
5
|
+
Object.freeze({ id: 'knowledgeResearch', label: 'Knowledge Research' }),
|
|
6
|
+
Object.freeze({ id: 'versionAnalysis', label: 'Version Analysis' }),
|
|
7
|
+
Object.freeze({ id: 'usageDiscovery', label: 'Repository Usage Discovery' }),
|
|
8
|
+
Object.freeze({ id: 'impactAnalysis', label: 'Repository Impact Analysis' }),
|
|
9
|
+
Object.freeze({ id: 'impactEvidence', label: 'Repository Impact Evidence' }),
|
|
10
|
+
Object.freeze({ id: 'upgradeDecision', label: 'Deterministic Upgrade Decision' }),
|
|
11
|
+
Object.freeze({ id: 'markdownReport', label: 'Markdown Report' })
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const MIGRATION_CHECKLIST_ANALYSIS_STAGE = Object.freeze({
|
|
15
|
+
id: 'migrationChecklist',
|
|
16
|
+
label: 'Migration Checklist'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const ANALYSIS_STAGE_INITIAL_ACTIVITIES = Object.freeze({
|
|
20
|
+
projectDiscovery: Object.freeze({
|
|
21
|
+
activityKind: 'DISCOVER_PROJECT_MANIFESTS',
|
|
22
|
+
subject: 'Discovering project manifests'
|
|
23
|
+
}),
|
|
24
|
+
knowledgeResearch: Object.freeze({
|
|
25
|
+
activityKind: 'RESEARCH_DEPENDENCY_METADATA',
|
|
26
|
+
subject: 'Researching dependency metadata'
|
|
27
|
+
}),
|
|
28
|
+
versionAnalysis: Object.freeze({
|
|
29
|
+
activityKind: 'PREPARE_DEPENDENCY_ANALYSIS',
|
|
30
|
+
subject: 'Preparing dependency analysis'
|
|
31
|
+
}),
|
|
32
|
+
usageDiscovery: Object.freeze({
|
|
33
|
+
activityKind: 'SCAN_SUPPORTED_SOURCE',
|
|
34
|
+
subject: 'Scanning supported source files'
|
|
35
|
+
}),
|
|
36
|
+
impactAnalysis: Object.freeze({
|
|
37
|
+
activityKind: 'MATCH_REPOSITORY_IMPACT',
|
|
38
|
+
subject: 'Matching repository impact'
|
|
39
|
+
}),
|
|
40
|
+
impactEvidence: Object.freeze({
|
|
41
|
+
activityKind: 'VALIDATE_IMPACT_EVIDENCE',
|
|
42
|
+
subject: 'Validating repository impact evidence'
|
|
43
|
+
}),
|
|
44
|
+
upgradeDecision: Object.freeze({
|
|
45
|
+
activityKind: 'APPLY_UPGRADE_DECISION_POLICY',
|
|
46
|
+
subject: 'Applying deterministic upgrade decision policy'
|
|
47
|
+
}),
|
|
48
|
+
migrationChecklist: Object.freeze({
|
|
49
|
+
activityKind: 'PREPARE_MIGRATION_CONTEXTS',
|
|
50
|
+
subject: 'Preparing Migration Checklist contexts'
|
|
51
|
+
}),
|
|
52
|
+
markdownReport: Object.freeze({
|
|
53
|
+
activityKind: 'RENDER_MARKDOWN_REPORT',
|
|
54
|
+
subject: 'Rendering Markdown report'
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export function createAnalysisStages({ migrationChecklist = false } = {}) {
|
|
59
|
+
if (!migrationChecklist) return ANALYSIS_STAGES;
|
|
60
|
+
return Object.freeze([
|
|
61
|
+
...ANALYSIS_STAGES.slice(0, -1),
|
|
62
|
+
MIGRATION_CHECKLIST_ANALYSIS_STAGE,
|
|
63
|
+
ANALYSIS_STAGES.at(-1)
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class PipelineStageError extends Error {
|
|
68
|
+
constructor(stage, cause) {
|
|
69
|
+
super(`${stage.label} failed: ${cause?.message ?? String(cause)}`, { cause });
|
|
70
|
+
this.name = 'PipelineStageError';
|
|
71
|
+
this.stage = stage;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class PipelineCancellationError extends Error {
|
|
76
|
+
constructor(stage = null, cause = null) {
|
|
77
|
+
super(stage ? `${stage.label} was cancelled.` : 'Analysis was cancelled.', { cause });
|
|
78
|
+
this.name = 'PipelineCancellationError';
|
|
79
|
+
this.code = 'ANALYSIS_CANCELLED';
|
|
80
|
+
this.stage = stage;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateRunners(runners, stages) {
|
|
85
|
+
for (const stage of stages) {
|
|
86
|
+
if (typeof runners?.[stage.id] !== 'function') {
|
|
87
|
+
throw new Error(`Analysis pipeline requires a ${stage.id} stage runner.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function failureReasonCode(cause) {
|
|
93
|
+
return typeof cause?.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(cause.code)
|
|
94
|
+
? cause.code
|
|
95
|
+
: 'STAGE_FAILED';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function abortError(stage, signal) {
|
|
99
|
+
return new PipelineCancellationError(stage, signal?.reason);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runWithSignal(run, signal, stage) {
|
|
103
|
+
if (!signal) return run();
|
|
104
|
+
if (signal.aborted) throw abortError(stage, signal);
|
|
105
|
+
let removeAbortListener = () => {};
|
|
106
|
+
const aborted = new Promise((resolve, reject) => {
|
|
107
|
+
const onAbort = () => reject(abortError(stage, signal));
|
|
108
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
return await Promise.race([run(), aborted]);
|
|
113
|
+
} finally {
|
|
114
|
+
removeAbortListener();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function runAnalysisPipeline({
|
|
119
|
+
repositoryRoot,
|
|
120
|
+
runners,
|
|
121
|
+
progressReporter,
|
|
122
|
+
stages = ANALYSIS_STAGES,
|
|
123
|
+
signal,
|
|
124
|
+
progressListener,
|
|
125
|
+
progressOptions = {}
|
|
126
|
+
}) {
|
|
127
|
+
validateRunners(runners, stages);
|
|
128
|
+
const artifacts = {};
|
|
129
|
+
const progress = createProgressEventRuntime({
|
|
130
|
+
stages,
|
|
131
|
+
listener(event) {
|
|
132
|
+
try {
|
|
133
|
+
progressReporter?.handle?.(event)?.catch?.(() => {});
|
|
134
|
+
} catch {
|
|
135
|
+
// Reporter failures are isolated from business execution.
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
progressListener?.(event)?.catch?.(() => {});
|
|
139
|
+
} catch {
|
|
140
|
+
// Observer failures are isolated from business execution.
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
...progressOptions
|
|
144
|
+
});
|
|
145
|
+
progress.startRun();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
if (signal?.aborted) {
|
|
149
|
+
for (const stage of stages) progress.skipStage(stage.id, 'RUN_CANCELLED');
|
|
150
|
+
progress.cancelRun();
|
|
151
|
+
throw abortError(null, signal);
|
|
152
|
+
}
|
|
153
|
+
for (let index = 0; index < stages.length; index += 1) {
|
|
154
|
+
const stage = stages[index];
|
|
155
|
+
progress.startStage(stage.id);
|
|
156
|
+
const initialActivity = ANALYSIS_STAGE_INITIAL_ACTIVITIES[stage.id];
|
|
157
|
+
if (initialActivity) progress.activity(stage.id, initialActivity);
|
|
158
|
+
const reportActivity = (activity) => {
|
|
159
|
+
try {
|
|
160
|
+
progress.activity(stage.id, activity);
|
|
161
|
+
} catch {
|
|
162
|
+
// Invalid or throwing progress instrumentation cannot affect stage output.
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
artifacts[stage.id] = await runWithSignal(() => runners[stage.id]({
|
|
167
|
+
repositoryRoot,
|
|
168
|
+
artifacts: Object.freeze({ ...artifacts }),
|
|
169
|
+
progress: reportActivity,
|
|
170
|
+
signal
|
|
171
|
+
}), signal, stage);
|
|
172
|
+
if (signal?.aborted) throw abortError(stage, signal);
|
|
173
|
+
progress.completeStage(stage.id);
|
|
174
|
+
} catch (cause) {
|
|
175
|
+
if (cause instanceof PipelineCancellationError || signal?.aborted) {
|
|
176
|
+
progress.cancelStage(stage.id, 'USER_CANCELLED');
|
|
177
|
+
for (const remaining of stages.slice(index + 1)) {
|
|
178
|
+
progress.skipStage(remaining.id, 'RUN_CANCELLED');
|
|
179
|
+
}
|
|
180
|
+
progress.cancelRun();
|
|
181
|
+
throw cause instanceof PipelineCancellationError ? cause : abortError(stage, signal);
|
|
182
|
+
}
|
|
183
|
+
progress.failStage(stage.id, failureReasonCode(cause));
|
|
184
|
+
for (const remaining of stages.slice(index + 1)) {
|
|
185
|
+
progress.skipStage(remaining.id, 'PRIOR_STAGE_FAILED');
|
|
186
|
+
}
|
|
187
|
+
progress.failRun();
|
|
188
|
+
throw new PipelineStageError(stage, cause);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
progress.completeRun();
|
|
192
|
+
return Object.freeze({
|
|
193
|
+
repositoryRoot,
|
|
194
|
+
artifacts: Object.freeze({ ...artifacts }),
|
|
195
|
+
progress: progress.snapshot()
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
progress.dispose();
|
|
199
|
+
}
|
|
200
|
+
}
|