@appaflytech/zeyrek-ai-mcp 0.0.51 → 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 (58) hide show
  1. package/dist/knowledge/componentCatalog.js +3 -1
  2. package/dist/knowledge/componentEvents.js +13 -1
  3. package/dist/knowledge/expressionPrelude.js +3 -0
  4. package/dist/knowledge/mobileComponentCatalog.js +4 -1
  5. package/dist/knowledge/mobileStore.js +258 -1
  6. package/dist/knowledge/normalize.js +4 -3
  7. package/dist/knowledge/platform/mobile.js +1 -1
  8. package/dist/knowledge/platform/mobileRules.js +79 -0
  9. package/dist/knowledge/rules.js +199 -6
  10. package/dist/knowledge/storeImport.js +69 -0
  11. package/dist/platform/argumentAliases.js +13 -4
  12. package/dist/platform/bodyFile.js +195 -38
  13. package/dist/platform/capture/actions.js +24 -5
  14. package/dist/platform/capture/capture.js +80 -11
  15. package/dist/platform/capture/persistent-browser.js +23 -1
  16. package/dist/platform/capture/readiness.js +32 -1
  17. package/dist/platform/errors.js +45 -5
  18. package/dist/platform/http.js +17 -9
  19. package/dist/platform/responses.js +7 -1
  20. package/dist/platform/secrets.js +8 -0
  21. package/dist/platform/token.js +32 -21
  22. package/dist/resources/examples/flow-credit-spend.json +233 -0
  23. package/dist/resources/examples/flow-purchase-paywall.json +662 -0
  24. package/dist/resources/examples/mobile-offline-list.json +1 -1
  25. package/dist/resources/examples/mobile-offline-queue.json +1 -1
  26. package/dist/resources/examples/mobile-offline-seamless.json +12 -3
  27. package/dist/resources/examples/store-import-mapping.json +25 -0
  28. package/dist/resources/guides/deployment.md +9 -0
  29. package/dist/resources/guides/expressions.md +1 -0
  30. package/dist/resources/guides/microservice.md +13 -1
  31. package/dist/resources/guides/mobile-publish.md +67 -2
  32. package/dist/resources/guides/mobile-steps.md +91 -3
  33. package/dist/resources/guides/mobile.md +38 -8
  34. package/dist/resources/guides/page.md +39 -8
  35. package/dist/resources/guides/translation.md +3 -1
  36. package/dist/resources/guides/workflow.md +30 -0
  37. package/dist/resources/schema-fingerprint.json +2 -2
  38. package/dist/resources/schemas/CreateWorkflowCommand.json +6 -1
  39. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +4 -0
  40. package/dist/resources/schemas/PreviewWorkflowCommand.json +6 -1
  41. package/dist/resources/schemas/StepDto.json +6 -1
  42. package/dist/resources/schemas/UpdateWorkflowCommand.json +6 -1
  43. package/dist/server.js +4 -0
  44. package/dist/tools/applications.js +7 -2
  45. package/dist/tools/deployments.js +1 -1
  46. package/dist/tools/designSchemaPatch.js +7 -2
  47. package/dist/tools/environments.js +40 -0
  48. package/dist/tools/flows.js +102 -9
  49. package/dist/tools/mobile.js +56 -0
  50. package/dist/tools/mobilePurchases.js +236 -0
  51. package/dist/tools/mobileStore.js +2 -2
  52. package/dist/tools/mobileStoreImport.js +173 -0
  53. package/dist/tools/pages.js +45 -11
  54. package/dist/tools/platformInfo.js +4 -4
  55. package/dist/tools/screenWrite.js +107 -1
  56. package/dist/tools/variables.js +1 -1
  57. package/package.json +1 -1
  58. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -374,6 +374,35 @@ gidilir, hiçbiri tutmazsa `else` dalı yürür:
374
374
  | `automation` | Entity satır olayına tepki | `entityId` + `operation` + `isQueue` | returnDtoId YASAK; aşağıdaki "Automation gerçekleri" |
375
375
  | `middleware` | Microservice/sayfa **çalışmadan önce** koşan guard/setter (auth, `user`/`settings` değişkenleri) | Bağlandığı uç çağrılınca (`middlewareIds`) | returnDtoId gerekmez; `set-user`/`variable`/`setcookie`/`redirect` yalnız bu tipte. **Ayrı rehberi var: `platform_guide('middleware')`** |
376
376
 
377
+ ## Balance adımı: bakiye oku, düş, ekle, hak var mı (kök ADR 0027, `StepType.Balance`)
378
+
379
+ Mobil uygulamanın tüketilen ürün bakiyesini (kredi, jeton) sunucu iş akışından yönetir. İşlem `rule.balanceOperation`
380
+ (`read` · `spend` · `grant` · `entitled`), girdi `rule.expression` nesne literalidir (parantezle ya da doğrudan `{ … }`):
381
+ `application` (mobil uygulama slug'ı) ya da `applicationId`, `userId` (yoksa oturumdaki uygulama kullanıcısı) ya da
382
+ `installationId`, `balance`, `amount` (tam sayı ya da metin), `reason` (`grant`, ≤ 200), `idempotencyKey`, `entitlement`
383
+ (`entitled`). Çıktı `read`/`spend`/`grant`'ta `{ balance, balances }`, `entitled`'da `{ entitled, expiresAt }` (birden çok
384
+ açık satırdan biri süresizse `expiresAt: null`). Hata — yetersiz bakiye, geçersiz girdi, kullanıcı yok — adımın **hata
385
+ dalına** gider; `BalanceInsufficientException` kodu `errorCode` olarak okunur. Üç workflow tipinde de serbesttir.
386
+
387
+ - **Bakiye adı katalogdadır:** statik `balance` uygulamanın tüketilen ürünlerinden birinin `balance` alanı, statik
388
+ `entitlement` bir ürünün `entitlement`'ı olmalı — olmayan ad yazımda `FLOW-046` ile durur (hata katalogdakileri sayar;
389
+ `mobile_product_list`). İfadeyle (`input.ad`) verilen ad yazımda denetlenmez. Katalog akışın uygulamasından (ya da
390
+ ifadedeki GUID `applicationId`'den) okunur; okunamazsa kural susar.
391
+ - **Tekrar güvenliği varsayılan DEĞİLDİR:** `idempotencyKey` verilmezse adım çalışma kimliği kullanılır ve o her çalışmada
392
+ yenidir — yeniden koşabilen akış (yeniden deneme, döngü, aynı olayın iki kez gelmesi) iki kez düşer/ekler. `spend`/`grant`
393
+ için olayı tanımlayan bir anahtar verin (`idempotencyKey: input.davetId`); `FLOW-046` anahtarsız `spend`/`grant`'ı
394
+ uyarır. `spend:` ve `grant:` anahtar uzayları ayrıdır.
395
+ - **Tarif — krediyi düş, işi yap, iş düşerse geri ekle:** (1) `balance` `spend` `{ balance: 'kredi', amount: 10,
396
+ idempotencyKey: input.istekId }`; `success` → (2) işi yapan adım (ör. `http`/`microfunction`); onun `fail` dalı → (3)
397
+ `balance` `grant` `{ balance: 'kredi', amount: 10, reason: 'rapor üretilemedi', idempotencyKey: 'iade-' + input.istekId }`;
398
+ `spend`'in kendi hata dalı (`insufficient`) → `response` 409 gövdesi. Önce düşmek yarışta bakiyeyi korur; iade ayrı
399
+ anahtarlı olduğu için tekrar koşarsa bir kez yazılır. Defterde `grant` satırı mağaza iadesinden (`refund`) ayrı türdür.
400
+ - **Kilit:** `spend`/`grant` aynı sahibin eşzamanlı harcamasını sıralayan danışma kilidini işlem sonuna kadar tutar —
401
+ `balance` adımını akışın sonuna yakın ve kısa tutun, ondan sonra uzun süren adım (HTTP, e-posta) koyma.
402
+ - `entitled` sunucudaki hak satırlarına bakar (açık ve süresi geçmemiş); istemci ne yollarsa yollasın cevap değişmez —
403
+ istemci tarafı `Entitlements.<hak>` yalnız ekranı gizler, sunucu denetimi bunun yerine geçmez.
404
+ - Önizleme (`workflow_preview`) işlemi geri alır: `grant`/`spend` gerçekten koşar ve kalıcı satır bırakmaz.
405
+
377
406
  ## Step tipi × workflow tipi izin matrisi
378
407
 
379
408
  | Step | workflow | automation | middleware |
@@ -384,6 +413,7 @@ gidilir, hiçbiri tutmazsa `else` dalı yürür:
384
413
  | convert | ✓ | – | – |
385
414
  | revert (Fail and Rollback) | – | ✓ | – |
386
415
  | process (BPM süreci) | ✓ | ✓ | – |
416
+ | balance (uygulama bakiyesi / hak denetimi) | ✓ | ✓ | ✓ |
387
417
 
388
418
  > **`process` adımı süreci başlatır ya da ona sinyal/vekalet yazar, BEKLEMEZ (2026-09-18).**
389
419
  > Kural alanları `processSlug`, `processOperation: start | signal | delegate` (boş = `start`) ve
@@ -1,6 +1,6 @@
1
1
  {
2
- "hash": "bbe34cf1a8761567c57d93203e7f68c603fc338b066670776edf9c224625f758",
3
- "generatedAt": "2026-10-30T15:49:07.850Z",
2
+ "hash": "162e8c2d14c5a0737d023b55790bab139812a00ec8e2e6335ed51981bb27e548",
3
+ "generatedAt": "2026-11-02T08:52:07.624Z",
4
4
  "types": 377,
5
5
  "files": 196,
6
6
  "note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
@@ -233,7 +233,8 @@
233
233
  "revert",
234
234
  "set-user",
235
235
  "microfunction",
236
- "process"
236
+ "process",
237
+ "balance"
237
238
  ],
238
239
  "type": "string",
239
240
  "format": ""
@@ -349,6 +350,10 @@
349
350
  "type": "string",
350
351
  "nullable": true
351
352
  },
353
+ "balanceOperation": {
354
+ "type": "string",
355
+ "nullable": true
356
+ },
352
357
  "queryId": {
353
358
  "type": "string",
354
359
  "format": "uuid",
@@ -254,6 +254,10 @@
254
254
  "push-notification",
255
255
  "mobile-build",
256
256
  "mobile-runner",
257
+ "mobile-store-product",
258
+ "mobile-store-import",
259
+ "app-purchase-import",
260
+ "mobile-store-diff",
257
261
  "organization-schema",
258
262
  "organization-unit",
259
263
  "organization-position",
@@ -171,7 +171,8 @@
171
171
  "revert",
172
172
  "set-user",
173
173
  "microfunction",
174
- "process"
174
+ "process",
175
+ "balance"
175
176
  ],
176
177
  "type": "string",
177
178
  "format": ""
@@ -287,6 +288,10 @@
287
288
  "type": "string",
288
289
  "nullable": true
289
290
  },
291
+ "balanceOperation": {
292
+ "type": "string",
293
+ "nullable": true
294
+ },
290
295
  "queryId": {
291
296
  "type": "string",
292
297
  "format": "uuid",
@@ -76,7 +76,8 @@
76
76
  "revert",
77
77
  "set-user",
78
78
  "microfunction",
79
- "process"
79
+ "process",
80
+ "balance"
80
81
  ],
81
82
  "type": "string",
82
83
  "format": ""
@@ -192,6 +193,10 @@
192
193
  "type": "string",
193
194
  "nullable": true
194
195
  },
196
+ "balanceOperation": {
197
+ "type": "string",
198
+ "nullable": true
199
+ },
195
200
  "queryId": {
196
201
  "type": "string",
197
202
  "format": "uuid",
@@ -233,7 +233,8 @@
233
233
  "revert",
234
234
  "set-user",
235
235
  "microfunction",
236
- "process"
236
+ "process",
237
+ "balance"
237
238
  ],
238
239
  "type": "string",
239
240
  "format": ""
@@ -349,6 +350,10 @@
349
350
  "type": "string",
350
351
  "nullable": true
351
352
  },
353
+ "balanceOperation": {
354
+ "type": "string",
355
+ "nullable": true
356
+ },
352
357
  "queryId": {
353
358
  "type": "string",
354
359
  "format": "uuid",
package/dist/server.js CHANGED
@@ -54,6 +54,8 @@ import { registerBpmReportingTools } from "./tools/bpmReporting.js";
54
54
  import { registerModuleTools } from "./tools/modules.js";
55
55
  import { registerMobileTools } from "./tools/mobile.js";
56
56
  import { registerMobileStoreTools } from "./tools/mobileStore.js";
57
+ import { registerMobilePurchaseTools } from "./tools/mobilePurchases.js";
58
+ import { registerMobileStoreImportTools } from "./tools/mobileStoreImport.js";
57
59
  import { registerAssistantTools } from "./tools/assistant.js";
58
60
  const moduleRegistrations = {
59
61
  platform: registerPlatformTools,
@@ -111,6 +113,8 @@ const moduleRegistrations = {
111
113
  mobile: (server, platform) => {
112
114
  registerMobileTools(server, platform);
113
115
  registerMobileStoreTools(server, platform);
116
+ registerMobilePurchaseTools(server, platform);
117
+ registerMobileStoreImportTools(server, platform);
114
118
  registerMobilePublishPrompt(server);
115
119
  },
116
120
  assistant: registerAssistantTools,
@@ -113,7 +113,7 @@ export function registerApplicationTools(server, platform) {
113
113
  }));
114
114
  server.registerTool('application_update', {
115
115
  title: 'Application güncelle',
116
- description: `Application bilgilerini günceller; homepageId zorunludur (mevcut değeri application_get ile alın). ${templateSubscriptionLock}`,
116
+ description: `Application bilgilerini günceller; homepageId zorunludur (mevcut değeri application_get ile alın). ORTAM (K5, b-15): ayar alanları — properties (mobile.offline, sekmeler, geçiş…), homepageId, notFoundPageId — yalnız development'a yazılır; production onları deployment_start ile alır (dal ortamları açıldıkları andaki kopyayı tutar), yani ayarı önce development'ta deneyin. Kimlik alanları (name, slug, color, dns) bütün ortamlara hemen gider. Yanıttaki environmentNote bunu çağrı başına söyler. ${templateSubscriptionLock}`,
117
117
  inputSchema: {
118
118
  id: guid,
119
119
  slug: z.string().min(1),
@@ -132,7 +132,12 @@ export function registerApplicationTools(server, platform) {
132
132
  : [];
133
133
  const response = await platform.org('PUT', `/applications/${id}`, { body });
134
134
  forgetApplication(platform, id);
135
- return withPlatformWarnings(response, warnings);
135
+ // Uç tam kaydı yazar: verilmeyen properties/notFoundPageId development'ta da boşalır (APP-001, inceleme #4).
136
+ const missing = ['properties', 'notFoundPageId'].filter((field) => body[field] === undefined);
137
+ return {
138
+ ...withPlatformWarnings(response, warnings),
139
+ environmentNote: `properties, homepageId ve notFoundPageId verildiği gibi yalnız development'a (ve Core'a) yazıldı; production onları deployment_start ile alır, dal ortamları hiç almaz (APP-001). name/slug/color/dns bütün ortamlara gitti.${missing.length > 0 ? ` ${missing.join(' ve ')} verilmedi: uç tam kaydı yazar, development'taki değer boşaltıldı — korumak için application_get'teki mevcut değeri gönderin.` : ''}`
140
+ };
136
141
  }));
137
142
  server.registerTool('application_delete', {
138
143
  title: 'Application sil',
@@ -51,7 +51,7 @@ export function registerDeploymentTools(server, platform) {
51
51
  }));
52
52
  server.registerTool('deployment_status', {
53
53
  title: 'Deployment durumu',
54
- description: "Projenin son deployment durumunu döner. 'projectId' ZORUNLUDUR (project_list/project_get'ten alın) — verilmezse çağrı MCP şema katmanında reddedilir, platforma hiç gitmez. Hiç deployment yapılmamış projede yanıt BOŞ gelir; o durumda tool 'hiç deployment kaydı yok' bilgisini açıkça döner, yani boş yanıtı 'tool çalışmadı' diye okumayın. Durum satırı modül başına ayrılmaz: süreç tanımları ve iş takvimleri sayfa ve workflow'lardan sonra aynı saga adımında aktarılır; hedefte çalışamayan (eksik workflow/entity/rol) tanım aktarımı DefinitionInvalid ile düşürür. type 'error' ise 'error' alanı düşüş nedenini taşır (DefinitionInvalid'de bulgular 'yol: mesaj | …' olarak eklenir); aktarım atomiktir, hedef değişmemiştir. type 'completed-with-mismatch': deploy tamamlandı, kayıtlar production'a yazıldı, ama ardından koşan özet denetimi production'da fark buldu; sonraki deploy otomatik tam (full) koşar. Yanıt mode (full | delta: deploy'un gerçekte koştuğu kip) ve counters (delta'da modül başına added/updated/deleted) taşır.",
54
+ description: "Projenin son deployment durumunu döner. Bittikten sonra iki ortamın tanım farkını environment_diff verir (özetten; tablo satırları hariç). 'projectId' ZORUNLUDUR (project_list/project_get'ten alın) — verilmezse çağrı MCP şema katmanında reddedilir, platforma hiç gitmez. Hiç deployment yapılmamış projede yanıt BOŞ gelir; o durumda tool 'hiç deployment kaydı yok' bilgisini açıkça döner, yani boş yanıtı 'tool çalışmadı' diye okumayın. Durum satırı modül başına ayrılmaz: süreç tanımları ve iş takvimleri sayfa ve workflow'lardan sonra aynı saga adımında aktarılır; hedefte çalışamayan (eksik workflow/entity/rol) tanım aktarımı DefinitionInvalid ile düşürür. type 'error' ise 'error' alanı düşüş nedenini taşır (DefinitionInvalid'de bulgular 'yol: mesaj | …' olarak eklenir); aktarım atomiktir, hedef değişmemiştir. type 'completed-with-mismatch': deploy tamamlandı, kayıtlar production'a yazıldı, ama ardından koşan özet denetimi production'da fark buldu; sonraki deploy otomatik tam (full) koşar. Yanıt mode (full | delta: deploy'un gerçekte koştuğu kip) ve counters (delta'da modül başına added/updated/deleted) taşır.",
55
55
  inputSchema: { projectId: guid },
56
56
  annotations: readOnly
57
57
  }, handle(config, async ({ projectId }) => {
@@ -2,7 +2,7 @@ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEvent
2
2
  import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, removeNodeFromTree, planEventRemovals, callerSlots, planEmulatedOps, recordsWithEventRemovals, serverOpsInOrder, assertOpBodies, applySkeletonOps, reshapesSkeleton } from './schemaOps.js';
3
3
  import { guid, projectParam, environmentParam, updates, microOps } from './common.js';
4
4
  import { fetchHost, hostPath } from './screens.js';
5
- import { assertScreenWriteGuards, screenCatalogNotes } from './screenWrite.js';
5
+ import { assertScreenWriteGuards, screenCatalogNotes, unknownPropOps } from './screenWrite.js';
6
6
  import { emptyWriteDiff, schemaOpDrops, withWriteDiff } from '../platform/writeDiff.js';
7
7
  import { fetchComponentEventNames } from './components.js';
8
8
  import { boxSectionNote, collectIds, ensureBoxSections, expandSectionToggles } from '../knowledge/boxSections.js';
@@ -10,7 +10,7 @@ import { PlatformApiError } from '../platform/errors.js';
10
10
  import { connectDataOpIssues, connectedPropKeys } from '../knowledge/connectDataDefinition.js';
11
11
  import { handle } from '../platform/responses.js';
12
12
  import { slotWarning } from './layouts.js';
13
- import { batchEnvelopeNote, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
13
+ import { batchEnvelopeNote, batchIndexHint, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
14
14
  import { applicationTypeField, resolveProfile } from '../knowledge/platform/resolve.js';
15
15
  import { boundFlowSteps, layoutPatchWarnings, screenProfileWarnings } from './profileWrite.js';
16
16
  import { enforceTextHooks, textHookOverrideParam } from './textHookGuard.js';
@@ -87,6 +87,11 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
87
87
  const events = host === 'layout' ? record.events : undefined;
88
88
  const normalized = normalizeSchemaOps(opRecords);
89
89
  assertOpBodies(opRecords, propTypeOps);
90
+ // g-21: tanınmayan prop sayfa yamasındaki gibi istek gitmeden op'uyla adlandırılır; yama bütün reddedilir.
91
+ const unknownProps = await unknownPropOps(platform, project, environment, applicationId, async () => nodes, allOps);
92
+ if (unknownProps.length > 0) {
93
+ throw new PlatformApiError(`${spec.tool}: tanınmayan prop (unknown-prop) — yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi): ${unknownProps.map(issue => issue.message).join(' | ')}${batchIndexHint(opened)}`, 400);
94
+ }
90
95
  // 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.
91
96
  const textFindings = await enforceTextHooks(config, {
92
97
  platform, project, environment, applicationId,
@@ -18,6 +18,46 @@ export function registerEnvironmentTools(server, platform) {
18
18
  inputSchema: { projectId: guid },
19
19
  annotations: readOnly
20
20
  }, handle(config, ({ projectId }) => platform.org('GET', `/environments/${projectId}`)));
21
+ server.registerTool('environment_diff', {
22
+ title: 'İki ortamın tanım farkı',
23
+ description: "İki ortamın (varsayılan development → production) TANIMLARINI karşılaştırır ve farklı olanları modül modül listeler: fark türü onlyInSource (yalnız kaynakta, dağıtılmamış) · onlyInTarget (yalnız hedefte) · changed (içerik farklı). Dağıtımdan sonra (deployment_status bitti dedikten sonra) 'production development'la aynı mı' sorusunun tek çağrılık cevabıdır. Fark tanım özetinden (definition digest) gelir, gövdeden değil: sırasız diziler ve çoğu üretilmiş alt kimlik karşılaştırmaya girmez — AMA akış adım parametrelerinin kimlikleri girer: development'ta yeniden yazılan (flow_update) bir akış içeriği aynı olsa da changed görünebilir; dağıtım kimlikleri koruduğu için dağıtımdan hemen sonraki karşılaştırma bundan etkilenmez. Kapsamı özetlenen modüllerdir (uygulama, rol, sorgu, DTO, görünüm, workflow, servis, klasör, layout, sayfa, akış, değişken, fonksiyon, mikrofonksiyon, çeviri); tablolar, bileşenler, erişim politikaları, süreçler, dosyalar ve asistan KAPSANMAZ — yanıtın notCovered alanı bu modülleri sayar, 'eşit' cevabı onlar için değildir. Tablo SATIRLARI (recordTransferMode) karşılaştırılmaz. En çok 50 kayıt satırı döner; fazlası truncated: true. Salt okunur; hiçbir ortama yazmaz.",
24
+ inputSchema: {
25
+ projectId: guid,
26
+ source: z.string().min(1).optional().describe("Kaynak ortam adı; varsayılan 'development'"),
27
+ target: z.string().min(1).optional().describe("Hedef ortam adı; varsayılan 'production'"),
28
+ modules: z.array(z.string().min(1)).max(20).optional().describe("Yalnız bu modüller, backend'in modül adıyla (yanıtın modules[].module değerleri; ör. 'page', 'flow', 'microservice', 'data_transfer_object', 'global-function')")
29
+ },
30
+ annotations: readOnly
31
+ }, handle(config, async ({ projectId, source = 'development', target = 'production', modules }) => {
32
+ // Tek modül ve 50 satır sınırı sunucuda uygulanır (adlar yalnız listelenen kayıtlar için okunur); birden çok modül
33
+ // süzgecinde sunucu tavanı istenir, süzme burada yapılır.
34
+ const single = modules?.length === 1 ? modules[0] : undefined;
35
+ const response = await platform.org('GET', `/revisions/${projectId}/environment-diff`, {
36
+ query: { source, target, ...(single ? { module: single } : {}), limit: modules && modules.length > 1 ? 1000 : 50 }
37
+ });
38
+ const diff = (response?.data ?? response ?? {});
39
+ const wanted = (module) => modules === undefined || modules.includes(module);
40
+ const records = (diff.records ?? []).filter((record) => wanted(record.module));
41
+ const total = modules && modules.length > 1 ? records.length : Math.max(diff.total ?? 0, records.length);
42
+ const moduleCounts = (diff.modules ?? []).filter((module) => wanted(module.module));
43
+ return {
44
+ source,
45
+ target,
46
+ equal: total === 0,
47
+ ...(total === 0 ? { note: `${source} ve ${target} özetlenen modüllerde eşit — notCovered'daki modüller karşılaştırılmadı.` } : {}),
48
+ modules: moduleCounts,
49
+ records: records.slice(0, 50).map((record) => ({
50
+ module: record.module,
51
+ ...(record.name ? { name: record.name } : {}),
52
+ id: record.recordId,
53
+ change: record.state === 'different' ? 'changed' : record.state
54
+ })),
55
+ ...(total > 50 || diff.truncated === true ? { truncated: true, total, truncatedNote: "50 satırdan fazla fark var — modules ile süzün (ör. modules: ['page'])." } : {}),
56
+ notCovered: diff.notCovered ?? [],
57
+ recordsNote: 'Tablo satırları (recordTransferMode) karşılaştırılmaz; bu yalnız tanım farkıdır.',
58
+ ...(diff.digestCheck?.possiblyStale ? { digestNote: 'Ortamlardan birinin özeti hiç denetlenmemiş, son denetimi düşmüş ya da güvenilir değil: fark bayat olabilir.' } : {})
59
+ };
60
+ }));
21
61
  server.registerTool('environment_create', {
22
62
  title: 'Dal environment\'ı aç',
23
63
  description: `Projede yeni bir dal environment'ı açar: kendi PG şeması kurulur ve development'ın tanımları oraya kopyalanır. Asenkron bir saga'dır — yanıt 202 döner, dal hemen kullanılabilir DEĞİLDİR. includeRecords true ise projenin KENDİ veritabanındaki bütün veri tablolarının satırları da kopyalanır (çok dilli companion tablolar ve junction'lar dahil; files/folders ve audit trail tabloları hariç, dış datasource'lara dokunulmaz) — entity'de kayıt taşıması ayarı açık olmasa bile. Merge'de yalnız kayıt taşıması ayarı açık tabloların satırları üç yollu geri taşınır (rehber: merge, Tablo kayıtları). ${templateSubscriptionLock}`,
@@ -17,6 +17,8 @@ import { randomUUID } from 'node:crypto';
17
17
  import { flowProfileWarnings } from './profileWrite.js';
18
18
  import { enforceTextHooks } from './textHookGuard.js';
19
19
  import { readMobileOffline } from '../knowledge/platform/mobileOffline.js';
20
+ import { restorePathWarnings, subscriptionDisclosureWarnings } from '../knowledge/platform/mobileRules.js';
21
+ import { balanceCatalogFindings, balanceWithoutSignInWarnings, purchaseCatalogFindings } from '../knowledge/mobileStore.js';
20
22
  import { resolveProfile } from '../knowledge/platform/resolve.js';
21
23
  import { jsonArray } from '../platform/jsonArray.js';
22
24
  import { z } from 'zod';
@@ -62,6 +64,23 @@ function hostNodes(record) {
62
64
  return record.schema ?? record.views ?? [];
63
65
  }
64
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
+ }
65
84
  /**
66
85
  * TB-126 / S2-36 — satır aksiyonu taşıyabilen komponentler. Bu tiplerin `actions` prop'unu çoğu
67
86
  * zaman YÜKLEME AKIŞI çalışma zamanında yazar (aksiyon role göre eklenip çıkarılıyorsa başka yolu
@@ -399,6 +418,60 @@ async function uncachedReadWarnings(platform, applicationId, steps, errorSteps,
399
418
  }
400
419
  return warnings;
401
420
  }
421
+ /**
422
+ * FLOW-046 katalog ayağı ve MOB-036 (g-02): yazılan akışta `balance-spend` varsa aynı katalog okumasından beslenir. Statik bakiye
423
+ * adı uygulamanın tüketilen ürünlerinde yoksa `block` (FLOW-045 ile aynı kalıp); tüketilen ürün var ama giriş ucu yoksa MOB-036
424
+ * uyarısı için uygulamanın microservice listesi okunur (okunamazsa susar).
425
+ *
426
+ * FLOW-045 katalog ayağı, MOB-034 ve MOB-042 (kök ADR 0027, plans/mobil-uygulama-ici-satin-alma g-01). Üçü de tanımda
427
+ * olmayan bilgiyi ister — uygulamanın ürün kataloğu (Core `mobile-store/{appId}/products`), ekranın öteki akışları ve
428
+ * sayfanın ağacı — ve yalnız yazılan akışta `purchase` adımı varken okunur. Katalogda olmayan statik anahtar `block`
429
+ * bulgusudur (`findings`; çağıran yazımdan ÖNCE `applyPolicyMode` + `enforcePolicy` ile uygular — CD-005 kalıbı), öteki
430
+ * ikisi uyarıdır. Web uygulamasında adım her zaman `unsupported` döner (barındırıcı satın alma taşımaz): katalog
431
+ * okunmaz, yalnız uyarı. Okunamayan katalog ya da akış listesi bulgu üretmez — bilmediğimizi ret diye söylemeyiz.
432
+ */
433
+ async function purchaseChecks(platform, host, project, environment, applicationId, hostId, hostRecord, flowId, steps, errorSteps) {
434
+ const written = [...(Array.isArray(steps) ? steps : []), ...(Array.isArray(errorSteps) ? errorSteps : [])].filter(isRecord);
435
+ const purchases = written.filter(step => String(step.type ?? '') === 'purchase');
436
+ const spends = written.filter(step => String(step.type ?? '') === 'balance-spend');
437
+ if (purchases.length === 0 && spends.length === 0)
438
+ return { findings: [], warnings: [] };
439
+ const resolved = await resolveProfile(platform, applicationId);
440
+ if (resolved.known && resolved.profile.platform !== 'mobile') {
441
+ return {
442
+ findings: [],
443
+ warnings: [
444
+ ...(purchases.length > 0 ? [`FLOW-045: '${String(purchases[0]?.stepId ?? purchases[0]?.name ?? '?')}' purchase adımı bu web uygulamasında her zaman 'unsupported' döner — uygulama içi satın alma yalnız mobil uygulamanın mağaza derlemesindedir (önizlemede deneme penceresi).`] : []),
445
+ ...(spends.length > 0 ? [`FLOW-046: '${String(spends[0]?.stepId ?? spends[0]?.name ?? '?')}' balance-spend adımı bu web uygulamasında her zaman 'unsupported' döner — bakiye harcama yalnız mobil uygulamanın mağaza derlemesindedir (önizlemede deneme penceresi).`] : [])
446
+ ]
447
+ };
448
+ }
449
+ const products = await platform.org('GET', `/mobile-store/${applicationId}/products`)
450
+ .then(payload => {
451
+ const data = isRecord(payload) && 'data' in payload ? payload.data : payload;
452
+ return Array.isArray(data) ? data.filter(isRecord) : undefined;
453
+ }, () => undefined);
454
+ const otherFlows = purchases.length > 0
455
+ ? await listHostFlows(platform, host, project, environment, applicationId, hostId, async () => hostRecord)
456
+ .then(flows => flows.filter(flow => String(flow.id ?? '').toLowerCase() !== flowId.toLowerCase()), () => undefined)
457
+ : undefined;
458
+ const microservices = spends.length > 0 && products
459
+ ? await platform.tenant('GET', project, `/microservices/${applicationId}`, { environment, query: { pageLength: 200 } })
460
+ .then(payload => {
461
+ const data = isRecord(payload) && 'data' in payload ? payload.data : payload;
462
+ const items = isRecord(data) && Array.isArray(data.items) ? data.items : data;
463
+ return Array.isArray(items) ? items.filter(isRecord) : undefined;
464
+ }, () => undefined)
465
+ : undefined;
466
+ return {
467
+ findings: products ? [...purchaseCatalogFindings(purchases, products), ...balanceCatalogFindings(spends, products)] : [],
468
+ warnings: [
469
+ ...(otherFlows ? restorePathWarnings(written, otherFlows, products) : []),
470
+ ...(products && purchases.length > 0 && host === 'page' ? subscriptionDisclosureWarnings(written, products, hostNodes(hostRecord), otherFlows ?? []) : []),
471
+ ...(products && spends.length > 0 ? balanceWithoutSignInWarnings(products, microservices) : [])
472
+ ]
473
+ };
474
+ }
402
475
  /**
403
476
  * FLOW-038 (g-02, ADR 0004 — ZM-07). `microservice.ts` `.finally` üç yoldan birini dener: adımın
404
477
  * kendi `error` dalı bağlıysa oradan sürer, yoksa flow `errorSteps`'i (`engine.onError`) devralır,
@@ -473,16 +546,26 @@ export function registerFlowTools(server, platform) {
473
546
  server.registerTool('flow_get', {
474
547
  title: 'Akış getir',
475
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.",
476
- 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
+ },
477
557
  annotations: readOnly
478
558
  }, handle(config, async ({ project, environment, host, applicationId, hostId, flowId }) => {
479
- const response = await platform.tenant('GET', project, flowPath(host, applicationId, hostId, flowId), { environment });
480
- 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);
481
563
  const warnings = targetlessSetterWarnings(response);
482
564
  if (!response || typeof response !== 'object' || Array.isArray(response))
483
565
  return response;
484
566
  return {
485
567
  ...response,
568
+ ...(resolved === undefined ? {} : { hostResolvedAs: resolved.host }),
486
569
  ...(record === undefined
487
570
  ? { bindingsNote: BINDINGS_UNAVAILABLE }
488
571
  : { bindings: flowBindings(hostNodes(record), record.events).get(flowId.toLowerCase()) ?? [] }),
@@ -534,6 +617,8 @@ export function registerFlowTools(server, platform) {
534
617
  where: `akış '${name}'`
535
618
  }, policyOverride);
536
619
  const readService = serviceDefinitionReader(platform, project, environment, applicationId);
620
+ const purchase = await purchaseChecks(platform, host, project, environment, applicationId, hostId, hostRecord, writtenFlowId, steps, errorSteps ?? []);
621
+ const purchaseFindings = enforcePolicy(config.policyMode === 'off' ? [] : applyPolicyMode(config, purchase.findings, policyOverride));
537
622
  const parameterWarnings = [
538
623
  ...await requiredParameterWarnings(platform, project, environment, applicationId, steps),
539
624
  ...await functionWarnings(platform, host, project, environment, applicationId, hostId, steps),
@@ -541,7 +626,8 @@ export function registerFlowTools(server, platform) {
541
626
  ...unknownSetterTargetWarnings(steps, errorSteps ?? [], hostNodes(hostRecord)),
542
627
  ...microserviceAlwaysDataWarnings(steps, errorSteps ?? []),
543
628
  ...await queuedBranchWarnings(platform, applicationId, steps, errorSteps ?? [], readService),
544
- ...await uncachedReadWarnings(platform, applicationId, steps, errorSteps ?? [], readService)
629
+ ...await uncachedReadWarnings(platform, applicationId, steps, errorSteps ?? [], readService),
630
+ ...purchase.warnings
545
631
  ];
546
632
  // Gövde yalnız akıştır (designer-partial-update K1): barındırıcı zarfı platformda hâlâ isteğe bağlı
547
633
  // ama gönderilirse okunan tam kaydı yeniden yazar, açık Studio designer'ının kayıtlı tabanını
@@ -569,7 +655,7 @@ export function registerFlowTools(server, platform) {
569
655
  ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
570
656
  ...(platformWarnings.length > 0 ? { platformWarnings } : {}),
571
657
  hint: "Akışı bir olaya bağlamak için: page_schema_patch (layout'ta layout_schema_patch, widget'ta component_schema_patch) ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]; olayda zaten akış bağlıysa ikinciyi add_event ile ekleyin (set_event bağlı akışı değiştirir, PAGE-034). Olay adları komponente özeldir ve 'on' ÖNEKİ ALMAZ (button → 'click', form → 'submit', table → 'mount'/'select'/'sort'…); uydurma ad 200 döner ama bağ sessizce ölü kalır — gateway PAGE-008 ile yakalar."
572
- }, normalized), drops.renamed), drops), [...findings, ...textFindings]);
658
+ }, normalized), drops.renamed), drops), [...findings, ...textFindings, ...purchaseFindings]);
573
659
  }));
574
660
  server.registerTool('flow_update', {
575
661
  title: 'Akış güncelle',
@@ -624,6 +710,9 @@ export function registerFlowTools(server, platform) {
624
710
  }, policyOverride)
625
711
  : [];
626
712
  const readService = serviceDefinitionReader(platform, project, environment, applicationId);
713
+ const purchase = await purchaseChecks(platform, host, project, environment, applicationId, hostId, hostRecord, flowId, flow.steps, flow.errorSteps);
714
+ // Kayıtlı gövde suçlanmaz: yalnız ad/slug değişen yazımda katalog bulgusu uygulanmaz (FLOW-* kuralları gibi).
715
+ const purchaseFindings = enforcePolicy(config.policyMode === 'off' || !(steps || errorSteps) ? [] : applyPolicyMode(config, purchase.findings, policyOverride));
627
716
  const parameterWarnings = [
628
717
  ...await requiredParameterWarnings(platform, project, environment, applicationId, flow.steps),
629
718
  ...await functionWarnings(platform, host, project, environment, applicationId, hostId, flow.steps),
@@ -631,7 +720,8 @@ export function registerFlowTools(server, platform) {
631
720
  ...unknownSetterTargetWarnings(flow.steps, flow.errorSteps, hostNodes(hostRecord)),
632
721
  ...microserviceAlwaysDataWarnings(flow.steps, flow.errorSteps),
633
722
  ...await queuedBranchWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService),
634
- ...await uncachedReadWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService)
723
+ ...await uncachedReadWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService),
724
+ ...purchase.warnings
635
725
  ];
636
726
  await platform.tenant('PUT', project, flowPath(host, applicationId, hostId, flowId), { environment, body: { flow } });
637
727
  // Kısmi güncellemede yalnız GÖNDERİLEN alanlar karşılaştırılır; verilmeyen alan için
@@ -644,7 +734,7 @@ export function registerFlowTools(server, platform) {
644
734
  });
645
735
  const platformWarnings = await flowProfileWarnings(platform, applicationId, [...(flow.steps ?? []), ...(flow.errorSteps ?? [])]);
646
736
  return attachFindings(withWriteDiff(withNormalization({ host, hostId, flow: flowSummary(flow), ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
647
- ...(platformWarnings.length > 0 ? { platformWarnings } : {}) }, normalized), drops), [...findings, ...textFindings]);
737
+ ...(platformWarnings.length > 0 ? { platformWarnings } : {}) }, normalized), drops), [...findings, ...textFindings, ...purchaseFindings]);
648
738
  }));
649
739
  server.registerTool('flow_patch', {
650
740
  title: "Akışa patch uygula",
@@ -690,6 +780,8 @@ export function registerFlowTools(server, platform) {
690
780
  }, policyOverride)
691
781
  ];
692
782
  const readService = serviceDefinitionReader(platform, project, environment, applicationId);
783
+ const purchase = await purchaseChecks(platform, host, project, environment, applicationId, hostId, hostRecord, flowId, flow.steps, flow.errorSteps);
784
+ const purchaseFindings = enforcePolicy(config.policyMode === 'off' ? [] : applyPolicyMode(config, purchase.findings, policyOverride));
693
785
  const parameterWarnings = [
694
786
  ...await requiredParameterWarnings(platform, project, environment, applicationId, flow.steps),
695
787
  ...await functionWarnings(platform, host, project, environment, applicationId, hostId, flow.steps),
@@ -697,7 +789,8 @@ export function registerFlowTools(server, platform) {
697
789
  ...unknownSetterTargetWarnings(flow.steps, flow.errorSteps, hostNodes(hostRecord)),
698
790
  ...microserviceAlwaysDataWarnings(flow.steps, flow.errorSteps),
699
791
  ...await queuedBranchWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService),
700
- ...await uncachedReadWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService)
792
+ ...await uncachedReadWarnings(platform, applicationId, flow.steps, flow.errorSteps, readService),
793
+ ...purchase.warnings
701
794
  ];
702
795
  // Gövde değil FARK gönderilir; platform PATCH ucu akışı tek başına yazar. Net diff okunan kayıt ile son hâl arasından hesaplanır.
703
796
  const netOps = fastJsonPatch.compare(current, flow);
@@ -717,7 +810,7 @@ export function registerFlowTools(server, platform) {
717
810
  flow: flowSummary(written.data),
718
811
  ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
719
812
  ...(platformWarnings.length > 0 ? { platformWarnings } : {})
720
- }, [...normalized, ...resolved.notes.map(note => `patch yolu seçiciden indekse çözüldü: ${note}`)]), drops), findings);
813
+ }, [...normalized, ...resolved.notes.map(note => `patch yolu seçiciden indekse çözüldü: ${note}`)]), drops), [...findings, ...purchaseFindings]);
721
814
  }));
722
815
  server.registerTool('flow_delete', {
723
816
  title: 'Akış sil',
@@ -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 = [];