@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.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/bin/depverdict.js +7 -0
- package/bin/upgradelens.js +7 -0
- package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
- package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
- package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
- package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
- package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
- package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
- package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
- package/docs/IA-04-CLI-Orchestration.md +235 -0
- package/docs/IA-05-Real-Provider-Validation.md +235 -0
- package/docs/IA-05-VinGrade-Validation.md +339 -0
- package/docs/MVP-01.md +48 -0
- package/docs/MVP-02-Architecture.md +536 -0
- package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
- package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
- package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
- package/docs/MVP-02-Knowledge-Manifest.md +279 -0
- package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
- package/docs/MVP-02-Knowledge-Store.md +87 -0
- package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
- package/docs/MVP-02-Research-Planning.md +196 -0
- package/docs/MVP-02-Source-Provenance.md +81 -0
- package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
- package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
- package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
- package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
- package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
- package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
- package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
- package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
- package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
- package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
- package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
- package/docs/VinGrade-MVP-02-Validation.md +480 -0
- package/docs/VinGrade-RC02-Live-Validation.md +273 -0
- package/docs/ai-capability-discovery.md +573 -0
- package/docs/ai-engineering-production-readiness.md +390 -0
- package/docs/ai-engineering-review.md +251 -0
- package/docs/ai-runtime-governance-discovery.md +754 -0
- package/docs/architecture-overview.md +78 -0
- package/docs/cli-progress.md +111 -0
- package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
- package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
- package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
- package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
- package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
- package/docs/gateway-runtime-discovery.md +614 -0
- package/docs/live-ai-validation.md +299 -0
- package/docs/migration-planning-qualification-resolution.md +99 -0
- package/docs/migrations/upgradelens-to-depverdict.md +97 -0
- package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
- package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
- package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
- package/docs/mvp-05-deterministic-context-runtime.md +127 -0
- package/docs/mvp-05-migration-checklist-contract.md +166 -0
- package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
- package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
- package/docs/mvp-05-provider-neutral-generator.md +79 -0
- package/docs/ollama-local-smoke-validation.md +156 -0
- package/docs/openai-compatible-runtime-discovery.md +789 -0
- package/docs/openrouter-one-dependency-validation.md +205 -0
- package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
- package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
- package/docs/package-content-policy.md +98 -0
- package/docs/releases/v0.5.0-technical-preview.md +114 -0
- package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
- package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
- package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
- package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
- package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
- package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
- package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
- package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
- package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
- package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
- package/docs/reviews/oss-03-community-scaffolding.md +378 -0
- package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
- package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
- package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
- package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
- package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
- package/docs/runtime-contract-discovery.md +521 -0
- package/docs/structured-output-compatibility-report.md +100 -0
- package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
- package/docs/version-analysis-architecture.md +827 -0
- package/eval/README.md +86 -0
- package/eval/datasets/generic/declared-constraint.json +59 -0
- package/eval/datasets/generic/evidence-conflict.json +73 -0
- package/eval/datasets/generic/major-breaking-release.json +66 -0
- package/eval/datasets/generic/missing-evidence.json +46 -0
- package/eval/datasets/generic/patch-release-low.json +59 -0
- package/eval/datasets/node/axios-patch-low.json +59 -0
- package/eval/datasets/node/react-major-breaking.json +66 -0
- package/eval/datasets/node/react-minor-compatibility.json +66 -0
- package/eval/datasets/python/fastapi-deprecation.json +66 -0
- package/eval/datasets/python/pydantic-major-breaking.json +66 -0
- package/eval/migration-planning/golden-dataset-v2.json +306 -0
- package/eval/migration-planning/golden-dataset.json +214 -0
- package/eval/schemas/expected-result.schema.json +88 -0
- package/eval/schemas/golden-case.schema.json +181 -0
- package/package.json +57 -0
- package/schemas/ai-scorecard.schema.json +132 -0
- package/schemas/benchmark-report.schema.json +189 -0
- package/schemas/benchmark.schema.json +55 -0
- package/schemas/capability-profile.schema.json +44 -0
- package/schemas/conformance-report.schema.json +150 -0
- package/schemas/deployment-profile.schema.json +64 -0
- package/schemas/evaluation-report.schema.json +158 -0
- package/schemas/knowledge-evidence-bundle.schema.json +150 -0
- package/schemas/knowledge-manifest.schema.json +548 -0
- package/schemas/metrics.schema.json +178 -0
- package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
- package/schemas/migration-checklist.schema.json +706 -0
- package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
- package/schemas/migration-evaluation-dataset.schema.json +204 -0
- package/schemas/migration-planning-qualification-record.schema.json +234 -0
- package/schemas/project-manifest.schema.json +308 -0
- package/schemas/qualification-record.schema.json +56 -0
- package/schemas/repository-impact-evidence.schema.json +232 -0
- package/schemas/repository-impact.schema.json +202 -0
- package/schemas/upgrade-decision.schema.json +273 -0
- package/schemas/usage-index.schema.json +179 -0
- package/schemas/version-analysis.schema.json +449 -0
- package/src/ai-runtime-debug.js +325 -0
- package/src/ai-runtime-error.js +42 -0
- package/src/ai-runtime.js +174 -0
- package/src/ai-scorecard.js +204 -0
- package/src/ai-version-analysis.js +484 -0
- package/src/artifact-root-compatibility.js +91 -0
- package/src/benchmark-report.js +111 -0
- package/src/benchmark-runner.js +191 -0
- package/src/canonical-json.js +69 -0
- package/src/cli.js +1299 -0
- package/src/conformance-report.js +158 -0
- package/src/conformance-runner.js +253 -0
- package/src/constants.js +73 -0
- package/src/cooperative-scheduler.js +79 -0
- package/src/dependencies.js +44 -0
- package/src/dependency-ai-context.js +625 -0
- package/src/detectors.js +253 -0
- package/src/discovery.js +234 -0
- package/src/ecosystem-version-adapter.js +294 -0
- package/src/environment-compatibility.js +77 -0
- package/src/evaluation-comparator.js +158 -0
- package/src/evaluation-report.js +76 -0
- package/src/evaluation-runner.js +248 -0
- package/src/evidence-source-adapter.js +472 -0
- package/src/files.js +89 -0
- package/src/governance-diagnostics.js +64 -0
- package/src/governance-loader.js +63 -0
- package/src/governance-metadata.js +346 -0
- package/src/governance-validator.js +360 -0
- package/src/http/bounded-fetch.js +278 -0
- package/src/http/cli-http-runtime.js +44 -0
- package/src/impact/input-loader.js +157 -0
- package/src/impact/matcher.js +40 -0
- package/src/impact/repository-impact.js +199 -0
- package/src/impact/runtime.js +24 -0
- package/src/impact/status.js +62 -0
- package/src/impact/writer.js +30 -0
- package/src/impact-evidence/input-loader.js +202 -0
- package/src/impact-evidence/repository-impact-evidence.js +234 -0
- package/src/impact-evidence/runtime.js +16 -0
- package/src/impact-evidence/writer.js +30 -0
- package/src/index.js +563 -0
- package/src/installed-version-baseline.js +196 -0
- package/src/knowledge-cache.js +324 -0
- package/src/knowledge-evidence-bundle.js +101 -0
- package/src/knowledge-evidence-producer.js +233 -0
- package/src/knowledge-manifest-builder.js +188 -0
- package/src/knowledge-manifest-writer.js +32 -0
- package/src/knowledge-manifest.js +255 -0
- package/src/knowledge-research.js +615 -0
- package/src/metrics-engine.js +205 -0
- package/src/migration-checklist/ai-candidate.js +320 -0
- package/src/migration-checklist/assembler.js +37 -0
- package/src/migration-checklist/context-runtime.js +828 -0
- package/src/migration-checklist/evaluation/action-criteria.js +244 -0
- package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
- package/src/migration-checklist/evaluation/comparator.js +279 -0
- package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
- package/src/migration-checklist/evaluation/dataset.js +336 -0
- package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
- package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
- package/src/migration-checklist/evaluation/metrics.js +158 -0
- package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
- package/src/migration-checklist/evaluation/qualification.js +239 -0
- package/src/migration-checklist/evaluation/runner-v2.js +294 -0
- package/src/migration-checklist/evaluation/runner.js +194 -0
- package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
- package/src/migration-checklist/evaluation/scorecard.js +86 -0
- package/src/migration-checklist/extractive-candidate.js +166 -0
- package/src/migration-checklist/extractive-prompt.js +62 -0
- package/src/migration-checklist/generator.js +702 -0
- package/src/migration-checklist/grounding-policy.js +117 -0
- package/src/migration-checklist/input-loader.js +613 -0
- package/src/migration-checklist/migration-checklist.js +635 -0
- package/src/migration-checklist/presentation.js +292 -0
- package/src/migration-checklist/progress.js +96 -0
- package/src/migration-checklist/prompt.js +83 -0
- package/src/migration-checklist/qualification-guard.js +462 -0
- package/src/migration-checklist/qualification-resolution.js +122 -0
- package/src/migration-checklist/qualification-store.js +225 -0
- package/src/migration-checklist/runtime.js +205 -0
- package/src/migration-checklist/verification.js +134 -0
- package/src/migration-checklist/writer.js +35 -0
- package/src/openai-compatible-provider.js +451 -0
- package/src/orchestration/failure-log.js +32 -0
- package/src/orchestration/pipeline.js +200 -0
- package/src/orchestration/progress-events.js +337 -0
- package/src/orchestration/progress-reporter.js +131 -0
- package/src/orchestration/text-writer.js +22 -0
- package/src/portable.js +13 -0
- package/src/product-completion.js +249 -0
- package/src/project-manifest-input.js +90 -0
- package/src/project-manifest.js +141 -0
- package/src/python-requirements.js +137 -0
- package/src/registry/npm-packument.js +256 -0
- package/src/registry/npm-registry-adapter.js +262 -0
- package/src/registry/pypi-project.js +300 -0
- package/src/registry/pypi-registry-adapter.js +235 -0
- package/src/registry/sanitize-registry-body.js +51 -0
- package/src/renderers/console.js +160 -0
- package/src/renderers/impact-presentation.js +278 -0
- package/src/renderers/markdown.js +172 -0
- package/src/research-plan.js +455 -0
- package/src/runtime-conformance.js +275 -0
- package/src/source-provenance.js +393 -0
- package/src/source-url.js +62 -0
- package/src/structured-output-schema.js +66 -0
- package/src/target-selector.js +306 -0
- package/src/upgrade-decision/input-loader.js +107 -0
- package/src/upgrade-decision/presentation.js +43 -0
- package/src/upgrade-decision/runtime.js +21 -0
- package/src/upgrade-decision/upgrade-decision.js +626 -0
- package/src/upgrade-decision/writer.js +30 -0
- package/src/usage/analyzer-registry.js +63 -0
- package/src/usage/coverage.js +116 -0
- package/src/usage/input-loader.js +139 -0
- package/src/usage/js/analyzer.js +240 -0
- package/src/usage/js/parser.js +21 -0
- package/src/usage/runtime.js +187 -0
- package/src/usage/scope.js +44 -0
- package/src/usage/source-files.js +50 -0
- package/src/usage/usage-index.js +217 -0
- package/src/usage/writer.js +31 -0
- package/src/version-analysis-loader.js +203 -0
- package/src/version-analysis-manifest.js +314 -0
- 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
|
+
}
|