@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,91 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_OUTPUT_DIRECTORY } from './constants.js';
|
|
5
|
+
import { isPortableRelativePath } from './portable.js';
|
|
6
|
+
|
|
7
|
+
export const LEGACY_OUTPUT_DIRECTORY = '.upgradelens';
|
|
8
|
+
|
|
9
|
+
export class ArtifactRootConflictError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(
|
|
12
|
+
'Artifact root conflict: the required input chain is partial or split between '
|
|
13
|
+
+ `${DEFAULT_OUTPUT_DIRECTORY}/ and ${LEGACY_OUTPUT_DIRECTORY}/.`
|
|
14
|
+
);
|
|
15
|
+
this.name = 'ArtifactRootConflictError';
|
|
16
|
+
this.code = 'ARTIFACT_ROOT_CONFLICT';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function legacyArtifactPath(canonicalArtifact) {
|
|
21
|
+
const prefix = `${DEFAULT_OUTPUT_DIRECTORY}/`;
|
|
22
|
+
if (!isPortableRelativePath(canonicalArtifact) || !canonicalArtifact.startsWith(prefix)) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`Canonical artifact must be below ${DEFAULT_OUTPUT_DIRECTORY}/.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return `${LEGACY_OUTPUT_DIRECTORY}/${canonicalArtifact.slice(prefix.length)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function exists(filename) {
|
|
31
|
+
try {
|
|
32
|
+
await access(filename);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return false;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Select one complete persisted artifact root. No returned chain ever mixes the
|
|
42
|
+
* canonical and legacy directories.
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveArtifactRootChain(
|
|
45
|
+
repositoryRoot,
|
|
46
|
+
canonicalArtifacts,
|
|
47
|
+
options = {}
|
|
48
|
+
) {
|
|
49
|
+
if (!Array.isArray(canonicalArtifacts) || canonicalArtifacts.length === 0) {
|
|
50
|
+
throw new TypeError('canonicalArtifacts must be a non-empty array.');
|
|
51
|
+
}
|
|
52
|
+
const canonical = [...canonicalArtifacts];
|
|
53
|
+
const legacy = canonical.map(legacyArtifactPath);
|
|
54
|
+
const [canonicalState, legacyState] = await Promise.all([
|
|
55
|
+
Promise.all(canonical.map((artifact) => exists(path.resolve(repositoryRoot, artifact)))),
|
|
56
|
+
Promise.all(legacy.map((artifact) => exists(path.resolve(repositoryRoot, artifact))))
|
|
57
|
+
]);
|
|
58
|
+
const canonicalAll = canonicalState.every(Boolean);
|
|
59
|
+
const canonicalAny = canonicalState.some(Boolean);
|
|
60
|
+
const legacyAll = legacyState.every(Boolean);
|
|
61
|
+
const legacyAny = legacyState.some(Boolean);
|
|
62
|
+
|
|
63
|
+
if (canonicalAll) {
|
|
64
|
+
if (legacyAny) {
|
|
65
|
+
options.onDiagnostic?.(
|
|
66
|
+
`LEGACY_ARTIFACT_ROOT_IGNORED: using complete ${DEFAULT_OUTPUT_DIRECTORY}/ input chain.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
root: DEFAULT_OUTPUT_DIRECTORY,
|
|
71
|
+
artifacts: Object.freeze(canonical)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!canonicalAny && legacyAll) {
|
|
76
|
+
options.onDiagnostic?.(
|
|
77
|
+
`LEGACY_ARTIFACT_ROOT_USED: using complete deprecated ${LEGACY_OUTPUT_DIRECTORY}/ input chain.`
|
|
78
|
+
);
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
root: LEGACY_OUTPUT_DIRECTORY,
|
|
81
|
+
artifacts: Object.freeze(legacy)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (canonicalAny || legacyAny) throw new ArtifactRootConflictError();
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
root: DEFAULT_OUTPUT_DIRECTORY,
|
|
89
|
+
artifacts: Object.freeze(canonical)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
|
|
7
|
+
import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
|
|
8
|
+
import { compareText } from './portable.js';
|
|
9
|
+
|
|
10
|
+
export const BENCHMARK_REPORT_SCHEMA_VERSION = '1.0.0';
|
|
11
|
+
export const DEFAULT_BENCHMARK_REPORT_PATH = 'benchmark-report.json';
|
|
12
|
+
|
|
13
|
+
const schema = JSON.parse(await readFile(
|
|
14
|
+
new URL('../schemas/benchmark-report.schema.json', import.meta.url),
|
|
15
|
+
'utf8'
|
|
16
|
+
));
|
|
17
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
18
|
+
addFormats(ajv);
|
|
19
|
+
const validateSchema = ajv.compile(schema);
|
|
20
|
+
|
|
21
|
+
function nullableNumberCompare(left, right) {
|
|
22
|
+
if (left === null && right === null) return 0;
|
|
23
|
+
if (left === null) return 1;
|
|
24
|
+
if (right === null) return -1;
|
|
25
|
+
return left - right;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareRank(left, right) {
|
|
29
|
+
return right.metrics.overallScore - left.metrics.overallScore
|
|
30
|
+
|| right.metrics.riskAccuracy - left.metrics.riskAccuracy
|
|
31
|
+
|| right.metrics.evidenceQuality - left.metrics.evidenceQuality
|
|
32
|
+
|| nullableNumberCompare(left.performance.latencyMs, right.performance.latencyMs)
|
|
33
|
+
|| nullableNumberCompare(left.performance.estimatedCost, right.performance.estimatedCost)
|
|
34
|
+
|| compareText(left.id, right.id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rankingFor(runs) {
|
|
38
|
+
return [...runs].sort(compareRank).map((run, index) => ({
|
|
39
|
+
rank: index + 1,
|
|
40
|
+
runId: run.id,
|
|
41
|
+
overallScore: run.metrics.overallScore,
|
|
42
|
+
riskAccuracy: run.metrics.riskAccuracy,
|
|
43
|
+
evidenceQuality: run.metrics.evidenceQuality,
|
|
44
|
+
latencyMs: run.performance.latencyMs,
|
|
45
|
+
estimatedCost: run.performance.estimatedCost
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildBenchmarkReport({
|
|
50
|
+
benchmark,
|
|
51
|
+
runs,
|
|
52
|
+
generatedAt = new Date()
|
|
53
|
+
}) {
|
|
54
|
+
const sortedRuns = [...runs].sort((left, right) => compareText(left.id, right.id));
|
|
55
|
+
const report = {
|
|
56
|
+
schemaVersion: BENCHMARK_REPORT_SCHEMA_VERSION,
|
|
57
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
58
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
59
|
+
benchmark: {
|
|
60
|
+
name: benchmark.name,
|
|
61
|
+
configPath: benchmark.configPath ?? null,
|
|
62
|
+
datasetPath: benchmark.datasetPath,
|
|
63
|
+
runCount: sortedRuns.length
|
|
64
|
+
},
|
|
65
|
+
runs: sortedRuns,
|
|
66
|
+
ranking: rankingFor(sortedRuns)
|
|
67
|
+
};
|
|
68
|
+
return validateBenchmarkReport(report);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function validateBenchmarkReport(report) {
|
|
72
|
+
if (report?.schemaVersion !== BENCHMARK_REPORT_SCHEMA_VERSION) {
|
|
73
|
+
throw new Error(`Benchmark Report validation error: unsupported schema version; expected ${BENCHMARK_REPORT_SCHEMA_VERSION}.`);
|
|
74
|
+
}
|
|
75
|
+
if (!validateSchema(report)) {
|
|
76
|
+
throw new Error(`Benchmark Report validation error: schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
77
|
+
}
|
|
78
|
+
const runIds = new Set(report.runs.map((run) => run.id));
|
|
79
|
+
for (const entry of report.ranking) {
|
|
80
|
+
if (!runIds.has(entry.runId)) {
|
|
81
|
+
throw new Error(`Benchmark Report validation error: ranking references unknown run ${entry.runId}.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return report;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function serializeBenchmarkReport(report) {
|
|
88
|
+
validateBenchmarkReport(report);
|
|
89
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeBenchmarkReport(outputPath, report) {
|
|
93
|
+
const target = path.resolve(outputPath);
|
|
94
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
95
|
+
const contents = serializeBenchmarkReport(report);
|
|
96
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
97
|
+
let handle;
|
|
98
|
+
try {
|
|
99
|
+
handle = await open(temporary, 'w', 0o600);
|
|
100
|
+
await handle.writeFile(contents, 'utf8');
|
|
101
|
+
await handle.sync();
|
|
102
|
+
await handle.close();
|
|
103
|
+
handle = null;
|
|
104
|
+
await rename(temporary, target);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
await handle?.close().catch(() => {});
|
|
107
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
return target;
|
|
111
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
|
|
6
|
+
import { buildAiScorecard } from './ai-scorecard.js';
|
|
7
|
+
import {
|
|
8
|
+
buildBenchmarkReport,
|
|
9
|
+
serializeBenchmarkReport,
|
|
10
|
+
writeBenchmarkReport
|
|
11
|
+
} from './benchmark-report.js';
|
|
12
|
+
import { runEvaluation } from './evaluation-runner.js';
|
|
13
|
+
import { buildMetrics } from './metrics-engine.js';
|
|
14
|
+
import { compareText } from './portable.js';
|
|
15
|
+
|
|
16
|
+
export const BENCHMARK_SCHEMA_VERSION = '1.0.0';
|
|
17
|
+
export const DEFAULT_BENCHMARK_CONFIG_PATH = 'benchmark.json';
|
|
18
|
+
|
|
19
|
+
const schema = JSON.parse(await readFile(
|
|
20
|
+
new URL('../schemas/benchmark.schema.json', import.meta.url),
|
|
21
|
+
'utf8'
|
|
22
|
+
));
|
|
23
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
24
|
+
const validateSchema = ajv.compile(schema);
|
|
25
|
+
|
|
26
|
+
function benchmarkError(message) {
|
|
27
|
+
return new Error(`Benchmark config error: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortedUnique(values = []) {
|
|
31
|
+
return [...new Set(values)].sort(compareText);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateBenchmarkConfigInvariants(config) {
|
|
35
|
+
const ids = config.runs.map((run) => run.id);
|
|
36
|
+
const duplicateIds = sortedUnique(ids.filter((id, index) => ids.indexOf(id) !== index));
|
|
37
|
+
if (duplicateIds.length > 0) throw benchmarkError(`duplicate run id ${duplicateIds[0]}.`);
|
|
38
|
+
return config;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateBenchmarkConfig(config) {
|
|
42
|
+
if (config?.schemaVersion !== BENCHMARK_SCHEMA_VERSION) {
|
|
43
|
+
throw benchmarkError(`unsupported schema version; expected ${BENCHMARK_SCHEMA_VERSION}.`);
|
|
44
|
+
}
|
|
45
|
+
if (!validateSchema(config)) {
|
|
46
|
+
throw benchmarkError(`schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
47
|
+
}
|
|
48
|
+
return validateBenchmarkConfigInvariants(config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadBenchmarkConfig(configPath = DEFAULT_BENCHMARK_CONFIG_PATH) {
|
|
52
|
+
try {
|
|
53
|
+
return validateBenchmarkConfig(JSON.parse(await readFile(configPath, 'utf8')));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof SyntaxError) throw benchmarkError(`${configPath} is not valid JSON.`);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function numberOrNull(value) {
|
|
61
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tokenUsageFrom(response) {
|
|
65
|
+
if (Number.isFinite(response?.tokenUsage)) return response.tokenUsage;
|
|
66
|
+
if (Number.isFinite(response?.usage?.totalTokens)) return response.usage.totalTokens;
|
|
67
|
+
if (Number.isFinite(response?.usage?.total_tokens)) return response.usage.total_tokens;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function costFrom(response) {
|
|
72
|
+
if (Number.isFinite(response?.estimatedCost)) return response.estimatedCost;
|
|
73
|
+
if (Number.isFinite(response?.cost?.estimated)) return response.cost.estimated;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createPerformanceCollector() {
|
|
78
|
+
const values = {
|
|
79
|
+
latencyMs: [],
|
|
80
|
+
tokenUsage: [],
|
|
81
|
+
estimatedCost: []
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
record(response) {
|
|
85
|
+
const latencyMs = numberOrNull(response?.latencyMs);
|
|
86
|
+
const tokenUsage = numberOrNull(tokenUsageFrom(response));
|
|
87
|
+
const estimatedCost = numberOrNull(costFrom(response));
|
|
88
|
+
if (latencyMs !== null) values.latencyMs.push(latencyMs);
|
|
89
|
+
if (tokenUsage !== null) values.tokenUsage.push(tokenUsage);
|
|
90
|
+
if (estimatedCost !== null) values.estimatedCost.push(estimatedCost);
|
|
91
|
+
},
|
|
92
|
+
summary() {
|
|
93
|
+
return {
|
|
94
|
+
latencyMs: values.latencyMs.length === 0
|
|
95
|
+
? null
|
|
96
|
+
: values.latencyMs.reduce((sum, value) => sum + value, 0) / values.latencyMs.length,
|
|
97
|
+
tokenUsage: values.tokenUsage.length === 0
|
|
98
|
+
? null
|
|
99
|
+
: values.tokenUsage.reduce((sum, value) => sum + value, 0),
|
|
100
|
+
estimatedCost: values.estimatedCost.length === 0
|
|
101
|
+
? null
|
|
102
|
+
: values.estimatedCost.reduce((sum, value) => sum + value, 0)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function wrapRuntime(runtime, collector) {
|
|
109
|
+
if (!runtime) return null;
|
|
110
|
+
return {
|
|
111
|
+
async generateStructured(request) {
|
|
112
|
+
const response = await runtime.generateStructured(request);
|
|
113
|
+
collector.record(response);
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runMetrics(metrics, scorecard) {
|
|
120
|
+
return {
|
|
121
|
+
overallScore: scorecard.overallScore,
|
|
122
|
+
riskAccuracy: metrics.metrics.riskClassificationAccuracy.value,
|
|
123
|
+
humanReviewAccuracy: metrics.metrics.humanReviewAccuracy.value,
|
|
124
|
+
evidenceQuality: scorecard.categoryScores.evidenceQuality,
|
|
125
|
+
unsupportedClaimRate: metrics.metrics.unsupportedClaimRate.value,
|
|
126
|
+
deterministicPassRate: metrics.metrics.deterministicPassRate.value
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function scorecardSummary(scorecard) {
|
|
131
|
+
return {
|
|
132
|
+
overallScore: scorecard.overallScore,
|
|
133
|
+
categoryScores: structuredClone(scorecard.categoryScores),
|
|
134
|
+
recommendationCodes: scorecard.recommendations.map((item) => item.code).sort(compareText)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function defaultRuntimeFactory(run) {
|
|
139
|
+
if (run.runtime.type === 'goldenFake') return null;
|
|
140
|
+
throw new Error(`Benchmark runtime ${run.runtime.type} requires an injected runtimeFactory.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runBenchmark(configInput, {
|
|
144
|
+
configPath = null,
|
|
145
|
+
runtimeFactory = defaultRuntimeFactory,
|
|
146
|
+
runEvaluationImplementation = runEvaluation,
|
|
147
|
+
buildMetricsImplementation = buildMetrics,
|
|
148
|
+
buildAiScorecardImplementation = buildAiScorecard,
|
|
149
|
+
generatedAt = new Date()
|
|
150
|
+
} = {}) {
|
|
151
|
+
const config = validateBenchmarkConfig(configInput);
|
|
152
|
+
const runs = [];
|
|
153
|
+
for (const run of config.runs) {
|
|
154
|
+
const collector = createPerformanceCollector();
|
|
155
|
+
const runtime = await runtimeFactory(run, config);
|
|
156
|
+
const evaluationReport = await runEvaluationImplementation({
|
|
157
|
+
datasetPath: run.datasetPath ?? config.datasetPath,
|
|
158
|
+
runtime: wrapRuntime(runtime, collector),
|
|
159
|
+
model: run.model,
|
|
160
|
+
promptVersion: run.promptVersion,
|
|
161
|
+
generatedAt
|
|
162
|
+
});
|
|
163
|
+
const metrics = buildMetricsImplementation(evaluationReport, { generatedAt });
|
|
164
|
+
const scorecard = buildAiScorecardImplementation(metrics, { generatedAt });
|
|
165
|
+
runs.push({
|
|
166
|
+
id: run.id,
|
|
167
|
+
promptVersion: run.promptVersion,
|
|
168
|
+
model: structuredClone(run.model),
|
|
169
|
+
runtime: structuredClone(run.runtime),
|
|
170
|
+
datasetPath: path.resolve(run.datasetPath ?? config.datasetPath),
|
|
171
|
+
evaluation: structuredClone(evaluationReport.summary),
|
|
172
|
+
metrics: runMetrics(metrics, scorecard),
|
|
173
|
+
scorecard: scorecardSummary(scorecard),
|
|
174
|
+
performance: collector.summary()
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return buildBenchmarkReport({
|
|
178
|
+
benchmark: {
|
|
179
|
+
name: config.name,
|
|
180
|
+
configPath,
|
|
181
|
+
datasetPath: path.resolve(config.datasetPath)
|
|
182
|
+
},
|
|
183
|
+
runs,
|
|
184
|
+
generatedAt
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
serializeBenchmarkReport,
|
|
190
|
+
writeBenchmarkReport
|
|
191
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { compareText } from './portable.js';
|
|
2
|
+
|
|
3
|
+
export class CanonicalJsonError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'CanonicalJsonError';
|
|
7
|
+
this.code = 'KNOWLEDGE_CACHE_INVALID_BODY';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fail(path, message) {
|
|
12
|
+
throw new CanonicalJsonError(`Invalid JSON value at ${path}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a JSON-compatible value with code-unit-sorted object keys. Arrays
|
|
17
|
+
* retain their order, so their order remains part of the content digest.
|
|
18
|
+
*/
|
|
19
|
+
export function canonicalJson(value) {
|
|
20
|
+
const ancestors = new Set();
|
|
21
|
+
|
|
22
|
+
function serialize(current, currentPath) {
|
|
23
|
+
if (current === null) return 'null';
|
|
24
|
+
|
|
25
|
+
switch (typeof current) {
|
|
26
|
+
case 'string':
|
|
27
|
+
return JSON.stringify(current);
|
|
28
|
+
case 'boolean':
|
|
29
|
+
return current ? 'true' : 'false';
|
|
30
|
+
case 'number':
|
|
31
|
+
if (!Number.isFinite(current)) fail(currentPath, 'numbers must be finite.');
|
|
32
|
+
return JSON.stringify(current);
|
|
33
|
+
case 'undefined':
|
|
34
|
+
case 'function':
|
|
35
|
+
case 'symbol':
|
|
36
|
+
case 'bigint':
|
|
37
|
+
fail(currentPath, `${typeof current} values are not JSON-compatible.`);
|
|
38
|
+
case 'object':
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
fail(currentPath, 'unsupported value type.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ancestors.has(current)) fail(currentPath, 'circular references are not supported.');
|
|
45
|
+
ancestors.add(current);
|
|
46
|
+
try {
|
|
47
|
+
if (Array.isArray(current)) {
|
|
48
|
+
return `[${current.map((item, index) => serialize(item, `${currentPath}[${index}]`)).join(',')}]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const prototype = Object.getPrototypeOf(current);
|
|
52
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
53
|
+
fail(currentPath, 'objects must be plain JSON objects.');
|
|
54
|
+
}
|
|
55
|
+
const entries = Object.keys(current)
|
|
56
|
+
.sort(compareText)
|
|
57
|
+
.map((key) => `${JSON.stringify(key)}:${serialize(current[key], `${currentPath}.${key}`)}`);
|
|
58
|
+
return `{${entries.join(',')}}`;
|
|
59
|
+
} finally {
|
|
60
|
+
ancestors.delete(current);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return serialize(value, '$');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function canonicalJsonBytes(value) {
|
|
68
|
+
return Buffer.from(canonicalJson(value), 'utf8');
|
|
69
|
+
}
|