@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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/bin/depverdict.js +7 -0
  4. package/bin/upgradelens.js +7 -0
  5. package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
  6. package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
  7. package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
  8. package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
  9. package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
  10. package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
  11. package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
  12. package/docs/IA-04-CLI-Orchestration.md +235 -0
  13. package/docs/IA-05-Real-Provider-Validation.md +235 -0
  14. package/docs/IA-05-VinGrade-Validation.md +339 -0
  15. package/docs/MVP-01.md +48 -0
  16. package/docs/MVP-02-Architecture.md +536 -0
  17. package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
  18. package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
  19. package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
  20. package/docs/MVP-02-Knowledge-Manifest.md +279 -0
  21. package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
  22. package/docs/MVP-02-Knowledge-Store.md +87 -0
  23. package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
  24. package/docs/MVP-02-Research-Planning.md +196 -0
  25. package/docs/MVP-02-Source-Provenance.md +81 -0
  26. package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
  27. package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
  28. package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
  29. package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
  30. package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
  31. package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
  32. package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
  33. package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
  34. package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
  35. package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
  36. package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
  37. package/docs/VinGrade-MVP-02-Validation.md +480 -0
  38. package/docs/VinGrade-RC02-Live-Validation.md +273 -0
  39. package/docs/ai-capability-discovery.md +573 -0
  40. package/docs/ai-engineering-production-readiness.md +390 -0
  41. package/docs/ai-engineering-review.md +251 -0
  42. package/docs/ai-runtime-governance-discovery.md +754 -0
  43. package/docs/architecture-overview.md +78 -0
  44. package/docs/cli-progress.md +111 -0
  45. package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
  46. package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
  47. package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
  48. package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
  49. package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
  50. package/docs/gateway-runtime-discovery.md +614 -0
  51. package/docs/live-ai-validation.md +299 -0
  52. package/docs/migration-planning-qualification-resolution.md +99 -0
  53. package/docs/migrations/upgradelens-to-depverdict.md +97 -0
  54. package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
  55. package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
  56. package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
  57. package/docs/mvp-05-deterministic-context-runtime.md +127 -0
  58. package/docs/mvp-05-migration-checklist-contract.md +166 -0
  59. package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
  60. package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
  61. package/docs/mvp-05-provider-neutral-generator.md +79 -0
  62. package/docs/ollama-local-smoke-validation.md +156 -0
  63. package/docs/openai-compatible-runtime-discovery.md +789 -0
  64. package/docs/openrouter-one-dependency-validation.md +205 -0
  65. package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
  66. package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
  67. package/docs/package-content-policy.md +98 -0
  68. package/docs/releases/v0.5.0-technical-preview.md +114 -0
  69. package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
  70. package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
  71. package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
  72. package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
  73. package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
  74. package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
  75. package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
  76. package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
  77. package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
  78. package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
  79. package/docs/reviews/oss-03-community-scaffolding.md +378 -0
  80. package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
  81. package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
  82. package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
  83. package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
  84. package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
  85. package/docs/runtime-contract-discovery.md +521 -0
  86. package/docs/structured-output-compatibility-report.md +100 -0
  87. package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
  88. package/docs/version-analysis-architecture.md +827 -0
  89. package/eval/README.md +86 -0
  90. package/eval/datasets/generic/declared-constraint.json +59 -0
  91. package/eval/datasets/generic/evidence-conflict.json +73 -0
  92. package/eval/datasets/generic/major-breaking-release.json +66 -0
  93. package/eval/datasets/generic/missing-evidence.json +46 -0
  94. package/eval/datasets/generic/patch-release-low.json +59 -0
  95. package/eval/datasets/node/axios-patch-low.json +59 -0
  96. package/eval/datasets/node/react-major-breaking.json +66 -0
  97. package/eval/datasets/node/react-minor-compatibility.json +66 -0
  98. package/eval/datasets/python/fastapi-deprecation.json +66 -0
  99. package/eval/datasets/python/pydantic-major-breaking.json +66 -0
  100. package/eval/migration-planning/golden-dataset-v2.json +306 -0
  101. package/eval/migration-planning/golden-dataset.json +214 -0
  102. package/eval/schemas/expected-result.schema.json +88 -0
  103. package/eval/schemas/golden-case.schema.json +181 -0
  104. package/package.json +57 -0
  105. package/schemas/ai-scorecard.schema.json +132 -0
  106. package/schemas/benchmark-report.schema.json +189 -0
  107. package/schemas/benchmark.schema.json +55 -0
  108. package/schemas/capability-profile.schema.json +44 -0
  109. package/schemas/conformance-report.schema.json +150 -0
  110. package/schemas/deployment-profile.schema.json +64 -0
  111. package/schemas/evaluation-report.schema.json +158 -0
  112. package/schemas/knowledge-evidence-bundle.schema.json +150 -0
  113. package/schemas/knowledge-manifest.schema.json +548 -0
  114. package/schemas/metrics.schema.json +178 -0
  115. package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
  116. package/schemas/migration-checklist.schema.json +706 -0
  117. package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
  118. package/schemas/migration-evaluation-dataset.schema.json +204 -0
  119. package/schemas/migration-planning-qualification-record.schema.json +234 -0
  120. package/schemas/project-manifest.schema.json +308 -0
  121. package/schemas/qualification-record.schema.json +56 -0
  122. package/schemas/repository-impact-evidence.schema.json +232 -0
  123. package/schemas/repository-impact.schema.json +202 -0
  124. package/schemas/upgrade-decision.schema.json +273 -0
  125. package/schemas/usage-index.schema.json +179 -0
  126. package/schemas/version-analysis.schema.json +449 -0
  127. package/src/ai-runtime-debug.js +325 -0
  128. package/src/ai-runtime-error.js +42 -0
  129. package/src/ai-runtime.js +174 -0
  130. package/src/ai-scorecard.js +204 -0
  131. package/src/ai-version-analysis.js +484 -0
  132. package/src/artifact-root-compatibility.js +91 -0
  133. package/src/benchmark-report.js +111 -0
  134. package/src/benchmark-runner.js +191 -0
  135. package/src/canonical-json.js +69 -0
  136. package/src/cli.js +1299 -0
  137. package/src/conformance-report.js +158 -0
  138. package/src/conformance-runner.js +253 -0
  139. package/src/constants.js +73 -0
  140. package/src/cooperative-scheduler.js +79 -0
  141. package/src/dependencies.js +44 -0
  142. package/src/dependency-ai-context.js +625 -0
  143. package/src/detectors.js +253 -0
  144. package/src/discovery.js +234 -0
  145. package/src/ecosystem-version-adapter.js +294 -0
  146. package/src/environment-compatibility.js +77 -0
  147. package/src/evaluation-comparator.js +158 -0
  148. package/src/evaluation-report.js +76 -0
  149. package/src/evaluation-runner.js +248 -0
  150. package/src/evidence-source-adapter.js +472 -0
  151. package/src/files.js +89 -0
  152. package/src/governance-diagnostics.js +64 -0
  153. package/src/governance-loader.js +63 -0
  154. package/src/governance-metadata.js +346 -0
  155. package/src/governance-validator.js +360 -0
  156. package/src/http/bounded-fetch.js +278 -0
  157. package/src/http/cli-http-runtime.js +44 -0
  158. package/src/impact/input-loader.js +157 -0
  159. package/src/impact/matcher.js +40 -0
  160. package/src/impact/repository-impact.js +199 -0
  161. package/src/impact/runtime.js +24 -0
  162. package/src/impact/status.js +62 -0
  163. package/src/impact/writer.js +30 -0
  164. package/src/impact-evidence/input-loader.js +202 -0
  165. package/src/impact-evidence/repository-impact-evidence.js +234 -0
  166. package/src/impact-evidence/runtime.js +16 -0
  167. package/src/impact-evidence/writer.js +30 -0
  168. package/src/index.js +563 -0
  169. package/src/installed-version-baseline.js +196 -0
  170. package/src/knowledge-cache.js +324 -0
  171. package/src/knowledge-evidence-bundle.js +101 -0
  172. package/src/knowledge-evidence-producer.js +233 -0
  173. package/src/knowledge-manifest-builder.js +188 -0
  174. package/src/knowledge-manifest-writer.js +32 -0
  175. package/src/knowledge-manifest.js +255 -0
  176. package/src/knowledge-research.js +615 -0
  177. package/src/metrics-engine.js +205 -0
  178. package/src/migration-checklist/ai-candidate.js +320 -0
  179. package/src/migration-checklist/assembler.js +37 -0
  180. package/src/migration-checklist/context-runtime.js +828 -0
  181. package/src/migration-checklist/evaluation/action-criteria.js +244 -0
  182. package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
  183. package/src/migration-checklist/evaluation/comparator.js +279 -0
  184. package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
  185. package/src/migration-checklist/evaluation/dataset.js +336 -0
  186. package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
  187. package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
  188. package/src/migration-checklist/evaluation/metrics.js +158 -0
  189. package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
  190. package/src/migration-checklist/evaluation/qualification.js +239 -0
  191. package/src/migration-checklist/evaluation/runner-v2.js +294 -0
  192. package/src/migration-checklist/evaluation/runner.js +194 -0
  193. package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
  194. package/src/migration-checklist/evaluation/scorecard.js +86 -0
  195. package/src/migration-checklist/extractive-candidate.js +166 -0
  196. package/src/migration-checklist/extractive-prompt.js +62 -0
  197. package/src/migration-checklist/generator.js +702 -0
  198. package/src/migration-checklist/grounding-policy.js +117 -0
  199. package/src/migration-checklist/input-loader.js +613 -0
  200. package/src/migration-checklist/migration-checklist.js +635 -0
  201. package/src/migration-checklist/presentation.js +292 -0
  202. package/src/migration-checklist/progress.js +96 -0
  203. package/src/migration-checklist/prompt.js +83 -0
  204. package/src/migration-checklist/qualification-guard.js +462 -0
  205. package/src/migration-checklist/qualification-resolution.js +122 -0
  206. package/src/migration-checklist/qualification-store.js +225 -0
  207. package/src/migration-checklist/runtime.js +205 -0
  208. package/src/migration-checklist/verification.js +134 -0
  209. package/src/migration-checklist/writer.js +35 -0
  210. package/src/openai-compatible-provider.js +451 -0
  211. package/src/orchestration/failure-log.js +32 -0
  212. package/src/orchestration/pipeline.js +200 -0
  213. package/src/orchestration/progress-events.js +337 -0
  214. package/src/orchestration/progress-reporter.js +131 -0
  215. package/src/orchestration/text-writer.js +22 -0
  216. package/src/portable.js +13 -0
  217. package/src/product-completion.js +249 -0
  218. package/src/project-manifest-input.js +90 -0
  219. package/src/project-manifest.js +141 -0
  220. package/src/python-requirements.js +137 -0
  221. package/src/registry/npm-packument.js +256 -0
  222. package/src/registry/npm-registry-adapter.js +262 -0
  223. package/src/registry/pypi-project.js +300 -0
  224. package/src/registry/pypi-registry-adapter.js +235 -0
  225. package/src/registry/sanitize-registry-body.js +51 -0
  226. package/src/renderers/console.js +160 -0
  227. package/src/renderers/impact-presentation.js +278 -0
  228. package/src/renderers/markdown.js +172 -0
  229. package/src/research-plan.js +455 -0
  230. package/src/runtime-conformance.js +275 -0
  231. package/src/source-provenance.js +393 -0
  232. package/src/source-url.js +62 -0
  233. package/src/structured-output-schema.js +66 -0
  234. package/src/target-selector.js +306 -0
  235. package/src/upgrade-decision/input-loader.js +107 -0
  236. package/src/upgrade-decision/presentation.js +43 -0
  237. package/src/upgrade-decision/runtime.js +21 -0
  238. package/src/upgrade-decision/upgrade-decision.js +626 -0
  239. package/src/upgrade-decision/writer.js +30 -0
  240. package/src/usage/analyzer-registry.js +63 -0
  241. package/src/usage/coverage.js +116 -0
  242. package/src/usage/input-loader.js +139 -0
  243. package/src/usage/js/analyzer.js +240 -0
  244. package/src/usage/js/parser.js +21 -0
  245. package/src/usage/runtime.js +187 -0
  246. package/src/usage/scope.js +44 -0
  247. package/src/usage/source-files.js +50 -0
  248. package/src/usage/usage-index.js +217 -0
  249. package/src/usage/writer.js +31 -0
  250. package/src/version-analysis-loader.js +203 -0
  251. package/src/version-analysis-manifest.js +314 -0
  252. 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
+ }