@convertcom/mcp-server 0.0.1-beta.17 → 0.0.1-beta.18
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/README.md +1 -1
- package/build/executor.js +271 -17
- package/build/executor.js.map +1 -1
- package/build/failure-attribution.d.ts +55 -0
- package/build/failure-attribution.js +326 -0
- package/build/failure-attribution.js.map +1 -0
- package/build/image-dimensions.d.ts +5 -0
- package/build/image-dimensions.js +203 -0
- package/build/image-dimensions.js.map +1 -0
- package/build/kb-search.js +16 -1
- package/build/kb-search.js.map +1 -1
- package/build/namespaced.d.ts +5 -0
- package/build/namespaced.js +199 -11
- package/build/namespaced.js.map +1 -1
- package/build/operator.d.ts +14 -0
- package/build/operator.js +128 -19
- package/build/operator.js.map +1 -1
- package/build/prompts.js +10 -3
- package/build/prompts.js.map +1 -1
- package/build/runtime.d.ts +15 -2
- package/build/runtime.js +186 -20
- package/build/runtime.js.map +1 -1
- package/build/whats-new.d.ts +24 -0
- package/build/whats-new.js +150 -0
- package/build/whats-new.js.map +1 -0
- package/data/convert-kb-legacy-map.json +24956 -9527
- package/data/convert-kb-manifest.json +26 -27
- package/data/convert-kb.json +44625 -64753
- package/data/kb-vectors/0/embedding.index.3.proxima +0 -0
- package/data/kb-vectors/0/scalar.0.ipc +0 -0
- package/data/kb-vectors/corpus.json +2 -2
- package/data/kb-vectors/del.0 +0 -0
- package/data/kb-vectors/idmap.0/000008.sst +0 -0
- package/data/kb-vectors/idmap.0/IDENTITY +1 -1
- package/data/kb-vectors/idmap.0/MANIFEST-000005 +0 -0
- package/data/kb-vectors/manifest.3 +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/build/operator.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
2
2
|
import { executeNamespacedTool, namespaceToolNameForAction } from './namespaced.js';
|
|
3
3
|
import { JSON_SCHEMA_DRAFT_2020_12 } from './shared.js';
|
|
4
4
|
import { scrubDiagnosticText } from './redaction.js';
|
|
5
|
+
import { failureAttribution, failureFromStructured, inputFailure, safeFailureAttribution, } from './failure-attribution.js';
|
|
5
6
|
import { FEEDBACK_BODY_SCHEMA, FEEDBACK_CONFIRMATION_SCHEMA, FeedbackContractError, feedbackConsentProof, feedbackSubmissionContextOutline, feedbackSubmissionDigest, sanitizeFeedbackBody, validateAndSanitizeFeedback, } from './feedback.js';
|
|
6
7
|
const APPROVAL_RULE_CODES = new Set([
|
|
7
8
|
'experience.activate',
|
|
@@ -129,7 +130,7 @@ const READ_ACTIONS = [
|
|
|
129
130
|
{
|
|
130
131
|
action: 'get_defaults',
|
|
131
132
|
mode: 'read',
|
|
132
|
-
description: 'Infer and cache project defaults such as preferred goal, audience, location, URL, and traffic split.',
|
|
133
|
+
description: 'Infer and cache suggested project defaults such as preferred goal, audience, location, URL, and traffic split. Suggestions are not consent to attach or create a resource.',
|
|
133
134
|
path_parameters: ['account_id', 'project_id'],
|
|
134
135
|
accepts_body: true,
|
|
135
136
|
},
|
|
@@ -145,14 +146,14 @@ const WRITE_ACTIONS = [
|
|
|
145
146
|
{
|
|
146
147
|
action: 'create_custom_code_ab_test',
|
|
147
148
|
mode: 'write',
|
|
148
|
-
description: 'Create a draft custom-code A/B experience and verify the persisted payload. Required body: url plus non-empty js or css. Optional: name, control_name, variation_name, description, objective, primary_goal, goals, audiences, locations, settings. Canonical custom variations may be supplied as variations[].changes[] with at most 100 changes per variation for bounded verification. Payload verification is independent from attached-goal lifecycle health; goal_configuration reports archived or unresolved goals without encouraging a duplicate create. Returns one authoritative experience plus a compact create receipt. Unknown fields are rejected.',
|
|
149
|
+
description: 'Create a draft custom-code A/B experience and verify the persisted payload. Required body: url plus non-empty js or css. Optional: name, control_name, variation_name, description, objective, primary_goal, goals, audiences, locations, settings. Omitted goals and goals: [] can both trigger Convert goal fallback; resolve user-intended existing goals and primary, send explicit IDs, and inspect the expanded returned experience. A bare URL does not select the UI Experience URL location; attach a user-intended location explicitly. Canonical custom variations may be supplied as variations[].changes[] with at most 100 changes per variation for bounded verification. Payload verification is independent from attached-goal lifecycle health; goal_configuration reports archived or unresolved goals without encouraging a duplicate create. Returns one authoritative experience plus a compact create receipt. Unknown fields are rejected.',
|
|
149
150
|
path_parameters: ['account_id', 'project_id'],
|
|
150
151
|
accepts_body: true,
|
|
151
152
|
},
|
|
152
153
|
{
|
|
153
154
|
action: 'create_split_url_test',
|
|
154
155
|
mode: 'write',
|
|
155
|
-
description: 'Create a draft split URL experience with an Original no-redirect control and a redirect treatment, allocated 50/50, and verify both arms, baseline, allocation and redirects. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support. Custom variations require unique names, exactly one explicit is_baseline: true control with no changes, one defaultRedirect per treatment, and positive numeric traffic_distribution per arm totaling 100; at most 100 changes per variation. goal_configuration separately reports archived or unresolved goals. Returns one authoritative experience plus a create receipt. If verified is false, inspect that existing draft; never repeat create or activate it automatically. Unknown fields are rejected.',
|
|
156
|
+
description: 'Create a draft split URL experience with an Original no-redirect control and a redirect treatment, allocated 50/50, and verify both arms, baseline, allocation and redirects. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support. Omitted goals and goals: [] can both trigger Convert goal fallback; resolve user-intended existing goals and primary, send explicit IDs, and inspect the expanded returned experience. A bare URL does not select the UI Experience URL location; attach a user-intended location explicitly. Custom variations require unique names, exactly one explicit is_baseline: true control with no changes, one defaultRedirect per treatment, and positive numeric traffic_distribution per arm totaling 100; at most 100 changes per variation. goal_configuration separately reports archived or unresolved goals. Returns one authoritative experience plus a create receipt. If verified is false, inspect that existing draft; never repeat create or activate it automatically. Unknown fields are rejected.',
|
|
156
157
|
path_parameters: ['account_id', 'project_id'],
|
|
157
158
|
accepts_body: true,
|
|
158
159
|
},
|
|
@@ -172,6 +173,10 @@ const WRITE_ACTIONS = [
|
|
|
172
173
|
},
|
|
173
174
|
];
|
|
174
175
|
const ALL_ACTIONS = [...READ_ACTIONS, ...WRITE_ACTIONS];
|
|
176
|
+
const ALL_ACTION_NAMES = new Set(ALL_ACTIONS.map((action) => action.action));
|
|
177
|
+
export function isRegisteredOperatorAction(action) {
|
|
178
|
+
return typeof action === 'string' && ALL_ACTION_NAMES.has(action);
|
|
179
|
+
}
|
|
175
180
|
const DEFAULT_RESULTS_PER_PAGE = 50;
|
|
176
181
|
const MAX_OPERATOR_COLLECTION_PAGES = 20;
|
|
177
182
|
const MAX_OPERATOR_COLLECTION_BYTES = 8 * 1024 * 1024;
|
|
@@ -254,6 +259,9 @@ export const OPERATOR_OUTPUT_SCHEMA = {
|
|
|
254
259
|
error: {},
|
|
255
260
|
issues: { type: 'array', items: { type: 'object' } },
|
|
256
261
|
verified: { type: 'boolean' },
|
|
262
|
+
failure: { type: 'object', description: 'Closed, identifier-free failure category and provenance for a failed operator call.' },
|
|
263
|
+
partial_failure_count: { type: 'integer', minimum: 1, description: 'Number of failed namespace calls when an operator workflow still returned partial evidence.' },
|
|
264
|
+
partial_failure: { type: 'object', description: 'Closed, identifier-free dominant failure category and provenance for a partially successful workflow.' },
|
|
257
265
|
},
|
|
258
266
|
additionalProperties: true,
|
|
259
267
|
};
|
|
@@ -617,7 +625,7 @@ async function prepareFeedback(args, toolSet, readOnlyImpersonation) {
|
|
|
617
625
|
approved_payload_digest: digest,
|
|
618
626
|
},
|
|
619
627
|
next_step: submitAvailable
|
|
620
|
-
? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Explain that context values are intentionally not re-echoed and that the digest binds approval to the complete sanitized payload. Only after explicit approval, call submit_feedback with the unchanged body and set all three confirmation_template booleans to true.'
|
|
628
|
+
? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Explain that context values are intentionally not re-echoed and that the digest binds approval to the complete sanitized payload. Only after explicit approval, call submit_feedback with the unchanged body and set all three confirmation_template booleans to true. If the host reports no approval while the confirmation matches this template exactly, the host failed to capture approval: re-present the host approval card instead of editing the confirmation.'
|
|
621
629
|
: readOnlyImpersonation
|
|
622
630
|
? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Staff impersonation is browse-only, so this preparation cannot be submitted or attributed as the customer. Remove as_user and prepare the feedback again as the connected staff member if they explicitly want to submit it.'
|
|
623
631
|
: 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. This read-only MCP profile cannot store feedback; do not claim submission. A write-enabled Convert MCP profile is required after approval.',
|
|
@@ -941,15 +949,23 @@ async function experimentPortfolio(args, context) {
|
|
|
941
949
|
}
|
|
942
950
|
async function experimentVelocity(args, context) {
|
|
943
951
|
await report(context, 'Fetching experiences for velocity report', 1, 3);
|
|
944
|
-
const experienceCall = await
|
|
952
|
+
const [experienceCall, goalCall] = await Promise.all([
|
|
953
|
+
listExperiences(basePath(args), portfolioFilters(args)),
|
|
954
|
+
listResource('goals', basePath(args)),
|
|
955
|
+
]);
|
|
945
956
|
const experiences = collectionFrom(experienceCall.data);
|
|
946
|
-
const summaries = summarizeExperiences(experiences);
|
|
957
|
+
const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
|
|
958
|
+
const goalCoverage = investigationCollectionCoverage(goalCall);
|
|
947
959
|
const launched = summaries.filter((item) => typeof item.start_time === 'number' && item.start_time > 0);
|
|
948
960
|
const completed = summaries.filter((item) => typeof item.end_time === 'number' && item.end_time > 0);
|
|
949
961
|
const coverage = investigationCollectionCoverage(experienceCall);
|
|
950
962
|
const completeCoverage = coverage.status === 'complete';
|
|
951
963
|
return {
|
|
952
|
-
source_status: {
|
|
964
|
+
source_status: {
|
|
965
|
+
experiences: experienceCall.ok ? 'available' : 'unavailable',
|
|
966
|
+
goals: goalCall.ok ? 'available' : 'unavailable',
|
|
967
|
+
},
|
|
968
|
+
goal_authority_coverage: goalCoverage,
|
|
953
969
|
coverage,
|
|
954
970
|
total: experienceCall.ok && completeCoverage ? summaries.length : null,
|
|
955
971
|
created_or_returned: experienceCall.ok ? summaries.length : null,
|
|
@@ -974,7 +990,8 @@ async function experimentVelocity(args, context) {
|
|
|
974
990
|
...(experienceCall.ok && !completeCoverage
|
|
975
991
|
? [`Experience velocity coverage is ${coverage.status}; exact totals and averages are null, while returned_* fields describe only fetched rows.`]
|
|
976
992
|
: []),
|
|
977
|
-
...
|
|
993
|
+
...goalAuthorityWarnings(goalCoverage),
|
|
994
|
+
...warningsFromCalls([experienceCall, goalCall]),
|
|
978
995
|
],
|
|
979
996
|
};
|
|
980
997
|
}
|
|
@@ -1474,9 +1491,13 @@ async function investigate(args, context) {
|
|
|
1474
1491
|
}
|
|
1475
1492
|
async function failedTestAnalysis(args, context) {
|
|
1476
1493
|
await report(context, 'Fetching completed and paused experiences', 1, 3);
|
|
1477
|
-
const experienceCall = await
|
|
1494
|
+
const [experienceCall, goalCall] = await Promise.all([
|
|
1495
|
+
listExperiences(basePath(args), { status: ['completed', 'paused'] }),
|
|
1496
|
+
listResource('goals', basePath(args)),
|
|
1497
|
+
]);
|
|
1478
1498
|
const experiences = collectionFrom(experienceCall.data);
|
|
1479
|
-
const summaries = summarizeExperiences(experiences);
|
|
1499
|
+
const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
|
|
1500
|
+
const goalCoverage = investigationCollectionCoverage(goalCall);
|
|
1480
1501
|
const applicable = summaries.filter((item) => item.outcome !== 'not_applicable');
|
|
1481
1502
|
const losses = applicable.filter((item) => [
|
|
1482
1503
|
'negative',
|
|
@@ -1488,7 +1509,11 @@ async function failedTestAnalysis(args, context) {
|
|
|
1488
1509
|
const coverage = investigationCollectionCoverage(experienceCall);
|
|
1489
1510
|
const completeCoverage = coverage.status === 'complete';
|
|
1490
1511
|
return {
|
|
1491
|
-
source_status: {
|
|
1512
|
+
source_status: {
|
|
1513
|
+
experiences: experienceCall.ok ? 'available' : 'unavailable',
|
|
1514
|
+
goals: goalCall.ok ? 'available' : 'unavailable',
|
|
1515
|
+
},
|
|
1516
|
+
goal_authority_coverage: goalCoverage,
|
|
1492
1517
|
coverage,
|
|
1493
1518
|
returned_reviewed: experienceCall.ok ? summaries.length : null,
|
|
1494
1519
|
total_reviewed: experienceCall.ok && completeCoverage ? summaries.length : null,
|
|
@@ -1509,7 +1534,8 @@ async function failedTestAnalysis(args, context) {
|
|
|
1509
1534
|
...(experienceCall.ok && !completeCoverage
|
|
1510
1535
|
? [`Failed-test analysis coverage is ${coverage.status}; exact totals are null and returned_* fields describe only fetched rows.`]
|
|
1511
1536
|
: []),
|
|
1512
|
-
...
|
|
1537
|
+
...goalAuthorityWarnings(goalCoverage),
|
|
1538
|
+
...warningsFromCalls([experienceCall, goalCall]),
|
|
1513
1539
|
],
|
|
1514
1540
|
};
|
|
1515
1541
|
}
|
|
@@ -1599,20 +1625,27 @@ async function cleanupCandidates(args, context) {
|
|
|
1599
1625
|
await report(context, 'Fetching cleanup candidates', 1, 4);
|
|
1600
1626
|
const path = basePath(args);
|
|
1601
1627
|
const staleDraftDays = numberFrom(args.body.stale_draft_days ?? args.query.stale_draft_days, 30);
|
|
1602
|
-
const [drafts, allExperiences, goals, audiences, locations, features] = await Promise.all([
|
|
1628
|
+
const [drafts, allExperiences, goals, audiences, locations, features, goalAuthority] = await Promise.all([
|
|
1603
1629
|
listExperiences(path, { status: ['draft'] }),
|
|
1604
1630
|
listExperiences(path),
|
|
1605
1631
|
listResource('goals', path, { usage: 'not_used' }),
|
|
1606
1632
|
listResource('audiences', path, { usage: 'not_used' }),
|
|
1607
1633
|
listResource('locations', path, { usage: 'not_used' }),
|
|
1608
1634
|
listResource('features', path),
|
|
1635
|
+
listResource('goals', path),
|
|
1609
1636
|
]);
|
|
1610
1637
|
const experienceItems = collectionFrom(allExperiences.data);
|
|
1611
1638
|
const draftItems = collectionFrom(drafts.data);
|
|
1639
|
+
// Shared goals can embed stale or reordered definitions per experience; resolve
|
|
1640
|
+
// every stale-draft summary against the goals authority (draft_qa parity) so a
|
|
1641
|
+
// shared goal reports one consistent type/name/status everywhere. Missing ids
|
|
1642
|
+
// fall back to their embeds, and an unavailable authority is disclosed below.
|
|
1643
|
+
const canonicalGoals = canonicalGoalDefinitions(collectionFrom(goalAuthority.data));
|
|
1644
|
+
const goalAuthorityCoverage = investigationCollectionCoverage(goalAuthority);
|
|
1612
1645
|
const staleCutoff = Date.now() / 1000 - staleDraftDays * 24 * 60 * 60;
|
|
1613
1646
|
const staleDrafts = draftItems
|
|
1614
1647
|
.filter((item) => typeof item.created_at !== 'number' || item.created_at < staleCutoff)
|
|
1615
|
-
.map((experience) => summarizeExperience(experience));
|
|
1648
|
+
.map((experience) => summarizeExperience(experience, canonicalGoals));
|
|
1616
1649
|
const experienceCoverage = investigationCollectionCoverage(allExperiences);
|
|
1617
1650
|
const draftCoverage = investigationCollectionCoverage(drafts);
|
|
1618
1651
|
const completeExperienceCoverage = experienceCoverage.status === 'complete';
|
|
@@ -1648,12 +1681,13 @@ async function cleanupCandidates(args, context) {
|
|
|
1648
1681
|
sdk_search_patterns: feature.key ? [`${feature.key}`, `isFeatureEnabled(\"${feature.key}\")`, `getFeature(\"${feature.key}\")`] : [],
|
|
1649
1682
|
};
|
|
1650
1683
|
});
|
|
1651
|
-
const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features]);
|
|
1684
|
+
const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features, goalAuthority]);
|
|
1652
1685
|
for (const [resource, status] of Object.entries(candidateSourceStatus)) {
|
|
1653
1686
|
if (status !== 'complete') {
|
|
1654
1687
|
cleanupWarnings.push(`${resource} cleanup candidates are unavailable because verification status is ${status}; other resource categories remain independently evaluated.`);
|
|
1655
1688
|
}
|
|
1656
1689
|
}
|
|
1690
|
+
cleanupWarnings.push(...goalAuthorityWarnings(goalAuthorityCoverage));
|
|
1657
1691
|
return {
|
|
1658
1692
|
stale_draft_days: staleDraftDays,
|
|
1659
1693
|
source_status: {
|
|
@@ -1668,6 +1702,7 @@ async function cleanupCandidates(args, context) {
|
|
|
1668
1702
|
drafts: draftCoverage,
|
|
1669
1703
|
experiences: experienceCoverage,
|
|
1670
1704
|
...resourceCoverage,
|
|
1705
|
+
goal_authority: goalAuthorityCoverage,
|
|
1671
1706
|
},
|
|
1672
1707
|
returned_stale_drafts: drafts.ok ? staleDrafts.length : null,
|
|
1673
1708
|
total_stale_drafts: drafts.ok && draftCoverage.status === 'complete' ? staleDrafts.length : null,
|
|
@@ -2412,6 +2447,7 @@ async function namespaceCall(namespace, action, path, body, query, confirmation)
|
|
|
2412
2447
|
error: structured.error ?? text ?? 'Unknown error',
|
|
2413
2448
|
details: structured,
|
|
2414
2449
|
text,
|
|
2450
|
+
failure: failureFromStructured(structured, 'namespace'),
|
|
2415
2451
|
});
|
|
2416
2452
|
}
|
|
2417
2453
|
return recordNamespaceCall({
|
|
@@ -2437,6 +2473,13 @@ async function safeNamespaceCall(namespace, action, path, body, query) {
|
|
|
2437
2473
|
namespace,
|
|
2438
2474
|
action,
|
|
2439
2475
|
error: error instanceof Error ? error.name : 'ThrownError',
|
|
2476
|
+
failure: failureAttribution({
|
|
2477
|
+
category: 'unknown',
|
|
2478
|
+
source: 'operator',
|
|
2479
|
+
stage: 'operator_workflow',
|
|
2480
|
+
dispatchState: 'unknown',
|
|
2481
|
+
code: error instanceof Error ? error.name : 'ThrownError',
|
|
2482
|
+
}),
|
|
2440
2483
|
});
|
|
2441
2484
|
}
|
|
2442
2485
|
}
|
|
@@ -2460,6 +2503,11 @@ function basePath(args, extra = [], override = {}) {
|
|
|
2460
2503
|
}
|
|
2461
2504
|
return path;
|
|
2462
2505
|
}
|
|
2506
|
+
function goalAuthorityWarnings(coverage) {
|
|
2507
|
+
return coverage.status === 'complete' ? [] : [
|
|
2508
|
+
`Goal type/name/status for unresolved IDs fell back to experience embeds because the goals authority is ${coverage.status}; verify shared goal definitions via goals.get before acting on them.`,
|
|
2509
|
+
];
|
|
2510
|
+
}
|
|
2463
2511
|
function summarizeExperiences(experiences, canonicalGoals = new Map()) {
|
|
2464
2512
|
return experiences.map((experience) => summarizeExperience(experience, canonicalGoals));
|
|
2465
2513
|
}
|
|
@@ -3213,11 +3261,25 @@ function stableLifecycleStatus(value) {
|
|
|
3213
3261
|
: 'unknown';
|
|
3214
3262
|
}
|
|
3215
3263
|
function stableCallErrorClass(call) {
|
|
3264
|
+
if (call.failure?.category === 'input')
|
|
3265
|
+
return 'invalid_input';
|
|
3266
|
+
if (call.failure?.category === 'permission')
|
|
3267
|
+
return call.status === 401 ? 'authentication_failed' : 'permission_denied';
|
|
3268
|
+
if (call.failure?.category === 'not_found')
|
|
3269
|
+
return 'not_found';
|
|
3270
|
+
if (call.failure?.category === 'timeout')
|
|
3271
|
+
return 'upstream_timeout';
|
|
3272
|
+
if (call.failure?.category === 'network')
|
|
3273
|
+
return 'upstream_network_error';
|
|
3274
|
+
if (call.failure?.category === 'cancellation')
|
|
3275
|
+
return 'cancelled';
|
|
3276
|
+
if (call.failure?.category === 'internal')
|
|
3277
|
+
return 'internal_error';
|
|
3216
3278
|
const status = call.status;
|
|
3217
3279
|
if (status === 401)
|
|
3218
3280
|
return 'authentication_failed';
|
|
3219
3281
|
if (status === 403)
|
|
3220
|
-
return '
|
|
3282
|
+
return 'upstream_request_failed';
|
|
3221
3283
|
if (status === 404)
|
|
3222
3284
|
return 'not_found';
|
|
3223
3285
|
if (status === 422)
|
|
@@ -4018,11 +4080,40 @@ async function report(context, message, progress, total) {
|
|
|
4018
4080
|
function finalizeOperatorResult(action, data, calls) {
|
|
4019
4081
|
const allCallsFailed = calls.length > 0 && calls.every((call) => !call.ok);
|
|
4020
4082
|
if (!allCallsFailed) {
|
|
4021
|
-
return okResult(action, data);
|
|
4083
|
+
return okResult(action, data, calls);
|
|
4022
4084
|
}
|
|
4023
4085
|
const status = dominantFailureStatus(calls);
|
|
4024
4086
|
const errors = calls.map((call) => `${call.namespace}.${call.action}: ${stableCallErrorClass(call)}`);
|
|
4025
|
-
|
|
4087
|
+
const partial = operatorFailureSummary(calls);
|
|
4088
|
+
return errorResult(action, 'AllUpstreamCallsFailed', `All upstream namespace calls failed for operator.${action}.`, {
|
|
4089
|
+
status,
|
|
4090
|
+
data,
|
|
4091
|
+
errors,
|
|
4092
|
+
...(partial ? { failure: partial.failure } : {}),
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
function operatorFailureSummary(calls) {
|
|
4096
|
+
const failures = calls
|
|
4097
|
+
.filter((call) => !call.ok)
|
|
4098
|
+
.map((call) => call.failure ?? failureFromStructured({
|
|
4099
|
+
...(typeof call.status === 'number' ? { status: call.status } : {}),
|
|
4100
|
+
error: typeof call.error === 'string' ? call.error : 'UnknownFailure',
|
|
4101
|
+
}, 'namespace'));
|
|
4102
|
+
if (failures.length === 0)
|
|
4103
|
+
return null;
|
|
4104
|
+
const priority = new Map([
|
|
4105
|
+
['permission', 0],
|
|
4106
|
+
['cancellation', 1],
|
|
4107
|
+
['timeout', 2],
|
|
4108
|
+
['network', 3],
|
|
4109
|
+
['not_found', 4],
|
|
4110
|
+
['upstream', 5],
|
|
4111
|
+
['input', 6],
|
|
4112
|
+
['internal', 7],
|
|
4113
|
+
['unknown', 8],
|
|
4114
|
+
]);
|
|
4115
|
+
const failure = failures.slice().sort((left, right) => (priority.get(left.category) ?? 99) - (priority.get(right.category) ?? 99))[0];
|
|
4116
|
+
return { count: failures.length, failure };
|
|
4026
4117
|
}
|
|
4027
4118
|
/** The most common HTTP status among failed calls, or 502 when no call reported a numeric status. */
|
|
4028
4119
|
function dominantFailureStatus(calls) {
|
|
@@ -4036,11 +4127,12 @@ function dominantFailureStatus(calls) {
|
|
|
4036
4127
|
return 502;
|
|
4037
4128
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
4038
4129
|
}
|
|
4039
|
-
function okResult(action, data) {
|
|
4130
|
+
function okResult(action, data, calls = []) {
|
|
4040
4131
|
const verified = typeof data.verified === 'boolean' ? data.verified : undefined;
|
|
4041
4132
|
const responseData = verified === undefined ? data : { ...data };
|
|
4042
4133
|
if (verified !== undefined)
|
|
4043
4134
|
delete responseData.verified;
|
|
4135
|
+
const partial = operatorFailureSummary(calls);
|
|
4044
4136
|
const payload = {
|
|
4045
4137
|
ok: true,
|
|
4046
4138
|
namespace: 'operator',
|
|
@@ -4049,6 +4141,10 @@ function okResult(action, data) {
|
|
|
4049
4141
|
status: 200,
|
|
4050
4142
|
data: responseData,
|
|
4051
4143
|
...(verified !== undefined ? { verified } : {}),
|
|
4144
|
+
...(partial ? {
|
|
4145
|
+
partial_failure_count: partial.count,
|
|
4146
|
+
partial_failure: partial.failure,
|
|
4147
|
+
} : {}),
|
|
4052
4148
|
};
|
|
4053
4149
|
return {
|
|
4054
4150
|
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
@@ -4059,6 +4155,10 @@ function errorResult(action, code, message, extra = {}) {
|
|
|
4059
4155
|
const scrubbedExtra = scrubOperatorErrorValue(extra);
|
|
4060
4156
|
const safeExtra = isObject(scrubbedExtra) ? scrubbedExtra : {};
|
|
4061
4157
|
const status = typeof safeExtra.status === 'number' ? safeExtra.status : 400;
|
|
4158
|
+
const existingFailure = safeFailureAttribution(safeExtra.failure);
|
|
4159
|
+
const inferredFailure = code === 'MissingPathParameters'
|
|
4160
|
+
? inputFailure(code, 'operator', 'required', 'path')
|
|
4161
|
+
: failureFromStructured({ ...safeExtra, error: code }, 'operator');
|
|
4062
4162
|
const payload = {
|
|
4063
4163
|
...safeExtra,
|
|
4064
4164
|
ok: false,
|
|
@@ -4068,6 +4168,7 @@ function errorResult(action, code, message, extra = {}) {
|
|
|
4068
4168
|
status,
|
|
4069
4169
|
error: code,
|
|
4070
4170
|
message: scrubDiagnosticText(message),
|
|
4171
|
+
failure: existingFailure ?? inferredFailure,
|
|
4071
4172
|
};
|
|
4072
4173
|
return {
|
|
4073
4174
|
isError: true,
|
|
@@ -4121,6 +4222,14 @@ function approvalRequiredOperatorErrorResult(action, details) {
|
|
|
4121
4222
|
error: 'ApprovalRequired',
|
|
4122
4223
|
code: 'ApprovalRequired',
|
|
4123
4224
|
message,
|
|
4225
|
+
failure: failureAttribution({
|
|
4226
|
+
category: 'permission',
|
|
4227
|
+
source: 'operator',
|
|
4228
|
+
stage: 'upstream_response',
|
|
4229
|
+
dispatchState: 'dispatched',
|
|
4230
|
+
code: 'ApprovalRequired',
|
|
4231
|
+
upstreamStatus: 422,
|
|
4232
|
+
}),
|
|
4124
4233
|
approval_state: approvalState,
|
|
4125
4234
|
approval_required: {
|
|
4126
4235
|
rule_code: ruleCode,
|