@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6

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 (101) hide show
  1. package/.env.example +8 -3
  2. package/.env.production.example +2 -0
  3. package/README.md +229 -193
  4. package/dist/config.js +20 -2
  5. package/dist/knowledge/backendConstraints.js +897 -0
  6. package/dist/knowledge/componentCatalog.js +196 -42
  7. package/dist/knowledge/coverage.js +43 -0
  8. package/dist/knowledge/flowGuards.js +30 -0
  9. package/dist/knowledge/guideSections.js +49 -0
  10. package/dist/knowledge/hydrate.js +97 -0
  11. package/dist/knowledge/languageCodes.js +36 -0
  12. package/dist/knowledge/languages.js +105 -0
  13. package/dist/knowledge/layoutSummary.js +10 -0
  14. package/dist/knowledge/localization.js +51 -0
  15. package/dist/knowledge/normalize.js +449 -16
  16. package/dist/knowledge/outlines.js +136 -12
  17. package/dist/knowledge/propTypes.js +80 -0
  18. package/dist/knowledge/rules.js +618 -43
  19. package/dist/knowledge/schemaGuard.js +199 -0
  20. package/dist/knowledge/styleResolve.js +63 -4
  21. package/dist/observability/trace.js +27 -3
  22. package/dist/platform/errors.js +46 -4
  23. package/dist/platform/http.js +33 -4
  24. package/dist/platform/patch.js +27 -0
  25. package/dist/platform/refs.js +104 -0
  26. package/dist/platform/responses.js +9 -2
  27. package/dist/platform/screenshot.js +527 -16
  28. package/dist/platform/secrets.js +40 -0
  29. package/dist/platform/tokenVault.js +89 -0
  30. package/dist/platform/writeDiff.js +281 -0
  31. package/dist/resources/examples/layout-app-shell.json +1 -1
  32. package/dist/resources/examples/microservice-table-list.json +2 -4
  33. package/dist/resources/examples/page-dynamic-form.json +2 -2
  34. package/dist/resources/examples/page-flex-sections.json +99 -0
  35. package/dist/resources/examples/page-sign-in.json +3 -2
  36. package/dist/resources/guides/component.md +69 -8
  37. package/dist/resources/guides/entity.md +404 -16
  38. package/dist/resources/guides/expressions.md +44 -2
  39. package/dist/resources/guides/external-component.md +9 -0
  40. package/dist/resources/guides/getting-started.md +77 -7
  41. package/dist/resources/guides/layout.md +53 -10
  42. package/dist/resources/guides/microservice.md +226 -13
  43. package/dist/resources/guides/middleware.md +89 -1
  44. package/dist/resources/guides/page.md +383 -46
  45. package/dist/resources/guides/translation.md +126 -8
  46. package/dist/resources/guides/view.md +16 -1
  47. package/dist/resources/guides/workflow.md +198 -27
  48. package/dist/resources/knowledge.js +5 -0
  49. package/dist/resources/rules.js +27 -0
  50. package/dist/resources/schema-fingerprint.json +8 -0
  51. package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
  52. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  53. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  54. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  55. package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
  56. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  57. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  58. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  59. package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
  60. package/dist/resources/schemas/GetRowsQuery.json +3 -0
  61. package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
  62. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
  64. package/dist/resources/schemas/StepDto.json +23 -3
  65. package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
  66. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  67. package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
  69. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  70. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  71. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  72. package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
  73. package/dist/server.js +4 -0
  74. package/dist/tools/access.js +3 -3
  75. package/dist/tools/applications.js +1 -1
  76. package/dist/tools/automations.js +63 -30
  77. package/dist/tools/common.js +39 -0
  78. package/dist/tools/components.js +16 -7
  79. package/dist/tools/datasources.js +83 -21
  80. package/dist/tools/deployments.js +34 -2
  81. package/dist/tools/dtos.js +25 -12
  82. package/dist/tools/entities.js +113 -28
  83. package/dist/tools/externalComponents.js +9 -3
  84. package/dist/tools/files.js +20 -6
  85. package/dist/tools/flows.js +110 -11
  86. package/dist/tools/folders.js +7 -2
  87. package/dist/tools/globalFunctions.js +63 -0
  88. package/dist/tools/layouts.js +103 -29
  89. package/dist/tools/microservices.js +229 -38
  90. package/dist/tools/organizations.js +5 -6
  91. package/dist/tools/pages.js +365 -100
  92. package/dist/tools/platformInfo.js +152 -11
  93. package/dist/tools/projects.js +183 -33
  94. package/dist/tools/queries.js +32 -10
  95. package/dist/tools/rows.js +159 -45
  96. package/dist/tools/screenWrite.js +59 -0
  97. package/dist/tools/screens.js +28 -0
  98. package/dist/tools/translations.js +24 -8
  99. package/dist/tools/views.js +20 -10
  100. package/dist/tools/workflows.js +33 -16
  101. package/package.json +6 -3
@@ -1,6 +1,8 @@
1
1
  import { reservedTenantTables } from './reservedTables.js';
2
- const terminalStepTypes = new Set(['response', 'convert', 'revert', 'end']);
3
- const DESCRIPTION_EXEMPT_STEPS = new Set(['start', 'end']);
2
+ import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
3
+ import { backendPolicyRules } from './backendConstraints.js';
4
+ const terminalStepTypes = new Set(['response', 'convert', 'revert']);
5
+ const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
4
6
  const stepKindMatrix = {
5
7
  start: ['workflow', 'automation', 'middleware'],
6
8
  condition: ['workflow', 'automation', 'middleware'],
@@ -18,8 +20,7 @@ const stepKindMatrix = {
18
20
  redirect: ['middleware'],
19
21
  response: ['workflow', 'middleware'],
20
22
  convert: ['workflow'],
21
- revert: ['automation'],
22
- end: ['workflow', 'automation', 'middleware']
23
+ revert: ['automation']
23
24
  };
24
25
  function asArray(value) {
25
26
  return Array.isArray(value) ? value : [];
@@ -99,6 +100,27 @@ const flowBranchNames = {
99
100
  end: []
100
101
  };
101
102
  const flowFreeBranchTypes = new Set(['condition']);
103
+ /** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
104
+ const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
105
+ // Sayfa akışında kuralın id'si hem dal adıdır (condition.ts `engine.next(rule.id)`) hem de
106
+ // backend'de bir Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; }`), yani
107
+ // okunabilir bir ad iki uçta birden tutmaz: istek System.Guid çevrimiyle reddedilir.
108
+ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
109
+ // alert.ts ve confirm.ts `task.argument.message`'ı doğrudan toast/dialog'a geçirir; argüman
110
+ // Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
111
+ const flowLiteralTextFields = new Map([
112
+ ['alert', ['message', 'description']],
113
+ ['confirm', ['message']]
114
+ ]);
115
+ const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
116
+ // Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
117
+ // BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten `redirect`e çeviriyor
118
+ // (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da koşar, yani vaka
119
+ // uyarılmaz, ORTADAN KALKAR. Geriye kalan `confirm`/`download` tek başına kusur değildir: sayfa
120
+ // tek bir uygulamaya aittir (`Page.ApplicationId`), uygulamanın tipi web ya da mobile'dır
121
+ // (`ApplicationType.cs`) ve gateway hangisi olduğunu bilmez. Mobil ayağı uygulama tarafında
122
+ // kapanır (`ScreenRuntime.tsx`), lint'te değil.
123
+ const flowUnregisteredStepTypes = new Set(['end', 'query']);
102
124
  const flowSetterIterateeTypes = new Set(['change', 'fill']);
103
125
  function flowStepList(value) {
104
126
  return asArray(value).filter((step) => Boolean(step) && typeof step === 'object');
@@ -109,8 +131,20 @@ function flowAllSteps(definition) {
109
131
  function flowStepLabel(step) {
110
132
  return step.stepId || step.name || step.type || '?';
111
133
  }
134
+ /**
135
+ * Dal listesi İKİ biçimde gelebilir ve motor ikisini de aynı şekle eşler (Runtime.svelte
136
+ * `mapStepBranches`, ScreenRuntime.tsx aynısı): dizi `[{ name, nextStepId }]` ya da nesne
137
+ * `{ <dal adı>: <nextStepId> }`. Studio'nun kendi varsayılan şeması nesne biçimini kullanır
138
+ * (`event-actions.constants.ts` DEFAULT_SCHEMA `branches: { always: '' }`), bu yüzden lint
139
+ * nesne biçimini görmezse Studio'dan gelen gövdeler bütün dal kurallarını atlar.
140
+ */
112
141
  function flowBranches(step) {
113
- return asArray(step.branches).filter((branch) => Boolean(branch) && typeof branch === 'object');
142
+ const branches = step.branches;
143
+ if (branches && typeof branches === 'object' && !Array.isArray(branches)) {
144
+ return Object.entries(branches)
145
+ .map(([name, nextStepId]) => ({ name, nextStepId: nextStepId ?? null }));
146
+ }
147
+ return asArray(branches).filter((branch) => Boolean(branch) && typeof branch === 'object');
114
148
  }
115
149
  function flowIteratees(step) {
116
150
  return asArray((step.args ?? {}).iteratees).filter((iteratee) => Boolean(iteratee) && typeof iteratee === 'object');
@@ -171,9 +205,25 @@ function flowCodeExpressions(step) {
171
205
  }
172
206
  return expressions;
173
207
  }
174
- /** `Steps.ad.headers` / `Steps["ad"].headers` yakalar, `Steps.ad?.headers` guard'lısını yakalamaz. */
175
- const flowHeadersAccess = /Steps\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*['"]([^'"]+)['"]\s*\])\s*\.\s*headers\b/g;
176
208
  const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
209
+ /** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
210
+ const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
211
+ const rolesSlugFieldId = '0199c35d-cfc8-77d4-9c20-29b857153147';
212
+ function writesRolesWithoutSlug(entityId, parameters) {
213
+ if (String(entityId ?? '').toLowerCase() !== rolesEntityId)
214
+ return false;
215
+ const operation = String(parameters?.operation ?? '').toLowerCase();
216
+ // Yalnız SATIR ÜRETEN yollar: 'update' var olan satırın slug'ını zaten taşır, orada slug
217
+ // yazdırmak eski slug'a bakan yetki ifadelerini kırar (aynı tuzağın upsert ayağı MS-016).
218
+ if (operation !== 'create' && operation !== 'upsert')
219
+ return false;
220
+ return !asArray(parameters?.fields)
221
+ .filter((field) => Boolean(field) && typeof field === 'object')
222
+ .some(field => String(field.fieldId ?? '').toLowerCase() === rolesSlugFieldId
223
+ || String(field.fieldSlug ?? field.slug ?? '') === 'slug');
224
+ }
225
+ 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
+ const contractKeys = ['returnDtoId', 'parameterDtoId', 'returnBehavior', 'parameterBehavior'];
177
227
  const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith']);
178
228
  function flattenFilterTree(filters) {
179
229
  return filters
@@ -300,6 +350,95 @@ export const policyRules = [
300
350
  guide: 'ngapps://guides/microservice'
301
351
  }]
302
352
  },
353
+ {
354
+ id: 'MS-015',
355
+ module: 'microservice',
356
+ appliesTo: ['create', 'update', 'preview'],
357
+ sourceRef: "TableMicroserviceExecutor.ProcessFilterExpressions (TableMicroserviceExecutor.cs:495-510) filter.Value'yu Source'a BAKMADAN engine.Evaluate'e verir; query-string/body/header kaynaklı bir filtreye value yazmak çalışma anında 'JavaScriptException: <value> is not defined' → HTTP 500 üretir. Bu kaynaklarda değer çağrıdan gelir, parametre adı alan slug'ından türer (İK koşusu M-2)",
358
+ check: definition => {
359
+ const parameters = (definition.sourceParameters ?? {});
360
+ const offenders = [];
361
+ const pending = [...asArray(parameters.filters)];
362
+ while (pending.length > 0) {
363
+ const filter = pending.shift();
364
+ if (!filter || typeof filter !== 'object')
365
+ continue;
366
+ pending.push(...asArray(filter.children));
367
+ const source = String(filter.source ?? '');
368
+ const value = filter.value;
369
+ if (source !== 'query-string' && source !== 'body' && source !== 'header')
370
+ continue;
371
+ if (value === null || value === undefined || String(value).trim() === '')
372
+ continue;
373
+ offenders.push(`${String(filter.alias ?? '?')} · source: '${source}' · value: ${JSON.stringify(value)}`);
374
+ }
375
+ if (offenders.length === 0)
376
+ return [];
377
+ return [{
378
+ ruleId: 'MS-015',
379
+ severity: 'block',
380
+ message: `query-string/body/header kaynaklı filtreye 'value' yazılmış (${offenders.join(' | ')}): platform filtre değerini kaynağa bakmadan JS olarak değerlendiriyor, bu yüzden uç çağrıldığında 'JavaScriptException: … is not defined' ile 500 döner. Bu kaynaklarda 'value' HİÇ verilmez — parametre adı alan slug'ından türer; sabit bir ön-süzme istiyorsanız source: 'expression' kullanın.`,
381
+ guide: 'ngapps://guides/microservice'
382
+ }];
383
+ }
384
+ },
385
+ {
386
+ id: 'MS-016',
387
+ module: 'microservice',
388
+ appliesTo: ['create', 'update', 'preview'],
389
+ sourceRef: "İK koşusu M-3: operation 'upsert' olan uçta expression'la yazılan alan UPDATE yolunda yeniden değerlendiriliyor; Incremental sicil alanı EMP-0005 → EMP-0006 diye sessizce değişti. Platformda alan bazlı writeOn (create|update|always) YOK — bu kural o gelene kadarki tek uyarı",
390
+ check: definition => {
391
+ const parameters = (definition.sourceParameters ?? {});
392
+ if (String(parameters.operation ?? '') !== 'upsert')
393
+ return [];
394
+ const generated = asArray(parameters.fields)
395
+ .map(entry => entry)
396
+ .filter(field => String(field.source ?? '') === 'expression')
397
+ .map(field => String(field.alias ?? field.fieldId ?? '?'));
398
+ if (generated.length === 0)
399
+ return [];
400
+ return [{
401
+ ruleId: 'MS-016',
402
+ severity: 'warn',
403
+ message: `upsert ucunda expression kaynaklı alan var (${generated.join(', ')}): upsert UPDATE yolunda da bu ifadeyi yeniden değerlendirir, yani sicil/kod gibi ÜRETİLEN bir değer güncellemede sessizce değişir. Alanı yalnız oluşturmada yazmak istiyorsanız create ve update için iki ayrı uç kurun — platformda alan bazlı 'yalnız create'te yaz' ifadesi henüz yok.`,
404
+ guide: 'ngapps://guides/microservice'
405
+ }];
406
+ }
407
+ },
408
+ {
409
+ id: 'QRY-006',
410
+ module: 'query',
411
+ appliesTo: ['create', 'update'],
412
+ sourceRef: "2026-09-09 koşusu S-1: query_create gövdesine yazılan description komut şemasında YOKTU ve sessizce düştü; alan backend'e eklendi (Query.Description + 20260909075142_AddQueryDescription). MS-014'ün query karşılığı, ama warn: uç açıklaması sorgununkinin yerine geçmez ve block bugün açıklamasız yazan her çağrıyı kırardı",
413
+ check: definition => hasText(definition.description)
414
+ ? []
415
+ : [{
416
+ ruleId: 'QRY-006',
417
+ severity: 'warn',
418
+ message: "Sorgunun 'description' alanı boş. 1-2 cümleyle bu sorgunun NE DÖNDÜRDÜĞÜNÜ yazın — sorgu listesinde ad ve slug dışında hiçbir ipucu yok ve aynı datasource üzerinde onlarca ham SQL birikiyor. Ucun kendi açıklaması (MS-014) bunun yerine geçmez: bir sorgu birden çok uçtan tüketilebilir ve uç sözleşmeyi anlatırken sorgu veriyi anlatır.",
419
+ guide: 'ngapps://guides/microservice'
420
+ }]
421
+ },
422
+ {
423
+ id: 'MS-018',
424
+ module: 'microservice',
425
+ appliesTo: ['create', 'update', 'preview'],
426
+ sourceRef: "2026-09-09 koşusu S-2: microservice_create gövdesine yazılan returnDtoId sessizce düştü — platform 200 döndü, uç sözleşmesiz kaldı. Alan Create/UpdateMicroserviceCommand şemasında YOK (additionalProperties:false); sözleşme Workflow.ReturnDtoId'ye ait (WF-002)",
427
+ check: definition => {
428
+ const written = contractKeys.filter(key => definition[key] !== undefined && definition[key] !== null);
429
+ if (written.length === 0)
430
+ return [];
431
+ const workflowId = String(definition.workflowId ?? '');
432
+ const dtoId = String(definition.returnDtoId ?? definition.parameterDtoId ?? '<dtoId>');
433
+ const patch = `workflow_patch(id: '${workflowId === '' ? '<workflowId>' : workflowId}', ops: [{ op: 'add', path: '/returnDtoId', value: '${dtoId}' }, { op: 'add', path: '/returnBehavior', value: 'object' }])`;
434
+ return [{
435
+ ruleId: 'MS-018',
436
+ severity: 'block',
437
+ message: `Uç komutunda DTO sözleşmesi alanı YOK — gövdeye yazılan ${written.join(', ')} platforma gitse 200 dönerdi ve alan SESSİZCE düşerdi; uç sözleşmesiz kalır (ölçüldü 2026-09-09, S-2). table/query/view kaynaklı ucun yanıt şekli kaynağın KENDİSİNDEN türer (selectFields / sorgunun returnScheme'i / view indeksi); açık yanıt sözleşmesi WORKFLOW kavramıdır. Reçete: ucu source:'workflow' + workflowId ile kurun, DTO'yu workflow'a bağlayın — ${patch}. WF-002: type:'workflow' akışta returnDtoId ZORUNLUDUR, returnBehavior'ı boş bırakırsanız gateway 'object' ile doldurur. Sözleşme yalnız 2xx yanıtlarda uygulanır (WF-026). Uç gövdesinden bu alanları çıkarıp yeniden çağırın.`,
438
+ guide: 'ngapps://guides/microservice'
439
+ }];
440
+ }
441
+ },
303
442
  {
304
443
  id: 'MS-002',
305
444
  module: 'microservice',
@@ -423,17 +562,25 @@ export const policyRules = [
423
562
  const kind = context.workflowKind ?? body.type;
424
563
  if (!kind)
425
564
  return [];
426
- return workflowSteps(body)
427
- .filter(step => {
565
+ return workflowSteps(body).flatMap(step => {
428
566
  const allowedKinds = stepKindMatrix[String(step.type)];
429
- return allowedKinds && !allowedKinds.includes(kind);
430
- })
431
- .map(step => ({
432
- ruleId: 'WF-005',
433
- severity: 'block',
434
- message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware.`,
435
- guide: 'ngapps://guides/workflow'
436
- }));
567
+ if (!allowedKinds) {
568
+ return [{
569
+ ruleId: 'WF-005',
570
+ severity: 'block',
571
+ message: `'${stepLabel(step)}' adımındaki '${step.type}' geçerli bir workflow step tipi değil. Geçerli tipler: ${Object.keys(stepKindMatrix).join(', ')}. Akışı bitirmek için ayrı bir end adımı eklemeyin; son dalda nextStepId: null kullanın.`,
572
+ guide: 'ngapps://guides/workflow'
573
+ }];
574
+ }
575
+ if (allowedKinds.includes(kind))
576
+ return [];
577
+ return [{
578
+ ruleId: 'WF-005',
579
+ severity: 'block',
580
+ message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware.`,
581
+ guide: 'ngapps://guides/workflow'
582
+ }];
583
+ });
437
584
  }
438
585
  },
439
586
  {
@@ -524,7 +671,32 @@ export const policyRules = [
524
671
  return [{
525
672
  ruleId: 'WF-024',
526
673
  severity: 'block',
527
- message: `Açıklama eksik — ${parts.join('; ')}. Her adıma ve akışın kendisine 1-2 cümle 'description' yazın: Studio kanvasında adımın yanında ⓘ ikonu olarak görünür ve akışı devralan geliştiricinin tek okuma kaynağıdır. NE YAPTIĞINI değil NEDEN olduğunu yazın: 'assignments tablosunu status=Aktif ile okur' mekanizmadır ve zaten koddan okunuyor; 'kullanıcının asal pozisyonunu seçer, birden fazla aktif atama varsa is_primary olanı tercih eder' niyettir. start/end adımları muaftır.`,
674
+ message: `Açıklama eksik — ${parts.join('; ')}. Her adıma ve akışın kendisine 1-2 cümle 'description' yazın: Studio kanvasında adımın yanında ⓘ ikonu olarak görünür ve akışı devralan geliştiricinin tek okuma kaynağıdır. NE YAPTIĞINI değil NEDEN olduğunu yazın: 'assignments tablosunu status=Aktif ile okur' mekanizmadır ve zaten koddan okunuyor; 'kullanıcının asal pozisyonunu seçer, birden fazla aktif atama varsa is_primary olanı tercih eder' niyettir. start adımı muaftır.`,
675
+ guide: 'ngapps://guides/workflow'
676
+ }];
677
+ }
678
+ },
679
+ {
680
+ id: 'WF-025',
681
+ module: 'workflow',
682
+ appliesTo: ['create', 'update'],
683
+ sourceRef: "WorkflowService.UpdateAsync adımları silip yeniden yazar; gövdede olmayan bir adıma bakan branches.next_step_id INSERT'te FK'ya takılır. Ölçüldü 2026-09-08 (workflow_patch ile bir adım silindi, kalan dal ona bakıyordu): 400 'This record is in use. Related table: Steps, column: Next Step Id'. Flow tarafındaki karşılığı FLOW-005",
684
+ check: (definition, context) => {
685
+ const body = workflowBody(definition, context.workflowKind);
686
+ const steps = workflowSteps(body);
687
+ if (steps.length === 0)
688
+ return [];
689
+ const stepIds = new Set(steps.map(step => String(step.id ?? '')).filter(id => id !== ''));
690
+ const dangling = steps.flatMap(step => asArray(step.branches)
691
+ .map(branch => ({ branch: branch, nextStepId: String(branch.nextStepId ?? '') }))
692
+ .filter(entry => entry.nextStepId !== '' && !stepIds.has(entry.nextStepId))
693
+ .map(entry => `${stepLabel(step)} · '${entry.branch.name ?? entry.branch.type}' → ${entry.nextStepId}`));
694
+ if (dangling.length === 0)
695
+ return [];
696
+ return [{
697
+ ruleId: 'WF-025',
698
+ severity: 'block',
699
+ message: `Hedefi gövdede olmayan dal var: ${dangling.join(', ')}. Workflow dalının 'nextStepId'si adımın GUID'idir ve platform onu silinmiş adıma bağlayamaz — istek 400 'This record is in use … Next Step Id' ile döner. Adımı silmeden ÖNCE ona bakan dalların 'nextStepId' değerini null yapın (aynı patch çağrısında iki op olarak gönderilebilir).`,
528
700
  guide: 'ngapps://guides/workflow'
529
701
  }];
530
702
  }
@@ -547,6 +719,32 @@ export const policyRules = [
547
719
  : [];
548
720
  }
549
721
  },
722
+ {
723
+ id: 'WF-025',
724
+ module: 'workflow',
725
+ appliesTo: ['create', 'update', 'preview'],
726
+ sourceRef: "Ölçüm 2026-09-07 (canlı, emrah/development): kayıtlı sorgunun parameterScheme'i doluyken library query adımı rule.queryParameters'ı HİÇ okumadı, rule.expression dalına düştü ve parametreler null bağlandı — çağrı 200 + 0 satır döndü, hiçbir katman uyarmadı (teşhis microservice_debug: adımın input girdisi ifadesiz/değersiz). Depo kodu düzeltilmiş (QueryStep.ExecuteLibraryAsync, fe7b923a — önce ruleParameters, expression yedek) ama canlı backend o commit'ten geride: B-63. Kural, dağıtım güncellenince düşer",
727
+ check: (definition, context) => {
728
+ const body = workflowBody(definition, context.workflowKind);
729
+ const unboundSteps = workflowSteps(body)
730
+ .filter(step => step.type === 'query')
731
+ .filter(step => {
732
+ const rule = (step.rule ?? {});
733
+ if (String(rule.queryStepSourceType ?? 'library') === 'custom')
734
+ return false;
735
+ return asArray(rule.queryParameters).length > 0 && String(rule.expression ?? '').trim() === '';
736
+ })
737
+ .map(stepLabel);
738
+ return unboundSteps.length > 0
739
+ ? [{
740
+ ruleId: 'WF-025',
741
+ severity: 'warn',
742
+ message: `Query adımı yalnız 'queryParameters' ile bağlanmış (${unboundSteps.join(', ')}) ve 'rule.expression' boş. Kayıtlı sorgunun parametreleri bildirilmişse ÇALIŞAN MOTOR BU DİZİYİ OKUMUYOR: parametreler null bağlanıyor ve çağrı 200 + SIFIR SATIR dönüyor — hata yok, uyarı yok (ölçüldü 2026-09-07; düzeltme depoda var, canlı dağıtım geride — B-63). Aynı bağlamayı 'rule.expression'a nesne literali olarak da yazın: "{ year: input.year, company_id: user.company_id }". Kapsam parametresini middleware'den okumanın yolu da budur (platform_guide('workflow') §'Query adımı').`,
743
+ guide: 'ngapps://guides/workflow'
744
+ }]
745
+ : [];
746
+ }
747
+ },
550
748
  {
551
749
  id: 'WF-010',
552
750
  module: 'workflow',
@@ -687,6 +885,28 @@ export const policyRules = [
687
885
  return [];
688
886
  }
689
887
  },
888
+ {
889
+ id: 'AUT-002',
890
+ module: 'workflow',
891
+ appliesTo: ['create', 'update'],
892
+ sourceRef: "2026-09-05 ölçümü + 2026-09-07 ürün sahibi cevabı (S-7): revert yapısı gereği exception fırlatır ve İKİ koşum yolunda da içinde bulunduğu transaction'ı geri alır — fark, o transaction'ın tetikleyen satır yazmasını kapsayıp kapsamamasıdır. isQueue:false → RowConsumer/AutomationConsumer, automation KENDİ transaction'ında (QueuedEventConsumer) satır COMMIT olduktan sonra koşar; akışın yazdıkları geri alınır, olay 'failed' damgalanır (beklenen iz, kusur değil), çağıran 200'ü çoktan almıştır. isQueue:true → TriggerAutomationWhenRowEventHandlerBase, satır işlemiyle AYNI transaction'da koşar; çağırana 400 { code: revertType, message: revertMessage } döner ve tetikleyen satır da geri alınır. Backend adlandırması: Entity.cs:735-736 GetSyncEventQueueAutomations = IsQueue==true",
893
+ check: (definition, context) => {
894
+ if (context.workflowKind !== 'automation')
895
+ return [];
896
+ const automation = definition.automation;
897
+ if (!automation || typeof automation !== 'object' || automation.isQueue === true)
898
+ return [];
899
+ const steps = [...asArray(definition.steps), ...asArray(automation.steps)];
900
+ if (!steps.some(step => step?.type === 'revert'))
901
+ return [];
902
+ return [{
903
+ ruleId: 'AUT-002',
904
+ severity: 'warn',
905
+ message: "Bu automation 'revert' taşıyor ve isQueue:false — akış RabbitMQ üzerinden ASYNC koşar. O yolda revert kendi transaction'ını geri alır (akışın yazdıkları kalmaz) ve olay 'failed' damgalanır; bu beklenen davranıştır, kapının çalıştığının izidir. Kapsam dışında kalan tek şey tetikleyen satırdır: o revert'e varılmadan COMMIT olmuş ve çağıran 200 almıştır. İş kuralı ÇAĞIRANI reddetmeli ve tetikleyen satır da geri alınmalıysa isQueue:true + queueOrder verin — o yol satır işlemiyle aynı transaction'da koşar ve çağırana 400 { code: revertType, message: revertMessage } döner.",
906
+ guide: 'ngapps://guides/workflow'
907
+ }];
908
+ }
909
+ },
690
910
  {
691
911
  id: 'MS-004',
692
912
  module: 'microservice',
@@ -935,7 +1155,7 @@ export const policyRules = [
935
1155
  id: 'ENT-004',
936
1156
  module: 'entity',
937
1157
  appliesTo: ['create', 'update'],
938
- sourceRef: 'ik-hrm teslim notu PLT-002: display alanı tekil',
1158
+ sourceRef: "ik-hrm teslim notu PLT-002: display alanı tekil. Backend'de de doğrulayıcı kuralı: CreateEntityCommandValidator / UpdateEntityCommandValidator Fields.Count(Display) <= 1 → 422 DuplicateValue 'Display' (2026-09-08 madenciliği; warn → block, çünkü gövde gitseydi aynı red gelirdi)",
939
1159
  check: definition => {
940
1160
  const displayFields = asArray(definition.fields)
941
1161
  .filter((field) => Boolean(field) && typeof field === 'object')
@@ -943,13 +1163,142 @@ export const policyRules = [
943
1163
  return displayFields.length > 1
944
1164
  ? [{
945
1165
  ruleId: 'ENT-004',
946
- severity: 'warn',
947
- message: `Birden çok display alanı var (${displayFields.map(field => field.slug ?? field.name).join(', ')}) — entity başına yalnız BİR display alanı olabilir.`,
1166
+ severity: 'block',
1167
+ message: `Birden çok display alanı var (${displayFields.map(field => field.slug ?? field.name).join(', ')}) — entity başına yalnız BİR display alanı olabilir; platform 422 "Duplicate value: 'Display'" döner.`,
948
1168
  guide: 'ngapps://guides/entity'
949
1169
  }]
950
1170
  : [];
951
1171
  }
952
1172
  },
1173
+ {
1174
+ id: 'ENT-013',
1175
+ module: 'entity',
1176
+ appliesTo: ['create', 'update'],
1177
+ sourceRef: "EntityIndexDto.cs: alan adları `unique` ve `fieldIds`, doğrulayıcı FieldIds NotEmpty + tekrarsız, ad önek hariç ≤32; Create/UpdateEntityCommandValidator ad benzersizliğini büyük/küçük harf duyarsız arıyor. B-57 (2026-09-06): 422 yalnız İLK yanlış anahtarı söylediği için `fields`→`fieldIds` öğrenildi ama `isUnique` öğrenilemedi — platform onu yok sayıp index'i UNIQUE OLMADAN kurdu",
1178
+ check: definition => {
1179
+ const indexes = asArray(definition.indexes).filter((index) => Boolean(index) && typeof index === 'object');
1180
+ if (indexes.length === 0)
1181
+ return [];
1182
+ const findings = [];
1183
+ const guide = 'ngapps://guides/entity';
1184
+ const knownFieldIds = new Set(asArray(definition.fields)
1185
+ .filter((field) => Boolean(field) && typeof field === 'object')
1186
+ .map(field => field.id)
1187
+ .filter((id) => typeof id === 'string' && id !== ''));
1188
+ const nameCounts = new Map();
1189
+ indexes.forEach((index, position) => {
1190
+ const name = typeof index.name === 'string' ? index.name : '';
1191
+ const label = name === '' ? `indexes[${position}]` : `'${name}'`;
1192
+ if (Object.hasOwn(index, 'isUnique')) {
1193
+ findings.push({
1194
+ ruleId: 'ENT-013',
1195
+ severity: 'block',
1196
+ message: `${label} index'inde 'isUnique' anahtarı var — platform bu adı TANIMAZ, sessizce yok sayar ve index UNIQUE OLMADAN kurulur. Doğru anahtar 'unique'.`,
1197
+ guide
1198
+ });
1199
+ }
1200
+ if (Object.hasOwn(index, 'fields')) {
1201
+ findings.push({
1202
+ ruleId: 'ENT-013',
1203
+ severity: 'block',
1204
+ message: `${label} index'inde 'fields' anahtarı var — doğru anahtar 'fieldIds' ve alan ID'si ister, slug değil.`,
1205
+ guide
1206
+ });
1207
+ }
1208
+ else {
1209
+ const fieldIds = asArray(index.fieldIds).filter((id) => typeof id === 'string' && id !== '');
1210
+ if (fieldIds.length === 0) {
1211
+ findings.push({
1212
+ ruleId: 'ENT-013',
1213
+ severity: 'block',
1214
+ message: `${label} index'inin 'fieldIds' alanı boş — platform 422 döner ("'Field Ids' must not be empty"). Alan id'lerini entity_get view:'outline' çıktısından alın.`,
1215
+ guide
1216
+ });
1217
+ }
1218
+ if (new Set(fieldIds).size !== fieldIds.length) {
1219
+ findings.push({
1220
+ ruleId: 'ENT-013',
1221
+ severity: 'block',
1222
+ message: `${label} index'inde aynı alan birden çok kez var — 'fieldIds' tekrar edemez.`,
1223
+ guide
1224
+ });
1225
+ }
1226
+ const foreign = knownFieldIds.size === 0 ? [] : fieldIds.filter(id => !knownFieldIds.has(id));
1227
+ if (foreign.length > 0) {
1228
+ findings.push({
1229
+ ruleId: 'ENT-013',
1230
+ severity: 'warn',
1231
+ message: `${label} index'i gövdedeki hiçbir alana ait olmayan id taşıyor (${foreign.join(', ')}). Alan id'lerini platform ÜRETİR: entity'yi önce kurup id'leri okuyun ya da 'fields[].id'leri kendiniz verip aynı guid'leri kullanın. Yollarsanız platform yanıltıcı bir mesajla reddeder: "This record is in use. Related table: 'Fields', column: 'Field Id'." — ortada bağımlılık yoktur, alan yoktur.`,
1232
+ guide
1233
+ });
1234
+ }
1235
+ }
1236
+ if (name === '') {
1237
+ findings.push({
1238
+ ruleId: 'ENT-013',
1239
+ severity: 'block',
1240
+ message: `indexes[${position}] adsız — 'name' zorunludur.`,
1241
+ guide
1242
+ });
1243
+ return;
1244
+ }
1245
+ // Uzunluk sınırı ÖNEK HARİÇ ölçülür: platform ada `IX_<5 hane>_` ekler ve okuma yolu
1246
+ // önekli adı döndürür, yani bir gidiş-dönüş gövdesi sırf önek yüzünden ihlal etmemeli.
1247
+ if (name.replace(/^IX_[0-9a-fA-F]{5}_/, '').length > 32) {
1248
+ findings.push({
1249
+ ruleId: 'ENT-013',
1250
+ severity: 'block',
1251
+ message: `${label} index adı çok uzun — platformun eklediği 'IX_<5 hane>_' öneki hariç en fazla 32 karakter olabilir.`,
1252
+ guide
1253
+ });
1254
+ }
1255
+ const key = name.toLocaleLowerCase('tr');
1256
+ nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
1257
+ });
1258
+ for (const [name, count] of nameCounts) {
1259
+ if (count < 2)
1260
+ continue;
1261
+ findings.push({
1262
+ ruleId: 'ENT-013',
1263
+ severity: 'block',
1264
+ message: `'${name}' adı ${count} index'te birden kullanılmış — index adları entity içinde büyük/küçük harf duyarsız benzersizdir.`,
1265
+ guide
1266
+ });
1267
+ }
1268
+ return findings;
1269
+ }
1270
+ },
1271
+ {
1272
+ id: 'ENT-011',
1273
+ module: 'entity',
1274
+ appliesTo: ['create', 'update'],
1275
+ sourceRef: "FieldDto.cs:170-178 When(IsLocalized): PrimaryKey/Identity/Unique false, RelationType None, CommonType Text zorunlu — beşi de platform validator'ında hard 400 (OnlyTextCanBeLocalizedException). İstek gitmeden yakalanır",
1276
+ check: definition => {
1277
+ const findings = [];
1278
+ for (const field of asArray(definition.fields).filter((entry) => Boolean(entry) && typeof entry === 'object')) {
1279
+ if (field.isLocalized !== true)
1280
+ continue;
1281
+ const name = String(field.slug ?? field.name ?? '?');
1282
+ const relationType = String(field.relationType ?? 'none').toLowerCase();
1283
+ const conflicts = [
1284
+ field.primaryKey === true ? 'primaryKey' : null,
1285
+ field.identity === true ? 'identity' : null,
1286
+ field.unique === true ? 'unique' : null,
1287
+ relationType !== 'none' && relationType !== '' ? `relationType: '${field.relationType}'` : null,
1288
+ field.commonType !== undefined && field.commonType !== 'text' ? `commonType: '${field.commonType}'` : null
1289
+ ].filter((entry) => entry !== null);
1290
+ if (conflicts.length === 0)
1291
+ continue;
1292
+ findings.push({
1293
+ ruleId: 'ENT-011',
1294
+ severity: 'block',
1295
+ message: `'${name}' alanı isLocalized ama ${conflicts.join(', ')} taşıyor — platform yalnız düz METİN alanını çok dilli yapar. Bu gövde 400 ile reddedilir (FieldDto validator'ı isLocalized alanında primaryKey/identity/unique false, relationType 'none' ve commonType 'text' ister). Çeviriyi metin alanında tutun; anahtar, benzersizlik ve ilişki ana tabloda kalır. Ayrıntı: platform_guide('entity') §'Çoklu dilli alan'.`,
1296
+ guide: 'ngapps://guides/entity'
1297
+ });
1298
+ }
1299
+ return findings;
1300
+ }
1301
+ },
953
1302
  {
954
1303
  id: 'MS-007',
955
1304
  module: 'microservice',
@@ -1460,6 +1809,199 @@ export const policyRules = [
1460
1809
  });
1461
1810
  }
1462
1811
  },
1812
+ {
1813
+ id: 'FLOW-019',
1814
+ module: 'flow',
1815
+ appliesTo: ['create', 'update'],
1816
+ sourceRef: "code.ts:18 kod gövdesini `task.argument?.value` alanından okur ve boşsa adımı NO-OP sayıp engine.next('always') çağırır. Gövde `args.value` alanındadır; `args.expression`/`args.script` gibi uydurma anahtarlar StepManagementArgument sözleşmesinde YOKTUR ve platform onları sessizce düşürür (ölçüldü 2026-09-05, Corecruiter koşusu S-19: akış koştu, filtre hiçbir şey yapmadı, teşhis ~10 dk sürdü)",
1817
+ check: definition => flowAllSteps(definition)
1818
+ .filter(step => step.type === 'code')
1819
+ .flatMap((step) => {
1820
+ const args = (step.args ?? {});
1821
+ const body = typeof args.value === 'string' ? args.value.trim() : '';
1822
+ if (body.length > 0)
1823
+ return [];
1824
+ const strays = ['expression', 'script', 'code', 'body', 'source']
1825
+ .filter(key => typeof args[key] === 'string' && String(args[key]).trim().length > 0);
1826
+ return [{
1827
+ ruleId: 'FLOW-019',
1828
+ severity: 'block',
1829
+ message: `'${flowStepLabel(step)}' code adımının gövdesi yok. Kod JS METNİ olarak args.value alanında durur — runtime onu \`(async () => { <value> })()\` içinde koşturur; alan boşken adım hata vermeden NO-OP olur ve akış hiçbir şey yapmadan devam eder.${strays.length > 0 ? ` Gövdeyi args.${strays.join('/args.')} alanına yazmışsınız: bu anahtar StepManagementArgument sözleşmesinde YOK ve platform onu sessizce düşürür — args.value olarak yazın.` : ''} Değeri bir komponentten okuyacaksanız code adımına hiç gerek yoktur: parametreye value { type: 'property', ref: { source: 'component', element, property } } verin.`,
1830
+ guide: 'ngapps://guides/page'
1831
+ }];
1832
+ })
1833
+ },
1834
+ {
1835
+ id: 'FLOW-020',
1836
+ module: 'flow',
1837
+ appliesTo: ['create', 'update'],
1838
+ sourceRef: "ng-apps-flow/tasks/condition/condition.ts + condition.types.ts: gövde `task.argument.rules: [{ id, expression, type: 'if'|'else-if'|'else' }]`; process() YALNIZ rules[0].type==='if' iken başlar, dal adı rule.id'dir (engine.next(rule.id) → Runtime.svelte:330 branch.name→branch.id → flow-engine.ts:93), 'else' yalnız expression BOŞKEN fallback'tir, eşleşmeyen kural rules[index+1]'e geçer. rule.id backend'de Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; } = Guid.CreateVersion7()`): okunabilir ad System.Guid çevrimiyle reddedilir, id hiç verilmezse backend kendi GUID'ini üretir ve branches[].name ölü kalır. 'always' yalnız EŞLEŞEN dalın completed/stopped'ından çağrılır — rules boş / rules[0].type!=='if' / hiçbir kural tutmuyor ve boş ifadeli else yok hâllerinde engine.next('always') HİÇ çalışmaz. Backend workflow'unun rule.items[].branchId şekli (StepArgument.cs StepRule) sayfa akışında HİÇ okunmaz — 2026-09-06·2 koşusu S-12: istek 200, adım kaydedildi, Studio çizdi, motor kuralı hiç değerlendirmedi",
1839
+ check: definition => flowAllSteps(definition)
1840
+ .filter(step => step.type === 'condition')
1841
+ .flatMap((step) => {
1842
+ const args = (step.args ?? {});
1843
+ const rules = asArray(args.rules).filter((rule) => Boolean(rule) && typeof rule === 'object');
1844
+ const label = flowStepLabel(step);
1845
+ const shape = "args.rules: [{ id: '<TAZE GUID>', expression: '<JS ifadesi>', type: 'if' }, { id: '<TAZE GUID>', expression: '', type: 'else' }] — id GERÇEK bir GUID olmalı (backend Guid'e çevirir) ve her rule.id için branches: [{ name: '<aynı GUID>', nextStepId: '<hedef stepId>' }]";
1846
+ if (rules.length === 0) {
1847
+ const workflowShape = args.rule !== undefined || args.items !== undefined || step.rule !== undefined;
1848
+ return [{
1849
+ ruleId: 'FLOW-020',
1850
+ severity: 'block',
1851
+ message: workflowShape
1852
+ ? `'${label}' condition adımı BACKEND WORKFLOW şeklini taşıyor (rule/items). Sayfa akışının motoru başka bir motordur ve yalnız 'args.rules' okur (condition.ts); workflow şekli 200 ile kaydedilir, Studio çizer ve kural HİÇ değerlendirilmez — akış sessizce dalsız kalır. Doğru gövde: ${shape}`
1853
+ : `'${label}' condition adımında 'args.rules' yok ya da boş. Motor kural listesi boşken hiçbir dala girmez ve akış o adımda sessizce biter (condition.ts). Doğru gövde: ${shape}`,
1854
+ guide: 'ngapps://guides/page'
1855
+ }];
1856
+ }
1857
+ const findings = [];
1858
+ const first = rules[0];
1859
+ if (first.type !== 'if') {
1860
+ findings.push({
1861
+ ruleId: 'FLOW-020',
1862
+ severity: 'block',
1863
+ message: `'${label}' condition adımının İLK kuralı type:'${String(first.type ?? '')}' — motor zinciri yalnız rules[0].type === 'if' iken başlatır (condition.ts), aksi hâlde adım hiçbir dala girmez ve akış sessizce biter. Zincirin başına 'if' kuralını koyun; devamı 'else-if', kapanışı boş ifadeli 'else' olur.`,
1864
+ guide: 'ngapps://guides/page'
1865
+ });
1866
+ }
1867
+ // YALNIZ `name` — motor dalı adıyla arar (Runtime.svelte:330 `branch.name || ''`).
1868
+ // `id` GUID'dir; kümeye karıştırılırsa hiçbir rule id'si tutmaz ve kural sahte blok üretir.
1869
+ const branchNames = new Set(flowBranches(step)
1870
+ .map(branch => (typeof branch.name === 'string' ? branch.name : ''))
1871
+ .filter(name => name.length > 0));
1872
+ for (const rule of rules) {
1873
+ const id = typeof rule.id === 'string' ? rule.id : '';
1874
+ const type = String(rule.type ?? '');
1875
+ const expression = typeof rule.expression === 'string' ? rule.expression : '';
1876
+ const ruleLabel = id || type || '?';
1877
+ if (id.length === 0) {
1878
+ findings.push({
1879
+ ruleId: 'FLOW-020',
1880
+ severity: 'block',
1881
+ message: `'${label}' condition adımının '${type || '?'}' kuralında 'id' yok. Dal adı kuralın id'sidir (engine.next(rule.id)); id vermezseniz backend kendi GUID'ini üretir (StepManagementSchema.cs \`Guid.CreateVersion7()\`), yazdığınız branches[].name ona ASLA eşleşmez ve dal sessizce ölür. Taze bir GUID üretip hem rule.id'ye hem branches[].name'e aynısını yazın.`,
1882
+ guide: 'ngapps://guides/page'
1883
+ });
1884
+ continue;
1885
+ }
1886
+ if (!conditionRuleIdPattern.test(id)) {
1887
+ findings.push({
1888
+ ruleId: 'FLOW-020',
1889
+ severity: 'block',
1890
+ message: `'${label}' condition adımının '${ruleLabel}' kuralında id GUID değil. Backend bu alanı Guid'e çevirir (StepManagementSchema.cs) ve okunabilir bir ad isteği \`The JSON value could not be converted to System.Guid\` ile reddeder. Dal adı da aynı GUID olmak zorunda olduğu için iki yerde birden okunabilir ad kullanamazsınız: taze bir GUID üretip hem rule.id'ye hem branches[].name'e yazın.`,
1891
+ guide: 'ngapps://guides/page'
1892
+ });
1893
+ continue;
1894
+ }
1895
+ if (!conditionRuleTypes.has(type)) {
1896
+ findings.push({
1897
+ ruleId: 'FLOW-020',
1898
+ severity: 'block',
1899
+ message: `'${label}' condition adımının '${ruleLabel}' kuralında type:'${type}' geçersiz — kabul edilen küme 'if' · 'else-if' · 'else' (condition.types.ts).`,
1900
+ guide: 'ngapps://guides/page'
1901
+ });
1902
+ }
1903
+ if (type === 'else' && expression.trim().length > 0) {
1904
+ findings.push({
1905
+ ruleId: 'FLOW-020',
1906
+ severity: 'warn',
1907
+ message: `'${label}' condition adımının '${ruleLabel}' kuralı type:'else' ama 'expression' dolu. Motor else dalını YALNIZ ifade boşken fallback sayar (condition.ts \`!rule.expression && rule.type === 'else'\`); dolu else normal bir karşılaştırmaya döner. Hiçbir kural tutmazsa akış dalsız biter ve 'always' dalı DA koşmaz — motor always'i yalnız eşleşen dalın completed/stopped'ından çağırır. Fallback istiyorsanız expression'ı boş bırakın.`,
1908
+ guide: 'ngapps://guides/page'
1909
+ });
1910
+ }
1911
+ if (branchNames.size > 0 && !branchNames.has(id)) {
1912
+ findings.push({
1913
+ ruleId: 'FLOW-020',
1914
+ severity: 'block',
1915
+ message: `'${label}' condition adımının '${ruleLabel}' kuralının id'si hiçbir branches[].name ile eşleşmiyor. Motor dalı adıyla arar (Runtime.svelte:330 branch.name → branch.id, flow-engine.ts:93) — eşleşme yoksa kural tutsa bile akış o dalda durur. branches'a { name: '${id}', nextStepId: '<hedef stepId>' } ekleyin.`,
1916
+ guide: 'ngapps://guides/page'
1917
+ });
1918
+ }
1919
+ }
1920
+ // Hiç 'else' kuralı yoksa fallback de yoktur. Dolu ifadeli else zaten yukarıda
1921
+ // uyarıldığı için burada YALNIZ else'siz zinciri raporluyoruz — aynı derdi iki kez
1922
+ // söylemeyelim.
1923
+ if (!rules.some(rule => String(rule.type ?? '') === 'else')) {
1924
+ findings.push({
1925
+ ruleId: 'FLOW-020',
1926
+ severity: 'warn',
1927
+ message: `'${label}' condition adımında 'else' kuralı yok. Hiçbir ifade tutmazsa motor zinciri sessizce bitirir: akış dalsız kalır ve 'always' dalı DA koşmaz — always yalnız eşleşen dalın completed/stopped'ından tetiklenir (condition.ts). Yükleniyor bayrağını always ile kapatıyorsanız bayrak açık kalır. Zinciri boş ifadeli bir { type: 'else', expression: '' } kuralıyla kapatın.`,
1928
+ guide: 'ngapps://guides/page'
1929
+ });
1930
+ }
1931
+ return findings;
1932
+ })
1933
+ },
1934
+ {
1935
+ id: 'FLOW-021',
1936
+ module: 'flow',
1937
+ appliesTo: ['create', 'update'],
1938
+ sourceRef: "flow-engine.ts hasErrorFlow(): `flow.errorTasks.some(t => t.type !== 'start')` — yalnız start taşıyan errorSteps hata yolu SAYILMAZ, onError false döner ve akış hata gövdesiyle 'always' dalından devam eder (2026-09-06·2 koşusu S-13)",
1939
+ check: definition => {
1940
+ const errorSteps = flowStepList(definition.errorSteps);
1941
+ if (errorSteps.length === 0)
1942
+ return [];
1943
+ return errorSteps.some(step => step.type !== 'start')
1944
+ ? []
1945
+ : [{
1946
+ ruleId: 'FLOW-021',
1947
+ severity: 'warn',
1948
+ message: "errorSteps yalnız 'start' adımı taşıyor — motor bunu hata yolu SAYMAZ (flow-engine.ts hasErrorFlow: start dışında en az bir adım arar). Hata yolu kurulmuş görünür ama uç 4xx dönünce onError false döner, akış 'always' dalından hata gövdesiyle devam eder ve kullanıcıya hiçbir mesaj ulaşmaz. start'ın ardına en az bir adım bağlayın: ucun kendi mesajını göstereceksen setter (value.type:'code' → Steps.error.message), sabit bir uyarı yetiyorsa alert — alert'in 'message' alanı DÜZ METİNDİR (alert.ts task.argument.message'ı doğrudan toast'a geçirir), oraya ifade yazarsanız kullanıcı ifadenin kendisini görür.",
1949
+ guide: 'ngapps://guides/page'
1950
+ }];
1951
+ }
1952
+ },
1953
+ {
1954
+ id: 'FLOW-022',
1955
+ module: 'flow',
1956
+ appliesTo: ['create', 'update'],
1957
+ sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(task.argument.message)`. Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu)",
1958
+ check: definition => flowAllSteps(definition)
1959
+ .flatMap((step) => {
1960
+ const type = String(step.type ?? '');
1961
+ const fields = flowLiteralTextFields.get(type);
1962
+ if (!fields)
1963
+ return [];
1964
+ const args = (step.args ?? {});
1965
+ return fields
1966
+ .filter(field => typeof args[field] === 'string' && flowExpressionLooking.test(args[field]))
1967
+ .map(field => ({
1968
+ ruleId: 'FLOW-022',
1969
+ severity: 'warn',
1970
+ message: `'${flowStepLabel(step)}' ${type} adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: ${type}.ts task.argument.${field}'i doğrudan ekrana geçirir ve args hiçbir yerde değerlendirilmez (Runtime.svelte \`argument: step.args\`). Kullanıcı ifadenin sonucunu değil ifadenin KENDİSİNİ görür. Ucun mesajını göstermek için bir setter (ya da code) adımı kullanın — value { type: 'code', ref: 'Steps.error?.message' }; hata metninin nerede durduğu page.md §hata yolu'nda.`,
1971
+ guide: 'ngapps://guides/page'
1972
+ }));
1973
+ })
1974
+ },
1975
+ {
1976
+ id: 'FLOW-023',
1977
+ module: 'flow',
1978
+ appliesTo: ['create', 'update'],
1979
+ sourceRef: "Adım tipi sözlüğü backend'indir (StepType.cs) ama koşan tipler barındırıcıdan gelir: flow-engine.ts start·setter·code·condition·microservice·validation·flow-trigger register eder, web Runtime.svelte alert·confirm·redirect·download·storage·setcookie ekler, mobil ScreenRuntime.tsx alert·redirect·navigate·storage·setcookie ekler. navigate web'de, confirm/download mobilde YOKTUR; end ve query hiçbir barındırıcıda yoktur (Studio kataloğu da bunu visibility ile söyler: navigate 'mobile', confirm/download 'web'; End eklenebilir adımlar arasında değil — EventActionsAdd.svelte). Register edilmemiş tipe gelen akış SESSİZCE durur: flow-engine.ts process() callback bulamazsa hiçbir şey yapmaz (next yok, completed/stopped yok, hata yok, log yok), run() Promise'i çözülmez ve flowRunFinished() hiç koşmaz — __ngAppsFlowActivity.running sıfıra inmez, page_screenshot'ın hazır bekleyişi her eylemde bütçesini sonuna kadar yakar",
1980
+ check: definition => {
1981
+ const steps = flowAllSteps(definition);
1982
+ // Studio her akış tuvaline dekoratif bir End düğümü koyar ve onu sildirmez; zararlı olan
1983
+ // düğümün VARLIĞI değil, bir dalın oraya İŞARET ETMESİDİR — motor o adıma girer ve çıkamaz.
1984
+ const targeted = new Set(steps.flatMap(step => flowBranches(step)
1985
+ .map(branch => String(branch.nextStepId ?? ''))
1986
+ .filter(id => id.length > 0)));
1987
+ const silentStop = "Register edilmemiş bir tipe gelen akış SESSİZCE durur — flow-engine.ts process() callback bulamazsa hiçbir şey yapmaz: next() yok, completed/stopped yok, hata yok, log yok. Akışın kalanı ölür ve run() Promise'i çözülmediği için flowRunFinished() hiç koşmaz; __ngAppsFlowActivity.running sıfıra inmez ve page_screenshot'ın hazır bekleyişi her eylemde bütçesini sonuna kadar yakar.";
1988
+ return steps.flatMap((step) => {
1989
+ const type = String(step.type ?? '');
1990
+ if (flowUnregisteredStepTypes.has(type)) {
1991
+ const reachable = type !== 'end' || targeted.has(String(step.stepId ?? ''));
1992
+ if (!reachable)
1993
+ return [];
1994
+ return [{
1995
+ ruleId: 'FLOW-023',
1996
+ severity: 'block',
1997
+ message: `'${flowStepLabel(step)}' adımının tipi '${type}' ve bir dal oraya işaret ediyor — bu tipi HİÇBİR barındırıcı register etmiyor (ne flow-engine.ts, ne web Runtime.svelte, ne mobil ScreenRuntime.tsx); Studio da onu eklenebilir adımlar arasında göstermez. ${silentStop} Akışı bitirmek için ayrı bir adım gerekmez: son adımın dalına nextStepId: null verin. Dekoratif bir '${type}' düğümü tutmak serbesttir, yeter ki hiçbir dal ona işaret etmesin.`,
1998
+ guide: 'ngapps://guides/page'
1999
+ }];
2000
+ }
2001
+ return [];
2002
+ });
2003
+ }
2004
+ },
1463
2005
  {
1464
2006
  id: 'FLOW-017',
1465
2007
  module: 'flow',
@@ -1624,31 +2166,32 @@ export const policyRules = [
1624
2166
  const microserviceStepsByName = new Map(steps
1625
2167
  .filter(step => step.type === 'microservice' && step.name)
1626
2168
  .map(step => [String(step.name), step]));
1627
- return steps.flatMap(step => flowCodeExpressions(step).flatMap(({ expression, origin }) => {
1628
- flowHeadersAccess.lastIndex = 0;
1629
- const findings = [];
1630
- let match;
1631
- while ((match = flowHeadersAccess.exec(expression)) !== null) {
1632
- const name = match[1] ?? match[2] ?? '';
2169
+ return steps.flatMap(step => {
2170
+ const seen = new Set();
2171
+ return flowCodeExpressions(step).flatMap(({ expression, origin }) => headersAccessNames(expression).flatMap((name) => {
2172
+ if (seen.has(name))
2173
+ return [];
1633
2174
  const source = microserviceStepsByName.get(name);
1634
2175
  if (source && (source.args ?? {}).isRaw === true) {
1635
- findings.push({
2176
+ seen.add(name);
2177
+ return [{
2178
+ ruleId: 'FLOW-011',
2179
+ severity: 'block',
2180
+ message: `'${flowStepLabel(step)}' adımının ${origin} ifadesi Steps.${name}.headers okuyor ama '${name}' adımı isRaw=true ile çalışıyor — o zaman storage ham response.data'dır, 'headers' diye bir alan HİÇ yazılmaz (microservice.ts:88). İfade her koşuda TypeError verir. isRaw'ı kaldırın (normalize false'a çeker) ya da header okumaktan vazgeçin.`,
2181
+ guide: 'ngapps://guides/page'
2182
+ }];
2183
+ }
2184
+ if (isHeadersAccessGuarded(expression, name))
2185
+ return [];
2186
+ seen.add(name);
2187
+ return [{
1636
2188
  ruleId: 'FLOW-011',
1637
- severity: 'block',
1638
- message: `'${flowStepLabel(step)}' adımının ${origin} ifadesi Steps.${name}.headers okuyor ama '${name}' adımı isRaw=true ile çalışıyor — o zaman storage ham response.data'dır, 'headers' diye bir alan HİÇ yazılmaz (microservice.ts:88). İfade her koşuda TypeError verir. isRaw'ı kaldırın (normalize false'a çeker) ya da header okumaktan vazgeçin.`,
2189
+ severity: 'warn',
2190
+ message: `'${flowStepLabel(step)}' adımının ${origin} ifadesinde Steps.${name}.headers guard'sız okunuyor. İstek HATA aldığında storage { error, data } olarak yazılır ve 'headers' alanı yoktur (microservice.ts:132) → TypeError; runCode undefined döner (prop'a NaN yazılır), ifade hatası toast'ı düşer ve flow-engine.ts:53 akışı onError'a çevirip durdurabilir. Guard'lı yazın: Number(Steps.${name}?.headers?.["total-count"] ?? 0) ya da Steps.${name}.headers && … önkoşulu.`,
1639
2191
  guide: 'ngapps://guides/page'
1640
- });
1641
- continue;
1642
- }
1643
- findings.push({
1644
- ruleId: 'FLOW-011',
1645
- severity: 'warn',
1646
- message: `'${flowStepLabel(step)}' adımının ${origin} ifadesinde Steps.${name}.headers guard'sız okunuyor. İstek HATA aldığında storage { error, data } olarak yazılır ve 'headers' alanı yoktur (microservice.ts:132) → TypeError; runCode undefined döner (prop'a NaN yazılır), ifade hatası toast'ı düşer ve flow-engine.ts:53 akışı onError'a çevirip durdurabilir. Guard'lı yazın: Number(Steps.${name}?.headers?.["total-count"] ?? 0).`,
1647
- guide: 'ngapps://guides/page'
1648
- });
1649
- }
1650
- return findings;
1651
- }));
2192
+ }];
2193
+ }));
2194
+ });
1652
2195
  }
1653
2196
  },
1654
2197
  {
@@ -1881,6 +2424,35 @@ export const policyRules = [
1881
2424
  }];
1882
2425
  }
1883
2426
  },
2427
+ {
2428
+ id: 'ROL-002',
2429
+ module: 'microservice',
2430
+ appliesTo: ['create', 'update', 'preview'],
2431
+ sourceRef: "Ürün sahibi cevabı 2026-09-07 (S-16): rol yönetimi entity + microservice yoludur. Kaynak: roles kolonları id · name (UNIQUE ix_roles_name) · slug (TenantContextModelSnapshot); yetkilendirme slug'a bakar (QueryConstants.GetUserQuery ARRAY(SELECT r.slug …), Authorized(user.roles)); slug'ı role_list DÖNDÜRMEZ (RoleDto = Id + Name)",
2432
+ check: definition => {
2433
+ const parameters = definition.sourceParameters;
2434
+ if (definition.source !== 'table')
2435
+ return [];
2436
+ return writesRolesWithoutSlug(parameters?.entityId ?? definition.entityId, parameters)
2437
+ ? [{ ruleId: 'ROL-002', severity: 'warn', message: rolesSlugMessage, guide: 'ngapps://guides/entity' }]
2438
+ : [];
2439
+ }
2440
+ },
2441
+ {
2442
+ id: 'ROL-002',
2443
+ module: 'workflow',
2444
+ appliesTo: ['create', 'update', 'preview'],
2445
+ sourceRef: "ROL-002'nin workflow ayağı: table step de aynı tabloya yazar (rule.entityId + rule.fields)",
2446
+ check: (definition, context) => workflowSteps(workflowBody(definition, context.workflowKind))
2447
+ .filter(step => step.type === 'table' && step.rule)
2448
+ .filter(step => writesRolesWithoutSlug(step.rule.entityId, step.rule))
2449
+ .map(step => ({
2450
+ ruleId: 'ROL-002',
2451
+ severity: 'warn',
2452
+ message: `'${stepLabel(step)}' table adımı: ${rolesSlugMessage}`,
2453
+ guide: 'ngapps://guides/entity'
2454
+ }))
2455
+ },
1884
2456
  {
1885
2457
  id: 'MW-003',
1886
2458
  module: 'workflow',
@@ -1902,5 +2474,8 @@ export const policyRules = [
1902
2474
  guide: 'ngapps://guides/middleware'
1903
2475
  }];
1904
2476
  }
1905
- }
2477
+ },
2478
+ // Backend doğrulayıcılarının aynası (ENT-015…018, MS-017, WF-026, QRY/VIEW/DTO/ROW/DS/PRJ/TR/FLD/FILE/EXT):
2479
+ // tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
2480
+ ...backendPolicyRules()
1906
2481
  ];