@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,279 @@
1
+ import { canonicalJson } from '../../canonical-json.js';
2
+ import { compareText } from '../../portable.js';
3
+ import {
4
+ isMigrationChecklistCandidateError,
5
+ isMigrationChecklistTrustError,
6
+ trustValidateMigrationChecklistCandidate,
7
+ validateMigrationChecklistCandidate
8
+ } from '../ai-candidate.js';
9
+ import { validateMigrationChecklistInstructionContent } from '../grounding-policy.js';
10
+ import { buildMigrationPolicyProbeCandidate } from './dataset.js';
11
+
12
+ function same(left, right) {
13
+ return canonicalJson(left) === canonicalJson(right);
14
+ }
15
+
16
+ function normalize(value) {
17
+ return String(value).toLowerCase();
18
+ }
19
+
20
+ function lineEndings(value) {
21
+ return value.replace(/\r\n?/g, '\n');
22
+ }
23
+
24
+ function instructionSupportsConcept(instruction, concept) {
25
+ const text = normalize(instruction);
26
+ return concept.requiredTokens.every((token) => text.includes(normalize(token)))
27
+ && (concept.forbiddenTokens ?? []).every((token) => !text.includes(normalize(token)));
28
+ }
29
+
30
+ function instructionSupported(instruction, concepts) {
31
+ return concepts.some((concept) => instructionSupportsConcept(instruction, concept));
32
+ }
33
+
34
+ function itemHasExactExcerpts(item, context) {
35
+ const evidenceById = new Map(context.evidence.map((value) => [value.id, value]));
36
+ const refs = new Set(item.evidenceRefs ?? []);
37
+ const excerpts = item.supportingExcerpts ?? [];
38
+ if (refs.size === 0 || excerpts.length !== refs.size) return false;
39
+ const seen = new Set();
40
+ for (const excerpt of excerpts) {
41
+ const evidence = evidenceById.get(excerpt.evidenceRef);
42
+ if (!evidence || !refs.has(excerpt.evidenceRef) || seen.has(excerpt.evidenceRef)) return false;
43
+ seen.add(excerpt.evidenceRef);
44
+ if (!lineEndings(evidence.content).includes(lineEndings(excerpt.text))) return false;
45
+ }
46
+ return [...refs].every((ref) => seen.has(ref));
47
+ }
48
+
49
+ function classifyRawOutput(rawOutput, context) {
50
+ let candidate;
51
+ try {
52
+ candidate = validateMigrationChecklistCandidate(rawOutput);
53
+ } catch (error) {
54
+ if (!isMigrationChecklistCandidateError(error)) throw error;
55
+ return {
56
+ outcome: 'INVALID', candidate: null, trustDecision: 'NOT_EVALUATED',
57
+ trustDetailCode: error.code
58
+ };
59
+ }
60
+ if (candidate.status === 'ABSTAIN') {
61
+ return {
62
+ outcome: 'ABSTAIN', candidate, trustDecision: 'NOT_EVALUATED', trustDetailCode: null
63
+ };
64
+ }
65
+ try {
66
+ trustValidateMigrationChecklistCandidate(candidate, context);
67
+ return { outcome: 'ACTIONABLE', candidate, trustDecision: 'ACCEPTED', trustDetailCode: null };
68
+ } catch (error) {
69
+ if (!isMigrationChecklistTrustError(error)) throw error;
70
+ return {
71
+ outcome: 'ACTIONABLE', candidate, trustDecision: 'REJECTED',
72
+ trustDetailCode: error.detailCode ?? error.code
73
+ };
74
+ }
75
+ }
76
+
77
+ function obviousProhibited(instruction) {
78
+ if (validateMigrationChecklistInstructionContent(instruction).length > 0) return true;
79
+ return /\b(?:rollback|roll back|story points?|confidence\s*[:=]?\s*\d|safe to upgrade|upgrade is safe|prerequisite|required before)\b/i.test(instruction)
80
+ || /```|diff --git|\b(?:npm|pnpm|yarn|pip)\s+\S+/i.test(instruction);
81
+ }
82
+
83
+ const EVALUATION_IDENTIFIER_PATTERNS = Object.freeze([
84
+ /`([^`\n]{1,80})`/g,
85
+ /(?<![A-Za-z0-9_])(--[a-z0-9][a-z0-9-]*)\b/gi,
86
+ /\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)\b/g,
87
+ /\b([a-z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)\b/g,
88
+ /\b([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\b/g,
89
+ /\b([A-Za-z_$][\w$]*)\(\)/g
90
+ ]);
91
+
92
+ function technicalIdentifiers(value) {
93
+ const identifiers = new Set();
94
+ for (const expression of EVALUATION_IDENTIFIER_PATTERNS) {
95
+ expression.lastIndex = 0;
96
+ for (const match of value.matchAll(expression)) identifiers.add(match[1] ?? match[0]);
97
+ }
98
+ return [...identifiers];
99
+ }
100
+
101
+ function itemInventsIdentifier(item) {
102
+ const verifiedText = (item.supportingExcerpts ?? []).map((excerpt) => excerpt.text).join('\n');
103
+ return technicalIdentifiers(item.instruction).some((identifier) => !verifiedText.includes(identifier));
104
+ }
105
+
106
+ function rawSignals(raw, goldenCase, context) {
107
+ const items = raw.candidate?.items ?? [];
108
+ const refs = items.flatMap((item) => item.evidenceRefs ?? []);
109
+ const allowed = new Set(context.evidenceAllowlist);
110
+ const matchedRefs = refs.filter((ref) => allowed.has(ref)).length;
111
+ const expectedSet = new Set(goldenCase.expected.evidenceRefs);
112
+ const coveredRefs = [...expectedSet].filter((ref) => refs.includes(ref)).length;
113
+ const supportedItems = items.filter((item) => (
114
+ goldenCase.expected.actionExpected
115
+ && instructionSupported(item.instruction, goldenCase.expected.actionConcepts)
116
+ )).length;
117
+ const exactItems = items.filter((item) => itemHasExactExcerpts(item, context)).length;
118
+ const instructions = items.map((item) => item.instruction);
119
+ const trustCode = raw.trustDetailCode ?? '';
120
+ return {
121
+ itemCount: items.length,
122
+ evidenceRefCount: refs.length,
123
+ matchedEvidenceRefCount: matchedRefs,
124
+ expectedEvidenceRefCount: expectedSet.size,
125
+ coveredEvidenceRefCount: coveredRefs,
126
+ exactExcerptItemCount: exactItems,
127
+ supportedActionItemCount: supportedItems,
128
+ unsupportedActionItemCount: items.length - supportedItems,
129
+ inventedIdentifierAttemptCount: items.filter(itemInventsIdentifier).length
130
+ || (trustCode === 'IDENTIFIER_NOT_IN_EXCERPT' ? 1 : 0),
131
+ inventedUrlAttemptCount: instructions.some((value) => /https?:\/\//i.test(value)) ? 1 : 0,
132
+ prohibitedCapabilityAttemptCount: instructions.some(obviousProhibited) ? 1 : 0,
133
+ repositoryLocationAttemptCount: instructions.some((value) => (
134
+ /\b(?:src|lib|app|test|tests)\/[A-Za-z0-9_./-]+|\b[A-Za-z0-9_.-]+\.(?:jsx?|tsx?|py|java|go|rs)\b/i.test(value)
135
+ )) ? 1 : 0
136
+ };
137
+ }
138
+
139
+ function publishedSignals(generation, goldenCase, context, rawMetrics) {
140
+ const finding = generation.record.findings.find((item) => item.id === context.finding.id);
141
+ const items = finding?.items ?? [];
142
+ const aiItems = items.filter((item) => item.basis === 'AI_AUTHORED');
143
+ const locationItems = items.filter((item) => item.kind === 'REVIEW_CANDIDATE_USAGE');
144
+ const instructions = aiItems.map((item) => item.instruction);
145
+ const refsValid = aiItems.every((item) => item.evidenceRefs.every((ref) => (
146
+ context.evidenceAllowlist.includes(ref)
147
+ )));
148
+ const unsupported = aiItems.filter((item) => (
149
+ !goldenCase.expected.actionExpected
150
+ || !instructionSupported(item.instruction, goldenCase.expected.actionConcepts)
151
+ )).length;
152
+ const forbiddenClaims = goldenCase.expected.forbiddenClaims.filter((claim) => (
153
+ instructions.some((instruction) => normalize(instruction).includes(normalize(claim)))
154
+ ));
155
+ const prohibited = instructions.filter(obviousProhibited).length;
156
+ const aiOwnedLocations = aiItems.reduce((count, item) => count + item.candidateLocations.length, 0);
157
+ const publishedLocations = locationItems.flatMap((item) => item.candidateLocations)
158
+ .sort((left, right) => compareText(left.impactEvidenceId, right.impactEvidenceId)
159
+ || compareText(left.symbol, right.symbol) || compareText(left.file, right.file));
160
+ const expectedLocations = [...goldenCase.expected.locations]
161
+ .sort((left, right) => compareText(left.impactEvidenceId, right.impactEvidenceId)
162
+ || compareText(left.symbol, right.symbol) || compareText(left.file, right.file));
163
+ return {
164
+ aiItemCount: aiItems.length,
165
+ supportedActionItemCount: aiItems.length - unsupported,
166
+ unsupportedActionItemCount: unsupported,
167
+ inventedIdentifierCount: generation.outcome === 'generated'
168
+ ? rawMetrics.inventedIdentifierAttemptCount : 0,
169
+ prohibitedCapabilityCount: prohibited,
170
+ forbiddenClaims,
171
+ evidenceRefsValid: refsValid,
172
+ aiOwnedLocationCount: aiOwnedLocations,
173
+ locationsPreserved: same(publishedLocations, expectedLocations),
174
+ humanReviewCorrect: items.every((item) => item.requiresHumanReview === true),
175
+ registryLatestRecommendationLeak: context.versions.targetPolicy === 'registryLatest'
176
+ && instructions.some((value) => /recommend/i.test(value)),
177
+ unsupportedUsageSafetyLeak: context.locationEligibility.reasonCode === 'UNSUPPORTED_USAGE_COVERAGE'
178
+ && instructions.some((value) => /\b(?:unused|safe|not impacted)\b/i.test(value))
179
+ };
180
+ }
181
+
182
+ export function evaluateMigrationPolicyProbes(goldenCase, context) {
183
+ return goldenCase.policyProbes.map((probe) => {
184
+ const candidate = validateMigrationChecklistCandidate(buildMigrationPolicyProbeCandidate(probe));
185
+ let actualDecision = 'ACCEPTED';
186
+ let actualDetailCode = null;
187
+ try {
188
+ trustValidateMigrationChecklistCandidate(candidate, context);
189
+ } catch (error) {
190
+ if (!isMigrationChecklistTrustError(error)) throw error;
191
+ actualDecision = 'REJECTED';
192
+ actualDetailCode = error.detailCode ?? error.code;
193
+ }
194
+ return {
195
+ id: probe.id,
196
+ expectedDecision: probe.expectedDecision,
197
+ actualDecision,
198
+ expectedDetailCode: probe.expectedDetailCode,
199
+ actualDetailCode,
200
+ oracleSupported: probe.oracleSupported,
201
+ passed: actualDecision === probe.expectedDecision
202
+ && actualDetailCode === probe.expectedDetailCode,
203
+ coverage: [...probe.coverage].sort(compareText)
204
+ };
205
+ }).sort((left, right) => compareText(left.id, right.id));
206
+ }
207
+
208
+ export function compareMigrationEvaluationCase(goldenCase, {
209
+ context,
210
+ generation,
211
+ rawOutput,
212
+ runtimeErrorCode = null,
213
+ deterministicReplayPassed
214
+ }) {
215
+ const raw = runtimeErrorCode
216
+ ? { outcome: 'RUNTIME_FAILURE', candidate: null, trustDecision: 'NOT_EVALUATED', trustDetailCode: runtimeErrorCode }
217
+ : classifyRawOutput(rawOutput, context);
218
+ const rawMetrics = rawSignals(raw, goldenCase, context);
219
+ const published = publishedSignals(generation, goldenCase, context, rawMetrics);
220
+ const policyProbes = evaluateMigrationPolicyProbes(goldenCase, context);
221
+ const identityPreserved = generation.record.analysisResultId === context.analysisResultId
222
+ && same(generation.record.dependency, context.dependency)
223
+ && same(generation.record.versions, context.versions)
224
+ && generation.record.findings.every((finding) => finding.id === context.finding.id);
225
+ const checks = {
226
+ rawOutcome: raw.outcome === goldenCase.expected.rawOutcome,
227
+ finalOutcome: generation.outcome === goldenCase.expected.finalOutcome,
228
+ trustDecision: raw.trustDecision === goldenCase.expected.trustDecision
229
+ && raw.trustDetailCode === goldenCase.expected.expectedTrustDetailCode,
230
+ actionSupport: rawMetrics.unsupportedActionItemCount === 0 || goldenCase.expected.oracleUnsafe,
231
+ evidenceGrounding: rawMetrics.evidenceRefCount === rawMetrics.matchedEvidenceRefCount,
232
+ exactExcerpts: rawMetrics.itemCount === rawMetrics.exactExcerptItemCount,
233
+ identityPreserved,
234
+ locationsPreserved: published.locationsPreserved,
235
+ humanReviewCorrect: published.humanReviewCorrect,
236
+ aiOwnsNoLocation: published.aiOwnedLocationCount === 0,
237
+ versionUncertaintyPreserved: same(generation.record.versions, context.versions),
238
+ eligibilityCorrect: generation.outcome === 'generated'
239
+ ? generation.record.findings[0]?.eligibilityReasonCode === 'ELIGIBLE'
240
+ : generation.record.findings[0]?.eligibilityReasonCode !== 'ELIGIBLE',
241
+ noForbiddenPublishedClaim: published.forbiddenClaims.length === 0,
242
+ deterministicReplay: deterministicReplayPassed === true,
243
+ policyProbes: policyProbes.every((probe) => probe.passed)
244
+ };
245
+ return {
246
+ id: goldenCase.id,
247
+ ecosystem: goldenCase.ecosystem,
248
+ scenarioGroup: goldenCase.scenarioGroup,
249
+ coverage: [...goldenCase.coverage].sort(compareText),
250
+ expected: {
251
+ actionExpected: goldenCase.expected.actionExpected,
252
+ oracleUnsafe: goldenCase.expected.oracleUnsafe,
253
+ rawOutcome: goldenCase.expected.rawOutcome,
254
+ finalOutcome: goldenCase.expected.finalOutcome
255
+ },
256
+ raw: {
257
+ outcome: raw.outcome,
258
+ trustDecision: raw.trustDecision,
259
+ trustDetailCode: raw.trustDetailCode,
260
+ ...rawMetrics
261
+ },
262
+ published: {
263
+ outcome: generation.outcome,
264
+ ...published
265
+ },
266
+ preservation: {
267
+ identityPreserved,
268
+ locationsPreserved: published.locationsPreserved,
269
+ humanReviewCorrect: published.humanReviewCorrect,
270
+ aiOwnsNoLocation: published.aiOwnedLocationCount === 0,
271
+ versionUncertaintyPreserved: same(generation.record.versions, context.versions),
272
+ eligibilityCorrect: checks.eligibilityCorrect
273
+ },
274
+ deterministicReplayPassed: deterministicReplayPassed === true,
275
+ policyProbes,
276
+ checks,
277
+ passed: Object.values(checks).every((value) => value === true)
278
+ };
279
+ }
@@ -0,0 +1,227 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import Ajv2020 from 'ajv/dist/2020.js';
7
+
8
+ import { AI_RUNTIME_ERROR_CODES } from '../../ai-runtime-error.js';
9
+ import { canonicalJson, canonicalJsonBytes } from '../../canonical-json.js';
10
+ import { compareText } from '../../portable.js';
11
+ import { validateMigrationChecklistCandidate } from '../ai-candidate.js';
12
+ import { MIGRATION_PLANNING_TASK } from '../prompt.js';
13
+ import { validateMigrationActionCriteria } from './action-criteria.js';
14
+ import {
15
+ buildMigrationPolicyProbeCandidate,
16
+ loadMigrationEvaluationDataset,
17
+ migrationEvaluationDatasetDigest
18
+ } from './dataset.js';
19
+
20
+ export const MIGRATION_EVALUATION_DATASET_V2_VERSION = '2.0.0';
21
+ export const DEFAULT_MIGRATION_EVALUATION_DATASET_V2_PATH = 'eval/migration-planning/golden-dataset-v2.json';
22
+ export const MIGRATION_EVALUATION_FIXTURE_ROLES = Object.freeze([
23
+ 'LIVE_QUALITY',
24
+ 'RECORDED_CONTAINMENT',
25
+ 'INJECTED_FAILURE'
26
+ ]);
27
+
28
+ const BUNDLED_PATH = fileURLToPath(
29
+ new URL('../../../eval/migration-planning/golden-dataset-v2.json', import.meta.url)
30
+ );
31
+ const schema = JSON.parse(await readFile(
32
+ new URL('../../../schemas/migration-evaluation-dataset-v2.schema.json', import.meta.url),
33
+ 'utf8'
34
+ ));
35
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
36
+ const validateSchema = ajv.compile(schema);
37
+
38
+ function deepFreeze(value) {
39
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
40
+ for (const child of Object.values(value)) deepFreeze(child);
41
+ return Object.freeze(value);
42
+ }
43
+
44
+ function digest(value) {
45
+ return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
46
+ }
47
+
48
+ function datasetError(message) {
49
+ return new Error(`Migration Evaluation Dataset v2 error: ${message}`);
50
+ }
51
+
52
+ function same(left, right) {
53
+ return canonicalJson(left) === canonicalJson(right);
54
+ }
55
+
56
+ function duplicate(values) {
57
+ return [...new Set(values.filter((value, index) => values.indexOf(value) !== index))]
58
+ .sort(compareText);
59
+ }
60
+
61
+ function assertRoleFields(item) {
62
+ const fieldsByRole = {
63
+ LIVE_QUALITY: ['id', 'role', 'baseCaseId', 'fakeResponseSource', 'expected'],
64
+ RECORDED_CONTAINMENT: ['id', 'role', 'baseCaseId', 'recordedSource', 'recordedExpected'],
65
+ INJECTED_FAILURE: ['id', 'role', 'baseCaseId', 'injectedFailure', 'injectedExpected']
66
+ };
67
+ const expected = fieldsByRole[item.role];
68
+ if (!expected || !same(Object.keys(item).sort(compareText), [...expected].sort(compareText))) {
69
+ throw datasetError(`${item.id} contains fields incompatible with role ${item.role}.`);
70
+ }
71
+ }
72
+
73
+ function baseCaseMap(baseDataset) {
74
+ return new Map(baseDataset.cases.map((item) => [item.id, item]));
75
+ }
76
+
77
+ function resolveRecordedCandidate(item, baseCase) {
78
+ if (item.recordedSource.kind === 'BASE_RESPONSE') {
79
+ if (baseCase.response.kind !== 'candidate') {
80
+ throw datasetError(`${item.id} BASE_RESPONSE is not a candidate.`);
81
+ }
82
+ return baseCase.response.candidate;
83
+ }
84
+ if (item.recordedSource.kind === 'CUSTOM_CANDIDATE') {
85
+ return item.recordedSource.candidate;
86
+ }
87
+ const probe = baseCase.policyProbes.find((value) => value.id === item.recordedSource.probeId);
88
+ if (!probe) throw datasetError(`${item.id} references unknown base policy probe ${item.recordedSource.probeId}.`);
89
+ return buildMigrationPolicyProbeCandidate(probe);
90
+ }
91
+
92
+ function validateBaseIdentity(dataset, baseDataset) {
93
+ if (dataset.baseDataset.datasetId !== baseDataset.datasetId
94
+ || dataset.baseDataset.version !== baseDataset.schemaVersion
95
+ || dataset.baseDataset.digest !== migrationEvaluationDatasetDigest({
96
+ schemaVersion: baseDataset.schemaVersion,
97
+ datasetId: baseDataset.datasetId,
98
+ task: baseDataset.task,
99
+ cases: structuredClone(baseDataset.cases)
100
+ })) {
101
+ throw datasetError('base dataset identity or digest does not match immutable v1.');
102
+ }
103
+ }
104
+
105
+ function validateTextSafety(dataset) {
106
+ const serialized = canonicalJson(dataset);
107
+ if (/\/(?:Users|home|private\/tmp)\//.test(serialized)
108
+ || /(?:api[_-]?key|authorization|bearer\s+[a-z0-9._-]+)/i.test(serialized)) {
109
+ throw datasetError('dataset contains a private path or secret-like field.');
110
+ }
111
+ }
112
+
113
+ export function validateMigrationEvaluationDatasetV2(dataset, baseDataset) {
114
+ if (dataset?.schemaVersion !== MIGRATION_EVALUATION_DATASET_V2_VERSION) {
115
+ throw datasetError(`unsupported schema version; expected ${MIGRATION_EVALUATION_DATASET_V2_VERSION}.`);
116
+ }
117
+ if (!baseDataset || baseDataset.schemaVersion !== '1.0.0') {
118
+ throw datasetError('immutable v1 base dataset is required for validation.');
119
+ }
120
+ if (!validateSchema(dataset)) {
121
+ throw datasetError(`schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
122
+ }
123
+ if (dataset.task !== MIGRATION_PLANNING_TASK) {
124
+ throw datasetError(`task must be ${MIGRATION_PLANNING_TASK}.`);
125
+ }
126
+ validateBaseIdentity(dataset, baseDataset);
127
+ validateTextSafety(dataset);
128
+ const ids = dataset.cases.map((item) => item.id);
129
+ const duplicates = duplicate(ids);
130
+ if (duplicates.length > 0) throw datasetError(`duplicate case id ${duplicates[0]}.`);
131
+ if (!same(ids, [...ids].sort(compareText))) throw datasetError('cases must use stable lexical ordering by id.');
132
+ const byBaseId = baseCaseMap(baseDataset);
133
+ for (const item of dataset.cases) {
134
+ assertRoleFields(item);
135
+ const baseCase = byBaseId.get(item.baseCaseId);
136
+ if (!baseCase) throw datasetError(`${item.id} references unknown base case ${item.baseCaseId}.`);
137
+ if (item.role === 'LIVE_QUALITY') {
138
+ if (baseCase.response.kind !== 'candidate') {
139
+ throw datasetError(`${item.id} LIVE_QUALITY requires a safe candidate/abstention fake response.`);
140
+ }
141
+ validateMigrationActionCriteria(item.expected.actionCriteria);
142
+ if (item.expected.actionExpected !== (item.expected.actionCriteria.actions.length > 0)) {
143
+ throw datasetError(`${item.id} actionExpected is inconsistent with action criteria.`);
144
+ }
145
+ if (item.expected.actionExpected !== baseCase.expected.actionExpected) {
146
+ throw datasetError(`${item.id} changes the base case action intention.`);
147
+ }
148
+ validateMigrationChecklistCandidate(baseCase.response.candidate);
149
+ } else if (item.role === 'RECORDED_CONTAINMENT') {
150
+ validateMigrationChecklistCandidate(resolveRecordedCandidate(item, baseCase));
151
+ if (item.recordedExpected.unsafe === false
152
+ && !item.recordedExpected.coverage.includes('SAFE_CANDIDATE')) {
153
+ throw datasetError(`${item.id} safe recorded fixture must declare SAFE_CANDIDATE coverage.`);
154
+ }
155
+ } else {
156
+ if (item.injectedFailure.kind === 'RUNTIME_ERROR'
157
+ && !AI_RUNTIME_ERROR_CODES.includes(item.injectedFailure.code)) {
158
+ throw datasetError(`${item.id} uses unknown runtime error code ${item.injectedFailure.code}.`);
159
+ }
160
+ const expectedOutcome = item.injectedFailure.kind === 'RUNTIME_ERROR' ? 'failed' : 'rejected';
161
+ if (item.injectedExpected.finalOutcome !== expectedOutcome) {
162
+ throw datasetError(`${item.id} injected failure outcome is inconsistent.`);
163
+ }
164
+ }
165
+ }
166
+ const roleCounts = Object.fromEntries(MIGRATION_EVALUATION_FIXTURE_ROLES.map((role) => [
167
+ role, dataset.cases.filter((item) => item.role === role).length
168
+ ]));
169
+ const liveCases = dataset.cases.filter((item) => item.role === 'LIVE_QUALITY');
170
+ const liveEcosystems = new Set(liveCases.map((item) => byBaseId.get(item.baseCaseId).ecosystem));
171
+ if (roleCounts.LIVE_QUALITY < 7 || roleCounts.RECORDED_CONTAINMENT < 1
172
+ || roleCounts.INJECTED_FAILURE < 3
173
+ || !['generic', 'node', 'python'].every((value) => liveEcosystems.has(value))) {
174
+ throw datasetError('dataset has insufficient role or live ecosystem coverage.');
175
+ }
176
+ return dataset;
177
+ }
178
+
179
+ export function migrationEvaluationDatasetV2Digest(dataset, baseDataset) {
180
+ validateMigrationEvaluationDatasetV2(dataset, baseDataset);
181
+ return digest(dataset);
182
+ }
183
+
184
+ export async function loadMigrationEvaluationDatasetV2(datasetPath) {
185
+ const [baseDataset, source] = await Promise.all([
186
+ loadMigrationEvaluationDataset(),
187
+ readFile(datasetPath ?? BUNDLED_PATH, 'utf8')
188
+ ]);
189
+ let parsed;
190
+ try {
191
+ parsed = JSON.parse(source);
192
+ } catch {
193
+ throw datasetError(`${datasetPath ?? BUNDLED_PATH} is not valid JSON.`);
194
+ }
195
+ validateMigrationEvaluationDatasetV2(parsed, baseDataset);
196
+ return deepFreeze({
197
+ datasetPath: path.resolve(datasetPath ?? BUNDLED_PATH),
198
+ datasetDigest: migrationEvaluationDatasetV2Digest(parsed, baseDataset),
199
+ ...structuredClone(parsed),
200
+ legacyDataset: baseDataset
201
+ });
202
+ }
203
+
204
+ export async function loadVersionedMigrationEvaluationDataset({ version = '1.0.0', datasetPath } = {}) {
205
+ if (version === '1.0.0') return loadMigrationEvaluationDataset(datasetPath);
206
+ if (version === MIGRATION_EVALUATION_DATASET_V2_VERSION) {
207
+ return loadMigrationEvaluationDatasetV2(datasetPath);
208
+ }
209
+ throw datasetError(`unsupported requested version ${version}.`);
210
+ }
211
+
212
+ export function resolveMigrationEvaluationV2Case(dataset, item) {
213
+ const baseCase = dataset.legacyDataset.cases.find((value) => value.id === item.baseCaseId);
214
+ if (!baseCase) throw datasetError(`${item.id} cannot resolve base case ${item.baseCaseId}.`);
215
+ let fixedOutput = null;
216
+ let runtimeErrorCode = null;
217
+ if (item.role === 'LIVE_QUALITY') {
218
+ fixedOutput = structuredClone(baseCase.response.candidate);
219
+ } else if (item.role === 'RECORDED_CONTAINMENT') {
220
+ fixedOutput = structuredClone(resolveRecordedCandidate(item, baseCase));
221
+ } else if (item.injectedFailure.kind === 'RUNTIME_ERROR') {
222
+ runtimeErrorCode = item.injectedFailure.code;
223
+ } else {
224
+ fixedOutput = structuredClone(item.injectedFailure.output);
225
+ }
226
+ return deepFreeze({ baseCase, fixedOutput, runtimeErrorCode });
227
+ }