@appaflytech/zeyrek-ai-mcp 0.0.31 → 0.0.33

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 (91) hide show
  1. package/README.md +5 -5
  2. package/dist/config.js +0 -26
  3. package/dist/knowledge/backendConstraints.js +1 -18
  4. package/dist/knowledge/componentCatalog.js +27 -493
  5. package/dist/knowledge/componentEvents.js +2 -120
  6. package/dist/knowledge/connectDataDefinition.js +4 -4
  7. package/dist/knowledge/connectDataRules.js +1 -1
  8. package/dist/knowledge/mobileComponentCatalog.js +13 -8
  9. package/dist/knowledge/mobileStore.js +227 -0
  10. package/dist/knowledge/normalize.js +48 -131
  11. package/dist/knowledge/outlines.js +0 -36
  12. package/dist/knowledge/platform/lint.js +1 -1
  13. package/dist/knowledge/platform/nativeBoxModel.js +1 -1
  14. package/dist/knowledge/policy.js +4 -18
  15. package/dist/knowledge/process.js +1 -1
  16. package/dist/knowledge/reservedTables.js +0 -79
  17. package/dist/knowledge/rules.js +10 -83
  18. package/dist/knowledge/schemaGuard.js +0 -11
  19. package/dist/observability/trace.js +2 -5
  20. package/dist/platform/capture/capture.js +1 -25
  21. package/dist/platform/errors.js +10 -162
  22. package/dist/platform/http.js +1 -6
  23. package/dist/platform/responses.js +4 -34
  24. package/dist/platform/schemaReadMemory.js +2 -2
  25. package/dist/platform/secrets.js +1 -8
  26. package/dist/resources/examples/middleware-authenticate.json +1 -1
  27. package/dist/resources/examples/mobile-app-shell-drawer.json +62 -61
  28. package/dist/resources/examples/mobile-list-detail-form.json +51 -20
  29. package/dist/resources/examples/page-detail-profile.json +1 -2
  30. package/dist/resources/examples/page-dynamic-form.json +2 -3
  31. package/dist/resources/examples/page-html-embed.json +1 -1
  32. package/dist/resources/examples/workflow-basic.json +2 -8
  33. package/dist/resources/guides/access.md +4 -18
  34. package/dist/resources/guides/bpm.md +1 -6
  35. package/dist/resources/guides/component.md +4 -83
  36. package/dist/resources/guides/entity.md +8 -50
  37. package/dist/resources/guides/getting-started.md +1 -38
  38. package/dist/resources/guides/merge.md +3 -37
  39. package/dist/resources/guides/microservice.md +0 -78
  40. package/dist/resources/guides/middleware.md +3 -35
  41. package/dist/resources/guides/mobile.md +85 -2
  42. package/dist/resources/guides/page.md +26 -278
  43. package/dist/resources/guides/workflow.md +0 -51
  44. package/dist/resources/rules.js +5 -11
  45. package/dist/resources/schema-fingerprint.json +5 -7
  46. package/dist/resources/schemas/CreateEntityCommand.json +0 -118
  47. package/dist/resources/schemas/CreateMicroserviceCommand.json +1 -13
  48. package/dist/resources/schemas/CreateQueryCommand.json +0 -4
  49. package/dist/resources/schemas/CreateWorkflowCommand.json +1 -146
  50. package/dist/resources/schemas/DataJobDto.json +1 -6
  51. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +2 -20
  52. package/dist/resources/schemas/MicroserviceDto.json +1 -13
  53. package/dist/resources/schemas/PreviewMicroserviceCommand.json +1 -10
  54. package/dist/resources/schemas/PreviewWorkflowCommand.json +1 -25
  55. package/dist/resources/schemas/StartMobileBuildCommand.json +54 -0
  56. package/dist/resources/schemas/StepDto.json +1 -25
  57. package/dist/resources/schemas/UpdateEntityCommand.json +0 -118
  58. package/dist/resources/schemas/UpdateMicroserviceCommand.json +1 -13
  59. package/dist/resources/schemas/UpdateQueryCommand.json +0 -4
  60. package/dist/resources/schemas/UpdateViewCommand.json +2 -0
  61. package/dist/resources/schemas/UpdateWorkflowCommand.json +1 -146
  62. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +259 -0
  63. package/dist/server.js +5 -25
  64. package/dist/tools/access.js +2 -369
  65. package/dist/tools/common.js +0 -7
  66. package/dist/tools/components.js +2 -15
  67. package/dist/tools/datasources.js +2 -2
  68. package/dist/tools/dependencies.js +1 -1
  69. package/dist/tools/deployments.js +3 -38
  70. package/dist/tools/designSchemaPatch.js +3 -13
  71. package/dist/tools/dtos.js +5 -36
  72. package/dist/tools/entities.js +12 -79
  73. package/dist/tools/externalComponents.js +1 -1
  74. package/dist/tools/flows.js +10 -45
  75. package/dist/tools/folders.js +1 -1
  76. package/dist/tools/imports.js +2 -5
  77. package/dist/tools/layouts.js +5 -6
  78. package/dist/tools/merges.js +2 -2
  79. package/dist/tools/microservices.js +20 -50
  80. package/dist/tools/mobileStore.js +268 -0
  81. package/dist/tools/pages.js +35 -89
  82. package/dist/tools/platformInfo.js +6 -20
  83. package/dist/tools/projects.js +9 -71
  84. package/dist/tools/rows.js +12 -62
  85. package/dist/tools/schemaOps.js +1 -29
  86. package/dist/tools/screenWrite.js +1 -22
  87. package/dist/tools/screens.js +3 -6
  88. package/dist/tools/templates.js +1 -26
  89. package/dist/tools/translations.js +1 -1
  90. package/dist/tools/workflows.js +5 -10
  91. package/package.json +2 -2
@@ -19,16 +19,7 @@ export const componentEventContracts = {
19
19
  rowChange: { payload: {}, reads: 'self.selectedRow (düzenlenen satır, YENİ değeriyle) — satır nesnesi self.rows içinde de yerinde güncellenir; değer gerçekten değişmediyse olay çalışmaz' }
20
20
  },
21
21
  datatable: {
22
- mount: { payload: {}, reads: 'self.filters · self.sortField · self.sortType · self.offset · self.limit' },
23
- save: {
24
- payload: {},
25
- reads: "self.changeset = { inserted:[{ rowId, values }], updated:[{ rowId, id, patch }], deleted:[{ rowId, id }] } (komponent olaydan ÖNCE yazar). Bağlı tabloda (rows connectData) yazmayı runtime bağlantısı yapar, akış ardından koşar; hatalar self.rowErrors [{ rowId, field?, message }]. İşlemin bağlamı da olaydan ÖNCE yazılır (2026-09-21): self.operation (rowAdd|rowEdit|cellEdit|rowDelete|bulkEdit|bulkDelete|batchSave), self.editedRows (tam satırlar), self.deletedRows (silinen tam satırlar), self.editedCell ({ rowId, field, value, previousValue }, yalnız cellEdit), self.bulkPatch (yalnız bulkEdit). Her işlem için ayrı olay yoktur; tek save akışı self.operation ile ayırır"
26
- },
27
- discard: { payload: {}, reads: 'self.discardedRowIds (vazgeçilen taslak satır kimlikleri)' },
28
- rowClick: { payload: {}, reads: 'self.selectedRow (tıklanan satırın sunucu satırı; kaydedilmemiş yeni satırda taslak değerleri). Veri hücresine tıklamada yayılır — onay kutusu, ✏/🗑 ve aksiyon düğmeleri tetiklemez; çift tık önce iki rowClick üretir' },
29
- rowDoubleClick: { payload: {}, reads: 'self.selectedRow (çift tıklanan satır). Düzenleme kipi açıksa satır/hücre ayrıca düzenlemeye açılır; editMode none tabloda detay sayfasına gitmek gibi akışlar için' },
30
- cellSelectionChange: { payload: {}, reads: "self.selectedCells = { column, rowIds[], anchorRowId } (aynı kolonda Shift aralık, Ctrl/Alt tekil; yalnız düzenlenebilen hücreler)" },
31
- rowSelectionChange: { payload: {}, reads: 'self.selectedRows (seçili satırların sunucu satırları; onay kutusu kolonu yalnız bulkActions açık ve editMode none değilken vardır)' }
22
+ mount: { payload: {}, reads: 'self.filters · self.sortField · self.sortType · self.offset · self.limit' }
32
23
  },
33
24
  button: { mount: NO_DATA, click: NO_DATA },
34
25
  card: {
@@ -114,7 +105,7 @@ export const componentEventContracts = {
114
105
  comments: {
115
106
  mount: NO_DATA,
116
107
  submit: {
117
- payload: { text: 'string', parentId: "string (yanıtsa üst yorumun id’si, kök yorumda '')", mentions: 'string[] (metinden çözülmüş @kişi listesi)', author: 'string (currentUser)', process: "{ scope: 'task' | 'instance', targetId, visibility, body (@Ad → @<id>), mentionUserIds } | null (serbest yorumda null)" },
108
+ payload: { text: 'string', parentId: "string (yanıtsa üst yorumun id’si, kök yorumda '')", mentions: 'string[] (metinden çözülmüş @kişi listesi)', author: 'string (currentUser)' },
118
109
  reads: 'Komponent yorumu ANINDA iyimser olarak listeye ekler (bekleyen katman); kalıcılık akışın işidir — rows tazelenmezse bekleyen satır bir sonraki içerik değişiminde kaybolur'
119
110
  },
120
111
  like: { payload: { id: 'string', liked: 'boolean (yeni durum)', author: 'string (currentUser — işlemi yapan)' }, reads: 'beğeni de iyimserdir; sayaç rows tazelenene kadar yerel. isDisabled/isReadonly/isViewmode açıkken yayılmaz' },
@@ -370,7 +361,6 @@ export const componentEventContracts = {
370
361
  sidebar: { mount: NO_DATA },
371
362
  html: { mount: NO_DATA },
372
363
  view: { mount: NO_DATA },
373
- 'datatable-cell': { mount: NO_DATA },
374
364
  'widget-area': { mount: NO_DATA },
375
365
  'document-editor': { mount: NO_DATA },
376
366
  video: {
@@ -725,114 +715,6 @@ export const componentEventContracts = {
725
715
  payload: { currentStep: 'number (engellenen adım)', previous: 'number (aynı değer)' },
726
716
  reads: 'YALNIZ İleri düğmesinden yayılır; engellenen başlık tıklaması sessiz kalır'
727
717
  }
728
- },
729
- 'task-inbox': {
730
- mount: NO_DATA,
731
- tabChange: {
732
- payload: { tab: "string (mine | group | delegated | started | done | planned)", scope: 'string (app | realm)' },
733
- reads: "setter: activeTab self'e yazılır; tab ve scope event.data ile gelir"
734
- },
735
- taskClick: { payload: { taskId: 'string', row: 'tıklanan satırın TAMAMI' }, reads: "self'e yazılmaz — event.data ile oku" },
736
- claim: { payload: { taskId: 'string', version: 'string (optimistic sürüm)', row: 'satırın TAMAMI' }, reads: "self'e yazılmaz — event.data ile oku; process.task.claim { id: taskId, version }" },
737
- bulkClaim: { payload: { taskIds: 'string[]', rows: 'seçili ve üstlenilebilir satırlar' }, reads: "self'e yazılmaz — event.data ile oku" },
738
- search: { payload: { q: 'string (kırpılmış, boş gönderilmez)' }, reads: "self'e yazılmaz — event.data ile oku; rows ← process.task.list { tab, q } sonucu; appliedFilter sistem ucunda yok — rozet gerekiyorsa Page.bpm.tasks(tab, { q }).appliedFilter" },
739
- clearFilter: { payload: {}, reads: "self'e yazılmaz; appliedFilter ← null ve liste q'suz yeniden okunur" }
740
- },
741
- 'task-actions': {
742
- mount: NO_DATA,
743
- action: {
744
- payload: { action: "string (approve | reject | complete | request-info | provide-info | reassign | sign | start-assessment | submit-assessment | özel eylem id)", taskId: 'string', version: "string? (start-assessment ve submit-assessment yükünde YOKTUR: sınavı açmak görevi üstlenir ve sürümü ilerletir, panelin çizildiği sürüm bayattır — gönderilse 409 alınır)", comment: 'string?', payload: "object? (sign: payload.signature = { meaning, email?, password }; submit-assessment: payload.answers = { soruId: cevap })", targetUserId: 'string? (reassign)' },
745
- reads: "self'e yazılmaz — event.data ile oku; boş alanlar yükte hiç yoktur"
746
- },
747
- assistantSummary: { payload: { taskId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; assistant ← await Page.bpm.task(taskId).assistant()" },
748
- assistantAsk: { payload: { taskId: 'string', question: 'string (kırpılmış)' }, reads: "self'e yazılmaz — event.data ile oku; assistant ← await Page.bpm.task(taskId).assistant(question)" }
749
- },
750
- 'process-timeline': {
751
- mount: NO_DATA,
752
- clickItem: { payload: { currentItem: 'tıklanan satır ({ key, kind, nodeId, taskId, title, actor, decision, round, isSuperseded })', index: 'number' }, reads: "self'e yazılmaz — event.data ile oku" },
753
- exportLedger: { payload: {}, reads: 'yük yok — akış Page.bpm.instance(instanceId).exportLedger() ile CSV metnini alır' },
754
- verifyLedger: { payload: {}, reads: "yük yok — akış process.instance.ledger.verify { id: instanceId } sonucunu (isValid, brokenAtSequence) ledgerStatus/brokenAtSequence'e yazar" }
755
- },
756
- 'process-monitor': {
757
- mount: NO_DATA,
758
- filter: {
759
- payload: { filter: 'object (güncel süzgeç: definitionId, state, unitId, priority, slaBreached …)', query: 'object (process.instance.list için yalnız sunucu alanları)' },
760
- reads: "event.data ile oku; filter prop'u komponent içinde de güncellenir"
761
- },
762
- clickItem: { payload: { currentItem: 'tıklanan satırın TAMAMI (currentNodes, ageHours, slaStatus dahil)', index: 'number' }, reads: "self'e yazılmaz — event.data ile oku" },
763
- instanceAction: {
764
- payload: { operation: 'string (retry | skip | assign)', instanceId: 'string', nodeId: 'string? (ilk bekleyen adım)', taskIds: 'string[] (sahipsiz görevler)', reason: 'string?', targetUserId: 'string? (assign)', dueAt: 'string?', targetNodeId: 'string?' },
765
- reads: "self'e yazılmaz — event.data ile oku; retry → process.instance.retry { id, reason }, skip → process.instance.skip { id, nodeId, reason }, assign → process.batch.execute { kind: 'reassign', taskIds, targetUserId, reason }"
766
- },
767
- bulkAction: {
768
- payload: { operation: 'string (reassign | extend-due | suspend | jump | cancel)', instanceIds: 'string[] (uygun durumdaki seçili örnekler)', taskIds: 'string[] (yalnız reassign/extend-due)', fromNodeIds: 'object? (yalnız jump: { instanceId: bekleyen ilk adım })', reason: 'string?', targetUserId: 'string?', dueAt: 'string? (UTC ISO; yerel girişten çevrilir)', targetNodeId: 'string?' },
769
- reads: "self'e yazılmaz — event.data ile oku; boş alanlar yükte hiç yoktur. reassign/extend-due → process.batch.execute { kind: operation, taskIds, … }; diğerleri örnek başına process.instance.<operation> { id, … } (jump: { fromNodeId: fromNodeIds[id], targetNodeId, reason }) — akış grafiğinde condition → microservice → code döngüsüyle"
770
- },
771
- undo: { payload: { batchOperationId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.batch.undo { id: batchOperationId }" }
772
- },
773
- 'process-variants': {
774
- mount: NO_DATA,
775
- select: {
776
- payload: { value: "string (düğüm kimlikleri '>' ile birleşik)", path: 'string[]', instanceCount: 'number', sampleInstanceIds: 'string[] (en çok beş örnek)' },
777
- reads: "value self'e yazılır (setter); path ve örnekler event.data ile okunur"
778
- },
779
- clickInstance: { payload: { instanceId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.instance.get { id: instanceId }" },
780
- download: { payload: { format: 'string (xes | ocel)' }, reads: "self'e yazılmaz — event.data ile oku; Page.bpm.exportLog(slug, format, { from, to })" }
781
- },
782
- 'read-proof-matrix': {
783
- mount: NO_DATA,
784
- remind: {
785
- payload: { revision: 'string', instanceId: 'string?', userIds: 'string[] (bekleyen okuyucular)' },
786
- reads: "self'e yazılmaz — event.data ile oku; okumayanlara bildirim gönderen akışı çağır"
787
- }
788
- },
789
- 'case-view': {
790
- mount: NO_DATA,
791
- action: {
792
- payload: { action: 'string (discretionary | start | complete | disable | enable)', itemId: 'string', planItemId: "string ('' — discretionary)" },
793
- reads: "self'e yazılmaz — event.data ile oku; Page.bpm.cases.act(caseInstanceId, event.data), sonra view'i yeniden setter yaz"
794
- },
795
- clickTask: { payload: { taskId: 'string', planItemId: 'string', itemId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.task.get { id: taskId }" }
796
- },
797
- 'delegation-editor': {
798
- mount: NO_DATA,
799
- save: {
800
- payload: { fromUserId: 'string? (yalnız başkası adına)', toUserId: 'string', startsAt: 'string (ISO)', endsAt: 'string? (ISO)', processSlugs: 'string[] (boş = tüm süreçler)', reason: 'string?' },
801
- reads: "self'e yazılmaz — event.data ile oku; process.delegation.create (event.data alanları). Yalnız istemci doğrulaması geçince yayılır"
802
- },
803
- end: { payload: { delegationId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.delegation.end { id: delegationId }" }
804
- },
805
- 'handover-wizard': {
806
- mount: NO_DATA,
807
- stepChange: { payload: { step: 'string (tasks | ownership | confirm)', index: 'number' }, reads: "self'e yazılmaz — event.data ile oku" },
808
- apply: {
809
- payload: { userId: 'string', decisions: '[{ kind, key, target, targetUserId? }]', force: 'boolean', deactivate: 'boolean' },
810
- reads: "self'e yazılmaz — event.data ile oku; process.handover.apply { userId, decisions, force }"
811
- }
812
- },
813
- 'notification-center': {
814
- mount: NO_DATA,
815
- open: { payload: { notificationId: 'string', link: 'string?', taskId: 'string?', instanceId: 'string?', markRead: 'boolean (okunmamış satırda true)' }, reads: "self'e yazılmaz — event.data ile oku" },
816
- read: { payload: { notificationId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.notification.read { id: notificationId }" },
817
- ack: { payload: { notificationId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.notification.ack { id: notificationId }" },
818
- markAllRead: { payload: { notificationIds: 'string[] (okunmamışlar)' }, reads: "self'e yazılmaz — event.data ile oku; her kimlik için read()" },
819
- savePreference: {
820
- payload: { mode: 'string (immediate | digest | sla-only)', digestAt: "string ('HH:mm:00')", quietFrom: "string | null", quietTo: "string | null", timeZone: 'string' },
821
- reads: "self'e yazılmaz — event.data ile oku; process.notification-preference.update (event.data alanları)"
822
- }
823
- },
824
- 'approval-chain-editor': {
825
- mount: NO_DATA,
826
- save: {
827
- payload: { definitionId: 'string', baseVersion: 'number (aktif sürüm)', overrideRevision: 'number (okunan revizyon; uyarlama yoksa 0)', patch: 'JsonPatchOperation[] (aktif sürüme karşı tam uyarlama, seçicili yollar)' },
828
- reads: "self'e yazılmaz — event.data ile oku; process.override.save { id: definitionId, baseVersion, overrideRevision, patch }. Yalnız istemci doğrulaması geçince yayılır"
829
- },
830
- reset: { payload: { definitionId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.override.reset { id: definitionId }" },
831
- migrateOpen: { payload: { definitionId: 'string' }, reads: "self'e yazılmaz — event.data ile oku; process.override.migrate-open { id: definitionId }" },
832
- simulate: {
833
- payload: { definitionId: 'string', subjectRowId: 'string?', overridePatch: 'JsonPatchOperation[] (kaydedilmemiş uyarlama)' },
834
- reads: "self'e yazılmaz — event.data ile oku; process.override.simulate { id: definitionId, subjectRowId, overridePatch } sonucunu simulation prop'una yaz"
835
- }
836
718
  }
837
719
  };
838
720
  export const eventPayloadNote = "Olay yükü motorda komponentin KENDİ prop'una yazılır (FlowMachine.onEmit → setNode) ve akışta " +
@@ -176,17 +176,17 @@ export function connectDataShapeIssues(ref, path = 'ref') {
176
176
  ...(definition.filter && !filtersOnServer ? ['filter'] : []),
177
177
  ...(definition.sort?.length && !filtersOnServer ? ['sort'] : []),
178
178
  ...(behavior?.filterable?.length && !filtersOnServer ? ['behavior.filterable'] : []),
179
- ...(behavior?.searchable?.length && !filtersOnServer ? ['behavior.searchable'] : []),
179
+ ...(behavior?.searchable?.length ? ['behavior.searchable'] : []),
180
180
  ...(behavior?.edit?.create || behavior?.edit?.update || behavior?.edit?.delete ? ['behavior.edit'] : []),
181
181
  ...(definition.page?.mode === 'cursor' ? ['page.mode'] : []),
182
182
  ...definition.fields.filter(field => field.expand?.length).map(field => `fields.${field.slug}.expand`),
183
183
  ...definition.fields.filter(field => field.lookup).map(field => `fields.${field.slug}.lookup`)
184
184
  ];
185
185
  for (const feature of unsupported) {
186
- const searchHint = feature === 'behavior.searchable' && kind === 'view'
187
- ? ' Arama view kaynağında searchable ile değil, istekteki search ile sunucuda çalışır (describe source.textSearch).'
186
+ const searchHint = feature === 'behavior.searchable' && (kind === 'view' || kind === 'microservice')
187
+ ? ' Arama view (ve view kaynaklı microservice) kaynağında searchable ile değil, istekteki search ile sunucuda çalışır (describe source.textSearch).'
188
188
  : '';
189
- issues.push({ code: 'CD-013', rule: 'connect-data-source-feature-unsupported', path: `${path}.${feature}`, message: `${kind} kaynağında '${feature}' yok: bu kaynak tanımda ${filtersOnServer ? 'yazma, expand ve lookup' : 'filtre, sıralama, searchable, yazma, expand ve lookup'} taşımaz${feature === 'page.mode' ? '; cursor sayfalama yalnız entity kaynağında' : ''}.${searchHint}` });
189
+ issues.push({ code: 'CD-013', rule: 'connect-data-source-feature-unsupported', path: `${path}.${feature}`, message: `${kind} kaynağında '${feature}' yok: bu kaynak tanımda ${filtersOnServer ? 'searchable, yazma, expand ve lookup' : 'filtre, sıralama, searchable, yazma, expand ve lookup'} taşımaz${feature === 'page.mode' ? '; cursor sayfalama yalnız entity kaynağında' : ''}.${searchHint}` });
190
190
  }
191
191
  }
192
192
  if (Object.keys(definition.params ?? {}).length && (kind === 'entity' || kind === 'view')) {
@@ -86,7 +86,7 @@ export const connectDataRuleCatalog = [
86
86
  {
87
87
  id: 'CD-013',
88
88
  severity: 'block',
89
- rule: "query, view ve workflow kaynağında filter, sort, behavior.filterable/searchable, açık edit, expand ve lookup yok; microservice kaynağında edit, expand ve lookup yok (filtre/sıralama/arama uç destekliyorsa describe canFilter/sortable ile açılır; arama yalnız içerir işleçli korumasız metin alanında — dinamik filtreye OR içerir koşulu olarak iner, 2026-10-02). page.mode 'cursor' yalnız entity kaynağında (b-35 A).",
89
+ rule: "query, view ve workflow kaynağında filter, sort, behavior.filterable/searchable, açık edit, expand ve lookup yok; microservice kaynağında searchable, edit, expand ve lookup yok (filtre/sıralama uç destekliyorsa describe canFilter/sortable ile açılır). page.mode 'cursor' yalnız entity kaynağında (b-35 A).",
90
90
  enforcedBy: 'set_connect_data op gövdesi ve connect_data_bind — connectDataShapeIssues; connect_data_bind params adları describe source.parameters\'a karşı (buildDefinition)',
91
91
  source: 'b-31/b-32 ConnectDataDefinitionValidator connect-data-source-feature-unsupported (422); f-33 ConnectDataService.ValidateSourcesAsync ref.params.<ad>'
92
92
  },
@@ -27,7 +27,7 @@ export const MOBILE_STATE_FLAG_NOTES = {
27
27
  * sabit katmanlar, alt ağaçları hizaya girmez. İçerik genişlikli: genişliği içeriğinden gelen
28
28
  * yapraklar, sağ kenar sapması sayılmaz.
29
29
  */
30
- export const MOBILE_DROPPABLE_TYPES = ['carousel', 'collapsible-container', 'container', 'drawer', 'floating-container', 'form', 'grouped-list', 'modal', 'repeater', 'safe-area', 'scroll-view', 'stack', 'tabs', 'tooltip', 'widget-area'];
30
+ export const MOBILE_DROPPABLE_TYPES = ['carousel', 'collapsible-container', 'container', 'drawer', 'floating-container', 'form', 'grouped-list', 'modal', 'repeater', 'safe-area', 'scroll-view', 'stack', 'tabs', 'tooltip', 'view', 'widget-area'];
31
31
  export const MOBILE_OUT_OF_FLOW_TYPES = ['app-bar', 'drawer', 'fab', 'floating-container', 'modal', 'toast'];
32
32
  export const MOBILE_INTRINSIC_WIDTH_TYPES = ['avatar', 'badge', 'icon', 'rating', 'text', 'timer'];
33
33
  /** Yalnız davranış ekleyen kap: çizimi çocuğundan ibarettir, kendi boşluğu içeriği içeri iter (mizanpaj S6). */
@@ -51,6 +51,13 @@ export const mobileComponentKnowledge = {
51
51
  'Özel sekme çubuğu (Ayarlar › Sekmeler › Özel widget, K14) bu kökü taşıyan mobil widget`tır: runtime onu `Widget.tabs = { items: [{ page, slug, label, icon, active }], activePage }` parametresiyle çizer, kök kimliği `__tabs`.'
52
52
  ]
53
53
  },
54
+ view: {
55
+ summary: 'Sabit görünümlü bileşenin görünüm düğümü (liste: satır şablonu, boş görünüm; ADR docs/adr/0005). Paletten eklenmez; sahibi doğurur.',
56
+ differences: [
57
+ 'Çizim `container`\'a devredilir: yerleşim prop\'u taşımaz, çocukları SÜTUNDA ve tam genişlikte akar (mobil `container` yön prop\'u yoksa satır dizer; görünüm dizmez).',
58
+ '`style` düşer. Sahibi hangi görünümü çizeceğine kendisi karar verir: liste satır varken ilkini her satırda, yokken ikincisini bir kez çizer.'
59
+ ]
60
+ },
54
61
  'safe-area': {
55
62
  summary: 'Güvenli alan kabı: içeriği durum çubuğu, çentik ve ev göstergesinden uzak tutar (D15).',
56
63
  differences: [
@@ -248,10 +255,10 @@ export const mobileComponentKnowledge = {
248
255
  ]
249
256
  },
250
257
  repeater: {
251
- summary: 'Şablon düğümlerini satır başına tekrarlar; satır kimlikleri `__row-<i>`.',
258
+ summary: 'İki görünümlü liste (YALNIZ mobil; web listesi tek şablonlu): children = [view (satır şablonu, satır başına tekrarlanır), view (boş görünüm, satır yokken bir kez)]; satır kimlikleri `__row-<i>`.',
252
259
  differences: [
253
260
  '`FlatList` v1`de KULLANILMAZ: sanallaştırma yalnız gerçek veriyle anlamlı, tasarım kipinde ölçüm haritasını bozar (kaplama görünmeyen satırın kutusunu bulamaz).',
254
- 'Tasarım kipinde şablon BİR kez çizilir. Runtime`da her satır web ile AYNI makine parametresiyle (`index`, `row`, `repeaterId`) çizilir: bağlı prop`lar `Row` / `RowIndex` / `item` okur. Satır yoksa (`rows` boş dizi ya da `null`) `emptyTitle`/`emptyDescription` doluysa `emptystate` çizilir; ikisi de boşsa hiçbir şey çizilmez (web ile aynı).',
261
+ 'Tasarım kipinde şablon BİR kez çizilir. Runtime`da her satır web ile AYNI makine parametresiyle (`index`, `row`, `repeaterId`) çizilir: bağlı prop`lar `Row` / `RowIndex` / `item` okur. Satır yoksa (`rows` boş dizi ya da `null`) İKİNCİ görünüm (boş görünüm) bir kez, tam genişlikte ve sayfa bağlamıyla çizilir (ADR docs/adr/0005, 2026-09-30; web listesi boşken hiçbir şey çizmez). Boş durumu oraya serbestçe tasarla (emptystate, düğme, görsel); `emptyTitle`/`emptyDescription`/`emptyIcon` prop\'ları ve kardeş `emptystate` yolu KALDIRILDI. Görünümün çocukları dikey akar (`view` çizimi container\'a sütun olarak devredilir).',
255
262
  'Satırdaki bir düğümün olayı ÖNCE tekrarlayıcıya `change { selectedIndex }` yazar (aynı satırsa yazmaz), sonra kendi akışını koşar (web sırası), çocuğun `click`inden sonra da `select` yayar. Akışta tıklanan satırın iki yolu var: `<tekrarlayıcı>.selectedItem` / `.selectedIndex` HER runtime sürümünde çalışır (önerilen); `Row` / `RowIndex` / `item` ARC-121 düzeltmesini (2026-09-27) taşıyan runtime`da da satırı verir ve `item`a yazmak YALNIZ o satırı günceller — daha eski runtime`da akış ifadesinde boştu (ölçüldü 2026-09-19).',
256
263
  'Satır bağlamındaki bir akışın setter hedefi satırın şablonu DIŞINDAysa (bir modal, sayfa düzeyi bir metin) yazım SAYFA kapsamında olur; İÇİNDEyse satır bazlı depoya (`_repeaterValues`) yazılır — kapsamı olayın kaynağı değil hedefin AĞAÇTAKİ yeri belirler, `flow-trigger` üzerinden koşan aynı akış için de değişmez (ZM-08, `isElementInRepeaterTemplate`, 2026-09-26).',
257
264
  'İç içe repeater zaten MCP tarafından reddedilir (`PAGE-013`) — başlıklar altında gruplu liste (güne göre görevler) için `grouped-list` kullanın; yoksa bir DIŞ repeater\'ın şablonuna İÇ satırdan yazan bir akışın yazımı dış satırın değil İÇ satırın kimliğiyle anahtarlanırdı — bilinen açık, `PAGE-013` riski düşük tutar.',
@@ -261,7 +268,7 @@ export const mobileComponentKnowledge = {
261
268
  'Çoklu seçim (MOBİLE ÖZGÜ): `selectionMode: multiple` satır başına onay kutusu çizer, dokunuş seçimi değiştirir ve `itemPress`/`change`/`select` YAYILMAZ; `selectionChange { selectedIndices, count }` yayılır. Seçim düğüme yazılır: `{{ <liste>.selectedItems.length }} seçildi`, `<liste>.selectedIndices`. `longPressSelects: true` uzun basışla kipi açar (basılan satır seçili). Kipi kapatmak: setter ile `selectionMode: \'single\'` + `selectedIndices: []` + `selectedItems: []`. Seçim başlığı ayrı bir `app-bar`dır, iki çubuk `isHidden` bağıyla yer değiştirir.',
262
269
  '`swipeActions` (eylemin `icon`u etiketin üstünde glif olarak çizilir) ve `itemPressable` MOBİLE ÖZGÜDÜR (webde yok). Jest RN `Animated` + `PanResponder` ile kurulur — paket v1`de `reanimated`/`gesture-handler` yok — ve yalnız YATAY harekette yakalanır, böylece dikey kaydırma kaba kalır. Tasarım kipinde satırlar şablon olarak çizildiği için jest hiç bağlanmaz.',
263
270
  '`swipe` ve `itemPress` yükleri satırı `row` alanında TAŞIR; akış ifadesindeki `Row`/`RowIndex` bağı ayrı bir konudur (ARC-121).',
264
- 'Düşenler: `autoRowSize` (mobil satır her zaman içeriğinin boyundadır).'
271
+ 'Düşenler: `autoRowSize` (mobil satır her zaman içeriğinin boyundadır), `size` (mobil kart hücresi çocuklarını büyütmez; 2026-09-30).'
265
272
  ],
266
273
  propNotes: {
267
274
  itemPressable: 'Satıra dokunulabilir yapar; dokunuş önce `change { selectedIndex }`, sonra `itemPress` yayar.',
@@ -269,9 +276,7 @@ export const mobileComponentKnowledge = {
269
276
  direction: '`vertical` (varsayılan) · `horizontal` — yatay şerit; `columnCount` orada yok sayılır.',
270
277
  itemWidth: 'Yatay öğenin genişliği (px). Boşsa şablonun kendi genişliği.',
271
278
  snap: '`none` · `item` — kaydırma kart + aralık adımına oturur.',
272
- emptyTitle: 'Satır yokken çizilen boş durumun başlığı (`emptystate` görünümü). Başlık da açıklama da boşsa boş durum çizilmez.',
273
- emptyDescription: 'Boş durumun açıklaması.',
274
- emptyIcon: 'Boş durumun ikonu (`{ name }`, varsayılan `package_2`).',
279
+ views: "Sabit iki görünüm: 'item' (satır şablonu) ve 'empty' (boş görünüm). Değer YAZILMAZ; görünümler children'daki iki view düğümüdür, başka şekil 422 `fixed-views`.",
275
280
  selectionMode: '`single` (varsayılan, bugünkü `selectedIndex`) · `multiple` (onay kutulu çoklu seçim, `selectionChange`) · `none` (seçim izlenmez).',
276
281
  longPressSelects: 'Uzun basış çoklu seçim kipini açar ve basılan satırı seçer.'
277
282
  }
@@ -712,7 +717,7 @@ export const mobileComponentKnowledge = {
712
717
  differences: [
713
718
  'Cihazda ECharts `@wuba/react-native-echarts`in SVG motoruyla (`react-native-svg`) çizilir, kanvas ve önizlemede ECharts`ın kendi tuvaliyle. Tema renkleri ve yazı ailesi temanın `--ng-*` değişkenlerinden; cihazda yazı yığının ilk ailesidir, yüklü değilse platform yazı tipi.',
714
719
  'Düşenler: `style`; `isExportable` ve `exportFileName` (CSV/PNG indirme, tablo görünümü, tam ekran — dosya kaydetmek ayrı native modül ister); `showTypeSwitcher` ve `showRefresh` ile `refresh` olayı (tip akışla `chartType` yazılarak, yenileme sayfanın kaydırarak yenilemesiyle); `isDrillDown` ile `breadcrumbClick` (tıklanan noktanın satırı `dataPointClick { row }` ile yine gelir).',
715
- 'Kutu: kabuktan gelen boy (`heightType: fixed` ölçülü ya da `fill`) ya da panelin `aspectRatio`su, yoksa 240 dp; `caption` web`deki gibi kutunun altında metindir ve grafik alanından pay alır. Durum web`in sırasıyla: yükleniyor (ortada dönen gösterge — web kutuyu boş bırakır, ARC-177), hata (`errorText` + Yeniden dene düğmesi `<elementId>__retry`, `retry` yayar), veri yok (`emptyText`).',
720
+ 'Kutu: kabuktan gelen boy (`heightType: fixed` ölçülü ya da `fill`) ya da panelin `aspectRatio`su, yoksa 240 dp; `caption` web`deki gibi kutunun altında metindir ve grafik alanından pay alır. Durum web`in sırasıyla: yükleniyor (ortada dönen gösterge — web kutuyu boş bırakır, ARC-127), hata (`errorText` + Yeniden dene düğmesi `<elementId>__retry`, `retry` yayar), veri yok (`emptyText`).',
716
721
  'Dokunuş: noktaya dokunmak YALNIZ `dataPointClick` (seri), kategori ekseninin etiketine dokunmak YALNIZ `axisLabelClick` (yatay çubukta etiketler y ekseninde), legend maddesi `legendClick`; hedef çizgisi, bant ve eksen başlığı olay değildir, `selectionChange` yalnız `selectionMode` açıkken gelir (web ile aynı). Dokunulan noktada ipucu açılır; sayfa kaydırılınca açık kalır, grafiğin başka yerine dokununca yer değiştirir. Grafiğin üstünden dikey kaydırma sayfayı kaydırır; 4 px`ten uzun sürükleme tıklama sayılmaz.',
717
722
  'Olay yükü düğüme yazılınca grafik yeniden KURULMAZ: seçim, gizlenen legend serisi ve yakınlaştırma korunur; seçenek girdinin içeriği değişince (bağlı veri yenilenince) yeniden uygulanır ve son legend seçimi korunur (web ile aynı). Bozuk bağlı veri bileşeni düşürmez: çözülemezse `emptyText`, seçenek kurulamazsa önceki çizim kalır. Başlık/alt başlık varken çizim alanı (ızgara; radar ve radyalde merkez) başlığın altına iner; eksen başlığı eksenin ortasına, etiketlerin dışına yazılır ve tuvalde kalır — ikisi web ile aynı. Prop varsayılanları web ile tek kaynaktır.'
718
723
  ]
@@ -0,0 +1,227 @@
1
+ export const PACKAGE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
2
+ export const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]{2,}$/;
3
+ export const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
+ export const LOCALE_PATTERN = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
5
+ export const COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
6
+ export const PERMISSION_KEY_PATTERN = /^[a-z][A-Za-z0-9]*$/;
7
+ export const RESERVED_SCHEME = 'zeyrek';
8
+ export const MAX_BUILD_NUMBER = 2_100_000_000;
9
+ export const DISPLAY_NAME_MAX_LENGTH = 30;
10
+ export const PERMISSION_TEXT_MAX_LENGTH = 500;
11
+ export const KEYWORDS_MAX_LENGTH = 100;
12
+ export const COPYRIGHT_MAX_LENGTH = 200;
13
+ export const ORIENTATIONS = ['portrait', 'landscape', 'default'];
14
+ export const USER_INTERFACE_STYLES = ['automatic', 'light', 'dark'];
15
+ export const SPLASH_RESIZE_MODES = ['contain', 'cover'];
16
+ export const RELEASE_STATUSES = ['draft', 'completed'];
17
+ export const LISTING_LIMITS = {
18
+ title: 30,
19
+ subtitle: 30,
20
+ shortDescription: 80,
21
+ description: 4000,
22
+ promoText: 170,
23
+ releaseNotes: 4000
24
+ };
25
+ export const URL_FIELDS = ['support', 'marketing', 'privacyPolicy', 'privacyChoices'];
26
+ /** İlk derlemenin kilitlediği alanlar (`MobileStoreSettings.FieldsLockedByFirstBuild`) */
27
+ export const LOCKABLE_FIELDS = ['bundleIdentifier', 'androidPackage', 'scheme', 'iosBuildNumber', 'androidVersionCode'];
28
+ export const JAVA_KEYWORDS = new Set([
29
+ 'abstract', 'assert', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'default',
30
+ 'do', 'double', 'else', 'enum', 'extends', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements', 'import',
31
+ 'instanceof', 'int', 'interface', 'long', 'native', 'new', 'package', 'private', 'protected', 'public', 'return',
32
+ 'short', 'static', 'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'try',
33
+ 'void', 'volatile', 'while', 'true', 'false', 'null'
34
+ ]);
35
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ const text = (value) => typeof value === 'string' ? value : '';
37
+ const isBlank = (value) => text(value).trim().length === 0;
38
+ /** Backend `EmailAddress()` (ASP.NET Core uyumlu): tek `@`, başta ve sonda değil */
39
+ const isEmail = (value) => {
40
+ const index = value.indexOf('@');
41
+ return index > 0 && index !== value.length - 1 && index === value.lastIndexOf('@');
42
+ };
43
+ /** .NET `Uri.TryCreate(Absolute)` + `https` + host; `https:host` (çift eğik çizgisiz) reddedilir */
44
+ const isHttpsUrl = (value) => {
45
+ if (!/^https:\/\//i.test(value.trim()))
46
+ return false;
47
+ try {
48
+ const url = new URL(value);
49
+ return url.protocol === 'https:' && url.hostname.length > 0;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ };
55
+ /** Biçim hataları: backend `PUT`'u 422 ile reddedecek her alan (alan yolu = backend `errorFields` anahtarı). */
56
+ export function storeSettingsFormatErrors(settings) {
57
+ const errors = [];
58
+ const add = (field, message) => errors.push({ field, message });
59
+ const displayName = settings.displayName;
60
+ if (isBlank(displayName))
61
+ add('displayName', 'boş olamaz');
62
+ else if (text(displayName).length > DISPLAY_NAME_MAX_LENGTH)
63
+ add('displayName', `en çok ${DISPLAY_NAME_MAX_LENGTH} karakter`);
64
+ for (const field of ['bundleIdentifier', 'androidPackage']) {
65
+ const value = text(settings[field]);
66
+ if (isBlank(value))
67
+ add(field, 'boş olamaz');
68
+ else if (!PACKAGE_NAME_PATTERN.test(value))
69
+ add(field, `ters alan adı biçiminde olmalı (ör. com.firma.uygulama): ${PACKAGE_NAME_PATTERN.source}`);
70
+ }
71
+ const keyword = text(settings.androidPackage).split('.').find(segment => JAVA_KEYWORDS.has(segment));
72
+ if (keyword)
73
+ add('androidPackage', `'${keyword}' segmenti Java anahtar sözcüğü — Android paket adında kullanılamaz`);
74
+ const scheme = text(settings.scheme);
75
+ if (isBlank(scheme))
76
+ add('scheme', 'boş olamaz');
77
+ else if (!SCHEME_PATTERN.test(scheme))
78
+ add('scheme', `küçük harfle başlar, en az 3 karakter, yalnız a-z 0-9 + . -: ${SCHEME_PATTERN.source}`);
79
+ else if (scheme === RESERVED_SCHEME)
80
+ add('scheme', `'${RESERVED_SCHEME}' oynatıcı uygulamanın şeması — mağaza derlemesi alamaz`);
81
+ const version = text(settings.version);
82
+ if (isBlank(version))
83
+ add('version', 'boş olamaz');
84
+ else if (!VERSION_PATTERN.test(version))
85
+ add('version', 'MAJOR.MINOR.PATCH biçiminde olmalı, öndeki sıfır yok (ör. 1.2.0)');
86
+ for (const field of ['iosBuildNumber', 'androidVersionCode']) {
87
+ const value = settings[field];
88
+ if (value !== undefined && value !== null && (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > MAX_BUILD_NUMBER)) {
89
+ add(field, `0 … ${MAX_BUILD_NUMBER} arası tam sayı`);
90
+ }
91
+ }
92
+ const choices = [
93
+ ['orientation', ORIENTATIONS],
94
+ ['userInterfaceStyle', USER_INTERFACE_STYLES],
95
+ ['splashResizeMode', SPLASH_RESIZE_MODES]
96
+ ];
97
+ for (const [field, allowed] of choices) {
98
+ if (!allowed.includes(text(settings[field])))
99
+ add(field, `kabul edilen değerler: ${allowed.join(' | ')}`);
100
+ }
101
+ for (const field of ['adaptiveIconBackground', 'splashBackgroundColor']) {
102
+ if (!COLOR_PATTERN.test(text(settings[field])))
103
+ add(field, '#RRGGBB biçiminde renk olmalı');
104
+ }
105
+ for (const [key, value] of Object.entries(isObject(settings.permissions) ? settings.permissions : {})) {
106
+ if (!PERMISSION_KEY_PATTERN.test(key))
107
+ add(`permissions.${key}`, `modül anahtarı ${PERMISSION_KEY_PATTERN.source} biçiminde olmalı (ör. camera)`);
108
+ else if (value !== null && (isBlank(value) || text(value).length > PERMISSION_TEXT_MAX_LENGTH))
109
+ add(`permissions.${key}`, `null ya da 1–${PERMISSION_TEXT_MAX_LENGTH} karakter izin açıklaması`);
110
+ }
111
+ for (const [locale, listing] of Object.entries(isObject(settings.listing) ? settings.listing : {})) {
112
+ if (!LOCALE_PATTERN.test(locale)) {
113
+ add(`listing.${locale}`, "dil kodu geçersiz (ör. 'tr', 'en-US')");
114
+ continue;
115
+ }
116
+ const entry = isObject(listing) ? listing : {};
117
+ for (const [field, max] of Object.entries(LISTING_LIMITS)) {
118
+ if (text(entry[field]).length > max)
119
+ add(`listing.${locale}.${field}`, `en çok ${max} karakter (${text(entry[field]).length} verildi)`);
120
+ }
121
+ const keywords = Array.isArray(entry.keywords) ? entry.keywords.map(text).join(',') : '';
122
+ if (keywords.length > KEYWORDS_MAX_LENGTH)
123
+ add(`listing.${locale}.keywords`, `virgülle birleşik en çok ${KEYWORDS_MAX_LENGTH} karakter (${keywords.length} verildi)`);
124
+ }
125
+ const urls = isObject(settings.urls) ? settings.urls : {};
126
+ for (const field of URL_FIELDS) {
127
+ if (!isBlank(urls[field]) && !isHttpsUrl(text(urls[field])))
128
+ add(`urls.${field}`, 'boş ya da mutlak https:// adres olmalı');
129
+ }
130
+ const play = isObject(settings.play) ? settings.play : {};
131
+ const review = isObject(settings.review) ? settings.review : {};
132
+ const apple = isObject(settings.apple) ? settings.apple : {};
133
+ if (!isBlank(play.contactEmail) && !isEmail(text(play.contactEmail)))
134
+ add('play.contactEmail', 'geçerli e-posta olmalı');
135
+ if (play.releaseStatus !== undefined && play.releaseStatus !== null && !RELEASE_STATUSES.includes(text(play.releaseStatus)))
136
+ add('play.releaseStatus', `kabul edilen değerler: ${RELEASE_STATUSES.join(' | ')}`);
137
+ if (!isBlank(review.email) && !isEmail(text(review.email)))
138
+ add('review.email', 'geçerli e-posta olmalı');
139
+ if (text(apple.copyright).length > COPYRIGHT_MAX_LENGTH)
140
+ add('apple.copyright', `en çok ${COPYRIGHT_MAX_LENGTH} karakter`);
141
+ return errors;
142
+ }
143
+ /**
144
+ * Mağazaya YÜKLEME (testflight/internal) için boş kalan gizli olmayan alanlar — `build` hedefini
145
+ * etkilemez, taslak kaydı engellemez. Kesin liste ön kontroldür (P-01, varsayılan dil projeden gelir).
146
+ */
147
+ export function storeSettingsMissingForUpload(settings) {
148
+ const urls = isObject(settings.urls) ? settings.urls : {};
149
+ const apple = isObject(settings.apple) ? settings.apple : {};
150
+ const categories = isObject(apple.categories) ? apple.categories : {};
151
+ const play = isObject(settings.play) ? settings.play : {};
152
+ const listings = Object.values(isObject(settings.listing) ? settings.listing : {}).filter(isObject);
153
+ const missing = [];
154
+ if (!listings.some(listing => !isBlank(listing.title) && !isBlank(listing.description)))
155
+ missing.push('listing.<dil>.title + description');
156
+ if (isBlank(urls.privacyPolicy))
157
+ missing.push('urls.privacyPolicy');
158
+ if (isBlank(categories.primary))
159
+ missing.push('apple.categories.primary (iOS)');
160
+ if (isBlank(apple.copyright))
161
+ missing.push('apple.copyright (iOS)');
162
+ if (isBlank(play.contactEmail))
163
+ missing.push('play.contactEmail (Android)');
164
+ return missing;
165
+ }
166
+ /**
167
+ * Backend `PUT` tam satırı yazar: gönderilmeyen alan varsayılana döner (`displayName` boşalır → 422,
168
+ * `listing` silinir). Yama bu yüzden okunan kaydın üstüne birleştirilir: düz nesneler alan alan,
169
+ * dizi ve skaler değer yerine geçer. Sayaçlar yalnız yamada varsa gider — okunan sayacı geri yazmak,
170
+ * arada derleme başlamışsa kilit ihlali (422) üretirdi (`null` = değiştirme).
171
+ */
172
+ export function mergeStoreSettings(current, patch) {
173
+ const merge = (base, overlay) => {
174
+ if (!isObject(base) || !isObject(overlay))
175
+ return overlay;
176
+ return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(overlay)])]
177
+ .map(key => [key, key in overlay ? merge(base[key], overlay[key]) : base[key]]));
178
+ };
179
+ const merged = merge(current, patch);
180
+ for (const counter of ['iosBuildNumber', 'androidVersionCode']) {
181
+ if (!(counter in patch))
182
+ delete merged[counter];
183
+ }
184
+ return merged;
185
+ }
186
+ /** Yamanın gerçekten değiştirdiği yaprak alan yolları (`listing.tr.title`, `bundleIdentifier`). */
187
+ export function changedStoreFields(current, patch, prefix = '') {
188
+ return Object.entries(patch).flatMap(([key, value]) => {
189
+ const before = current[key];
190
+ const path = `${prefix}${key}`;
191
+ if (isObject(value) && isObject(before))
192
+ return changedStoreFields(before, value, `${path}.`);
193
+ return JSON.stringify(value) === JSON.stringify(before ?? null) ? [] : [path];
194
+ });
195
+ }
196
+ const guide = "platform_guide('mobile', 'yayin')";
197
+ /** MOB-022 — `definition` birleşmiş kayıttır; biçim hatası backend'in 422'siyle aynı ret (block), eksik alan uyarı. */
198
+ export const mobileStorePolicyRules = [
199
+ {
200
+ id: 'MOB-022',
201
+ module: 'mobileStore',
202
+ appliesTo: ['update'],
203
+ sourceRef: 'MobileStoreSettingsValidator (zeyrek-backend/docs/mobile-builds.md §Doğrulama tablosu); yükleme eksikleri MobileStorePreflightService P-01',
204
+ check: (definition) => {
205
+ const errors = storeSettingsFormatErrors(definition);
206
+ const missing = storeSettingsMissingForUpload(definition);
207
+ return [
208
+ ...(errors.length > 0
209
+ ? [{
210
+ ruleId: 'MOB-022',
211
+ severity: 'block',
212
+ message: `Mağaza ayarı biçimsiz — backend PUT'u 422 ile reddeder, hiçbir alan yazılmaz: ${errors.map(error => `${error.field}: ${error.message}`).join(' · ')}`,
213
+ guide
214
+ }]
215
+ : []),
216
+ ...(missing.length > 0
217
+ ? [{
218
+ ruleId: 'MOB-022',
219
+ severity: 'warn',
220
+ message: `Mağazaya yükleme (testflight/internal) için eksik: ${missing.join(', ')}. Kayıt taslak olarak yazılır ve 'build' hedefini etkilemez; kesin liste mobile_build_start { dryRun: true } ön kontrolündedir. Ekran görüntüsü, ikon, gizli bilgi ve beyanlar MCP'den yazılmaz — Studio › Yayın.`,
221
+ guide
222
+ }]
223
+ : [])
224
+ ];
225
+ }
226
+ }
227
+ ];