@convertcom/mcp-server 0.0.1-beta.16 → 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 +227 -12
- package/build/namespaced.js.map +1 -1
- package/build/operator.d.ts +14 -0
- package/build/operator.js +187 -22
- 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 +19 -1
- package/build/runtime.js +194 -18
- 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 and verify
|
|
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
|
};
|
|
@@ -519,16 +527,24 @@ function validateOperatorWriteInput(args) {
|
|
|
519
527
|
if (args.action === 'create_split_url_test') {
|
|
520
528
|
const original = args.body.original_pattern ?? args.body.url;
|
|
521
529
|
const variation = args.body.variation_pattern ?? args.body.variation_url ?? args.body.redirect_url;
|
|
522
|
-
const variationsIssue = validateCanonicalOperatorVariations(args.body.variations);
|
|
530
|
+
const variationsIssue = validateCanonicalOperatorVariations(args.body.variations, true);
|
|
523
531
|
if (variationsIssue)
|
|
524
532
|
return variationsIssue;
|
|
525
533
|
if (typeof original !== 'string' || !original.trim()) {
|
|
526
534
|
return 'operator.create_split_url_test requires body.original_pattern or body.url.';
|
|
527
535
|
}
|
|
536
|
+
if (Array.isArray(args.body.variations)) {
|
|
537
|
+
const splitIssue = validateSplitUrlArms(args.body.variations);
|
|
538
|
+
if (splitIssue)
|
|
539
|
+
return splitIssue;
|
|
540
|
+
}
|
|
528
541
|
if (!Array.isArray(args.body.variations)) {
|
|
529
542
|
if (typeof variation !== 'string' || !variation.trim()) {
|
|
530
543
|
return 'operator.create_split_url_test requires body.variation_pattern; body.variation_url and body.redirect_url are accepted aliases.';
|
|
531
544
|
}
|
|
545
|
+
const splitIssue = validateSplitUrlArms(buildSplitUrlExperienceBody(args.body).variations);
|
|
546
|
+
if (splitIssue)
|
|
547
|
+
return splitIssue;
|
|
532
548
|
}
|
|
533
549
|
}
|
|
534
550
|
if (args.action === 'enable_split_url_regex'
|
|
@@ -609,7 +625,7 @@ async function prepareFeedback(args, toolSet, readOnlyImpersonation) {
|
|
|
609
625
|
approved_payload_digest: digest,
|
|
610
626
|
},
|
|
611
627
|
next_step: submitAvailable
|
|
612
|
-
? '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.'
|
|
613
629
|
: readOnlyImpersonation
|
|
614
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.'
|
|
615
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.',
|
|
@@ -639,13 +655,15 @@ function feedbackActionConstraints(actions) {
|
|
|
639
655
|
});
|
|
640
656
|
return constraints;
|
|
641
657
|
}
|
|
642
|
-
function validateCanonicalOperatorVariations(value) {
|
|
658
|
+
function validateCanonicalOperatorVariations(value, allowBaseline = false) {
|
|
643
659
|
if (typeof value === 'undefined')
|
|
644
660
|
return null;
|
|
645
661
|
if (!Array.isArray(value) || value.length === 0) {
|
|
646
662
|
return 'body.variations must be a non-empty array when provided.';
|
|
647
663
|
}
|
|
648
664
|
const allowedVariationKeys = new Set(['name', 'status', 'traffic_distribution', 'traffic_percentage', 'changes']);
|
|
665
|
+
if (allowBaseline)
|
|
666
|
+
allowedVariationKeys.add('is_baseline');
|
|
649
667
|
for (const [index, variation] of value.entries()) {
|
|
650
668
|
if (!isObject(variation))
|
|
651
669
|
return `body.variations[${index}] must be an object.`;
|
|
@@ -665,6 +683,28 @@ function validateCanonicalOperatorVariations(value) {
|
|
|
665
683
|
}
|
|
666
684
|
return null;
|
|
667
685
|
}
|
|
686
|
+
function validateSplitUrlArms(variations) {
|
|
687
|
+
const hint = 'Split URL tests require at least two uniquely named arms, exactly one explicit is_baseline: true control with no changes, and a defaultRedirect change in each treatment. Supply numeric traffic_distribution for every arm (positive, totaling 100).';
|
|
688
|
+
if (variations.some((v) => !isObject(v) || typeof v.name !== 'string' || !v.name.trim()
|
|
689
|
+
|| !Array.isArray(v.changes) || v.changes.some((change) => !isObject(change))))
|
|
690
|
+
return hint;
|
|
691
|
+
if (variations.length < 2 || new Set(variations.map((v) => v.name)).size !== variations.length)
|
|
692
|
+
return hint;
|
|
693
|
+
const controls = variations.filter((v) => v.is_baseline === true);
|
|
694
|
+
if (controls.length !== 1 || controls[0].changes.length !== 0)
|
|
695
|
+
return hint;
|
|
696
|
+
for (const variation of variations) {
|
|
697
|
+
if (typeof variation.traffic_distribution !== 'number' || !Number.isFinite(variation.traffic_distribution)
|
|
698
|
+
|| variation.traffic_distribution <= 0 || variation.traffic_distribution > 100)
|
|
699
|
+
return hint;
|
|
700
|
+
if (variation.is_baseline !== true
|
|
701
|
+
&& variation.changes.filter((change) => change.type === 'defaultRedirect').length !== 1)
|
|
702
|
+
return hint;
|
|
703
|
+
}
|
|
704
|
+
if (Math.abs(variations.reduce((sum, v) => sum + v.traffic_distribution, 0) - 100) > 0.00005)
|
|
705
|
+
return hint;
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
668
708
|
function operatorVariationsContainCode(value) {
|
|
669
709
|
if (!Array.isArray(value))
|
|
670
710
|
return false;
|
|
@@ -909,15 +949,23 @@ async function experimentPortfolio(args, context) {
|
|
|
909
949
|
}
|
|
910
950
|
async function experimentVelocity(args, context) {
|
|
911
951
|
await report(context, 'Fetching experiences for velocity report', 1, 3);
|
|
912
|
-
const experienceCall = await
|
|
952
|
+
const [experienceCall, goalCall] = await Promise.all([
|
|
953
|
+
listExperiences(basePath(args), portfolioFilters(args)),
|
|
954
|
+
listResource('goals', basePath(args)),
|
|
955
|
+
]);
|
|
913
956
|
const experiences = collectionFrom(experienceCall.data);
|
|
914
|
-
const summaries = summarizeExperiences(experiences);
|
|
957
|
+
const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
|
|
958
|
+
const goalCoverage = investigationCollectionCoverage(goalCall);
|
|
915
959
|
const launched = summaries.filter((item) => typeof item.start_time === 'number' && item.start_time > 0);
|
|
916
960
|
const completed = summaries.filter((item) => typeof item.end_time === 'number' && item.end_time > 0);
|
|
917
961
|
const coverage = investigationCollectionCoverage(experienceCall);
|
|
918
962
|
const completeCoverage = coverage.status === 'complete';
|
|
919
963
|
return {
|
|
920
|
-
source_status: {
|
|
964
|
+
source_status: {
|
|
965
|
+
experiences: experienceCall.ok ? 'available' : 'unavailable',
|
|
966
|
+
goals: goalCall.ok ? 'available' : 'unavailable',
|
|
967
|
+
},
|
|
968
|
+
goal_authority_coverage: goalCoverage,
|
|
921
969
|
coverage,
|
|
922
970
|
total: experienceCall.ok && completeCoverage ? summaries.length : null,
|
|
923
971
|
created_or_returned: experienceCall.ok ? summaries.length : null,
|
|
@@ -942,7 +990,8 @@ async function experimentVelocity(args, context) {
|
|
|
942
990
|
...(experienceCall.ok && !completeCoverage
|
|
943
991
|
? [`Experience velocity coverage is ${coverage.status}; exact totals and averages are null, while returned_* fields describe only fetched rows.`]
|
|
944
992
|
: []),
|
|
945
|
-
...
|
|
993
|
+
...goalAuthorityWarnings(goalCoverage),
|
|
994
|
+
...warningsFromCalls([experienceCall, goalCall]),
|
|
946
995
|
],
|
|
947
996
|
};
|
|
948
997
|
}
|
|
@@ -1442,9 +1491,13 @@ async function investigate(args, context) {
|
|
|
1442
1491
|
}
|
|
1443
1492
|
async function failedTestAnalysis(args, context) {
|
|
1444
1493
|
await report(context, 'Fetching completed and paused experiences', 1, 3);
|
|
1445
|
-
const experienceCall = await
|
|
1494
|
+
const [experienceCall, goalCall] = await Promise.all([
|
|
1495
|
+
listExperiences(basePath(args), { status: ['completed', 'paused'] }),
|
|
1496
|
+
listResource('goals', basePath(args)),
|
|
1497
|
+
]);
|
|
1446
1498
|
const experiences = collectionFrom(experienceCall.data);
|
|
1447
|
-
const summaries = summarizeExperiences(experiences);
|
|
1499
|
+
const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
|
|
1500
|
+
const goalCoverage = investigationCollectionCoverage(goalCall);
|
|
1448
1501
|
const applicable = summaries.filter((item) => item.outcome !== 'not_applicable');
|
|
1449
1502
|
const losses = applicable.filter((item) => [
|
|
1450
1503
|
'negative',
|
|
@@ -1456,7 +1509,11 @@ async function failedTestAnalysis(args, context) {
|
|
|
1456
1509
|
const coverage = investigationCollectionCoverage(experienceCall);
|
|
1457
1510
|
const completeCoverage = coverage.status === 'complete';
|
|
1458
1511
|
return {
|
|
1459
|
-
source_status: {
|
|
1512
|
+
source_status: {
|
|
1513
|
+
experiences: experienceCall.ok ? 'available' : 'unavailable',
|
|
1514
|
+
goals: goalCall.ok ? 'available' : 'unavailable',
|
|
1515
|
+
},
|
|
1516
|
+
goal_authority_coverage: goalCoverage,
|
|
1460
1517
|
coverage,
|
|
1461
1518
|
returned_reviewed: experienceCall.ok ? summaries.length : null,
|
|
1462
1519
|
total_reviewed: experienceCall.ok && completeCoverage ? summaries.length : null,
|
|
@@ -1477,7 +1534,8 @@ async function failedTestAnalysis(args, context) {
|
|
|
1477
1534
|
...(experienceCall.ok && !completeCoverage
|
|
1478
1535
|
? [`Failed-test analysis coverage is ${coverage.status}; exact totals are null and returned_* fields describe only fetched rows.`]
|
|
1479
1536
|
: []),
|
|
1480
|
-
...
|
|
1537
|
+
...goalAuthorityWarnings(goalCoverage),
|
|
1538
|
+
...warningsFromCalls([experienceCall, goalCall]),
|
|
1481
1539
|
],
|
|
1482
1540
|
};
|
|
1483
1541
|
}
|
|
@@ -1567,20 +1625,27 @@ async function cleanupCandidates(args, context) {
|
|
|
1567
1625
|
await report(context, 'Fetching cleanup candidates', 1, 4);
|
|
1568
1626
|
const path = basePath(args);
|
|
1569
1627
|
const staleDraftDays = numberFrom(args.body.stale_draft_days ?? args.query.stale_draft_days, 30);
|
|
1570
|
-
const [drafts, allExperiences, goals, audiences, locations, features] = await Promise.all([
|
|
1628
|
+
const [drafts, allExperiences, goals, audiences, locations, features, goalAuthority] = await Promise.all([
|
|
1571
1629
|
listExperiences(path, { status: ['draft'] }),
|
|
1572
1630
|
listExperiences(path),
|
|
1573
1631
|
listResource('goals', path, { usage: 'not_used' }),
|
|
1574
1632
|
listResource('audiences', path, { usage: 'not_used' }),
|
|
1575
1633
|
listResource('locations', path, { usage: 'not_used' }),
|
|
1576
1634
|
listResource('features', path),
|
|
1635
|
+
listResource('goals', path),
|
|
1577
1636
|
]);
|
|
1578
1637
|
const experienceItems = collectionFrom(allExperiences.data);
|
|
1579
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);
|
|
1580
1645
|
const staleCutoff = Date.now() / 1000 - staleDraftDays * 24 * 60 * 60;
|
|
1581
1646
|
const staleDrafts = draftItems
|
|
1582
1647
|
.filter((item) => typeof item.created_at !== 'number' || item.created_at < staleCutoff)
|
|
1583
|
-
.map((experience) => summarizeExperience(experience));
|
|
1648
|
+
.map((experience) => summarizeExperience(experience, canonicalGoals));
|
|
1584
1649
|
const experienceCoverage = investigationCollectionCoverage(allExperiences);
|
|
1585
1650
|
const draftCoverage = investigationCollectionCoverage(drafts);
|
|
1586
1651
|
const completeExperienceCoverage = experienceCoverage.status === 'complete';
|
|
@@ -1616,12 +1681,13 @@ async function cleanupCandidates(args, context) {
|
|
|
1616
1681
|
sdk_search_patterns: feature.key ? [`${feature.key}`, `isFeatureEnabled(\"${feature.key}\")`, `getFeature(\"${feature.key}\")`] : [],
|
|
1617
1682
|
};
|
|
1618
1683
|
});
|
|
1619
|
-
const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features]);
|
|
1684
|
+
const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features, goalAuthority]);
|
|
1620
1685
|
for (const [resource, status] of Object.entries(candidateSourceStatus)) {
|
|
1621
1686
|
if (status !== 'complete') {
|
|
1622
1687
|
cleanupWarnings.push(`${resource} cleanup candidates are unavailable because verification status is ${status}; other resource categories remain independently evaluated.`);
|
|
1623
1688
|
}
|
|
1624
1689
|
}
|
|
1690
|
+
cleanupWarnings.push(...goalAuthorityWarnings(goalAuthorityCoverage));
|
|
1625
1691
|
return {
|
|
1626
1692
|
stale_draft_days: staleDraftDays,
|
|
1627
1693
|
source_status: {
|
|
@@ -1636,6 +1702,7 @@ async function cleanupCandidates(args, context) {
|
|
|
1636
1702
|
drafts: draftCoverage,
|
|
1637
1703
|
experiences: experienceCoverage,
|
|
1638
1704
|
...resourceCoverage,
|
|
1705
|
+
goal_authority: goalAuthorityCoverage,
|
|
1639
1706
|
},
|
|
1640
1707
|
returned_stale_drafts: drafts.ok ? staleDrafts.length : null,
|
|
1641
1708
|
total_stale_drafts: drafts.ok && draftCoverage.status === 'complete' ? staleDrafts.length : null,
|
|
@@ -1804,6 +1871,10 @@ async function createSplitUrlTest(args, context) {
|
|
|
1804
1871
|
readback_status: readback?.status ?? null,
|
|
1805
1872
|
},
|
|
1806
1873
|
verified,
|
|
1874
|
+
...(verified ? {} : {
|
|
1875
|
+
remediation_required: true,
|
|
1876
|
+
next_steps: ['Do not repeat create or activate this experience. Inspect the existing resource using create_receipt.experience_id; if the ID is unavailable, locate it before any further write. Correct the draft only after reviewing the persisted arms and allocation.'],
|
|
1877
|
+
}),
|
|
1807
1878
|
goal_configuration: goalConfigurationSummary(goalStatus),
|
|
1808
1879
|
...(verified && verification.normalizedCodeWhitespace ? {
|
|
1809
1880
|
verification_normalizations: [
|
|
@@ -1816,7 +1887,9 @@ async function createSplitUrlTest(args, context) {
|
|
|
1816
1887
|
? 'Readback confirmed the draft split URL experience and exact redirect patterns after Convert canonicalized only surrounding code whitespace.'
|
|
1817
1888
|
: 'Readback confirmed the draft split URL experience and exact redirect patterns.'
|
|
1818
1889
|
: 'Readback succeeded, but one or more requested split URL fields did not persist exactly.'
|
|
1819
|
-
:
|
|
1890
|
+
: experienceId
|
|
1891
|
+
? 'Create succeeded, but readback failed. The existing draft is unverified; do not repeat create.'
|
|
1892
|
+
: 'Create succeeded but returned no usable experience ID. Locate the existing draft before any further write; do not repeat create.',
|
|
1820
1893
|
};
|
|
1821
1894
|
}
|
|
1822
1895
|
async function enableSplitUrlRegex(args, context) {
|
|
@@ -2095,8 +2168,11 @@ export function buildSplitUrlExperienceBody(input) {
|
|
|
2095
2168
|
}
|
|
2096
2169
|
: input.settings,
|
|
2097
2170
|
variations: Array.isArray(input.variations) ? input.variations : [
|
|
2171
|
+
{ name: 'Original', is_baseline: true, traffic_distribution: 50, changes: [] },
|
|
2098
2172
|
{
|
|
2099
2173
|
name: stringFrom(input.variation_name, 'Variation URL'),
|
|
2174
|
+
is_baseline: false,
|
|
2175
|
+
traffic_distribution: 50,
|
|
2100
2176
|
changes: [
|
|
2101
2177
|
{
|
|
2102
2178
|
type: 'defaultRedirect',
|
|
@@ -2371,6 +2447,7 @@ async function namespaceCall(namespace, action, path, body, query, confirmation)
|
|
|
2371
2447
|
error: structured.error ?? text ?? 'Unknown error',
|
|
2372
2448
|
details: structured,
|
|
2373
2449
|
text,
|
|
2450
|
+
failure: failureFromStructured(structured, 'namespace'),
|
|
2374
2451
|
});
|
|
2375
2452
|
}
|
|
2376
2453
|
return recordNamespaceCall({
|
|
@@ -2396,6 +2473,13 @@ async function safeNamespaceCall(namespace, action, path, body, query) {
|
|
|
2396
2473
|
namespace,
|
|
2397
2474
|
action,
|
|
2398
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
|
+
}),
|
|
2399
2483
|
});
|
|
2400
2484
|
}
|
|
2401
2485
|
}
|
|
@@ -2419,6 +2503,11 @@ function basePath(args, extra = [], override = {}) {
|
|
|
2419
2503
|
}
|
|
2420
2504
|
return path;
|
|
2421
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
|
+
}
|
|
2422
2511
|
function summarizeExperiences(experiences, canonicalGoals = new Map()) {
|
|
2423
2512
|
return experiences.map((experience) => summarizeExperience(experience, canonicalGoals));
|
|
2424
2513
|
}
|
|
@@ -3172,11 +3261,25 @@ function stableLifecycleStatus(value) {
|
|
|
3172
3261
|
: 'unknown';
|
|
3173
3262
|
}
|
|
3174
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';
|
|
3175
3278
|
const status = call.status;
|
|
3176
3279
|
if (status === 401)
|
|
3177
3280
|
return 'authentication_failed';
|
|
3178
3281
|
if (status === 403)
|
|
3179
|
-
return '
|
|
3282
|
+
return 'upstream_request_failed';
|
|
3180
3283
|
if (status === 404)
|
|
3181
3284
|
return 'not_found';
|
|
3182
3285
|
if (status === 422)
|
|
@@ -3719,6 +3822,21 @@ function splitUrlExperienceVerification(data, expected) {
|
|
|
3719
3822
|
if (!experienceIdentityMatches(actual, expected)) {
|
|
3720
3823
|
return { matches: false, normalizedCodeWhitespace: false };
|
|
3721
3824
|
}
|
|
3825
|
+
const arms = collectionFrom(actual.variations);
|
|
3826
|
+
const expectedArms = collectionFrom(expected.variations);
|
|
3827
|
+
if (arms.length !== expectedArms.length || validateSplitUrlArms(arms) !== null
|
|
3828
|
+
|| new Set(arms.map((arm) => Number(arm.id))).size !== arms.length
|
|
3829
|
+
|| arms.some((arm) => !Number.isSafeInteger(Number(arm.id)) || Number(arm.id) <= 0)
|
|
3830
|
+
|| arms.some((arm) => (arm.is_baseline === true)
|
|
3831
|
+
!== (expectedArms.find((item) => item.name === arm.name)?.is_baseline === true))) {
|
|
3832
|
+
return { matches: false, normalizedCodeWhitespace: false };
|
|
3833
|
+
}
|
|
3834
|
+
for (const arm of arms) {
|
|
3835
|
+
const expectedArm = expectedArms.find((item) => item.name === arm.name);
|
|
3836
|
+
if (!expectedArm || arm.changes.length !== collectionFrom(expectedArm.changes).length) {
|
|
3837
|
+
return { matches: false, normalizedCodeWhitespace: false };
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3722
3840
|
return expectedVariationsVerification(actual.variations, expected.variations, false);
|
|
3723
3841
|
}
|
|
3724
3842
|
function experienceIdentityMatches(actual, expected) {
|
|
@@ -3962,11 +4080,40 @@ async function report(context, message, progress, total) {
|
|
|
3962
4080
|
function finalizeOperatorResult(action, data, calls) {
|
|
3963
4081
|
const allCallsFailed = calls.length > 0 && calls.every((call) => !call.ok);
|
|
3964
4082
|
if (!allCallsFailed) {
|
|
3965
|
-
return okResult(action, data);
|
|
4083
|
+
return okResult(action, data, calls);
|
|
3966
4084
|
}
|
|
3967
4085
|
const status = dominantFailureStatus(calls);
|
|
3968
4086
|
const errors = calls.map((call) => `${call.namespace}.${call.action}: ${stableCallErrorClass(call)}`);
|
|
3969
|
-
|
|
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 };
|
|
3970
4117
|
}
|
|
3971
4118
|
/** The most common HTTP status among failed calls, or 502 when no call reported a numeric status. */
|
|
3972
4119
|
function dominantFailureStatus(calls) {
|
|
@@ -3980,11 +4127,12 @@ function dominantFailureStatus(calls) {
|
|
|
3980
4127
|
return 502;
|
|
3981
4128
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
3982
4129
|
}
|
|
3983
|
-
function okResult(action, data) {
|
|
4130
|
+
function okResult(action, data, calls = []) {
|
|
3984
4131
|
const verified = typeof data.verified === 'boolean' ? data.verified : undefined;
|
|
3985
4132
|
const responseData = verified === undefined ? data : { ...data };
|
|
3986
4133
|
if (verified !== undefined)
|
|
3987
4134
|
delete responseData.verified;
|
|
4135
|
+
const partial = operatorFailureSummary(calls);
|
|
3988
4136
|
const payload = {
|
|
3989
4137
|
ok: true,
|
|
3990
4138
|
namespace: 'operator',
|
|
@@ -3993,6 +4141,10 @@ function okResult(action, data) {
|
|
|
3993
4141
|
status: 200,
|
|
3994
4142
|
data: responseData,
|
|
3995
4143
|
...(verified !== undefined ? { verified } : {}),
|
|
4144
|
+
...(partial ? {
|
|
4145
|
+
partial_failure_count: partial.count,
|
|
4146
|
+
partial_failure: partial.failure,
|
|
4147
|
+
} : {}),
|
|
3996
4148
|
};
|
|
3997
4149
|
return {
|
|
3998
4150
|
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
@@ -4003,6 +4155,10 @@ function errorResult(action, code, message, extra = {}) {
|
|
|
4003
4155
|
const scrubbedExtra = scrubOperatorErrorValue(extra);
|
|
4004
4156
|
const safeExtra = isObject(scrubbedExtra) ? scrubbedExtra : {};
|
|
4005
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');
|
|
4006
4162
|
const payload = {
|
|
4007
4163
|
...safeExtra,
|
|
4008
4164
|
ok: false,
|
|
@@ -4012,6 +4168,7 @@ function errorResult(action, code, message, extra = {}) {
|
|
|
4012
4168
|
status,
|
|
4013
4169
|
error: code,
|
|
4014
4170
|
message: scrubDiagnosticText(message),
|
|
4171
|
+
failure: existingFailure ?? inferredFailure,
|
|
4015
4172
|
};
|
|
4016
4173
|
return {
|
|
4017
4174
|
isError: true,
|
|
@@ -4065,6 +4222,14 @@ function approvalRequiredOperatorErrorResult(action, details) {
|
|
|
4065
4222
|
error: 'ApprovalRequired',
|
|
4066
4223
|
code: 'ApprovalRequired',
|
|
4067
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
|
+
}),
|
|
4068
4233
|
approval_state: approvalState,
|
|
4069
4234
|
approval_required: {
|
|
4070
4235
|
rule_code: ruleCode,
|