@appaflytech/zeyrek-ai-mcp 0.0.43 → 0.0.46

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 (75) hide show
  1. package/dist/knowledge/backendConstraints.js +86 -2
  2. package/dist/knowledge/componentCatalog.js +63 -26
  3. package/dist/knowledge/componentEvents.js +6 -0
  4. package/dist/knowledge/connectDataBind.js +43 -6
  5. package/dist/knowledge/connectDataDefinition.js +74 -3
  6. package/dist/knowledge/connectDataRules.js +14 -0
  7. package/dist/knowledge/mobileComponentCatalog.js +67 -8
  8. package/dist/knowledge/mobileStore.js +61 -2
  9. package/dist/knowledge/platform/lint.js +23 -0
  10. package/dist/knowledge/platform/mobile.js +1 -1
  11. package/dist/knowledge/platform/mobileOffline.js +45 -0
  12. package/dist/knowledge/platform/mobileRules.js +8 -1
  13. package/dist/knowledge/platform/mobileTabs.js +31 -2
  14. package/dist/knowledge/platform/web.js +1 -1
  15. package/dist/knowledge/rules.js +220 -7
  16. package/dist/knowledge/textHooks.js +434 -0
  17. package/dist/platform/capture/capture.js +1 -1
  18. package/dist/platform/capture/readiness.js +4 -0
  19. package/dist/platform/errors.js +22 -2
  20. package/dist/prompts/mobilePublish.js +21 -0
  21. package/dist/resources/examples/flow-biometric-confirm.json +199 -0
  22. package/dist/resources/examples/microfunction-node-dependencies.json +19 -0
  23. package/dist/resources/examples/mobile-offline-list.json +357 -0
  24. package/dist/resources/examples/mobile-offline-queue.json +82 -0
  25. package/dist/resources/examples/mobile-offline-seamless.json +1520 -0
  26. package/dist/resources/guides/access.md +4 -4
  27. package/dist/resources/guides/component.md +20 -4
  28. package/dist/resources/guides/deployment.md +98 -0
  29. package/dist/resources/guides/entity.md +1 -1
  30. package/dist/resources/guides/expressions.md +5 -1
  31. package/dist/resources/guides/external-component.md +6 -4
  32. package/dist/resources/guides/merge.md +81 -4
  33. package/dist/resources/guides/microfunction.md +104 -9
  34. package/dist/resources/guides/microservice.md +34 -5
  35. package/dist/resources/guides/mobile-connect-data.md +70 -0
  36. package/dist/resources/guides/mobile-flow.md +64 -0
  37. package/dist/resources/guides/mobile-publish.md +161 -16
  38. package/dist/resources/guides/mobile-steps.md +21 -1
  39. package/dist/resources/guides/mobile.md +45 -23
  40. package/dist/resources/guides/page.md +122 -5
  41. package/dist/resources/guides/translation.md +97 -2
  42. package/dist/resources/knowledge.js +3 -1
  43. package/dist/resources/prompts/mobile-publish-prepare.md +89 -0
  44. package/dist/resources/schema-fingerprint.json +4 -4
  45. package/dist/resources/schemas/ApplyMicrofunctionArchiveChangesCommand.json +76 -0
  46. package/dist/resources/schemas/ConnectDataDescribeDto.json +44 -0
  47. package/dist/resources/schemas/ConnectDataResultDto.json +10 -0
  48. package/dist/resources/schemas/CreateMicrofunctionCommand.json +8 -0
  49. package/dist/resources/schemas/CreateMicroserviceCommand.json +3 -0
  50. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +0 -4
  51. package/dist/resources/schemas/MicroserviceDto.json +3 -0
  52. package/dist/resources/schemas/MobileStorePreflightScope.json +13 -0
  53. package/dist/resources/schemas/StartMobileBuildCommand.json +35 -1
  54. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +8 -0
  55. package/dist/resources/schemas/UpdateMicroserviceCommand.json +3 -0
  56. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +75 -0
  57. package/dist/server.js +2 -0
  58. package/dist/tools/accessRuntime.js +1 -1
  59. package/dist/tools/applications.js +16 -2
  60. package/dist/tools/common.js +6 -0
  61. package/dist/tools/components.js +19 -6
  62. package/dist/tools/connectData.js +9 -4
  63. package/dist/tools/deployments.js +1 -1
  64. package/dist/tools/designSchemaPatch.js +16 -4
  65. package/dist/tools/environments.js +3 -3
  66. package/dist/tools/files.js +1 -1
  67. package/dist/tools/flows.js +118 -7
  68. package/dist/tools/layouts.js +32 -12
  69. package/dist/tools/merges.js +8 -8
  70. package/dist/tools/microfunctions.js +53 -3
  71. package/dist/tools/mobileStore.js +83 -21
  72. package/dist/tools/pages.js +59 -24
  73. package/dist/tools/platformInfo.js +4 -2
  74. package/dist/tools/textHookGuard.js +116 -0
  75. package/package.json +2 -2
@@ -0,0 +1,45 @@
1
+ export const MOBILE_OFFLINE_MAX_AGE_DAYS = [1, 7, 30];
2
+ export const MOBILE_OFFLINE_DEFAULTS = { screens: true, data: false, maxAgeDays: 7, banner: true };
3
+ const FIELDS = Object.keys(MOBILE_OFFLINE_DEFAULTS);
4
+ /** `properties.mobile.offline`'ın HAM değeri; alan yoksa `undefined`. */
5
+ export function rawMobileOffline(properties) {
6
+ const mobile = properties?.mobile;
7
+ return mobile && typeof mobile === 'object' ? mobile.offline : undefined;
8
+ }
9
+ /** Cihazın okuduğu ayar: küme dışı ya da yanlış tipli değer varsayılana düşer. */
10
+ export function readMobileOffline(properties) {
11
+ const raw = rawMobileOffline(properties);
12
+ const offline = raw && typeof raw === 'object' ? raw : {};
13
+ return {
14
+ screens: typeof offline.screens === 'boolean' ? offline.screens : MOBILE_OFFLINE_DEFAULTS.screens,
15
+ data: typeof offline.data === 'boolean' ? offline.data : MOBILE_OFFLINE_DEFAULTS.data,
16
+ maxAgeDays: MOBILE_OFFLINE_MAX_AGE_DAYS.includes(offline.maxAgeDays) ? offline.maxAgeDays : MOBILE_OFFLINE_DEFAULTS.maxAgeDays,
17
+ banner: typeof offline.banner === 'boolean' ? offline.banner : MOBILE_OFFLINE_DEFAULTS.banner
18
+ };
19
+ }
20
+ /**
21
+ * MOB-033'in ayar ayağı. Backend `properties`'i doğrulamaz (jsonb serbest) ve cihazın okuyucusu okuyamadığı değeri
22
+ * SESSİZCE varsayılana düşürür — uyarının söylendiği tek yer burasıdır. Yazım yine platforma gider.
23
+ */
24
+ export function mobileOfflineProblems(properties) {
25
+ const raw = rawMobileOffline(properties);
26
+ if (raw === undefined || raw === null)
27
+ return [];
28
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
29
+ return [`properties.mobile.offline nesne olmalı (gelen: ${JSON.stringify(raw)}) — cihaz okumaz, varsayılanlar geçerli (screens: true, data: false, maxAgeDays: 7, banner: true)`];
30
+ }
31
+ const offline = raw;
32
+ const problems = [];
33
+ for (const field of ['screens', 'data', 'banner']) {
34
+ if (offline[field] !== undefined && typeof offline[field] !== 'boolean') {
35
+ problems.push(`properties.mobile.offline.${field} boolean olmalı (gelen: ${JSON.stringify(offline[field])}) — yok sayılır, varsayılan (${MOBILE_OFFLINE_DEFAULTS[field]}) geçerli`);
36
+ }
37
+ }
38
+ if (offline.maxAgeDays !== undefined && !MOBILE_OFFLINE_MAX_AGE_DAYS.includes(offline.maxAgeDays)) {
39
+ problems.push(`properties.mobile.offline.maxAgeDays ${JSON.stringify(offline.maxAgeDays)} kümede değil (${MOBILE_OFFLINE_MAX_AGE_DAYS.join(' | ')}) — sayı olarak yazılır; yok sayılır, ${MOBILE_OFFLINE_DEFAULTS.maxAgeDays} gün geçerli`);
40
+ }
41
+ for (const key of Object.keys(offline).filter(key => !FIELDS.includes(key))) {
42
+ problems.push(`properties.mobile.offline içinde tanınmayan anahtar '${key}' — cihaz okumaz (${FIELDS.join(' | ')})`);
43
+ }
44
+ return problems;
45
+ }
@@ -132,6 +132,12 @@ export const MOBILE_RULES = {
132
132
  severity: 'info',
133
133
  sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts (kural; ayna heroTag.ts); zeyrek-frontend/docs/adr/0022-hero-yolu.md; simülatör ölçümü 2026-10-10',
134
134
  summary: "Aynı sayfada aynı statik heroTag'i birden çok düğüm taşıyor — sayfa yakınlaşmanın hedefi olduğunda ilk çizilen hizalanır; kaynak sayfada sorun değildir"
135
+ },
136
+ 'MOB-033': {
137
+ id: 'MOB-033',
138
+ severity: 'warn',
139
+ sourceRef: 'docs/adr/0023-mobil-cevrimdisi-yerel-kopya-ve-uygulama-paketi.md K13, K8; zeyrek-frontend/packages/ng-apps-ui-native/src/shell/mobile-offline.ts (okuyucu; ayna mobileOffline.ts); ng-apps-flow tasks/microservice/microservice.ts (error.network yalnız veri önbelleği açıkken)',
140
+ summary: "properties.mobile.offline okunamıyor (nesne değil, boolean olmayan screens/data/banner, 1 | 7 | 30 dışı maxAgeDays, tanınmayan anahtar) — cihaz varsayılana düşer; akış ağ hatasını error.status 503 ile ayırıyor (bilgi: error.network)"
135
141
  }
136
142
  };
137
143
  /**
@@ -201,7 +207,8 @@ export const MOBILE_WEB_ONLY = {
201
207
  'theme-toggle': "uygulama içi tema ayarı: 'choice-chips' (variant: segmented, Sistem/Açık/Koyu) → storage adımı name: 'zeyrek:theme-mode', value: system | light | dark; barındırıcı temayı anında değiştirir, cihazda kalıcı (seçim yazılmadıysa açık tema; cihaz teması yalnız system seçilince)",
202
208
  pagination: "'repeater' + daha fazla yükle düğmesi",
203
209
  breadcrumb: "yığın gezinmesi (app-bar geri düğmesi)",
204
- fileUpload: "'fileInput' (alan) ya da 'filebutton' (düğme) — dokunmatikte sürükle-bırak yok; ikisi de aynı base64 değeri yazar"
210
+ fileUpload: "'fileInput' (alan) ya da 'filebutton' (düğme) — dokunmatikte sürükle-bırak yok; ikisi de aynı base64 değeri yazar",
211
+ attachments: "'filebutton' (isMultiple açık; seçilenler düğmenin altında satır satır) — mobilde önizlemeli kart ızgarası, popup ve sürükle-bırak yok; değer aynı base64 dizisidir"
205
212
  };
206
213
  const STYLE_REPLACEMENTS = {
207
214
  className: "yerine surfaceStyle (zemin, kenar, köşe, gölge) ve typographyStyle tipli gruplarını kullanın",
@@ -6,6 +6,11 @@ function record(value) {
6
6
  function text(value) {
7
7
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
8
8
  }
9
+ function badgeOf(value) {
10
+ const badge = record(value);
11
+ const ref = badge?.source === 'state' ? text(badge.ref) : undefined;
12
+ return ref ? { source: 'state', ref } : undefined;
13
+ }
9
14
  /** Öğenin sayfa id'si: `{ page }` ya da düz metin (`items: ['id']`). */
10
15
  function entryPage(entry) {
11
16
  const item = record(entry);
@@ -23,8 +28,12 @@ export function readMobileTabs(properties) {
23
28
  * - Öğe `{ page }` ya da düz sayfa id'sidir; sayfası yoksa düşer; `pages` verildiyse orada olmayan
24
29
  * sayfa düşer; tekrar eden sayfa (ilki kalır) atılır; `MAX_MOBILE_TABS`'tan sonrası kırpılır.
25
30
  * `label`/`icon` boş metinse yazılmaz.
31
+ * - `badge` yalnız `{ source: 'state', ref }` ve `ref` metinse yazılır (11a); `reselect` yalnız `keep` ise
32
+ * yazılır — `root` alan yokken de geçerli davranıştır.
26
33
  * - Görünüm alanı yoktur: çubuk temadan çizilir; eski kayıtların `appearance`'ı okunmaz.
27
34
  * - `customWidgetId` yalnız `custom` türde ve metinse yazılır.
35
+ * - `keepStackOnSwitch` yalnız `false`, `hideOnKeyboard` yalnız `true` ise yazılır — alan yokken de bugünkü
36
+ * davranış (yığın korunur, çubuk klavyede görünür) geçerlidir; tanınmayan değer yok sayılır.
28
37
  */
29
38
  export function normalizeMobileTabs(tabs, pages) {
30
39
  const source = record(tabs);
@@ -41,7 +50,8 @@ export function normalizeMobileTabs(tabs, pages) {
41
50
  seen.add(page);
42
51
  const label = text(item?.label);
43
52
  const icon = text(item?.icon);
44
- items.push({ page, ...(label ? { label } : {}), ...(icon ? { icon } : {}) });
53
+ const badge = badgeOf(item?.badge);
54
+ items.push({ page, ...(label ? { label } : {}), ...(icon ? { icon } : {}), ...(badge ? { badge } : {}), ...(item?.reselect === 'keep' ? { reselect: 'keep' } : {}) });
45
55
  if (items.length === MAX_MOBILE_TABS)
46
56
  break;
47
57
  }
@@ -54,7 +64,9 @@ export function normalizeMobileTabs(tabs, pages) {
54
64
  style,
55
65
  items,
56
66
  ...(customWidgetId ? { customWidgetId } : {}),
57
- showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : true
67
+ showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : true,
68
+ ...(source.keepStackOnSwitch === false ? { keepStackOnSwitch: false } : {}),
69
+ ...(source.hideOnKeyboard === true ? { hideOnKeyboard: true } : {})
58
70
  };
59
71
  }
60
72
  /**
@@ -75,6 +87,10 @@ export function mobileTabsProblems(tabs, context = {}) {
75
87
  problems.push(`style '${String(source.style)}' tanınmıyor (default | floating | custom) — 'default' çizilir`);
76
88
  if (source.items !== undefined && !rawItems)
77
89
  problems.push('items dizi olmalı — hiçbir sekme çizilmez');
90
+ for (const key of ['keepStackOnSwitch', 'hideOnKeyboard']) {
91
+ if (source[key] !== undefined && typeof source[key] !== 'boolean')
92
+ problems.push(`${key} boolean olmalı (gelen: ${JSON.stringify(source[key])}) — yok sayılır, varsayılan davranış geçerli`);
93
+ }
78
94
  const items = rawItems ?? [];
79
95
  const pageIds = items.map(entryPage);
80
96
  if (source.enabled === true && items.length === 0)
@@ -87,6 +103,19 @@ export function mobileTabsProblems(tabs, context = {}) {
87
103
  const duplicates = pageIds.filter((id, index) => id && pageIds.indexOf(id) !== index);
88
104
  if (duplicates.length > 0)
89
105
  problems.push(`aynı sayfa birden çok sekmede: ${[...new Set(duplicates)].join(', ')} — tekrarlar atılır`);
106
+ for (const entry of items) {
107
+ const item = record(entry);
108
+ const page = entryPage(entry) ?? '?';
109
+ if (item?.badge !== undefined) {
110
+ const badge = record(item.badge);
111
+ if (!badge || badge.source !== 'state')
112
+ problems.push(`${page} rozet kaynağı '${String(badge?.source ?? item.badge)}' tanınmıyor — bugün yalnız { source: 'state', ref: '<state yolu>' }; rozet çizilmez`);
113
+ else if (!text(badge.ref))
114
+ problems.push(`${page} rozetinde ref (state yolu) yok — rozet çizilmez`);
115
+ }
116
+ if (item?.reselect !== undefined && item.reselect !== 'root' && item.reselect !== 'keep')
117
+ problems.push(`${page} reselect '${String(item.reselect)}' tanınmıyor (root | keep) — ikinci dokunuş başa döner`);
118
+ }
90
119
  if (context.pages) {
91
120
  const known = new Set(context.pages.map(page => page.id));
92
121
  const unknown = pageIds.filter((id) => Boolean(id) && !known.has(id));
@@ -20,7 +20,7 @@ export const webProfile = Object.freeze({
20
20
  },
21
21
  style: { freeStyle: true, hoverStyle: true, resolver: 'css' },
22
22
  steps: {
23
- registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'scan-code']),
23
+ registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'biometric-auth', 'scan-code']),
24
24
  aliases: STEP_ALIASES,
25
25
  infoOnly: new Set()
26
26
  },
@@ -3,7 +3,7 @@ import { PlatformApiError } from '../platform/errors.js';
3
3
  import { reservedTenantTables } from './reservedTables.js';
4
4
  import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
5
5
  import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
6
- import { backendPolicyRules } from './backendConstraints.js';
6
+ import { backendPolicyRules, effectiveDependencyLines } from './backendConstraints.js';
7
7
  import { accessPolicyRules } from './access.js';
8
8
  import { isGuid } from '../platform/refs.js';
9
9
  import { themePolicyRules } from './themeRules.js';
@@ -155,11 +155,13 @@ export function readsBeforeGuard(source, root) {
155
155
  * KENDİSİ tanır (ADR 0004, ZM-07/g-02): bağlıysa hata anında `always` hiç koşmaz, sürer
156
156
  * (microservice.ts:182 `hasBranch`) — eski motor tanımadığı dalı hiç aramadığı için bu ekleme
157
157
  * geriye dönük zararsızdır. `validation` tüm kurallar geçince 'success', biri düşünce 'error' dalına,
158
- * ardından her durumda 'always'e gider (validation.ts; kök ADR 0014, g-18).
158
+ * ardından her durumda 'always'e gider (validation.ts; kök ADR 0014, g-18). microservice'in 'queued' dalı mobilde,
159
+ * `offlineWritable` serviste ağ hatası kuyruğa alınınca koşar (kök ADR 0022, plans/mobil-cevrimdisi-calisma g-02);
160
+ * mobil/bildirim koşulunu tanım taşımadığı için akış aracı FLOW-043 ile platformdan okur.
159
161
  */
160
- const flowBranchNames = {
162
+ export const flowBranchNames = {
161
163
  start: ['always'],
162
- microservice: ['always', 'success', 'error'],
164
+ microservice: ['always', 'success', 'error', 'queued'],
163
165
  validation: ['always', 'success', 'error'],
164
166
  confirm: ['yes', 'no', 'always'],
165
167
  function: ['always'],
@@ -172,6 +174,7 @@ const flowBranchNames = {
172
174
  permission: ['always'],
173
175
  'open-settings': ['always'],
174
176
  'ad-show': ['always', 'error'],
177
+ 'biometric-auth': ['always', 'error'],
175
178
  back: ['always'],
176
179
  'scan-code': ['always', 'error'],
177
180
  end: []
@@ -186,7 +189,7 @@ export const knownFlowStepTypes = new Set([
186
189
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
187
190
  'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
188
191
  'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
189
- 'permission', 'open-settings', 'ad-show', 'back', 'scan-code'
192
+ 'permission', 'open-settings', 'ad-show', 'back', 'biometric-auth', 'scan-code'
190
193
  ]);
191
194
  /**
192
195
  * Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
@@ -200,6 +203,12 @@ export const locationAccuracies = new Set(['low', 'balanced', 'high']);
200
203
  * ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
201
204
  */
202
205
  export const adPlacementNamePattern = /^[a-z][A-Za-z0-9]{0,39}$/;
206
+ /**
207
+ * `biometric-auth` adımının yedeği (`args.value`) — `@ng-apps/flow` `biometric-golden.json` `fallbacks`'ın ve backend
208
+ * `TestData/flows/biometric-fallbacks.json`'ın aynası (`test/flow-step-parity.test.ts` üçünü bağlar); backend
209
+ * `FlowDtoValidator.HasKnownDeviceChoice` küme dışını 422 ile reddeder, boş değer `device`'tır. Kök ADR 0021 madde 2.
210
+ */
211
+ export const biometricFallbacks = new Set(['device', 'none']);
203
212
  /**
204
213
  * `scan-code` adımının kod türleri — `@ng-apps/flow` `SCAN_CODE_TYPES`'ın aynası (kök ADR 0017); altın dosya
205
214
  * `scan-code-golden.json` ile kilitlidir (`test/flow-step-parity.test.ts`), backend `ScanCodeType` aynı kümeyle 422 döner.
@@ -228,7 +237,8 @@ const flowTemplateTextFields = new Map([
228
237
  ['alert', ['message', 'description']],
229
238
  ['confirm', ['message', 'title', 'confirmText', 'cancelText']],
230
239
  ['notification-schedule', ['name', 'title']],
231
- ['notification-cancel', ['name']]
240
+ ['notification-cancel', ['name']],
241
+ ['biometric-auth', ['title', 'cancelText']]
232
242
  ]);
233
243
  // Düz metin alanları: ne ifade ne şablon çözülür (`share.title` resolveShareContent'te, bildirim `path`'i K4 kapsamı dışında).
234
244
  const flowLiteralTextFields = new Map([
@@ -646,6 +656,26 @@ export const policyRules = [
646
656
  }];
647
657
  }
648
658
  },
659
+ {
660
+ id: 'MS-025',
661
+ module: 'microservice',
662
+ appliesTo: ['create', 'update'],
663
+ sourceRef: "Kök ADR 0022 (b-03, g-02): 'offlineWritable' kuyruktan gelen yazmayı kabul eder; okuyan uç hiç kuyruğa girmez — akış okumayı veri önbelleğinden alır (faz 04). Okuyan: tablo 'get'/'count' (MicroserviceTableOperation'da 'list' yoktur) ve görünüm kaynağı (ViewMicroserviceExecutor yalnız arar). Bildirim orada anlamsızdır ve okunan ucu 'yazan' gibi gösterir",
664
+ check: definition => {
665
+ const parameters = (definition.sourceParameters ?? {});
666
+ const source = String(definition.source ?? '').toLowerCase();
667
+ const operation = String(parameters.operation ?? '').toLowerCase();
668
+ const reads = source === 'view' || (source === 'table' && ['get', 'count'].includes(operation));
669
+ if (definition.offlineWritable !== true || !reads)
670
+ return [];
671
+ return [{
672
+ ruleId: 'MS-025',
673
+ severity: 'warn',
674
+ message: `offlineWritable bir okuma ucuna verilmiş (${source === 'view' ? "kaynak: 'view'" : `operation: '${operation}'`}): kuyruk yalnız yazmaları tutar, okuma çevrimdışıyken veri önbelleğinden gelir (uygulamanın 'mobile.offline.data' ayarı). Bildirimi kaldırın; yazan uçta (create/update/upsert/delete ya da yazan iş akışı) kalsın.`,
675
+ guide: 'ngapps://guides/page'
676
+ }];
677
+ }
678
+ },
649
679
  {
650
680
  id: 'QRY-006',
651
681
  module: 'query',
@@ -697,6 +727,39 @@ export const policyRules = [
697
727
  }];
698
728
  }
699
729
  },
730
+ {
731
+ id: 'MS-019',
732
+ module: 'microservice',
733
+ appliesTo: ['create', 'update', 'preview'],
734
+ sourceRef: "ZM-50 / b-24 (ADR 0018): TableMicroserviceExecutor.ValidateAsync update/upsert/delete'te tek kök ister — tek filtre ya da çocukları yalnız And bağlı tekil filtre olan bir grup; koşullardan benzersiz olanlardan biri 'eq' olmalı (FilterFieldMustUniqueEqException). AND/OR çocuğun bir öncekine bağlantısıdır (PostgreSqlEntityConnector.ApplyFilters Where/OrWhere). Alanın benzersizliği gateway'de bilinmez; FilterFieldMustUniqueException backend'de kalır",
735
+ check: definition => {
736
+ const parameters = (definition.sourceParameters ?? {});
737
+ const operation = String(parameters.operation ?? '').toLowerCase();
738
+ if (definition.source !== 'table')
739
+ return [];
740
+ if (operation !== 'update' && operation !== 'upsert' && operation !== 'delete')
741
+ return [];
742
+ const roots = asArray(parameters.filters).filter((filter) => Boolean(filter) && typeof filter === 'object');
743
+ const recipe = "Tek satırı hedefleyin: ya tek filtre { filterType: 'single', field: <benzersiz alan>, operator: 'eq' } ya da tek kök grup { filterType: 'group', children: [<benzersiz alan> eq, <sahip alanı> eq {{user.id}} …] } — çocukların hepsi conditionType: 'and' (ilki dahil), iç içe grup yok. Ek koşul yalnız daraltır; tutmazsa işlem 0 satır döner.";
744
+ const root = roots.length === 1 ? roots[0] : undefined;
745
+ const children = asArray(root?.children).map(child => (child ?? {}));
746
+ const problem = root === undefined
747
+ ? `kökte ${roots.length} filtre var; update/upsert/delete tam bir kök ister (InvalidFilterCountException)`
748
+ : root.filterType !== 'group'
749
+ ? String(root.operator ?? 'eq') === 'eq' ? '' : `tek filtrenin işleci '${String(root.operator)}' — 'eq' dışındaki işleç hedefi tek satırdan çok satıra genişletir (ör. 'id neq X' X dışındaki bütün satırları değiştirir)`
750
+ : children.some(child => child.filterType === 'group' || String(child.conditionType ?? 'and') !== 'and')
751
+ ? "grubun bir çocuğu 'or' ile bağlı ya da iç içe grup — AND/OR çocuğun bir öncekine bağlantısıdır, 'or' hedefi tek satırdan genişletir"
752
+ : children.some(child => String(child.operator ?? 'eq') === 'eq') ? '' : "grupta 'eq' işleçli koşul yok — hedefi benzersiz alan + 'eq' belirler";
753
+ return problem === ''
754
+ ? []
755
+ : [{
756
+ ruleId: 'MS-019',
757
+ severity: 'block',
758
+ message: `operation '${operation}' filtre ağacı tek satırı hedeflemiyor: ${problem}. Platform kaydı FilterFieldMustUniqueEqException ile reddeder. ${recipe} Toplu yazma bulk-update/bulk-delete ucuyla yapılır.`,
759
+ guide: 'ngapps://guides/microservice'
760
+ }];
761
+ }
762
+ },
700
763
  {
701
764
  id: 'MS-002',
702
765
  module: 'microservice',
@@ -2547,7 +2610,7 @@ export const policyRules = [
2547
2610
  id: 'FLOW-022',
2548
2611
  module: 'flow',
2549
2612
  appliesTo: ['create', 'update'],
2550
- sourceRef: "Metin şablonu (kök docs/adr/0003-cihaz-yetenegi-adimlari.md §Metin şablonu, plans/mobil-acik-isler K4, 2026-10-08): alert.ts/confirm.ts (web) ve ui-native runtime-tasks.ts + @ng-apps/flow device.functions.ts (cihaz, önizleme) `alert.message/description`, `confirm.message/title/confirmText/cancelText`, `notification-schedule.name/title`, `notification-cancel.name`'i @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan geçirir: yalnız `{{ }}` içi çözülür (çeviri `t('anahtar')`, `state`, `Steps`, `item`), dışındaki çıplak ifade ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu; 38 S-12, 37 S-11). Düz metin kalanlar: share `title` (resolveShareContent olduğu gibi verir; `message` ve `path` ifadedir) ve notification-schedule `path` (K4 kapsamı dışı) — orada `{{ }}` de çözülmez",
2613
+ sourceRef: "Metin şablonu (kök docs/adr/0003-cihaz-yetenegi-adimlari.md §Metin şablonu, plans/mobil-acik-isler K4, 2026-10-08): alert.ts/confirm.ts (web) ve ui-native runtime-tasks.ts + @ng-apps/flow device.functions.ts (cihaz, önizleme) `alert.message/description`, `confirm.message/title/confirmText/cancelText`, `notification-schedule.name/title`, `notification-cancel.name`, `biometric-auth.title/cancelText` (kök ADR 0021)'i @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan geçirir: yalnız `{{ }}` içi çözülür (çeviri `t('anahtar')`, `state`, `Steps`, `item`), dışındaki çıplak ifade ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu; 38 S-12, 37 S-11). Düz metin kalanlar: share `title` (resolveShareContent olduğu gibi verir; `message` ve `path` ifadedir) ve notification-schedule `path` (K4 kapsamı dışı) — orada `{{ }}` de çözülmez",
2551
2614
  check: definition => flowAllSteps(definition)
2552
2615
  .flatMap((step) => {
2553
2616
  const type = String(step.type ?? '');
@@ -2717,6 +2780,87 @@ export const policyRules = [
2717
2780
  return findings;
2718
2781
  })
2719
2782
  },
2783
+ {
2784
+ id: 'FLOW-042',
2785
+ module: 'flow',
2786
+ appliesTo: ['create', 'update'],
2787
+ sourceRef: "Biyometrik doğrulama adımı (kök docs/adr/0021-biyometrik-dogrulama-adimi-ve-uygulama-kilidi.md madde 2): args.title sistem penceresindeki gerekçe, args.cancelText vazgeç düğmesi (ikisi de metin şablonu), args.value yedek — DÜZ METİN, device (biyometri olmazsa cihaz parolası; boşsa bu) ya da none. Backend FlowDtoValidator küme dışını 422 ile reddeder; fallback/prompt/reason/cancelLabel gibi anahtarlar StepManagementArgument sözleşmesinde YOKTUR ve 200 ile sessizce düşer. Vazgeçme bir hatadır: adımın error dalı → akışın hata adımları → o da yoksa akış bu adımda BİTER (@ng-apps/flow device.functions.ts settle 'stop'); always'e bağlı sonraki adım koşmaz. Web uygulaması ve production /view adımı soramaz, unsupported döner",
2788
+ check: definition => {
2789
+ // flow-engine.ts hasErrorFlow(): yalnız start taşıyan errorSteps hata yolu sayılmaz (FLOW-030 ile aynı kural).
2790
+ // Hata akışının İÇİNDEKİ adım için hata akışı yoktur: motor onu `${id}__error` olarak hata adımsız kurar
2791
+ // (flow-engine.ts), `onError` false döner ve `settle` 'stop' o akışı bitirir.
2792
+ const errorSteps = flowStepList(definition.errorSteps);
2793
+ const errorFlow = errorSteps.some(step => step.type !== 'start');
2794
+ return flowAllSteps(definition)
2795
+ .filter(step => step.type === 'biometric-auth')
2796
+ .flatMap((step) => {
2797
+ const label = flowStepLabel(step);
2798
+ // Sonuç adımın ADIYLA yazılır (device.functions.ts `machine.setStorage(task.name, …)`), stepId'yle değil.
2799
+ const resultName = step.name || label;
2800
+ const args = (step.args ?? {});
2801
+ // Backend `HasKnownDeviceChoice` ve runtime `biometricFallback` değeri KIRPMADAN karşılaştırır: ' none' 422'dir.
2802
+ const value = typeof args.value === 'string' ? args.value : '';
2803
+ const unquoted = value.trim().replace(/^['"`](.*)['"`]$/, '$1');
2804
+ const findings = [];
2805
+ if (value.trim() && unquoted !== value.trim()) {
2806
+ findings.push({
2807
+ ruleId: 'FLOW-042',
2808
+ severity: 'block',
2809
+ message: `'${label}' biometric-auth adımının yedeği tırnaklı yazılmış: ${value}. Bu alan DÜZ METİNDİR, ifade değil — args.value: '${unquoted}' yazın.`,
2810
+ guide: 'ngapps://guides/mobile-steps'
2811
+ });
2812
+ }
2813
+ else if (value.trim() && !biometricFallbacks.has(value)) {
2814
+ findings.push({
2815
+ ruleId: 'FLOW-042',
2816
+ severity: 'block',
2817
+ message: `'${label}' biometric-auth adımının yedeği '${value}' kümede değil: args.value ${[...biometricFallbacks].join(', ')} olur (boş = device; ifade ve çevresinde boşluk yazılamaz). Platform 422 döner.`,
2818
+ guide: 'ngapps://guides/mobile-steps'
2819
+ });
2820
+ }
2821
+ if (args.fallback !== undefined && args.fallback !== null && args.fallback !== '') {
2822
+ const kept = args.fallback === value;
2823
+ findings.push({
2824
+ ruleId: 'FLOW-042',
2825
+ severity: kept ? 'warn' : 'block',
2826
+ message: kept
2827
+ ? `'${label}' biometric-auth adımında args.fallback var: sözleşmede YOK ve 200 ile sessizce düşer. Yedek args.value'da zaten '${value}' — args.fallback'ı kaldırın.`
2828
+ : `'${label}' biometric-auth adımında args.fallback var: yedek args.value alanındadır (${[...biometricFallbacks].join(' | ')}). args.fallback sözleşmede YOK ve 200 ile sessizce düşer — adım args.value ile ('${value || 'device'}') kaydedilir, istediğiniz yedek kaybolur.`,
2829
+ guide: 'ngapps://guides/mobile-steps'
2830
+ });
2831
+ }
2832
+ const reasonKeys = ['reason', 'prompt', 'promptMessage'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2833
+ if (reasonKeys.length > 0) {
2834
+ findings.push({
2835
+ ruleId: 'FLOW-042',
2836
+ severity: 'warn',
2837
+ message: `'${label}' biometric-auth adımında args.${reasonKeys.join('/args.')} var: sistem penceresindeki gerekçe args.title alanındadır. Bu anahtar sözleşmede YOK ve 200 ile sessizce düşer; pencere uygulama dilindeki varsayılan gerekçeyle açılır.`,
2838
+ guide: 'ngapps://guides/mobile-steps'
2839
+ });
2840
+ }
2841
+ const cancelKeys = ['cancelLabel', 'cancel'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
2842
+ if (cancelKeys.length > 0) {
2843
+ findings.push({
2844
+ ruleId: 'FLOW-042',
2845
+ severity: 'warn',
2846
+ message: `'${label}' biometric-auth adımında args.${cancelKeys.join('/args.')} var: vazgeç düğmesinin metni args.cancelText alanındadır. Bu anahtar sözleşmede YOK ve 200 ile sessizce düşer.`,
2847
+ guide: 'ngapps://guides/mobile-steps'
2848
+ });
2849
+ }
2850
+ const errorBranch = flowBranches(step).some(branch => branch.name === 'error' && Boolean(branch.nextStepId));
2851
+ const insideErrorFlow = errorSteps.includes(step);
2852
+ if (!errorBranch && (insideErrorFlow || !errorFlow)) {
2853
+ findings.push({
2854
+ ruleId: 'FLOW-042',
2855
+ severity: 'warn',
2856
+ message: `'${label}' biometric-auth adımının error dalı da akışın hata adımları da bağlı değil: kullanıcı vazgeçince ya da doğrulama olmayınca akış BU ADIMDA BİTER, always'e bağlı sonraki adım koşmaz (kapı kapalı kalır). Kullanıcıya bir şey söylenecekse error dalını bağlayın${insideErrorFlow ? ' (adım hata akışının içinde: hata akışının kendi hata akışı yoktur)' : ''} — Steps.${resultName}.error.status: cancelled, not-enrolled, lockout, failed, unavailable, unsupported (web uygulaması ve production /view), invalid.`,
2857
+ guide: 'ngapps://guides/mobile-steps'
2858
+ });
2859
+ }
2860
+ return findings;
2861
+ });
2862
+ }
2863
+ },
2720
2864
  {
2721
2865
  id: 'FLOW-042',
2722
2866
  module: 'flow',
@@ -3638,6 +3782,22 @@ export const policyRules = [
3638
3782
  }];
3639
3783
  }
3640
3784
  },
3785
+ {
3786
+ id: 'MF-007',
3787
+ module: 'microfunction',
3788
+ appliesTo: ['create', 'update'],
3789
+ sourceRef: "LambdaMicrofunctionConnector paketi olduğu gibi yükler; MicrofunctionPackage.RequiresBuild yalnız Built-In'de (FissionMicrofunctionConnector) build'e dönüşür. Karar: kök docs/adr/0001-microfunction-bagimlilik-bildirimi-ve-build-yolu.md, plans/microfunction-faz-2 K15",
3790
+ check: definition => String(definition.provider ?? '') === 'aws-lambda' &&
3791
+ String(definition.sourceKind ?? 'inline') === 'inline' &&
3792
+ effectiveDependencyLines(definition.dependencies).length > 0
3793
+ ? [{
3794
+ ruleId: 'MF-007',
3795
+ severity: 'warn',
3796
+ message: "provider 'aws-lambda' kayıtta dependencies KURULMAZ: bağımlılık build'i yalnız Built-In'de (Fission builder) koşar, Lambda'ya paket olduğu gibi gider. Paketler kurulmuş hâlde zip'in içinde gelmeli (archive kaynağı; Python'da paket klasörleri, Node'da node_modules). Aksi hâlde yayının probe adımı import hatasıyla publish-failed olur.",
3797
+ guide: 'ngapps://guides/microfunction'
3798
+ }]
3799
+ : []
3800
+ },
3641
3801
  // 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):
3642
3802
  // tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
3643
3803
  ...themePolicyRules,
@@ -3726,6 +3886,59 @@ export async function microfunctionStepFindings(platform, project, environment,
3726
3886
  }
3727
3887
  return applyPolicyMode(platform.config, findings, policyOverride);
3728
3888
  }
3889
+ /**
3890
+ * MF-006'nın canlı yarısı: işlem listesi kaydın entrypoint modülünü siliyor ya da taşıyorsa uyarır.
3891
+ * Block değildir — backend yazmayı kabul eder, ama yayın kapısı
3892
+ * (`MicrofunctionService.PublishAsync`) modül zip'te yoksa 400 döner; kullanıcı sonra entrypoint'i
3893
+ * `microfunction_update` ile yeni modüle çevirebilir. Modül yolu platformdaki
3894
+ * `MicrofunctionLanguageProfile.ModulePath` kuralıdır: son '.'dan öncesi + dilin uzantısı.
3895
+ * Sonraki bir işlem aynı yola dosya yazar ya da oraya taşırsa uyarı düşer.
3896
+ *
3897
+ * Okuma başarısız olursa kural susar (WF-MF-001 ile aynı gerekçe).
3898
+ */
3899
+ export async function microfunctionArchiveEntrypointFindings(platform, project, environment, id, operations) {
3900
+ if (platform.config.policyMode === 'off')
3901
+ return [];
3902
+ let record;
3903
+ try {
3904
+ const payload = await platform.tenant('GET', project, `/microfunctions/${id}`, { environment });
3905
+ record = (payload?.data ?? payload);
3906
+ }
3907
+ catch {
3908
+ return [];
3909
+ }
3910
+ const entrypoint = hasText(record?.entrypoint) ? String(record.entrypoint) : '';
3911
+ const separatorIndex = entrypoint.lastIndexOf('.');
3912
+ const extension = microfunctionModuleExtensions[String(record?.language ?? '')];
3913
+ if (separatorIndex <= 0 || extension === undefined)
3914
+ return [];
3915
+ const modulePath = `${entrypoint.slice(0, separatorIndex)}${extension}`;
3916
+ let breaking;
3917
+ const archiveOperations = (Array.isArray(operations) ? operations : [])
3918
+ .filter((operation) => operation !== null && typeof operation === 'object');
3919
+ const coversModule = (path) => path === modulePath || modulePath.startsWith(`${path}/`);
3920
+ archiveOperations.forEach((operation, index) => {
3921
+ const type = String(operation.type ?? '');
3922
+ const path = String(operation.path ?? '');
3923
+ if ((type === 'delete' || type === 'rename') && coversModule(path))
3924
+ breaking = { index, type, path };
3925
+ if (type === 'write-file' && path === modulePath)
3926
+ breaking = undefined;
3927
+ // Modülün klasörünü geri getiren taşıma da onarır (lib → src → lib); içerik bilinmez, uyarı sezgiseldir.
3928
+ if (type === 'rename' && coversModule(String(operation.newPath ?? '')))
3929
+ breaking = undefined;
3930
+ });
3931
+ if (breaking === undefined)
3932
+ return [];
3933
+ return [{
3934
+ ruleId: 'MF-006',
3935
+ severity: 'warn',
3936
+ message: `operations[${breaking.index}] (${breaking.type} ${breaking.path}) kaydın entrypoint modülünü (${modulePath}, entrypoint '${entrypoint}') ${breaking.type === 'delete' ? 'siliyor' : 'taşıyor'}. Değişiklik yazılır ama yayın 400 ile düşer (MicrofunctionArchiveInvalidException): ya aynı yola dosya yazın ya da microfunction_update ile entrypoint'i yeni modüle çevirin.`,
3937
+ guide: 'ngapps://guides/microfunction'
3938
+ }];
3939
+ }
3940
+ /** `MicrofunctionLanguageProfile.ModuleExtension` — dil adı bilgi katmanında taşınır. */
3941
+ const microfunctionModuleExtensions = { node: '.js', python: '.py' };
3729
3942
  /**
3730
3943
  * WF-032'nin ikinci yarısı. `rule.expression`'ın işe yarayıp yaramayacağı gövdeden görülemez:
3731
3944
  * `WorkflowStep` ifadeyi YALNIZ çağrılan akışın parametre DTO'su varken değerlendirir