@appaflytech/zeyrek-ai-mcp 0.0.52 → 0.0.54

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 (43) 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/merge.md +5 -0
  21. package/dist/resources/guides/microservice.md +13 -1
  22. package/dist/resources/guides/mobile-publish.md +7 -4
  23. package/dist/resources/guides/mobile-steps.md +40 -3
  24. package/dist/resources/guides/mobile.md +39 -8
  25. package/dist/resources/guides/page.md +39 -8
  26. package/dist/resources/guides/translation.md +3 -1
  27. package/dist/resources/prompts/mobile-publish-prepare.md +3 -2
  28. package/dist/resources/schema-fingerprint.json +2 -2
  29. package/dist/tools/applications.js +9 -4
  30. package/dist/tools/connectData.js +13 -4
  31. package/dist/tools/deployments.js +1 -1
  32. package/dist/tools/designSchemaPatch.js +7 -2
  33. package/dist/tools/environments.js +40 -0
  34. package/dist/tools/flows.js +30 -3
  35. package/dist/tools/microservices.js +25 -7
  36. package/dist/tools/mobile.js +56 -0
  37. package/dist/tools/mobileStore.js +2 -2
  38. package/dist/tools/pages.js +69 -16
  39. package/dist/tools/platformInfo.js +1 -1
  40. package/dist/tools/screenWrite.js +107 -1
  41. package/dist/tools/variables.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -64,6 +64,23 @@ function hostNodes(record) {
64
64
  return record.schema ?? record.views ?? [];
65
65
  }
66
66
  const BINDINGS_UNAVAILABLE = 'Barındırıcı kaydı okunamadığı için olay bağları hesaplanamadı; akış listesi yine de günceldir.';
67
+ /**
68
+ * g-21 (D-21, plans/defter-saha-bulgulari): `flow_get` `host` olmadan da çalışır. Akış kimliği sahibini söylemez ve
69
+ * platformda kimlikten sahibe giden uç yoktur; kimlik sırayla sayfa → layout → komponent kaydı olarak okunur. Yalnız
70
+ * 404 sonrakine geçer — başka hata (yetki, 500) sahibi hakkında bir şey söylemez ve olduğu gibi döner.
71
+ */
72
+ async function resolveFlowHost(platform, project, environment, applicationId, hostId) {
73
+ for (const host of ['page', 'layout', 'component']) {
74
+ try {
75
+ return { host, record: await fetchHost(platform, host, project, environment, applicationId, hostId) };
76
+ }
77
+ catch (error) {
78
+ if (!(error instanceof PlatformApiError) || error.status !== 404)
79
+ throw error;
80
+ }
81
+ }
82
+ throw new PlatformApiError(`flow_get: bu kimlikte sayfa/layout/komponent yok (${hostId}, uygulama ${applicationId}). hostId akışın barındırıcısının id'sidir, akışın kendi id'si değil — page_list / layout_list / component_list ile doğrulayın ya da host'u açıkça verin.`, 404);
83
+ }
67
84
  /**
68
85
  * TB-126 / S2-36 — satır aksiyonu taşıyabilen komponentler. Bu tiplerin `actions` prop'unu çoğu
69
86
  * zaman YÜKLEME AKIŞI çalışma zamanında yazar (aksiyon role göre eklenip çıkarılıyorsa başka yolu
@@ -529,16 +546,26 @@ export function registerFlowTools(server, platform) {
529
546
  server.registerTool('flow_get', {
530
547
  title: 'Akış getir',
531
548
  description: "Tek akışı adımlarıyla getirir; yeni akış yazarken en iyi şablon budur. bindings bu akışa bağlı olaylardır ([{ target: elementId | 'page', event }]; boşsa akış sahipsizdir). Hedefi silinmiş (element'siz) setter iteratee'si varsa yanıttaki flowWarnings söyler — o adım koşar ama hiçbir şeye yazmaz.",
532
- inputSchema: { project: projectParam, environment: environmentParam, host: hostParam, applicationId: guid, hostId: guid, flowId: flowIdParam },
549
+ inputSchema: {
550
+ project: projectParam,
551
+ environment: environmentParam,
552
+ host: hostParam.optional().describe("Akışın barındırıcısı: page | layout | component. Verilmezse gateway hostId'yi sırayla sayfa → layout → komponent olarak okur (en çok üç okuma, 404 sonrakine geçer) ve yanıtın hostResolvedAs alanı bulduğunu söyler; biliyorsanız verin, tek okuma yapılır"),
553
+ applicationId: guid,
554
+ hostId: guid,
555
+ flowId: flowIdParam
556
+ },
533
557
  annotations: readOnly
534
558
  }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId }) => {
535
- const response = await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment });
536
- const record = await fetchHost(platform, host, project, environment, applicationId, hostId).catch(() => undefined);
559
+ const resolved = host === undefined ? await resolveFlowHost(platform, project, environment, applicationId, hostId) : undefined;
560
+ const owner = resolved?.host ?? host;
561
+ const response = await platform.tenant('GET', project, flowPath(owner, applicationId, hostId, flowId), { environment });
562
+ const record = resolved?.record ?? await fetchHost(platform, owner, project, environment, applicationId, hostId).catch(() => undefined);
537
563
  const warnings = targetlessSetterWarnings(response);
538
564
  if (!response || typeof response !== 'object' || Array.isArray(response))
539
565
  return response;
540
566
  return {
541
567
  ...response,
568
+ ...(resolved === undefined ? {} : { hostResolvedAs: resolved.host }),
542
569
  ...(record === undefined
543
570
  ? { bindingsNote: BINDINGS_UNAVAILABLE }
544
571
  : { bindings: flowBindings(hostNodes(record), record.events).get(flowId.toLowerCase()) ?? [] }),
@@ -214,25 +214,36 @@ function executePath(prefix, application, microservice, routeParams) {
214
214
  /**
215
215
  * Slug application_update ile değişebildiği için önbellek kısa ömürlüdür (yalnız aynı turdaki tekrar okumaları ucuzlatır);
216
216
  * runtime yolu 404 dönen çağıran `fresh: true` ile önbelleği atlar. Org API'nin 404 dışı hatası (yetki, ağ) çağırana gider.
217
+ *
218
+ * Uygulama kaydı (ad, slug, ayar) ve varlığı production'a yalnız dağıtımla gider, dal açıldığı andaki kopyayı tutar
219
+ * (backend ADR 0073, g-24): `GET /applications/{id}` Core'dur = development. Başka ortamın slug'ı o ortamın kendi
220
+ * kaydından (tenant `GET /{project}/applications/{id}`, X-Environment) okunur; yoksa uygulama oraya dağıtılmamıştır (APP-002).
217
221
  */
218
222
  const APPLICATION_SLUG_TTL_MS = 60_000;
219
223
  const applicationSlugCache = new Map();
220
224
  export async function applicationSlug(platform, applicationId, options = {}) {
221
- const cached = options.fresh ? undefined : applicationSlugCache.get(applicationId);
225
+ const environment = platform.resolveEnvironment(options.environment);
226
+ const core = environment === 'development';
227
+ const key = `${core ? '' : platform.resolveProject(options.project)}:${environment}:${applicationId}`;
228
+ const cached = options.fresh ? undefined : applicationSlugCache.get(key);
222
229
  if (cached && cached.expiresAt > Date.now())
223
230
  return cached.slug;
224
231
  try {
225
- const payload = await platform.org('GET', `/applications/${applicationId}`);
232
+ const payload = (core
233
+ ? await platform.org('GET', `/applications/${applicationId}`)
234
+ : await platform.tenant('GET', options.project, `/applications/${applicationId}`, { environment }));
226
235
  const application = (payload?.data ?? payload);
227
236
  const slug = typeof application.slug === 'string' && application.slug !== '' ? application.slug : undefined;
228
237
  if (slug)
229
- applicationSlugCache.set(applicationId, { slug, expiresAt: Date.now() + APPLICATION_SLUG_TTL_MS });
238
+ applicationSlugCache.set(key, { slug, expiresAt: Date.now() + APPLICATION_SLUG_TTL_MS });
230
239
  return slug;
231
240
  }
232
241
  catch (error) {
233
- if (error instanceof PlatformApiError && error.status === 404)
242
+ if (!(error instanceof PlatformApiError) || error.status !== 404)
243
+ throw error;
244
+ if (core)
234
245
  return undefined;
235
- throw error;
246
+ throw new PlatformApiError(`Uygulama ${applicationId} '${environment}' ortamında yok: uygulama kaydı (ad, slug, ayar) ve varlığı production'a yalnız dağıtımla gider, dal açıldığı andaki kopyayı tutar (APP-002). Production için deployment_start ile dağıtın; dalda yalnız dal açılmadan önce var olan uygulama bulunur — development'ta deneyin ya da dalı yeniden açın.`, 404);
236
247
  }
237
248
  }
238
249
  /**
@@ -247,7 +258,14 @@ async function resolveRuntimeUrl(platform, project, environment, applicationId,
247
258
  const method = String(definition.httpMethod ?? 'get').toUpperCase();
248
259
  const resolvedProject = platform.resolveProject(project);
249
260
  const resolvedEnvironment = platform.resolveEnvironment(environment);
250
- const application = await applicationSlug(platform, applicationId);
261
+ // Dağıtılmamış uygulamada tanım yine döner; tarif yer tutucu slug ve nedeni taşır.
262
+ let notDeployed;
263
+ const application = await applicationSlug(platform, applicationId, { project, environment }).catch((error) => {
264
+ if (!(error instanceof PlatformApiError) || error.status !== 404)
265
+ throw error;
266
+ notDeployed = error.message;
267
+ return undefined;
268
+ });
251
269
  const path = `/${resolvedProject}/m/${application ?? '<application slug>'}/${slug}`;
252
270
  return {
253
271
  method,
@@ -257,7 +275,7 @@ async function resolveRuntimeUrl(platform, project, environment, applicationId,
257
275
  Host: `${platform.config.organization}.${platform.config.hostSuffix}`,
258
276
  'X-Environment': resolvedEnvironment
259
277
  },
260
- note: `Yolu SLUG kurar, 'url' alanı ('${String(definition.url ?? '')}') rotaya girmez. Route parametreleri slug'ın ardına eklenir. Anonim erişim testi için microservice_execute'u anonymous: true ile çağırın; korumalı uçlarda 401, anonim uçlarda 200 beklenir.`
278
+ note: `Yolu SLUG kurar, 'url' alanı ('${String(definition.url ?? '')}') rotaya girmez. Route parametreleri slug'ın ardına eklenir. Anonim erişim testi için microservice_execute'u anonymous: true ile çağırın; korumalı uçlarda 401, anonim uçlarda 200 beklenir.${notDeployed ? ` ${notDeployed}` : ''}`
261
279
  };
262
280
  }
263
281
  export function registerMicroserviceTools(server, platform) {
@@ -114,6 +114,57 @@ export function registerMobileTools(server, platform) {
114
114
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid.describe('Mobil uygulama (slug da verilebilir, gateway GUID\'e çevirir)'), ...pushAudienceFields, pageIndex: pagingParams.pageIndex, pageLength: pagingParams.pageLength.describe('Sayfa boyu; varsayılan 25') },
115
115
  annotations: readOnly
116
116
  }, handle(config, ({ project, environment, applicationId, ...query }) => platform.tenant('GET', project, `/push/devices/${applicationId}`, { environment, query })));
117
+ server.registerTool('mobile_error_list', {
118
+ title: 'Telefonda düşen akış hataları',
119
+ description: "Mobil uygulamanın TELEFONDA düşen akış hatalarını okur (kabuk gönderir, sunucu saklar — defter-saha-bulgulari b-10/f-10). Kayıt biçimi page_screenshot.runtimeErrors ile aynıdır (flowId · step{id,name,type} · code · message · element · property · kind) ve şunları ekler: pageSlug, platform, appVersion, devices (kaç kurulumda), count, lastAt. Aynı akış/adım/mesaj üçlüsü tek satıra katlanır, en yeni önce. Önizlemede görülmeyen cihaz hatasını (ağsız yazma, sayfa kapanınca atlanan adım `page-unmounted`, geri gidilemeyen `back-unavailable`) kullanıcının anlatmasını beklemeden okumak için. Kabuk hatayı ağ gelince gönderir: uçak kipinde düşen hata ağ dönene kadar görünmez; occurredAt telefondaki saattir. Sunucu kurulum başına son 200 kaydı ve 7 günü tutar; mesaj gateway maskesinden geçer. Uygulama ayarı properties.mobile.reportErrors: false gönderimi kapatır. Web önizlemesinin hataları burada YOK — onları page_screenshot.runtimeErrors verir.",
120
+ inputSchema: {
121
+ project: projectParam,
122
+ environment: environmentParam,
123
+ applicationId: guid.describe('Mobil uygulama (slug da verilebilir, gateway GUID\'e çevirir)'),
124
+ pageSlug: z.string().min(1).optional().describe('Yalnız bu sayfada düşenler'),
125
+ flowId: z.string().min(1).optional().describe('Yalnız bu akış'),
126
+ installationId: z.string().min(1).optional().describe('Yalnız bu kurulum (notification_device_list\'teki installationId ile aynı kimlik)'),
127
+ since: z.string().min(1).optional().describe('ISO zaman; yalnız bundan sonra görülenler'),
128
+ limit: z.number().int().min(1).max(100).optional().describe('Katlanmış satır sayısı; varsayılan 20')
129
+ },
130
+ annotations: readOnly
131
+ }, handle(config, async ({ project, environment, applicationId, limit = 20, ...query }) => {
132
+ let response;
133
+ try {
134
+ response = await platform.tenant('GET', project, `/runtime-errors/${applicationId}`, { environment, query: { ...query, limit: 200 } });
135
+ }
136
+ catch (error) {
137
+ if (error instanceof PlatformApiError && error.status === 404) {
138
+ throw new PlatformApiError('mobile_error_list: bu backend istemci akış hatası kaydetmiyor (runtime-errors ucu yok) — backend imajı güncel değil ya da uç bu kurulumda kapalı. Önizleme hatalarını page_screenshot.runtimeErrors verir.', 404);
139
+ }
140
+ throw error;
141
+ }
142
+ const rows = unwrap(response) ?? [];
143
+ // Aynı akış/adım/mesaj tek satır: sayılar toplanır, en yeni kaydın bağlamı (sayfa, sürüm, platform) taşınır.
144
+ const folded = new Map();
145
+ for (const row of rows) {
146
+ const key = `${row.flowId ?? ''}|${row.step?.id ?? ''}|${row.message ?? ''}`;
147
+ const lastAt = row.lastAt ?? row.occurredAt ?? '';
148
+ const existing = folded.get(key);
149
+ if (!existing) {
150
+ folded.set(key, { ...row, lastAt, count: row.count ?? 1, devices: new Set(row.installationId ? [row.installationId] : []) });
151
+ continue;
152
+ }
153
+ existing.count = (existing.count ?? 1) + (row.count ?? 1);
154
+ if (row.installationId)
155
+ existing.devices.add(row.installationId);
156
+ if (lastAt > (existing.lastAt ?? ''))
157
+ Object.assign(existing, { ...row, lastAt, count: existing.count, devices: existing.devices });
158
+ }
159
+ const errors = [...folded.values()]
160
+ .sort((left, right) => (right.lastAt ?? '').localeCompare(left.lastAt ?? ''))
161
+ .map(({ devices, installationId: _installation, ...error }) => ({ ...error, devices: devices.size }));
162
+ return {
163
+ errors: errors.slice(0, limit),
164
+ ...(errors.length > limit ? { truncated: true, total: errors.length } : {}),
165
+ ...(errors.length === 0 ? { note: 'Bu süzgeçle kayıt yok. Telefon hatayı ağ gelince gönderir; uçak kipindeki hata ağ dönene kadar görünmez. Uygulama ayarı properties.mobile.reportErrors: false ise kabuk göndermez.' } : {})
166
+ };
167
+ }));
117
168
  server.registerTool('notification_create', {
118
169
  title: 'Push bildirimi gönder (varsayılan kuru koşu)',
119
170
  description: "Kitleye dile göre içerikle push bildirimi gönderir — `SendPushNotificationCommand: { dryRun, audience, content, defaultLanguage, path?, query?, androidChannel?, badge?, testInstallationId? }`. dryRun VARSAYILAN TRUE: yalnız kitleyi sayar ({ total, byLanguage }), hiçbir şey yazmaz; göndermek için dryRun: false AÇIKÇA verilir. Gerçek gönderim kuyruğa girer ve yanıt id döner; sonucu notification_history { id } ile okuyun (birkaç saniye sonra sent/failed, teslim sayıları ve hatalı cihazlar). Kitle her zaman aktif ve izinli cihazlara daraltılır; testInstallationId verilirse yalnız o kurulum (notification_device_list'teki installationId). İçerik dil → { title, body }; başlığı boş dil düşer ve o cihaz varsayılan dili alır; varsayılan dilde başlık zorunlu (NTF-002, platform 400). Dil anahtarları küçük harfe indirilir. path dokunuşta açılacak sayfanın slug'ı, query onun sorgusu. Ret: production'da dryRun: false istek gitmeden reddedilir (NTF-001).",
@@ -169,6 +220,11 @@ export function registerMobileTools(server, platform) {
169
220
  const plan = buildMobileAppPlan(input);
170
221
  if (dryRun)
171
222
  return { dryRun: true, plan };
223
+ // APP-001 (K5, b-15): sekme ve ana sayfa ayarı yalnız development'a yazılır. Dalda kurulan sayfalara bağlı ayar
224
+ // development'ta olmayan sayfaları gösterir, dal sekmesiz kalırdı.
225
+ if (platform.resolveEnvironment(environment) !== 'development') {
226
+ throw new PlatformApiError(`mobile_app_create [APP-001]: uygulama ayarı (sekmeler, ana sayfa) yalnız development'a yazılır; '${platform.resolveEnvironment(environment)}' ortamında kurulan sayfalara bağlanamaz. Mobil uygulamayı development'ta kurun, production'a deployment_start ile taşıyın. Hiçbir şey yazılmadı.`, 400);
227
+ }
172
228
  const completed = [];
173
229
  const skipped = new Set();
174
230
  const warnings = [];
@@ -467,7 +467,7 @@ export function registerMobileStoreTools(server, platform, storeArt = {}) {
467
467
  }));
468
468
  server.registerTool('mobile_build_start', {
469
469
  title: 'Mobil derleme başlat',
470
- description: "Mobil uygulama için derleme işi açar (POST mobile-builds { platform, target }). Önce ön kontrolü okur (GET mobile-store/{appId}/preflight — Studio'nun Yayımla düğmesiyle aynı liste: ayar, görsel, gizli bilgi, App Store Connect / Play erişimi, production sayfası, etkin iş, çevrimiçi runner, beyanlar; iOS yükleme hedeflerinde App Review satırları P-15…P-21, yayın hedeflerinde kaynak derleme P-22); bir satır bile 'fail' ise iş BAŞLATILMAZ ve fail satırları kod + öneriyle hata olarak döner ('manual' satırlar başlatmayı engellemez, kullanıcıya söylenir). dryRun:true yalnız ön kontrolü döner, iş açmaz — yayından önce bununla bakın. Hedef–platform: build her ikisinde, testflight ve app-store-review/app-store-release yalnız ios, internal ve play-promote yalnız android. Mağaza yayın hedefleri (app-store-review, app-store-release, play-promote + releaseTrack closed|production) geri dönüşsüz ya da insan onaylıdır: YALNIZ dryRun ile kabul edilir, gerçek gönderim Studio › Yayınla'dadır; mağaza durumu ve sıradaki eylem mobile_store_get { detail: ['status'] }. packageType: aab (varsayılan) | apk — APK yalnız android build'de (mağazaya gitmez, telefona kurulur). Runner: runnerScope verilmezse backend seçer — kurumun kendi Mac'i çevrimiçiyse dedicated, değilse Zeyrek'in ortak runner'ı (shared): kurumun kendi Mac'i yoksa iş Zeyrek'in runner'ına düşer; yanıtta runnerKind own | zeyrek. runnerId işi kullanıcının o Mac'ine ayırır (runnerScope gerekmez — iş dedicated kapsamında o runner'ı bekler; kullanıcının birden çok çevrimiçi Mac'i varken işin hangisine düşeceğini sabitler): runner kurumun kendi runner'ı değilse, platformu desteklemiyorsa ya da isteyene derleyemiyorsa 404 döner, runnerScope: 'shared' ile birlikte 400 döner — ikisinde de numara ayrılmaz; çevrimdışı Mac reddedilmez, iş onu bekler. runnerId verilmezse iş eskisi gibi ayrılmaz (kapsamdaki ilk yoklayan runner alır). Bu araç Cihazda dene önizlemesini başlatmaz (önizleme görevi, kod ve davet kullanıcının Studio'daki işidir). Tuzak: her başlatma derleme numarasını AYIRIR ve ilk başlatma kimlik alanlarını kilitler; iptal ya da hata numarayı geri vermez. Aynı platformda etkin iş varsa 409. İş asenkrondur: mobile_build_status ile izleyin. Ortam: environment (ad ya da slug; boş = production) derlemenin içeriğini o ortamdan alır — development, production ya da hazır bir dal; yalnız build | testflight | internal'da verilir, yükseltme hedefinde gateway reddeder: mağazaya yalnız production işinin derlemesi gider, development/dal test derlemesi asla yükseltilemez. Ön kontrolün kapsamı scope: 'store' (varsayılan; mağazaya giden işin bütün kuralları) | 'test' (Yayınla 3 · Test sürümü: yalnız TestFlight / Play iç testin istedikleri — kimlik, hesap ve imza, konsolda uygulama kaydı, seçilen ortamda açılış sayfası, runner; metin, görsel, beyan ve App Review satırları sorulmaz); environment verilince scope 'test' olur, 'store' ile birlikte reddedilir; scope işe yazılmaz. Derlenen uygulama işin ortamını okur — yükleme hedeflerinde açılış sayfası o ortamda olmalı (P-06 fail; production'da dağıtılmış olmalı); yalın build'de P-06 yalnız uyarıdır. Yanıt işin environment'ını taşır.",
470
+ description: "Mobil uygulama için derleme işi açar (POST mobile-builds { platform, target }). Önce ön kontrolü okur (GET mobile-store/{appId}/preflight — Studio'nun Yayımla düğmesiyle aynı liste: ayar, görsel, gizli bilgi, App Store Connect / Play erişimi, production sayfası, etkin iş, çevrimiçi runner, beyanlar; iOS yükleme hedeflerinde App Review satırları P-15…P-21, yayın hedeflerinde kaynak derleme P-22); bir satır bile 'fail' ise iş BAŞLATILMAZ ve fail satırları kod + öneriyle hata olarak döner ('manual' satırlar başlatmayı engellemez, kullanıcıya söylenir). dryRun:true yalnız ön kontrolü döner, iş açmaz — yayından önce bununla bakın. Hedef–platform: build her ikisinde, testflight ve app-store-review/app-store-release yalnız ios, internal ve play-promote yalnız android. Mağaza yayın hedefleri (app-store-review, app-store-release, play-promote + releaseTrack closed|production) geri dönüşsüz ya da insan onaylıdır: YALNIZ dryRun ile kabul edilir, gerçek gönderim Studio › Yayınla'dadır; mağaza durumu ve sıradaki eylem mobile_store_get { detail: ['status'] }. packageType: aab (varsayılan) | apk — APK yalnız android build'de (mağazaya gitmez, telefona kurulur). Runner: runnerScope verilmezse backend seçer — kurumun kendi Mac'i çevrimiçiyse dedicated, değilse Zeyrek'in ortak runner'ı (shared): kurumun kendi Mac'i yoksa iş Zeyrek'in runner'ına düşer; yanıtta runnerKind own | zeyrek. runnerId işi kullanıcının o Mac'ine ayırır (runnerScope gerekmez — iş dedicated kapsamında o runner'ı bekler; kullanıcının birden çok çevrimiçi Mac'i varken işin hangisine düşeceğini sabitler): runner kurumun kendi runner'ı değilse, platformu desteklemiyorsa ya da isteyene derleyemiyorsa 404 döner, runnerScope: 'shared' ile birlikte 400 döner — ikisinde de numara ayrılmaz; çevrimdışı Mac reddedilmez, iş onu bekler. runnerId verilmezse iş eskisi gibi ayrılmaz (kapsamdaki ilk yoklayan runner alır). Bu araç Cihazda dene önizlemesini başlatmaz (önizleme görevi, kod ve davet kullanıcının Studio'daki işidir). Sayfa değişikliğini yerelde görmek için derleme gerekmez: platform_guide('mobile') §Cihazda dene → Senin Mac'inde; iOS target 'build' imzalama kimliği yokken imzasız simülatör .app'i derler (kimlik tamsa imzalı .ipa — simülatörde açılmaz). Tuzak: her başlatma derleme numarasını AYIRIR ve ilk başlatma kimlik alanlarını kilitler; iptal ya da hata numarayı geri vermez. Aynı platformda etkin iş varsa 409. İş asenkrondur: mobile_build_status ile izleyin. Ortam: environment (ad ya da slug; boş = production) derlemenin içeriğini o ortamdan alır — development, production ya da hazır bir dal; yalnız build | testflight | internal'da verilir, yükseltme hedefinde gateway reddeder: mağazaya yalnız production işinin derlemesi gider, development/dal test derlemesi asla yükseltilemez. Ön kontrolün kapsamı scope: 'store' (varsayılan; mağazaya giden işin bütün kuralları) | 'test' (Yayınla 3 · Test sürümü: yalnız TestFlight / Play iç testin istedikleri — kimlik, hesap ve imza, konsolda uygulama kaydı, seçilen ortamda açılış sayfası, runner; metin, görsel, beyan ve App Review satırları sorulmaz); environment verilince scope 'test' olur, 'store' ile birlikte reddedilir; scope işe yazılmaz. Derlenen uygulama işin ortamını okur — yükleme hedeflerinde açılış sayfası o ortamda olmalı (P-06 fail; production'da dağıtılmış olmalı); yalın build'de P-06 yalnız uyarıdır. Yanıt işin environment'ını taşır.",
471
471
  inputSchema: {
472
472
  applicationId,
473
473
  platform: mobilePlatform,
@@ -590,7 +590,7 @@ export function registerMobileStoreTools(server, platform, storeArt = {}) {
590
590
  }, handle(config, async ({ buildId: id }) => buildSummary(unwrap(await platform.org('POST', `/mobile-builds/${id}/cancel`)), new Date().toISOString())));
591
591
  server.registerTool('mobile_asset_create', {
592
592
  title: 'Mağaza görseli yükle',
593
- description: `Mağaza görselini yükler (POST mobile-store/{appId}/assets, multipart). Kaynak path (gateway'in okuyabildiği yerel dosya) ya da base64 content'tir — dosya ajanın bağlamına girmez. Yuvalar: derleme görselleri icon (1024×1024 PNG, alfasız), adaptiveForeground / adaptiveBackground / adaptiveMonochrome (1024×1024 PNG), splash (≥ 1242×2436 PNG) — dilsizdir, locale verilmez; mağaza görselleri featureGraphic (1024×500, alfasız), promo, screenshot (deviceClass zorunlu, locale önerilir: 'screenshot/tr/iphone69'); reviewAttachment (dilsiz, PNG/JPEG) App Review'a ek görseldir — yalnız app-store-review gönderimine girer. Tuzak: screenshot ve promo dışındaki türler TEKLİDİR — yükleme yuvadaki önceki görselin YERİNE geçer ve eskisi silinir. Yuva başına en çok 10 ekran görüntüsü, dosya ≤ 10 MB, PNG ya da JPEG (derleme görseli yalnız PNG). Ölçü, biçim ve alfa backend'de denetlenir; 422 hangi ölçünün beklendiğini söyler. EKRAN GÖRÜNTÜSÜ uygulamanın kendi sayfasından üretilir: önce page_screenshot { environment: 'production', applicationSlug, pageSlug, device, format: 'jpeg', quality: 92, language, colorScheme: 'light', outPath }, sonra bu araç { path: <outPath>, kind: 'screenshot', deviceClass, locale }. Cihaz sınıfı → device: ${captureTable}. JPEG şarttır: iPhone ve iPad ekran görüntüsünde alfa kanalı reddedilir. Giriş/splash ekranını çekmeyin (App Review 2.3.3: uygulama kullanımda gösterilir). Sıra: mobile_asset_update; silme: mobile_asset_delete; liste: mobile_store_get { detail: ['assets'] }.`,
593
+ description: `Mağaza görselini yükler (POST mobile-store/{appId}/assets, multipart). Kaynak path (gateway'in okuyabildiği yerel dosya) ya da base64 content'tir — dosya ajanın bağlamına girmez. Yuvalar: derleme görselleri icon (1024×1024 PNG, alfasız), adaptiveForeground / adaptiveBackground / adaptiveMonochrome (1024×1024 PNG), splash (≥ 1242×2436 PNG) — dilsizdir, locale verilmez; mağaza görselleri featureGraphic (1024×500, alfasız), promo, screenshot (deviceClass zorunlu, locale önerilir: 'screenshot/tr/iphone69'); reviewAttachment (dilsiz, PNG/JPEG) App Review'a ek görseldir — yalnız app-store-review gönderimine girer. Tuzak: screenshot ve promo dışındaki türler TEKLİDİR — yükleme yuvadaki önceki görselin YERİNE geçer ve eskisi silinir. Yuva başına en çok 10 ekran görüntüsü, dosya ≤ 10 MB, PNG ya da JPEG (derleme görseli yalnız PNG). Ölçü, biçim ve alfa backend'de denetlenir; 422 hangi ölçünün beklendiğini söyler. EKRAN GÖRÜNTÜSÜ uygulamanın kendi sayfasından üretilir: önce page_screenshot { environment: 'production', applicationSlug: <uygulama id'si — production'ın kendi slug'ına çözülür, APP-002>, pageSlug, device, format: 'jpeg', quality: 92, language, colorScheme: 'light', outPath }, sonra bu araç { path: <outPath>, kind: 'screenshot', deviceClass, locale }. Cihaz sınıfı → device: ${captureTable}. JPEG şarttır: iPhone ve iPad ekran görüntüsünde alfa kanalı reddedilir. Giriş/splash ekranını çekmeyin (App Review 2.3.3: uygulama kullanımda gösterilir). Sıra: mobile_asset_update; silme: mobile_asset_delete; liste: mobile_store_get { detail: ['assets'] }.`,
594
594
  inputSchema: {
595
595
  applicationId,
596
596
  path: z.string().min(1).optional().describe("Gateway sürecinin okuyabildiği yerel PNG/JPEG yolu (page_screenshot outPath'i); content ile birlikte verilmez"),
@@ -4,14 +4,14 @@ import { functionSignature } from '../knowledge/functionGuards.js';
4
4
  import { attachFindings } from '../knowledge/policy.js';
5
5
  import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath, countSchemaNodes, missingElementIds, readFullSchema } from './screens.js';
6
6
  import { fetchComponentEventNames } from './components.js';
7
- import { assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog } from './screenWrite.js';
7
+ import { assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog, unknownPropOps } from './screenWrite.js';
8
8
  import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, callerSlots, planEmulatedOps, recordsWithEventRemovals, serverOpsInOrder, rejectedOpsMessage } from './schemaOps.js';
9
9
  import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
10
10
  import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
11
11
  import { PlatformApiError } from '../platform/errors.js';
12
12
  import { existingMatch, ifExistsParam, listAll, withOutcome } from '../platform/ifExists.js';
13
13
  import { handle, toolResult, toolError } from '../platform/responses.js';
14
- import { expressionErrorsNote, runtimeErrorsNote, StudioScreenshotter } from '../platform/capture/index.js';
14
+ import { awaitingUserNote, expressionErrorsNote, runtimeErrorsNote, StudioScreenshotter } from '../platform/capture/index.js';
15
15
  import { captureTargetFor } from '../platform/capture/target.js';
16
16
  import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
17
17
  import { captureKey, imageHash, sharedCaptureMemory } from '../platform/screenshotMemory.js';
@@ -30,6 +30,8 @@ import { boxSectionNote, collectIds, ensureBoxSections, expandSectionToggles } f
30
30
  import { designSnapshotInput } from '../knowledge/designSnapshot.js';
31
31
  import { discoverPage } from '../knowledge/pageDiscover.js';
32
32
  import { applicationTypeField, resolveProfile, resolveProfileBySlug } from '../knowledge/platform/resolve.js';
33
+ import { applicationSlug as environmentApplicationSlug } from './microservices.js';
34
+ import { isGuid } from '../platform/refs.js';
33
35
  import { batchEnvelopeNote, batchIndexHint, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
34
36
  import { heroTags } from '../knowledge/platform/heroTag.js';
35
37
  import { profileLintReadsSavedNodes } from '../knowledge/platform/lint.js';
@@ -58,6 +60,20 @@ function runtimeFunctions(value) {
58
60
  .map(parameter => ({ name: parameter.name, required: parameter.required === true, defaultValue: parameter.defaultValue ?? null }))
59
61
  }));
60
62
  }
63
+ /**
64
+ * g-24 / APP-002: uygulama kaydı production'a yalnız dağıtımla gider, dal açıldığı andaki kopyayı tutar; application_list
65
+ * ve application_get Core'u (development) döner. Yeniden adlanan uygulamanın production/dal adresi eski slug'la çalışır,
66
+ * Core slug'ı orada 404 verir — id verilirse slug'ı o ortamın kaydından gateway okur.
67
+ */
68
+ const runtimeApplicationParam = z.string().min(1).describe("Uygulamanın slug'ı ya da id'si (GUID). application_list / application_get DEVELOPMENT slug'ını döner; production ve dal uygulama kaydını yalnız dağıtımla/açılışla alır, yeniden adlanan uygulama orada dağıtıma kadar eski slug'la açılır. production ya da dalda id verin: gateway o ortamın kendi slug'ını okur, uygulama o ortamda yoksa (dağıtılmamış) istek gitmeden hata döner (APP-002)");
69
+ async function runtimeApplicationSlug(platform, project, ref, environment) {
70
+ if (!isGuid(ref))
71
+ return ref;
72
+ const slug = await environmentApplicationSlug(platform, ref, { project, environment });
73
+ if (!slug)
74
+ throw new PlatformApiError(`Uygulama ${ref} bulunamadı: id'yi application_list ile doğrulayın.`, 404);
75
+ return slug;
76
+ }
61
77
  /**
62
78
  * G-10: `index` taşıyan add/move op'u varsa sayfa okunur ve `flexIndexOrder` flex kapta görünen
63
79
  * sırayı konumlara çevirir. Okuma başarısız olursa op'lar olduğu gibi gider — bugünkü davranış.
@@ -181,7 +197,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
181
197
  inputSchema: {
182
198
  project: projectParam,
183
199
  environment: environmentParam,
184
- applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
200
+ applicationSlug: runtimeApplicationParam,
185
201
  pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
186
202
  view: z.enum(['summary', 'full']).optional().describe('summary (varsayılan): şema/layout ağaçları id/name/type özetine indirgenir. full: ham gövde — büyük olabilir'),
187
203
  resolveStyles: z.boolean().optional().describe("summary görünümünde her düğüme EFEKTİF stilini ekler: tema componentStyles → preset → düğümün kendi stili kaskadı çözülür (aibox'ta tipli stil grupları da derlenir ve TABAN katman olur). styleLayers hangi katmanın konuştuğunu söyler — 'bu kutu neden böyle görünüyor' sorusunun cevabı budur."),
@@ -190,7 +206,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
190
206
  asUserId: asUserIdParam
191
207
  },
192
208
  annotations: readOnly
193
- }, handle(config, async ({ project, environment, applicationSlug, pageSlug, view, resolveStyles, anonymous, asUser, asUserId }) => {
209
+ }, handle(config, async ({ project, environment, applicationSlug: applicationRef, pageSlug, view, resolveStyles, anonymous, asUser, asUserId }) => {
194
210
  let response;
195
211
  if (anonymous === true && asUser !== undefined) {
196
212
  throw new PlatformApiError('page_runtime_get: anonymous:true ile asUser birlikte verilemez — biri kimliksiz isteği, diğeri belirli bir üyenin kimliğiyle atılan isteği ister.');
@@ -199,6 +215,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
199
215
  // yapısal olarak geçemiyordu. Kimlik artık iki kanalda birden gider (Y-02).
200
216
  const eye = await impersonationHeaders(platform, 'page_runtime_get', project, environment, { asUserId, asUser, anonymous }, { runtime: true });
201
217
  const identity = asUser === undefined ? (eye ? { headers: eye } : {}) : { headers: { authorization: `Bearer ${await resolveIdentity(asUser, platform, project, environment)}` } };
218
+ const applicationSlug = await runtimeApplicationSlug(platform, project, applicationRef, environment);
202
219
  try {
203
220
  response = await platform.tenant('GET', project, `/${applicationSlug}/p/${pageSlug}`, { environment, anonymous, mirrorAuthorizationCookie: true, ...identity });
204
221
  }
@@ -210,7 +227,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
210
227
  status: error.status,
211
228
  message: error.message,
212
229
  ...(decision ? { accessDecision: decision } : {}),
213
- note: "Sayfaya bağlı middleware isteği reddetti ya da kayıt bulunamadı. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır. Sayfanın bağlarını page_get ile, middleware tarifini platform_guide('middleware') ile kontrol edin; slug'ları page_list/application_list doğrular."
230
+ note: "Sayfaya bağlı middleware isteği reddetti ya da kayıt bulunamadı. Korumalı sayfada kimliksiz/yetkisiz isteğin reddedilmesi BEKLENEN davranıştır. Sayfanın bağlarını page_get ile, middleware tarifini platform_guide('middleware') ile kontrol edin; slug'ları page_list/application_list doğrular — application_list development slug'ını verir; production ya da dalda applicationSlug yerine uygulama id'sini verin (APP-002)."
214
231
  };
215
232
  }
216
233
  throw error;
@@ -272,11 +289,11 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
272
289
  const maxInlinePngBytes = 5 * 1024 * 1024;
273
290
  server.registerTool('page_screenshot', {
274
291
  title: 'Sayfanın ekran görüntüsünü al',
275
- description: "ÖNCE KARESİZ DENE: 'kart göründü mü, tablo doldu mu, hata var mı' gibi olgu sorularında image:'none' verin — kare çekilmez, yanıttaki summaryText her kök düğümün görünürlüğünü, metnini, satır sayısını, açık sekmesini ve hata mesajlarını söyler (bir tam sayfa karesinin yüzlerce katı küçük). Kareyi yalnız görsel soru (renk, hizalama, referansa sadakat) için alın ve küçültün: clip ile bölge, scale:0.5 ile yarı çözünürlük; aynı sayfayı yeniden çekerken image:'ifChanged'. Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. Bir komponent prop'unun İFADESİ patlarsa (Studio'da 'Expression error' toast'ı) yanıt expressionErrors taşır: {elementId, prop, expression, message, count}, öğe başına en çok iki; aynı satırın konsol kopyası consoleErrors'a düşmez. Sayfa ya da bir bileşen ÇİZİLEMEZSE neden yanıttadır: mobilde çöken düğüm renderErrors ({elementId, message}; sayfanın geri kalanı çizilir), yakalanmamış istisna pageErrors, konsol hatası consoleErrors — kare alınamadıysa bütün nedenler hata metninde. KARE DONUKTUR: tarayıcı bağlamı 'hareketi azalt' tercihiyle açılır; mobil runtime bileşen hareketini (giriş, döngü, geçiş, iskelet parıltısı, yükleme göstergesi) durdurur ve aynı sayfanın iki karesi tekrarlanabilir. Web sayfasında yalnız CSS'i prefers-reduced-motion'a uyan animasyonlar durur. Hareketin kendisini görmek için motion:'play'. Kare DİLİ ve TEMASI seçilebilir: 'language' adrese ?lang ekler (çeviriye bağlı prop'lar da çok dilli alanların satır değerleri de o dilde çözülür), 'colorScheme' ziyaretçinin tema tercihini sayfa açılmadan eker — ikisi de sayfaya dil değiştirici ya da theme-toggle koymayı GEREKTİRMEZ, yani referansa sadık bir tasarımın ikinci dili ve koyu teması bozulmadan kanıtlanır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir. DAL ORTAMI DA GÖRÜNTÜLENİR (2026-09-09'dan beri): 'development' /dev, 'production' /view, dal ise /branch/{dal} şeridinden açılır ve sayfanın BÜTÜN istekleri (şema da akışlar da) o dala gider — dalda kurulan sayfa artık merge'i beklemeden gözle doğrulanır (platform_guide('merge') §Dalda görsel doğrulama).",
292
+ description: "Telefonda (mağaza kabuğu ya da oynatıcı) düşen akış hatasını bu araç görmez — onu mobile_error_list okur. ÖNCE KARESİZ DENE: 'kart göründü mü, tablo doldu mu, hata var mı' gibi olgu sorularında image:'none' verin — kare çekilmez, yanıttaki summaryText her kök düğümün görünürlüğünü, metnini, satır sayısını, açık sekmesini ve hata mesajlarını söyler (bir tam sayfa karesinin yüzlerce katı küçük). Kareyi yalnız görsel soru (renk, hizalama, referansa sadakat) için alın ve küçültün: clip ile bölge, scale:0.5 ile yarı çözünürlük; aynı sayfayı yeniden çekerken image:'ifChanged'. Studio arayüzünün runtime render rotasını (/dev ya da /view) headless Chromium'da açar ve sayfanın PNG ekran görüntüsünü inline görüntü olarak döner — kurduğun sayfayı GÖZLE doğrulamanın yolu budur (page_runtime_get paketi verir, bu tool ekranda ne ÇİZİLDİĞİNİ gösterir). Sayfaya bağlı middleware'ler gerçekten koşar; reddedilirse yanıtta middlewareBlocked işaretlenir ve Studio'nun çizdiği hata durumu karesi döner — bu da bir doğrulamadır. YÜKLENMEMİŞ SAYFA ASLA ÇEKİLMEZ: kare, runtime çizilip mount akışları bitince alınır (yanıtta pageState + readyAfterMs); bütçe dolduğunda sayfa hâlâ iskelette ya da akış koşuyorsa çağrı ne görüldüğünü söyleyen bir hatayla döner, boş/yarım kare dönmez. Sayfa akışı bir adımda düşerse (patlayan ifade, hata dönen uç) yanıt runtimeErrors taşır ve hangi komponentin hangi prop'unun YAZILMADIĞINI söyler — kare 'çalışıyor' görünürken sessizce boş kalan komponenti ancak bu yakalar. Bir komponent prop'unun İFADESİ patlarsa (Studio'da 'Expression error' toast'ı) yanıt expressionErrors taşır: {elementId, prop, expression, message, count}, öğe başına en çok iki; aynı satırın konsol kopyası consoleErrors'a düşmez. Sayfa ya da bir bileşen ÇİZİLEMEZSE neden yanıttadır: mobilde çöken düğüm renderErrors ({elementId, message}; sayfanın geri kalanı çizilir), yakalanmamış istisna pageErrors, konsol hatası consoleErrors — kare alınamadıysa bütün nedenler hata metninde. KARE DONUKTUR: tarayıcı bağlamı 'hareketi azalt' tercihiyle açılır; mobil runtime bileşen hareketini (giriş, döngü, geçiş, iskelet parıltısı, yükleme göstergesi) durdurur ve aynı sayfanın iki karesi tekrarlanabilir. Web sayfasında yalnız CSS'i prefers-reduced-motion'a uyan animasyonlar durur. Hareketin kendisini görmek için motion:'play'. Kare DİLİ ve TEMASI seçilebilir: 'language' adrese ?lang ekler (çeviriye bağlı prop'lar da çok dilli alanların satır değerleri de o dilde çözülür), 'colorScheme' ziyaretçinin tema tercihini sayfa açılmadan eker — ikisi de sayfaya dil değiştirici ya da theme-toggle koymayı GEREKTİRMEZ, yani referansa sadık bir tasarımın ikinci dili ve koyu teması bozulmadan kanıtlanır. STUDIO_BASE_URL tanımlı olmalı; ilk kullanım için `npx playwright-core install chromium` gerekir. DAL ORTAMI DA GÖRÜNTÜLENİR (2026-09-09'dan beri): 'development' /dev, 'production' /view, dal ise /branch/{dal} şeridinden açılır ve sayfanın BÜTÜN istekleri (şema da akışlar da) o dala gider — dalda kurulan sayfa artık merge'i beklemeden gözle doğrulanır (platform_guide('merge') §Dalda görsel doğrulama). Production ve dalda uygulamanın slug'ı o ortamın kendi kaydıdır (dağıtıma/açılışa kadar eski slug; application_list development'ı döner): applicationSlug'a uygulama id'sini verin, gateway ortamın slug'ını okur (APP-002).",
276
293
  inputSchema: {
277
294
  project: projectParam,
278
295
  environment: environmentParam,
279
- applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
296
+ applicationSlug: runtimeApplicationParam,
280
297
  pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list); sayfa parametresi sorguyla verilebilir: 'ilac-detay?id=7' (language '&lang=' olarak eklenir)"),
281
298
  viewport: z.object({
282
299
  width: z.number().int().min(320).max(3840),
@@ -300,14 +317,16 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
300
317
  layoutReport: z.boolean().optional().describe("true: kareyle birlikte her runtime düğümünün efektif piksel kutusunu ve özetini (taşan / viewport dışı / sıfır boyutlu elementId'ler) döner — taşma ve çakışmayı görüntüsüz, sayısal yakalar. Mobil uygulamada özet ayrıca layoutSummary.alignment taşır: gutter (ekran kenarı boşluğu), leftEdges (aynı kapsamdaki geniş içeriğin sol kenar kümeleri — sözleşmede tek küme), surfaces (iç hizası bozuk yüzey kapları), widthOutliers (sağ kenarı sapan geniş öğeler, içeri itilme dp'si), offScaleGaps (0/4/8/16/24/32 dışı dikey aralıklar). Yan yana dizilen, ekran dışında kalan ve akış dışı (app-bar, fab, modal…) öğeler kıyasa girmez. Düğümler data-element-id taşımıyorsa layoutReportUnavailable döner."),
301
318
  cookies: z.record(z.string()).optional().describe("Tarayıcıya enjekte edilecek EK çerezler (ad → değer): uygulamanın kendi oturum çerezi buraya verilir. Aynı ada sahip çerez gateway jetonunun yerine geçer. anonymous ile birlikte verilemez."),
302
319
  actions: z.array(z.object({
303
- action: z.enum(['set', 'click', 'wait', 'drag', 'swipe', 'tab', 'select', 'submit']),
304
- target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur. 'drag' için sürüklenen öğe (ör. 'kb_board__card-42', 'cal_plan__event-7'); 'swipe' için jestin basıldığı öğe (ör. tutamaç 'lst__row-2__handle', satır 'lst__row-3'); 'tab' için sekme GRUBUNUN kendisi ('tabs_settings'), 'select' için açılır kutu, 'submit' için form komponenti"),
320
+ action: z.enum(['set', 'click', 'wait', 'drag', 'swipe', 'tab', 'select', 'submit', 'network']),
321
+ target: z.string().min(1).optional().describe("Hedefin elementId'si (page_schema_get); runtime düğümü data-element-id ile bulunur. 'drag' için sürüklenen öğe (ör. 'kb_board__card-42', 'cal_plan__event-7'); 'swipe' için jestin basıldığı öğe (ör. tutamaç 'lst__row-2__handle', satır 'lst__row-3'); 'tab' için sekme GRUBUNUN kendisi ('tabs_settings'), 'select' için açılır kutu, 'submit' için form komponenti. Önizlemenin kendi pencereleri de hedeftir: confirm/alert adımı 'preview-dialog__confirm' / 'preview-dialog__cancel', biometric-auth 'preview-biometric__verify' / '__cancel' — pencereyi açan eylemin raporu pageState 'awaiting-user' der"),
305
322
  to: z.string().min(1).optional().describe("'drag' ve 'swipe': bırakma hedefinin elementId'si ('swipe'ta jest hedefin içinde, hareket yönündeki kenarına 2 px kala biter — sıralamada satır tam hedefin yerine iner; dx/dy ile birlikte verilmez) — kanban kolonu '<elementId>__col-<value>', takvim günü '<elementId>__day-<yyyy-MM-dd>', tüm gün hücresi '<elementId>__allday-<yyyy-MM-dd>', saat dilimi '<elementId>__slot-<yyyy-MM-dd>T<HH>'"),
306
323
  value: z.unknown().optional().describe("'set' için yazılacak değer. Tarih kutularına ISO yazın ('2026-08-01'); çok alanlı tarih aralığında kök DEĞİL kutu hedeflenir: '<elementId>__start' / '<elementId>__end'. 'tab' için sekmenin ANAHTARI ('iot') ya da görünen metni; 'select' için seçeneğin value'su ya da görünen metni"),
307
324
  ms: z.number().int().min(0).max(15000).optional().describe("'wait' için bekleme süresi"),
308
325
  dx: z.number().optional().describe("Yalnız 'swipe': jestin yatay kayması (CSS pikseli; sola kaydırma eksi). to ile birlikte verilmez"),
309
- dy: z.number().optional().describe("Yalnız 'swipe': jestin dikey kayması (CSS pikseli; yukarı eksi). to ile birlikte verilmez")
310
- })).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}, {action:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. Autocomplete'te 'set' metni alana yazar, 'select' metni yazıp öneri satırını GÖRÜNEN METİNLE seçer (value ile değil; eşleşme yoksa yazılan metin alanda kalır ve note bunu söyler). 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'swipe' DOKUNMATİK jesttir (mobil): hedefin merkezine basar, 12 adımda 16 ms arayla taşır ve bırakır (pointer + mouse olayları; sıfır kayma reddedilir — dokunuş 'click'tir) — tutamaçtan sıralama {action:'swipe', target:'lst__row-2__handle', to:'lst__row-0'}, satır kaydırma eylemi {action:'swipe', target:'lst__row-1', dx:-160, dy:0}; 'drag' web'in HTML5 sürüklemesi, 'swipe' dokunmatik jest. 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
326
+ dy: z.number().optional().describe("Yalnız 'swipe': jestin dikey kayması (CSS pikseli; yukarı eksi). to ile birlikte verilmez"),
327
+ blur: z.boolean().optional().describe("Yalnız 'set': false verilirse odak alanda KALIR (telefonda klavye açıkken yazılan hâl) — input+change atılır, blur/focusout atılmaz; odak kaybına bağlı akış ('changed') bu adımda KOŞMAZ. Varsayılan true: gerçek kullanıcı gibi yazıp alandan çıkar")
328
+ })).max(20).optional().describe("Kare ALINMADAN ÖNCE sayfada koşturulacak etkileşim adımları: [{action:'set', target:'flt_month', value:'2022-10'}, {action:'click', target:'btn_sign_in'}, {action:'tab', target:'tabs_settings', value:'iot'}, {action:'select', target:'dd_city', value:'34'}, {action:'submit', target:'frm_note'}, {action:'drag', target:'kb_board__card-42', to:'kb_board__col-Assigned'}]. 'tab' SEKME SEÇER: sekme grubunun köküne 'click' göndermek DOM sırasındaki İLK sekmeye gider ve ikinci sekme hiç açılamıyordu — tablist köküne click artık reddedilir ve bu fiile yönlendirir. Eşleme önce '<elementId>__tab-<sekme anahtarı>' kimliğiyle, o yoksa görünen metinle yapılır; actionsReport.matchedBy hangisi olduğunu, actionsReport.activeTab adımdan SONRA hangi sekmenin açık olduğunu söyler. Etkin olmayan sekmenin içeriği DOM'da YOKTUR: o sekmedeki bir düğüme yazmadan önce 'tab' adımı gerekir. 'select' açılır kutudan seçer — kutu native alan değildir, değeri yazmak hiçbir şey yapmaz, seçeneğe tıklanması gerekir; liste 3 sn'ye kadar YOKLANIR (sorgudan beslenen kutuda seçenekler geç basılır) ve actionsReport.optionsSeen + waitedMs neyin görüldüğünü söyler. Autocomplete'te 'set' metni alana yazar, 'select' metni yazıp öneri satırını GÖRÜNEN METİNLE seçer (value ile değil; eşleşme yoksa yazılan metin alanda kalır ve note bunu söyler). 'submit' formu gönder butonu OLMADAN gönderir (form.requestSubmit); actionsReport.submitVia hangi yolun kullanıldığını yazar. 'drag' HTML5 sürükle-bırak olaylarını (dragstart→dragover→drop→dragend) hedefin merkezinde atar; kanban cardMove ve takvim eventMove bununla tetiklenir. 'swipe' DOKUNMATİK jesttir (mobil): hedefin merkezine basar, 12 adımda 16 ms arayla taşır ve bırakır (pointer + mouse olayları; sıfır kayma reddedilir — dokunuş 'click'tir) — tutamaçtan sıralama {action:'swipe', target:'lst__row-2__handle', to:'lst__row-0'}, satır kaydırma eylemi {action:'swipe', target:'lst__row-1', dx:-160, dy:0}; 'drag' web'in HTML5 sürüklemesi, 'swipe' dokunmatik jest. 'network' bağlamın AĞINI keser ya da açar ({action:'network', value:'offline'} → ağsız yaz/kaydet → {action:'network', value:'online'}): sayfanın tamamı etkilenir, hedef almaz; actionsReport.online sayfanın gördüğü navigator.onLine'dır, ağ açılınca koşan eşitleme akışları flowRuns'a yazılır. Çağrının sonunda ağ yeniden açılır; ağsız davranışı iki çağrı arasında sürdürmek için session ile depo taşınır, ağ taşınmaz. 'set' + blur:false odağı alanda bırakır (telefonda klavye açıkken). 'set' native setter kullanır ve input+change olaylarını bubbles ile atar (düz DOM ataması framework'ün duyduğu olayı üretmez); ardından kök düğümün runtime prop'larını yeniden okur: actionsReport.propWritten değerin prop'a ULAŞIP ulaşmadığıdır ve false ise applied da false döner — DOM'a yazılmış ama bileşenin duymadığı değer 'uygulandı' sayılmaz (G-14). Her adımdan sonra yerleşme beklenir ve yanıttaki actionsReport hedefin bulunup bulunmadığını ve adımdan sonra KAÇ AĞ İSTEĞİ çıktığını söyler — 'bağ kuruldu' ile 'çalışıyor' arasındaki farkı ancak bu ayırır (G-24)."),
329
+ session: z.string().regex(/^[a-z0-9-]{1,40}$/).optional().describe("Çağrılar arası tarayıcı oturumu (kısa ad, [a-z0-9-]{1,40}): verilirse çekim sonunda çerezler, localStorage ve IndexedDB gateway BELLEĞİNDE saklanır ve aynı adla gelen sonraki çekim o depoyla açılır — kalıcı state.app ve çevrimdışı servis kuyruğu ikinci çağrıda okunur. Yanıttaki sessionRestored önceki depo yüklendi mi söyler (ilk çağrıda false). Kimlik ve adres anahtara dahildir: aynı adla farklı kullanıcı (jetonun sub'ı; yenilenen jeton aynı oturumdur), asUserId, cookies, uygulama ya da ortam ayrı oturumdur. Depo okunamazsa oturum silinir ve yanıt sessionSaved: false der. Ömür 30 dk, en çok 8 oturum (en eskisi düşer); gateway yeniden başlayınca silinir. sessionStorage (güvenli depo) taşınmaz. Ağ durumu taşınmaz: her çekim ağ AÇIK başlar. Verilmezse her çekim boş depoyla başlar (bugünkü yalıtım)."),
311
330
  language: z.string().min(1).optional().describe("Kareyi BU DİLDE çeker: adrese '?lang=<kod>' eklenir ve runtime dili oradan seçer (öncelik: ?lang → tarayıcıda kayıtlı seçim → tarayıcı dilleri → projenin varsayılanı). Seçim hem çeviriye bağlı prop'ları hem çok dilli alanların satır değerlerini kapsar, yani ikinci dilin karesi için sayfaya dil değiştirici KOYMAK GEREKMEZ — referans tasarımı bozulmadan ikinci dil kanıtlanır. ISO 639-1 iki harfli kod ('tr', 'en'); bölgeli kod ('tr-TR') indirgenir ve indirgeme yanıtta bildirilir. Kod projenin languages listesinde yoksa çağrı REDDEDİLİR: runtime tanımadığı kodu yok sayıp kareyi varsayılan dilde çizerdi ve 'çevrildi' sanılan yanlış bir kare raporlanırdı. MOBİL uygulamada da geçerlidir (/preview aynı ?lang'i okur); cihazın kendisinde ?lang yoktur (platform_guide('mobile') §Çok dil)."),
312
331
  colorScheme: z.enum(['light', 'dark']).optional().describe("Kareyi açık ya da koyu temada çeker. Ziyaretçi tema tercihi çerezde değil localStorage'da yaşar ve tema store'u onu sayfa açılırken bir kez okur; gateway tercihi sayfa script'lerinden ÖNCE eker, bu yüzden sayfada 'theme-toggle' komponenti OLMASA DA koyu tema karesi alınabilir. Verilmezse projenin kendi modu çizilir."),
313
332
  motion: z.enum(['frozen', 'play']).optional().describe("Varsayılan 'frozen': bağlam 'hareketi azalt' (prefers-reduced-motion: reduce) ile açılır; mobil runtime hareketi dondurur ve kare tekrarlanabilir olur — diff'in imageUnchanged'ı ancak böyle anlamlıdır. Web sayfasında yalnız bu tercihe uyan CSS animasyonları durur. 'play': hareket oynar (giriş animasyonu, dönen gösterge, iskelet parıltısı); kare çekildiği ana bağlıdır, iki kare bayt bayt aynı çıkmayabilir ve yanıt bunu motionNote ile söyler. diff belleği iki kipi ayrı tutar."),
@@ -324,7 +343,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
324
343
  outPath: z.string().min(1).optional().describe('Verilirse PNG bu yerel yola yazılır ve inline görüntü DÖNMEZ; yalnız özet (yol, boyut) döner — büyük fullPage kareleri için')
325
344
  },
326
345
  annotations: readOnly
327
- }, async ({ project, environment, applicationSlug, pageSlug, viewport, device, fullPage, delayMs, captureUnready, anonymous, asUser, asUserId, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme, motion, summary, image, format, quality, scale, clip, diff }) => {
346
+ }, async ({ project, environment, applicationSlug: applicationRef, pageSlug, viewport, device, fullPage, delayMs, captureUnready, anonymous, asUser, asUserId, cookies, layoutReport, actions, outPath, expectFailure, language, colorScheme, motion, summary, image, format, quality, scale, clip, diff, session }) => {
328
347
  try {
329
348
  if (anonymous === true && (asUser !== undefined || cookies !== undefined)) {
330
349
  throw new PlatformApiError("page_screenshot: anonymous:true ile asUser/cookies birlikte verilemez — biri kimliksiz kareyi, diğeri belirli bir kimlikle çekilen kareyi ister. Anonim kare için yalnız anonymous:true, üye kimliğiyle kare için yalnız asUser (ve gerekiyorsa cookies) gönderin.");
@@ -353,6 +372,14 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
353
372
  if (stillSwipe >= 0) {
354
373
  throw new PlatformApiError(`page_screenshot: actions[${stillSwipe}] 'swipe' adımının kayması sıfır — yerinde jest bir basıştır; dokunuş için {action:'click', target:'${actions?.[stillSwipe]?.target}'} kullanın.`);
355
374
  }
375
+ const badNetwork = actions?.findIndex(step => step.action === 'network' && (step.value !== 'offline' && step.value !== 'online' || step.target !== undefined || step.to !== undefined)) ?? -1;
376
+ if (badNetwork >= 0) {
377
+ throw new PlatformApiError(`page_screenshot: actions[${badNetwork}] 'network' adımı yalnız value: 'offline' | 'online' alır, hedef almaz — bağlamın tamamının ağını keser/açar. Ör. {action:'network', value:'offline'}.`);
378
+ }
379
+ const misplacedBlur = actions?.findIndex(step => step.blur !== undefined && step.action !== 'set') ?? -1;
380
+ if (misplacedBlur >= 0) {
381
+ throw new PlatformApiError(`page_screenshot: actions[${misplacedBlur}] '${actions?.[misplacedBlur]?.action}' adımı blur almaz — odakta kalma yalnız 'set'in seçeneğidir.`);
382
+ }
356
383
  if (!config.studioBaseUrl) {
357
384
  throw new PlatformApiError("page_screenshot için STUDIO_BASE_URL tanımlı değil. Studio arayüzünün kök adresini .env'e ekleyin (yerel geliştirmede http://localhost:3000, dağıtımda nginx adresi).");
358
385
  }
@@ -388,6 +415,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
388
415
  // 37 S-17 / 38 S-13: sorgulu slug (`ilac-detay?id=7`) dizeyle birleşince dil ikinci bir '?' ile
389
416
  // ekleniyordu ve runtime `id`'yi '7?lang=tr' okudu; sayfa isteği işareti de sorguyu taşıdığı
390
417
  // için sayfanın kendi veri isteğiyle hiç eşleşmiyordu.
418
+ const applicationSlug = await runtimeApplicationSlug(platform, project, applicationRef, environment);
391
419
  const address = new URL(`${config.studioBaseUrl}/${lane}/${projectSlug}/${applicationSlug}/${pageSlug}`);
392
420
  if (languageChoice)
393
421
  address.searchParams.set('lang', languageChoice.code);
@@ -395,7 +423,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
395
423
  const pagePath = pageSlug.split(/[?#]/, 1)[0];
396
424
  // Hedef ve cihaz uygulamanın platform profilinden (G3): mobil uygulama react-native-web
397
425
  // çizimidir, varsayılan kare telefon ön ayarıdır. Tip okunamazsa web davranışı aynen.
398
- const resolved = await resolveProfileBySlug(platform, projectSlug, applicationSlug);
426
+ // Eski production slug'ı Core'da yoktur: id verildiyse tip id'den okunur, slug'la arama web'e düşerdi (APP-002).
427
+ const resolved = isGuid(applicationRef) ? await resolveProfile(platform, applicationRef) : await resolveProfileBySlug(platform, projectSlug, applicationSlug);
399
428
  const target = captureTargetFor(resolved.profile.capture.target);
400
429
  const mobile = resolved.profile.platform === 'mobile';
401
430
  if (device !== undefined && !mobile) {
@@ -439,7 +468,27 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
439
468
  ...(clip === undefined ? {} : { clip: clip }),
440
469
  ...(actions === undefined ? {} : { actions: actions }),
441
470
  ...(mobile ? { target } : {}),
442
- ...(dpr === undefined ? {} : { dpr })
471
+ ...(dpr === undefined ? {} : { dpr }),
472
+ // Oturum kimlikle ve adresle ayrılır: jetonun öznesi (`sub`; yenilenen gateway jetonu oturumu düşürmesin, yoksa
473
+ // jetonun kendisi), asUserId, çerez değerleri, proje, uygulama ve ortam anahtara hash'lenerek girer.
474
+ ...(session === undefined ? {} : {
475
+ session: `${session}|${captureKey({
476
+ identity,
477
+ principal: `${(() => {
478
+ try {
479
+ const claims = JSON.parse(Buffer.from(String(identityToken ?? '').split('.')[1] ?? '', 'base64url').toString('utf8'));
480
+ return typeof claims.sub === 'string' ? `sub:${claims.sub}` : identityToken ?? '';
481
+ }
482
+ catch {
483
+ return identityToken ?? '';
484
+ }
485
+ })()}|${asUserId ?? ''}`,
486
+ cookieValues: extraCookies.map(cookie => `${cookie.name}=${cookie.value}`),
487
+ project: projectSlug,
488
+ applicationSlug,
489
+ environment: resolvedEnvironment
490
+ })}`
491
+ })
443
492
  });
444
493
  // Fark ÖZETTEN çıkar; `summary:false` verilse de fark için toplanmıştır (yanıta girmez).
445
494
  // Anahtar KİMLİĞİ ayırır (K8): 'asUser' etiketi yetmez — iki farklı üyenin jetonu aynı
@@ -494,7 +543,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
494
543
  const emptyOptions = actionsOf(outcome => outcome.action === 'select' && outcome.optionsSeen === 0);
495
544
  const unwritten = actionsOf(outcome => outcome.propWritten === false);
496
545
  // 'wait' bir eylem değil bekleyiştir: akış sayacı ona bağlanmaz, notu tetiklemez (33/S-1).
497
- const silent = actionsOf(outcome => outcome.action !== 'wait' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0);
546
+ const silent = actionsOf(outcome => outcome.action !== 'wait' && outcome.action !== 'network' && outcome.applied && outcome.requests === 0 && (outcome.flowRuns ?? 0) === 0);
498
547
  if (missing.length > 0)
499
548
  actionNotes.push(`Bulunamayan hedef var: ${missing.map(outcome => outcome.target ?? '?').join(' · ')}. elementId'leri page_schema_get ile doğrulayın; runtime düğümü data-element-id ile bulunuyor.`);
500
549
  if (textMatchedTabs.length > 0)
@@ -521,6 +570,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
521
570
  fullPage: fullPage === true,
522
571
  identity,
523
572
  cookieNames,
573
+ ...(session === undefined ? {} : { session, sessionRestored: shot.sessionRestored === true, ...(shot.sessionSaved === false ? { sessionSaved: false, sessionNote: 'Depo durumu okunamadı: oturum SİLİNDİ, aynı adla sonraki çekim boş depoyla başlar (sessionRestored: false).' } : {}) }),
524
574
  ...(shot.png === undefined
525
575
  ? { image: 'none' }
526
576
  : {
@@ -582,7 +632,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
582
632
  ...(shot.pageState === 'ready-no-flows'
583
633
  ? { pageStateNote: 'Runtime çizildi ama hiç mount akışı koşmadı. Sayfa akışsızsa (giriş ekranı) ya da verisi veri kaynağı bağlantısından geliyorsa (connectData prop akış koşturmadan okunur; summaryText satırları gösterir) normaldir; veri bekliyor ve göremiyorsanız page_schema_get ile mount olayına bağlı akışa, bağlı prop için connect_data_get / connect_data_execute (op list) ile bağlantıya bakın.' }
584
634
  : shot.pageState === 'awaiting-user'
585
- ? { pageStateNote: "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." }
635
+ ? { pageStateNote: awaitingUserNote(shot.awaitingWindow) }
586
636
  : shot.pageState === 'ready-no-counter'
587
637
  ? { pageStateNote: 'Runtime koşan akış sayacı yayınlamıyor (eski runtime); hazırlık DOM durulmasından çıkarıldı, akışların bittiği kanıtlanamadı.' }
588
638
  : shot.pageState === 'error'
@@ -816,6 +866,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
816
866
  for (const entry of opRecords)
817
867
  reject('PAGE-008', entry, unknownEventOps([entry], typesByElementId, eventsBySlug));
818
868
  }
869
+ // g-21: tanınmayan prop platformda bütün yamayı `unknown-prop` ile düşürür; op'u adıyla burada söyleriz.
870
+ for (const issue of await unknownPropOps(platform, project, environment, applicationId, async () => (await readPage()).schema, allOps))
871
+ reject('unknown-prop', issue.op, [issue.message]);
819
872
  if (rejected.length > 0)
820
873
  throw new PlatformApiError(rejectedOpsMessage(rejected, batchIndexHint(opened)), 400);
821
874
  // TR-005 / TR-006: yamanın yükü op'ların EZDİĞİ kayıtlı değerle karşılaştırılır — orada zaten duran kanca eskidir.
@@ -102,7 +102,7 @@ export function registerPlatformTools(server, platform) {
102
102
  'Modülün iş kuralları rehberini döner. Bir modülde İLK yazma işleminden önce ilgili rehberi okuyun:',
103
103
  ...guides.map(name => `- ${name}${guideRouting[name] ? `: ${guideRouting[name]}` : ''}`),
104
104
  "Büyük rehberler (page ~120 KB) section'sız çağrıda TAM METİN DÖNMEZ: giriş + bölüm listesi (outline) döner, truncated: true olur. Gövdeyi section ile bölüm bölüm alın.",
105
- "Büyük gövdeyi (sayfa şeması, akış adımları, uç/workflow tanımı, patch op'ları) dosyaya yazıp bodyFile ile verin: gövde bağlamınızdan geçmez, satır içiyle aynı doğrulamadan geçer (page_*/flow_*/microservice_*/workflow_* create/update/patch). microservice_execute yanıt başlıkları varsayılan kısadır; ucun kendi başlığı için includeHeaders:'all'.",
105
+ "Büyük gövdeyi (sayfa şeması, akış adımları, uç/workflow tanımı, patch op'ları) dosyaya yazıp bodyFile ile verin: gövde bağlamınızdan geçmez, satır içiyle aynı doğrulamadan geçer (page_*/flow_*/microservice_*/workflow_* create/update/patch). Uzun kod alanı ayrı dosyada durabilir: gövdede { \"$file\": \"./adim.js\" } o dosyanın METNİYLE değişir (çalıştırılmaz; yol bodyFile'ın klasörüne göre, GW-013). microservice_execute yanıt başlıkları varsayılan kısadır; ucun kendi başlığı için includeHeaders:'all'.",
106
106
  "Örnek gövdeler ve kural katalogları (ngapps://examples/<ad>, ngapps://rules/<ad>) istemci MCP resource okuyamıyorsa da bu araçla okunur: platform_guide({ resource: 'ngapps://rules/connect-data' }) — guide verilmez; dizin için 'ngapps://examples/index'."
107
107
  ].join('\n');
108
108
  server.registerTool('platform_whoami', {