@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,325 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { canonicalJsonBytes } from './canonical-json.js';
4
+ import { compareText } from './portable.js';
5
+
6
+ const MAX_DEBUG_TEXT_CHARACTERS = 500;
7
+ const MAX_SCHEMA_NODES = 10_000;
8
+ const MAX_SCHEMA_DEPTH = 64;
9
+ const SCHEMA_KEYWORDS = Object.freeze([
10
+ '$schema',
11
+ '$type',
12
+ 'type',
13
+ 'properties',
14
+ 'required',
15
+ 'additionalProperties',
16
+ 'enum',
17
+ 'const',
18
+ 'pattern',
19
+ 'format',
20
+ 'items',
21
+ 'minItems',
22
+ 'maxItems',
23
+ 'uniqueItems',
24
+ 'anyOf',
25
+ 'oneOf',
26
+ 'allOf',
27
+ '$ref',
28
+ 'definitions',
29
+ '$defs'
30
+ ]);
31
+
32
+ function digestBytes(bytes) {
33
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
34
+ }
35
+
36
+ function digestText(value) {
37
+ return digestBytes(Buffer.from(value, 'utf8'));
38
+ }
39
+
40
+ function redactDebugSecrets(value) {
41
+ return value
42
+ .replace(/\bbearer\s+[^\s"',;]+/gi, '[REDACTED_AUTH]')
43
+ .replace(/\bsk-or-v1-[a-z0-9_-]+/gi, '[REDACTED_KEY]')
44
+ .replace(/\bsk-[a-z0-9_-]+/gi, '[REDACTED_KEY]')
45
+ .replace(/\b(api[_-]?key|authorization)\s*[=:]\s*[^\s"',;&]+/gi, '$1=[REDACTED]')
46
+ .replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, '$1[REDACTED]@')
47
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[REDACTED_EMAIL]')
48
+ .replace(/\b(account(?:_id)?|billing(?:_id|_detail)?)\s*[=:]\s*[^\s"',;&]+/gi, '$1=[REDACTED]');
49
+ }
50
+
51
+ export function sanitizeDebugText(value, maxCharacters = MAX_DEBUG_TEXT_CHARACTERS) {
52
+ if (typeof value !== 'string' && typeof value !== 'number') return '';
53
+ if (!Number.isSafeInteger(maxCharacters) || maxCharacters <= 0) {
54
+ throw new TypeError('maxCharacters must be a positive integer.');
55
+ }
56
+ const normalized = String(value)
57
+ .normalize('NFKC')
58
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+ return redactDebugSecrets(normalized).slice(0, maxCharacters);
62
+ }
63
+
64
+ export function isAiRuntimeDebugEnabled(env = {}) {
65
+ const raw = env?.DEPVERDICT_AI_DEBUG ?? env?.UPGRADELENS_AI_DEBUG;
66
+ if (raw === undefined || raw === '0' || String(raw).toLowerCase() === 'false') return false;
67
+ if (raw === '1' || String(raw).toLowerCase() === 'true') return true;
68
+ throw new TypeError('DEPVERDICT_AI_DEBUG must be one of: 1, true, 0, false.');
69
+ }
70
+
71
+ function boundedSchemaSnapshot(value, state, depth = 0) {
72
+ if (value === null || typeof value !== 'object') return value;
73
+ if (depth > MAX_SCHEMA_DEPTH) {
74
+ state.truncated = true;
75
+ return '[MAX_DEPTH]';
76
+ }
77
+ if (state.seen.has(value)) return '[CIRCULAR]';
78
+ if (state.visitedNodes >= MAX_SCHEMA_NODES) {
79
+ state.truncated = true;
80
+ return '[MAX_NODES]';
81
+ }
82
+ state.seen.add(value);
83
+ state.visitedNodes += 1;
84
+ try {
85
+ if (Array.isArray(value)) {
86
+ const items = [];
87
+ for (const item of value) {
88
+ if (state.visitedEntries >= MAX_SCHEMA_NODES) {
89
+ state.truncated = true;
90
+ items.push('[MAX_ENTRIES]');
91
+ break;
92
+ }
93
+ state.visitedEntries += 1;
94
+ items.push(boundedSchemaSnapshot(item, state, depth + 1));
95
+ }
96
+ return items;
97
+ }
98
+ const entries = [];
99
+ for (const key of Object.keys(value).sort(compareText)) {
100
+ if (state.visitedEntries >= MAX_SCHEMA_NODES) {
101
+ state.truncated = true;
102
+ break;
103
+ }
104
+ state.visitedEntries += 1;
105
+ entries.push([key, boundedSchemaSnapshot(value[key], state, depth + 1)]);
106
+ }
107
+ return Object.fromEntries(entries);
108
+ } finally {
109
+ state.seen.delete(value);
110
+ }
111
+ }
112
+
113
+ function countSchemaKeywords(value) {
114
+ const keywordCounts = Object.fromEntries(SCHEMA_KEYWORDS.map((keyword) => [keyword, 0]));
115
+ const state = { seen: new WeakSet(), visitedNodes: 0, visitedEntries: 0, truncated: false };
116
+
117
+ function visit(current, depth) {
118
+ if (!current || typeof current !== 'object') return;
119
+ if (depth > MAX_SCHEMA_DEPTH || state.visitedNodes >= MAX_SCHEMA_NODES) {
120
+ state.truncated = true;
121
+ return;
122
+ }
123
+ if (state.seen.has(current)) return;
124
+ state.seen.add(current);
125
+ state.visitedNodes += 1;
126
+ try {
127
+ if (Array.isArray(current)) {
128
+ for (const item of current) {
129
+ if (state.visitedEntries >= MAX_SCHEMA_NODES) {
130
+ state.truncated = true;
131
+ break;
132
+ }
133
+ state.visitedEntries += 1;
134
+ visit(item, depth + 1);
135
+ }
136
+ return;
137
+ }
138
+ for (const key of Object.keys(current).sort(compareText)) {
139
+ if (state.visitedEntries >= MAX_SCHEMA_NODES) {
140
+ state.truncated = true;
141
+ break;
142
+ }
143
+ state.visitedEntries += 1;
144
+ if (Object.hasOwn(keywordCounts, key)) keywordCounts[key] += 1;
145
+ const child = current[key];
146
+ if (['properties', '$defs', 'definitions'].includes(key)
147
+ && child && typeof child === 'object' && !Array.isArray(child)) {
148
+ for (const schemaName of Object.keys(child).sort(compareText)) {
149
+ if (state.visitedEntries >= MAX_SCHEMA_NODES) {
150
+ state.truncated = true;
151
+ break;
152
+ }
153
+ state.visitedEntries += 1;
154
+ visit(child[schemaName], depth + 1);
155
+ }
156
+ } else {
157
+ visit(child, depth + 1);
158
+ }
159
+ }
160
+ } finally {
161
+ state.seen.delete(current);
162
+ }
163
+ }
164
+
165
+ visit(value, 0);
166
+ return { keywordCounts, visitedNodeCount: state.visitedNodes, traversalTruncated: state.truncated };
167
+ }
168
+
169
+ export function buildSchemaDiagnostics(schema) {
170
+ const snapshotState = {
171
+ seen: new WeakSet(),
172
+ visitedNodes: 0,
173
+ visitedEntries: 0,
174
+ truncated: false
175
+ };
176
+ const snapshot = boundedSchemaSnapshot(schema, snapshotState);
177
+ const canonicalBytes = canonicalJsonBytes(snapshot);
178
+ let schemaBytes;
179
+ try {
180
+ schemaBytes = Buffer.byteLength(JSON.stringify(schema), 'utf8');
181
+ } catch {
182
+ schemaBytes = canonicalBytes.length;
183
+ }
184
+ const traversal = countSchemaKeywords(schema);
185
+ const topLevelKeys = schema && typeof schema === 'object' && !Array.isArray(schema)
186
+ ? Object.keys(schema).sort(compareText).map((key) => sanitizeDebugText(key, 128))
187
+ : [];
188
+ return {
189
+ schemaDigest: digestBytes(canonicalBytes),
190
+ schemaBytes,
191
+ topLevelKeys,
192
+ propertyCount: schema?.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)
193
+ ? Object.keys(schema.properties).length
194
+ : 0,
195
+ requiredCount: Array.isArray(schema?.required) ? schema.required.length : 0,
196
+ keywordCounts: traversal.keywordCounts,
197
+ visitedNodeCount: traversal.visitedNodeCount,
198
+ traversalTruncated: snapshotState.truncated || traversal.traversalTruncated
199
+ };
200
+ }
201
+
202
+ export function parseProviderErrorDescriptor(responseText) {
203
+ try {
204
+ const body = JSON.parse(responseText);
205
+ const error = body && typeof body === 'object' ? body.error : null;
206
+ if (!error || typeof error !== 'object' || Array.isArray(error)) return Object.freeze({});
207
+ const metadata = error.metadata && typeof error.metadata === 'object' && !Array.isArray(error.metadata)
208
+ ? error.metadata
209
+ : {};
210
+ return Object.freeze(Object.fromEntries(Object.entries({
211
+ code: sanitizeDebugText(error.code),
212
+ type: sanitizeDebugText(error.type),
213
+ message: sanitizeDebugText(error.message),
214
+ metadataErrorType: sanitizeDebugText(metadata.error_type),
215
+ metadataCode: sanitizeDebugText(metadata.code),
216
+ metadataMessage: sanitizeDebugText(metadata.message)
217
+ }).filter(([, value]) => value.length > 0)));
218
+ } catch {
219
+ return Object.freeze({});
220
+ }
221
+ }
222
+
223
+ export function buildRequestDebugRecord({
224
+ provider,
225
+ endpoint,
226
+ model,
227
+ requestBody,
228
+ serializedBody,
229
+ structuredOutputMode = 'jsonSchema',
230
+ requestExtraKeys = []
231
+ }) {
232
+ const url = endpoint instanceof URL ? endpoint : new URL(endpoint);
233
+ const schema = requestBody?.response_format?.json_schema?.schema;
234
+ const diagnostics = buildSchemaDiagnostics(schema);
235
+ return {
236
+ event: 'ai.runtime.request',
237
+ provider: sanitizeDebugText(provider, 128),
238
+ endpoint: {
239
+ protocol: url.protocol,
240
+ hostname: url.hostname,
241
+ pathname: url.pathname
242
+ },
243
+ model: sanitizeDebugText(model, 256),
244
+ method: 'POST',
245
+ stream: requestBody?.stream === true,
246
+ messageCount: Array.isArray(requestBody?.messages) ? requestBody.messages.length : 0,
247
+ messages: (requestBody?.messages ?? []).map((message) => ({
248
+ role: sanitizeDebugText(message?.role, 32),
249
+ characterCount: typeof message?.content === 'string' ? message.content.length : 0,
250
+ sha256: digestText(typeof message?.content === 'string' ? message.content : '')
251
+ })),
252
+ structuredOutput: {
253
+ mode: structuredOutputMode,
254
+ name: sanitizeDebugText(requestBody?.response_format?.json_schema?.name, 128),
255
+ strict: requestBody?.response_format?.json_schema?.strict === true,
256
+ schemaDigest: diagnostics.schemaDigest,
257
+ schemaBytes: diagnostics.schemaBytes,
258
+ schemaKeywords: diagnostics.topLevelKeys
259
+ },
260
+ schemaDiagnostics: {
261
+ propertyCount: diagnostics.propertyCount,
262
+ requiredCount: diagnostics.requiredCount,
263
+ keywordCounts: diagnostics.keywordCounts,
264
+ visitedNodeCount: diagnostics.visitedNodeCount,
265
+ traversalTruncated: diagnostics.traversalTruncated
266
+ },
267
+ requestBodyBytes: Buffer.byteLength(serializedBody, 'utf8'),
268
+ requestExtraKeys: [...requestExtraKeys].sort(compareText).map((key) => sanitizeDebugText(key, 128))
269
+ };
270
+ }
271
+
272
+ export function buildResponseDebugRecord({
273
+ status,
274
+ contentType,
275
+ responseText,
276
+ body,
277
+ assistantContent,
278
+ requestedModel,
279
+ actualModel,
280
+ usage,
281
+ latencyMs
282
+ }) {
283
+ return {
284
+ event: 'ai.runtime.response',
285
+ status,
286
+ contentType: sanitizeDebugText(contentType, 128),
287
+ responseBytes: Buffer.byteLength(responseText, 'utf8'),
288
+ requestedModel: sanitizeDebugText(requestedModel, 256),
289
+ actualModel: actualModel === null ? null : sanitizeDebugText(actualModel, 256),
290
+ choiceCount: Array.isArray(body?.choices) ? body.choices.length : 0,
291
+ finishReason: sanitizeDebugText(body?.choices?.[0]?.finish_reason, 64),
292
+ usage,
293
+ latencyMs,
294
+ assistantContentChars: assistantContent.length,
295
+ assistantContentDigest: digestText(assistantContent)
296
+ };
297
+ }
298
+
299
+ export function buildErrorDebugRecord({
300
+ status,
301
+ runtimeError,
302
+ providerError,
303
+ responseText,
304
+ latencyMs
305
+ }) {
306
+ return {
307
+ event: 'ai.runtime.error',
308
+ status,
309
+ runtimeCode: runtimeError.code,
310
+ retryable: runtimeError.retryable,
311
+ providerError: Object.fromEntries(Object.entries(providerError).map(([key, value]) => [
312
+ key,
313
+ sanitizeDebugText(value)
314
+ ])),
315
+ responseBytes: Buffer.byteLength(responseText, 'utf8'),
316
+ latencyMs
317
+ };
318
+ }
319
+
320
+ export function writeAiRuntimeDebugRecord(writer, record) {
321
+ const line = `${JSON.stringify(record)}\n`;
322
+ if (typeof writer === 'function') writer(line);
323
+ else if (writer && typeof writer.write === 'function') writer.write(line);
324
+ else throw new TypeError('AI runtime debug writer must be a function or writable stream.');
325
+ }
@@ -0,0 +1,42 @@
1
+ export const AI_RUNTIME_ERROR_CODES = Object.freeze([
2
+ 'CONFIGURATION_ERROR',
3
+ 'AUTH_ERROR',
4
+ 'ACCESS_DENIED',
5
+ 'INSUFFICIENT_CREDIT',
6
+ 'INVALID_REQUEST',
7
+ 'MODEL_NOT_FOUND',
8
+ 'NETWORK_ERROR',
9
+ 'TIMEOUT',
10
+ 'CANCELLED',
11
+ 'RATE_LIMITED',
12
+ 'PROVIDER_UNAVAILABLE',
13
+ 'SCHEMA_REJECTED',
14
+ 'STRUCTURED_OUTPUT_UNSUPPORTED',
15
+ 'INVALID_RESPONSE',
16
+ 'OUTPUT_TRUNCATED',
17
+ 'CONTENT_REFUSED',
18
+ 'RESPONSE_TOO_LARGE',
19
+ 'PROVIDER_ERROR',
20
+ 'IDENTITY_MISMATCH',
21
+ 'UNKNOWN'
22
+ ]);
23
+
24
+ const ERROR_CODE_SET = new Set(AI_RUNTIME_ERROR_CODES);
25
+
26
+ export class AiRuntimeError extends Error {
27
+ constructor(code, message, { status, retryable = false } = {}) {
28
+ if (!ERROR_CODE_SET.has(code)) throw new TypeError('Unknown AI runtime error code.');
29
+ super(message);
30
+ this.name = 'AiRuntimeError';
31
+ this.code = code;
32
+ this.retryable = retryable === true;
33
+ if (Number.isInteger(status)) this.status = status;
34
+ }
35
+ }
36
+
37
+ export function isAiRuntimeError(error) {
38
+ return error instanceof AiRuntimeError
39
+ && ERROR_CODE_SET.has(error.code)
40
+ && typeof error.message === 'string'
41
+ && typeof error.retryable === 'boolean';
42
+ }
@@ -0,0 +1,174 @@
1
+ export const AI_RUNTIME_CONTRACT_VERSION = '1';
2
+
3
+ /**
4
+ * @typedef {object} AiRuntimeRequest
5
+ * @property {string} runId
6
+ * @property {string} contextId
7
+ * @property {string} task
8
+ * @property {string} promptVersion
9
+ * @property {string} systemPrompt
10
+ * @property {string} userPrompt
11
+ * @property {{ mode: string, name: string, schema: object }} structuredOutput
12
+ * @property {AbortSignal=} signal
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} AiRuntimeResult
17
+ * @property {string|object} output
18
+ * @property {string} provider
19
+ * @property {string} model
20
+ * @property {number} latencyMs
21
+ * @property {{ inputTokens?: number|null, outputTokens?: number|null, totalTokens?: number|null }=} usage
22
+ */
23
+
24
+ /**
25
+ * Boundary contract for VA-03 model invocation. VA-02 intentionally does not
26
+ * provide a provider implementation or call this interface.
27
+ *
28
+ * @typedef {object} AiRuntime
29
+ * @property {(request: AiRuntimeRequest) => Promise<AiRuntimeResult>} generateStructured
30
+ */
31
+
32
+ export function validateAiRuntime(runtime) {
33
+ if (!runtime || typeof runtime.generateStructured !== 'function') {
34
+ throw new TypeError('AiRuntime must provide generateStructured(request).');
35
+ }
36
+ return runtime;
37
+ }
38
+
39
+ function runtimeError(message) {
40
+ return new TypeError(`AiRuntime configuration error: ${message}`);
41
+ }
42
+
43
+ function nowMs(clock) {
44
+ return typeof clock?.now === 'function' ? clock.now() : Date.now();
45
+ }
46
+
47
+ /**
48
+ * Create a vendor-neutral runtime around a provider implementation. The
49
+ * provider owns SDK/HTTP details; the runtime owns the stable UpgradeLens
50
+ * request shape and compatibility mapping for legacy providers.
51
+ */
52
+ export function createProviderAiRuntime({
53
+ provider,
54
+ promptBuilder,
55
+ clock
56
+ } = {}) {
57
+ if (!provider || typeof provider.generateStructured !== 'function') {
58
+ throw runtimeError('provider must implement generateStructured(request).');
59
+ }
60
+ if (promptBuilder !== undefined && typeof promptBuilder !== 'function') {
61
+ throw runtimeError('promptBuilder must be a function when provided.');
62
+ }
63
+
64
+ return validateAiRuntime({
65
+ async generateStructured(request) {
66
+ const startedAt = nowMs(clock);
67
+ let prompt;
68
+ let outputSchema;
69
+ if (typeof request?.systemPrompt === 'string' && typeof request?.userPrompt === 'string') {
70
+ prompt = {
71
+ promptVersion: request.promptVersion,
72
+ system: request.systemPrompt,
73
+ user: request.userPrompt
74
+ };
75
+ outputSchema = request.structuredOutput?.schema;
76
+ } else {
77
+ if (typeof promptBuilder !== 'function') {
78
+ throw runtimeError('rendered prompts are required by Runtime Contract v1.');
79
+ }
80
+ outputSchema = request.outputSchema;
81
+ prompt = promptBuilder({
82
+ context: request.context,
83
+ outputSchema,
84
+ promptVersion: request.promptVersion
85
+ });
86
+ }
87
+ const response = await provider.generateStructured({
88
+ ...request,
89
+ contractVersion: request.contractVersion ?? AI_RUNTIME_CONTRACT_VERSION,
90
+ systemPrompt: request.systemPrompt ?? prompt.system,
91
+ userPrompt: request.userPrompt ?? prompt.user,
92
+ structuredOutput: request.structuredOutput ?? {
93
+ mode: 'jsonSchema',
94
+ name: 'upgradelens_structured_output',
95
+ schema: outputSchema
96
+ },
97
+ prompt,
98
+ outputSchema
99
+ });
100
+ const latencyMs = Number.isFinite(response?.latencyMs)
101
+ ? response.latencyMs
102
+ : Math.max(0, nowMs(clock) - startedAt);
103
+ return {
104
+ ...response,
105
+ output: response?.output,
106
+ provider: response?.provider ?? provider.name ?? 'unknown',
107
+ model: response?.model ?? provider.model ?? 'unknown',
108
+ latencyMs
109
+ };
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Minimal generic HTTP provider. It does not know about OpenAI, Gemini,
116
+ * Claude, or any ecosystem; callers provide request/response mapping for the
117
+ * service they choose.
118
+ */
119
+ export function createHttpJsonAiProvider({
120
+ endpoint,
121
+ fetchImplementation = globalThis.fetch,
122
+ headers = {},
123
+ model = 'unknown',
124
+ provider = 'http-json',
125
+ buildRequestBody = ({ prompt, outputSchema }) => ({ prompt, outputSchema }),
126
+ extractOutput = (body) => body.output
127
+ } = {}) {
128
+ if (typeof endpoint !== 'string' || endpoint.length === 0) {
129
+ throw runtimeError('endpoint is required for the HTTP provider.');
130
+ }
131
+ if (typeof fetchImplementation !== 'function') {
132
+ throw runtimeError('fetchImplementation must be a function.');
133
+ }
134
+ if (typeof buildRequestBody !== 'function') {
135
+ throw runtimeError('buildRequestBody must be a function.');
136
+ }
137
+ if (typeof extractOutput !== 'function') {
138
+ throw runtimeError('extractOutput must be a function.');
139
+ }
140
+
141
+ return {
142
+ name: provider,
143
+ model,
144
+ async generateStructured(request) {
145
+ const startedAt = Date.now();
146
+ const response = await fetchImplementation(endpoint, {
147
+ method: 'POST',
148
+ headers: {
149
+ 'content-type': 'application/json',
150
+ ...headers
151
+ },
152
+ body: JSON.stringify(buildRequestBody(request)),
153
+ ...(request.signal ? { signal: request.signal } : {})
154
+ });
155
+ const text = await response.text();
156
+ if (!response.ok) {
157
+ throw new Error(`AI provider ${provider} returned HTTP ${response.status}.`);
158
+ }
159
+ let body;
160
+ try {
161
+ body = JSON.parse(text);
162
+ } catch {
163
+ throw new Error(`AI provider ${provider} returned invalid JSON.`);
164
+ }
165
+ return {
166
+ output: extractOutput(body),
167
+ provider,
168
+ model,
169
+ latencyMs: Math.max(0, Date.now() - startedAt),
170
+ ...(body.usage ? { usage: body.usage } : {})
171
+ };
172
+ }
173
+ };
174
+ }