@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,244 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { canonicalJsonBytes } from '../../canonical-json.js';
4
+ import { compareText } from '../../portable.js';
5
+
6
+ export const MIGRATION_ACTION_EVALUATION_CRITERIA_ID = 'migration-action-evaluation';
7
+ export const MIGRATION_ACTION_EVALUATION_CRITERIA_VERSION = '1.0.0';
8
+ export const MIGRATION_ACTION_COMPARATOR_VERSION = '2.0.0';
9
+ export const MIGRATION_ACTION_NORMALIZATION_VERSION = '1.0.0';
10
+ export const MIGRATION_ACTION_SUPPORT_STATUSES = Object.freeze([
11
+ 'SUPPORTED',
12
+ 'UNSUPPORTED',
13
+ 'AMBIGUOUS',
14
+ 'NOT_APPLICABLE'
15
+ ]);
16
+
17
+ const CRITERIA_IDENTITY = deepFreeze({
18
+ evaluationCriteriaId: MIGRATION_ACTION_EVALUATION_CRITERIA_ID,
19
+ evaluationCriteriaVersion: MIGRATION_ACTION_EVALUATION_CRITERIA_VERSION,
20
+ comparatorVersion: MIGRATION_ACTION_COMPARATOR_VERSION,
21
+ normalizationVersion: MIGRATION_ACTION_NORMALIZATION_VERSION,
22
+ matchingSemantics: {
23
+ acceptablePattern: 'allOf-and-anyOf-and-anyActionVerb',
24
+ forbiddenExpansion: 'allOf-and-anyOf',
25
+ unknownAction: 'AMBIGUOUS',
26
+ forbiddenOrWrongVersion: 'UNSUPPORTED',
27
+ specificity: 'independent-anchor-presence'
28
+ }
29
+ });
30
+
31
+ function deepFreeze(value) {
32
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
33
+ for (const child of Object.values(value)) deepFreeze(child);
34
+ return Object.freeze(value);
35
+ }
36
+
37
+ function digest(value) {
38
+ return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
39
+ }
40
+
41
+ /** Deterministic normalization only; no stemming, fuzzy matching, or runtime synonyms. */
42
+ export function normalizeMigrationActionText(value) {
43
+ return String(value)
44
+ .normalize('NFKC')
45
+ .toLowerCase()
46
+ .replace(/[`'“”‘’()[\]{}]/g, ' ')
47
+ .replace(/[^a-z0-9_.+-]+/g, ' ')
48
+ .replace(/(?<!\d)\.|\.(?!\d)/g, ' ')
49
+ .replace(/\s+/g, ' ')
50
+ .trim();
51
+ }
52
+
53
+ function includesPhrase(text, value) {
54
+ const phrase = normalizeMigrationActionText(value);
55
+ return phrase.length > 0 && ` ${text} `.includes(` ${phrase} `);
56
+ }
57
+
58
+ function matchesPattern(text, pattern) {
59
+ const allOf = pattern.allOf ?? [];
60
+ const anyOf = pattern.anyOf ?? [];
61
+ const anyActionVerb = pattern.anyActionVerb ?? [];
62
+ return allOf.every((value) => includesPhrase(text, value))
63
+ && (anyOf.length === 0 || anyOf.some((value) => includesPhrase(text, value)))
64
+ && (anyActionVerb.length === 0
65
+ || anyActionVerb.some((value) => includesPhrase(text, value)));
66
+ }
67
+
68
+ function assertTextArray(value, field, { allowEmpty = true } = {}) {
69
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0)
70
+ || value.some((item) => typeof item !== 'string' || item.length === 0)
71
+ || new Set(value).size !== value.length) {
72
+ throw new TypeError(`${field} must be ${allowEmpty ? 'a' : 'a non-empty'} unique string array.`);
73
+ }
74
+ }
75
+
76
+ function validatePattern(pattern, field) {
77
+ if (!pattern || typeof pattern !== 'object' || Array.isArray(pattern)) {
78
+ throw new TypeError(`${field} must be an object.`);
79
+ }
80
+ const allowed = new Set(['allOf', 'anyOf', 'anyActionVerb']);
81
+ const unknown = Object.keys(pattern).filter((key) => !allowed.has(key));
82
+ if (unknown.length > 0) throw new TypeError(`${field} contains unsupported field ${unknown.sort(compareText)[0]}.`);
83
+ for (const key of allowed) assertTextArray(pattern[key] ?? [], `${field}.${key}`);
84
+ if ([...(pattern.allOf ?? []), ...(pattern.anyOf ?? []), ...(pattern.anyActionVerb ?? [])].length === 0) {
85
+ throw new TypeError(`${field} must contain at least one matching anchor.`);
86
+ }
87
+ }
88
+
89
+ function validateSpecificity(value, field) {
90
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
91
+ throw new TypeError(`${field} must be an object.`);
92
+ }
93
+ const allowed = new Set(['actionVerbs', 'sourceIdentifiers', 'targetIdentifiers', 'objectAnchors']);
94
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
95
+ if (unknown.length > 0) throw new TypeError(`${field} contains unsupported field ${unknown.sort(compareText)[0]}.`);
96
+ assertTextArray(value.actionVerbs, `${field}.actionVerbs`, { allowEmpty: false });
97
+ for (const key of ['sourceIdentifiers', 'targetIdentifiers', 'objectAnchors']) {
98
+ assertTextArray(value[key] ?? [], `${field}.${key}`);
99
+ }
100
+ }
101
+
102
+ export function validateMigrationActionCriteria(criteria) {
103
+ if (!criteria || typeof criteria !== 'object' || Array.isArray(criteria)) {
104
+ throw new TypeError('actionCriteria must be an object.');
105
+ }
106
+ const allowed = new Set(['actions', 'forbiddenExpansions', 'forbiddenModalities', 'allowedVersions']);
107
+ const unknown = Object.keys(criteria).filter((key) => !allowed.has(key));
108
+ if (unknown.length > 0) throw new TypeError(`actionCriteria contains unsupported field ${unknown.sort(compareText)[0]}.`);
109
+ if (!Array.isArray(criteria.actions)) throw new TypeError('actionCriteria.actions must be an array.');
110
+ const actionIds = criteria.actions.map((item) => item?.id);
111
+ if (actionIds.some((id) => typeof id !== 'string' || id.length === 0)
112
+ || new Set(actionIds).size !== actionIds.length) {
113
+ throw new TypeError('actionCriteria action IDs must be unique non-empty strings.');
114
+ }
115
+ for (const [index, action] of criteria.actions.entries()) {
116
+ const field = `actionCriteria.actions[${index}]`;
117
+ const actionFields = new Set(['id', 'acceptablePatterns', 'specificity']);
118
+ const actionUnknown = Object.keys(action).filter((key) => !actionFields.has(key));
119
+ if (actionUnknown.length > 0) throw new TypeError(`${field} contains unsupported field ${actionUnknown.sort(compareText)[0]}.`);
120
+ if (!Array.isArray(action.acceptablePatterns) || action.acceptablePatterns.length === 0) {
121
+ throw new TypeError(`${field}.acceptablePatterns must be a non-empty array.`);
122
+ }
123
+ action.acceptablePatterns.forEach((pattern, patternIndex) => (
124
+ validatePattern(pattern, `${field}.acceptablePatterns[${patternIndex}]`)
125
+ ));
126
+ validateSpecificity(action.specificity, `${field}.specificity`);
127
+ }
128
+ if (!Array.isArray(criteria.forbiddenExpansions)) {
129
+ throw new TypeError('actionCriteria.forbiddenExpansions must be an array.');
130
+ }
131
+ const forbiddenIds = criteria.forbiddenExpansions.map((item) => item?.id);
132
+ if (forbiddenIds.some((id) => typeof id !== 'string' || id.length === 0)
133
+ || new Set(forbiddenIds).size !== forbiddenIds.length) {
134
+ throw new TypeError('forbidden expansion IDs must be unique non-empty strings.');
135
+ }
136
+ for (const [index, expansion] of criteria.forbiddenExpansions.entries()) {
137
+ const fields = new Set(['id', 'allOf', 'anyOf']);
138
+ const unknownExpansion = Object.keys(expansion).filter((key) => !fields.has(key));
139
+ if (unknownExpansion.length > 0) {
140
+ throw new TypeError(`forbidden expansion contains unsupported field ${unknownExpansion.sort(compareText)[0]}.`);
141
+ }
142
+ validatePattern(
143
+ { allOf: expansion.allOf ?? [], anyOf: expansion.anyOf ?? [] },
144
+ `actionCriteria.forbiddenExpansions[${index}]`
145
+ );
146
+ }
147
+ assertTextArray(criteria.forbiddenModalities, 'actionCriteria.forbiddenModalities');
148
+ assertTextArray(criteria.allowedVersions, 'actionCriteria.allowedVersions');
149
+ return criteria;
150
+ }
151
+
152
+ function present(text, values = []) {
153
+ return values.length === 0 ? null : values.some((value) => includesPhrase(text, value));
154
+ }
155
+
156
+ function specificityFor(text, action, criteria) {
157
+ const actionVerbPresent = present(text, action.specificity.actionVerbs) === true;
158
+ const sourceIdentifierPresent = present(text, action.specificity.sourceIdentifiers);
159
+ const targetIdentifierPresent = present(text, action.specificity.targetIdentifiers);
160
+ const objectPresent = present(text, action.specificity.objectAnchors);
161
+ const versionScopePresent = present(text, criteria.allowedVersions);
162
+ const required = [
163
+ actionVerbPresent,
164
+ sourceIdentifierPresent,
165
+ targetIdentifierPresent,
166
+ objectPresent,
167
+ versionScopePresent
168
+ ].filter((value) => value !== null);
169
+ return {
170
+ status: required.every(Boolean) ? 'SPECIFIC' : 'LOW_SPECIFICITY',
171
+ actionVerbPresent,
172
+ sourceIdentifierPresent,
173
+ targetIdentifierPresent,
174
+ objectPresent,
175
+ versionScopePresent,
176
+ anchorCount: required.filter(Boolean).length,
177
+ applicableAnchorCount: required.length,
178
+ identifierSpecific: [sourceIdentifierPresent, targetIdentifierPresent]
179
+ .filter((value) => value !== null).every(Boolean)
180
+ };
181
+ }
182
+
183
+ /** Evaluate one instruction against predeclared, versioned atomic-action criteria. */
184
+ export function evaluateMigrationActionInstruction(instruction, criteria) {
185
+ validateMigrationActionCriteria(criteria);
186
+ if (criteria.actions.length === 0) {
187
+ return deepFreeze({
188
+ supportStatus: 'NOT_APPLICABLE',
189
+ reasonCode: 'NO_ACTION_EXPECTED',
190
+ matchedActionIds: [],
191
+ forbiddenExpansionIds: [],
192
+ specificity: null
193
+ });
194
+ }
195
+ const text = normalizeMigrationActionText(instruction);
196
+ const forbiddenExpansionIds = criteria.forbiddenExpansions
197
+ .filter((item) => matchesPattern(text, item)).map((item) => item.id).sort(compareText);
198
+ const forbiddenModalities = criteria.forbiddenModalities
199
+ .filter((value) => includesPhrase(text, value)).sort(compareText);
200
+ const observedVersions = [...new Set(
201
+ String(instruction).match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/g) ?? []
202
+ )].sort(compareText);
203
+ const unexpectedVersions = observedVersions.filter((value) => !criteria.allowedVersions.includes(value));
204
+ if (forbiddenExpansionIds.length > 0 || forbiddenModalities.length > 0 || unexpectedVersions.length > 0) {
205
+ return deepFreeze({
206
+ supportStatus: 'UNSUPPORTED',
207
+ reasonCode: forbiddenExpansionIds.length > 0 ? 'FORBIDDEN_EXPANSION'
208
+ : forbiddenModalities.length > 0 ? 'MODALITY_EXPANSION' : 'WRONG_VERSION_SCOPE',
209
+ matchedActionIds: [],
210
+ forbiddenExpansionIds,
211
+ forbiddenModalities,
212
+ unexpectedVersions,
213
+ specificity: null
214
+ });
215
+ }
216
+ const matched = criteria.actions.filter((action) => (
217
+ action.acceptablePatterns.some((pattern) => matchesPattern(text, pattern))
218
+ ));
219
+ if (matched.length === 0) {
220
+ return deepFreeze({
221
+ supportStatus: 'AMBIGUOUS',
222
+ reasonCode: 'NO_ACCEPTABLE_ACTION_PATTERN',
223
+ matchedActionIds: [],
224
+ forbiddenExpansionIds: [],
225
+ specificity: null
226
+ });
227
+ }
228
+ const specificity = specificityFor(text, matched[0], criteria);
229
+ return deepFreeze({
230
+ supportStatus: 'SUPPORTED',
231
+ reasonCode: 'ACCEPTABLE_ACTION_PATTERN',
232
+ matchedActionIds: matched.map((item) => item.id).sort(compareText),
233
+ forbiddenExpansionIds: [],
234
+ specificity
235
+ });
236
+ }
237
+
238
+ export function migrationActionEvaluationCriteriaIdentity() {
239
+ return structuredClone(CRITERIA_IDENTITY);
240
+ }
241
+
242
+ export function migrationActionEvaluationCriteriaDigest() {
243
+ return digest(CRITERIA_IDENTITY);
244
+ }
@@ -0,0 +1,332 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { canonicalJson } from '../../canonical-json.js';
4
+ import { compareText } from '../../portable.js';
5
+ import {
6
+ isMigrationChecklistCandidateError,
7
+ isMigrationChecklistTrustError,
8
+ trustValidateMigrationChecklistCandidate,
9
+ validateMigrationChecklistCandidate
10
+ } from '../ai-candidate.js';
11
+ import { validateMigrationChecklistInstructionContent } from '../grounding-policy.js';
12
+ import { evaluateMigrationActionInstruction } from './action-criteria.js';
13
+
14
+ const MAX_RETAINED_INSTRUCTION_CHARACTERS = 300;
15
+ const MAX_RETAINED_EXCERPT_CHARACTERS = 160;
16
+
17
+ function same(left, right) {
18
+ return canonicalJson(left) === canonicalJson(right);
19
+ }
20
+
21
+ function digest(value) {
22
+ return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`;
23
+ }
24
+
25
+ function lineEndings(value) {
26
+ return value.replace(/\r\n?/g, '\n');
27
+ }
28
+
29
+ function classifyRawOutput(rawOutput, context, runtimeErrorCode) {
30
+ if (runtimeErrorCode) {
31
+ return {
32
+ outcome: 'RUNTIME_FAILURE', candidate: null,
33
+ trustDecision: 'NOT_EVALUATED', trustDetailCode: runtimeErrorCode
34
+ };
35
+ }
36
+ let candidate;
37
+ try {
38
+ candidate = validateMigrationChecklistCandidate(rawOutput);
39
+ } catch (error) {
40
+ if (!isMigrationChecklistCandidateError(error)) throw error;
41
+ return {
42
+ outcome: 'INVALID', candidate: null,
43
+ trustDecision: 'NOT_EVALUATED', trustDetailCode: error.code
44
+ };
45
+ }
46
+ if (candidate.status === 'ABSTAIN') {
47
+ return {
48
+ outcome: 'ABSTAIN', candidate,
49
+ trustDecision: 'NOT_EVALUATED', trustDetailCode: null
50
+ };
51
+ }
52
+ try {
53
+ trustValidateMigrationChecklistCandidate(candidate, context);
54
+ return { outcome: 'ACTIONABLE', candidate, trustDecision: 'ACCEPTED', trustDetailCode: null };
55
+ } catch (error) {
56
+ if (!isMigrationChecklistTrustError(error)) throw error;
57
+ return {
58
+ outcome: 'ACTIONABLE', candidate,
59
+ trustDecision: 'REJECTED', trustDetailCode: error.detailCode ?? error.code
60
+ };
61
+ }
62
+ }
63
+
64
+ function itemHasExactExcerpts(item, context) {
65
+ const evidenceById = new Map(context.evidence.map((value) => [value.id, value]));
66
+ const refs = new Set(item.evidenceRefs ?? []);
67
+ const excerpts = item.supportingExcerpts ?? [];
68
+ if (refs.size === 0 || excerpts.length !== refs.size) return false;
69
+ const seen = new Set();
70
+ for (const excerpt of excerpts) {
71
+ const evidence = evidenceById.get(excerpt.evidenceRef);
72
+ if (!evidence || !refs.has(excerpt.evidenceRef) || seen.has(excerpt.evidenceRef)) return false;
73
+ seen.add(excerpt.evidenceRef);
74
+ if (!lineEndings(evidence.content).includes(lineEndings(excerpt.text))) return false;
75
+ }
76
+ return [...refs].every((ref) => seen.has(ref));
77
+ }
78
+
79
+ function obviousProhibited(instruction) {
80
+ return validateMigrationChecklistInstructionContent(instruction).length > 0
81
+ || /\b(?:rollback|roll back|story points?|confidence\s*[:=]?\s*\d|safe to upgrade|upgrade is safe|prerequisite|required before)\b/i.test(instruction)
82
+ || /```|diff --git|\b(?:npm|pnpm|yarn|pip)\s+\S+/i.test(instruction);
83
+ }
84
+
85
+ const IDENTIFIER_PATTERNS = Object.freeze([
86
+ /`([^`\n]{1,80})`/g,
87
+ /(?<![A-Za-z0-9_])(--[a-z0-9][a-z0-9-]*)\b/gi,
88
+ /\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)\b/g,
89
+ /\b([a-z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)\b/g,
90
+ /\b([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\b/g,
91
+ /\b([A-Za-z_$][\w$]*)\(\)/g
92
+ ]);
93
+
94
+ function technicalIdentifiers(value) {
95
+ const identifiers = new Set();
96
+ for (const expression of IDENTIFIER_PATTERNS) {
97
+ expression.lastIndex = 0;
98
+ for (const match of value.matchAll(expression)) identifiers.add(match[1] ?? match[0]);
99
+ }
100
+ return [...identifiers];
101
+ }
102
+
103
+ function itemInventsIdentifier(item) {
104
+ const excerpts = (item.supportingExcerpts ?? []).map((value) => value.text).join('\n');
105
+ return technicalIdentifiers(item.instruction).some((identifier) => !excerpts.includes(identifier));
106
+ }
107
+
108
+ function evaluationFor(item, goldenCase) {
109
+ if (goldenCase.role !== 'LIVE_QUALITY') return null;
110
+ if (!goldenCase.expected.actionExpected) {
111
+ return {
112
+ supportStatus: 'UNSUPPORTED', reasonCode: 'ACTION_NOT_EXPECTED',
113
+ matchedActionIds: [], forbiddenExpansionIds: [], specificity: null
114
+ };
115
+ }
116
+ return evaluateMigrationActionInstruction(item.instruction, goldenCase.expected.actionCriteria);
117
+ }
118
+
119
+ function counts(evaluations) {
120
+ return {
121
+ supportedActionItemCount: evaluations.filter((item) => item?.supportStatus === 'SUPPORTED').length,
122
+ unsupportedActionItemCount: evaluations.filter((item) => item?.supportStatus === 'UNSUPPORTED').length,
123
+ ambiguousActionItemCount: evaluations.filter((item) => item?.supportStatus === 'AMBIGUOUS').length,
124
+ specificActionItemCount: evaluations.filter((item) => item?.specificity?.status === 'SPECIFIC').length,
125
+ identifierSpecificItemCount: evaluations.filter((item) => item?.specificity?.identifierSpecific === true).length,
126
+ versionSpecificItemCount: evaluations.filter((item) => item?.specificity?.versionScopePresent === true).length
127
+ };
128
+ }
129
+
130
+ function rawSignals(raw, goldenCase, baseCase, context) {
131
+ const items = raw.candidate?.items ?? [];
132
+ const refs = items.flatMap((item) => item.evidenceRefs ?? []);
133
+ const allowed = new Set(context.evidenceAllowlist);
134
+ const expectedRefs = new Set(baseCase.expected.evidenceRefs);
135
+ const evaluations = items.map((item) => evaluationFor(item, goldenCase));
136
+ return {
137
+ itemCount: items.length,
138
+ evidenceRefCount: refs.length,
139
+ matchedEvidenceRefCount: refs.filter((ref) => allowed.has(ref)).length,
140
+ expectedEvidenceRefCount: expectedRefs.size,
141
+ coveredEvidenceRefCount: [...expectedRefs].filter((ref) => refs.includes(ref)).length,
142
+ exactExcerptItemCount: items.filter((item) => itemHasExactExcerpts(item, context)).length,
143
+ ...counts(evaluations),
144
+ inventedIdentifierAttemptCount: items.filter(itemInventsIdentifier).length
145
+ || (raw.trustDetailCode === 'IDENTIFIER_NOT_IN_EXCERPT' ? 1 : 0),
146
+ prohibitedCapabilityAttemptCount: items.filter((item) => obviousProhibited(item.instruction)).length,
147
+ itemEvaluations: evaluations.map((item, index) => item ? {
148
+ itemIndex: index,
149
+ supportStatus: item.supportStatus,
150
+ reasonCode: item.reasonCode,
151
+ matchedActionIds: item.matchedActionIds,
152
+ forbiddenExpansionIds: item.forbiddenExpansionIds,
153
+ specificity: item.specificity
154
+ } : null).filter(Boolean)
155
+ };
156
+ }
157
+
158
+ function publishedSignals(
159
+ generation,
160
+ goldenCase,
161
+ baseCase,
162
+ context,
163
+ rawMetrics,
164
+ publishedEvaluationInstructions = null
165
+ ) {
166
+ const finding = generation.record.findings.find((item) => item.id === context.finding.id);
167
+ const items = finding?.items ?? [];
168
+ const aiItems = items.filter((item) => item.basis === 'AI_AUTHORED');
169
+ const locationItems = items.filter((item) => item.kind === 'REVIEW_CANDIDATE_USAGE');
170
+ const evaluations = aiItems.map((item, index) => evaluationFor(
171
+ publishedEvaluationInstructions?.[index]
172
+ ? { ...item, instruction: publishedEvaluationInstructions[index] }
173
+ : item,
174
+ goldenCase
175
+ ));
176
+ const instructions = aiItems.map((item) => item.instruction);
177
+ const expectedLocations = [...baseCase.expected.locations].sort((left, right) => (
178
+ compareText(left.impactEvidenceId, right.impactEvidenceId)
179
+ || compareText(left.symbol, right.symbol) || compareText(left.file, right.file)
180
+ ));
181
+ const publishedLocations = locationItems.flatMap((item) => item.candidateLocations)
182
+ .sort((left, right) => compareText(left.impactEvidenceId, right.impactEvidenceId)
183
+ || compareText(left.symbol, right.symbol) || compareText(left.file, right.file));
184
+ return {
185
+ aiItemCount: aiItems.length,
186
+ ...counts(evaluations),
187
+ evidenceRefsValid: aiItems.every((item) => item.evidenceRefs.every((ref) => (
188
+ context.evidenceAllowlist.includes(ref)
189
+ ))),
190
+ inventedIdentifierCount: generation.outcome === 'generated'
191
+ ? rawMetrics.inventedIdentifierAttemptCount : 0,
192
+ prohibitedCapabilityCount: instructions.filter(obviousProhibited).length,
193
+ aiOwnedLocationCount: aiItems.reduce((total, item) => total + item.candidateLocations.length, 0),
194
+ locationsPreserved: same(publishedLocations, expectedLocations),
195
+ humanReviewCorrect: items.every((item) => item.requiresHumanReview === true),
196
+ registryLatestRecommendationLeak: context.versions.targetPolicy === 'registryLatest'
197
+ && instructions.some((value) => /recommend/i.test(value)),
198
+ unsupportedUsageSafetyLeak: context.locationEligibility.reasonCode === 'UNSUPPORTED_USAGE_COVERAGE'
199
+ && instructions.some((value) => /\b(?:unused|safe|not impacted)\b/i.test(value))
200
+ };
201
+ }
202
+
203
+ function boundedText(value, limit) {
204
+ if (typeof value !== 'string'
205
+ || /\/(?:Users|home|private\/tmp)\//.test(value)
206
+ || /\b(?:authorization|api[_-]?key|bearer)\s*[:=]?\s*\S+/i.test(value)) return null;
207
+ return value.length <= limit ? value : null;
208
+ }
209
+
210
+ function retainedDetails(goldenCase, raw, context, rawMetrics, published, enabled) {
211
+ if (!enabled || goldenCase.role !== 'LIVE_QUALITY') return [];
212
+ const candidateItems = raw.candidate?.items ?? [];
213
+ return rawMetrics.itemEvaluations.flatMap((evaluation) => {
214
+ const failed = evaluation.supportStatus !== 'SUPPORTED'
215
+ || evaluation.specificity?.status === 'LOW_SPECIFICITY'
216
+ || raw.trustDecision === 'REJECTED';
217
+ if (!failed) return [];
218
+ const item = candidateItems[evaluation.itemIndex];
219
+ return [{
220
+ caseId: goldenCase.id,
221
+ itemIndex: evaluation.itemIndex,
222
+ instruction: boundedText(item.instruction, MAX_RETAINED_INSTRUCTION_CHARACTERS),
223
+ instructionDigest: digest(item.instruction),
224
+ evidence: item.supportingExcerpts.map((excerpt) => ({
225
+ evidenceRef: excerpt.evidenceRef,
226
+ excerpt: boundedText(excerpt.text, MAX_RETAINED_EXCERPT_CHARACTERS),
227
+ excerptDigest: digest(excerpt.text),
228
+ locator: context.evidence.find((value) => value.id === excerpt.evidenceRef)?.locator ?? null
229
+ })),
230
+ actionCriteriaIds: evaluation.matchedActionIds,
231
+ comparatorResult: evaluation.supportStatus,
232
+ reasonCode: evaluation.reasonCode,
233
+ trustDecision: raw.trustDecision,
234
+ published: published.aiItemCount > 0
235
+ }];
236
+ });
237
+ }
238
+
239
+ export function compareMigrationEvaluationCaseV2(goldenCase, {
240
+ baseCase,
241
+ context,
242
+ generation,
243
+ rawOutput,
244
+ runtimeErrorCode = null,
245
+ rawClassification = null,
246
+ publishedEvaluationInstructions = null,
247
+ deterministicReplayPassed,
248
+ retainFailureDetails = true
249
+ }) {
250
+ const raw = rawClassification ?? classifyRawOutput(rawOutput, context, runtimeErrorCode);
251
+ const rawMetrics = rawSignals(raw, goldenCase, baseCase, context);
252
+ const published = publishedSignals(
253
+ generation,
254
+ goldenCase,
255
+ baseCase,
256
+ context,
257
+ rawMetrics,
258
+ publishedEvaluationInstructions
259
+ );
260
+ const identityPreserved = generation.record.analysisResultId === context.analysisResultId
261
+ && same(generation.record.dependency, context.dependency)
262
+ && same(generation.record.versions, context.versions)
263
+ && generation.record.findings.every((finding) => finding.id === context.finding.id);
264
+ const preservation = {
265
+ identityPreserved,
266
+ locationsPreserved: published.locationsPreserved,
267
+ humanReviewCorrect: published.humanReviewCorrect,
268
+ aiOwnsNoLocation: published.aiOwnedLocationCount === 0,
269
+ versionUncertaintyPreserved: same(generation.record.versions, context.versions),
270
+ eligibilityCorrect: generation.outcome === 'generated'
271
+ ? generation.record.findings[0]?.eligibilityReasonCode === 'ELIGIBLE'
272
+ : generation.record.findings[0]?.eligibilityReasonCode !== 'ELIGIBLE'
273
+ };
274
+ let checks;
275
+ if (goldenCase.role === 'LIVE_QUALITY') {
276
+ checks = {
277
+ expectedBehavior: goldenCase.expected.actionExpected
278
+ ? raw.outcome === 'ACTIONABLE' : raw.outcome === 'ABSTAIN',
279
+ actionSupport: rawMetrics.unsupportedActionItemCount === 0
280
+ && rawMetrics.ambiguousActionItemCount === 0,
281
+ evidenceGrounding: rawMetrics.evidenceRefCount === rawMetrics.matchedEvidenceRefCount,
282
+ exactExcerpts: rawMetrics.itemCount === rawMetrics.exactExcerptItemCount
283
+ };
284
+ } else if (goldenCase.role === 'RECORDED_CONTAINMENT') {
285
+ checks = {
286
+ trustDecision: raw.trustDecision === goldenCase.recordedExpected.expectedTrustDecision
287
+ && raw.trustDetailCode === goldenCase.recordedExpected.expectedDetailCode,
288
+ containment: goldenCase.recordedExpected.unsafe
289
+ ? published.aiItemCount === 0 : published.outcome === 'generated'
290
+ };
291
+ } else {
292
+ checks = {
293
+ expectedFailure: generation.outcome === goldenCase.injectedExpected.finalOutcome
294
+ && raw.trustDetailCode === goldenCase.injectedExpected.detailCode,
295
+ failClosed: published.aiItemCount === 0
296
+ };
297
+ }
298
+ const sharedChecks = {
299
+ identityPreserved,
300
+ locationsPreserved: preservation.locationsPreserved,
301
+ humanReviewCorrect: preservation.humanReviewCorrect,
302
+ aiOwnsNoLocation: preservation.aiOwnsNoLocation,
303
+ versionUncertaintyPreserved: preservation.versionUncertaintyPreserved,
304
+ eligibilityCorrect: preservation.eligibilityCorrect,
305
+ deterministicReplay: deterministicReplayPassed === true
306
+ };
307
+ const allChecks = { ...checks, ...sharedChecks };
308
+ return {
309
+ id: goldenCase.id,
310
+ role: goldenCase.role,
311
+ ecosystem: baseCase.ecosystem,
312
+ expected: goldenCase.role === 'LIVE_QUALITY'
313
+ ? { actionExpected: goldenCase.expected.actionExpected }
314
+ : goldenCase.role === 'RECORDED_CONTAINMENT'
315
+ ? structuredClone(goldenCase.recordedExpected)
316
+ : structuredClone(goldenCase.injectedExpected),
317
+ raw: {
318
+ outcome: raw.outcome,
319
+ trustDecision: raw.trustDecision,
320
+ trustDetailCode: raw.trustDetailCode,
321
+ ...rawMetrics
322
+ },
323
+ published: { outcome: generation.outcome, ...published },
324
+ preservation,
325
+ deterministicReplayPassed: deterministicReplayPassed === true,
326
+ checks: allChecks,
327
+ retainedFailureDetails: retainedDetails(
328
+ goldenCase, raw, context, rawMetrics, published, retainFailureDetails
329
+ ),
330
+ passed: Object.values(allChecks).every((value) => value === true)
331
+ };
332
+ }