@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.53

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 (39) hide show
  1. package/dist/knowledge/componentCatalog.js +3 -1
  2. package/dist/knowledge/componentEvents.js +13 -1
  3. package/dist/knowledge/mobileComponentCatalog.js +4 -1
  4. package/dist/knowledge/normalize.js +4 -3
  5. package/dist/platform/argumentAliases.js +13 -4
  6. package/dist/platform/bodyFile.js +195 -38
  7. package/dist/platform/capture/actions.js +24 -5
  8. package/dist/platform/capture/capture.js +80 -11
  9. package/dist/platform/capture/persistent-browser.js +23 -1
  10. package/dist/platform/capture/readiness.js +32 -1
  11. package/dist/platform/errors.js +45 -5
  12. package/dist/platform/http.js +17 -9
  13. package/dist/platform/responses.js +7 -1
  14. package/dist/platform/secrets.js +8 -0
  15. package/dist/platform/token.js +32 -21
  16. package/dist/resources/examples/mobile-offline-list.json +1 -1
  17. package/dist/resources/examples/mobile-offline-queue.json +1 -1
  18. package/dist/resources/examples/mobile-offline-seamless.json +12 -3
  19. package/dist/resources/guides/deployment.md +9 -0
  20. package/dist/resources/guides/microservice.md +13 -1
  21. package/dist/resources/guides/mobile-publish.md +4 -2
  22. package/dist/resources/guides/mobile-steps.md +40 -3
  23. package/dist/resources/guides/mobile.md +38 -8
  24. package/dist/resources/guides/page.md +39 -8
  25. package/dist/resources/guides/translation.md +3 -1
  26. package/dist/resources/schema-fingerprint.json +2 -2
  27. package/dist/tools/applications.js +7 -2
  28. package/dist/tools/deployments.js +1 -1
  29. package/dist/tools/designSchemaPatch.js +7 -2
  30. package/dist/tools/environments.js +40 -0
  31. package/dist/tools/flows.js +30 -3
  32. package/dist/tools/mobile.js +56 -0
  33. package/dist/tools/mobileStore.js +1 -1
  34. package/dist/tools/pages.js +45 -11
  35. package/dist/tools/platformInfo.js +1 -1
  36. package/dist/tools/screenWrite.js +107 -1
  37. package/dist/tools/variables.js +1 -1
  38. package/package.json +1 -1
  39. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -6,7 +6,7 @@ import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfile
6
6
  import { actionScript, activeTabScript, 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
- import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
9
+ import { awaitingUserNote, decideReadiness, describeUnready, parseAwaitingWindow, parseReadiness, readinessScript, runtimePath } from './readiness.js';
10
10
  /** Runtime'ın Connect Data bağlantısı için bastığı sözde akış kimliği: hata bir akış adımından değil bağlı prop'un okumasından gelir. */
11
11
  const CONNECT_DATA_FLOW_ID = 'connect-data';
12
12
  /**
@@ -159,6 +159,9 @@ const resizeSettleBudgetMs = 3000;
159
159
  * çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
160
160
  */
161
161
  const navigationAttempts = 2;
162
+ /** g-12 oturumları: depo durumu çerez (jeton dahil) taşıdığı için diske değil süreç belleğine yazılır. */
163
+ const sessionTtlMs = 30 * 60_000;
164
+ const maxSessions = 8;
162
165
  /**
163
166
  * Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
164
167
  * hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
@@ -232,6 +235,7 @@ export class StudioScreenshotter {
232
235
  queue = Promise.resolve();
233
236
  pending = 0;
234
237
  recycleNext = false;
238
+ sessions = new Map();
235
239
  constructor(config, launcher) {
236
240
  this.config = config;
237
241
  this.launcher = launcher;
@@ -307,7 +311,7 @@ export class StudioScreenshotter {
307
311
  * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
308
312
  * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
309
313
  */
310
- async runActions(page, request, consoleLog) {
314
+ async runActions(page, context, request, consoleLog) {
311
315
  if (!request.actions || request.actions.length === 0)
312
316
  return undefined;
313
317
  const outcomes = [];
@@ -319,12 +323,23 @@ export class StudioScreenshotter {
319
323
  }
320
324
  const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
321
325
  const activityBefore = snapshotBefore?.activity ?? null;
322
- const raw = await page.evaluate(actionScript(action, request.target)).catch(error => ({
323
- found: false,
324
- applied: false,
325
- note: error instanceof Error ? error.message : String(error),
326
- requests: 0
327
- }));
326
+ // `network` (g-12) sayfada betik koşturmaz: bağlamın ağını değiştirir ve sayfanın gördüğü `navigator.onLine`'ı
327
+ // okur. Ağ açılınca koşan eşitleme akışları aşağıdaki aynı hazırlık beklemesiyle `flowRuns`'a yazılır.
328
+ const raw = action.action === 'network'
329
+ ? context.setOffline === undefined
330
+ ? { found: true, applied: false, note: 'ağ denetimi yok: tarayıcı bağlamı setOffline desteklemiyor — ağ DEĞİŞMEDİ', requests: 0 }
331
+ : await (async () => {
332
+ const count = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => 0);
333
+ await context.setOffline?.(action.value === 'offline');
334
+ const online = await page.evaluate('navigator.onLine').catch(() => undefined);
335
+ return { found: true, applied: true, requests: typeof count === 'number' ? count : 0, ...(typeof online === 'boolean' ? { online } : {}) };
336
+ })()
337
+ : await page.evaluate(actionScript(action, request.target)).catch(error => ({
338
+ found: false,
339
+ applied: false,
340
+ note: error instanceof Error ? error.message : String(error),
341
+ requests: 0
342
+ }));
328
343
  const before = (raw ?? {});
329
344
  // Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
330
345
  // iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
@@ -335,6 +350,9 @@ export class StudioScreenshotter {
335
350
  : action.target.includes(INTERNAL_ROLE_SEPARATOR)
336
351
  ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
337
352
  : `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
353
+ const openWindow = parseAwaitingWindow(before.openWindow);
354
+ if (openWindow !== null)
355
+ before.note = `${before.note} Açık önizleme penceresi: ${openWindow.id}${openWindow.kind === null ? '' : ` (${openWindow.kind})`}; sayfanın düğmeleri onun altında kalır.`;
338
356
  }
339
357
  // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
340
358
  // sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
@@ -376,9 +394,11 @@ export class StudioScreenshotter {
376
394
  : undefined;
377
395
  const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
378
396
  const applied = before.applied === true && propWritten !== false;
397
+ // g-11: eylem bir önizleme penceresi açtıysa (onay, uyarı, biyometri) sıradaki eylemin neye basacağı burada söylenir.
398
+ const awaitingNote = readiness.state === 'awaiting-user' ? awaitingUserNote(readiness.snapshot?.awaitingWindow) : undefined;
379
399
  const note = propWritten === false
380
400
  ? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
381
- : before.note;
401
+ : [before.note, awaitingNote].filter((part) => part !== undefined && part !== '').join(' ') || undefined;
382
402
  outcomes.push({
383
403
  index,
384
404
  action: String(action.action),
@@ -395,6 +415,7 @@ export class StudioScreenshotter {
395
415
  ...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
396
416
  ...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
397
417
  ...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
418
+ ...(before.online === undefined ? {} : { online: before.online }),
398
419
  pageState: readiness.state
399
420
  });
400
421
  }
@@ -491,7 +512,47 @@ export class StudioScreenshotter {
491
512
  context = await browser.newContext(contextOptions);
492
513
  }
493
514
  logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}${request.motion === 'play' ? ' · hareket oynar' : ''}${request.headers ? ` · başlık: ${Object.keys(request.headers).join(', ')}` : ''}`);
515
+ let sessionRestored;
516
+ // Oturum depoyu bir kez kaydeder: başarılı yolda yanıttan önce, hata yolunda `finally`'de. Yükleme yarıda kaldıysa
517
+ // (`setStorageState` önce siler, sonra yazar) yarı boş depo iyi kaydın üstüne yazılmaz (g-12 inceleme #6).
518
+ let sessionPending = request.session !== undefined;
519
+ // Okunamayan depo eski kaydı BIRAKMAZ: bırakılsaydı sonraki çekim aradaki yazmaları sessizce kaybeden eski depoyu
520
+ // `sessionRestored: true` ile yüklerdi (g-12 inceleme #5).
521
+ const saveSession = async () => {
522
+ sessionPending = false;
523
+ if (request.session === undefined || !context.storageState)
524
+ return true;
525
+ const state = await context.storageState().catch(() => undefined);
526
+ this.sessions.delete(request.session);
527
+ if (state === undefined)
528
+ return false;
529
+ this.sessions.set(request.session, { state, at: Date.now() });
530
+ for (const [key, entry] of this.sessions) {
531
+ if (Date.now() - entry.at > sessionTtlMs || this.sessions.size > maxSessions)
532
+ this.sessions.delete(key);
533
+ }
534
+ return true;
535
+ };
494
536
  try {
537
+ // Oturum çerezlerden ÖNCE yüklenir: `setStorageState` mevcut depoyu siler, sonra yazılan jeton çerezi kalsın.
538
+ // Yeteneği olmayan bağlamda çağrı sessizce taze depoyla çekilmez (colorScheme'in kuralı).
539
+ if (request.session !== undefined) {
540
+ if (!context.storageState || !context.setStorageState) {
541
+ throw new PlatformApiError(`session='${request.session.split('|')[0]}' uygulanamadı: tarayıcı bağlamı depo durumunu okuyup yükleyemiyor (storageState/setStorageState). Kare taze depoyla çekilirdi ve bu sessizce yanlış olurdu.`);
542
+ }
543
+ const saved = this.sessions.get(request.session);
544
+ if (saved !== undefined && Date.now() - saved.at > sessionTtlMs)
545
+ this.sessions.delete(request.session);
546
+ sessionRestored = saved !== undefined && Date.now() - saved.at <= sessionTtlMs;
547
+ // İlk çağrı da boş depoyla başlar: kalıcı profilde önceki çekimlerden kalan köken anlık görüntüye girmesin (#8).
548
+ try {
549
+ await context.setStorageState(sessionRestored && saved !== undefined ? saved.state : { cookies: [], origins: [] });
550
+ }
551
+ catch (error) {
552
+ sessionPending = false;
553
+ throw error;
554
+ }
555
+ }
495
556
  const cookies = new Map();
496
557
  if (request.token)
497
558
  cookies.set('NG_AccessToken', request.token);
@@ -573,7 +634,7 @@ export class StudioScreenshotter {
573
634
  : readiness.state === 'not-ready' ? false : undefined;
574
635
  const finalHref = readiness.snapshot?.href;
575
636
  const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
576
- const actions = await this.runActions(page, request, consoleLog);
637
+ const actions = await this.runActions(page, context, request, consoleLog);
577
638
  // Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
578
639
  // boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
579
640
  // tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
@@ -643,6 +704,7 @@ export class StudioScreenshotter {
643
704
  if (runtimeErrors.length > 0) {
644
705
  logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
645
706
  }
707
+ const sessionSaved = await saveSession();
646
708
  return {
647
709
  ...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
648
710
  ...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
@@ -653,6 +715,7 @@ export class StudioScreenshotter {
653
715
  renderWaitTimedOut: apiStatus === undefined,
654
716
  elapsedMs,
655
717
  pageState: readiness.state,
718
+ ...(readiness.state === 'awaiting-user' && readiness.snapshot?.awaitingWindow ? { awaitingWindow: readiness.snapshot.awaitingWindow } : {}),
656
719
  readyAfterMs: readiness.waitedMs,
657
720
  ...(finalUrl === undefined ? {} : { finalUrl }),
658
721
  ...(measured === undefined ? {} : { contentHeight: measured }),
@@ -665,10 +728,16 @@ export class StudioScreenshotter {
665
728
  ...(pageErrors.length > 0 ? { pageErrors } : {}),
666
729
  ...(renderErrors.length > 0 ? { renderErrors } : {}),
667
730
  ...(flowsSettled === undefined ? {} : { flowsSettled }),
668
- ...(navigationRetried ? { navigationRetried: true } : {})
731
+ ...(navigationRetried ? { navigationRetried: true } : {}),
732
+ ...(sessionRestored === undefined ? {} : { sessionRestored }),
733
+ ...(sessionSaved ? {} : { sessionSaved: false })
669
734
  };
670
735
  }
671
736
  finally {
737
+ // Hata yolunda da depo kapanıştan ÖNCE alınır (kalıcı profil kapanışta depoyu siler): ağsız yazılmış kayıt
738
+ // çekim düşse de oturumda kalır. En yenisi sona yazılır; süresi dolan ve sekizi aşan en eski oturum düşer.
739
+ if (sessionPending)
740
+ await saveSession();
672
741
  await context.close().catch(() => undefined);
673
742
  // Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
674
743
  // kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
@@ -72,6 +72,8 @@ export class PersistentScreenshotBrowser {
72
72
  * Tek çekimlik oturum. Eski taze bağlamın yalıtımı korunur: başlık ve init script'i bağlama
73
73
  * değil sayfaya yazılır (sonraki çekime sızmaz — `impersonated-user-id` dahil), kapanışta
74
74
  * çerezler ve gezilen kökenlerin depoları (localStorage, IndexedDB, …) diskteki profilden silinir.
75
+ * `session` (g-12) bu silmeyi atlamaz: depo kapanıştan önce okunur, sonraki çekimde yüklenir; yüklenen kökenler de
76
+ * kapanışta silinir. Ağ kesildiyse sayfalar kapandıktan SONRA geri açılır — bağlam sonraki çekimle paylaşılır.
75
77
  */
76
78
  class PersistentContextSession {
77
79
  context;
@@ -79,6 +81,8 @@ class PersistentContextSession {
79
81
  initScripts = [];
80
82
  headers;
81
83
  pages = [];
84
+ restoredOrigins = new Set();
85
+ offline = false;
82
86
  constructor(context, options) {
83
87
  this.context = context;
84
88
  this.options = options;
@@ -92,6 +96,20 @@ class PersistentContextSession {
92
96
  async setExtraHTTPHeaders(headers) {
93
97
  this.headers = headers;
94
98
  }
99
+ async setOffline(offline) {
100
+ await this.context.setOffline(offline);
101
+ this.offline = offline;
102
+ }
103
+ storageState() {
104
+ return this.context.storageState({ indexedDB: true });
105
+ }
106
+ async setStorageState(state) {
107
+ for (const entry of state.origins ?? []) {
108
+ if (typeof entry.origin === 'string')
109
+ this.restoredOrigins.add(entry.origin);
110
+ }
111
+ await this.context.setStorageState(state);
112
+ }
95
113
  async newPage() {
96
114
  const page = await this.context.newPage();
97
115
  this.pages.push(page);
@@ -104,7 +122,7 @@ class PersistentContextSession {
104
122
  return page;
105
123
  }
106
124
  async close() {
107
- const origins = new Set();
125
+ const origins = new Set(this.restoredOrigins);
108
126
  for (const page of this.pages) {
109
127
  try {
110
128
  const origin = new URL(page.url()).origin;
@@ -126,5 +144,9 @@ class PersistentContextSession {
126
144
  await this.context.clearCookies().catch(() => undefined);
127
145
  for (const page of this.pages)
128
146
  await page.close().catch(() => undefined);
147
+ // Ağ EN SON açılır: sayfa açıkken açılsaydı çevrimdışı uygulama `online` olayıyla kuyruğunu sunucuya boşaltırdı —
148
+ // salt okunur bir çekimden görünmez yazma, oturumla taşınan kuyrukta da çift kayıt (g-12 inceleme #4).
149
+ if (this.offline)
150
+ await this.context.setOffline(false).catch(() => undefined);
129
151
  }
130
152
  }
@@ -14,6 +14,13 @@
14
14
  * (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
15
15
  * neyin görüldüğünü zaten söyler.
16
16
  */
17
+ /**
18
+ * Önizlemenin kendi pencereleri — uygulama düğümü değil, Studio kabuğunun öğesi. Açıkken bir akış adımı kullanıcının
19
+ * yanıtını bekler: `biometric-auth` (kök ADR 0021, `@ng-apps/ui-native` `preview-biometric.tsx`) ve `confirm`/`alert`
20
+ * (Studio `components/dialog`, plans/defter-saha-bulgulari f-11; tür `data-dialog-kind`'da).
21
+ * Seçici yalnız çift tırnak taşır: betiklere tek tırnakla gömülür.
22
+ */
23
+ export const AWAITING_WINDOW_SELECTOR = '[data-element-id="preview-biometric"], [data-element-id="preview-dialog"]';
17
24
  export const readinessScript = `(function () { /* readinessSnapshot */
18
25
  var activity = window.__ngAppsFlowActivity;
19
26
  return {
@@ -22,11 +29,34 @@ export const readinessScript = `(function () { /* readinessSnapshot */
22
29
  skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
23
30
  viewDetail: !!document.querySelector('.view-detail'),
24
31
  errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
25
- awaitingUser: !!document.querySelector('[data-element-id="preview-biometric"]'),
32
+ awaitingUser: !!document.querySelector('${AWAITING_WINDOW_SELECTOR}'),
33
+ awaitingWindow: (function () {
34
+ var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
35
+ return open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null;
36
+ })(),
26
37
  readyState: String(document.readyState),
27
38
  href: String(location.href)
28
39
  };
29
40
  })()`;
41
+ /** Tarayıcıdan gelen pencere değeri; kimliği olmayan değer pencere sayılmaz. */
42
+ export function parseAwaitingWindow(raw) {
43
+ if (!raw || typeof raw !== 'object')
44
+ return null;
45
+ const { id, kind } = raw;
46
+ return typeof id === 'string' && id !== '' ? { id, kind: typeof kind === 'string' ? kind : null } : null;
47
+ }
48
+ /**
49
+ * `awaiting-user` karesinin ve pencere açıkken bulunamayan `click` hedefinin notu: hangi pencere açık, dalı sürmek için hangi
50
+ * düğmeye basılır. Kimlikler çevrilmiş metinden bağımsızdır; production'da (cihaz, `/view`) bu pencereler yoktur.
51
+ */
52
+ export function awaitingUserNote(window) {
53
+ if (window?.id === 'preview-dialog') {
54
+ return window.kind === 'alert'
55
+ ? "Bir akış kullanıcı penceresini bekliyor: önizlemenin uyarı penceresi (alert adımı) açık. Kare pencereyi gösterir; kapatıp akışı sürdürmek için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }. Kimlik yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir."
56
+ : "Bir akış kullanıcı penceresini bekliyor: önizlemenin onay penceresi (confirm adımı) açık. Kare pencereyi gösterir; onay dalı için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }, red dalı için 'preview-dialog__cancel'. Kimlikler yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir.";
57
+ }
58
+ return "Bir akış kullanıcı penceresini bekliyor: önizlemenin biyometri penceresi açık (kök ADR 0021). Kare pencereyi gösterir; dalı sürmek için sonraki eylem { action: 'click', target: 'preview-biometric__verify' } (Doğrulandı say) ya da 'preview-biometric__cancel' (vazgeç). Production /view'da pencere yoktur.";
59
+ }
30
60
  /** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
31
61
  export function isFlowActivity(activity) {
32
62
  if (!activity || typeof activity !== 'object')
@@ -48,6 +78,7 @@ export function parseReadiness(raw) {
48
78
  viewDetail: value.viewDetail === true,
49
79
  errorState: value.errorState === true,
50
80
  awaitingUser: value.awaitingUser === true,
81
+ awaitingWindow: parseAwaitingWindow(value.awaitingWindow),
51
82
  readyState: String(value.readyState ?? ''),
52
83
  href: value.href
53
84
  };
@@ -12,6 +12,12 @@ export class PlatformApiError extends Error {
12
12
  * izin slug'ları geçer ('entity.reveal grant'i'); `accessDecision` izni ondan okursa yanlış izin yazar.
13
13
  */
14
14
  apiMessage;
15
+ /**
16
+ * Ağ hatası geçici: ad çözümleme ya da yanıt öncesi bağlantı kopması (D-19). Ajan tekrar edilebilirliği metni
17
+ * ayrıştırmadan bilsin diye araç hata yanıtına `transient: true` olarak çıkar. Zaman aşımı geçici SAYILMAZ:
18
+ * uzun işlemi tekrar etmek aynı süreyi bir kez daha bekletir.
19
+ */
20
+ transient;
15
21
  constructor(message, status, code) {
16
22
  super(message);
17
23
  this.status = status;
@@ -474,17 +480,51 @@ export function mapPlatformError(status, payload, context) {
474
480
  }
475
481
  const CONNECTION_RESET_CODES = new Set(['ECONNRESET', 'UND_ERR_SOCKET', 'EPIPE', 'ECONNABORTED']);
476
482
  /**
477
- * Bağlantı yolda koptu mu (yanıt gelmeden): Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara
478
- * düşürür — konteyner içinden aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800).
483
+ * Ad çözümlemesi bağlantı açılmadan düşer; istek sunucuya hiç gitmemiştir. Defter uzun betiklerin ortasında `ENOTFOUND`
484
+ * gördü ve kendi istemcisine üç denemelik sarmal yazdı (D-19).
479
485
  */
480
- export function isConnectionReset(error) {
486
+ const NAME_RESOLUTION_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN']);
487
+ function transientNetworkKind(error) {
481
488
  const failure = error;
482
- return CONNECTION_RESET_CODES.has(String(failure?.code)) || CONNECTION_RESET_CODES.has(String(failure?.cause?.code));
489
+ const codes = [String(failure?.code), String(failure?.cause?.code)];
490
+ if (codes.some(code => NAME_RESOLUTION_CODES.has(code)))
491
+ return 'name-resolution';
492
+ if (codes.some(code => CONNECTION_RESET_CODES.has(code)))
493
+ return 'connection-reset';
494
+ return undefined;
495
+ }
496
+ /**
497
+ * Tekrarı zararsız istekte (GET/HEAD, kimlik jetonu) geçici ağ hatasından sonraki beklemeler; uzunluğu tekrar sayısıdır
498
+ * (D-19: en çok iki tekrar). Zaman aşımı bu yola girmez — varsayılan istek süresi 100 sn, tekrar onu bir kez daha bekletirdi.
499
+ */
500
+ export const NETWORK_RETRY_DELAYS_MS = [250, 1000];
501
+ /**
502
+ * Yanıt gelmeden düşen ve kısa beklemeyle geçebilen ağ hatası: ad çözümleme (`ENOTFOUND`/`EAI_AGAIN`) ya da yolda kopan
503
+ * bağlantı. Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara düşürür — konteyner içinden
504
+ * aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800). `undici` kodu kimi
505
+ * zaman `cause.code`'da taşır.
506
+ */
507
+ export function isTransientNetworkError(error) {
508
+ return transientNetworkKind(error) !== undefined;
483
509
  }
484
- export function mapNetworkError(error, context) {
510
+ export function mapNetworkError(error, context, attempt = { attempts: 1, readOnly: context.method === 'GET' || context.method === 'HEAD' }) {
485
511
  const cause = error instanceof Error ? error : new Error(String(error));
486
512
  if (cause.name === 'TimeoutError' || cause.name === 'AbortError') {
487
513
  return new PlatformApiError(`${context.api} API zaman aşımına uğradı (${context.method} ${context.path}). Uzun süren bir işlemse durumu ilgili *_history/*_status tool'undan izleyin.`);
488
514
  }
515
+ const kind = transientNetworkKind(error);
516
+ const tried = attempt.attempts > 1 ? ` (${attempt.attempts} deneme)` : '';
517
+ if (kind === 'name-resolution') {
518
+ const failure = new PlatformApiError(`${context.api} API'nin adı çözülemedi (${context.baseUrl})${tried}: ${cause.message}. İstek sunucuya ULAŞMADI (ad çözülemedi) — aynı çağrıyı tekrarlamak güvenli. Sürüyorsa DNS/ağ bağlantısını kontrol edin.`);
519
+ failure.transient = true;
520
+ return failure;
521
+ }
522
+ if (kind === 'connection-reset') {
523
+ const failure = new PlatformApiError(attempt.readOnly
524
+ ? `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path})${tried}: ${cause.message}. Okuma isteği — tekrarlamak güvenli.`
525
+ : `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path}): ${cause.message}. İstek sunucuya ulaşmış olabilir — tekrarlamadan önce yazının gerçekleşip gerçekleşmediğini ilgili *_get/*_list tool'uyla doğrulayın; yeniden yazmak çift kayıt üretebilir.`);
526
+ failure.transient = true;
527
+ return failure;
528
+ }
489
529
  return new PlatformApiError(`${context.api} API'ye ulaşılamadı (${context.baseUrl}): ${cause.message}. API ayakta mı? Lokal ortam için docker compose ya da dotnet run kontrol edin.`);
490
530
  }
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, isConnectionReset, mapNetworkError, mapPlatformError } from './errors.js';
1
+ import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
2
2
  import { logStep, logsDetail, renderJson } from '../observability/trace.js';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { TokenProvider } from './token.js';
@@ -30,6 +30,9 @@ export function responseHeadersForTool(raw) {
30
30
  .map(([name, value]) => [name.toLowerCase(), Array.isArray(value) ? value.join(', ') : value])
31
31
  .filter((entry) => typeof entry[1] === 'string' && !hiddenResponseHeaders.has(entry[0])));
32
32
  }
33
+ function sleep(ms) {
34
+ return new Promise(resolve => setTimeout(resolve, ms));
35
+ }
33
36
  function buildMultipart(parts, boundary) {
34
37
  const chunks = [];
35
38
  for (const part of parts) {
@@ -74,10 +77,12 @@ function appendQuery(url, query) {
74
77
  }
75
78
  export class Platform {
76
79
  config;
80
+ wait;
77
81
  tokens;
78
- constructor(config, tokens) {
82
+ constructor(config, tokens, wait = sleep) {
79
83
  this.config = config;
80
- this.tokens = tokens ?? new TokenProvider(config);
84
+ this.wait = wait;
85
+ this.tokens = tokens ?? new TokenProvider(config, wait);
81
86
  }
82
87
  resolveProject(project) {
83
88
  const resolved = project ?? this.config.defaultProject;
@@ -134,7 +139,7 @@ export class Platform {
134
139
  }
135
140
  return `Bearer ${await this.tokens.getToken()}`;
136
141
  }
137
- async send(api, method, path, options, raw = false, isRetry = false, networkRetried = false) {
142
+ async send(api, method, path, options, raw = false, isRetry = false, networkAttempt = 1) {
138
143
  const baseUrl = api === 'org' ? this.config.orgApiBaseUrl : this.config.tenantApiBaseUrl;
139
144
  const context = { api, method, path, baseUrl, environment: options.environment, authMode: this.config.authMode };
140
145
  const url = new URL(`${baseUrl}${path}`);
@@ -207,12 +212,15 @@ export class Platform {
207
212
  }
208
213
  catch (error) {
209
214
  logStep('ağ ✕', `${method} ${url.href} · ${Date.now() - startedAt} ms sonra ulaşılamadı`, [error instanceof Error ? `${error.name}: ${error.message}` : String(error)]);
210
- // Yalnız tekrarı zararsız istek (GET/HEAD) ve yalnız bir kez: yazan istek sunucuya ulaşmış olabilir.
211
- if ((method === 'GET' || method === 'HEAD') && !networkRetried && !options.noNetworkRetry && isConnectionReset(error)) {
212
- logStep('ağ ⟳', `${method} ${url.href} · bağlantı koptu, bir kez yeniden deneniyor`);
213
- return this.send(api, method, path, options, raw, isRetry, true);
215
+ // Yalnız tekrarı zararsız istek (GET/HEAD): yazan istek sunucuya ulaşmış olabilir ve kimliksiz yazma çift kayıt üretir (D-19).
216
+ const readOnly = (method === 'GET' || method === 'HEAD') && !options.noNetworkRetry;
217
+ const delay = NETWORK_RETRY_DELAYS_MS[networkAttempt - 1];
218
+ if (readOnly && delay !== undefined && isTransientNetworkError(error)) {
219
+ logStep('ağ ⟳', `${method} ${url.href} · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${networkAttempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
220
+ await this.wait(delay);
221
+ return this.send(api, method, path, options, raw, isRetry, networkAttempt + 1);
214
222
  }
215
- throw mapNetworkError(error, context);
223
+ throw mapNetworkError(error, context, { attempts: networkAttempt, readOnly });
216
224
  }
217
225
  logStep('yanıt ←', `HTTP ${statusCode} · ${Date.now() - startedAt} ms · ${responseBytes} B`, statusCode >= 400 || logsDetail()
218
226
  ? [`gövde: ${options.expectBinary && statusCode < 400 ? `<ikili içerik · ${responseBytes} B>` : renderJson(payload)}`]
@@ -211,9 +211,15 @@ export function toolError(error, expectFailure) {
211
211
  // Kasıtlı negatif testte (bayat version 409, yetkisiz kişi 403) platform hatası da bir KANITTIR:
212
212
  // ölçüm aracı bu satırı plansız hata saymasın diye işaret taşınır.
213
213
  const isExpectedPlatformError = expectFailure === true && (error instanceof PlatformApiError || error instanceof PolicyBlockError);
214
+ // Ajan tekrar edilebilir ağ hatasını metni ayrıştırmadan bilsin (D-19); yazan istekte metin tekrarın güvenli olup olmadığını söyler.
215
+ const isTransient = error instanceof PlatformApiError && error.transient === true;
216
+ const structuredContent = {
217
+ ...(isExpectedPlatformError ? { expectedFailure: true } : {}),
218
+ ...(isTransient ? { transient: true } : {})
219
+ };
214
220
  return {
215
221
  content: [{ type: 'text', text: message }],
216
- ...(isExpectedPlatformError ? { structuredContent: { expectedFailure: true } } : {}),
222
+ ...(Object.keys(structuredContent).length > 0 ? { structuredContent } : {}),
217
223
  isError: true
218
224
  };
219
225
  }
@@ -27,6 +27,12 @@ const maxDepth = 12;
27
27
  * `StepCustomParameterDto {name,value,order}`, `QueryParameter {name,type,value}`: değer bağlama
28
28
  * ifadesidir (`input.token`), maskelenirse get → update turunda `***` geri yazılır (GW-001).
29
29
  * `expression` + `value` hiçbir paketli şemada geçmez, o yüzden bu ayrımın dışındadır.
30
+ *
31
+ * `expiresInHours` taşıyan ve değeri bir ifade olan satır da tanımdır (g-17): `setcookie` adımının `args`'ı
32
+ * `{ name: 'APP_Token', value: 'Steps.sign_in.data.token', expiresInHours }` — maskelenince flow_get → flow_update
33
+ * turu çereze `***` yazıp girişi bozuyordu. Değer tırnakla başlıyorsa dizgi sabitidir ve maskelenir: yürütme yanıtının
34
+ * `uiActions`'ı aynı şekli GERÇEK değerle taşır (`SetCookieStep.cs`: `value: "\"<değer>\""`), tanımdaki sabit de
35
+ * gömülü bir sırdır. `path` muafiyet değildir: `{ name, value, path }` gerçek bir çerezin serileşmiş hâlidir.
30
36
  */
31
37
  export function secretPairName(record) {
32
38
  if (!('value' in record))
@@ -35,6 +41,8 @@ export function secretPairName(record) {
35
41
  return record.expression;
36
42
  if ('type' in record || 'order' in record)
37
43
  return undefined;
44
+ if ('expiresInHours' in record && typeof record.value === 'string' && !/^\s*["'`]/.test(record.value))
45
+ return undefined;
38
46
  if (typeof record.name === 'string')
39
47
  return record.name;
40
48
  return typeof record.key === 'string' ? record.key : undefined;
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, mapNetworkError, mapPlatformError } from './errors.js';
1
+ import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
2
2
  import { logStep } from '../observability/trace.js';
3
3
  import { request } from 'undici';
4
4
  function readExpiry(token) {
@@ -17,9 +17,11 @@ function readExpiry(token) {
17
17
  }
18
18
  export class TokenProvider {
19
19
  config;
20
+ wait;
20
21
  cached;
21
- constructor(config) {
22
+ constructor(config, wait = ms => new Promise(resolve => setTimeout(resolve, ms))) {
22
23
  this.config = config;
24
+ this.wait = wait;
23
25
  }
24
26
  async getToken() {
25
27
  if (this.config.token)
@@ -40,29 +42,38 @@ export class TokenProvider {
40
42
  let statusCode;
41
43
  let payload;
42
44
  logStep('kimlik →', `önbellekte geçerli token yok — POST ${this.config.orgApiBaseUrl}/connect/token · kullanıcı ${this.config.username ?? '(yok)'}`);
43
- try {
44
- const response = await request(`${this.config.orgApiBaseUrl}/connect/token`, {
45
- method: 'POST',
46
- headers: {
47
- host: `${this.config.organization}.${this.config.hostSuffix}`,
48
- 'content-type': 'application/x-www-form-urlencoded',
49
- accept: 'application/json'
50
- },
51
- body: form.toString(),
52
- signal: AbortSignal.timeout(this.config.requestTimeoutMs)
53
- });
54
- statusCode = response.statusCode;
55
- const text = await response.body.text();
45
+ // Jeton isteği kayıt yazmaz: geçici ağ hatasında GET gibi yeniden denenir ve hata tekrarın güvenli olduğunu söyler
46
+ // (D-19; inceleme: soğuk jeton önbelleğinde tek DNS kesintisi okuyan aracı da düşürüyordu).
47
+ for (let attempt = 1;; attempt += 1) {
56
48
  try {
57
- payload = text ? JSON.parse(text) : undefined;
49
+ const response = await request(`${this.config.orgApiBaseUrl}/connect/token`, {
50
+ method: 'POST',
51
+ headers: {
52
+ host: `${this.config.organization}.${this.config.hostSuffix}`,
53
+ 'content-type': 'application/x-www-form-urlencoded',
54
+ accept: 'application/json'
55
+ },
56
+ body: form.toString(),
57
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs)
58
+ });
59
+ statusCode = response.statusCode;
60
+ const text = await response.body.text();
61
+ try {
62
+ payload = text ? JSON.parse(text) : undefined;
63
+ }
64
+ catch {
65
+ payload = text;
66
+ }
67
+ break;
58
68
  }
59
- catch {
60
- payload = text;
69
+ catch (error) {
70
+ const delay = NETWORK_RETRY_DELAYS_MS[attempt - 1];
71
+ if (delay === undefined || !isTransientNetworkError(error))
72
+ throw mapNetworkError(error, context, { attempts: attempt, readOnly: true });
73
+ logStep('kimlik ⟳', `POST ${this.config.orgApiBaseUrl}/connect/token · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${attempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
74
+ await this.wait(delay);
61
75
  }
62
76
  }
63
- catch (error) {
64
- throw mapNetworkError(error, context);
65
- }
66
77
  if (statusCode >= 400)
67
78
  throw mapPlatformError(statusCode, payload, context);
68
79
  const accessToken = payload && typeof payload === 'object' ? payload.access_token : undefined;
@@ -12,7 +12,7 @@
12
12
  "networkError": "Yanıt hiç alınamadıysa (bağlantı yok; saklanan okumalarda 8 sn süre sınırı da) ve kayıt yoksa adım hata dalına gider: Steps.<ad>.error.network true, status 503. 503'ü sunucu da döner — 'bağlantı yok' için network'e bakın (MOB-033 bilgi notu; status == 503 && !network sunucu arızasını ayıran doğru desendir, not almaz). network alanı yalnız veri önbelleği açık uygulamada yazılır. Saklanmayan isteklerde süre sınırı yoktur: yanıt vermeyen sunucuda adım bekler.",
13
13
  "stale": "Önbellek son başarılı yanıtı aynen verir: her çağrıda değişen bir GET (sayaç, zaman, rastgele) ağ yokken eski değerini döner. Parametreler ve veri dili isteğin parçasıdır; başka parametreli çağrı başka kayıttır.",
14
14
  "detailFromSteps": "Satırlar ve güncelleme satırı SETTER adımıyla yazılır: `{{ Steps.… }}` kod bağı ilk çizimde okunur, Steps'e yazmak düğümü yeniden okutmaz (mobile-list-detail-form ile aynı desen).",
15
- "preview": "Önizleme (/dev, Studio, page_screenshot) önbellek KULLANMAZ — ayar açık olsa da her okuma ağdan gider ve cache alanı hiç yazılmaz; güncelleme satırı önizlemede gizli kalır. page_screenshot ağı kesemez: çevrimdışı davranış yalnız cihazda (uçak modu) görülür.",
15
+ "preview": "Önizleme (/dev, Studio, page_screenshot) önbellek KULLANMAZ — ayar açık olsa da her okuma ağdan gider ve cache alanı hiç yazılmaz; güncelleme satırı önizlemede gizli kalır. page_screenshot network eylemiyle ağı keser (SCR-020) ama önbellek olmadığı için okuma hata yolundan döner; önbellekli davranış yalnız cihazda (uçak modu) görülür.",
16
16
  "verify": "page_runtime_get ile sayfa paketi ve akış okunur; page_screenshot({ applicationSlug: '<uygulama>', pageSlug: 'siparisler' }) satırları çizer, txt_guncelleme görünmez."
17
17
  },
18
18
  "pages": [
@@ -12,7 +12,7 @@
12
12
  "visibility": "Kaydın sonucu akışa dönmez. Bekleyen ve reddedilen sayısı Device.offline.queue = { pending, rejected }, reddedilenler Device.offline.rejected (id, service, at, status, code, message). Kod adımında Page.offline.list(), Page.offline.count(), Page.offline.sync() ve Page.offline.resolve(id, 'retry' | 'discard').",
13
13
  "sync": "Gönderim: bağlantının dönüşü, uygulamanın ön plana dönüşü ve ağla soğuk açılış. Sıra kuyruğa giriş zamanı; tek tek; 4xx kaydı reddeder ve sıra sürer, oturum (401/403), 408, 429, 5xx ve ağ hatası sırayı durdurur.",
14
14
  "limits": "Kuyruktaki kayda bağlı ikinci işlem yok: queued yolu Steps.<ad>.data okumaz (dönen kimliği bekleyen adım çevrimdışıyken koşamaz). Dosya parametreli çağrı kuyruğa girmez — adım error'a gider, Steps.<ad>.error.code = 'fileNotQueued'. Kayıt 7 günde gönderilemezse 'expired' ile reddedilir. Kayıtlar kullanıcıya bağlıdır: başkasının bekleyeni gönderilmez, çıkışta silinmez.",
15
- "preview": "Studio önizlemesi ve web çalışma zamanında kuyruk yoktur (barındırıcı depo vermez): queued dalı hiç koşmaz. page_screenshot ağı kesemez; davranış yalnız cihazda görülür. Açık ekran ilk çizildiği kopyayla kalır: akışa eklenen queued dalı ekran bir sonraki açılışta etkindir.",
15
+ "preview": "Studio önizlemesi ve web çalışma zamanında kuyruk yoktur (barındırıcı depo vermez): queued dalı hiç koşmaz. page_screenshot network eylemiyle ağı keser (SCR-020) ama yazma error dalından gider; kuyruklu davranış yalnız cihazda görülür. Açık ekran ilk çizildiği kopyayla kalır: akışa eklenen queued dalı ekran bir sonraki açılışta etkindir.",
16
16
  "verify": "microservice_get ile offlineWritable, flow_get ile queued dalı okunur. Cihazda: bağlantıyı kes, dozu işaretle (queued dalı koşar), bağlantıyı aç; row_list'te tek kayıt."
17
17
  },
18
18
  "microservicePatchArgs": {
@@ -12,8 +12,8 @@
12
12
  "list": "Liste = sunucunun yanıtı (ağsızken son okunan) + Page.offline.list()'teki status === 'pending' kayıtlar (service = yazma ucunun slug'ı, parameters'tan alan). Reddedilen kayıt (status 'rejected') listeye katılmaz. Ağ yokluğu hata metni göstermez: tx_liste_hata yalnız Steps.<okuma>.error && !error.network iken görünür.",
13
13
  "lock": "Yazan düğme istek süresince isLoading + isDisabled. Kilitsiz düğmede hızlı ikinci dokunuş YENİ bir istek ve yeni bir Idempotency-Key'dir — sunucu ikisini de uygular (ölçüldü: 1,6 sn'de üç dokunuş, üç kayıt). Kilit her dalın sonunda (always, queued, error) açılır.",
14
14
  "landing": "Açılış sayfası korumasız giriş ekranıdır; kalıcı state.app.oturumVar (storage: device) true ise mount akışı listeye replace ile geçer. Giriş işareti yazar, çıkış siler. Geçici çare: korumalı açılış sayfası ağsız ilk açılışta çizilemiyor (kök ADR 0024 taslak) ve kabuk bilgisi yokken middleware reddi girişe yönlenmiyor (zeyrek-frontend ARC-342). Mağaza derlemesinde açılış manifestin landingPageSlug'ıdır.",
15
- "refresh": "Liste scroll-view refreshable + refresh olayı aynı yükleme akışına bağlı: bağlantı dönünce açık ekranın mount akışı kendiliğinden koşmaz (kök ADR 0025 taslak). Kuyruk gönderilince kayıt artık bekleyen değildir ve önbellekteki eski listede de yoktur — kullanıcı aşağı çekene kadar listeden düşer; ADR 0025 kapanınca kalkar.",
16
- "preview": "Studio önizlemesi, /dev ve page_screenshot önbellek ve kuyruk KULLANMAZ: page_screenshot ağı kesemez, ağsız davranış yalnız cihazda (uçak kipi) görülür. Ekran açıkken değişen akış ekran yeniden açılınca etkindir.",
15
+ "refresh": "Liste scroll-view refreshable + refresh olayı ve sayfanın offlineQueueChange olayı aynı yükleme akışına bağlı (pageUpdateArgs, kök ADR 0029): kuyruğa giren kayıt listeye hemen katılır, bağlantı dönünce gönderilen kayıt kuyruk değiştiği için akış yeniden koşup kaydı sunucudan getirir — listeden düşmez. Bağlantı dönüşünün kendisi açık ekranın mount akışını koşturmaz (kök ADR 0025 taslak); çekerek yenileme yine çalışır.",
16
+ "preview": "Studio önizlemesi, /dev ve page_screenshot önbellek ve kuyruk KULLANMAZ: page_screenshot network eylemiyle ağı keser (SCR-020) ama okuma da yazma da hata yolundan döner; önbellekli ve kuyruklu davranış yalnız cihazda (uçak kipi) görülür. Ekran açıkken değişen akış ekran yeniden açılınca etkindir.",
17
17
  "notWhere": "Kuyruktaki kayda bağlı ikinci işlem (dönen kimlikle yönlendirme) çevrimdışıyken koşamaz; o akışlara queued dalı vermeyin. Giriş ve hiç açılmamış ekran ağ ister — fark yalnız orada görünür."
18
18
  },
19
19
  "applicationUpdateArgs": {
@@ -1514,7 +1514,16 @@
1514
1514
  "event": "click",
1515
1515
  "eventValue": "<cikis akış id>"
1516
1516
  }
1517
- ]
1517
+ ],
1518
+ "pageUpdateArgs": {
1519
+ "_note": "Sayfa olayı page_update { events } ile yazılır (mobil sayfa olayı; listeyi kuyruk değişince yeniden kurar).",
1520
+ "events": [
1521
+ {
1522
+ "name": "offlineQueueChange",
1523
+ "value": "<notlar-yukle akış id>"
1524
+ }
1525
+ ]
1526
+ }
1518
1527
  }
1519
1528
  ]
1520
1529
  }
@@ -97,6 +97,15 @@ değişimidir: adresin gösterdiği **dosya kopyalanmaz**, production'da adres *
97
97
  production'a taşımak için onu dosya tipli prop'a (`ref`) ya da taşınan bir satırın dosya sütununa
98
98
  bağlayın.
99
99
 
100
+ ## Dağıtımdan sonra doğrula
101
+
102
+ `deployment_status` bitti dedikten sonra `environment_diff({ projectId })` (varsayılan development → production)
103
+ iki ortamın tanım farkını tek çağrıda verir: `equal: true` ise özetlenen modüllerde fark yok. Fark tanım özetinden
104
+ gelir, gövdeyi elle karşılaştırmaya gerek yok. Dağıtım kimlikleri korur; ama development'ta yeniden yazılan bir akış
105
+ (adım parametrelerinin kimlikleri yenilenir) içeriği aynı olsa da `changed` görünebilir.
106
+ `notCovered`'daki modüller (tablolar, bileşenler, erişim, süreçler) ve tablo satırları bu karşılaştırmanın
107
+ dışındadır; satır için §Tablo satırları. `digestNote` varsa özet bayat olabilir.
108
+
100
109
  ## Production'a doğrudan yazılmaz
101
110
 
102
111
  Eksik satır ya da dosya production'da elle tamamlanamaz. Production ortamında `file_upload`,