@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,337 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
3
|
+
export const PROGRESS_EVENT_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export const PROGRESS_EVENT_TYPES = Object.freeze([
|
|
6
|
+
'RUN_STARTED',
|
|
7
|
+
'STAGE_SCHEDULED',
|
|
8
|
+
'STAGE_STARTED',
|
|
9
|
+
'STAGE_ACTIVITY',
|
|
10
|
+
'STAGE_HEARTBEAT',
|
|
11
|
+
'STAGE_COMPLETED',
|
|
12
|
+
'STAGE_FAILED',
|
|
13
|
+
'STAGE_SKIPPED',
|
|
14
|
+
'STAGE_CANCELLED',
|
|
15
|
+
'RUN_COMPLETED',
|
|
16
|
+
'RUN_FAILED',
|
|
17
|
+
'RUN_CANCELLED'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const PROGRESS_STAGE_STATUSES = Object.freeze([
|
|
21
|
+
'SCHEDULED',
|
|
22
|
+
'STARTED',
|
|
23
|
+
'COMPLETED',
|
|
24
|
+
'FAILED',
|
|
25
|
+
'SKIPPED',
|
|
26
|
+
'CANCELLED'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
30
|
+
export const MAX_PROGRESS_SUBJECT_LENGTH = 120;
|
|
31
|
+
|
|
32
|
+
const EVENT_TYPE_SET = new Set(PROGRESS_EVENT_TYPES);
|
|
33
|
+
const TERMINAL_STATUSES = new Set(['COMPLETED', 'FAILED', 'SKIPPED', 'CANCELLED']);
|
|
34
|
+
const REASON_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
35
|
+
const ACTIVITY_KIND_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
36
|
+
|
|
37
|
+
function defaultScheduler() {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
setTimeout(callback, delay) {
|
|
40
|
+
return setTimeout(callback, delay);
|
|
41
|
+
},
|
|
42
|
+
clearTimeout(timer) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cloneAndFreeze(value) {
|
|
49
|
+
return Object.freeze(structuredClone(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sanitizeProgressSubject(value) {
|
|
53
|
+
if (value === undefined || value === null) return null;
|
|
54
|
+
const normalized = String(value)
|
|
55
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
|
56
|
+
.replace(/["\\]/g, "'")
|
|
57
|
+
.replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]')
|
|
58
|
+
.replace(/\b(authorization|api[_-]?key|access[_-]?token|secret)\s*[:=]\s*\S+/gi, '$1=[REDACTED]')
|
|
59
|
+
.replace(/\bhttps?:\/\/[^\s/]+(?:\/[^\s?#]*)?[?#]\S*/gi, '[REDACTED_URL]')
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.trim();
|
|
62
|
+
if (!normalized) return null;
|
|
63
|
+
return normalized.slice(0, MAX_PROGRESS_SUBJECT_LENGTH);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function constrainedCode(value, pattern, field) {
|
|
67
|
+
if (value === undefined || value === null) return null;
|
|
68
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
69
|
+
throw new TypeError(`${field} must be a constrained uppercase code.`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeCount(completed, total) {
|
|
75
|
+
if ((completed === undefined || completed === null) !== (total === undefined || total === null)) {
|
|
76
|
+
throw new TypeError('Progress completed and total must be supplied together.');
|
|
77
|
+
}
|
|
78
|
+
if (completed === undefined || completed === null) return { completed: null, total: null };
|
|
79
|
+
if (!Number.isSafeInteger(completed) || !Number.isSafeInteger(total)
|
|
80
|
+
|| completed < 0 || total < 0 || completed > total) {
|
|
81
|
+
throw new TypeError('Progress counts must be non-negative integers with completed <= total.');
|
|
82
|
+
}
|
|
83
|
+
return { completed, total };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeNotify(listener, event) {
|
|
87
|
+
if (!listener) return;
|
|
88
|
+
try {
|
|
89
|
+
const result = typeof listener === 'function' ? listener(event) : listener.handle?.(event);
|
|
90
|
+
result?.catch?.(() => {});
|
|
91
|
+
} catch {
|
|
92
|
+
// Progress is presentation-only and must not affect pipeline semantics.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateStages(stages) {
|
|
97
|
+
const ids = new Set();
|
|
98
|
+
for (const stage of stages) {
|
|
99
|
+
if (!stage || typeof stage.id !== 'string' || typeof stage.label !== 'string'
|
|
100
|
+
|| stage.id.length === 0 || stage.label.length === 0 || ids.has(stage.id)) {
|
|
101
|
+
throw new TypeError('Progress stages require unique non-empty ids and labels.');
|
|
102
|
+
}
|
|
103
|
+
ids.add(stage.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Owns normalized lifecycle state and heartbeat timing for the sequential pipeline.
|
|
109
|
+
* Stage adapters may only contribute bounded activity metadata.
|
|
110
|
+
*/
|
|
111
|
+
export function createProgressEventRuntime({
|
|
112
|
+
stages,
|
|
113
|
+
listener,
|
|
114
|
+
monotonicClock = () => performance.now(),
|
|
115
|
+
wallClock = () => new Date(),
|
|
116
|
+
scheduler = defaultScheduler(),
|
|
117
|
+
heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS
|
|
118
|
+
}) {
|
|
119
|
+
validateStages(stages);
|
|
120
|
+
if (!Number.isSafeInteger(heartbeatIntervalMs) || heartbeatIntervalMs <= 0) {
|
|
121
|
+
throw new TypeError('heartbeatIntervalMs must be a positive integer.');
|
|
122
|
+
}
|
|
123
|
+
if (typeof monotonicClock !== 'function' || typeof wallClock !== 'function'
|
|
124
|
+
|| typeof scheduler?.setTimeout !== 'function' || typeof scheduler?.clearTimeout !== 'function') {
|
|
125
|
+
throw new TypeError('Progress runtime requires clock and scheduler functions.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stageById = new Map(stages.map((stage) => [stage.id, stage]));
|
|
129
|
+
const stateById = new Map(stages.map((stage) => [stage.id, {
|
|
130
|
+
status: null,
|
|
131
|
+
startedAt: null,
|
|
132
|
+
activityKind: null,
|
|
133
|
+
subject: null,
|
|
134
|
+
completed: null,
|
|
135
|
+
total: null
|
|
136
|
+
}]));
|
|
137
|
+
let sequence = 0;
|
|
138
|
+
let runStartedAt = null;
|
|
139
|
+
let runTerminal = false;
|
|
140
|
+
let activeStageId = null;
|
|
141
|
+
let heartbeatTimer = null;
|
|
142
|
+
let heartbeatToken = 0;
|
|
143
|
+
|
|
144
|
+
function timestamp() {
|
|
145
|
+
const value = wallClock();
|
|
146
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
147
|
+
if (Number.isNaN(date.getTime())) throw new TypeError('Progress wall clock returned an invalid date.');
|
|
148
|
+
return date.toISOString();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function elapsedSince(startedAt) {
|
|
152
|
+
if (startedAt === null) return 0;
|
|
153
|
+
return Math.max(0, Math.round(monotonicClock() - startedAt));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function emit(type, {
|
|
157
|
+
stage = null,
|
|
158
|
+
state = null,
|
|
159
|
+
status = null,
|
|
160
|
+
reasonCode = null,
|
|
161
|
+
completed,
|
|
162
|
+
total
|
|
163
|
+
} = {}) {
|
|
164
|
+
if (!EVENT_TYPE_SET.has(type)) throw new TypeError(`Unknown progress event type ${type}.`);
|
|
165
|
+
const counts = normalizeCount(
|
|
166
|
+
completed === undefined ? state?.completed : completed,
|
|
167
|
+
total === undefined ? state?.total : total
|
|
168
|
+
);
|
|
169
|
+
const event = cloneAndFreeze({
|
|
170
|
+
eventVersion: PROGRESS_EVENT_VERSION,
|
|
171
|
+
sequence: ++sequence,
|
|
172
|
+
type,
|
|
173
|
+
stageId: stage?.id ?? null,
|
|
174
|
+
stageLabel: stage?.label ?? null,
|
|
175
|
+
activityKind: state?.activityKind ?? null,
|
|
176
|
+
subject: state?.subject ?? null,
|
|
177
|
+
completed: counts.completed,
|
|
178
|
+
total: counts.total,
|
|
179
|
+
elapsedMs: elapsedSince(stage ? state?.startedAt : runStartedAt),
|
|
180
|
+
status,
|
|
181
|
+
reasonCode: constrainedCode(reasonCode, REASON_CODE_PATTERN, 'reasonCode'),
|
|
182
|
+
timestamp: timestamp()
|
|
183
|
+
});
|
|
184
|
+
safeNotify(listener, event);
|
|
185
|
+
return event;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function clearHeartbeat() {
|
|
189
|
+
heartbeatToken += 1;
|
|
190
|
+
if (heartbeatTimer !== null) {
|
|
191
|
+
scheduler.clearTimeout(heartbeatTimer);
|
|
192
|
+
heartbeatTimer = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function scheduleHeartbeat(stageId) {
|
|
197
|
+
clearHeartbeat();
|
|
198
|
+
const token = heartbeatToken;
|
|
199
|
+
heartbeatTimer = scheduler.setTimeout(() => {
|
|
200
|
+
heartbeatTimer = null;
|
|
201
|
+
if (token !== heartbeatToken || activeStageId !== stageId || runTerminal) return;
|
|
202
|
+
const stage = stageById.get(stageId);
|
|
203
|
+
const state = stateById.get(stageId);
|
|
204
|
+
if (state.status !== 'STARTED') return;
|
|
205
|
+
emit('STAGE_HEARTBEAT', { stage, state, status: 'STARTED' });
|
|
206
|
+
scheduleHeartbeat(stageId);
|
|
207
|
+
}, heartbeatIntervalMs);
|
|
208
|
+
heartbeatTimer?.unref?.();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function requireStage(stageId) {
|
|
212
|
+
const stage = stageById.get(stageId);
|
|
213
|
+
if (!stage) throw new TypeError(`Unknown progress stage ${stageId}.`);
|
|
214
|
+
return { stage, state: stateById.get(stageId) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function requireActive(stageId) {
|
|
218
|
+
const pair = requireStage(stageId);
|
|
219
|
+
if (activeStageId !== stageId || pair.state.status !== 'STARTED') {
|
|
220
|
+
throw new Error(`Progress stage ${stageId} is not active.`);
|
|
221
|
+
}
|
|
222
|
+
return pair;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function terminal(stageId, status, type, reasonCode = null) {
|
|
226
|
+
const { stage, state } = requireActive(stageId);
|
|
227
|
+
if (TERMINAL_STATUSES.has(state.status)) {
|
|
228
|
+
throw new Error(`Progress stage ${stageId} already has a terminal event.`);
|
|
229
|
+
}
|
|
230
|
+
clearHeartbeat();
|
|
231
|
+
state.status = status;
|
|
232
|
+
activeStageId = null;
|
|
233
|
+
return emit(type, { stage, state, status, reasonCode });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
startRun() {
|
|
238
|
+
if (runStartedAt !== null) throw new Error('Progress run has already started.');
|
|
239
|
+
runStartedAt = monotonicClock();
|
|
240
|
+
emit('RUN_STARTED', { status: 'STARTED', completed: 0, total: stages.length });
|
|
241
|
+
for (const stage of stages) {
|
|
242
|
+
const state = stateById.get(stage.id);
|
|
243
|
+
state.status = 'SCHEDULED';
|
|
244
|
+
emit('STAGE_SCHEDULED', {
|
|
245
|
+
stage,
|
|
246
|
+
state,
|
|
247
|
+
status: 'SCHEDULED',
|
|
248
|
+
completed: null,
|
|
249
|
+
total: null
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
startStage(stageId) {
|
|
254
|
+
if (runStartedAt === null || runTerminal) throw new Error('Progress run is not active.');
|
|
255
|
+
if (activeStageId !== null) throw new Error(`Progress stage ${activeStageId} is already active.`);
|
|
256
|
+
const { stage, state } = requireStage(stageId);
|
|
257
|
+
if (state.status !== 'SCHEDULED') throw new Error(`Progress stage ${stageId} is not scheduled.`);
|
|
258
|
+
state.status = 'STARTED';
|
|
259
|
+
state.startedAt = monotonicClock();
|
|
260
|
+
activeStageId = stageId;
|
|
261
|
+
const event = emit('STAGE_STARTED', { stage, state, status: 'STARTED' });
|
|
262
|
+
scheduleHeartbeat(stageId);
|
|
263
|
+
return event;
|
|
264
|
+
},
|
|
265
|
+
activity(stageId, {
|
|
266
|
+
activityKind,
|
|
267
|
+
subject = null,
|
|
268
|
+
completed,
|
|
269
|
+
total
|
|
270
|
+
}) {
|
|
271
|
+
const { stage, state } = requireActive(stageId);
|
|
272
|
+
const normalizedKind = constrainedCode(activityKind, ACTIVITY_KIND_PATTERN, 'activityKind');
|
|
273
|
+
if (normalizedKind === null) throw new TypeError('activityKind is required.');
|
|
274
|
+
const normalizedSubject = sanitizeProgressSubject(subject);
|
|
275
|
+
const counts = normalizeCount(completed, total);
|
|
276
|
+
state.activityKind = normalizedKind;
|
|
277
|
+
state.subject = normalizedSubject;
|
|
278
|
+
state.completed = counts.completed;
|
|
279
|
+
state.total = counts.total;
|
|
280
|
+
const event = emit('STAGE_ACTIVITY', { stage, state, status: 'STARTED' });
|
|
281
|
+
scheduleHeartbeat(stageId);
|
|
282
|
+
return event;
|
|
283
|
+
},
|
|
284
|
+
completeStage(stageId) {
|
|
285
|
+
return terminal(stageId, 'COMPLETED', 'STAGE_COMPLETED');
|
|
286
|
+
},
|
|
287
|
+
failStage(stageId, reasonCode = 'STAGE_FAILED') {
|
|
288
|
+
return terminal(stageId, 'FAILED', 'STAGE_FAILED', reasonCode);
|
|
289
|
+
},
|
|
290
|
+
cancelStage(stageId, reasonCode = 'USER_CANCELLED') {
|
|
291
|
+
return terminal(stageId, 'CANCELLED', 'STAGE_CANCELLED', reasonCode);
|
|
292
|
+
},
|
|
293
|
+
skipStage(stageId, reasonCode) {
|
|
294
|
+
const { stage, state } = requireStage(stageId);
|
|
295
|
+
if (state.status !== 'SCHEDULED') throw new Error(`Progress stage ${stageId} is not scheduled.`);
|
|
296
|
+
state.status = 'SKIPPED';
|
|
297
|
+
return emit('STAGE_SKIPPED', { stage, state, status: 'SKIPPED', reasonCode });
|
|
298
|
+
},
|
|
299
|
+
completeRun() {
|
|
300
|
+
if (activeStageId !== null || runTerminal) throw new Error('Progress run cannot complete.');
|
|
301
|
+
runTerminal = true;
|
|
302
|
+
clearHeartbeat();
|
|
303
|
+
return emit('RUN_COMPLETED', {
|
|
304
|
+
status: 'COMPLETED',
|
|
305
|
+
completed: stages.length,
|
|
306
|
+
total: stages.length
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
failRun() {
|
|
310
|
+
if (activeStageId !== null || runTerminal) throw new Error('Progress run cannot fail.');
|
|
311
|
+
runTerminal = true;
|
|
312
|
+
clearHeartbeat();
|
|
313
|
+
const completed = [...stateById.values()].filter((state) => state.status === 'COMPLETED').length;
|
|
314
|
+
return emit('RUN_FAILED', { status: 'FAILED', completed, total: stages.length });
|
|
315
|
+
},
|
|
316
|
+
cancelRun() {
|
|
317
|
+
if (activeStageId !== null || runTerminal) throw new Error('Progress run cannot cancel.');
|
|
318
|
+
runTerminal = true;
|
|
319
|
+
clearHeartbeat();
|
|
320
|
+
const completed = [...stateById.values()].filter((state) => state.status === 'COMPLETED').length;
|
|
321
|
+
return emit('RUN_CANCELLED', { status: 'CANCELLED', completed, total: stages.length });
|
|
322
|
+
},
|
|
323
|
+
snapshot() {
|
|
324
|
+
return cloneAndFreeze({
|
|
325
|
+
activeStageId,
|
|
326
|
+
runTerminal,
|
|
327
|
+
stages: stages.map((stage) => ({
|
|
328
|
+
id: stage.id,
|
|
329
|
+
status: stateById.get(stage.id).status
|
|
330
|
+
}))
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
dispose() {
|
|
334
|
+
clearHeartbeat();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { PRODUCT_NAME } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
const MODES = new Set(['auto', 'interactive', 'plain']);
|
|
4
|
+
|
|
5
|
+
export function selectProgressMode(stream, mode) {
|
|
6
|
+
if (!MODES.has(mode)) throw new TypeError('Progress mode must be auto, interactive, or plain.');
|
|
7
|
+
return mode === 'auto' ? (stream.isTTY ? 'interactive' : 'plain') : mode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function elapsed(milliseconds) {
|
|
11
|
+
return `${(Math.max(0, milliseconds) / 1000).toFixed(1)}s`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function activityText(event) {
|
|
15
|
+
const base = event.subject ?? event.activityKind?.toLowerCase().replaceAll('_', ' ') ?? null;
|
|
16
|
+
const count = event.completed === null ? '' : ` (${event.completed}/${event.total})`;
|
|
17
|
+
return base ? `${base}${count}` : count.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function boundedLine(line, width) {
|
|
21
|
+
if (!Number.isSafeInteger(width) || width < 24 || line.length <= width) return line;
|
|
22
|
+
return `${line.slice(0, Math.max(1, width - 1))}…`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function plainLine(event) {
|
|
26
|
+
const prefix = `[${elapsed(event.elapsedMs)}]`;
|
|
27
|
+
const activity = activityText(event);
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case 'RUN_STARTED':
|
|
30
|
+
return `${prefix} RUN START stages=${event.total}`;
|
|
31
|
+
case 'STAGE_STARTED':
|
|
32
|
+
return `${prefix} STAGE START id=${event.stageId} label="${event.stageLabel}"`;
|
|
33
|
+
case 'STAGE_ACTIVITY':
|
|
34
|
+
return `${prefix} STAGE ACTIVITY id=${event.stageId} kind=${event.activityKind}${activity ? ` detail="${activity}"` : ''}`;
|
|
35
|
+
case 'STAGE_HEARTBEAT':
|
|
36
|
+
return `${prefix} STAGE HEARTBEAT id=${event.stageId}${activity ? ` detail="${activity}"` : ''}`;
|
|
37
|
+
case 'STAGE_COMPLETED':
|
|
38
|
+
return `${prefix} STAGE COMPLETE id=${event.stageId} label="${event.stageLabel}"`;
|
|
39
|
+
case 'STAGE_FAILED':
|
|
40
|
+
return `${prefix} STAGE FAILED id=${event.stageId} reason=${event.reasonCode}`;
|
|
41
|
+
case 'STAGE_SKIPPED':
|
|
42
|
+
return null;
|
|
43
|
+
case 'STAGE_CANCELLED':
|
|
44
|
+
return `${prefix} STAGE CANCELLED id=${event.stageId} reason=${event.reasonCode}`;
|
|
45
|
+
case 'RUN_COMPLETED':
|
|
46
|
+
return `${prefix} RUN COMPLETE completed=${event.completed}/${event.total} failed=0 skipped=0`;
|
|
47
|
+
case 'RUN_FAILED':
|
|
48
|
+
return `${prefix} RUN FAILED completed=${event.completed}/${event.total} next=REVIEW_FAILURE_DETAILS`;
|
|
49
|
+
case 'RUN_CANCELLED':
|
|
50
|
+
return `${prefix} RUN CANCELLED completed=${event.completed}/${event.total} next=RERUN_WHEN_READY`;
|
|
51
|
+
default:
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function interactiveLine(event, noColor) {
|
|
57
|
+
const activity = activityText(event);
|
|
58
|
+
const marker = noColor
|
|
59
|
+
? {
|
|
60
|
+
STAGE_STARTED: 'START',
|
|
61
|
+
STAGE_ACTIVITY: 'WORKING',
|
|
62
|
+
STAGE_HEARTBEAT: 'WAITING',
|
|
63
|
+
STAGE_COMPLETED: 'DONE',
|
|
64
|
+
STAGE_FAILED: 'FAILED',
|
|
65
|
+
STAGE_CANCELLED: 'CANCELLED'
|
|
66
|
+
}[event.type]
|
|
67
|
+
: {
|
|
68
|
+
STAGE_STARTED: '●',
|
|
69
|
+
STAGE_ACTIVITY: ' ↳',
|
|
70
|
+
STAGE_HEARTBEAT: ' …',
|
|
71
|
+
STAGE_COMPLETED: '✓',
|
|
72
|
+
STAGE_FAILED: '✗',
|
|
73
|
+
STAGE_CANCELLED: '■'
|
|
74
|
+
}[event.type];
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case 'RUN_STARTED':
|
|
77
|
+
return `Running ${PRODUCT_NAME} Analysis (${event.total} stages)...`;
|
|
78
|
+
case 'STAGE_STARTED':
|
|
79
|
+
return `${marker} ${event.stageLabel} [${elapsed(event.elapsedMs)}]`;
|
|
80
|
+
case 'STAGE_ACTIVITY':
|
|
81
|
+
return `${marker} ${event.stageLabel} — ${activity} [${elapsed(event.elapsedMs)}]`;
|
|
82
|
+
case 'STAGE_HEARTBEAT':
|
|
83
|
+
return `${marker} ${event.stageLabel} — ${activity || 'Still running'} [${elapsed(event.elapsedMs)}]`;
|
|
84
|
+
case 'STAGE_COMPLETED':
|
|
85
|
+
return `${marker} ${event.stageLabel} completed [${elapsed(event.elapsedMs)}]`;
|
|
86
|
+
case 'STAGE_FAILED':
|
|
87
|
+
return `${marker} ${event.stageLabel} failed [${elapsed(event.elapsedMs)}] reason=${event.reasonCode}`;
|
|
88
|
+
case 'STAGE_CANCELLED':
|
|
89
|
+
return `${marker} ${event.stageLabel} cancelled [${elapsed(event.elapsedMs)}]`;
|
|
90
|
+
case 'RUN_COMPLETED':
|
|
91
|
+
return `Analysis completed: ${event.completed}/${event.total} stages completed.`;
|
|
92
|
+
case 'RUN_FAILED':
|
|
93
|
+
return `Analysis stopped: ${event.completed}/${event.total} stages completed. Review failure details below.`;
|
|
94
|
+
case 'RUN_CANCELLED':
|
|
95
|
+
return `Analysis cancelled: ${event.completed}/${event.total} stages completed.`;
|
|
96
|
+
default:
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Append-only renderer: interactive output is TTY-aware but never hides the cursor or
|
|
103
|
+
* rewrites previous lines, so success, failure, and cancellation need no terminal repair.
|
|
104
|
+
*/
|
|
105
|
+
export function createProgressReporter(stream, {
|
|
106
|
+
mode = 'auto',
|
|
107
|
+
noColor = false,
|
|
108
|
+
width = stream?.columns
|
|
109
|
+
} = {}) {
|
|
110
|
+
if (!stream || typeof stream.write !== 'function') {
|
|
111
|
+
throw new TypeError('Progress reporter requires a writable stream.');
|
|
112
|
+
}
|
|
113
|
+
const activeMode = selectProgressMode(stream, mode);
|
|
114
|
+
let writable = true;
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
mode: activeMode,
|
|
117
|
+
handle(event) {
|
|
118
|
+
if (!writable) return;
|
|
119
|
+
const line = activeMode === 'plain'
|
|
120
|
+
? plainLine(event)
|
|
121
|
+
: interactiveLine(event, noColor);
|
|
122
|
+
if (!line) return;
|
|
123
|
+
try {
|
|
124
|
+
stream.write(`${boundedLine(line, width)}\n`);
|
|
125
|
+
} catch {
|
|
126
|
+
// Artifact and pipeline semantics remain authoritative if presentation fails.
|
|
127
|
+
writable = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function writeTextArtifact(outputPath, contents) {
|
|
5
|
+
const target = path.resolve(outputPath);
|
|
6
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
7
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
8
|
+
let handle;
|
|
9
|
+
try {
|
|
10
|
+
handle = await open(temporary, 'w', 0o600);
|
|
11
|
+
await handle.writeFile(contents, 'utf8');
|
|
12
|
+
await handle.sync();
|
|
13
|
+
await handle.close();
|
|
14
|
+
handle = null;
|
|
15
|
+
await rename(temporary, target);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
await handle?.close().catch(() => {});
|
|
18
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/portable.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function compareText(left = '', right = '') {
|
|
2
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isPortableRelativePath(value) {
|
|
6
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('\\')) return false;
|
|
7
|
+
if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) return false;
|
|
8
|
+
return !value.split('/').includes('..');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isSorted(items, comparator) {
|
|
12
|
+
return items.every((item, index) => index === 0 || comparator(items[index - 1], item) <= 0);
|
|
13
|
+
}
|