@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,294 @@
1
+ import { canonicalJson } from '../../canonical-json.js';
2
+ import { AiRuntimeError, isAiRuntimeError } from '../../ai-runtime-error.js';
3
+ import { compareText } from '../../portable.js';
4
+ import {
5
+ generateMigrationChecklistForContext,
6
+ generateMigrationExtractiveChecklistForContext
7
+ } from '../generator.js';
8
+ import {
9
+ MIGRATION_EXTRACTIVE_PLANNING_TASK,
10
+ MIGRATION_EXTRACTIVE_PROMPT_VERSION
11
+ } from '../extractive-prompt.js';
12
+ import { MIGRATION_PLANNING_PROMPT_VERSION, MIGRATION_PLANNING_TASK } from '../prompt.js';
13
+ import { compareMigrationEvaluationCaseV2 } from './comparator-v2.js';
14
+ import { buildMigrationEvaluationContext, loadMigrationEvaluationDataset } from './dataset.js';
15
+ import {
16
+ loadMigrationEvaluationDatasetV2,
17
+ migrationEvaluationDatasetV2Digest,
18
+ resolveMigrationEvaluationV2Case,
19
+ validateMigrationEvaluationDatasetV2
20
+ } from './dataset-v2.js';
21
+ import { computeMigrationEvaluationMetricsV2 } from './metrics-v2.js';
22
+ import {
23
+ qualifyMigrationExtractiveRuntimeV2,
24
+ qualifyMigrationPlanningRuntimeV2
25
+ } from './qualification-v2.js';
26
+ import { buildMigrationEvaluationScorecardV2 } from './scorecard-v2.js';
27
+ import {
28
+ classifyMigrationExtractiveOutput,
29
+ resolveMigrationExtractiveEvaluationV2Case
30
+ } from './extractive-fixtures-v2.js';
31
+
32
+ export const MIGRATION_EVALUATION_REPORT_V2_VERSION = '2.0.0';
33
+ export const MIGRATION_EXTRACTIVE_EVALUATION_REPORT_V2_VERSION = '2.0.0-extractive';
34
+
35
+ function deepFreeze(value) {
36
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
37
+ for (const child of Object.values(value)) deepFreeze(child);
38
+ return Object.freeze(value);
39
+ }
40
+
41
+ function isoTimestamp(value) {
42
+ const timestamp = value instanceof Date ? value.toISOString() : value;
43
+ if (typeof timestamp !== 'string' || !Number.isFinite(Date.parse(timestamp))) {
44
+ throw new TypeError('Migration evaluation v2 requires an injected Date or ISO timestamp.');
45
+ }
46
+ return timestamp;
47
+ }
48
+
49
+ function runtimeIdentity(mode, metadata = {}, fakeDefaults) {
50
+ const allowed = new Set(['provider', 'model', 'adapter']);
51
+ const unknown = Object.keys(metadata).filter((field) => !allowed.has(field));
52
+ if (unknown.length > 0) {
53
+ throw new TypeError(`Migration evaluation v2 runtime metadata contains unsupported field ${unknown.sort(compareText)[0]}.`);
54
+ }
55
+ const defaults = mode === 'fake' ? (fakeDefaults ?? {
56
+ provider: 'migration-golden-fake',
57
+ model: '2.0.0',
58
+ adapter: 'role-routed-recorded-runtime'
59
+ }) : {};
60
+ const identity = { mode, ...defaults, ...metadata };
61
+ for (const field of allowed) {
62
+ if (typeof identity[field] !== 'string' || identity[field].length === 0) {
63
+ throw new TypeError(`Migration evaluation v2 requires runtimeMetadata.${field}.`);
64
+ }
65
+ }
66
+ return identity;
67
+ }
68
+
69
+ function fixedRuntime(output, runtimeErrorCode, identity) {
70
+ return {
71
+ async generateStructured() {
72
+ if (runtimeErrorCode) {
73
+ throw new AiRuntimeError(runtimeErrorCode, 'Injected sanitized evaluation failure.', {
74
+ retryable: false
75
+ });
76
+ }
77
+ return {
78
+ output: structuredClone(output),
79
+ provider: identity.provider,
80
+ model: identity.model,
81
+ latencyMs: 0
82
+ };
83
+ }
84
+ };
85
+ }
86
+
87
+ async function evaluateCase({
88
+ dataset,
89
+ goldenCase,
90
+ activeRuntime,
91
+ identity,
92
+ promptVersion,
93
+ retainFailureDetails,
94
+ strategy
95
+ }) {
96
+ const resolved = strategy.resolveCase(dataset, goldenCase);
97
+ const context = buildMigrationEvaluationContext(resolved.baseCase);
98
+ const localRuntime = goldenCase.role === 'LIVE_QUALITY'
99
+ ? activeRuntime
100
+ : fixedRuntime(resolved.fixedOutput, resolved.runtimeErrorCode, identity);
101
+ let rawOutput;
102
+ let runtimeErrorCode = null;
103
+ const observedIdentity = {};
104
+ const capturingRuntime = {
105
+ async generateStructured(request) {
106
+ try {
107
+ const response = await localRuntime.generateStructured(request);
108
+ rawOutput = structuredClone(response?.output);
109
+ if (goldenCase.role === 'LIVE_QUALITY') {
110
+ if (typeof response?.provider === 'string') observedIdentity.provider = response.provider;
111
+ if (typeof response?.model === 'string') observedIdentity.model = response.model;
112
+ }
113
+ return response;
114
+ } catch (error) {
115
+ runtimeErrorCode = isAiRuntimeError(error) ? error.code : 'UNKNOWN';
116
+ throw error;
117
+ }
118
+ }
119
+ };
120
+ const generation = await strategy.generateContext(context, {
121
+ aiRuntime: capturingRuntime,
122
+ runId: `migration-evaluation-v2:${goldenCase.id}`,
123
+ promptVersion
124
+ });
125
+ const replay = await strategy.generateContext(context, {
126
+ aiRuntime: fixedRuntime(rawOutput, runtimeErrorCode, identity),
127
+ runId: `migration-evaluation-v2:${goldenCase.id}`,
128
+ promptVersion
129
+ });
130
+ const rawClassification = strategy.classifyRawOutput
131
+ ? strategy.classifyRawOutput(rawOutput, context, runtimeErrorCode)
132
+ : null;
133
+ return {
134
+ result: compareMigrationEvaluationCaseV2(goldenCase, {
135
+ baseCase: resolved.baseCase,
136
+ context,
137
+ generation,
138
+ rawOutput,
139
+ runtimeErrorCode,
140
+ rawClassification,
141
+ publishedEvaluationInstructions:
142
+ strategy.evaluatePublishedSupportFromSelectedSpans
143
+ && rawClassification?.outcome === 'ACTIONABLE'
144
+ ? rawClassification.candidate.items.map((item) => item.instruction)
145
+ : null,
146
+ deterministicReplayPassed: canonicalJson(generation) === canonicalJson(replay),
147
+ retainFailureDetails
148
+ }),
149
+ observedIdentity
150
+ };
151
+ }
152
+
153
+ const FREE_FORM_STRATEGY = Object.freeze({
154
+ task: MIGRATION_PLANNING_TASK,
155
+ reportVersion: MIGRATION_EVALUATION_REPORT_V2_VERSION,
156
+ defaultPromptVersion: MIGRATION_PLANNING_PROMPT_VERSION,
157
+ fakeRuntime: {
158
+ provider: 'migration-golden-fake',
159
+ model: '2.0.0',
160
+ adapter: 'role-routed-recorded-runtime'
161
+ },
162
+ resolveCase: resolveMigrationEvaluationV2Case,
163
+ generateContext: generateMigrationChecklistForContext,
164
+ classifyRawOutput: null,
165
+ qualify: qualifyMigrationPlanningRuntimeV2
166
+ });
167
+
168
+ const EXTRACTIVE_STRATEGY = Object.freeze({
169
+ task: MIGRATION_EXTRACTIVE_PLANNING_TASK,
170
+ reportVersion: MIGRATION_EXTRACTIVE_EVALUATION_REPORT_V2_VERSION,
171
+ defaultPromptVersion: MIGRATION_EXTRACTIVE_PROMPT_VERSION,
172
+ fakeRuntime: {
173
+ provider: 'migration-extractive-golden-fake',
174
+ model: '2.0.0',
175
+ adapter: 'extractive-role-routed-recorded-runtime'
176
+ },
177
+ resolveCase: resolveMigrationExtractiveEvaluationV2Case,
178
+ generateContext: generateMigrationExtractiveChecklistForContext,
179
+ classifyRawOutput: classifyMigrationExtractiveOutput,
180
+ evaluatePublishedSupportFromSelectedSpans: true,
181
+ qualify: qualifyMigrationExtractiveRuntimeV2
182
+ });
183
+
184
+ async function runMigrationEvaluationV2WithStrategy({
185
+ dataset,
186
+ datasetPath,
187
+ mode = 'fake',
188
+ runtime,
189
+ runtimeMetadata,
190
+ generatedAt,
191
+ promptVersion,
192
+ retainFailureDetails = true
193
+ }, strategy) {
194
+ if (!['fake', 'real'].includes(mode)) {
195
+ throw new TypeError('Migration evaluation v2 mode must be fake or real.');
196
+ }
197
+ if (generatedAt === undefined) {
198
+ throw new TypeError('Migration evaluation v2 requires an injected generatedAt timestamp.');
199
+ }
200
+ if (mode === 'real' && !runtime) {
201
+ throw new TypeError('Real migration evaluation v2 requires an explicitly injected AiRuntime.');
202
+ }
203
+ if (typeof retainFailureDetails !== 'boolean') {
204
+ throw new TypeError('retainFailureDetails must be boolean.');
205
+ }
206
+ const loaded = dataset ? {
207
+ ...structuredClone(dataset),
208
+ legacyDataset: dataset.legacyDataset ?? await loadMigrationEvaluationDataset()
209
+ } : await loadMigrationEvaluationDatasetV2(datasetPath);
210
+ const core = {
211
+ schemaVersion: loaded.schemaVersion,
212
+ datasetId: loaded.datasetId,
213
+ task: loaded.task,
214
+ baseDataset: structuredClone(loaded.baseDataset),
215
+ cases: structuredClone(loaded.cases)
216
+ };
217
+ validateMigrationEvaluationDatasetV2(core, loaded.legacyDataset);
218
+ const timestamp = isoTimestamp(generatedAt);
219
+ const activePromptVersion = promptVersion ?? strategy.defaultPromptVersion;
220
+ const identity = runtimeIdentity(mode, runtimeMetadata, strategy.fakeRuntime);
221
+ let providerRequestCount = 0;
222
+ const liveRuntime = mode === 'real' ? {
223
+ async generateStructured(request) {
224
+ providerRequestCount += 1;
225
+ return runtime.generateStructured(request);
226
+ }
227
+ } : null;
228
+ const cases = [];
229
+ const observedProviders = new Set();
230
+ const observedModels = new Set();
231
+ for (const goldenCase of [...loaded.cases].sort((left, right) => compareText(left.id, right.id))) {
232
+ const resolved = strategy.resolveCase(loaded, goldenCase);
233
+ const activeRuntime = goldenCase.role === 'LIVE_QUALITY'
234
+ ? (liveRuntime ?? fixedRuntime(resolved.fixedOutput, null, identity))
235
+ : null;
236
+ const evaluated = await evaluateCase({
237
+ dataset: loaded,
238
+ goldenCase,
239
+ activeRuntime,
240
+ identity,
241
+ promptVersion: activePromptVersion,
242
+ retainFailureDetails,
243
+ strategy
244
+ });
245
+ cases.push(evaluated.result);
246
+ if (evaluated.observedIdentity.provider) observedProviders.add(evaluated.observedIdentity.provider);
247
+ if (evaluated.observedIdentity.model) observedModels.add(evaluated.observedIdentity.model);
248
+ }
249
+ const observedRuntime = {
250
+ ...identity,
251
+ observedProviders: [...observedProviders].sort(compareText),
252
+ observedModels: [...observedModels].sort(compareText)
253
+ };
254
+ const metrics = computeMigrationEvaluationMetricsV2(cases, { providerRequestCount });
255
+ const datasetIdentity = {
256
+ datasetId: core.datasetId,
257
+ schemaVersion: core.schemaVersion,
258
+ datasetDigest: migrationEvaluationDatasetV2Digest(core, loaded.legacyDataset)
259
+ };
260
+ const qualification = strategy.qualify({
261
+ dataset: datasetIdentity,
262
+ metrics,
263
+ runtime: observedRuntime,
264
+ generatedAt: timestamp,
265
+ promptVersion: activePromptVersion
266
+ });
267
+ const base = {
268
+ schemaVersion: strategy.reportVersion,
269
+ generatedAt: timestamp,
270
+ task: strategy.task,
271
+ dataset: datasetIdentity,
272
+ promptVersion: activePromptVersion,
273
+ runtime: observedRuntime,
274
+ cases,
275
+ metrics,
276
+ qualification
277
+ };
278
+ return deepFreeze({ ...base, scorecard: buildMigrationEvaluationScorecardV2(base) });
279
+ }
280
+
281
+ /**
282
+ * Historical free-form v2 evaluation. Retained byte-for-byte in identity and behavior.
283
+ */
284
+ export async function runMigrationEvaluationV2(options = {}) {
285
+ return runMigrationEvaluationV2WithStrategy(options, FREE_FORM_STRATEGY);
286
+ }
287
+
288
+ /**
289
+ * Offline-first production extractive evaluation. Recorded containment and injected
290
+ * failures always remain local; only LIVE_QUALITY may reach an explicitly injected runtime.
291
+ */
292
+ export async function runMigrationExtractiveEvaluationV2(options = {}) {
293
+ return runMigrationEvaluationV2WithStrategy(options, EXTRACTIVE_STRATEGY);
294
+ }
@@ -0,0 +1,194 @@
1
+ import { canonicalJson } from '../../canonical-json.js';
2
+ import { isAiRuntimeError, AiRuntimeError } from '../../ai-runtime-error.js';
3
+ import { compareText } from '../../portable.js';
4
+ import { generateMigrationChecklistForContext } from '../generator.js';
5
+ import { MIGRATION_PLANNING_PROMPT_VERSION, MIGRATION_PLANNING_TASK } from '../prompt.js';
6
+ import { compareMigrationEvaluationCase } from './comparator.js';
7
+ import {
8
+ buildMigrationEvaluationContext,
9
+ createMigrationGoldenFakeRuntime,
10
+ loadMigrationEvaluationDataset,
11
+ migrationEvaluationDatasetDigest,
12
+ validateMigrationEvaluationDataset
13
+ } from './dataset.js';
14
+ import { computeMigrationEvaluationMetrics } from './metrics.js';
15
+ import { qualifyMigrationPlanningRuntime } from './qualification.js';
16
+ import { buildMigrationEvaluationScorecard } from './scorecard.js';
17
+
18
+ export const MIGRATION_EVALUATION_REPORT_VERSION = '1.0.0';
19
+
20
+ function deepFreeze(value) {
21
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
22
+ for (const child of Object.values(value)) deepFreeze(child);
23
+ return Object.freeze(value);
24
+ }
25
+
26
+ function coreDataset(dataset) {
27
+ return {
28
+ schemaVersion: dataset.schemaVersion,
29
+ datasetId: dataset.datasetId,
30
+ task: dataset.task,
31
+ cases: structuredClone(dataset.cases)
32
+ };
33
+ }
34
+
35
+ function isoTimestamp(value) {
36
+ const timestamp = value instanceof Date ? value.toISOString() : value;
37
+ if (typeof timestamp !== 'string' || !Number.isFinite(Date.parse(timestamp))) {
38
+ throw new TypeError('Migration evaluation generatedAt must be an injected Date or ISO timestamp.');
39
+ }
40
+ return timestamp;
41
+ }
42
+
43
+ function runtimeIdentity(mode, metadata = {}) {
44
+ const unknownFields = Object.keys(metadata).filter((field) => (
45
+ !['provider', 'model', 'adapter'].includes(field)
46
+ ));
47
+ if (unknownFields.length > 0) {
48
+ throw new TypeError(`Migration evaluation runtimeMetadata contains unsupported field ${unknownFields.sort(compareText)[0]}.`);
49
+ }
50
+ const defaults = mode === 'fake' ? {
51
+ provider: 'migration-golden-fake',
52
+ model: '1.0.0',
53
+ adapter: 'recorded-candidate-runtime'
54
+ } : {};
55
+ const identity = { mode, ...defaults, ...metadata };
56
+ for (const field of ['provider', 'model', 'adapter']) {
57
+ if (typeof identity[field] !== 'string' || identity[field].length === 0) {
58
+ throw new TypeError(`Real migration evaluation requires runtimeMetadata.${field}.`);
59
+ }
60
+ }
61
+ return identity;
62
+ }
63
+
64
+ function replayRuntime(rawOutput, runtimeErrorCode, identity) {
65
+ return {
66
+ async generateStructured() {
67
+ if (runtimeErrorCode) {
68
+ throw new AiRuntimeError(runtimeErrorCode, 'Recorded sanitized runtime failure.', {
69
+ retryable: false
70
+ });
71
+ }
72
+ return {
73
+ output: structuredClone(rawOutput),
74
+ provider: identity.provider,
75
+ model: identity.model,
76
+ latencyMs: 0
77
+ };
78
+ }
79
+ };
80
+ }
81
+
82
+ async function evaluateCase(goldenCase, activeRuntime, identity, promptVersion) {
83
+ const context = buildMigrationEvaluationContext(goldenCase);
84
+ let rawOutput;
85
+ let runtimeErrorCode = null;
86
+ const observedIdentity = {};
87
+ const capturingRuntime = {
88
+ async generateStructured(request) {
89
+ try {
90
+ const result = await activeRuntime.generateStructured(request);
91
+ rawOutput = structuredClone(result?.output);
92
+ if (typeof result?.provider === 'string') observedIdentity.provider = result.provider;
93
+ if (typeof result?.model === 'string') observedIdentity.model = result.model;
94
+ return result;
95
+ } catch (error) {
96
+ runtimeErrorCode = isAiRuntimeError(error) ? error.code : 'UNKNOWN';
97
+ throw error;
98
+ }
99
+ }
100
+ };
101
+ const generation = await generateMigrationChecklistForContext(context, {
102
+ aiRuntime: capturingRuntime,
103
+ runId: `migration-evaluation:${goldenCase.id}`,
104
+ promptVersion
105
+ });
106
+ const replay = await generateMigrationChecklistForContext(context, {
107
+ aiRuntime: replayRuntime(rawOutput, runtimeErrorCode, identity),
108
+ runId: `migration-evaluation:${goldenCase.id}`,
109
+ promptVersion
110
+ });
111
+ const result = compareMigrationEvaluationCase(goldenCase, {
112
+ context,
113
+ generation,
114
+ rawOutput,
115
+ runtimeErrorCode,
116
+ deterministicReplayPassed: canonicalJson(generation) === canonicalJson(replay)
117
+ });
118
+ return { result, observedIdentity };
119
+ }
120
+
121
+ /**
122
+ * Run the migration-specific dataset. Real providers are called only when mode is explicitly `real`.
123
+ * Raw provider payloads and raw error messages are never included in the returned report.
124
+ */
125
+ export async function runMigrationEvaluation({
126
+ dataset,
127
+ datasetPath,
128
+ mode = 'fake',
129
+ runtime,
130
+ runtimeMetadata,
131
+ generatedAt,
132
+ promptVersion = MIGRATION_PLANNING_PROMPT_VERSION
133
+ } = {}) {
134
+ if (!['fake', 'real'].includes(mode)) {
135
+ throw new TypeError('Migration evaluation mode must be fake or real.');
136
+ }
137
+ if (generatedAt === undefined) {
138
+ throw new TypeError('Migration evaluation requires an injected generatedAt timestamp.');
139
+ }
140
+ if (mode === 'real' && !runtime) {
141
+ throw new TypeError('Real migration evaluation requires an explicitly injected AiRuntime.');
142
+ }
143
+ const loaded = dataset ?? await loadMigrationEvaluationDataset(datasetPath);
144
+ const value = coreDataset(loaded);
145
+ validateMigrationEvaluationDataset(value);
146
+ if (value.task !== MIGRATION_PLANNING_TASK) {
147
+ throw new TypeError(`Migration evaluation dataset task must be ${MIGRATION_PLANNING_TASK}.`);
148
+ }
149
+ const timestamp = isoTimestamp(generatedAt);
150
+ const identity = runtimeIdentity(mode, runtimeMetadata);
151
+ const activeRuntime = runtime ?? createMigrationGoldenFakeRuntime(value);
152
+ const cases = [];
153
+ const observedProviders = new Set();
154
+ const observedModels = new Set();
155
+ for (const goldenCase of [...value.cases].sort((left, right) => compareText(left.id, right.id))) {
156
+ const evaluated = await evaluateCase(goldenCase, activeRuntime, identity, promptVersion);
157
+ cases.push(evaluated.result);
158
+ if (evaluated.observedIdentity.provider) observedProviders.add(evaluated.observedIdentity.provider);
159
+ if (evaluated.observedIdentity.model) observedModels.add(evaluated.observedIdentity.model);
160
+ }
161
+ const observedRuntime = {
162
+ ...identity,
163
+ observedProviders: [...observedProviders].sort(compareText),
164
+ observedModels: [...observedModels].sort(compareText)
165
+ };
166
+ const metrics = computeMigrationEvaluationMetrics(cases);
167
+ const datasetIdentity = {
168
+ datasetId: value.datasetId,
169
+ schemaVersion: value.schemaVersion,
170
+ datasetDigest: migrationEvaluationDatasetDigest(value)
171
+ };
172
+ const qualification = qualifyMigrationPlanningRuntime({
173
+ dataset: datasetIdentity,
174
+ metrics,
175
+ runtime: observedRuntime,
176
+ generatedAt: timestamp,
177
+ promptVersion
178
+ });
179
+ const base = {
180
+ schemaVersion: MIGRATION_EVALUATION_REPORT_VERSION,
181
+ generatedAt: timestamp,
182
+ task: MIGRATION_PLANNING_TASK,
183
+ dataset: datasetIdentity,
184
+ promptVersion,
185
+ runtime: observedRuntime,
186
+ cases,
187
+ metrics,
188
+ qualification
189
+ };
190
+ return deepFreeze({
191
+ ...base,
192
+ scorecard: buildMigrationEvaluationScorecard(base)
193
+ });
194
+ }
@@ -0,0 +1,106 @@
1
+ import { compareText } from '../../portable.js';
2
+
3
+ const METRIC_SECTIONS = Object.freeze({
4
+ liveProviderQuality: [
5
+ 'actionSupportPrecision', 'publishedUnsupportedActionRate', 'ambiguousPublishedActionRate',
6
+ 'actionSpecificityRate', 'identifierSpecificityRate', 'versionScopePreservationRate',
7
+ 'abstentionPrecision', 'abstentionRecall', 'falseAbstentionRate',
8
+ 'safeCandidateAcceptanceRate', 'liveProviderCompletionRate'
9
+ ],
10
+ trustContainment: [
11
+ 'unsafeCandidateContainmentRate', 'inventedIdentifierContainmentRate',
12
+ 'prohibitedCapabilityContainmentRate', 'invalidEvidenceContainmentRate',
13
+ 'wholeCandidateContainmentRate', 'recordedSafeCandidateAcceptanceRate'
14
+ ],
15
+ runtimeFailureHandling: [
16
+ 'injectedFailureFallbackRate', 'providerFailureIsolationRate',
17
+ 'schemaFailureContainmentRate', 'sanitizedFailureRate'
18
+ ],
19
+ sharedInvariants: [
20
+ 'stepEvidenceReferencePrecision', 'exactExcerptPassRate', 'locationPreservationRate',
21
+ 'identityPreservationRate', 'humanReviewCorrectnessRate',
22
+ 'versionUncertaintyPreservationRate', 'deterministicPostProcessingPassRate'
23
+ ]
24
+ });
25
+
26
+ function formatRate(metric) {
27
+ return metric.value === null
28
+ ? `n/a (${metric.numerator}/${metric.denominator})`
29
+ : `${(metric.value * 100).toFixed(2)}% (${metric.numerator}/${metric.denominator})`;
30
+ }
31
+
32
+ export function buildMigrationEvaluationScorecardV2(evaluation) {
33
+ const section = (name) => Object.fromEntries(METRIC_SECTIONS[name]
34
+ .filter((key) => evaluation.metrics.metrics[key])
35
+ .map((key) => [key, structuredClone(evaluation.metrics.metrics[key])]));
36
+ return Object.freeze({
37
+ schemaVersion: '2.0.0',
38
+ identity: structuredClone(evaluation.qualification.identity),
39
+ coverage: structuredClone(evaluation.metrics.coverage),
40
+ providerRequestCount: evaluation.metrics.runtime.providerRequestCount,
41
+ sections: {
42
+ liveProviderQuality: section('liveProviderQuality'),
43
+ trustContainment: section('trustContainment'),
44
+ runtimeFailureHandling: section('runtimeFailureHandling'),
45
+ sharedInvariants: section('sharedInvariants')
46
+ },
47
+ criticalGates: structuredClone(evaluation.metrics.criticalGates),
48
+ thresholdResults: structuredClone(evaluation.qualification.thresholdResults),
49
+ limitations: structuredClone(evaluation.qualification.limitations),
50
+ retainedFailureDetails: evaluation.cases.flatMap((item) => (
51
+ structuredClone(item.retainedFailureDetails)
52
+ )),
53
+ verdict: evaluation.qualification.verdict
54
+ });
55
+ }
56
+
57
+ export function renderMigrationEvaluationScorecardV2(scorecard) {
58
+ const lines = [
59
+ '# Migration Planning Evaluation Scorecard v2',
60
+ '',
61
+ `- Task: ${scorecard.identity.task}`,
62
+ `- Dataset: ${scorecard.identity.datasetId}@${scorecard.identity.datasetVersion}`,
63
+ `- Evaluation criteria: ${scorecard.identity.evaluationCriteriaId}@${scorecard.identity.evaluationCriteriaVersion}`,
64
+ `- Comparator/normalization: ${scorecard.identity.comparatorVersion}/${scorecard.identity.normalizationVersion}`,
65
+ `- Provider/model: ${scorecard.identity.runtime.provider}/${scorecard.identity.runtime.model}`,
66
+ `- Runtime mode/adapter: ${scorecard.identity.runtime.mode}/${scorecard.identity.runtime.adapter}`,
67
+ `- Real provider calls: ${scorecard.providerRequestCount}`,
68
+ `- Verdict: ${scorecard.verdict}`,
69
+ '',
70
+ '## Fixture Coverage',
71
+ '',
72
+ `- Roles: ${JSON.stringify(scorecard.coverage.roles)}`,
73
+ `- Live ecosystems: ${scorecard.coverage.ecosystems.join(', ')}`,
74
+ `- Live actionable/abstention: ${scorecard.coverage.liveActionableCases}/${scorecard.coverage.liveAbstentionCases}`,
75
+ `- Recorded unsafe/safe: ${scorecard.coverage.recordedUnsafeCases}/${scorecard.coverage.recordedSafeCases}`,
76
+ `- Injected failures: ${scorecard.coverage.injectedFailureCases}`
77
+ ];
78
+ const headings = {
79
+ liveProviderQuality: 'Live Provider Quality',
80
+ trustContainment: 'Trust Containment',
81
+ runtimeFailureHandling: 'Runtime Failure Handling',
82
+ sharedInvariants: 'Shared Invariants'
83
+ };
84
+ for (const sectionName of Object.keys(headings)) {
85
+ lines.push('', `## ${headings[sectionName]}`, '');
86
+ for (const name of Object.keys(scorecard.sections[sectionName]).sort(compareText)) {
87
+ const metric = scorecard.sections[sectionName][name];
88
+ lines.push(`- ${name}: ${formatRate(metric)}; role=${metric.caseRole}; cases=${metric.applicableCaseCount}`);
89
+ }
90
+ }
91
+ lines.push('', '## Critical Gates', '');
92
+ for (const gate of scorecard.criticalGates) {
93
+ lines.push(`- ${gate.id}: ${gate.passed ? 'PASS' : `FAIL (${gate.violations.join(', ')})`}`);
94
+ }
95
+ lines.push('', '## Thresholds', '');
96
+ for (const item of scorecard.thresholdResults) {
97
+ lines.push(`- ${item.metric}: ${item.passed ? 'PASS' : 'FAIL'}; actual=${item.actual ?? 'n/a'}; ${item.operator}=${item.threshold}`);
98
+ }
99
+ lines.push('', '## Limitations', '');
100
+ if (scorecard.limitations.length === 0) lines.push('- None.');
101
+ else for (const item of scorecard.limitations) lines.push(`- ${item.code}: ${item.message}`);
102
+ lines.push('', '## Retained Sanitized Failure Details', '');
103
+ lines.push(scorecard.retainedFailureDetails.length === 0
104
+ ? '- None.' : `- ${scorecard.retainedFailureDetails.length} bounded record(s).`);
105
+ return `${lines.join('\n')}\n`;
106
+ }
@@ -0,0 +1,86 @@
1
+ import { compareText } from '../../portable.js';
2
+
3
+ function formatRate(metric) {
4
+ return metric.value === null
5
+ ? `n/a (${metric.numerator}/${metric.denominator})`
6
+ : `${(metric.value * 100).toFixed(2)}% (${metric.numerator}/${metric.denominator})`;
7
+ }
8
+
9
+ /** Build a presentation-only scorecard from already computed results. */
10
+ export function buildMigrationEvaluationScorecard(evaluation) {
11
+ const metrics = evaluation.metrics;
12
+ return Object.freeze({
13
+ schemaVersion: '1.0.0',
14
+ identity: structuredClone(evaluation.qualification.identity),
15
+ coverage: structuredClone(metrics.coverage),
16
+ rawOutcomes: Object.fromEntries(
17
+ ['ACTIONABLE', 'ABSTAIN', 'INVALID', 'RUNTIME_FAILURE'].map((outcome) => [
18
+ outcome,
19
+ evaluation.cases.filter((item) => item.raw.outcome === outcome).length
20
+ ])
21
+ ),
22
+ trustOutcomes: Object.fromEntries(
23
+ ['ACCEPTED', 'REJECTED', 'NOT_EVALUATED'].map((outcome) => [
24
+ outcome,
25
+ evaluation.cases.filter((item) => item.raw.trustDecision === outcome).length
26
+ ])
27
+ ),
28
+ publishedOutcomes: {
29
+ generated: metrics.runtime.generated,
30
+ abstained: metrics.runtime.abstained,
31
+ rejected: metrics.runtime.rejected,
32
+ failed: metrics.runtime.failed
33
+ },
34
+ metrics: structuredClone(metrics.metrics),
35
+ criticalGates: structuredClone(metrics.criticalGates),
36
+ thresholdResults: structuredClone(evaluation.qualification.thresholdResults),
37
+ limitations: structuredClone(evaluation.qualification.limitations),
38
+ verdict: evaluation.qualification.verdict
39
+ });
40
+ }
41
+
42
+ /** Render only precomputed scorecard data; this function never derives a verdict. */
43
+ export function renderMigrationEvaluationScorecard(scorecard) {
44
+ const lines = [
45
+ '# Migration Planning Evaluation Scorecard',
46
+ '',
47
+ `- Task: ${scorecard.identity.task}`,
48
+ `- Dataset: ${scorecard.identity.datasetId}@${scorecard.identity.datasetVersion}`,
49
+ `- Provider/model: ${scorecard.identity.runtime.provider}/${scorecard.identity.runtime.model}`,
50
+ `- Runtime mode/adapter: ${scorecard.identity.runtime.mode}/${scorecard.identity.runtime.adapter}`,
51
+ `- Verdict: ${scorecard.verdict}`,
52
+ '',
53
+ '## Coverage',
54
+ '',
55
+ `- Ecosystems: ${scorecard.coverage.ecosystems.join(', ')}`,
56
+ `- Cases by ecosystem: ${JSON.stringify(scorecard.coverage.casesByEcosystem)}`,
57
+ `- Cases by scenario group: ${JSON.stringify(scorecard.coverage.scenarioGroups)}`,
58
+ `- Actionable quality cases: ${scorecard.coverage.actionableQualityCases}`,
59
+ `- Abstention quality cases: ${scorecard.coverage.abstentionQualityCases}`,
60
+ `- Adversarial policy probes: ${scorecard.coverage.adversarialPolicyProbes}`,
61
+ '',
62
+ '## Raw / Trust / Published',
63
+ '',
64
+ `- Raw: ${JSON.stringify(scorecard.rawOutcomes)}`,
65
+ `- Trust: ${JSON.stringify(scorecard.trustOutcomes)}`,
66
+ `- Published: ${JSON.stringify(scorecard.publishedOutcomes)}`,
67
+ '',
68
+ '## Metrics',
69
+ ''
70
+ ];
71
+ for (const name of Object.keys(scorecard.metrics).sort(compareText)) {
72
+ lines.push(`- ${name}: ${formatRate(scorecard.metrics[name])}`);
73
+ }
74
+ lines.push('', '## Critical Gates', '');
75
+ for (const gate of scorecard.criticalGates) {
76
+ lines.push(`- ${gate.id}: ${gate.passed ? 'PASS' : `FAIL (${gate.violations.join(', ')})`}`);
77
+ }
78
+ lines.push('', '## Thresholds', '');
79
+ for (const item of scorecard.thresholdResults) {
80
+ lines.push(`- ${item.metric}: ${item.passed ? 'PASS' : 'FAIL'}; actual=${item.actual ?? 'n/a'}; ${item.operator}=${item.threshold}`);
81
+ }
82
+ lines.push('', '## Limitations', '');
83
+ if (scorecard.limitations.length === 0) lines.push('- None.');
84
+ else for (const item of scorecard.limitations) lines.push(`- ${item.code}: ${item.message}`);
85
+ return `${lines.join('\n')}\n`;
86
+ }