@appaflytech/zeyrek-ai-mcp 0.0.6 → 0.0.7

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/.env.example +2 -2
  2. package/.env.production.example +8 -0
  3. package/README.md +9 -8
  4. package/dist/config.js +18 -3
  5. package/dist/knowledge/backendConstraints.js +85 -0
  6. package/dist/knowledge/componentCatalog.js +357 -54
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/functionGuards.js +134 -0
  9. package/dist/knowledge/normalize.js +53 -2
  10. package/dist/knowledge/outlines.js +31 -0
  11. package/dist/knowledge/policy.js +14 -5
  12. package/dist/knowledge/rules.js +343 -1
  13. package/dist/platform/csv.js +123 -0
  14. package/dist/platform/errors.js +62 -3
  15. package/dist/platform/http.js +1 -0
  16. package/dist/platform/responses.js +16 -4
  17. package/dist/platform/screenshot.js +18 -1
  18. package/dist/resources/examples/flow-call-function.json +66 -0
  19. package/dist/resources/examples/function-total.json +70 -0
  20. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  21. package/dist/resources/examples/page-dynamic-form.json +1 -1
  22. package/dist/resources/examples/page-sign-in.json +1 -1
  23. package/dist/resources/guides/component.md +13 -1
  24. package/dist/resources/guides/entity.md +321 -17
  25. package/dist/resources/guides/expressions.md +3 -0
  26. package/dist/resources/guides/functions.md +262 -0
  27. package/dist/resources/guides/getting-started.md +5 -0
  28. package/dist/resources/guides/merge.md +231 -0
  29. package/dist/resources/guides/microfunction.md +166 -0
  30. package/dist/resources/guides/microservice.md +61 -2
  31. package/dist/resources/guides/modify-existing.md +23 -0
  32. package/dist/resources/guides/page.md +20 -4
  33. package/dist/resources/guides/translation.md +24 -0
  34. package/dist/resources/guides/workflow.md +1 -1
  35. package/dist/resources/schema-fingerprint.json +4 -4
  36. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  37. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  38. package/dist/resources/schemas/CreateBulkRowCommand.json +3 -0
  39. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  40. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  41. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  42. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  43. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  44. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  45. package/dist/resources/schemas/CreateMicroserviceCommand.json +119 -1
  46. package/dist/resources/schemas/CreateWorkflowCommand.json +13 -2
  47. package/dist/resources/schemas/DataJobDto.json +267 -0
  48. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  49. package/dist/resources/schemas/Filter.json +3 -1
  50. package/dist/resources/schemas/GetRowsQuery.json +3 -1
  51. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  52. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  53. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  54. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  55. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  56. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  57. package/dist/resources/schemas/PreviewMicroserviceCommand.json +119 -1
  58. package/dist/resources/schemas/PreviewWorkflowCommand.json +13 -2
  59. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  60. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  61. package/dist/resources/schemas/RunImportCommand.json +62 -0
  62. package/dist/resources/schemas/StepDto.json +13 -2
  63. package/dist/resources/schemas/UpdateBulkRowCommand.json +3 -0
  64. package/dist/resources/schemas/UpdateEntityCommand.json +3 -1
  65. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  66. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  67. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +119 -1
  69. package/dist/resources/schemas/UpdateWorkflowCommand.json +13 -2
  70. package/dist/server.js +15 -3
  71. package/dist/tools/activities.js +40 -0
  72. package/dist/tools/automations.js +14 -5
  73. package/dist/tools/common.js +16 -0
  74. package/dist/tools/dependencies.js +1 -1
  75. package/dist/tools/environments.js +52 -0
  76. package/dist/tools/files.js +51 -50
  77. package/dist/tools/flows.js +95 -2
  78. package/dist/tools/functions.js +331 -0
  79. package/dist/tools/importSchemas.js +79 -0
  80. package/dist/tools/imports.js +241 -0
  81. package/dist/tools/merges.js +113 -0
  82. package/dist/tools/microfunctions.js +155 -0
  83. package/dist/tools/microservices.js +42 -3
  84. package/dist/tools/pages.js +62 -10
  85. package/dist/tools/platformInfo.js +4 -1
  86. package/dist/tools/revisions.js +94 -0
  87. package/dist/tools/rows.js +342 -22
  88. package/dist/tools/screenWrite.js +11 -2
  89. package/dist/tools/templates.js +32 -5
  90. package/dist/tools/workflows.js +24 -6
  91. package/package.json +3 -1
@@ -1,5 +1,8 @@
1
+ import { applyPolicyMode } from './policy.js';
2
+ import { PlatformApiError } from '../platform/errors.js';
1
3
  import { reservedTenantTables } from './reservedTables.js';
2
4
  import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
5
+ import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
3
6
  import { backendPolicyRules } from './backendConstraints.js';
4
7
  const terminalStepTypes = new Set(['response', 'convert', 'revert']);
5
8
  const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
@@ -14,6 +17,7 @@ const stepKindMatrix = {
14
17
  workflow: ['workflow', 'automation', 'middleware'],
15
18
  email: ['workflow', 'automation', 'middleware'],
16
19
  'file-upload': ['workflow', 'automation', 'middleware'],
20
+ microfunction: ['workflow', 'automation', 'middleware'],
17
21
  variable: ['middleware'],
18
22
  'set-user': ['middleware'],
19
23
  setcookie: ['middleware'],
@@ -97,8 +101,19 @@ const flowBranchNames = {
97
101
  start: ['always'],
98
102
  microservice: ['always', 'success'],
99
103
  confirm: ['yes', 'no', 'always'],
104
+ function: ['always'],
100
105
  end: []
101
106
  };
107
+ /**
108
+ * Gateway'in tanıdığı sayfa akışı adım tipleri — frontend `FlowTaskType` enum'unun aynası
109
+ * (`ng-apps-flow/src/flow-execution.enums.ts`). `flowBranchNames[type] ?? ['always']` varsayımı
110
+ * her tipi kabul ettiği için sürüklenme ancak bu AÇIK listeyle ölçülür
111
+ * (`test/flow-step-parity.test.ts`). Frontend'e yeni tip girince buraya da yazılır.
112
+ */
113
+ export const knownFlowStepTypes = new Set([
114
+ 'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
115
+ 'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function'
116
+ ]);
102
117
  const flowFreeBranchTypes = new Set(['condition']);
103
118
  /** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
104
119
  const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
@@ -205,6 +220,21 @@ function flowCodeExpressions(step) {
205
220
  }
206
221
  return expressions;
207
222
  }
223
+ /**
224
+ * Kod ifadesi taşıyan her alan: `flowCodeExpressions` + condition kurallarının `expression`ı.
225
+ * FLOW-025 (`Functions.<ad>` çağrı taraması) bunu tarar; FLOW-011'in taradığı küme değişmez.
226
+ */
227
+ export function flowExpressionSites(step) {
228
+ const shaped = step;
229
+ const sites = flowCodeExpressions(shaped);
230
+ if (shaped.type === 'condition') {
231
+ for (const rule of asArray((shaped.args ?? {}).rules).filter((rule) => Boolean(rule) && typeof rule === 'object')) {
232
+ if (typeof rule.expression === 'string' && rule.expression.trim() !== '')
233
+ sites.push({ expression: rule.expression, origin: `rules['${String(rule.id ?? '?')}'].expression` });
234
+ }
235
+ }
236
+ return sites;
237
+ }
208
238
  const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
209
239
  /** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
210
240
  const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
@@ -224,7 +254,7 @@ function writesRolesWithoutSlug(entityId, parameters) {
224
254
  }
225
255
  const rolesSlugMessage = "Built-in 'roles' entity'sine slug YAZMADAN rol satırı ÜRETİLİYOR (operation create/upsert). Insert 200 döner, satır role_list'te görünür ve page.roleIds ile bağlanır — ama yetkilendirme SLUG karşılaştırır: platformun get-user sorgusu roles+user_roles join'inden slug dizisi üretir ve Authorized(user.roles) o rol için hiçbir zaman tutmaz. fields'a slug'ı ekleyin ('super-admin' slug'ını vermeyin — platform süper yöneticiyi o slug ile arar). Upsert'te slug'ı İFADEYLE üretmeyin: UPDATE yolunda ifade yeniden değerlendirilir ve var olan rolün slug'ı sessizce değişir (MS-016) — üretilen slug'ı yalnız create ucunda yazın. Rolü kullanıcıya bağlamak ayrı bir user_roles satırıdır.";
226
256
  const contractKeys = ['returnDtoId', 'parameterDtoId', 'returnBehavior', 'parameterBehavior'];
227
- const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith']);
257
+ const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith', 'isnull', 'isnotnull']);
228
258
  function flattenFilterTree(filters) {
229
259
  return filters
230
260
  .filter((filter) => Boolean(filter) && typeof filter === 'object')
@@ -316,6 +346,8 @@ function unreadSelectFields(step, steps) {
316
346
  .map(field => String(field.fieldSlug ?? field.slug ?? ''))
317
347
  .filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
318
348
  }
349
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
350
+ const functionsGuide = 'ngapps://guides/functions';
319
351
  export const policyRules = [
320
352
  {
321
353
  id: 'MS-001',
@@ -583,6 +615,38 @@ export const policyRules = [
583
615
  });
584
616
  }
585
617
  },
618
+ {
619
+ id: 'WF-MF-001',
620
+ module: 'workflow',
621
+ appliesTo: ['create', 'update', 'preview'],
622
+ sourceRef: "MicrofunctionStep.cs: rule.MicrofunctionId ile kayıt yüklenir, Status != Published ise FailedOutput(MicrofunctionNotPublishedException) — adım hata dalına düşer, akış 200 dönse bile iş yapılmaz. Ölçüldü 2026-09-10 (B4 doğrulaması: taslak fonksiyona bağlı uç 500 MicrofunctionNotPublishedException)",
623
+ check: definition => {
624
+ const body = workflowBody(definition);
625
+ return workflowSteps(body)
626
+ .filter(step => step.type === 'microfunction')
627
+ .flatMap((step) => {
628
+ const rule = (step.rule ?? {});
629
+ const microfunctionId = rule.microfunctionId;
630
+ if (!hasText(microfunctionId)) {
631
+ return [{
632
+ ruleId: 'WF-MF-001',
633
+ severity: 'block',
634
+ message: `'${stepLabel(step)}' microfunction adımında rule.microfunctionId yok — adım hangi fonksiyonu koşturacağını bilmez. microfunction_list(status: 'published') ile id seçin; argümanlar rule.expression'a yazılır (fonksiyonun parameterDto'su varsa o şemaya göre).`,
635
+ guide: 'ngapps://guides/microfunction'
636
+ }];
637
+ }
638
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(microfunctionId))) {
639
+ return [{
640
+ ruleId: 'WF-MF-001',
641
+ severity: 'block',
642
+ message: `'${stepLabel(step)}' adımının rule.microfunctionId'si GUID değil: '${String(microfunctionId)}'. Platform alanı Guid'e çeviremediği için 400 döner.`,
643
+ guide: 'ngapps://guides/microfunction'
644
+ }];
645
+ }
646
+ return [];
647
+ });
648
+ }
649
+ },
586
650
  {
587
651
  id: 'WF-006',
588
652
  module: 'workflow',
@@ -1704,6 +1768,203 @@ export const policyRules = [
1704
1768
  }];
1705
1769
  })
1706
1770
  },
1771
+ {
1772
+ id: 'FN-001',
1773
+ module: 'function',
1774
+ appliesTo: ['create', 'update'],
1775
+ sourceRef: "GlobalFunctionRules.cs IsValidName(name, source)/IsValidParameterName → 422 GlobalFunctionNameInvalid; function-source.ts isValidName — geçersiz ad iki dünyada da fonksiyonu ATLATIR. Kaynağa özel ayrılmış ad: studio 'Functions' (K1), workflow WorkflowReservedNames (motor globalleri)",
1776
+ check: definition => {
1777
+ const name = String(definition.name ?? '');
1778
+ const source = sourceOf(definition);
1779
+ const findings = [];
1780
+ const problem = nameProblem(definition.name, source);
1781
+ if (problem !== undefined) {
1782
+ findings.push({
1783
+ ruleId: 'FN-001',
1784
+ severity: 'block',
1785
+ message: source === 'workflow'
1786
+ ? `Fonksiyon adı '${name}' geçersiz: ${problem}. Ad /^[A-Za-z_$][A-Za-z0-9_$]*$/ kalıbına uymalı, JavaScript ayrılmış sözcüğü ve workflow motorunun global adı (system, input, Now, crypto…) olmamalı — platform 422 GlobalFunctionNameInvalid döner.`
1787
+ : `Fonksiyon adı '${name}' geçersiz: ${problem}. Ad /^[A-Za-z_$][A-Za-z0-9_$]*$/ kalıbına uymalı, JavaScript ayrılmış sözcüğü ve 'Functions' olmamalı — platform 422 GlobalFunctionNameInvalid döner; kayıt bir yolla geçse bile runtime böyle bir adı atlar ve Functions.${name} tanımsız kalır.`,
1788
+ guide: functionsGuide
1789
+ });
1790
+ }
1791
+ for (const parameter of asArray(definition.parameters).filter(isRecord)) {
1792
+ if (isValidParameterName(parameter.name))
1793
+ continue;
1794
+ findings.push({
1795
+ ruleId: 'FN-001',
1796
+ severity: 'block',
1797
+ message: `'${name}' fonksiyonunun parametre adı '${String(parameter.name ?? '')}' geçersiz (${nameProblem(parameter.name) ?? 'geçersiz'}) — platform 422 GlobalFunctionNameInvalid döner; runtime geçersiz parametre adlı fonksiyonu TÜMÜYLE atlar.`,
1798
+ guide: functionsGuide
1799
+ });
1800
+ }
1801
+ return findings;
1802
+ }
1803
+ },
1804
+ {
1805
+ id: 'FN-002',
1806
+ module: 'function',
1807
+ appliesTo: ['create', 'update'],
1808
+ sourceRef: 'CreateGlobalFunctionCommandValidator: parametre adı tekrarı → 422 GlobalFunctionParameterNameDuplicate; bildirim parametreleri konumsal bağlar, aynı ad ikinci değeri gölgeler',
1809
+ check: definition => {
1810
+ const names = asArray(definition.parameters).filter(isRecord).map(parameter => String(parameter.name ?? ''));
1811
+ const duplicates = [...new Set(names.filter((entry, index) => entry !== '' && names.indexOf(entry) !== index))];
1812
+ return duplicates.length === 0
1813
+ ? []
1814
+ : [{
1815
+ ruleId: 'FN-002',
1816
+ severity: 'block',
1817
+ message: `'${String(definition.name ?? '?')}' fonksiyonunda parametre adı tekrar ediyor: ${duplicates.join(', ')} — platform 422 GlobalFunctionParameterNameDuplicate döner. Çağrı konumsaldır (K5); aynı ad iki konumu birden alamaz.`,
1818
+ guide: functionsGuide
1819
+ }];
1820
+ }
1821
+ },
1822
+ {
1823
+ id: 'FN-003',
1824
+ module: 'function',
1825
+ appliesTo: ['create', 'update'],
1826
+ sourceRef: 'K15: fonksiyon başka fonksiyonu çağırabilir, tanım sırası önemsiz — kendini çağıran gövde durma koşulu yoksa sonsuz özyinelemedir; runCode RangeError\'ı yutar, sunucuda Jint LimitRecursion henüz yok (B-16, Faz 9)',
1827
+ check: definition => {
1828
+ const name = String(definition.name ?? '');
1829
+ const source = sourceOf(definition);
1830
+ const body = typeof definition.body === 'string' ? definition.body : '';
1831
+ if (name === '' || !callsItself(name, body, source))
1832
+ return [];
1833
+ return [{
1834
+ ruleId: 'FN-003',
1835
+ severity: 'warn',
1836
+ message: `'${name}' gövdesi ${callPrefix(source)}${name}(…) ile KENDİNİ çağırıyor. Durma koşulu yoksa ${source === 'workflow' ? 'Jint sınırsız özyinelemeye girer (B-16 sertleştirmesi Faz 9\'da)' : "tarayıcıda 'Maximum call stack size exceeded' (runCode yutar, hedef prop yazılmaz)"}. Bilinçli özyinelemeyse yok sayın.`,
1837
+ guide: functionsGuide
1838
+ }];
1839
+ }
1840
+ },
1841
+ {
1842
+ id: 'FN-004',
1843
+ module: 'function',
1844
+ appliesTo: ['create', 'update'],
1845
+ sourceRef: "K6: isAsync istemci semantiğidir — function-source.ts compiles(): await taşıyan senkron gövde derlenmez ve atlanır; {{ }} inline bağlama Promise'i [object Promise] çizer. source:'workflow' senkron Jint motorunda koşar: GlobalFunction.ValidateState isAsync'i reddeder (422 GlobalFunctionAsyncNotAllowed)",
1846
+ check: definition => {
1847
+ const name = String(definition.name ?? '?');
1848
+ const body = typeof definition.body === 'string' ? definition.body : '';
1849
+ const isAsync = definition.isAsync === true;
1850
+ if (isAsync && sourceOf(definition) === 'workflow') {
1851
+ return [{
1852
+ ruleId: 'FN-004',
1853
+ severity: 'block',
1854
+ message: `'${name}' source:'workflow' ama isAsync=true — workflow motoru senkron çalışır, async bildirimin dönüşü Promise olur ve code adımı '[object Promise]' görür; platform 422 GlobalFunctionAsyncNotAllowed döner. isAsync:false yapın; await gerekiyorsa bu bir studio fonksiyonudur.`,
1855
+ guide: functionsGuide
1856
+ }];
1857
+ }
1858
+ if (isAsync && !mentionsAwait(body)) {
1859
+ return [{
1860
+ ruleId: 'FN-004',
1861
+ severity: 'warn',
1862
+ message: `'${name}' isAsync=true ama gövdede await yok — async gereksiz ve bedeli var: {{ }} inline bağlamada dönüş Promise'dir ve ekrana '[object Promise]' yazılır. await gerekmiyorsa isAsync:false yapın.`,
1863
+ guide: functionsGuide
1864
+ }];
1865
+ }
1866
+ if (!isAsync && mentionsAwait(body)) {
1867
+ return [{
1868
+ ruleId: 'FN-004',
1869
+ severity: 'warn',
1870
+ message: `'${name}' gövdesi await içeriyor ama isAsync=false — senkron function içinde await SyntaxError'dur: tarayıcı gövdeyi derleyemez ve fonksiyonu ATLAR (function-source.ts compiles), sunucu da derleyemediği gövdeyi atlar; Functions.${name} hiçbir yerde tanımlı olmaz. Ya isAsync:true yapın (o zaman yalnız tarayıcıda, function/code adımından çağrılır) ya da await'i kaldırın.`,
1871
+ guide: functionsGuide
1872
+ }];
1873
+ }
1874
+ return [];
1875
+ }
1876
+ },
1877
+ {
1878
+ id: 'FN-005',
1879
+ module: 'function',
1880
+ appliesTo: ['create'],
1881
+ sourceRef: "CreateGlobalFunctionCommandValidator When(source): workflow ⇒ applicationId/pageId/scope null (422 GlobalFunctionSourceWorkflowScopeForbidden); studio ⇒ applicationId ve scope zorunlu, scope=page ⇒ pageId (422 GlobalFunctionScopePageRequired), scope=application ⇒ pageId yok (422 GlobalFunctionScopeApplicationPageForbidden)",
1882
+ check: definition => {
1883
+ const name = String(definition.name ?? '?');
1884
+ const source = sourceOf(definition);
1885
+ const has = (key) => definition[key] !== undefined && definition[key] !== null && definition[key] !== '';
1886
+ const block = (message) => ({ ruleId: 'FN-005', severity: 'block', message, guide: functionsGuide });
1887
+ if (source === 'workflow') {
1888
+ const extras = ['applicationId', 'pageId', 'scope'].filter(has);
1889
+ return extras.length === 0
1890
+ ? []
1891
+ : [block(`'${name}' source:'workflow' ama ${extras.join(', ')} verilmiş — workflow fonksiyonu TENANT genelidir, uygulamaya/kapsama/sayfaya bağlanmaz (platform 422 GlobalFunctionSourceWorkflowScopeForbidden). Alanları kaldırın; uygulamaya özel bir tarayıcı fonksiyonu istiyorsanız source:'studio'.`)];
1892
+ }
1893
+ if (!has('applicationId'))
1894
+ return [block(`'${name}' source:'studio' ama applicationId yok — studio fonksiyonu bir uygulamaya aittir (platform 422). application_list ile uygulama id'sini verin; tenant geneli bir SUNUCU fonksiyonu istiyorsanız source:'workflow'.`)];
1895
+ if (!has('scope'))
1896
+ return [block(`'${name}' source:'studio' ama scope yok — 'application' (her sayfada) ya da 'page' (tek sayfa, pageId zorunlu) seçin (platform 422).`)];
1897
+ if (definition.scope === 'page' && !has('pageId'))
1898
+ return [block(`'${name}' scope:'page' ama pageId yok — platform 422 GlobalFunctionScopePageRequired verir; page_list ile sayfa id'sini alın.`)];
1899
+ if (definition.scope === 'application' && has('pageId'))
1900
+ return [block(`'${name}' scope:'application' fonksiyonda pageId verilmez — platform 422 GlobalFunctionScopeApplicationPageForbidden verir; sayfaya bağlamak istiyorsanız scope:'page'.`)];
1901
+ return [];
1902
+ }
1903
+ },
1904
+ {
1905
+ id: 'FLOW-024',
1906
+ module: 'flow',
1907
+ appliesTo: ['create', 'update'],
1908
+ sourceRef: 'K9 + B-19: function adımında args.functionId zorunlu ve uygulamada var olmalı (CreateFlowCommandHandler ilk satırında 422 UiFunctionNotFound); tasks/function/function.ts önce id, sonra functionName ile çözer; parameters bildirim sırasına göre ada göre eşlenir',
1909
+ check: definition => flowAllSteps(definition)
1910
+ .filter(step => step.type === 'function')
1911
+ .flatMap(step => {
1912
+ const args = (step.args ?? {});
1913
+ const label = flowStepLabel(step);
1914
+ const findings = [];
1915
+ if (typeof args.functionId !== 'string' || !conditionRuleIdPattern.test(args.functionId)) {
1916
+ findings.push({
1917
+ ruleId: 'FLOW-024',
1918
+ severity: 'block',
1919
+ message: `'${label}' function adımında args.functionId ${args.functionId === undefined || args.functionId === null ? 'yok' : `geçersiz ('${String(args.functionId)}')`} — platform 422 UiFunctionNotFound döner. Gerçek id'yi function_list ile alın; runtime fonksiyonu ÖNCE id ile çözer, functionName yalnız yedektir (K9).`,
1920
+ guide: functionsGuide
1921
+ });
1922
+ }
1923
+ if (typeof args.functionName !== 'string' || args.functionName.trim() === '') {
1924
+ findings.push({
1925
+ ruleId: 'FLOW-024',
1926
+ severity: 'warn',
1927
+ message: `'${label}' function adımında args.functionName boş — Studio adım özeti 'seçili fonksiyon yok' gösterir ve id çözülemezse ad yedeği de kalmaz. functionName: '<fonksiyon adı>' yazın.`,
1928
+ guide: functionsGuide
1929
+ });
1930
+ }
1931
+ if (args.parameters !== undefined && args.parameters !== null && !Array.isArray(args.parameters)) {
1932
+ findings.push({
1933
+ ruleId: 'FLOW-024',
1934
+ severity: 'block',
1935
+ message: `'${label}' function adımında args.parameters dizi değil (${typeof args.parameters}) — şekil: [{ name: '<bildirimdeki ad>', value: { type: 'static'|'property'|'storage'|'state'|'code'|'variable'|'page'|'item', ref } }].`,
1936
+ guide: functionsGuide
1937
+ });
1938
+ }
1939
+ const nameless = asArray(args.parameters).filter(isRecord).filter(parameter => typeof parameter.name !== 'string' || parameter.name.trim() === '');
1940
+ if (nameless.length > 0) {
1941
+ findings.push({
1942
+ ruleId: 'FLOW-024',
1943
+ severity: 'block',
1944
+ message: `'${label}' function adımında ${nameless.length} parametrenin name'i yok — eşleme bildirimdeki ada göre yapılır, adsız parametre hiçbir argümana bağlanmaz.`,
1945
+ guide: functionsGuide
1946
+ });
1947
+ }
1948
+ return findings;
1949
+ })
1950
+ },
1951
+ {
1952
+ id: 'WF-030',
1953
+ module: 'workflow',
1954
+ appliesTo: ['create', 'update', 'preview'],
1955
+ sourceRef: "2026-09-10 kararı (Owner MR notu, zeyrek-backend docs/global-functions/02 §1): sunucu UI fonksiyonlarını YÜKLEMEZ — WorkflowContext'in 'Functions' ad alanı ve yükleme yolu kaldırıldı. Workflow/middleware/automation ifadesindeki Functions.<ad>(…) çağrısı Jint'te 'Functions is not defined' ile düşer (canlı ölçüldü: workflow_preview code adımı). Sunucuda paylaşılan hesap source:'workflow' fonksiyondur, çıplak adla çağrılır",
1956
+ check: (definition, context) => {
1957
+ const names = [...new Set(functionCallRefs(JSON.stringify(workflowBody(definition, context.workflowKind) ?? {})).map(ref => ref.name))];
1958
+ return names.length === 0
1959
+ ? []
1960
+ : [{
1961
+ ruleId: 'WF-030',
1962
+ severity: 'block',
1963
+ message: `İfadeler ${names.map(name => `Functions.${name}(…)`).join(', ')} çağırıyor ama SUNUCUDA 'Functions' ad alanı YOKTUR (2026-09-10): motor kullanıcı fonksiyonlarını yüklemez, çağrı 'Functions is not defined' ile düşer ve adım hata yoluna girer. Sunucuda paylaşılan hesap source:'workflow' fonksiyondur — function_create source:'workflow' ile tanımlayıp ifadede çıplak adla çağırın ('${names[0]}(…)'). Kullanıcı fonksiyonları yalnız tarayıcıda çalışır ({{ }} bağlaması ve UI flow adımları).`,
1964
+ guide: functionsGuide
1965
+ }];
1966
+ }
1967
+ },
1707
1968
  {
1708
1969
  id: 'FLOW-005',
1709
1970
  module: 'flow',
@@ -2479,3 +2740,84 @@ export const policyRules = [
2479
2740
  // tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
2480
2741
  ...backendPolicyRules()
2481
2742
  ];
2743
+ /**
2744
+ * WF-MF-001'in ikinci yarısı. Bir adımın koşup koşmayacağı yalnız GÖVDEDEN anlaşılamaz: karar
2745
+ * seçilen microfunction'ın YAYIN DURUMUNA bağlıdır ve o durum platformda yaşar. Politika
2746
+ * kuralları senkron ve saf olduğu için bu denetim ayrı durur — gateway `microfunction_get`
2747
+ * ucunu okur, bulguyu aynı `policyFindings` kanalına koyar ve `policyOverride` / `POLICY=warn`
2748
+ * kaçışları (applyPolicyMode) aynen geçerli kalır.
2749
+ *
2750
+ * Okuma başarısız olursa (ağ, yetki, beklenmeyen gövde) kural SUSAR: ön-denetim bir kolaylıktır,
2751
+ * gateway'in kendi arızası yüzünden geçerli bir workflow yazılamaz hâle gelmemeli.
2752
+ */
2753
+ export async function microfunctionStepFindings(platform, project, environment, definition, kind, policyOverride = false) {
2754
+ if (platform.config.policyMode === 'off')
2755
+ return [];
2756
+ const steps = workflowSteps(workflowBody(definition, kind))
2757
+ .filter(step => step.type === 'microfunction')
2758
+ .filter(step => hasText((step.rule ?? {}).microfunctionId));
2759
+ if (steps.length === 0)
2760
+ return [];
2761
+ const records = new Map();
2762
+ const findings = [];
2763
+ for (const step of steps) {
2764
+ const rule = (step.rule ?? {});
2765
+ const microfunctionId = String(rule.microfunctionId);
2766
+ if (!records.has(microfunctionId)) {
2767
+ try {
2768
+ const payload = await platform.tenant('GET', project, `/microfunctions/${microfunctionId}`, { environment });
2769
+ records.set(microfunctionId, (payload?.data ?? payload));
2770
+ }
2771
+ catch (error) {
2772
+ records.set(microfunctionId, error instanceof PlatformApiError && error.status === 404 ? 'missing' : 'unreadable');
2773
+ }
2774
+ }
2775
+ const record = records.get(microfunctionId);
2776
+ if (record === undefined || record === 'unreadable')
2777
+ continue;
2778
+ if (record === 'missing') {
2779
+ findings.push({
2780
+ ruleId: 'WF-MF-001',
2781
+ severity: 'block',
2782
+ message: `'${stepLabel(step)}' adımının microfunctionId'si (${microfunctionId}) bu ortamda YOK. Adım kuralı step_rules.microfunction_id ile kayda FK'lıdır: gövde gitseydi platform 400 dönerdi. microfunction_list ile bu ortamdaki kayıtları görün — fonksiyon başka bir dalda/ortamda kalmış olabilir.`,
2783
+ guide: 'ngapps://guides/microfunction'
2784
+ });
2785
+ continue;
2786
+ }
2787
+ const status = String(record.status ?? '');
2788
+ if (status === 'draft' || status === 'publish-failed') {
2789
+ findings.push({
2790
+ ruleId: 'WF-MF-001',
2791
+ severity: 'block',
2792
+ message: `'${stepLabel(step)}' adımı '${String(record.name ?? microfunctionId)}' fonksiyonunu seçiyor ama kaydın durumu '${status}'${status === 'publish-failed' && hasText(record.publishError) ? ` (publishError: ${String(record.publishError)})` : ''}. Step YALNIZ yayındaki kaydı koşturur: MicrofunctionStep kaydı yükleyip Status != Published görünce MicrofunctionNotPublishedException ile FAIL dalına düşer — akış kurulur, kaydedilir ve koşumda sessizce hata yoluna girer. Önce microfunction_publish çağırın ve microfunction_get ile 'published' olduğunu görün.`,
2793
+ guide: 'ngapps://guides/microfunction'
2794
+ });
2795
+ continue;
2796
+ }
2797
+ if (status === 'publishing') {
2798
+ findings.push({
2799
+ ruleId: 'WF-MF-001',
2800
+ severity: 'warn',
2801
+ message: `'${stepLabel(step)}' adımının fonksiyonu ('${String(record.name ?? microfunctionId)}') şu anda yayınlanıyor. Yayın ASENKRONDUR: bu akış şimdi koşarsa adım fail dalına düşer. microfunction_get ile 'published' olduğunu doğrulayın (yayın başarısız olursa durum 'publish-failed' olur).`,
2802
+ guide: 'ngapps://guides/microfunction'
2803
+ });
2804
+ }
2805
+ if (record.hasUnpublishedChanges === true) {
2806
+ findings.push({
2807
+ ruleId: 'WF-MF-001',
2808
+ severity: 'warn',
2809
+ message: `'${String(record.name ?? microfunctionId)}' fonksiyonu yayında ama üstüne yazılmış (hasUnpublishedChanges: true, revision ${String(record.revision ?? '?')} ≠ publishedRevision ${String(record.publishedRevision ?? '?')}). Runtime hâlâ ESKİ sürümü koşuyor — yeni kodu canlıya almak için microfunction_publish çağırın.`,
2810
+ guide: 'ngapps://guides/microfunction'
2811
+ });
2812
+ }
2813
+ if (!hasText(rule.expression) && hasText(record.parameterDtoId)) {
2814
+ findings.push({
2815
+ ruleId: 'WF-MF-001',
2816
+ severity: 'warn',
2817
+ message: `'${stepLabel(step)}' adımında rule.expression boş ama '${String(record.name ?? microfunctionId)}' fonksiyonunun bir parametre DTO'su var (parameterDtoId: ${String(record.parameterDtoId)}). Fonksiyon args olarak boş nesne alır ve kendi alanlarını undefined görür. Argümanları ifadeyle geçirin — şemayı microfunction_get çıktısındaki parameterScheme verir.`,
2818
+ guide: 'ngapps://guides/microfunction'
2819
+ });
2820
+ }
2821
+ }
2822
+ return applyPolicyMode(platform.config, findings, policyOverride);
2823
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Gateway'in kendi minimal RFC 4180 okuyucusu. `packages/ng-apps-ui`'daki `csv-parse.ts`
3
+ * kopyalanmaz: o dosya tarayıcı için yazılmıştır ve bu proje frontend workspace'ine dahil
4
+ * değildir (sözleşme D5) — kopya iki yerde birden bozulurdu.
5
+ *
6
+ * Değerler HEP string döner. Tip dönüşümü backend'in işidir: `1.234,56` ile `1,234.56` arasındaki
7
+ * fark kültüre bağlıdır ve gateway istek kültürünü bilmez.
8
+ */
9
+ /**
10
+ * Ayraç ilk satırdan tahmin edilir. Türkçe Excel `;` ile yazar ve o dosyalarda virgül ONDALIK
11
+ * AYRACIDIR — yanlış tahmin `1,5` değerini iki sütuna böler, hem de sessizce.
12
+ */
13
+ export function detectDelimiter(firstLine) {
14
+ let semicolons = 0;
15
+ let commas = 0;
16
+ let quoted = false;
17
+ for (const character of firstLine) {
18
+ if (character === '"')
19
+ quoted = !quoted;
20
+ else if (quoted)
21
+ continue;
22
+ else if (character === ';')
23
+ semicolons += 1;
24
+ else if (character === ',')
25
+ commas += 1;
26
+ }
27
+ return semicolons > commas ? ';' : ',';
28
+ }
29
+ function stripBom(text) {
30
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
31
+ }
32
+ function firstLineOf(text) {
33
+ let quoted = false;
34
+ for (let index = 0; index < text.length; index += 1) {
35
+ const character = text[index];
36
+ if (character === '"')
37
+ quoted = !quoted;
38
+ else if (!quoted && (character === '\n' || character === '\r'))
39
+ return text.slice(0, index);
40
+ }
41
+ return text;
42
+ }
43
+ /**
44
+ * Satır satır hücre dizisi. Tırnaklı hücre ayraç ve satır sonu taşıyabilir, `""` tek tırnaktır;
45
+ * tamamen boş satırlar atılır — Excel'in dosya sonuna bıraktığı boş satır aksi hâlde her alanı
46
+ * boş bir kayıt üretirdi.
47
+ */
48
+ export function parseCsv(text, delimiter) {
49
+ const content = stripBom(text);
50
+ const separator = delimiter ?? detectDelimiter(firstLineOf(content));
51
+ const rows = [];
52
+ let row = [];
53
+ let value = '';
54
+ let quoted = false;
55
+ let index = 0;
56
+ const endValue = () => {
57
+ row.push(value);
58
+ value = '';
59
+ };
60
+ const endRow = () => {
61
+ endValue();
62
+ if (row.some(cell => cell.length > 0))
63
+ rows.push(row);
64
+ row = [];
65
+ };
66
+ while (index < content.length) {
67
+ const character = content[index];
68
+ if (quoted) {
69
+ if (character === '"') {
70
+ if (content[index + 1] === '"') {
71
+ value += '"';
72
+ index += 2;
73
+ continue;
74
+ }
75
+ quoted = false;
76
+ index += 1;
77
+ continue;
78
+ }
79
+ value += character;
80
+ index += 1;
81
+ continue;
82
+ }
83
+ if (character === '"' && value.length === 0) {
84
+ quoted = true;
85
+ index += 1;
86
+ continue;
87
+ }
88
+ if (character === separator) {
89
+ endValue();
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (character === '\r' || character === '\n') {
94
+ endRow();
95
+ index += character === '\r' && content[index + 1] === '\n' ? 2 : 1;
96
+ continue;
97
+ }
98
+ value += character;
99
+ index += 1;
100
+ }
101
+ if (value.length > 0 || row.length > 0)
102
+ endRow();
103
+ return rows;
104
+ }
105
+ /**
106
+ * Başlık satırını anahtar yapan satır sözlükleri. Başlık boş olan sütun atlanır (Excel'in sağa
107
+ * doğru uzattığı boş sütunlar aksi hâlde `''` anahtarlı bir alan üretirdi) ve fazladan hücre
108
+ * taşıyan satır kırpılır.
109
+ */
110
+ export function csvRows(text, delimiter) {
111
+ const grid = parseCsv(text, delimiter);
112
+ if (grid.length === 0)
113
+ return [];
114
+ const headers = (grid[0] ?? []).map(header => header.trim());
115
+ return grid.slice(1).map(cells => {
116
+ const row = {};
117
+ headers.forEach((header, column) => {
118
+ if (header.length > 0)
119
+ row[header] = cells[column] ?? '';
120
+ });
121
+ return row;
122
+ });
123
+ }
@@ -1,6 +1,12 @@
1
1
  export class PlatformApiError extends Error {
2
2
  status;
3
3
  code;
4
+ /**
5
+ * Toplu satır yazmasında düşen satırın parti içi indeksi (`BulkRowErrorResponse.rowIndex`).
6
+ * Mesajda da geçer ama metinden ayıklanamaz: parti döngüsü dosya satır numarasını hesaplamak
7
+ * için sayıya ihtiyaç duyar, çağıran hangi satırı düzelteceğini ancak öyle bilir.
8
+ */
9
+ rowIndex;
4
10
  constructor(message, status, code) {
5
11
  super(message);
6
12
  this.status = status;
@@ -50,6 +56,56 @@ function collectValidationMessages(payload) {
50
56
  * hatalı prop'ları sessizce kaydediyordu). Orada op listesi yoktur ve `page_schema_validate` de
51
57
  * uygulanmaz — ipucu bu yüzden yola göre ayrışır.
52
58
  */
59
+ /**
60
+ * Merge apply'ın iki 409'u da aynı şeyi söyler: plan artık uygulanabilir değil. Ajan bunu
61
+ * 'geçici hata, tekrar dene' diye okuyup aynı çağrıyı yollamasın diye yol açıkça yazılır.
62
+ */
63
+ function mergeHint(path) {
64
+ if (!/\/merges\//.test(path))
65
+ return '';
66
+ return " Üç sebepten biri: çözülmemiş çakışma, ya da plan alındığından beri DALIN veya development’ın değişmesi (MergePlanStaleException) — ikincisinde planın taşıdığı liste eskidir ve uygulansaydı sonradan yapılan iş sessizce düşerdi. merge_get ile planı yeniden okuyun (isStale), bayatsa merge_refresh ile tazeleyin — yeni plan açmanız gerekmez. Aynı merge_apply çağrısını tekrarlamak sonucu değiştirmez.";
67
+ }
68
+ function mergeRevertHint(code) {
69
+ if (code === 'MergeRevertWindowExpiredException') {
70
+ return " Geri alma penceresi doldu: pencere kapanınca merge öncesi anlık görüntü de silinir, bu yüzden plan artık geri alınamaz. merge_list status:'applied' ile pencere içindeki bir plan seçin.";
71
+ }
72
+ if (code === 'MergePlanNotRevertableException') {
73
+ return ' Bu plan bir GERİ ALMA planı (kind=revert) ve geri almanın geri alması yoktur. Geri almayı iptal etmek istiyorsanız dalı yeniden merge edin.';
74
+ }
75
+ if (code === 'MergePlanNotAppliedException') {
76
+ return " Plan uygulanmış DEĞİL — durumu mesajda yazıyor (failed / discarded / reverted / planned…). Yalnız 'applied' ve 'applied-with-drift' geri alınabilir; merge_list status:'applied' ile uygun planı seçin. Zaten geri alınmış bir planın durumu 'reverted'dır ve ikinci kez geri alınamaz.";
77
+ }
78
+ return '';
79
+ }
80
+ /**
81
+ * İçe aktarma zinciri dört çağrıdır ve her hata farklı bir adıma geri gönderir: plan süresi
82
+ * dolduysa analizden, anahtar benzersiz değilse plan gövdesinden, hata oranı yüksekse dosyanın
83
+ * kendisinden. Kod olmadan mesajlar "422" diye okunur ve ajan aynı çağrıyı tekrarlar.
84
+ */
85
+ function importHint(code, path) {
86
+ if (code === 'ImportPlanExpiredException') {
87
+ return ' Plan bir saat geçerlidir ve süresi doldu (ya da başka bir kullanıcıya ait). import_analyze ile dosyayı yeniden okuyup import_plan ile yeni bir plan üretin — aynı planId bir daha çalışmaz.';
88
+ }
89
+ if (code === 'ImportTooManyErrorsException') {
90
+ return " Satırların yarısından fazlası kuru koşuda düştü. Önce planın `errors` listesine bakın: çoğu zaman tek bir yanlış `locale` ya da yanlış sütun tipi bütün satırları düşürür. Dosya gerçekten böyleyse import_run'ı `force: true` ile çağırın.";
91
+ }
92
+ if (code === 'ImportKeyFieldNotUniqueException') {
93
+ return " Upsert/update anahtarı benzersiz olmalı: alanın kendisi unique/primary key olmalı, ya da tek alanlı bir unique index taşımalı, ya da 'id'/'code' olmalı. entity_get ile alanın `unique` bayrağını doğrulayın.";
94
+ }
95
+ if (code === 'ImportRelationMatchFieldNotUniqueException') {
96
+ return ' Ad → id eşlemesi yalnız hedef tablonun display alanı ya da benzersiz bir alanıyla yapılır; benzersiz olmayan bir alanda aynı ad iki satıra gidebilir ve hangisinin yazılacağı belirsiz olur.';
97
+ }
98
+ if (code === 'ImportSheetEmptyException' || code === 'ImportRowLimitExceededException') {
99
+ return ' `headerRow` doğru mu kontrol edin (0 = başlık yok); satır sınırı aşıldıysa dosyayı sayfalara bölün.';
100
+ }
101
+ if (code === 'DataJobNotUndoableException' && path.endsWith('/cancel')) {
102
+ return ' İş zaten bitmiş (completed / failed / undone / cancelled); iptal edilecek bir şey yok. Tamamlanmış işi geri almak için import_undo.';
103
+ }
104
+ if (code === 'DataJobNotUndoableException' && path.endsWith('/undo')) {
105
+ return ' Yalnız tamamlanmış ve undoAvailableUntil geçmemiş bir içe aktarma geri alınır; mevcut tabloya yalnız ekleme yapan işin penceresi hiç açılmaz. Süren işi durdurmak için import_cancel.';
106
+ }
107
+ return '';
108
+ }
53
109
  function pageSchemaHint(code, path) {
54
110
  if (code === 'PageSchemaVersionConflictException') {
55
111
  return ' Sayfa siz okuduktan sonra değişti ve HİÇBİR op uygulanmadı. page_schema_get ile yeniden okuyup planı tazeleyin, yeni `version` değerini `baseVersion` olarak gönderin.';
@@ -133,7 +189,7 @@ export function mapPlatformError(status, payload, context) {
133
189
  if (validationMessages.length > 0) {
134
190
  return new PlatformApiError(`Doğrulama hatası (${location}): ${validationMessages.join(' ; ')}. Gövde şeması için ilgili ngapps://schemas kaynağına ya da mevcut bir kaydın *_get çıktısına bakın.`, status, code);
135
191
  }
136
- return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}`, status, code);
192
+ return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}${mergeRevertHint(code)}${importHint(code, context.path)}`, status, code);
137
193
  }
138
194
  if (status === 401) {
139
195
  const hint = context.authMode === 'local'
@@ -151,7 +207,7 @@ export function mapPlatformError(status, payload, context) {
151
207
  if (context.method !== 'GET' && /\/flows(\/|$)/.test(context.path)) {
152
208
  return new PlatformApiError(`Adımda referans verilen kayıt çözümlenemedi (${location}). Bu uçta 404 akışın kendisini DEĞİL bir adım argümanını kastediyor: flow-trigger adımının args'ında ne flowId ne triggerElementId var (ya da triggerElementId sayfadaki hiçbir elementId ile eşleşmiyor), setter adımının iteratees[].element'i bulunamıyor, ya da bir adım silinmiş bir komponente bakıyor. Adım argümanlarını page_schema_get'teki elementId'lerle karşılaştırın; flow-trigger'ın iki geçerli şekli için platform_guide('page') Kural 1c.`, status, code);
153
209
  }
154
- return new PlatformApiError(`Kayıt bulunamadı (${location}).${environmentHint} id/slug değerini ilgili *_list tool'uyla doğrulayın.`, status, code);
210
+ return new PlatformApiError(`Kayıt bulunamadı (${location}).${environmentHint} id/slug değerini ilgili *_list tool'uyla doğrulayın.${importHint(code, context.path)}`, status, code);
155
211
  }
156
212
  if (status === 409 || status === 422) {
157
213
  const projectConflictHint = status === 409 && context.method === 'POST' && /\/projects\/?$/.test(context.path)
@@ -159,7 +215,10 @@ export function mapPlatformError(status, payload, context) {
159
215
  : '';
160
216
  const validationMessages = collectValidationMessages(body);
161
217
  const detail = validationMessages.length > 0 ? ` → ${validationMessages.join(' ; ')}` : '';
162
- return new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${bulkRowHint(code)}${versionConflictHint(code, context.path)}`, status, code);
218
+ const rejected = new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${bulkRowHint(code)}${importHint(code, context.path)}${versionConflictHint(code, context.path)}${mergeHint(context.path)}`, status, code);
219
+ if (typeof body.rowIndex === 'number')
220
+ rejected.rowIndex = body.rowIndex;
221
+ return rejected;
163
222
  }
164
223
  const fallbackValidationMessages = collectValidationMessages(body);
165
224
  const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
@@ -125,6 +125,7 @@ export class Platform {
125
125
  ...(authorization === undefined ? {} : { authorization }),
126
126
  ...(options.environment ? { 'x-environment': options.environment } : {}),
127
127
  ...callerHeaders,
128
+ 'x-client': 'mcp',
128
129
  ...(options.mirrorAuthorizationCookie && authorization !== undefined && callerHeaders.cookie === undefined
129
130
  ? { cookie: `NG_AccessToken=${authorization.replace(/^Bearer /, '')}` }
130
131
  : {})