@appaflytech/zeyrek-ai-mcp 0.0.62 → 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.
Files changed (41) hide show
  1. package/.env.example +6 -0
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/backendConstraints.js +2 -2
  4. package/dist/knowledge/mobileStore.js +24 -4
  5. package/dist/knowledge/normalize.js +10 -0
  6. package/dist/knowledge/platform/mobileLayoutLint.js +8 -0
  7. package/dist/knowledge/platform/mobileRules.js +6 -0
  8. package/dist/knowledge/rules.js +89 -0
  9. package/dist/observability/gapSink.js +43 -0
  10. package/dist/platform/argumentAliases.js +6 -0
  11. package/dist/platform/capture/actions.js +71 -2
  12. package/dist/platform/capture/capture.js +5 -2
  13. package/dist/platform/responses.js +5 -2
  14. package/dist/platform/writeDiff.js +52 -1
  15. package/dist/resources/examples/middleware-password-reset.json +1281 -0
  16. package/dist/resources/examples/mobile-reorder-persist.json +33 -7
  17. package/dist/resources/examples/workflow-email-step.json +67 -0
  18. package/dist/resources/guides/deployment.md +13 -1
  19. package/dist/resources/guides/entity.md +7 -0
  20. package/dist/resources/guides/expressions.md +5 -0
  21. package/dist/resources/guides/getting-started.md +15 -30
  22. package/dist/resources/guides/microservice.md +30 -2
  23. package/dist/resources/guides/middleware.md +83 -3
  24. package/dist/resources/guides/mobile-layout.md +35 -3
  25. package/dist/resources/guides/mobile-publish.md +33 -1
  26. package/dist/resources/guides/mobile-steps.md +12 -2
  27. package/dist/resources/guides/mobile.md +11 -8
  28. package/dist/resources/guides/page.md +28 -3
  29. package/dist/resources/guides/workflow.md +30 -1
  30. package/dist/resources/schema-fingerprint.json +2 -2
  31. package/dist/tools/applications.js +6 -3
  32. package/dist/tools/common.js +8 -0
  33. package/dist/tools/designSchemaPatch.js +4 -1
  34. package/dist/tools/flows.js +14 -4
  35. package/dist/tools/layouts.js +3 -1
  36. package/dist/tools/microservices.js +8 -2
  37. package/dist/tools/mobileStore.js +13 -2
  38. package/dist/tools/pages.js +27 -12
  39. package/dist/tools/platformInfo.js +15 -6
  40. package/dist/tools/workflows.js +4 -2
  41. 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 !== undefined && operation.startsWith('bulk-') && asObjects(parameters.filters).length > 0) {
421
- findings.push(`sourceParameters.filters: '${operation}' ucu filtre KULLANMAZ — bulk create/update satırları gövdeden, bulk delete birincil anahtarları gövdeden okur. Filtreleri kaldırın.`);
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`), kullandığı izin ve kabuk yeteneğinin sürümü (`MobileBuildOptions` `appLock`) */
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
- return [`${asked.join(', ')}: açılışta sorulacak diye ayarlı ama yayındaki kabuk paketi (${text(shell.version)}) açılışta izin sormuyor — 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}).`];
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
- return [`appLock: uygulama kilidi açık ama yayındaki kabuk paketi (${text(shell.version)}) kilidi taşımıyor — kabuk ${APP_LOCK_SHELL_VERSION} 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}).`];
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',
@@ -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
- target.click();
67
- return { found: true, applied: true, requests: count };
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)).catch(error => ({
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: safe, masked, tokenRefs } = maskSecrets(pruned);
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: 'Liste, yanıt boyutu sınırı nedeniyle kırpıldı; sayfalama ya da daha dar filtre kullanın.' } : {}),
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
- for (const op of ops) {
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. */