@appaflytech/zeyrek-ai-mcp 0.0.7 → 0.0.9

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 (54) hide show
  1. package/README.md +27 -21
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/backendConstraints.js +3 -0
  4. package/dist/knowledge/componentCatalog.js +172 -33
  5. package/dist/knowledge/componentEvents.js +35 -4
  6. package/dist/knowledge/errorPath.js +1 -1
  7. package/dist/knowledge/normalize.js +474 -23
  8. package/dist/knowledge/propTypes.js +5 -4
  9. package/dist/knowledge/rules.js +113 -6
  10. package/dist/knowledge/styleResolve.js +52 -5
  11. package/dist/knowledge/themeResolve.js +936 -0
  12. package/dist/knowledge/themeRules.js +221 -0
  13. package/dist/knowledge/themeSchema.js +335 -0
  14. package/dist/platform/domSummary.js +144 -0
  15. package/dist/platform/http.js +10 -2
  16. package/dist/platform/screenshot.js +492 -29
  17. package/dist/platform/screenshotMemory.js +55 -0
  18. package/dist/platform/strictArguments.js +32 -0
  19. package/dist/platform/writeDiff.js +6 -3
  20. package/dist/resources/examples/layout-app-shell.json +1 -1
  21. package/dist/resources/examples/page-sign-in.json +1 -1
  22. package/dist/resources/examples/theme-brand.json +162 -0
  23. package/dist/resources/guides/component.md +8 -2
  24. package/dist/resources/guides/expressions.md +6 -0
  25. package/dist/resources/guides/getting-started.md +6 -0
  26. package/dist/resources/guides/layout.md +36 -26
  27. package/dist/resources/guides/microservice.md +160 -6
  28. package/dist/resources/guides/page.md +165 -59
  29. package/dist/resources/guides/theme.md +210 -0
  30. package/dist/resources/guides/translation.md +13 -10
  31. package/dist/resources/guides/workflow.md +141 -7
  32. package/dist/resources/schema-fingerprint.json +2 -2
  33. package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
  34. package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
  35. package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
  36. package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
  37. package/dist/resources/schemas/StepDto.json +4 -1
  38. package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
  39. package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
  40. package/dist/server.js +52 -42
  41. package/dist/tools/components.js +8 -0
  42. package/dist/tools/designSchemaPatch.js +152 -0
  43. package/dist/tools/flows.js +24 -21
  44. package/dist/tools/layouts.js +8 -8
  45. package/dist/tools/microservices.js +74 -10
  46. package/dist/tools/pages.js +275 -332
  47. package/dist/tools/projects.js +66 -9
  48. package/dist/tools/queries.js +72 -6
  49. package/dist/tools/schemaOps.js +280 -0
  50. package/dist/tools/screenWrite.js +2 -1
  51. package/dist/tools/screens.js +0 -7
  52. package/dist/tools/themes.js +163 -0
  53. package/dist/tools/workflows.js +10 -6
  54. package/package.json +1 -1
@@ -4,6 +4,7 @@ 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
6
  import { backendPolicyRules } from './backendConstraints.js';
7
+ import { themePolicyRules } from './themeRules.js';
7
8
  const terminalStepTypes = new Set(['response', 'convert', 'revert']);
8
9
  const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
9
10
  const stepKindMatrix = {
@@ -125,7 +126,7 @@ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
125
126
  // Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
126
127
  const flowLiteralTextFields = new Map([
127
128
  ['alert', ['message', 'description']],
128
- ['confirm', ['message']]
129
+ ['confirm', ['message', 'title', 'confirmText', 'cancelText']]
129
130
  ]);
130
131
  const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
131
132
  // Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
@@ -235,7 +236,7 @@ export function flowExpressionSites(step) {
235
236
  }
236
237
  return sites;
237
238
  }
238
- const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
239
+ const writeOperations = new Set(['create', 'update', 'delete', 'upsert', 'bulk-create', 'bulk-update', 'bulk-delete']);
239
240
  /** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
240
241
  const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
241
242
  const rolesSlugFieldId = '0199c35d-cfc8-77d4-9c20-29b857153147';
@@ -346,6 +347,38 @@ function unreadSelectFields(step, steps) {
346
347
  .map(field => String(field.fieldSlug ?? field.slug ?? ''))
347
348
  .filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
348
349
  }
350
+ const SQL_COMMENT_PATTERN = /--[^\n]*|\/\*[\s\S]*?\*\//g;
351
+ const QUERY_PARAMETER_TOKEN = String.raw `(?:\{\{\s*([A-Za-z_]\w*)\s*\}\}|(?<![:@\w'])@([A-Za-z_]\w*))(?:\s*::\s*\w+)*`;
352
+ const EMPTY_STRING_COMPARISON_PATTERN = new RegExp(`${QUERY_PARAMETER_TOKEN}\\s*=\\s*(?<!')''(?!')|(?<!')''\\s*=\\s*${QUERY_PARAMETER_TOKEN}`, 'g');
353
+ /**
354
+ * `rawQuery`'de bir parametreyi boş string'le EŞİTLİK üzerinden karşılaştıran yerlerin parametre
355
+ * adları (`@p = ''`, `@p::text = ''`, `'' = {{p}}`). Yorumlar önce atılır — platform da yorum
356
+ * içindeki belirteci parametre saymıyor. Yalnız `=` aranır: `@p <> ''` boş değerde NULL (yanlış)
357
+ * verir ve "doluysa" niyetiyle yazıldığında sonuç zaten doğrudur.
358
+ */
359
+ function emptyStringComparisons(rawQuery) {
360
+ if (typeof rawQuery !== 'string')
361
+ return [];
362
+ const names = new Set();
363
+ for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(EMPTY_STRING_COMPARISON_PATTERN)) {
364
+ const name = match[1] ?? match[2] ?? match[3] ?? match[4];
365
+ if (name !== undefined)
366
+ names.add(name);
367
+ }
368
+ return [...names];
369
+ }
370
+ /** `rawQuery`'nin başvurduğu parametre adları (`@p`, `{{p}}`), yorumlar atılarak; ilk görülme sırasıyla. */
371
+ export function rawQueryParameterNames(rawQuery) {
372
+ if (typeof rawQuery !== 'string')
373
+ return [];
374
+ const names = new Set();
375
+ for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(new RegExp(QUERY_PARAMETER_TOKEN, 'g'))) {
376
+ const name = match[1] ?? match[2];
377
+ if (name !== undefined)
378
+ names.add(name);
379
+ }
380
+ return [...names];
381
+ }
349
382
  const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
350
383
  const functionsGuide = 'ngapps://guides/functions';
351
384
  export const policyRules = [
@@ -418,7 +451,7 @@ export const policyRules = [
418
451
  id: 'MS-016',
419
452
  module: 'microservice',
420
453
  appliesTo: ['create', 'update', 'preview'],
421
- 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ı",
454
+ 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. Alan bazlı writeOn (create|update|always) 2026-09-04'te eklendi (B-12), bu yüzden kural artık çözümü söyler ve writeOn:'create' taşıyan alanda susar",
422
455
  check: definition => {
423
456
  const parameters = (definition.sourceParameters ?? {});
424
457
  if (String(parameters.operation ?? '') !== 'upsert')
@@ -426,13 +459,14 @@ export const policyRules = [
426
459
  const generated = asArray(parameters.fields)
427
460
  .map(entry => entry)
428
461
  .filter(field => String(field.source ?? '') === 'expression')
462
+ .filter(field => String(field.writeOn ?? 'always').toLowerCase() !== 'create')
429
463
  .map(field => String(field.alias ?? field.fieldId ?? '?'));
430
464
  if (generated.length === 0)
431
465
  return [];
432
466
  return [{
433
467
  ruleId: 'MS-016',
434
468
  severity: 'warn',
435
- 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.`,
469
+ 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 yazılacaksa fields[] öğesine writeOn: 'create' koyun — upsert'in UPDATE dalı o alanı satıra hiç koymaz ve bu uyarı susar. writeOn yalnız operation:'upsert' ucunda geçerlidir (MS-017) ve bir 'kısmi güncelleme' bayrağı DEĞİLDİR: listelenen alan gövdede gelmezse yine NULL yazılır.`,
436
470
  guide: 'ngapps://guides/microservice'
437
471
  }];
438
472
  }
@@ -451,6 +485,23 @@ export const policyRules = [
451
485
  guide: 'ngapps://guides/microservice'
452
486
  }]
453
487
  },
488
+ {
489
+ id: 'QRY-007',
490
+ module: 'query',
491
+ appliesTo: ['create', 'update', 'preview'],
492
+ sourceRef: "2026-09-15 bulgusu: pano sorgusu '(@tech = '' OR w.technician_id::text = @tech)' ile 0 satır döndü, hata yok. Kök neden Query.PrepareQueryParameters / NormalizePreviewParameters: boş ya da yalnız boşluktan oluşan değer NULL bağlanır, bu yüzden '@p = ''' hiçbir değerle doğru olmaz. Bulgunun 'dokunulmamış dropdown null gönderiyor' teşhisi yanlıştı — '' gönderen form da aynı sonucu alır. warn: kalıp hatalı ama yazmayı durdurmak gerekmiyor",
493
+ check: definition => {
494
+ const names = emptyStringComparisons(definition.rawQuery);
495
+ if (names.length === 0)
496
+ return [];
497
+ return [{
498
+ ruleId: 'QRY-007',
499
+ severity: 'warn',
500
+ message: `rawQuery parametreyi boş string'le karşılaştırıyor (${names.map(name => `@${name} = ''`).join(', ')}): platform boş ya da yalnız boşluktan oluşan parametre değerini NULL bağlar, NULL = '' hiçbir zaman doğru olmaz ve 'filtre boşsa hepsini getir' dalı ASLA çalışmaz — filtre boş geldiğinde sorgu hatasız 0 satır döner. Değerin form bileşeninden null ya da '' gelmesi fark etmez. Şöyle yazın: (@${names[0]} IS NULL OR kolon::text = @${names[0]}).`,
501
+ guide: 'ngapps://guides/microservice'
502
+ }];
503
+ }
504
+ },
454
505
  {
455
506
  id: 'MS-018',
456
507
  module: 'microservice',
@@ -2215,7 +2266,7 @@ export const policyRules = [
2215
2266
  id: 'FLOW-022',
2216
2267
  module: 'flow',
2217
2268
  appliesTo: ['create', 'update'],
2218
- 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)",
2269
+ sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). 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)",
2219
2270
  check: definition => flowAllSteps(definition)
2220
2271
  .flatMap((step) => {
2221
2272
  const type = String(step.type ?? '');
@@ -2666,7 +2717,7 @@ export const policyRules = [
2666
2717
  id: 'WF-021',
2667
2718
  module: 'workflow',
2668
2719
  appliesTo: ['create', 'update'],
2669
- sourceRef: "IStep.cs:74-86 bağlanmamış/eksik fail dalını global-error dalına yönlendirir; ikisi de yoksa ActivityContext.cs:176-183 ham exception mesajını 500 gövdesine basar. automation'da 'response' adımı yasak (stepKindMatrix) olduğu için gateway bu tipte enjeksiyon yapmaz — errorPath.ts",
2720
+ sourceRef: "IStep.cs:88-104 bağlanmamış/eksik fail dalını global-error dalına yönlendirir; ikisi de yoksa canlıda WorkflowStepFailedException fırlatılır (2026-09-16), önizlemede ActivityContext.SetUnhandledError 500 gövdesi kurar. automation'da 'response' adımı yasak (stepKindMatrix) olduğu için gateway bu tipte enjeksiyon yapmaz — errorPath.ts",
2670
2721
  check: (definition, context) => {
2671
2722
  if (context.workflowKind !== 'automation')
2672
2723
  return [];
@@ -2738,6 +2789,7 @@ export const policyRules = [
2738
2789
  },
2739
2790
  // 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):
2740
2791
  // tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
2792
+ ...themePolicyRules,
2741
2793
  ...backendPolicyRules()
2742
2794
  ];
2743
2795
  /**
@@ -2821,3 +2873,58 @@ export async function microfunctionStepFindings(platform, project, environment,
2821
2873
  }
2822
2874
  return applyPolicyMode(platform.config, findings, policyOverride);
2823
2875
  }
2876
+ const partialWriteOperations = new Set(['create', 'update', 'upsert']);
2877
+ const inputReferencePattern = /\binput\.([A-Za-z_$][\w$]*)/g;
2878
+ function inputReferencesOf(rule) {
2879
+ return asArray(rule.fields)
2880
+ .filter((field) => Boolean(field) && typeof field === 'object')
2881
+ .flatMap(field => [...String(field.value ?? '').matchAll(inputReferencePattern)].map(match => match[1]))
2882
+ .filter((name) => typeof name === 'string');
2883
+ }
2884
+ /**
2885
+ * WF-031 — yazma adımı, akışın giriş DTO'sunda OLMAYAN bir `input.x` alanını yazıyorsa uyarır.
2886
+ * Böyle bir ifade çalışma anında `undefined` döner, `Parse` onu `null` yapar ve platform kolonu
2887
+ * NULL yazar: satırdaki mevcut değer silinir, yanıt yine 200'dür (risk uygulaması Kanban taşıma
2888
+ * bulgusu, 2026-09-15 — `{ id, status }` gövdesi `title`/`owner_id`/`due_date`'i sildi).
2889
+ *
2890
+ * Kural yalnız DTO'da HİÇ olmayan alanı sayar: "DTO'da var ama çağıran göndermedi" vakası
2891
+ * tanımdan görülemez, onu rehber anlatır. DTO şeması okunamazsa (404/erişim) kural susar —
2892
+ * bir okuma hatası yazma çağrısını engellememelidir.
2893
+ */
2894
+ export async function undefinedInputFieldFindings(platform, project, environment, definition, kind, policyOverride = false) {
2895
+ if (platform.config.policyMode === 'off')
2896
+ return [];
2897
+ const body = workflowBody(definition, kind);
2898
+ if (!hasText(body.parameterDtoId))
2899
+ return [];
2900
+ const candidates = workflowSteps(body)
2901
+ .filter(step => step.type === 'table' && step.rule)
2902
+ .filter(step => partialWriteOperations.has(String(step.rule.operation ?? '').toLowerCase()))
2903
+ .map(step => ({ step, references: [...new Set(inputReferencesOf(step.rule))] }))
2904
+ .filter(candidate => candidate.references.length > 0);
2905
+ if (candidates.length === 0)
2906
+ return [];
2907
+ let dtoFieldNames;
2908
+ try {
2909
+ const payload = await platform.tenant('GET', project, `/datatransferobjects/${String(body.parameterDtoId)}/scheme`, { environment });
2910
+ const items = asArray(payload?.data ?? payload).filter((item) => Boolean(item) && typeof item === 'object');
2911
+ dtoFieldNames = new Set(items.map(item => String(item.name ?? '')));
2912
+ }
2913
+ catch (error) {
2914
+ if (error instanceof PlatformApiError)
2915
+ return [];
2916
+ throw error;
2917
+ }
2918
+ const findings = candidates.flatMap(({ step, references }) => {
2919
+ const missing = references.filter(name => !dtoFieldNames.has(name));
2920
+ return missing.length > 0
2921
+ ? [{
2922
+ ruleId: 'WF-031',
2923
+ severity: 'warn',
2924
+ message: `'${stepLabel(step)}' adımı ${missing.map(name => `input.${name}`).join(', ')} alanlarını yazıyor ama akışın giriş DTO'sunda bu alanlar YOK: ifade undefined döner, platform kolonu NULL yazar ve satırdaki mevcut değer SİLİNİR (yanıt yine 200). Yazma adımında SET listesi fields[]'tir, gövde değil — 'göndermediğim alan değişmez' (REST PATCH) sezgisi burada geçmez. İki doğru çıkış: (1) adımı EYLEM BAŞINA daraltın, fields[] yalnız bu eylemin gerçekten yazdığı alanları listelesin (kanban taşıma: yalnız status); (2) alanı DTO'ya ekleyin VE çağıranın her istekte gönderdiğinden emin olun — DTO'ya eklemek yalnız izin verir, göndermeyi garanti etmez. Alan kümesi ancak çağrı anında belliyse akışta oku-birleştir: önce table get, sonra "typeof input.x !== 'undefined' && input.x !== null ? input.x : read_row.data.x" (bedeli fazladan okuma ve son-yazan-kazanır).`,
2925
+ guide: 'ngapps://guides/workflow'
2926
+ }]
2927
+ : [];
2928
+ });
2929
+ return applyPolicyMode(platform.config, findings, policyOverride);
2930
+ }
@@ -22,6 +22,12 @@
22
22
  * `TYPED_STYLE_COMPONENTS` kaydına eklenir — başka hiçbir yer değişmez.
23
23
  */
24
24
  export const STYLE_PRESET_KEY = '$preset';
25
+ /**
26
+ * Düğümün hangi BİLEŞEN PRESETİ versiyonundan kurulduğunu anan işaret (frontend
27
+ * `style-presets.ts` kopyası). Kaskad katmanı değildir: versiyonun prop'ları düğüm oluşturulurken
28
+ * zaten yazılır, bu anahtar yalnız seçicinin hangi satırı seçili göstereceğini söyler.
29
+ */
30
+ export const STYLE_COMPONENT_PRESET_KEY = '$componentPreset';
25
31
  /**
26
32
  * Satır yüksekliği ÜÇ yazımı da kabul eder: oran (1.4), yüzde ('140%') ve CSS
27
33
  * uzunluğu ('24px'). Çıplak sayı ikircikli tek durumdur — eski panel YÜZDE
@@ -53,17 +59,24 @@ export const deepMergeStyle = (base, override) => {
53
59
  };
54
60
  export const resolveNodeStyleLayers = (style, source, nodeType) => {
55
61
  const styleObject = isPlainObject(style) ? style : undefined;
56
- const { [STYLE_PRESET_KEY]: presetName, ...own } = styleObject ?? {};
62
+ // İKİ sihirli anahtar da düşer. `$componentPreset` bir kaskad katmanı DEĞİLDİR: bileşen preseti
63
+ // versiyonu düğüm kurulurken prop'lara materialize edilir, bu anahtar yalnız "hangi versiyondan
64
+ // geldi"yi anar (`style-presets.ts:180-184`, 2026-09-07). Ayıklanmazsa emotion onu çöp bir CSS
65
+ // kuralına çevirir ve `resolvedStyle` ajana stil diye '$componentPreset: V1' gösterir (S-2).
66
+ const { [STYLE_PRESET_KEY]: presetName, [STYLE_COMPONENT_PRESET_KEY]: componentPreset, ...own } = styleObject ?? {};
57
67
  const componentStyle = nodeType && isPlainObject(source?.componentStyles?.[nodeType]) ? source.componentStyles[nodeType] : undefined;
58
68
  const preset = typeof presetName === 'string' && isPlainObject(source?.presets?.[presetName]) ? source.presets[presetName] : undefined;
59
69
  let theme;
60
70
  if (componentStyle || preset) {
61
71
  theme = deepMergeStyle(componentStyle ?? {}, preset ?? {});
72
+ // Preset'in KENDİSİ de işaret taşıyabilir: tema JSON'u elle düzenlenmişse.
62
73
  delete theme[STYLE_PRESET_KEY];
74
+ delete theme[STYLE_COMPONENT_PRESET_KEY];
63
75
  }
76
+ const version = typeof componentPreset === 'string' && componentPreset ? componentPreset : undefined;
64
77
  if (!styleObject && typeof style === 'string' && style)
65
- return { theme, own: style };
66
- return { theme, own: Object.keys(own).length ? own : undefined };
78
+ return { theme, own: style, componentPreset: version };
79
+ return { theme, own: Object.keys(own).length ? own : undefined, componentPreset: version };
67
80
  };
68
81
  export const resolveNodeStyle = (style, source, nodeType) => {
69
82
  if (!isPlainObject(style) && style !== undefined && style !== null)
@@ -308,6 +321,23 @@ export const applyLegacyTextProps = (input, root) => {
308
321
  : `var(${STATUS_COLOR_VARS[color] ?? STATUS_COLOR_VARS.primary})`;
309
322
  }
310
323
  };
324
+ /* ---- button: emekli kind:'custom' takma adı (frontend `button-style-logic.ts` kopyası) ---- */
325
+ export const BUTTON_LEGACY_CUSTOM_COLOR_VAR = '--ng-btn-custom-color';
326
+ /**
327
+ * `button`'ın EMEKLİ `kind: 'custom'` + `customColor` ikilisi (Dalga 4, 2026-09-14) kayıtlı
328
+ * sayfalarda aynen çizilir: Surface › Background BOŞSA köke eski `--ng-btn-custom-color`
329
+ * değişkeni yazılır, bileşenin `.ng-button--custom` bloğu okur. Bilerek `background` DEĞİL
330
+ * değişken: eski custom outline/text butonlar zemin değil kenarlık/yazı rengi alıyordu.
331
+ */
332
+ export const applyLegacyButtonProps = (input, root) => {
333
+ if (root.background !== undefined)
334
+ return;
335
+ if (input.kind !== 'custom')
336
+ return;
337
+ if (!isRenderableColorLike(input.customColor))
338
+ return;
339
+ root[BUTTON_LEGACY_CUSTOM_COLOR_VAR] = String(input.customColor).trim();
340
+ };
311
341
  /* ---- container: başlık bölgesi (frontend `container-style-logic.ts` kopyası) ---- */
312
342
  const CONTAINER_HEADER_SELECTOR = '&__header';
313
343
  const CONTAINER_TITLE_SELECTOR = '&__title';
@@ -419,6 +449,20 @@ export const TYPED_STYLE_COMPONENTS = {
419
449
  // Taşma hedefi olmayan komponentte contentOverflow ölü kural üretirdi.
420
450
  omit: { surfaceStyle: ['contentOverflow'] }
421
451
  },
452
+ button: {
453
+ groups: ['surfaceStyle'],
454
+ ext: { extra: applyLegacyButtonProps },
455
+ // `kind` emekli değil; takma ad yalnız eski 'custom' değerini okur.
456
+ legacyProps: ['kind', 'customColor'],
457
+ // Kaydırılan içeriği yok; taşma alanı ölü kural üretirdi.
458
+ omit: { surfaceStyle: ['contentOverflow'] }
459
+ },
460
+ form: {
461
+ // Çerçeve <form> KÖKÜNDE: yüzey köke, taşma gövdeye (S-19, 2026-09-15).
462
+ // Genel düğüm yüzeyi yolu değeri kökün dışındaki kutuya yazıp ikinci çerçeve çiziyordu.
463
+ groups: ['surfaceStyle'],
464
+ ext: { overflowTarget: '&__body' }
465
+ },
422
466
  icon: {
423
467
  // Yalnız hover: surface çipin (chipShape/chipColor), typography da boyutun
424
468
  // (size) ve rengin (color) çift kanalı olurdu — görsel kararlar semantik
@@ -439,7 +483,6 @@ export const TYPED_STYLE_COMPONENTS = {
439
483
  export const NODE_SURFACE_TYPES = new Set([
440
484
  'table',
441
485
  'datatable',
442
- 'form',
443
486
  'collapsible-container',
444
487
  'drawer',
445
488
  'modal',
@@ -511,12 +554,16 @@ export function resolveNodeStyleReport(node, source) {
511
554
  const ownStyle = rawProp(props, 'style');
512
555
  const config = node.type ? TYPED_STYLE_COMPONENTS[node.type] : undefined;
513
556
  const presetName = isPlainObject(ownStyle) ? ownStyle[STYLE_PRESET_KEY] : undefined;
557
+ const versionName = isPlainObject(ownStyle) ? ownStyle[STYLE_COMPONENT_PRESET_KEY] : undefined;
514
558
  const typedGroups = config ? config.groups.filter(name => isPlainObject(rawProp(props, name))) : [];
559
+ const magicKeys = [STYLE_PRESET_KEY, STYLE_COMPONENT_PRESET_KEY];
515
560
  const layers = {
516
561
  typedGroups,
517
562
  componentStyle: Boolean(node.type && isPlainObject(source?.componentStyles?.[node.type])),
518
563
  preset: typeof presetName === 'string' && presetName ? presetName : null,
519
- own: isPlainObject(ownStyle) ? Object.keys(ownStyle).filter(key => key !== STYLE_PRESET_KEY).length > 0 : Boolean(ownStyle)
564
+ componentPreset: typeof versionName === 'string' && versionName ? versionName : null,
565
+ // İşaretler stil DEĞİLDİR: yalnız onları taşıyan düğüm "kendi stili var" sayılmamalı.
566
+ own: isPlainObject(ownStyle) ? Object.keys(ownStyle).filter(key => !magicKeys.includes(key)).length > 0 : Boolean(ownStyle)
520
567
  };
521
568
  if (!config) {
522
569
  // Kayıtsız tip: eski birleşik kaskad. Kap tiplerinde tipli yüzey grubu