@appaflytech/zeyrek-ai-mcp 0.0.61 → 0.0.63
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/.env.example +6 -0
- package/dist/config.js +2 -0
- package/dist/knowledge/backendConstraints.js +2 -2
- package/dist/knowledge/mobileStore.js +24 -4
- package/dist/knowledge/normalize.js +10 -0
- package/dist/knowledge/platform/mobileLayoutLint.js +8 -0
- package/dist/knowledge/platform/mobileRules.js +6 -0
- package/dist/knowledge/rules.js +89 -0
- package/dist/observability/gapSink.js +43 -0
- package/dist/platform/argumentAliases.js +6 -0
- package/dist/platform/capture/actions.js +71 -2
- package/dist/platform/capture/capture.js +5 -2
- package/dist/platform/responses.js +5 -2
- package/dist/platform/writeDiff.js +52 -1
- package/dist/resources/examples/middleware-password-reset.json +1281 -0
- package/dist/resources/examples/mobile-reorder-persist.json +33 -7
- package/dist/resources/examples/workflow-email-step.json +67 -0
- package/dist/resources/guides/deployment.md +13 -1
- package/dist/resources/guides/entity.md +7 -0
- package/dist/resources/guides/expressions.md +5 -0
- package/dist/resources/guides/getting-started.md +15 -30
- package/dist/resources/guides/microservice.md +30 -2
- package/dist/resources/guides/middleware.md +83 -3
- package/dist/resources/guides/mobile-layout.md +35 -3
- package/dist/resources/guides/mobile-publish.md +33 -1
- package/dist/resources/guides/mobile-steps.md +12 -2
- package/dist/resources/guides/mobile.md +11 -8
- package/dist/resources/guides/page.md +28 -3
- package/dist/resources/guides/workflow.md +30 -1
- package/dist/resources/schema-fingerprint.json +3 -3
- package/dist/tools/applications.js +6 -3
- package/dist/tools/common.js +8 -0
- package/dist/tools/designSchemaPatch.js +4 -1
- package/dist/tools/flows.js +14 -4
- package/dist/tools/layouts.js +3 -1
- package/dist/tools/microservices.js +8 -2
- package/dist/tools/mobileStore.js +13 -2
- package/dist/tools/pages.js +27 -12
- package/dist/tools/platformInfo.js +15 -6
- package/dist/tools/workflows.js +4 -2
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -40,6 +40,12 @@ LOG_DIR=
|
|
|
40
40
|
LOG_FILE=
|
|
41
41
|
LOG_MAX_BODY_CHARS=2000
|
|
42
42
|
|
|
43
|
+
# platform_gap_report kayitlarinin ekibe gidecegi webhook (JSON POST, 3 sn, tek deneme).
|
|
44
|
+
# Bos birakilirsa kayit yalniz bu makinedeki gunlukte kalir ve yanit delivered: log-only der.
|
|
45
|
+
# Adresi insan yazar; ornek ya da varsayilan adres yoktur. Govde: goal, blocker, severity, module,
|
|
46
|
+
# attempted, workaround, project, gatewayVersion, at — metinlerdeki jetonlar maskelenir.
|
|
47
|
+
GAP_REPORT_WEBHOOK_URL=
|
|
48
|
+
|
|
43
49
|
# Yanit budama esigi (bayt) ve istek zaman asimi (ms)
|
|
44
50
|
MAX_RESPONSE_BYTES=262144
|
|
45
51
|
REQUEST_TIMEOUT_MS=100000
|
package/dist/config.js
CHANGED
|
@@ -146,6 +146,7 @@ const environmentSchema = z.object({
|
|
|
146
146
|
LOG_DIR: z.string().min(1).optional(),
|
|
147
147
|
LOG_FILE: z.string().min(1).optional(),
|
|
148
148
|
LOG_MAX_BODY_CHARS: z.coerce.number().int().positive().default(2000),
|
|
149
|
+
GAP_REPORT_WEBHOOK_URL: z.string().url().optional(),
|
|
149
150
|
MCP_PORT: z.coerce.number().int().positive().default(5100),
|
|
150
151
|
STUDIO_BASE_URL: z.string().url().optional(),
|
|
151
152
|
SCREENSHOT_BROWSER_PATH: z.string().min(1).optional(),
|
|
@@ -257,6 +258,7 @@ export function loadConfig(env) {
|
|
|
257
258
|
logDirectory: values.LOG_DIR,
|
|
258
259
|
logFile: values.LOG_FILE,
|
|
259
260
|
logMaxBodyChars: values.LOG_MAX_BODY_CHARS,
|
|
261
|
+
gapReportWebhookUrl: values.GAP_REPORT_WEBHOOK_URL,
|
|
260
262
|
port: values.MCP_PORT,
|
|
261
263
|
studioBaseUrl: values.STUDIO_BASE_URL?.replace(/\/+$/, ""),
|
|
262
264
|
screenshotBrowserPath: values.SCREENSHOT_BROWSER_PATH,
|
|
@@ -417,8 +417,8 @@ function microserviceFindings(body) {
|
|
|
417
417
|
findings.push(`sourceParameters.fields[${index}]: writeOn:'${String(field.writeOn)}' yalnız operation:'upsert' ucunda anlamlı; bu uç '${operation ?? '?'}' — writeOn'u kaldırın ya da 'always' bırakın (Fields.All(WriteOn == Always) when Operation != Upsert).`);
|
|
418
418
|
}
|
|
419
419
|
});
|
|
420
|
-
if (body.source === 'table' && operation
|
|
421
|
-
findings.push(
|
|
420
|
+
if (body.source === 'table' && operation === 'bulk-create' && asObjects(parameters.filters).length > 0) {
|
|
421
|
+
findings.push("sourceParameters.filters: 'bulk-create' ucu filtre KULLANMAZ — satırlar gövdeden okunur. Filtreleri kaldırın. (bulk-update/bulk-delete'te filtre kapsamdır: MS-027.)");
|
|
422
422
|
}
|
|
423
423
|
flattenFilters(parameters.filters).forEach((filter, index) => {
|
|
424
424
|
if (!isSingleFilter(filter))
|
|
@@ -37,10 +37,9 @@ export const AD_UNITS_MAX_COUNT = 40;
|
|
|
37
37
|
export const AD_FORMATS = ['banner', 'interstitial', 'rewarded', 'rewardedInterstitial', 'appOpen'];
|
|
38
38
|
/** Reklam açıkken taşınması gereken izin: iOS ATT (backend `MobileStoreSettings.AdsPermission`). */
|
|
39
39
|
export const ADS_PERMISSION = 'tracking';
|
|
40
|
-
/** Uygulama kilidi (kök ADR 0021 madde 8): kapalı süre kümesi (saniye, `MobileStoreSettings.AppLockTimeouts`)
|
|
40
|
+
/** Uygulama kilidi (kök ADR 0021 madde 8): kapalı süre kümesi (saniye, `MobileStoreSettings.AppLockTimeouts`) ve kullandığı izin. Kabuk sürümü backend'in yetenek listesinden okunur (g-05). */
|
|
41
41
|
export const APP_LOCK_TIMEOUTS = [0, 60, 300, 900];
|
|
42
42
|
export const APP_LOCK_PERMISSION = 'biometric';
|
|
43
|
-
export const APP_LOCK_SHELL_VERSION = '0.6.0';
|
|
44
43
|
export const LISTING_LIMITS = {
|
|
45
44
|
title: 30,
|
|
46
45
|
subtitle: 30,
|
|
@@ -377,6 +376,25 @@ export function permissionStoreBuildWarnings(permissions, environment) {
|
|
|
377
376
|
: [];
|
|
378
377
|
});
|
|
379
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* MOB-044 (g-05) — kabuk yetenek tablosunun tek kaynağı backend'dir (`MobileBuildOptions.ShellCapabilities`, b-05): kabuk
|
|
381
|
+
* ucunun ve izin görünümünün `capabilities[] { name, minVersion, carried }` listesi ad → `{ carried, since }` biçimine
|
|
382
|
+
* çevrilir. Listeyi vermeyen eski backend'de yalnız sürüm döner.
|
|
383
|
+
*/
|
|
384
|
+
export function shellCapabilityView(shell) {
|
|
385
|
+
if (!isObject(shell))
|
|
386
|
+
return undefined;
|
|
387
|
+
const capabilities = Array.isArray(shell.capabilities) ? shell.capabilities.filter(isObject) : undefined;
|
|
388
|
+
return {
|
|
389
|
+
version: shell.version,
|
|
390
|
+
...(capabilities ? { capabilities: Object.fromEntries(capabilities.map(capability => [text(capability.name), { carried: capability.carried === true, since: capability.minVersion }])) } : {})
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const shellSince = (shell, name) => {
|
|
394
|
+
const capabilities = isObject(shell) && Array.isArray(shell.capabilities) ? shell.capabilities.filter(isObject) : [];
|
|
395
|
+
const since = capabilities.find(capability => capability.name === name)?.minVersion;
|
|
396
|
+
return typeof since === 'string' && since.length > 0 ? since : undefined;
|
|
397
|
+
};
|
|
380
398
|
/**
|
|
381
399
|
* MOB-025 — açılışta sorma kabuğun ayrı bir yeteneğidir (`shell.carriesLaunchPermissions`). Yayındaki kabuk onu
|
|
382
400
|
* taşımıyorsa ayar saklanır ama derleme sormaz: liste manifeste yazılmaz. Eski backend alanı söylemez; o zaman
|
|
@@ -386,7 +404,8 @@ export function permissionLaunchShellWarnings(permissions, shell) {
|
|
|
386
404
|
const asked = permissions.filter(entry => entry.askOnLaunch === true).map(entry => text(entry.key));
|
|
387
405
|
if (asked.length === 0 || !isObject(shell) || shell.carriesLaunchPermissions !== false)
|
|
388
406
|
return [];
|
|
389
|
-
|
|
407
|
+
const since = shellSince(shell, 'launchPermissions');
|
|
408
|
+
return [`${asked.join(', ')}: açılışta sorulacak diye ayarlı ama yayındaki kabuk paketi (${text(shell.version)}) açılışta izin sormuyor${since ? ` (kabuk ${since} ister)` : ''} — ayar saklanır, mağaza derlemesi sormaz (ön kontrol P-12 uyarır). Daha yeni bir kabuk yayınlanana kadar izni bir akış adımından isteyin (MOB-025, ${guide}).`];
|
|
390
409
|
}
|
|
391
410
|
/**
|
|
392
411
|
* MOB-031 — uygulama kilidi kabuğun ayrı bir yeteneğidir (`shell.carriesAppLock`, kök ADR 0021 madde 8). Kilit açıkken
|
|
@@ -397,7 +416,8 @@ export function permissionLockShellWarnings(permissions, shell) {
|
|
|
397
416
|
const locked = permissions.some(entry => entry.key === APP_LOCK_PERMISSION && Array.isArray(entry.usage) && entry.usage.some(place => isObject(place) && place.source === 'lock'));
|
|
398
417
|
if (!locked || !isObject(shell) || shell.carriesAppLock !== false)
|
|
399
418
|
return [];
|
|
400
|
-
|
|
419
|
+
const since = shellSince(shell, 'appLock');
|
|
420
|
+
return [`appLock: uygulama kilidi açık ama yayındaki kabuk paketi (${text(shell.version)}) kilidi taşımıyor — ${since ? `kabuk ${since} ister` : 'daha yeni bir kabuk ister'}; ayar saklanır, mağaza sürümü kilitsiz açılır (ön kontrol P-12 uyarır; kabuk uyarıları tek satırda birleştiği için kod kabuğun sürümüne göre değişir) (MOB-031, ${guide}).`];
|
|
401
421
|
}
|
|
402
422
|
/**
|
|
403
423
|
* MOB-027 — izleme izni (kök ADR 0011): mağaza derlemesi `tracking`'i taşıyorsa App Store Connect App Privacy'de
|
|
@@ -2415,6 +2415,16 @@ export function orphanedFlowPlan(schema, pageEvents, ops) {
|
|
|
2415
2415
|
* akışı, akıllı öneri bağlanınca çalışmaz oldu). Olaya ikinci akış eklemek `add_event`'tir; bilerek
|
|
2416
2416
|
* değiştirmek için op'a `replaceBoundFlows: true` verilir. Okunan ağaçta olmayan hedef denetlenmez.
|
|
2417
2417
|
*/
|
|
2418
|
+
/**
|
|
2419
|
+
* PAGE-052 (PS-19): `set_event`/`add_event` akışı `eventValue` ile alır. `flowId` başka araçlarda başka anlam taşır;
|
|
2420
|
+
* platform yok sayar ve olaya boş bağ yazar, yanıt yine başarılıdır.
|
|
2421
|
+
*/
|
|
2422
|
+
export function flowIdEventOps(ops) {
|
|
2423
|
+
return asObjects(ops)
|
|
2424
|
+
.filter(op => (op.op === 'set_event' || op.op === 'add_event') && (typeof op.eventValue !== 'string' || op.eventValue === '') && typeof op.flowId === 'string' && op.flowId !== '')
|
|
2425
|
+
.map(op => `PAGE-052: '${String(op.target ?? op.elementId ?? '?')}' ${String(op.op)} op'u akışı flowId ile veriyor — olay akışı eventValue ile alır, flowId okunmaz ve platform olaya boş bağ yazar. `
|
|
2426
|
+
+ `{ op: '${String(op.op)}', target: '${String(op.target ?? op.elementId ?? '')}', event: '${String(op.event ?? '')}', eventValue: '${String(op.flowId)}' } yazın.`);
|
|
2427
|
+
}
|
|
2418
2428
|
export function boundEventOps(schema, ops) {
|
|
2419
2429
|
const nodes = asObjects(schema);
|
|
2420
2430
|
return asObjects(ops)
|
|
@@ -189,6 +189,14 @@ function widthFindings(root) {
|
|
|
189
189
|
if (!FLOW_TYPES[String(node.type ?? '')])
|
|
190
190
|
return;
|
|
191
191
|
if (flowDirection(node) === 'row') {
|
|
192
|
+
// MOB-043: yazılmış `row` ve sarmasız kapta üç ya da daha çok akış çocuğu, ikisi tam genişlik → telefonda dar
|
|
193
|
+
// sütunlara sıkışır, önizlemede de (ölçüldü 2026-11-04, f-10: 2 alan + düğme, her biri 106 dp). İki `fill`
|
|
194
|
+
// çocuğun eşit bölüşmesi ([İptal | Kaydet], Ad | Soyad) ve tek `fill` alan + düğme (arama satırı) bilinçli
|
|
195
|
+
// satırdır. Bağlı `wrap` çalışma anında açık olabilir; akış dışı tipler ve koşullu gizli çocuklar sayılmaz.
|
|
196
|
+
const inFlow = children.filter(child => !MOBILE_OUT_OF_FLOW_TYPES.includes(String(child.type ?? '')) && !boundVisibility(child));
|
|
197
|
+
if (['container', 'form'].includes(String(node.type ?? '')) && propData(node, 'direction') === 'row' && !setOrBound(node, 'wrap') && inFlow.length >= 3 && inFlow.filter(fills).length >= 2) {
|
|
198
|
+
findings.push(rule('MOB-043', label(node), "satır kapta üç ya da daha çok çocuk yan yana, en az ikisi tam genişlik (widthType: 'fill'): önizlemede de telefonda da dar sütunlara sıkışırlar. Alt alta isteniyorsa direction:'column' yazın; yan yana kalacaksa satırı ikiye bölün (iki fill çocuk eşit paylaşır)"));
|
|
199
|
+
}
|
|
192
200
|
// Yazılmış `row` bilinçlidir (arama satırı: alan + düğme); yazılmamış yön unutulmuş sütundur.
|
|
193
201
|
if (node !== root && propData(node, 'direction') === undefined && children.some(child => FIELD_TYPES.includes(String(child.type ?? '')))) {
|
|
194
202
|
findings.push(rule('MOB-020', label(node), "yönü yazılmamış kap form alanı taşıyor — yazılmamış yön satırdır, alanlar yan yana sıkışır. direction:'column' ve horizontalAlign:'stretch' yazın"));
|
|
@@ -109,6 +109,12 @@ export const MOBILE_RULES = {
|
|
|
109
109
|
sourceRef: 'zeyrek-mcp guides/mobile-layout.md §Sözleşme S8/S9; renderer.functions.ts containerDirection (yazılmamış yön satır)',
|
|
110
110
|
summary: "Genişlik tutarsızlığı: sütunda fill olmayan tam genişlik öğesi ya da form alanı taşıyan satır kabı"
|
|
111
111
|
},
|
|
112
|
+
'MOB-043': {
|
|
113
|
+
id: 'MOB-043',
|
|
114
|
+
severity: 'warn',
|
|
115
|
+
sourceRef: 'zeyrek-mcp guides/mobile-layout.md §Genişlik ve hiza; renderer.functions.ts resolveContainerStyle (wrap yoksa nowrap) + shellAxisStyle (satırda fill çocuk flexBasis 100%)',
|
|
116
|
+
summary: "Satırda üç ya da daha çok çocuk, ikisi tam genişlik: önizlemede ve telefonda dar sütunlara sıkışır"
|
|
117
|
+
},
|
|
112
118
|
'MOB-021': {
|
|
113
119
|
id: 'MOB-021',
|
|
114
120
|
severity: 'info',
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -806,6 +806,53 @@ export const policyRules = [
|
|
|
806
806
|
}];
|
|
807
807
|
}
|
|
808
808
|
},
|
|
809
|
+
{
|
|
810
|
+
id: 'MS-027',
|
|
811
|
+
module: 'microservice',
|
|
812
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
813
|
+
sourceRef: "zeyrek-backend ADR 0074 (b-03, 2026-11-04): bulk-update/bulk-delete ucunun filtresi kapsamdır — Microservice.Validate kök + single + and + eq + sunucu değerli (static/expression) ister, ifade çağıranın doldurduğu değişkeni okuyamaz (MicroserviceConstants.CallerValuedExpressionPattern), kapsam alanı uçta yazılamaz (BulkWriteScopeFilterException 400). Çalışmada kapsam her satırın UPDATE/DELETE WHERE'ine girer, kapsam dışı tek kimlik bütün partiyi 404 ile reddeder. Filtresiz uç her kimliği yazar",
|
|
814
|
+
check: definition => {
|
|
815
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
816
|
+
const operation = String(parameters.operation ?? '').toLowerCase();
|
|
817
|
+
if (definition.source !== 'table')
|
|
818
|
+
return [];
|
|
819
|
+
if (operation !== 'bulk-update' && operation !== 'bulk-delete')
|
|
820
|
+
return [];
|
|
821
|
+
const filters = asArray(parameters.filters).filter((filter) => Boolean(filter) && typeof filter === 'object');
|
|
822
|
+
const written = new Set(asArray(parameters.fields)
|
|
823
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
824
|
+
.flatMap(field => [field.fieldId, field.fieldSlug])
|
|
825
|
+
.filter(key => key !== undefined && key !== null)
|
|
826
|
+
.map(String));
|
|
827
|
+
const recipe = "Kapsamı sunucu belirler: { filterType: 'single', conditionType: 'and', field: <sahip alanı>, operator: 'eq', source: 'expression', value: 'user.id' } + auth middleware (middlewareIds).";
|
|
828
|
+
const broken = filters.filter(filter => filter.filterType === 'group' ||
|
|
829
|
+
asArray(filter.children).length > 0 ||
|
|
830
|
+
String(filter.conditionType ?? 'and') !== 'and' ||
|
|
831
|
+
String(filter.operator ?? 'eq') !== 'eq' ||
|
|
832
|
+
['body', 'header', 'query-string'].includes(String(filter.source ?? '')) ||
|
|
833
|
+
(String(filter.source ?? '') === 'expression' && /\b(body|header|queryString|cookie|route|RequestUrl|RequestPathAndQuery)\b/.test(String(filter.value ?? ''))) ||
|
|
834
|
+
written.has(String(filter.fieldId ?? '')) ||
|
|
835
|
+
written.has(String(filter.fieldSlug ?? '')));
|
|
836
|
+
if (broken.length > 0) {
|
|
837
|
+
return [{
|
|
838
|
+
ruleId: 'MS-027',
|
|
839
|
+
severity: 'block',
|
|
840
|
+
message: `operation '${operation}' filtresi kapsam olamaz (${broken.map(filter => String(filter.fieldSlug ?? filter.fieldId ?? '?')).join(', ')}): toplu yazma filtresi her satırın WHERE'ine girer, bu yüzden kök + 'and' + 'eq' olmalı, değeri istemciden (body/header/query-string ya da onları okuyan ifade) gelmemeli ve alan uçta yazılmamalı. Platform kaydı BulkWriteScopeFilterException ile reddeder. ${recipe}`,
|
|
841
|
+
guide: 'ngapps://guides/microservice'
|
|
842
|
+
}];
|
|
843
|
+
}
|
|
844
|
+
if (filters.length > 0 && asArray(definition.middlewareIds).length > 0)
|
|
845
|
+
return [];
|
|
846
|
+
return [{
|
|
847
|
+
ruleId: 'MS-027',
|
|
848
|
+
severity: 'warn',
|
|
849
|
+
message: filters.length === 0
|
|
850
|
+
? `operation '${operation}' ucunda filtre yok: istemci gönderdiği her kimliği yazar, başkasının satırı dahil. ${recipe} Kapsam dışı tek kimlik bütün partiyi 404 ile reddeder.`
|
|
851
|
+
: `operation '${operation}' ucunda middlewareIds boş: uç anonimdir ve kapsam ifadesi 'user' değişkenine ulaşamaz. Auth middleware'ini bağlayın (platform_guide('middleware')).`,
|
|
852
|
+
guide: 'ngapps://guides/microservice'
|
|
853
|
+
}];
|
|
854
|
+
}
|
|
855
|
+
},
|
|
809
856
|
{
|
|
810
857
|
id: 'MS-002',
|
|
811
858
|
module: 'microservice',
|
|
@@ -980,6 +1027,48 @@ export const policyRules = [
|
|
|
980
1027
|
});
|
|
981
1028
|
}
|
|
982
1029
|
},
|
|
1030
|
+
{
|
|
1031
|
+
id: 'WF-038',
|
|
1032
|
+
module: 'workflow',
|
|
1033
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1034
|
+
sourceRef: "zeyrek-backend Branch.cs BranchType: success · fail · impersonation · global-error; StringEnumConverter bilinmeyen değeri InvalidEnumTokenException (400) ile reddeder, workflow kaydı ve önizlemesi aynı BranchDto'yu kullanır. 'start' başlangıç adımının dal ADIDIR (workflow.md asgari iskelet, examples/workflow-call-workflow.json). Saha: Defter workflow_preview 400 'success, fail, impersonation, global-error' (PS-27, 2026-11-04)",
|
|
1035
|
+
check: (definition, context) => workflowSteps(workflowBody(definition, context.workflowKind)).flatMap((step) => {
|
|
1036
|
+
const startTyped = asArray(step.branches)
|
|
1037
|
+
.filter((branch) => Boolean(branch) && typeof branch === 'object')
|
|
1038
|
+
.filter(branch => String(branch.type ?? '') === 'start');
|
|
1039
|
+
return startTyped.length === 0
|
|
1040
|
+
? []
|
|
1041
|
+
: [{
|
|
1042
|
+
ruleId: 'WF-038',
|
|
1043
|
+
severity: 'block',
|
|
1044
|
+
message: `'${stepLabel(step)}' adımının dalı type: 'start' taşıyor — platform 400 döner (dal tipi yalnız success · fail · impersonation · global-error). 'start' dalın ADIDIR, tipi success: { name: 'start', type: 'success', nextStepId: '<ilk adım>' }.`,
|
|
1045
|
+
guide: 'ngapps://guides/workflow'
|
|
1046
|
+
}];
|
|
1047
|
+
})
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
id: 'WF-037',
|
|
1051
|
+
module: 'workflow',
|
|
1052
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1053
|
+
sourceRef: "EmailStep.cs EvaluateAsync: alıcı yalnız rule.expression'dan okunur; ifade { to, cc, bcc } dizgi listeleri döndürür (eski toMail/bccMails de okunur). StepRule'da send/to/toMail/sendTo/recipients alanı yoktur, bağlanırken atılır. Dizgi olmayan öğe EmailRecipientInvalidException, alıcısız adım EmailRecipientsMissingException ile fail dalına düşer (zeyrek-backend incident #197, 2026-11-04). Saha: Defter/Anatoli 31 deneme 'No recipients have been specified.'",
|
|
1054
|
+
check: (definition, context) => stepCallSites(workflowBody(definition, context.workflowKind), 'email').flatMap(({ label, rule }) => {
|
|
1055
|
+
const recipe = 'Alıcı rule.expression ile verilir ve { to, cc, bcc } adres dizgisi listeleri döndürür: "({ to: [input.email] })" — cc/bcc isteğe bağlıdır; konu rule.subject, HTML gövde rule.body, gönderen adı rule.displayName.';
|
|
1056
|
+
const ignoredKeys = ['send', 'to', 'toMail', 'toMails', 'sendTo', 'recipients', 'cc', 'bcc'].filter(key => carriesValue(rule[key]));
|
|
1057
|
+
const problems = [
|
|
1058
|
+
ignoredKeys.length > 0 ? `${ignoredKeys.map(key => `rule.${key}`).join(', ')} okunmaz, platform bağlarken atar` : '',
|
|
1059
|
+
!hasText(rule.expression) ? 'rule.expression yok — adımın alıcısı bağlı değil, platform EmailRecipientsMissingException ile fail dalına düşer' : '',
|
|
1060
|
+
hasText(rule.expression) && /\{\s*type\s*:/.test(String(rule.expression)) ? "alıcı öğesi { type, value } nesnesi — öğe adres dizgisi olmalı ('ad@alan'), platform EmailRecipientInvalidException ile fail dalına düşer" : ''
|
|
1061
|
+
].filter(problem => problem !== '');
|
|
1062
|
+
return problems.length === 0
|
|
1063
|
+
? []
|
|
1064
|
+
: [{
|
|
1065
|
+
ruleId: 'WF-037',
|
|
1066
|
+
severity: 'block',
|
|
1067
|
+
message: `'${label}' e-posta adımı alıcıyı yanlış yerden veriyor: ${problems.join('; ')}. ${recipe} workflow_step_schema'daki expression.to/cc/bcc bu ifadenin döndürdüğü şekildir.`,
|
|
1068
|
+
guide: 'ngapps://guides/workflow'
|
|
1069
|
+
}];
|
|
1070
|
+
})
|
|
1071
|
+
},
|
|
983
1072
|
{
|
|
984
1073
|
id: 'WF-032',
|
|
985
1074
|
module: 'workflow',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { maskSecretText } from '../platform/secrets.js';
|
|
3
|
+
import { logStep } from './trace.js';
|
|
4
|
+
const gatewayVersion = String(createRequire(import.meta.url)('../../package.json').version ?? 'bilinmiyor');
|
|
5
|
+
/**
|
|
6
|
+
* Kabiliyet boşluğu kaydını ekibin alıcısına gönderir (GW-014, ADR 0006). Adres yalnız `.env`'den gelir; tanımlı
|
|
7
|
+
* değilse hiçbir yere istek gitmez. Gövde boşluk kaydının kendisidir: proje adı dışında platform verisi, jeton ya da
|
|
8
|
+
* kullanıcı bilgisi eklemez. Serbest metinlerde yalnız jeton biçimleri (`Bearer …`, JWT) maskelenir; ajanın metne yazdığı
|
|
9
|
+
* başka bilgi (e-posta, satır verisi) olduğu gibi gider. Tek deneme, zaman aşımı kısa: alıcının
|
|
10
|
+
* yavaşlığı ajanı bekletmez, başarısızlık kaydı düşürmez — kayıt günlükte zaten durur.
|
|
11
|
+
*/
|
|
12
|
+
export async function deliverGapReport(url, gap, timeoutMs = 3000) {
|
|
13
|
+
if (!url)
|
|
14
|
+
return 'log-only';
|
|
15
|
+
const body = {
|
|
16
|
+
goal: maskSecretText(gap.goal),
|
|
17
|
+
blocker: maskSecretText(gap.blocker),
|
|
18
|
+
severity: gap.severity,
|
|
19
|
+
module: gap.module === undefined ? null : maskSecretText(gap.module),
|
|
20
|
+
attempted: (gap.attempted ?? []).map(entry => maskSecretText(entry)),
|
|
21
|
+
workaround: gap.workaround === undefined ? null : maskSecretText(gap.workaround),
|
|
22
|
+
project: gap.project ?? null,
|
|
23
|
+
gatewayVersion,
|
|
24
|
+
at: new Date().toISOString()
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
32
|
+
});
|
|
33
|
+
await response.body?.cancel();
|
|
34
|
+
if (response.ok)
|
|
35
|
+
return 'webhook';
|
|
36
|
+
logStep('boşluk webhook ⚠', `alıcı ${response.status} döndü — kayıt yalnız bu makinedeki günlükte`);
|
|
37
|
+
return 'webhook-failed';
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
logStep('boşluk webhook ⚠', `alıcıya ulaşılamadı (${error instanceof Error ? error.name : 'hata'}) — kayıt yalnız bu makinedeki günlükte`);
|
|
41
|
+
return 'webhook-failed';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -9,6 +9,12 @@ const hostAlias = (from, host) => ({
|
|
|
9
9
|
apply: async (value) => ({ set: { host, hostId: value }, note: `${from} → host: '${host}', hostId: ${text(value)}` })
|
|
10
10
|
});
|
|
11
11
|
export const ARGUMENT_ALIASES = [
|
|
12
|
+
{
|
|
13
|
+
tool: 'component_get',
|
|
14
|
+
from: 'type',
|
|
15
|
+
when: (_shape, args) => args.slug === undefined && args.id === undefined,
|
|
16
|
+
apply: async (value) => ({ set: { slug: text(value) }, note: `type → slug: '${text(value)}'` })
|
|
17
|
+
},
|
|
12
18
|
hostAlias('pageId', 'page'),
|
|
13
19
|
hostAlias('layoutId', 'layout'),
|
|
14
20
|
hostAlias('componentId', 'component'),
|
|
@@ -13,6 +13,25 @@ export const SWIPE_INTERVAL_MS = 16;
|
|
|
13
13
|
* taşıma bir satır eksik inerdi (inceleme bulgusu 2, oyun alanında `row-0 → row-2` → `reorder 0→1`).
|
|
14
14
|
*/
|
|
15
15
|
export const SWIPE_EDGE_INSET = 2;
|
|
16
|
+
/**
|
|
17
|
+
* g-15 (K8) — `click`'in ikinci yarısı. Sayfa içi betik hedefi seçer ve basılacak noktayı (`press`) döndürür; basış
|
|
18
|
+
* gateway'den gerçek fare olayıyla yapılır, çünkü yalnız gerçek olay o noktada en üstteki öğeye iner ve telefondaki
|
|
19
|
+
* parmakla aynı sonucu verir. `press` taşımayan sonuç (bulunamadı, örtülü, pencere kalkanı) olduğu gibi döner.
|
|
20
|
+
*/
|
|
21
|
+
export async function pressPoint(page, raw) {
|
|
22
|
+
if (raw === null || typeof raw !== 'object' || !('press' in raw))
|
|
23
|
+
return raw;
|
|
24
|
+
const { press, ...rest } = raw;
|
|
25
|
+
if (!page.mouse)
|
|
26
|
+
return { ...rest, applied: false, note: 'tarayıcı fare olayı desteklemiyor: tıklama UYGULANMADI' };
|
|
27
|
+
try {
|
|
28
|
+
await page.mouse.click(press.x, press.y);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { ...rest, applied: false, note: `fare tıklaması düştü, eylem UYGULANMADI: ${error instanceof Error ? error.message : String(error)}` };
|
|
32
|
+
}
|
|
33
|
+
return { ...rest, applied: true };
|
|
34
|
+
}
|
|
16
35
|
/**
|
|
17
36
|
* G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
|
|
18
37
|
* TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
|
|
@@ -63,8 +82,58 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
63
82
|
if (target.closest && target.closest('[role="tablist"]') && !node.matches('[role="tab"]')) {
|
|
64
83
|
return { found: true, applied: false, note: "hedef bir sekme grubu: click İLK sekmeye giderdi — sekme seçmek için { action: 'tab', target: '" + ${JSON.stringify(String(action.target ?? ''))} + "', value: '<sekme anahtarı ya da metni>' } kullanın", requests: count };
|
|
65
84
|
}
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
// g-15 (K8): tıklama burada YAPILMAZ. Basılacak nokta döner, gateway oraya gerçek fare olayıyla basar (pressPoint).
|
|
86
|
+
// Basış alanı hedefin kutusudur; kutusu boş form denetiminde (web anahtarının gizli girdisi) etiketi, kutusu boş
|
|
87
|
+
// basış yüzeyinde düğümün kendisi. Nokta "hedefin" sayılır: alanın içinde ve en yakın basılabilir atası hedefin
|
|
88
|
+
// kendisi ya da hedefi saran bir öğe. Hedefin içindeki BAŞKA basılabilir öğe (kabın düğmesi, çipin "kaldır"ı)
|
|
89
|
+
// hedefin noktası değildir; ortası öyleyse alanın başka noktaları denenir.
|
|
90
|
+
var pressable = 'button, a, input, select, textarea, [role="button"], [role="switch"], [role="checkbox"], [role="combobox"], [data-element-id$="${INTERNAL_ROLE_SEPARATOR}press"]';
|
|
91
|
+
var filled = function (element) { var r = element.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
92
|
+
var label = !filled(target) && target.labels && target.labels.length ? target.labels[0] : (!filled(target) && target.closest ? target.closest('label') : null);
|
|
93
|
+
var area = filled(target) ? target : label && filled(label) ? label : node;
|
|
94
|
+
var owns = function (hit) {
|
|
95
|
+
if (!hit || !area.contains(hit)) return false;
|
|
96
|
+
var owner = hit.closest ? hit.closest(pressable) : null;
|
|
97
|
+
return !owner || owner === target || owner.contains(target) || (label !== null && owner === label);
|
|
98
|
+
};
|
|
99
|
+
var spots = [[0.5, 0.5], [0.15, 0.5], [0.85, 0.5], [0.5, 0.2], [0.5, 0.8], [0.15, 0.2], [0.85, 0.2], [0.15, 0.8], [0.85, 0.8], [0.04, 0.5], [0.96, 0.5]];
|
|
100
|
+
var probe = function () {
|
|
101
|
+
var box = area.getBoundingClientRect();
|
|
102
|
+
if (box.width <= 0 || box.height <= 0) return { hidden: true };
|
|
103
|
+
var first = null;
|
|
104
|
+
for (var i = 0; i < spots.length; i++) {
|
|
105
|
+
var x = box.left + box.width * spots[i][0], y = box.top + box.height * spots[i][1];
|
|
106
|
+
if (x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight) continue;
|
|
107
|
+
var hit = document.elementFromPoint(x, y);
|
|
108
|
+
if (first === null) first = { hit: hit };
|
|
109
|
+
if (owns(hit)) return { at: { x: x, y: y } };
|
|
110
|
+
}
|
|
111
|
+
return first === null ? { outside: true } : first;
|
|
112
|
+
};
|
|
113
|
+
var found = probe();
|
|
114
|
+
// Ekran dışındaki ya da bir iç kaydırıcının görünen alanı dışında kalan hedef: kullanıcı önce kaydırır.
|
|
115
|
+
if (!found.at && !found.hidden && area.scrollIntoView) {
|
|
116
|
+
area.scrollIntoView({ block: 'center', inline: 'center' });
|
|
117
|
+
found = probe();
|
|
118
|
+
}
|
|
119
|
+
if (found.at) return { found: true, applied: false, requests: count, press: { x: found.at.x, y: found.at.y, id: target.getAttribute('data-element-id') } };
|
|
120
|
+
if (found.hidden || found.outside) {
|
|
121
|
+
return { found: true, applied: false, requests: count, note: "hedef görünmüyor (kutusu boş ya da kaydırınca da ekran dışında): gerçek kullanıcı ona basamaz, eylem uygulanmadı — görünürlüğü summaryText'ten okuyun" };
|
|
122
|
+
}
|
|
123
|
+
var hit = found.hit;
|
|
124
|
+
var inner = hit && area.contains(hit);
|
|
125
|
+
var cover = hit && hit.closest ? (inner ? hit.closest(pressable) : hit).closest('[data-element-id]') : null;
|
|
126
|
+
// Şema düğümü olmayan örtü (kabuğun bildirim katmanı, toast) kimlik taşımaz: rolü ve metni söylenir.
|
|
127
|
+
var described = function (element) {
|
|
128
|
+
var roled = element.closest ? element.closest('[role]') : null;
|
|
129
|
+
var shown = roled && roled.contains(element) && !roled.contains(area) ? roled : element;
|
|
130
|
+
var text = String(shown.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 40);
|
|
131
|
+
return String(shown.tagName).toLowerCase() + (shown.getAttribute && shown.getAttribute('role') ? '[role=' + shown.getAttribute('role') + ']' : '') + (text ? ' "' + text + '"' : '');
|
|
132
|
+
};
|
|
133
|
+
var coveredBy = cover ? String(cover.getAttribute('data-element-id')) : hit ? described(inner ? hit.closest(pressable) : hit) : 'bilinmeyen öğe';
|
|
134
|
+
return { found: true, applied: false, requests: count, coveredBy: coveredBy, note: inner
|
|
135
|
+
? "hedefin her noktasında içindeki başka bir basılabilir öğe (" + coveredBy + ") var: basış ona gider, telefonda da — eylem uygulanmadı. O öğeyi istiyorsanız onu hedefleyin"
|
|
136
|
+
: "hedefin üstünde " + coveredBy + " var: basışı o alır, telefonda da — eylem uygulanmadı. Örten bir stack katmanı ya da yerleşim kabıysa boş alanı dokunuşu geçirmeli; bilerek üstteyse onu hedefleyin" };
|
|
68
137
|
})()`;
|
|
69
138
|
}
|
|
70
139
|
// S-2 — sekme SEÇİLEMİYORDU: kimlik `<elementId>__tab-<key>` (frontend f-02) önce denenir,
|
|
@@ -3,7 +3,7 @@ import { parseDomSummary } from '../domSummary.js';
|
|
|
3
3
|
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/componentCatalog.js';
|
|
4
4
|
import { logStep } from '../../observability/trace.js';
|
|
5
5
|
import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfileRoot } from './persistent-browser.js';
|
|
6
|
-
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
6
|
+
import { actionScript, activeTabScript, pressPoint, rootElementId, runtimePropsScript } from './actions.js';
|
|
7
7
|
import { domSummaryScript } from './summary.js';
|
|
8
8
|
import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
|
|
9
9
|
import { awaitingUserNote, decideReadiness, describeUnready, parseAwaitingWindow, parseReadiness, readinessScript, runtimePath } from './readiness.js';
|
|
@@ -334,7 +334,9 @@ export class StudioScreenshotter {
|
|
|
334
334
|
const online = await page.evaluate('navigator.onLine').catch(() => undefined);
|
|
335
335
|
return { found: true, applied: true, requests: typeof count === 'number' ? count : 0, ...(typeof online === 'boolean' ? { online } : {}) };
|
|
336
336
|
})()
|
|
337
|
-
: await page.evaluate(actionScript(action, request.target))
|
|
337
|
+
: await page.evaluate(actionScript(action, request.target))
|
|
338
|
+
.then(result => action.action === 'click' ? pressPoint(page, result) : result)
|
|
339
|
+
.catch(error => ({
|
|
338
340
|
found: false,
|
|
339
341
|
applied: false,
|
|
340
342
|
note: error instanceof Error ? error.message : String(error),
|
|
@@ -406,6 +408,7 @@ export class StudioScreenshotter {
|
|
|
406
408
|
...(action.to === undefined ? {} : { to: action.to }),
|
|
407
409
|
found: before.found === true,
|
|
408
410
|
applied,
|
|
411
|
+
...(typeof before.coveredBy === 'string' ? { coveredBy: before.coveredBy } : {}),
|
|
409
412
|
...(propWritten === undefined ? {} : { propWritten }),
|
|
410
413
|
...(note === undefined ? {} : { note }),
|
|
411
414
|
requests,
|
|
@@ -169,7 +169,10 @@ function asStructured(payload) {
|
|
|
169
169
|
}
|
|
170
170
|
export function toolResult(payload, config, expectFailure) {
|
|
171
171
|
const pruned = collapseStackTraces(prune(compactDiagnostics(payload)));
|
|
172
|
-
const { payload:
|
|
172
|
+
const { payload: masked0, masked, tokenRefs } = maskSecrets(pruned);
|
|
173
|
+
const hinted = masked0 !== null && typeof masked0 === 'object' && !Array.isArray(masked0) && typeof masked0.truncationHint === 'string';
|
|
174
|
+
const truncationHint = hinted ? String(masked0.truncationHint) : undefined;
|
|
175
|
+
const safe = hinted ? Object.fromEntries(Object.entries(masked0).filter(([key]) => key !== 'truncationHint')) : masked0;
|
|
173
176
|
const runtimeErrors = runtimeErrorSummary(safe);
|
|
174
177
|
if (runtimeErrors.length > 0) {
|
|
175
178
|
// Kasıtlı negatif test (expectFailure:true) bir kusur değil bir KANIT: çağıran beklediğini
|
|
@@ -191,7 +194,7 @@ export function toolResult(payload, config, expectFailure) {
|
|
|
191
194
|
...(runtimeErrors.length > 0 && expectFailure === true ? { expectedFailure: true } : {}),
|
|
192
195
|
...(masked.length > 0 ? { gatewayMasked: masked } : {}),
|
|
193
196
|
...(Object.keys(tokenRefs).length > 0 ? { tokenRefs, tokenRefNote: `Maskelenen JWT'ler gateway içinde tutuldu; bu referansları microservice_execute / microservice_debug / page_runtime_get / page_screenshot 'asUser' alanına verebilirsiniz (${tokenTtlMinutes()} dk geçerli; kompleks projede referans access_session_create'ten gelir). Jetonun kendisi yanıta ve günlüğe hiç girmez.` } : {}),
|
|
194
|
-
...(truncated ? { truncated: true, truncationNote:
|
|
197
|
+
...(truncated ? { truncated: true, truncationNote: `Liste, yanıt boyutu sınırı nedeniyle kırpıldı; ${truncationHint ?? 'sayfalama ya da daha dar filtre kullanın.'}` } : {}),
|
|
195
198
|
...(argumentNotes().aliasesApplied.length > 0 ? { aliasesApplied: argumentNotes().aliasesApplied } : {}),
|
|
196
199
|
...(argumentNotes().bodyFile ? { bodyFile: argumentNotes().bodyFile } : {}),
|
|
197
200
|
...(argumentNotes().codeFile ? { codeFile: argumentNotes().codeFile } : {})
|
|
@@ -274,7 +274,8 @@ export function schemaOpDrops(schema, ops) {
|
|
|
274
274
|
const drops = { dropped: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
|
|
275
275
|
if (!Array.isArray(ops))
|
|
276
276
|
return drops;
|
|
277
|
-
|
|
277
|
+
const list = ops;
|
|
278
|
+
for (const [index, op] of list.entries()) {
|
|
278
279
|
if (op.op === 'add_component') {
|
|
279
280
|
const component = (op.component ?? {});
|
|
280
281
|
const elementId = String(component.elementId ?? '');
|
|
@@ -298,8 +299,58 @@ export function schemaOpDrops(schema, ops) {
|
|
|
298
299
|
continue;
|
|
299
300
|
}
|
|
300
301
|
mergeNested(drops, writeDiff({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) }, {}, false), `replace_prop '${op.target}' → `);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if ((op.op === 'set_event' || op.op === 'add_event') && typeof op.target === 'string' && typeof op.event === 'string') {
|
|
305
|
+
const saved = findByElementId(schema, op.target);
|
|
306
|
+
const wanted = { name: op.event, value: op.eventValue };
|
|
307
|
+
// Aynı yamada sonra gelen `set_event` bağı değiştirir, `remove_event` (akışı verilmemişse ya da aynıysa) kaldırır:
|
|
308
|
+
// son hâli kayıtlı şemayla karşılaştırmak o op'un işidir.
|
|
309
|
+
const superseded = list.slice(index + 1).some(later => later.target === op.target
|
|
310
|
+
&& (later.op === 'set_event' || later.op === 'remove_event')
|
|
311
|
+
&& String(later.event ?? '').toLowerCase() === String(op.event).toLowerCase()
|
|
312
|
+
&& (later.op === 'set_event' || later.eventValue === undefined || later.eventValue === null || later.eventValue === ''
|
|
313
|
+
|| String(later.eventValue).toLowerCase() === String(op.eventValue ?? '').toLowerCase()));
|
|
314
|
+
if (!saved || superseded)
|
|
315
|
+
continue;
|
|
316
|
+
const bound = Array.isArray(saved.events) && saved.events.some(event => sameEvent(event, wanted));
|
|
317
|
+
if (!bound)
|
|
318
|
+
addDrop(drops, `${op.op} '${op.target}.${op.event}': olay bağı şemada yok — yama 200 döndü ama bağ kaydedilmemiş.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return finishReport(drops);
|
|
322
|
+
}
|
|
323
|
+
/** Ad büyük/küçük harf duyarsız (platform `Page.AddEvent` böyle karşılaştırır); değer verilmediyse yalnız ad. */
|
|
324
|
+
function sameEvent(saved, wanted) {
|
|
325
|
+
if (!saved || typeof saved !== 'object')
|
|
326
|
+
return false;
|
|
327
|
+
const record = saved;
|
|
328
|
+
const sameName = String(record.name ?? '').toLowerCase() === String(wanted.name ?? '').toLowerCase();
|
|
329
|
+
return sameName && (wanted.value === undefined || wanted.value === null || wanted.value === ''
|
|
330
|
+
|| String(record.value ?? '').toLowerCase() === String(wanted.value).toLowerCase());
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Tam sayfa yazımında (`page_update`, `page_create ifExists:'update'`) gönderilen olay ve düğüm sayısını PUT
|
|
334
|
+
* yanıtındaki KAYITLI sayfayla karşılaştırır (PAGE-053). Platform aynı akışa bağlı ikinci olayı hatasız atabiliyordu
|
|
335
|
+
* ve yanıt gönderilen gövdeden "korundu" diyordu. `saved` yoksa (yanıt kaydı taşımıyorsa) karşılaştırma yapılmaz.
|
|
336
|
+
*/
|
|
337
|
+
export function pageWriteDrops(sent, saved) {
|
|
338
|
+
const drops = { dropped: [], droppedCount: 0, droppedCountCapped: false, notes: [] };
|
|
339
|
+
if (!saved)
|
|
340
|
+
return drops;
|
|
341
|
+
const savedEvents = Array.isArray(saved.events) ? saved.events : [];
|
|
342
|
+
for (const event of Array.isArray(sent.events) ? sent.events : []) {
|
|
343
|
+
const record = (event ?? {});
|
|
344
|
+
if (!savedEvents.some(candidate => sameEvent(candidate, { name: record.name, value: record.value }))) {
|
|
345
|
+
addDrop(drops, `events[${String(record.name ?? '')} → ${String(record.value ?? '')}]: platform bu olayı kaydetmedi.`);
|
|
301
346
|
}
|
|
302
347
|
}
|
|
348
|
+
if (drops.droppedCount > 0) {
|
|
349
|
+
drops.notes.push("Aynı akış iki olaya bağlıysa eski backend sürümü ikincisini atar: olaya ayrı akış bağlayın ya da backend'i güncelleyin; sonucu page_get ile doğrulayın.");
|
|
350
|
+
}
|
|
351
|
+
if (saved.schemaNodes < sent.schemaNodes) {
|
|
352
|
+
addDrop(drops, `schema: ${sent.schemaNodes} düğüm gönderildi, ${saved.schemaNodes} düğüm kaydedildi — eksik düğümleri page_schema_get ile bulun.`);
|
|
353
|
+
}
|
|
303
354
|
return finishReport(drops);
|
|
304
355
|
}
|
|
305
356
|
/** İç karşılaştırmanın satırlarını, sayısını ve notlarını dış rapora önekle taşır; kesme en dışta yapılır. */
|