@appaflytech/zeyrek-ai-mcp 0.0.28 → 0.0.30

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 (201) hide show
  1. package/.env.example +15 -0
  2. package/README.md +5 -5
  3. package/dist/config.js +37 -0
  4. package/dist/index.js +2 -0
  5. package/dist/knowledge/access.js +802 -0
  6. package/dist/knowledge/backendConstraints.js +18 -1
  7. package/dist/knowledge/case.js +41 -0
  8. package/dist/knowledge/componentCatalog.js +494 -34
  9. package/dist/knowledge/componentEvents.js +132 -7
  10. package/dist/knowledge/connectDataBind.js +819 -0
  11. package/dist/knowledge/connectDataDefinition.js +341 -0
  12. package/dist/knowledge/connectDataExecute.js +93 -0
  13. package/dist/knowledge/connectDataRules.js +128 -0
  14. package/dist/knowledge/decision.js +121 -0
  15. package/dist/knowledge/expressionPrelude.js +59 -0
  16. package/dist/knowledge/layoutSummary.js +140 -2
  17. package/dist/knowledge/microserviceParameters.js +58 -0
  18. package/dist/knowledge/mining.js +94 -0
  19. package/dist/knowledge/mobileComponentCatalog.js +859 -0
  20. package/dist/knowledge/mobileComponentEvents.js +239 -0
  21. package/dist/knowledge/normalize.js +236 -67
  22. package/dist/knowledge/outlines.js +36 -0
  23. package/dist/knowledge/platform/lint.js +385 -0
  24. package/dist/knowledge/platform/mobile.js +142 -0
  25. package/dist/knowledge/platform/mobileLayoutLint.js +324 -0
  26. package/dist/knowledge/platform/mobileRules.js +231 -0
  27. package/dist/knowledge/platform/mobileSpacing.js +82 -0
  28. package/dist/knowledge/platform/mobileTabs.js +108 -0
  29. package/dist/knowledge/platform/nativeBoxModel.js +44 -0
  30. package/dist/knowledge/platform/profile.js +25 -0
  31. package/dist/knowledge/platform/resolve.js +122 -0
  32. package/dist/knowledge/platform/web.js +39 -0
  33. package/dist/knowledge/policy.js +18 -4
  34. package/dist/knowledge/process.js +510 -0
  35. package/dist/knowledge/reservedTables.js +79 -0
  36. package/dist/knowledge/rowFilter.js +43 -0
  37. package/dist/knowledge/rules.js +319 -21
  38. package/dist/knowledge/schemaGuard.js +11 -0
  39. package/dist/knowledge/styleResolve.js +50 -4
  40. package/dist/knowledge/styleResolveNative.js +527 -0
  41. package/dist/knowledge/themeResolve.js +3 -3
  42. package/dist/observability/trace.js +6 -3
  43. package/dist/platform/argumentAliases.js +128 -0
  44. package/dist/platform/bodyFile.js +103 -0
  45. package/dist/platform/capture/actions.js +333 -0
  46. package/dist/platform/capture/browser.js +1 -0
  47. package/dist/platform/{screenshot.js → capture/capture.js} +105 -519
  48. package/dist/platform/capture/console-log.js +122 -0
  49. package/dist/platform/capture/index.js +11 -0
  50. package/dist/platform/capture/readiness.js +103 -0
  51. package/dist/platform/capture/summary.js +77 -0
  52. package/dist/platform/capture/target.js +5 -0
  53. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  54. package/dist/platform/capture/targets/studio-web.js +5 -0
  55. package/dist/platform/diagnostics.js +10 -4
  56. package/dist/platform/domSummary.js +1 -1
  57. package/dist/platform/errors.js +162 -10
  58. package/dist/platform/http.js +29 -8
  59. package/dist/platform/impersonation.js +45 -0
  60. package/dist/platform/jsonArray.js +24 -0
  61. package/dist/platform/responses.js +39 -5
  62. package/dist/platform/schemaReadMemory.js +60 -0
  63. package/dist/platform/secrets.js +17 -1
  64. package/dist/platform/strictArguments.js +68 -12
  65. package/dist/platform/tokenVault.js +187 -4
  66. package/dist/resources/examples/bpm-gorev-kontrol.json +294 -0
  67. package/dist/resources/examples/bpm-iso-dokuman.json +228 -0
  68. package/dist/resources/examples/bpm-satinalma.json +376 -0
  69. package/dist/resources/examples/connect-data-convert-verify.json +107 -0
  70. package/dist/resources/examples/connect-data-dropdown-roles.json +66 -0
  71. package/dist/resources/examples/connect-data-edit-m2m.json +73 -0
  72. package/dist/resources/examples/connect-data-query-params.json +50 -0
  73. package/dist/resources/examples/connect-data-table-entity.json +147 -0
  74. package/dist/resources/examples/connect-data-workflow-microservice.json +65 -0
  75. package/dist/resources/examples/mobile-app-shell-drawer.json +243 -0
  76. package/dist/resources/examples/mobile-app-shell-stack.json +157 -0
  77. package/dist/resources/examples/mobile-app-shell-tab.json +208 -0
  78. package/dist/resources/examples/mobile-form-screen.json +365 -0
  79. package/dist/resources/examples/mobile-list-detail-form.json +672 -0
  80. package/dist/resources/examples/page-detail-profile.json +2 -1
  81. package/dist/resources/examples/page-dynamic-form.json +3 -2
  82. package/dist/resources/examples/page-flex-sections.json +1 -1
  83. package/dist/resources/examples/page-html-embed.json +1 -1
  84. package/dist/resources/examples/workflow-basic.json +8 -2
  85. package/dist/resources/guides/access.md +796 -0
  86. package/dist/resources/guides/bpm.md +686 -0
  87. package/dist/resources/guides/case.md +153 -0
  88. package/dist/resources/guides/component.md +91 -4
  89. package/dist/resources/guides/decision.md +185 -0
  90. package/dist/resources/guides/entity.md +50 -8
  91. package/dist/resources/guides/expressions.md +14 -5
  92. package/dist/resources/guides/functions.md +1 -1
  93. package/dist/resources/guides/getting-started.md +38 -1
  94. package/dist/resources/guides/layout.md +11 -7
  95. package/dist/resources/guides/microservice.md +81 -2
  96. package/dist/resources/guides/middleware.md +44 -2
  97. package/dist/resources/guides/mobile-layout.md +193 -0
  98. package/dist/resources/guides/mobile.md +495 -0
  99. package/dist/resources/guides/page.md +454 -75
  100. package/dist/resources/guides/theme.md +8 -2
  101. package/dist/resources/guides/translation.md +1 -1
  102. package/dist/resources/guides/workflow.md +58 -1
  103. package/dist/resources/read.js +41 -0
  104. package/dist/resources/rules.js +11 -5
  105. package/dist/resources/schema-fingerprint.json +7 -5
  106. package/dist/resources/schemas/AccessExplanationDto.json +144 -0
  107. package/dist/resources/schemas/AccessMatrixDto.json +170 -0
  108. package/dist/resources/schemas/AccessPermissionDto.json +57 -0
  109. package/dist/resources/schemas/AccessPolicyDto.json +223 -0
  110. package/dist/resources/schemas/AccessResourceDto.json +86 -0
  111. package/dist/resources/schemas/AppSessionDto.json +53 -0
  112. package/dist/resources/schemas/ConnectDataDescribeDto.json +344 -0
  113. package/dist/resources/schemas/ConnectDataRequestModel.json +202 -0
  114. package/dist/resources/schemas/ConnectDataResultDto.json +421 -0
  115. package/dist/resources/schemas/ConnectorCredentialDto.json +70 -0
  116. package/dist/resources/schemas/ConvertConnectDataToMicroserviceCommand.json +37 -0
  117. package/dist/resources/schemas/CreateAccessPolicyCommand.json +209 -0
  118. package/dist/resources/schemas/CreateDecisionCommand.json +221 -0
  119. package/dist/resources/schemas/CreateEntityCommand.json +118 -0
  120. package/dist/resources/schemas/CreateMicroserviceCommand.json +13 -1
  121. package/dist/resources/schemas/CreateProcessDefinitionCommand.json +1337 -0
  122. package/dist/resources/schemas/CreateQueryCommand.json +4 -0
  123. package/dist/resources/schemas/CreateWorkflowCommand.json +143 -1
  124. package/dist/resources/schemas/DataJobDto.json +6 -1
  125. package/dist/resources/schemas/DecisionDto.json +302 -0
  126. package/dist/resources/schemas/DetachConnectDataMicroserviceCommand.json +28 -0
  127. package/dist/resources/schemas/ExplainAccessQuery.json +33 -0
  128. package/dist/resources/schemas/GenerateProcessDocumentCommand.json +116 -0
  129. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +21 -1
  130. package/dist/resources/schemas/MicroserviceDto.json +15 -3
  131. package/dist/resources/schemas/OrganizationProjectDto.json +335 -0
  132. package/dist/resources/schemas/OrganizationSchemaDto.json +196 -0
  133. package/dist/resources/schemas/OrganizationSchemaVersionDto.json +207 -0
  134. package/dist/resources/schemas/PatchProcessDefinitionCommand.json +76 -0
  135. package/dist/resources/schemas/PreviewMicroserviceCommand.json +10 -1
  136. package/dist/resources/schemas/PreviewWorkflowCommand.json +22 -1
  137. package/dist/resources/schemas/ProcessDefinitionDto.json +1480 -0
  138. package/dist/resources/schemas/ProcessSimulationDto.json +96 -0
  139. package/dist/resources/schemas/SaveConnectorCredentialCommand.json +90 -0
  140. package/dist/resources/schemas/SaveProcessEventSourceCommand.json +61 -0
  141. package/dist/resources/schemas/SaveProcessMailSourceCommand.json +89 -0
  142. package/dist/resources/schemas/SignProcessTaskCommand.json +73 -0
  143. package/dist/resources/schemas/SimulateProcessDefinitionQuery.json +92 -0
  144. package/dist/resources/schemas/StepDto.json +22 -1
  145. package/dist/resources/schemas/UpdateAccessPolicyCommand.json +209 -0
  146. package/dist/resources/schemas/UpdateDecisionCommand.json +216 -0
  147. package/dist/resources/schemas/UpdateEntityCommand.json +118 -0
  148. package/dist/resources/schemas/UpdateMicroserviceCommand.json +13 -1
  149. package/dist/resources/schemas/UpdateProcessDefinitionCommand.json +1357 -0
  150. package/dist/resources/schemas/UpdateQueryCommand.json +4 -0
  151. package/dist/resources/schemas/UpdateViewCommand.json +0 -2
  152. package/dist/resources/schemas/UpdateWorkflowCommand.json +143 -1
  153. package/dist/resources/schemas/ValidateProcessDefinitionQuery.json +1339 -0
  154. package/dist/server.js +27 -1
  155. package/dist/tools/access.js +369 -2
  156. package/dist/tools/accessOperations.js +135 -0
  157. package/dist/tools/accessPolicies.js +170 -0
  158. package/dist/tools/accessRuntime.js +203 -0
  159. package/dist/tools/applications.js +60 -3
  160. package/dist/tools/bpm.js +343 -0
  161. package/dist/tools/bpmGovernance.js +266 -0
  162. package/dist/tools/bpmOperations.js +230 -0
  163. package/dist/tools/bpmReporting.js +154 -0
  164. package/dist/tools/bpmRouting.js +150 -0
  165. package/dist/tools/bpmRuntime.js +396 -0
  166. package/dist/tools/case.js +178 -0
  167. package/dist/tools/caseRuntime.js +89 -0
  168. package/dist/tools/common.js +7 -0
  169. package/dist/tools/components.js +118 -14
  170. package/dist/tools/connectData.js +449 -0
  171. package/dist/tools/connectors.js +193 -0
  172. package/dist/tools/datasources.js +3 -3
  173. package/dist/tools/decisions.js +206 -0
  174. package/dist/tools/dependencies.js +1 -1
  175. package/dist/tools/deployments.js +2 -2
  176. package/dist/tools/designSchemaPatch.js +34 -9
  177. package/dist/tools/dtos.js +36 -5
  178. package/dist/tools/entities.js +79 -12
  179. package/dist/tools/externalComponents.js +26 -5
  180. package/dist/tools/flows.js +122 -14
  181. package/dist/tools/folders.js +1 -1
  182. package/dist/tools/imports.js +5 -2
  183. package/dist/tools/layouts.js +50 -17
  184. package/dist/tools/merges.js +1 -1
  185. package/dist/tools/microservices.js +77 -25
  186. package/dist/tools/mobile.js +221 -0
  187. package/dist/tools/modules.js +48 -0
  188. package/dist/tools/organizationStructure.js +137 -0
  189. package/dist/tools/pages.js +465 -98
  190. package/dist/tools/platformInfo.js +27 -8
  191. package/dist/tools/profileWrite.js +92 -0
  192. package/dist/tools/projects.js +71 -9
  193. package/dist/tools/rows.js +69 -13
  194. package/dist/tools/schemaOps.js +61 -1
  195. package/dist/tools/screenWrite.js +37 -8
  196. package/dist/tools/screens.js +95 -5
  197. package/dist/tools/templates.js +26 -1
  198. package/dist/tools/translations.js +1 -1
  199. package/dist/tools/variables.js +11 -5
  200. package/dist/tools/workflows.js +10 -5
  201. package/package.json +2 -2
@@ -0,0 +1,819 @@
1
+ import { connectDataShapeIssues, connectedPropKeys, selectedPaths, toWildcardPath } from './connectDataDefinition.js';
2
+ import { deepWrapPropData } from './normalize.js';
3
+ export const CONNECT_DATA_DEFAULT_PAGE_SIZE = 25;
4
+ export const CONNECT_DATA_PREVIEW_LIMIT = 50;
5
+ const PROTECTED_OPERATORS = ['eq', 'neq', 'in', 'notin'];
6
+ const EDIT_KEYS = ['create', 'update', 'delete', 'bulk'];
7
+ /**
8
+ * CD-017: anonim sayfada yazmaya açık bağlantı. Backend yazmada kimliği yalnız anonim OLMAYAN sayfada ister
9
+ * (`ExecuteConnectDataBehavior`): anonim sayfada oturumsuz ziyaretçi yazar — basit projede her zaman, kompleks projede
10
+ * politika anonim özneye izin veriyorsa.
11
+ */
12
+ export function anonymousWriteFindings(isPageAnonymous, edit) {
13
+ const open = ['create', 'update', 'delete'].filter(key => edit?.[key] === true);
14
+ return isPageAnonymous && open.length
15
+ ? [{ ruleId: 'CD-017', severity: 'warn', message: `Sayfa anonim (isAnonymous) ve bağlantı yazmaya açık (${open.join(', ')}): oturumsuz ziyaretçi bu tabloya yazabilir — basit projede her zaman, kompleks projede politika anonim özneye izin veriyorsa. Yazma gerekmiyorsa edit'i kapatın ya da sayfayı oturumlu yapın.` }]
16
+ : [];
17
+ }
18
+ export const SAME_SOURCE_BINDING_LIMIT = 5;
19
+ /**
20
+ * CD-010: sayfada aynı kaynağa 5+ bağlantı (bu bağlantı dahil) — runtime her bağlantı için ayrı okuma yapar, sayfa
21
+ * açılışında o kadar istek gider. Bağlanan prop sayılmaz, yerine bu çağrı bir kez sayılır (yeniden bağlama).
22
+ */
23
+ export function sameSourceFindings(nodes, sourceId, element, prop) {
24
+ const others = [...connectedPropKeys(nodes)]
25
+ .filter(key => key !== `${element}.${prop}`)
26
+ .filter(key => {
27
+ const [target = '', ...path] = key.split('.');
28
+ const ref = readPropValue(findSchemaNode(nodes, target)?.props, path.join('.'))?.ref;
29
+ return ref?.source?.id === sourceId;
30
+ });
31
+ return others.length + 1 >= SAME_SOURCE_BINDING_LIMIT
32
+ ? [{ ruleId: 'CD-010', severity: 'warn', message: `Sayfada aynı kaynağa ${others.length + 1} bağlantı (${[...others, `${element}.${prop}`].join(', ')}): runtime her bağlantı için ayrı istek atar, sayfa açılışı o kadar okur. Aynı satırları gösteren bileşenler için alan seçimini daraltın ya da bağlantı sayısını azaltın.` }]
33
+ : [];
34
+ }
35
+ /**
36
+ * CD-011 uyarı dalı: tanımda lookup ayarı olan ilişki alanının hedef tablosunu bu kullanıcı okuyamıyor
37
+ * (`relation.targetReadable: false`) — hücre kilitli çizilir, lookup açılmaz; ayar bu kullanıcı için etkisiz.
38
+ */
39
+ export function lookupReadabilityFindings(definition, describe) {
40
+ return (definition.fields ?? [])
41
+ .filter(field => field.lookup && describe.fields.find(item => item.slug === field.slug)?.relation?.targetReadable === false)
42
+ .map(field => ({ ruleId: 'CD-011', severity: 'warn', message: `'${field.slug}' için lookup ayarı var ama bu kullanıcı hedef tabloyu okuyamıyor (relation.targetReadable: false): hücre kilitli çizilir, lookup açılmaz. Hedef tabloya okuma izni verin ya da lookup ayarını kaldırın.` }));
43
+ }
44
+ /**
45
+ * CD-006: tanımda açık düzenleme eylemine ya da editable alana bu öznenin politikası izin vermiyorsa ayar
46
+ * etkisizdir — runtime 403 (`ConnectDataWriteDisabled` değil, PEP: `entity.<op>` / `AccessFieldWriteDenied`).
47
+ */
48
+ export function editFindings(edit, describe) {
49
+ const findings = [];
50
+ for (const key of EDIT_KEYS) {
51
+ if (edit?.[key] === true && describe.write && describe.write[key] === false) {
52
+ findings.push({ ruleId: 'CD-006', severity: 'warn', message: `edit.${key} açık ama politika bu kullanıcıya ${key} izni vermiyor — ayar etkisiz, runtime 403 döner.` });
53
+ }
54
+ }
55
+ for (const slug of edit?.editable ?? []) {
56
+ const field = describe.fields.find(item => item.slug === slug);
57
+ // Politikanın gizlediği alan kabiliyetin fields'ında hiç yoktur, yalnız hidden[]'dadır: find boş döndüğü için
58
+ // eskiden sessizce atlanıyordu (39/M-17).
59
+ if (!field && (describe.hidden ?? []).includes(slug)) {
60
+ findings.push({ ruleId: 'CD-006', severity: 'warn', message: `'${slug}' editable ama politika bu alanı bu kullanıcıdan gizliyor (hidden) — kolon çizilmez, hücre açılmaz, yazma 403 AccessFieldWriteDenied döner.` });
61
+ continue;
62
+ }
63
+ if (field && field.canWrite === false && !field.isKey) {
64
+ findings.push({ ruleId: 'CD-006', severity: 'warn', message: `'${slug}' editable ama bu kullanıcı için canWrite yok — hücre düzenlemeye açılmaz, yazma 403 AccessFieldWriteDenied döner.` });
65
+ }
66
+ }
67
+ return findings;
68
+ }
69
+ /**
70
+ * 38/M-13: CD-006 ve CD-011 uyarıları kabiliyete bakar; bağlama gateway kimliğiyle (yönetici) okuduğu için ikisi
71
+ * hiç çıkmıyordu. `asUserId` verildiğinde aynı iki kural o kullanıcının kabiliyetiyle ikinci kez koşar; mesaj
72
+ * hangi gözle üretildiğini söyler (yönetici gözündeki aynı uyarıyla karışmasın).
73
+ */
74
+ export function userEyeFindings(definition, describe, userId) {
75
+ const edit = definition.behavior?.edit;
76
+ return [...editFindings(edit, describe), ...lookupReadabilityFindings(definition, describe)]
77
+ .map(finding => ({ ...finding, message: `kullanıcı ${userId} gözüyle: ${finding.message}` }));
78
+ }
79
+ /** İndeksli prop yolunun kaydı: `series.1.data` → `series[].data`. */
80
+ export function resolveConnectable(connectData, propPath) {
81
+ return connectData?.props?.[toWildcardPath(propPath)] ?? undefined;
82
+ }
83
+ const ROLE_FIELD_TYPES = {
84
+ any: null,
85
+ string: ['string'],
86
+ number: ['number'],
87
+ boolean: ['boolean'],
88
+ date: ['date', 'datetime'],
89
+ parent: ['relation', 'guid']
90
+ };
91
+ const SCALAR_FIELD_TYPES = ['string', 'number', 'date', 'datetime', 'time', 'boolean', 'guid'];
92
+ /**
93
+ * Rolün kabul ettiği alan tipleri (CD-016 uyarısı; öneri listesinden gevşek): metin rolü her skaler değeri gösterir
94
+ * (anahtar `guid` dahil), ilişki/dosya nesnesini göstermez; sayı/boolean/tarih rolü kendi tipini, üst kayıt rolü
95
+ * anahtar tiplerini ister. `null` her tipi kabul eder.
96
+ */
97
+ const ROLE_ACCEPTED_TYPES = {
98
+ any: null,
99
+ string: SCALAR_FIELD_TYPES,
100
+ number: ['number'],
101
+ boolean: ['boolean'],
102
+ date: ['date', 'datetime'],
103
+ parent: ['relation', 'guid', 'string', 'number']
104
+ };
105
+ /** Eşlenen kök alanın tipi rolü besleyemiyorsa `{ type, expected }`; yol (`sube.ad`) ve kaynakta olmayan alan denetlenmez. */
106
+ export function roleTypeMismatch(roleType, slug, fields) {
107
+ const accepted = ROLE_ACCEPTED_TYPES[roleType ?? 'any'] ?? null;
108
+ const field = slug.includes('.') ? undefined : fields.find(item => item.slug === slug);
109
+ return accepted && field && !accepted.includes(field.type) ? { type: field.type, expected: accepted } : null;
110
+ }
111
+ /** Tanım filtresindeki tekil koşulların alan yolları (grup içleri dahil). */
112
+ export function filterFieldPaths(filter) {
113
+ if (!filter)
114
+ return [];
115
+ if (filter.filterType === 'group')
116
+ return (Array.isArray(filter.children) ? filter.children : []).flatMap(child => filterFieldPaths(child));
117
+ return typeof filter.field === 'string' ? [filter.field] : [];
118
+ }
119
+ /**
120
+ * `fixDrift`: kökü düşen alana giden koşullar filtreden çıkar, boşalan grup düşer, kök boşalırsa `null` — tanım
121
+ * filtresi silinmiş alanı işaret ettikçe backend 422 verir.
122
+ */
123
+ export function dropFilterPaths(filter, isDropped) {
124
+ if (!filter)
125
+ return null;
126
+ if (filter.filterType === 'group') {
127
+ const children = (Array.isArray(filter.children) ? filter.children : [])
128
+ .map(child => dropFilterPaths(child, isDropped))
129
+ .filter((child) => child !== null);
130
+ return children.length ? { ...filter, children } : null;
131
+ }
132
+ return typeof filter.field === 'string' && isDropped(filter.field) ? null : filter;
133
+ }
134
+ /** Rol türüne uyan, politikada gizli olmayan alanlar. */
135
+ export function roleCandidates(roleType, fields) {
136
+ const types = ROLE_FIELD_TYPES[roleType] ?? null;
137
+ return fields.filter(field => field.access !== 'hidden' && (types === null || types.includes(field.type)));
138
+ }
139
+ const KEY_ROLES = ['value', 'key', 'id'];
140
+ const DISPLAY_ROLES = ['label', 'name', 'title', 'text', 'category'];
141
+ /**
142
+ * Rol eşleme önerisi (Studio sihirbazıyla aynı kural): rol adıyla aynı slug; `value/key/id` ←
143
+ * birincil anahtar (rol türü sayı/boolean/tarih değilse — grafiğin `value`'su ölçüdür, anahtar değil);
144
+ * `label/name/title/text/category` ← görünen alan;
145
+ * `parent` ← kaynağa işaret eden ilişki. Türündeki ilk kullanılmamış alana düşme YALNIZ zorunlu
146
+ * rollerde yapılır: dropdown'un `caption` / `isDisabled` gibi isteğe bağlı rolleri ilgisiz bir alana
147
+ * eşlenmez.
148
+ */
149
+ export function defaultMapping(connectable, describe) {
150
+ const roles = {};
151
+ const used = new Set();
152
+ const visible = describe.fields.filter(field => field.access !== 'hidden');
153
+ const required = connectable?.required ?? [];
154
+ for (const [role, roleType] of Object.entries(connectable?.roles ?? {})) {
155
+ const candidates = roleCandidates(roleType, describe.fields);
156
+ const bySlug = candidates.find(field => field.slug === role);
157
+ const byRole = KEY_ROLES.includes(role) && !['number', 'boolean', 'date'].includes(roleType) && describe.source?.primaryKey
158
+ ? visible.find(field => field.slug === describe.source?.primaryKey)
159
+ : DISPLAY_ROLES.includes(role) && describe.source?.display
160
+ ? visible.find(field => field.slug === describe.source?.display)
161
+ : roleType === 'parent' && required.includes(role)
162
+ ? candidates.find(field => field.relation?.targetEntityId === describe.source?.id)
163
+ : undefined;
164
+ const fallback = roleType === 'parent' || !required.includes(role) ? undefined : candidates.find(field => !field.isKey && !used.has(field.slug));
165
+ const chosen = bySlug ?? byRole ?? fallback;
166
+ if (chosen) {
167
+ roles[role] = chosen.slug;
168
+ used.add(chosen.slug);
169
+ }
170
+ }
171
+ return roles;
172
+ }
173
+ /**
174
+ * Alan tipi → öğenin tip/biçim seçeneklerinde aranan değerler, öncelik sırasıyla (Studio
175
+ * `FIELD_LIST_TYPE_CANDIDATES` aynası). Bileşen kendi seçeneklerini bildirir, ilk sunulan kazanır.
176
+ */
177
+ export const FIELD_LIST_TYPE_CANDIDATES = {
178
+ string: ['text'],
179
+ number: ['number'],
180
+ boolean: ['switch', 'boolean'],
181
+ date: ['date'],
182
+ datetime: ['datetime', 'date'],
183
+ time: ['time', 'text'],
184
+ guid: ['text'],
185
+ relation: ['relation', 'tag', 'text'],
186
+ file: ['file', 'text'],
187
+ enum: ['tag', 'text']
188
+ };
189
+ /** Motorun tek okumada döndürdüğü en çok satır (backend `page.limit` üst sınırı). */
190
+ export const CONNECT_DATA_READ_ALL_PAGE_SIZE = 1000;
191
+ /**
192
+ * Sayfalama yüzeyi olmayan bileşen (bileşen tanımında `isPaginated` yok — grafik, dropdown, kart…) tanımın ilk
193
+ * sayfasını değil kaynağın bütün sonucunu çizer (39/03 U-5, Studio `pageForSurface` aynası): açık `pageSize`
194
+ * verilmediyse sayfa boyu tek okuma tavanıdır, kayıtlı tanımın eski sayfa boyu (25) de ezilir. Sayfalanan bileşende
195
+ * ve tanımı bilinmeyen bileşende `undefined` — sayfa boyu girdiden ya da kayıtlı tanımdan gelir.
196
+ */
197
+ export function readAllPageSize(definitionProps, requested) {
198
+ if (requested !== undefined)
199
+ return requested;
200
+ const props = Array.isArray(definitionProps) ? definitionProps : null;
201
+ return props && !props.some(prop => prop?.name === 'isPaginated') ? CONNECT_DATA_READ_ALL_PAGE_SIZE : undefined;
202
+ }
203
+ /** Bileşen tanımındaki liste prop'unun öğe prop'ları (`fieldsFrom` hedefi). */
204
+ export function fieldListItemProps(definitionProps, fieldsFrom) {
205
+ const props = Array.isArray(definitionProps) ? definitionProps : [];
206
+ const target = props.find(prop => prop?.name === fieldsFrom);
207
+ return (Array.isArray(target?.props) ? target.props : []).filter(prop => typeof prop?.name === 'string');
208
+ }
209
+ const firstItemProp = (itemProps, names) => names.map(name => itemProps.find(prop => prop.name === name)).find(Boolean);
210
+ /**
211
+ * Seçili yollardan liste öğeleri (Studio `fieldListFromSource` aynası): öğe şemasının bildiği adlar
212
+ * doldurulur — ad (`name/field/key/value`), başlık (`label/title/header`), tip (`type/format`, bileşenin
213
+ * sunduğu seçeneklerden), sıralanabilir, filtrelenebilir. Öğe şeması bilinmiyorsa `name/label/type`.
214
+ * Expand yolu (`sube.ad`) kaynak describe'ında yoksa metin kolonu olur.
215
+ */
216
+ export function columnsFromDescribe(describe, paths, itemProps = [], filterable) {
217
+ const schema = itemProps.length > 0 ? itemProps : [{ name: 'name' }, { name: 'label' }, { name: 'type' }, { name: 'isSortable' }, { name: 'isFilterable' }];
218
+ return paths.map(path => {
219
+ const field = describe.fields.find(item => item.slug === path) ?? { slug: path, name: path, type: 'string', access: 'plain' };
220
+ const item = {};
221
+ const nameProp = firstItemProp(schema, ['name', 'field', 'key', 'value']);
222
+ const labelProp = firstItemProp(schema, ['label', 'title', 'header']);
223
+ const typeProp = firstItemProp(schema, ['type', 'format']);
224
+ const sortProp = firstItemProp(schema, ['isSortable', 'sortable']);
225
+ const filterProp = firstItemProp(schema, ['isFilterable', 'filterable']);
226
+ if (nameProp)
227
+ item[nameProp.name] = field.slug;
228
+ if (labelProp)
229
+ item[labelProp.name] = field.name || field.slug;
230
+ if (typeProp) {
231
+ const offered = (typeProp.options ?? []).map(option => String(option.value));
232
+ const candidates = field.enum?.length ? FIELD_LIST_TYPE_CANDIDATES.enum : FIELD_LIST_TYPE_CANDIDATES[field.type];
233
+ const match = (candidates ?? ['text']).find(candidate => offered.length === 0 || offered.includes(candidate));
234
+ if (match)
235
+ item[typeProp.name] = match;
236
+ }
237
+ if (sortProp && field.sortable)
238
+ item[sortProp.name] = true;
239
+ // 38/M-5: kolonun filtre düğmesi tanımın behavior.filterable'ına bağlıdır — platform listede olmayan alanın
240
+ // filtresini ConnectDataFilterNotAllowed ile reddeder (ConnectDataService.EnsureFilterAllowed; boş liste = hiçbiri).
241
+ if (filterProp && field.canFilter && (field.operators ?? []).length > 0 && (filterable === undefined || filterable.includes(field.slug)))
242
+ item[filterProp.name] = true;
243
+ return item;
244
+ });
245
+ }
246
+ /**
247
+ * CD-002 — filtre ağacındaki her koşulun alanı kaynağın süzülebilir alanı mı, işleci kabiliyetin
248
+ * listesinde mi (korumalı alanda yalnız eq/neq/in/notin). `filterable` verilirse (runtime/preview
249
+ * isteği) alan orada da olmalı; tanım filtresi için verilmez.
250
+ */
251
+ export function filterFindings(filter, describe, filterable) {
252
+ const findings = [];
253
+ const visit = (node) => {
254
+ if (!node || typeof node !== 'object')
255
+ return;
256
+ if (node.filterType === 'group' || Array.isArray(node.children) && !node.field) {
257
+ for (const child of node.children ?? [])
258
+ visit(child);
259
+ return;
260
+ }
261
+ const slug = String(node.field ?? '');
262
+ const field = describe.fields.find(item => item.slug === slug && item.access !== 'hidden');
263
+ const operators = field?.operators ?? [];
264
+ if (!field && slug.includes('.'))
265
+ return;
266
+ if (!field || field.canFilter === false) {
267
+ findings.push({ ruleId: 'CD-002', severity: 'block', message: `'${slug}' bu kaynakta süzülebilir bir alan değil${field ? '' : ' (yok ya da politikada gizli)'}.` });
268
+ }
269
+ else if (filterable && !filterable.includes(slug)) {
270
+ findings.push({ ruleId: 'CD-002', severity: 'block', message: `'${slug}' tanımın behavior.filterable listesinde değil; platform ConnectDataFilterNotAllowed ile reddeder. Filtrelenebilir: ${filterable.join(' · ') || '(boş)'}.` });
271
+ }
272
+ else if (node.operator && !operators.includes(node.operator)) {
273
+ findings.push({ ruleId: 'CD-002', severity: 'block', message: `'${slug}' için '${node.operator}' işleci izinli değil. İzinli: ${operators.join(' · ')}${field.isProtected ? ' (korumalı alan: yalnız ' + PROTECTED_OPERATORS.join('/') + ')' : ''}.` });
274
+ }
275
+ };
276
+ visit(filter);
277
+ return findings;
278
+ }
279
+ /**
280
+ * CD-002 sıralama mesajı kaynak türüne göre (38/M-7): query kaynağında da "microservice kaynağında…" yazıyordu.
281
+ * query/view/workflow sıralamayı hiç desteklemez (CD-013 aynı çağrıda ayrıca söyler).
282
+ */
283
+ function unsortableReason(kind) {
284
+ if (kind === 'microservice')
285
+ return "microservice kaynağında uç bu alanda sıralama sunmuyor (dinamik filtre kapalı ya da alan dönüş şemasında sıralanamaz); tasarım anı 422 'connect-data-source-feature-unsupported'. Sıralanabilir bir alan seçin.";
286
+ if (kind === 'entity')
287
+ return "entity kaynağında tanım sıralaması denetlenmez ve korumalı/sıralanamaz kolonun üstünde anlamsız bir sıra üretir (kullanıcı aynı alanla sıralarsa istek 422 'ConnectDataFilterNotAllowed' alır). Sıralanabilir bir alan seçin.";
288
+ return `${kind} kaynağı sunucuda sıralamaz (CD-013): tanıma sort yazılmaz — platform 422 'connect-data-source-feature-unsupported' verir. Sıralamayı kaldırın; satırlar kaynağın kendi sırasıyla gelir (query'de ORDER BY), bileşen client kipte tarayıcıda sıralar.`;
289
+ }
290
+ /**
291
+ * Girdi + kabiliyet + bağlanabilir prop kaydından tanım. Block bulgusu varsa tanım yine döner
292
+ * (dryRun'da görülsün) ama çağıran yazmaz.
293
+ */
294
+ export function buildDefinition(input, describe, connectable, connectablePaths = [], itemProps = []) {
295
+ const findings = [];
296
+ const block = (ruleId, message) => { findings.push({ ruleId, severity: 'block', message }); };
297
+ const warn = (ruleId, message) => { findings.push({ ruleId, severity: 'warn', message }); };
298
+ const visible = describe.fields.filter(field => field.access !== 'hidden');
299
+ const visibleSlugs = visible.map(field => field.slug);
300
+ const primaryKey = describe.source?.primaryKey ?? undefined;
301
+ const isListComponent = Boolean(connectable?.fieldsFrom);
302
+ if (!connectable) {
303
+ block('CD-001', `Bu prop veri kaynağına bağlanamaz. Bağlanabilir prop'lar: ${connectablePaths.join(' · ') || '(yok — component_get.connectData boş)'}.`);
304
+ }
305
+ const removed = new Set((describe.drift ?? []).filter(item => item.removed && item.was).map(item => String(item.was)));
306
+ const isDrifted = (path) => Boolean(input.fixDrift) && removed.has(path.split('.')[0] ?? path);
307
+ const keptSlugs = (slugs) => slugs?.filter(slug => !isDrifted(slug));
308
+ const roles = Object.fromEntries(Object.entries(input.roles ?? defaultMapping(connectable, describe)).filter(([, slug]) => !isDrifted(slug)));
309
+ for (const role of connectable?.required ?? []) {
310
+ if (roles[role])
311
+ continue;
312
+ const roleType = connectable?.roles?.[role] ?? 'any';
313
+ const candidates = roleCandidates(roleType, describe.fields).map(field => field.slug);
314
+ block('CD-001', `'${role}' rolü zorunlu ama eşlenemedi. Aday alanlar: ${candidates.join(', ') || '(yok)'} — roles: { ${role}: '<alan>' } verin.`);
315
+ }
316
+ for (const [role, slug] of Object.entries(roles)) {
317
+ if (connectable?.roles && !(role in connectable.roles))
318
+ block('CD-001', `'${role}' bu prop'un rolü değil. Roller: ${Object.keys(connectable.roles).join(' · ')}.`);
319
+ if (!visibleSlugs.includes(slug.split('.')[0] ?? slug))
320
+ block('CD-004', `'${role}' rolü kaynakta olmayan ya da politikada gizli '${slug}' alanına eşlenmiş.`);
321
+ const mismatch = roleTypeMismatch(connectable?.roles?.[role], slug, describe.fields);
322
+ if (mismatch)
323
+ warn('CD-016', `'${role}' rolü ${connectable?.roles?.[role]} bekliyor ama '${slug}' ${mismatch.type}: bileşen değeri beklediği gibi çizmeyebilir. Uyan tipler: ${mismatch.expected.join(' · ')}.`);
324
+ }
325
+ if ((describe.drift ?? []).some(item => item.idMissing)) {
326
+ warn('CD-008', 'Tanım alan KİMLİĞİ taşımıyor (eski bağlantı): alan yalnız adıyla bulunur, kaynakta yeniden adlandırma izlenemez ve sayfa sessizce boş kolona düşer. Bu çağrı tanımı kimliklerle yeniden yazar.');
327
+ }
328
+ const requestedFields = keptSlugs(input.fields) ?? [];
329
+ for (const slug of requestedFields) {
330
+ if (!visibleSlugs.includes(slug))
331
+ block('CD-004', `'${slug}' kaynakta yok ya da politikada gizli. Alanlar: ${visibleSlugs.join(' · ')}.`);
332
+ }
333
+ const referencedRoots = [...(input.fields ?? []), ...(input.edit?.editable ?? []), ...(input.filterable ?? []), ...(input.searchable ?? []), ...(input.sort ?? []).map(item => item.field), ...Object.values(input.roles ?? {}), ...Object.keys(input.expand ?? {}), ...filterFieldPaths(input.filter)]
334
+ .map(path => path.split('.')[0] ?? path);
335
+ for (const slug of [...new Set(referencedRoots)].filter(item => removed.has(item))) {
336
+ warn('CD-008', `'${slug}' kaynaktan silinmiş (drift)${input.fixDrift ? ' — fixDrift ile tanımdan düşürüldü (alanlar, expand, filtre koşulları, editable, filterable/searchable, sıralama ve roller)' : '; fixDrift: true ile düşürün'}.`);
337
+ }
338
+ const roleRoots = Object.values(roles).map(path => path.split('.')[0] ?? path);
339
+ const roots = input.fields
340
+ ? requestedFields.filter(slug => visibleSlugs.includes(slug))
341
+ : isListComponent
342
+ ? visible.map(field => field.slug)
343
+ : [...new Set([...(primaryKey && visibleSlugs.includes(primaryKey) ? [primaryKey] : []), ...roleRoots.filter(slug => visibleSlugs.includes(slug))])];
344
+ for (const [relation, targets] of Object.entries(input.expand ?? {})) {
345
+ const field = visible.find(item => item.slug === relation);
346
+ if (!field?.relation)
347
+ block('CD-004', `expand yalnız ilişki alanında: '${relation}' ilişki değil ya da yok.`);
348
+ if (!targets.length)
349
+ block('CD-004', `'${relation}' expand listesi boş.`);
350
+ }
351
+ const fieldEntries = [...roots, ...Object.keys(input.expand ?? {}).filter(slug => !roots.includes(slug) && !isDrifted(slug))].map(slug => {
352
+ const field = describe.fields.find(item => item.slug === slug);
353
+ const expand = input.expand?.[slug];
354
+ return {
355
+ ...(field?.fieldId ? { id: field.fieldId } : {}),
356
+ slug,
357
+ ...(expand?.length ? { expand: expand.map(target => ({ slug: target })) } : {})
358
+ };
359
+ });
360
+ const display = describe.source?.display ?? undefined;
361
+ const displayField = visible.find(field => field.slug === display);
362
+ const sort = input.sort?.filter(item => !isDrifted(item.field)) ?? (displayField?.sortable && roots.includes(displayField.slug) ? [{ field: displayField.slug, ascending: true }] : []);
363
+ const switches = Object.fromEntries(EDIT_KEYS.map(key => [key, input.edit?.[key] === true]));
364
+ const anyEdit = EDIT_KEYS.some(key => switches[key]);
365
+ const writableRoots = roots.filter(slug => {
366
+ const field = describe.fields.find(item => item.slug === slug);
367
+ return Boolean(field?.canWrite) && !field?.isKey && field?.type !== 'file';
368
+ });
369
+ if (EDIT_KEYS.filter(key => key !== 'bulk').some(key => switches[key]) && describe.source?.isSystem)
370
+ block('CD-003', `Sistem tablosu '${describe.source.slug ?? describe.source.id}' düzenlemeye açılamaz: edit anahtarlarını kapatın.`);
371
+ for (const finding of editFindings({ ...switches, editable: input.edit?.editable }, describe))
372
+ warn(finding.ruleId, finding.message);
373
+ // CD-018 (38/M-4): platform kaydı 422 connect-data-editable-file ile reddeder (ConnectDataService.ValidateSourcesAsync);
374
+ // dryRun bunu görmüyordu ve gerçek yazmada ilk kez öğreniliyordu. Dosya hücresi bağlı bileşende salt okunurdur.
375
+ for (const slug of (keptSlugs(input.edit?.editable) ?? []).filter(item => describe.fields.find(field => field.slug === item)?.type === 'file')) {
376
+ block('CD-018', `'${slug}' dosya alanı: edit.editable'a konamaz — bağlı bileşen dosyayı satır içinde yazmaz, platform kaydı 422 connect-data-editable-file ile reddeder. '${slug}'i editable'dan çıkarın; dosyayı file_upload + row_update (ya da bir microservice) yazar.`);
377
+ }
378
+ for (const slug of [...(keptSlugs(input.filterable) ?? []), ...(keptSlugs(input.searchable) ?? [])]) {
379
+ const field = describe.fields.find(item => item.slug === slug);
380
+ if (field && (field.canFilter === false || (field.operators ?? []).length === 0)) {
381
+ block('CD-002', `'${slug}' süzülemez (canFilter yok ya da işleç yok); filterable/searchable'a konamaz.`);
382
+ }
383
+ }
384
+ for (const item of sort) {
385
+ const field = describe.fields.find(entry => entry.slug === item.field);
386
+ if (field && field.sortable === false) {
387
+ block('CD-002', `'${item.field}' bu kaynakta sıralanamaz (sortable yok): ${unsortableReason(input.source.kind)}`);
388
+ }
389
+ }
390
+ for (const slug of keptSlugs(input.searchable) ?? []) {
391
+ const field = describe.fields.find(item => item.slug === slug);
392
+ if (field && field.canFilter !== false && (field.type !== 'string' || field.isProtected || !(field.operators ?? []).includes('icontains'))) {
393
+ block('CD-002', `'${slug}' aranamaz: arama yalnız korumasız, içerir (icontains) işleci olan metin alanında çalışır — platform bu alanı aramada YOK SAYAR (geçerli hiç alan kalmazsa 422 ConnectDataFilterNotAllowed). Aranabilir bir metin alanı seçin.`);
394
+ }
395
+ }
396
+ const definition = {
397
+ version: 1,
398
+ source: { kind: input.source.kind, id: input.source.id, ...(input.source.slug ?? describe.source?.slug ? { slug: input.source.slug ?? describe.source?.slug } : {}) },
399
+ fields: fieldEntries,
400
+ filter: input.fixDrift ? dropFilterPaths(input.filter, isDrifted) : input.filter ?? null,
401
+ sort,
402
+ page: { mode: input.pageMode ?? (describe.source?.pageMode === 'client' ? 'client' : 'offset'), limit: input.pageSize ?? CONNECT_DATA_DEFAULT_PAGE_SIZE },
403
+ language: input.language ?? 'request',
404
+ mapping: { roles, ...(isListComponent ? { columns: roots.map(slug => ({ field: slug })) } : {}) },
405
+ behavior: {
406
+ filterable: keptSlugs(input.filterable) ?? [],
407
+ searchable: keptSlugs(input.searchable) ?? [],
408
+ edit: {
409
+ ...switches,
410
+ saveMode: input.edit?.saveMode ?? 'row',
411
+ editable: keptSlugs(input.edit?.editable) ?? (anyEdit ? writableRoots : []),
412
+ defaults: input.edit?.defaults ?? {},
413
+ writeMode: input.edit?.writeMode ?? 'atomic'
414
+ },
415
+ refresh: { afterWrite: input.refresh?.afterWrite ?? 'reload', poll: input.refresh?.poll ?? null }
416
+ },
417
+ ...(input.params && Object.keys(input.params).length > 0 ? { params: input.params } : {})
418
+ };
419
+ const declaredParameters = (describe.source?.parameters ?? []).map(parameter => parameter.name);
420
+ for (const name of Object.keys(input.params ?? {}).filter(item => !declaredParameters.some(declared => declared.toLowerCase() === item.toLowerCase()))) {
421
+ block('CD-013', `params.${name}: '${describe.source?.slug ?? describe.source?.id}' böyle bir parametre almaz. Alınanlar: ${declaredParameters.join(' · ') || '(yok)'} (describe source.parameters; sayfalama, sıralama, dinamik filtre ve dil parametreleri motorundur).`);
422
+ }
423
+ if (input.pageMode && input.pageMode !== 'client' && describe.source?.pageMode === 'client') {
424
+ block('CD-013', `page.mode '${input.pageMode}': '${describe.source.slug ?? describe.source.id}' sunucuda sayfalamaz (describe source.pageMode client), motor offset/cursor okumaz. Tek seçenek 'client'.`);
425
+ }
426
+ if (describe.source?.pageMode === 'client') {
427
+ warn('CD-007', `'${describe.source.slug ?? describe.source.id}' sunucuda sayfalamaz (page.mode client): runtime satırları tek okumada (en çok 1000) alır, bileşenin süzme, arama ve sayfalaması tarayıcıda yapılır; sunucuya offset yok sayılır, 1000'den büyük sonuç eksik görünür.`);
428
+ }
429
+ if (roles.category && roles.value && !connectable?.fieldsFrom) {
430
+ warn('CD-015', `Rol eşlemesi bileşenin çizim ayarlarını kapsamaz: kategori ('${roles.category}') satır başına tekrar ediyorsa bileşenin 'aggregate' ayarı 'none' olduğu sürece her satır ayrı nokta çizilir — toplamak için aggregate'i (sum/avg/count) elle ayarlayın.`);
431
+ }
432
+ if (roles.children) {
433
+ warn('CD-009', `Ağaç (children ← '${roles.children}'): runtime tüm satırları tek istekte okur, tavan 1000; daha büyük tabloda ağaç eksik çizilir ve üstü gelmeyen satır köke düşer.`);
434
+ }
435
+ if (input.filter)
436
+ findings.push(...filterFindings(input.filter, describe));
437
+ for (const issue of connectDataShapeIssues(definition))
438
+ block(issue.code, `${issue.path}: ${issue.message} (${issue.rule})`);
439
+ const columns = isListComponent
440
+ ? input.columns === undefined || input.columns === 'auto'
441
+ ? columnsFromDescribe(describe, selectedPaths(definition).filter(path => roots.includes(path)), itemProps, definition.behavior?.filterable ?? [])
442
+ : input.columns
443
+ : null;
444
+ return { definition, columns, findings };
445
+ }
446
+ /** Tanımın filtre değerlerinde ve `params`'ında geçen `$ad` sayfa durumu adları (screen-builder `stateReferenceNames` aynası). */
447
+ export function stateReferenceNames(definition) {
448
+ const names = [];
449
+ const add = (value) => {
450
+ if (typeof value === 'string' && /^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value) && !names.includes(value.slice(1)))
451
+ names.push(value.slice(1));
452
+ };
453
+ const walk = (filter) => {
454
+ if (!filter)
455
+ return;
456
+ if (filter.filterType === 'group')
457
+ filter.children?.forEach(walk);
458
+ else
459
+ add(filter.value);
460
+ };
461
+ walk(definition.filter);
462
+ Object.values(definition.params ?? {}).forEach(add);
463
+ return names;
464
+ }
465
+ /**
466
+ * CD-014: `$ad` sayfa ya da uygulama değişkenlerinde (page_state) yoksa runtime değeri `null` gönderir — filtre hiçbir
467
+ * satırı eşlemez, parametre boş gider. Değişken listesi okunamadıysa (`null`) bulgu üretilmez.
468
+ */
469
+ export function stateReferenceFindings(definition, stateNames) {
470
+ if (!stateNames)
471
+ return [];
472
+ return stateReferenceNames(definition)
473
+ .filter(name => !stateNames.includes(name))
474
+ .map(name => ({ ruleId: 'CD-014', severity: 'warn', message: `'$${name}' sayfa/uygulama değişkenlerinde yok (page_state): runtime değeri null gönderir. variable_create ile tanımlayın ya da adı düzeltin. Tanımlılar: ${stateNames.join(' · ') || '(yok)'}.` }));
475
+ }
476
+ /** Önizleme isteğinin `params`'ında karşılığı olmayan `$ad` adları — platform bunları null bağlar. */
477
+ export function unboundStateNames(definition, params) {
478
+ return stateReferenceNames(definition).filter(name => !Object.hasOwn(params ?? {}, name));
479
+ }
480
+ /**
481
+ * CD-014 önizleme dalı (38/M-8): runtime `$ad`'ı sayfa durumundan doldurur, `/connect-data/preview` doldurmaz —
482
+ * değer `request.params`'tan gelmezse null bağlanır ve parametreli query `WHERE fiyat >= NULL` ile 0 satır döner;
483
+ * yanıt bunu hiçbir yerde söylemiyordu. Sayfa değişkeni okunabildiyse başlangıç değeri öneri olarak yazılır.
484
+ */
485
+ export function previewStateFindings(names, state) {
486
+ return names.map(name => {
487
+ const entry = state?.find(item => item.key === name);
488
+ const hint = entry === undefined
489
+ ? ''
490
+ : ` Sayfa/uygulama değişkeninin başlangıç değeri ${JSON.stringify(entry.value)}: request.params: { ${name}: ${JSON.stringify(entry.value)} } ile runtime'ın ilk açılışını önizlersiniz.`;
491
+ return { ruleId: 'CD-014', severity: 'warn', message: `'$${name}' önizleme isteğinde değersiz: runtime değeri sayfa durumundan doldurur, önizleme doldurmaz — request.params.${name} verilmezse platform null bağlar ve sonuç sessizce boş ya da eksik döner.${hint}` };
492
+ });
493
+ }
494
+ /** Prop microservice'e çevrilmişse (`{ version: 1, microserviceId }`, b-34) microservice id'si. */
495
+ export function microserviceReferenceId(ref) {
496
+ const value = ref;
497
+ return value && typeof value === 'object' && !value.source && typeof value.microserviceId === 'string' ? value.microserviceId : null;
498
+ }
499
+ /**
500
+ * Kolon öğeleri her seviyede Prop'a sarılır (Studio `mapRawPropToSchemaProp` biçimi): `colorMap` gibi
501
+ * iç içe dizi/nesne değerleri de sarılır, zaten sarmal (`{ type, data }`) değerlere dokunulmaz —
502
+ * platform düz iç değeri JsonException ile reddeder (normalize S-001).
503
+ */
504
+ export function staticListItems(items) {
505
+ const wrapped = structuredClone(items);
506
+ deepWrapPropData(wrapped);
507
+ return wrapped;
508
+ }
509
+ /**
510
+ * Var olan bağlantıyı yeniden bağlarken verilmeyen parçalar kayıtlı tanımdan taşınır: alanlar (liste
511
+ * bileşeninde kolon sırası), expand, roller, filtre, sıralama, sayfa boyu, dil, filtre/arama
512
+ * listeleri, düzenleme ve yenileme. Kaynak değiştiyse hiçbir şey taşınmaz. `keepColumns`: alan listesi
513
+ * aynı kaldı ve `columns` verilmedi — kolon listesi yeniden yazılmaz (genişlik, biçim, başlık korunur).
514
+ * `fixDrift` verildiyse kolonlar HER ZAMAN yeniden yazılır: alan listesi girdide hâlâ ölü alanı
515
+ * taşıdığı için burada "değişmedi" görünür, tanımdan düşme daha sonra (`buildDefinition`) olur ve
516
+ * kolon listesi dokunulmazsa bileşen silinmiş alanın kolonunu çizmeye devam ederdi (koşu bulgusu S-7).
517
+ */
518
+ export function inputFromExisting(input, existing) {
519
+ if (!existing?.source || existing.source.id !== input.source.id)
520
+ return { input, keepColumns: false };
521
+ const existingFields = (existing.mapping?.columns?.length
522
+ ? existing.mapping.columns.map(column => String(column.field ?? ''))
523
+ : (existing.fields ?? []).map(field => field.slug)).filter(slug => slug && !slug.includes('.'));
524
+ const existingExpand = Object.fromEntries((existing.fields ?? [])
525
+ .filter(field => field.expand?.length)
526
+ .map(field => [field.slug, (field.expand ?? []).map(item => item.slug)]));
527
+ const edit = existing.behavior?.edit;
528
+ const existingEdit = edit
529
+ ? {
530
+ create: edit.create ?? false,
531
+ update: edit.update ?? false,
532
+ delete: edit.delete ?? false,
533
+ bulk: edit.bulk ?? false,
534
+ ...(edit.saveMode === 'row' || edit.saveMode === 'cell' || edit.saveMode === 'batch' ? { saveMode: edit.saveMode } : {}),
535
+ editable: [...(edit.editable ?? [])],
536
+ defaults: { ...(edit.defaults ?? {}) },
537
+ ...(edit.writeMode ? { writeMode: edit.writeMode } : {})
538
+ }
539
+ : undefined;
540
+ const merged = {
541
+ ...input,
542
+ fields: input.fields ?? existingFields,
543
+ expand: input.expand ?? (Object.keys(existingExpand).length > 0 ? existingExpand : undefined),
544
+ roles: input.roles ?? (Object.keys(existing.mapping?.roles ?? {}).length > 0 ? { ...(existing.mapping?.roles ?? {}) } : undefined),
545
+ filter: input.filter !== undefined ? input.filter : existing.filter ?? null,
546
+ sort: input.sort ?? (existing.sort ?? []).map(item => ({ field: item.field, ascending: item.ascending })),
547
+ pageSize: input.pageSize ?? existing.page?.limit,
548
+ params: input.params ?? (existing.params && Object.keys(existing.params).length > 0 ? { ...existing.params } : undefined),
549
+ language: input.language ?? existing.language ?? undefined,
550
+ filterable: input.filterable ?? [...(existing.behavior?.filterable ?? [])],
551
+ searchable: input.searchable ?? [...(existing.behavior?.searchable ?? [])],
552
+ edit: input.edit ? { ...existingEdit, ...input.edit } : existingEdit,
553
+ refresh: input.refresh ?? (existing.behavior?.refresh
554
+ ? {
555
+ ...(existing.behavior.refresh.afterWrite === 'reload' || existing.behavior.refresh.afterWrite === 'merge' ? { afterWrite: existing.behavior.refresh.afterWrite } : {}),
556
+ poll: existing.behavior.refresh.poll ?? null
557
+ }
558
+ : undefined)
559
+ };
560
+ const keepColumns = input.columns === undefined && !input.fixDrift && JSON.stringify(merged.fields) === JSON.stringify(existingFields) && JSON.stringify(merged.expand ?? {}) === JSON.stringify(existingExpand);
561
+ return { input: merged, keepColumns };
562
+ }
563
+ /** Describe'ın bildirdiği yeniden adlandırmalar (kayıtlı yol → güncel yol); silinen alan girmez. */
564
+ export function renamesFromDrift(drift) {
565
+ return Object.fromEntries((drift ?? [])
566
+ .filter(item => !item.removed && item.was && item.now && item.was !== item.now)
567
+ .map(item => [String(item.was), String(item.now)]));
568
+ }
569
+ export function invertRenames(renames) {
570
+ return Object.fromEntries(Object.entries(renames).map(([recorded, current]) => [current, recorded]));
571
+ }
572
+ /** Yolun yeni adı: tam eşleşme, sonra en uzun yeniden adlanmış önek (backend `RenamePath`, screen-builder `renamePath` aynası). */
573
+ export function renamePath(path, renames) {
574
+ const has = (key) => Object.prototype.hasOwnProperty.call(renames, key);
575
+ if (!path || Object.keys(renames).length === 0)
576
+ return path;
577
+ if (has(path))
578
+ return renames[path];
579
+ for (let cut = path.lastIndexOf('.'); cut > 0; cut = path.lastIndexOf('.', cut - 1)) {
580
+ if (has(path.slice(0, cut)))
581
+ return renames[path.slice(0, cut)] + path.slice(cut);
582
+ }
583
+ return path;
584
+ }
585
+ /**
586
+ * Tanımdaki bütün alan yollarını yeniden adlandırır (backend `WithRenamedPaths` aynası): alan/expand slug'ları, filtre,
587
+ * sıralama, roller, kolon eşlemesi, filterable/searchable, editable ve varsayılan anahtarları; lookup hedefe göredir, kalır.
588
+ * Kaynakta yeniden adlanan alan tanımda `id` ile eşleşir: bağlama güncel adlarla kurulur, kayıtlı adlarla yazılır.
589
+ */
590
+ export function renameDefinitionPaths(definition, renames) {
591
+ if (Object.keys(renames).length === 0)
592
+ return definition;
593
+ const rename = (path) => renamePath(path, renames);
594
+ const renameFields = (fields, prefix) => fields?.map(field => {
595
+ if (!field?.slug)
596
+ return field;
597
+ const path = prefix === null ? field.slug : `${prefix}.${field.slug}`;
598
+ const expand = renameFields(field.expand, path);
599
+ return { ...field, slug: rename(path).split('.').pop() ?? field.slug, ...(expand ? { expand } : {}) };
600
+ });
601
+ const renameFilter = (filter) => !filter
602
+ ? filter
603
+ : { ...filter, ...(typeof filter.field === 'string' ? { field: rename(filter.field) } : {}), ...(filter.children ? { children: filter.children.map(child => renameFilter(child)) } : {}) };
604
+ const edit = definition.behavior?.edit;
605
+ return {
606
+ ...definition,
607
+ fields: renameFields(definition.fields, null) ?? [],
608
+ ...(definition.filter !== undefined ? { filter: renameFilter(definition.filter) } : {}),
609
+ ...(definition.sort ? { sort: definition.sort.map(item => ({ ...item, field: rename(item.field) })) } : {}),
610
+ ...(definition.mapping
611
+ ? {
612
+ mapping: {
613
+ ...definition.mapping,
614
+ ...(definition.mapping.roles ? { roles: Object.fromEntries(Object.entries(definition.mapping.roles).map(([role, path]) => [role, rename(path)])) } : {}),
615
+ ...(definition.mapping.columns ? { columns: definition.mapping.columns.map(column => (typeof column.field === 'string' ? { ...column, field: rename(column.field) } : column)) } : {})
616
+ }
617
+ }
618
+ : {}),
619
+ ...(definition.behavior
620
+ ? {
621
+ behavior: {
622
+ ...definition.behavior,
623
+ ...(definition.behavior.filterable ? { filterable: definition.behavior.filterable.map(rename) } : {}),
624
+ ...(definition.behavior.searchable ? { searchable: definition.behavior.searchable.map(rename) } : {}),
625
+ ...(edit
626
+ ? {
627
+ edit: {
628
+ ...edit,
629
+ ...(edit.editable ? { editable: edit.editable.map(rename) } : {}),
630
+ ...(edit.defaults ? { defaults: Object.fromEntries(Object.entries(edit.defaults).map(([path, value]) => [rename(path), value])) } : {})
631
+ }
632
+ }
633
+ : {})
634
+ }
635
+ }
636
+ : {})
637
+ };
638
+ }
639
+ /**
640
+ * Yeniden bağlamada kurulan tanıma kayıtlı tanımın alan ayrıntıları taşınır: aynı slug'lı alanın kimliği, `lookup`
641
+ * ayarı ve expand alt alanlarının kimlikleri (girdi bunları taşımaz; taşınmazsa lookup ayarı sessizce kaybolurdu).
642
+ */
643
+ export function carryFieldDetails(definition, previous) {
644
+ if (!previous)
645
+ return definition;
646
+ return {
647
+ ...definition,
648
+ fields: definition.fields.map(field => {
649
+ const saved = (previous.fields ?? []).find(item => item.slug === field.slug);
650
+ if (!saved)
651
+ return field;
652
+ return {
653
+ ...(field.id ?? saved.id ? { id: field.id ?? saved.id } : {}),
654
+ ...field,
655
+ ...(field.expand ? { expand: field.expand.map(child => (child.id || !saved.expand?.find(item => item.slug === child.slug)?.id ? child : { id: saved.expand.find(item => item.slug === child.slug)?.id, ...child })) } : {}),
656
+ ...(!field.lookup && saved.lookup ? { lookup: saved.lookup } : {})
657
+ };
658
+ })
659
+ };
660
+ }
661
+ /** Expand alt alanlarına, tanımla alınan describe'ın bildirdiği hedef alan kimliklerini yazar (`sube.ad` → fieldId). */
662
+ export function stampExpandIds(definition, describe) {
663
+ const stamp = (fields, prefix) => fields?.map(child => {
664
+ const path = `${prefix}.${child.slug}`;
665
+ const fieldId = describe.fields.find(item => item.slug === path)?.fieldId;
666
+ const expand = stamp(child.expand, path);
667
+ return { ...(child.id || !fieldId ? {} : { id: fieldId }), ...child, ...(expand ? { expand } : {}) };
668
+ });
669
+ return { ...definition, fields: definition.fields.map(field => (field.expand ? { ...field, expand: stamp(field.expand, field.slug) } : field)) };
670
+ }
671
+ /** Kabiliyetin alanları tanımın KAYITLI adlarıyla (describe drift'i): kayıtlı adla yazılmış filtre ve satırlar bununla denetlenir. */
672
+ export function capabilityUnderRecordedNames(describe) {
673
+ const recorded = invertRenames(renamesFromDrift(describe.drift));
674
+ return Object.keys(recorded).length === 0 ? describe : { ...describe, fields: describe.fields.map(field => ({ ...field, slug: renamePath(field.slug, recorded) })) };
675
+ }
676
+ /** Bind girdisindeki yolları (alanlar, roller, expand, filtre, sıralama, filterable/searchable, edit) yeniden adlandırır. */
677
+ export function renameBindInput(input, renames) {
678
+ if (Object.keys(renames).length === 0)
679
+ return input;
680
+ const rename = (path) => renamePath(path, renames);
681
+ const renameFilter = (filter) => ({
682
+ ...filter,
683
+ ...(typeof filter.field === 'string' ? { field: rename(filter.field) } : {}),
684
+ ...(Array.isArray(filter.children) ? { children: filter.children.map(renameFilter) } : {})
685
+ });
686
+ return {
687
+ ...input,
688
+ ...(input.fields ? { fields: input.fields.map(rename) } : {}),
689
+ ...(input.roles ? { roles: Object.fromEntries(Object.entries(input.roles).map(([role, path]) => [role, rename(path)])) } : {}),
690
+ ...(input.expand ? { expand: Object.fromEntries(Object.entries(input.expand).map(([field, targets]) => [rename(field), targets])) } : {}),
691
+ ...(input.filter ? { filter: renameFilter(input.filter) } : {}),
692
+ ...(input.sort ? { sort: input.sort.map(item => ({ ...item, field: rename(item.field) })) } : {}),
693
+ ...(input.filterable ? { filterable: input.filterable.map(rename) } : {}),
694
+ ...(input.searchable ? { searchable: input.searchable.map(rename) } : {}),
695
+ ...(input.edit
696
+ ? {
697
+ edit: {
698
+ ...input.edit,
699
+ ...(input.edit.editable ? { editable: input.edit.editable.map(rename) } : {}),
700
+ ...(input.edit.defaults ? { defaults: Object.fromEntries(Object.entries(input.edit.defaults).map(([path, value]) => [rename(path), value])) } : {})
701
+ }
702
+ }
703
+ : {})
704
+ };
705
+ }
706
+ /** Üretilen kolonların alan adı prop'larını (name/field/key/value) yeniden adlandırır: kolonlar sayfanın gördüğü kayıtlı adla yazılır. */
707
+ export function renameColumnItems(columns, renames) {
708
+ return columns?.map(item => Object.fromEntries(Object.entries(item).map(([key, value]) => [key, ['name', 'field', 'key', 'value'].includes(key) && typeof value === 'string' ? renamePath(value, renames) : value]))) ?? null;
709
+ }
710
+ /**
711
+ * Kayıtlı kolon listesini yeni seçimle birleştirir (screen-builder `mergeSchemaFieldList` aynası): seçili kalan alanın
712
+ * kolonu yerinde ve ayarlarıyla durur, seçimden çıkan kaynak alanının kolonu düşer, kaynakta olmayan kolon kalır,
713
+ * yeni alanlar sona eklenir. Kayıtlı liste yoksa üretilen döner.
714
+ */
715
+ export function mergeColumnItems(saved, generated, sourceNames) {
716
+ const savedItems = saved?.data;
717
+ if (!Array.isArray(savedItems))
718
+ return generated;
719
+ const items = staticListItems(savedItems);
720
+ const nameOf = (item) => {
721
+ const key = ['name', 'field', 'key', 'value'].find(name => typeof item?.[name]?.data === 'string');
722
+ return key ? String((item?.[key]).data) : undefined;
723
+ };
724
+ const wanted = generated.map(nameOf);
725
+ const kept = items.filter(item => {
726
+ const name = nameOf(item);
727
+ return name === undefined || wanted.includes(name) || !sourceNames.includes(name);
728
+ });
729
+ const keptNames = kept.map(nameOf);
730
+ return [...kept, ...generated.filter(item => !keptNames.includes(nameOf(item)))];
731
+ }
732
+ /**
733
+ * Pano/seçenek listesi olan bileşende (kanban `columns`) rolün enum değerlerinden kolonlar: bağlanan
734
+ * prop'tan BAŞKA, `value` ve `label` isteyen bağlanabilir prop hedeftir; öğe anahtarları o prop'un
735
+ * kendi rollerinden gelir (`value`/`label`/`color`). Kaynağın enum'u yoksa ya da böyle bir prop yoksa
736
+ * hiçbir şey yazılmaz — bileşenin örnek kolonları kalır ve kartlar hiçbir kolona düşmez (koşu bulgusu S-3).
737
+ */
738
+ export function enumOptionItems(connectData, propPath, definition, describe) {
739
+ const target = Object.entries(connectData?.props ?? {}).find(([path, entry]) => path !== propPath && (entry.required ?? []).includes('value') && (entry.required ?? []).includes('label'));
740
+ if (!target)
741
+ return undefined;
742
+ const roles = definition.mapping?.roles ?? {};
743
+ const slug = roles.column ?? Object.values(roles).find(field => (describe.fields.find(item => item.slug === field)?.enum ?? []).length > 0);
744
+ const values = describe.fields.find(item => item.slug === slug)?.enum ?? [];
745
+ if (values.length === 0)
746
+ return undefined;
747
+ const keys = Object.keys(target[1].roles ?? {});
748
+ return {
749
+ prop: target[0],
750
+ items: values.map(value => ({
751
+ ...(keys.includes('value') ? { value: value.value } : {}),
752
+ ...(keys.includes('label') ? { label: value.label || value.value } : {}),
753
+ ...(keys.includes('color') && value.color ? { color: value.color } : {})
754
+ }))
755
+ };
756
+ }
757
+ /**
758
+ * Pano kolonu seçenekleri yeniden bağlamada kayıtlı listeyle birleşir (M-6): sıra ve küme enum değerlerinden gelir
759
+ * (kaynaktan silinen değer düşer, yenisi eklenir); aynı `value`'lu kayıtlı öğenin öbür alanları (elle girilmiş
760
+ * `wipLimit`, başlık, renk) korunur. Kayıtlı liste yoksa üretilen liste olduğu gibi (sarmallı) döner.
761
+ */
762
+ export function mergeOptionItems(saved, items) {
763
+ const savedItems = (Array.isArray(saved?.data) ? saved.data : [])
764
+ .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item));
765
+ const valueOf = (value) => (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value ? value.data : value);
766
+ return staticListItems(items).map((wrapped, index) => {
767
+ const previous = savedItems.find(item => String(valueOf(item.value)) === String(items[index]?.value));
768
+ if (!previous)
769
+ return wrapped;
770
+ const { value: _value, ...kept } = previous;
771
+ return { ...wrapped, ...staticListItems([kept])[0] };
772
+ });
773
+ }
774
+ /**
775
+ * Tek batch: bağla (`set_connect_data`), liste bileşeninde öğe listesini, `fields` kaydı olan
776
+ * bileşende kardeş alan adı prop'larını yaz. Sıra önemlidir: bağlama önce. `merge` verilirse kolon listesi kayıtlı
777
+ * listeyle birleştirilir (aynı kaynağa yeniden bağlama: sıra ve kolon ayarları korunur).
778
+ */
779
+ export function bindOps(target, propPath, definition, connectable, columns, includeColumns = true, merge, options) {
780
+ const ops = [{ op: 'set_connect_data', target, path: propPath, ref: definition }];
781
+ if (connectable?.fieldsFrom && columns && includeColumns) {
782
+ ops.push({ op: 'replace_prop', target, prop: connectable.fieldsFrom, value: merge ? mergeColumnItems(merge.saved, staticListItems(columns), merge.sourceNames) : staticListItems(columns) });
783
+ }
784
+ if (options)
785
+ ops.push({ op: 'replace_prop', target, prop: options.prop, value: staticListItems(options.items) });
786
+ for (const [role, prop] of Object.entries(connectable?.fields ?? {})) {
787
+ const field = definition.mapping?.roles?.[role];
788
+ if (field)
789
+ ops.push({ op: 'replace_prop', target, prop, value: field });
790
+ }
791
+ return ops;
792
+ }
793
+ /** Şema ağacında elementId ile düğüm. */
794
+ export function findSchemaNode(nodes, elementId) {
795
+ for (const node of nodes ?? []) {
796
+ if (node.elementId === elementId)
797
+ return node;
798
+ const found = findSchemaNode(Array.isArray(node.children) ? node.children : [], elementId);
799
+ if (found)
800
+ return found;
801
+ }
802
+ return undefined;
803
+ }
804
+ /**
805
+ * Prop yolundaki değer: sarmal (`{ type, data }`) ara adımlarda `data`'sından geçilir, dizi öğesi
806
+ * (anahtarları sarmal düz nesne) olduğu gibi okunur — `series.1.data`'nın son `data`'sı öğenin prop'udur.
807
+ */
808
+ export function readPropValue(props, propPath) {
809
+ let current = props;
810
+ for (const [index, segment] of propPath.split('.').entries()) {
811
+ if (current === null || typeof current !== 'object')
812
+ return undefined;
813
+ const holder = index > 0 && typeof current.type === 'string' ? current.data : current;
814
+ if (holder === null || typeof holder !== 'object')
815
+ return undefined;
816
+ current = Array.isArray(holder) ? holder[Number(segment)] : holder[segment];
817
+ }
818
+ return current && typeof current === 'object' ? current : undefined;
819
+ }