@appaflytech/zeyrek-ai-mcp 0.0.40 → 0.0.42

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 (60) hide show
  1. package/dist/knowledge/componentCatalog.js +95 -104
  2. package/dist/knowledge/componentEvents.js +34 -18
  3. package/dist/knowledge/mobileComponentCatalog.js +119 -20
  4. package/dist/knowledge/mobileComponentEvents.js +41 -0
  5. package/dist/knowledge/motionResolve.js +252 -0
  6. package/dist/knowledge/normalize.js +117 -71
  7. package/dist/knowledge/pageDiscover.js +2 -2
  8. package/dist/knowledge/platform/batchOps.js +70 -0
  9. package/dist/knowledge/platform/heroTag.js +153 -0
  10. package/dist/knowledge/platform/lint.js +128 -6
  11. package/dist/knowledge/platform/mobile.js +1 -1
  12. package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
  13. package/dist/knowledge/platform/mobileRules.js +18 -0
  14. package/dist/knowledge/platform/pageTransition.js +57 -0
  15. package/dist/knowledge/platform/web.js +1 -1
  16. package/dist/knowledge/rules.js +217 -24
  17. package/dist/knowledge/styleResolve.js +0 -1
  18. package/dist/knowledge/styleResolveNative.js +88 -4
  19. package/dist/platform/capture/actions.js +27 -1
  20. package/dist/platform/capture/capture.js +10 -13
  21. package/dist/platform/capture/persistent-browser.js +130 -0
  22. package/dist/platform/capture/summary.js +4 -1
  23. package/dist/resources/examples/flow-list-to-form.json +8 -10
  24. package/dist/resources/examples/flow-load-table.json +1 -1
  25. package/dist/resources/examples/mobile-app-shell-drawer.json +1 -0
  26. package/dist/resources/examples/mobile-list-detail-form.json +7 -3
  27. package/dist/resources/examples/mobile-scan-code.json +285 -0
  28. package/dist/resources/guides/component.md +3 -3
  29. package/dist/resources/guides/entity.md +16 -0
  30. package/dist/resources/guides/expressions.md +8 -7
  31. package/dist/resources/guides/merge.md +98 -18
  32. package/dist/resources/guides/microservice.md +3 -3
  33. package/dist/resources/guides/middleware.md +2 -0
  34. package/dist/resources/guides/mobile-layout.md +2 -1
  35. package/dist/resources/guides/mobile-publish.md +1 -0
  36. package/dist/resources/guides/mobile-steps.md +67 -0
  37. package/dist/resources/guides/mobile.md +51 -70
  38. package/dist/resources/guides/page.md +229 -137
  39. package/dist/resources/guides/translation.md +67 -1
  40. package/dist/resources/guides/workflow.md +31 -1
  41. package/dist/resources/schema-fingerprint.json +2 -2
  42. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
  43. package/dist/tools/activities.js +3 -1
  44. package/dist/tools/applications.js +16 -2
  45. package/dist/tools/components.js +12 -6
  46. package/dist/tools/designSchemaPatch.js +18 -13
  47. package/dist/tools/entities.js +41 -4
  48. package/dist/tools/layouts.js +29 -9
  49. package/dist/tools/merges.js +29 -9
  50. package/dist/tools/mobile.js +15 -1
  51. package/dist/tools/pages.js +156 -168
  52. package/dist/tools/platformInfo.js +3 -2
  53. package/dist/tools/profileWrite.js +2 -2
  54. package/dist/tools/queries.js +1 -1
  55. package/dist/tools/rows.js +14 -3
  56. package/dist/tools/schemaOps.js +132 -34
  57. package/dist/tools/screenWrite.js +3 -3
  58. package/dist/tools/screens.js +42 -3
  59. package/dist/tools/translations.js +148 -6
  60. package/package.json +2 -2
@@ -53,9 +53,20 @@ const bulkBatchSize = 1000;
53
53
  const maxPathBytes = 50 * 1024 * 1024;
54
54
  const maxPathRows = 100_000;
55
55
  const pathParam = z.string().min(1).optional()
56
- .describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
56
+ .describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar; yanıt { written, batches, ids (platform döndürdüyse ilk 10), truncated } — tam id listesi için row_list. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
57
57
  const relationsByNameParam = z.boolean().optional()
58
58
  .describe("İlişki alanlarına id yerine GÖRÜNEN AD yazmayı açar: 'departman: \"Satış\"' hedef tablonun display ya da benzersiz alanında aranır ve id'ye çevrilir. Yalnız toplu yolda (rows / path) çalışır. Eşleşmeyen değer 422 döndürür ve hangi satırın hangi alanı olduğunu söyler — sessizce boş bırakmaz.");
59
+ /**
60
+ * 37 S-4: `path` "satırlar ajan bağlamından hiç geçmez" der, ama toplu uç her partide yazdığı id'leri
61
+ * döndürünce yanıt dosyanın satır sayısı kadar id taşıyordu (82 satır → `ids[82]`). Dosya yolunda
62
+ * yanıt sayıdır; id'lerden yalnız ilk birkaçı örnek olarak kalır, tamamı row_list'tedir.
63
+ */
64
+ const fileIdSample = 10;
65
+ function withIdSample(outcome) {
66
+ if (!outcome.ids)
67
+ return outcome;
68
+ return { ...outcome, ids: outcome.ids.slice(0, fileIdSample), truncated: outcome.ids.length > fileIdSample };
69
+ }
59
70
  function collectIds(payload, into) {
60
71
  const data = payload?.data;
61
72
  if (Array.isArray(data))
@@ -315,7 +326,7 @@ export function registerRowTools(server, platform) {
315
326
  const outcome = await writeInBatches(platform, 'POST', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
316
327
  if (fileRows)
317
328
  notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
318
- return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
329
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(fileRows ? withIdSample(outcome) : outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
319
330
  }
320
331
  const body = { row, ...(wantedLocalizations ? { localizations: wantedLocalizations } : {}), ...(rowContext ? { rowContext } : {}) };
321
332
  const drift = unmappedMembers('CreateRowCommand', body);
@@ -384,7 +395,7 @@ export function registerRowTools(server, platform) {
384
395
  const outcome = await writeInBatches(platform, 'PUT', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
385
396
  if (fileRows)
386
397
  notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
387
- return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
398
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(fileRows ? withIdSample(outcome) : outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
388
399
  }
389
400
  const body = { row, ...(localizations ? { localizations } : {}), ...(rowContext ? { rowContext } : {}) };
390
401
  const drift = unmappedMembers('UpdateRowCommand', body);
@@ -1,4 +1,5 @@
1
- import { isFlexContainer, schemaOpValidationErrors, FLEX_LAYOUT_PROP_NAMES, PAGE_CONTAINER_ELEMENT_ID, COMPONENT_BODY_RULE } from '../knowledge/normalize.js';
1
+ import { isFlexContainer, schemaOpValidationErrors, FLEX_LAYOUT_PROP_NAMES, PAGE_CONTAINER_ELEMENT_ID, COMPONENT_BODY_RULE, RETIRED_TYPE_RULE } from '../knowledge/normalize.js';
2
+ import { retiredComponent } from '../knowledge/componentCatalog.js';
2
3
  import { findNode, outlineAppliedPatch } from './screens.js';
3
4
  import { setPropTypeErrors } from '../knowledge/propTypes.js';
4
5
  import { connectDataOpIssues } from '../knowledge/connectDataDefinition.js';
@@ -26,13 +27,18 @@ export function outlineWriteResponse(response) {
26
27
  if (Array.isArray(container.appliedPatch))
27
28
  container.appliedPatch = outlineAppliedPatch(container.appliedPatch);
28
29
  }
29
- export const opsParam = jsonArray(z.array(z.record(z.unknown()))).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} (value ham değer, code sarmalı {type: 'code', ref: '{{ … }}'} ya da translation sarmalı {type: 'translation', ref: '<çeviri anahtarı>'} — prop tipini değiştirmek için komponenti silip yeniden EKLEMEYİN, ona yazan setter'ların hedefi silinir (FLOW-018); platform replace_prop'ta expression alanını OKUMAZ — gateway onu code sarmalına çevirir) | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component: {elementId, name (= elementId), type, props, blocks: [{breakpoint: 1024, size: {w,h}, position: {x,y}}]}} (blocks ZORUNLU, gateway doldurmaz — blocks'suz ya da null blocks'lu düğüm istek gitmeden PAGE-037 ile reddedilir, platform onu kayıtta 500 NullReferenceException ile düşürürdü; flex kapta position yalnız sıradır; ifadede name okunur, elementId değil) | remove_component {target} | move_component {target, parent, index?, position: {x, y}} (index ÇİZİM sırası değildir, runtime çocukları blocks.position'a göre (y, sonra x) çizer. page_schema_patch'te flex kapta gateway index'i görünen sıraya çevirir ve kardeş konumlarını yeniden yazar; ızgara kapta sırayı position.y verir, sona eklemeyen index PAGE-033 uyarısıdır. layout_schema_patch ve component_schema_patch bu çeviriyi YAPMAZ: orada sırayı position.y ile verin) | set_event {target, event, eventValue, replaceBoundFlows?} (olayın BÜTÜN bağlarını bu tek akışa indirir; olayda başka akış bağlıysa gateway PAGE-034 ile reddeder — bilerek değiştirmek için replaceBoundFlows: true) | add_event {target, event, eventValue} (olaya bir akış DAHA bağlar; bağlı akışların hepsi paralel ve birbirinden bağımsız koşar, sıra garantisi yoktur — sıra gerekiyorsa flow-trigger. Aynı akışı ikinci kez bağlamak etkisizdir (unchanged; tekrar güvenli). Mobil runtime yalnız İLK akışı koşar) | remove_event {target, event, eventValue?} (olayı diziden çıkarır; target: 'page' sayfa düzeyi olaylar. Olaya birden fazla akış bağlıysa eventValue ile hangisi olduğunu verin, yoksa 422 ambiguous-event. set_event ile eventValue: null olay SİLMEZ, değersiz ölü kayıt bırakır. Olmayan olay 422 missing-event) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | set_appearance {target, appearance: {heightType?, widthType?, tooltip?, background?, …}} (kurulu düğümün appearance'ına YALNIZ verilen alanları yazar, gerisini korur; null alanı temizler; bilinmeyen alan unknown-appearance-field ile reddedilir. GÖRÜNÜRLÜK appearance'ta DEĞİLDİR: gizlemek için replace_prop {target, prop: 'isHidden', value: true}, koşul için set_binding {prop: 'isHidden'} — add_component gövdesindeki appearance.isHidden'ı gateway props.isHidden'a taşır) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway adı: platforma set_block {target, breakpoint, size} + set_appearance {target, appearance} + set_states {target, states} olarak gider — o üç op'u doğrudan da gönderebilirsiniz. move_component'e konan size reddedilir. Izgarada blocks[].size, flex'te appearance geçerlidir, blocks yine zorunlu; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — düğümü silmeden isWrapper bayrağını değiştirmenin yolu budur). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (replace_prop sarmalıyla aynı sonucu verir ve mevcut data'yı tasarım yedeği olarak taşır. Bu op prop'un TİPİNİ yerinde değiştirir. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir) | reorder {parent ('root' ya da elementId), order: [çocuk elementId'lerinin TAMAMI, yeni sırada]} (yalnız DİZİ sırasını değiştirir; çizim sırası blocks.position'dır — görünen sırayı değiştirmek için konumları set_block/move_component ile de yazın) | set_state_binding {target, binding: {name, expression, targets}} (ada göre ekler/değiştirir) | remove_state_binding {target, name} | set_connect_data {target, path, ref, data?} (prop'u veri kaynağına BAĞLAR: değer { type: 'connectData', ref: <tanım>, data: <son satırlar> } olur; path düz prop 'rows', nesne içi 'config.series' ya da dizi öğesi 'series.1.data'; data verilmezse bağlı prop'un satırları korunur, bağlı olmayan prop'un statik satırları (iç içe sarmallar açılarak) son satır olarak taşınır — D-11; tanım şekli ve alt küme kuralları istek gitmeden denetlenir — PAGE-035 · CD-004; platform_guide('page') §Kural 1c-3) | remove_connect_data {target, path} (bağlantıyı kaldırır, değer static olur; son satırlar static öğelere sarılarak korunur) — bağlı prop'a replace_prop ile connectData sarmalı, set_binding ya da set_prop_type yazılmaz; replace_prop { value: [ham satırlar] } yalnız örnek satırları değiştirir (CD-005) | batch {gestureId?, label?, ops: [...]} (iç içe batch yok). target = elementId. İstek atomiktir: tek op reddedilirse hiçbiri yazılmaz.");
30
+ export const opsParam = jsonArray(z.array(z.record(z.unknown()))).describe("Micro-op listesi. Op sözlüğü: replace_prop {target, prop, value} (value ham değer, code sarmalı {type: 'code', ref: '{{ … }}'} ya da translation sarmalı {type: 'translation', ref: '<çeviri anahtarı>'} — prop tipini değiştirmek için komponenti silip yeniden EKLEMEYİN, ona yazan setter'ların hedefi silinir (FLOW-018); platform replace_prop'ta expression alanını OKUMAZ — gateway onu code sarmalına çevirir) | set_binding {target, prop, expression: '{{ Page.variables.x }}'} | add_component {parent (üst elementId ya da 'root'), index?, component: {elementId, name (= elementId), type, props, blocks: [{breakpoint: 1024, size: {w,h}, position: {x,y}}]}} (blocks ZORUNLU, gateway doldurmaz — blocks'suz ya da null blocks'lu düğüm istek gitmeden PAGE-037 ile reddedilir, platform onu kayıtta 500 NullReferenceException ile düşürürdü; katalogdan kaldırılan tip (component_list'te retired: true — ör. table) istek gitmeden PAGE-051 ile reddedilir, replacedBy tipini kullanın; flex kapta position yalnız sıradır; ifadede name okunur, elementId değil) | remove_component {target} | move_component {target, parent, index?, position: {x, y}} (index ÇİZİM sırası değildir, runtime çocukları blocks.position'a göre (y, sonra x) çizer. page_schema_patch'te flex kapta gateway index'i görünen sıraya çevirir ve kardeş konumlarını yeniden yazar; ızgara kapta sırayı position.y verir, sona eklemeyen index PAGE-033 uyarısıdır. layout_schema_patch ve component_schema_patch bu çeviriyi YAPMAZ: orada sırayı position.y ile verin) | set_event {target, event, eventValue, replaceBoundFlows?} (olayın BÜTÜN bağlarını bu tek akışa indirir; olayda başka akış bağlıysa gateway PAGE-034 ile reddeder — bilerek değiştirmek için replaceBoundFlows: true) | add_event {target, event, eventValue} (olaya bir akış DAHA bağlar; bağlı akışların hepsi paralel ve birbirinden bağımsız koşar, sıra garantisi yoktur — sıra gerekiyorsa flow-trigger. Aynı akışı ikinci kez bağlamak etkisizdir (unchanged; tekrar güvenli). Mobil runtime yalnız İLK akışı koşar) | remove_event {target, event, eventValue?} (olayı diziden çıkarır; target: 'page' sayfa düzeyi olaylar. Olaya birden fazla akış bağlıysa eventValue ile hangisi olduğunu verin, yoksa 422 ambiguous-event. set_event ile eventValue: null olay SİLMEZ, değersiz ölü kayıt bırakır. Olmayan olay 422 missing-event) | set_validations {target, validations: [{type, message, value, isActive}]} (komponentin validation listesini TAMAMEN değiştirir; boş dizi hepsini siler) | set_appearance {target, appearance: {heightType?, widthType?, tooltip?, background?, …}} (kurulu düğümün appearance'ına YALNIZ verilen alanları yazar, gerisini korur; null alanı temizler; bilinmeyen alan unknown-appearance-field ile reddedilir. GÖRÜNÜRLÜK appearance'ta DEĞİLDİR: gizlemek için replace_prop {target, prop: 'isHidden', value: true}, koşul için set_binding {prop: 'isHidden'} — add_component gövdesindeki appearance.isHidden'ı gateway props.isHidden'a taşır) | resize_component {target, size?: {w,h}, appearance?: {widthType,widthString,heightType,heightString}, states?: {isWrapper,isScrollable,isSticky,isDroppable,isFloating,isQuickStack}, breakpoint?: 1024} (gateway adı: platforma set_block {target, breakpoint, size} + set_appearance {target, appearance} + set_states {target, states} olarak gider — o üç op'u doğrudan da gönderebilirsiniz. move_component'e konan size reddedilir. Izgarada blocks[].size, flex'te appearance geçerlidir, blocks yine zorunlu; ebeveynin düzeni yazılanı etkisiz kılıyorsa PAGE-019 uyarısı çıkar. states bir PROP DEĞİL düğüm alanıdır ve replace_prop onu yazamaz — düğümü silmeden isWrapper bayrağını değiştirmenin yolu budur). set_prop_type {target, prop, propType: 'static'|'code'|'translation', ref?, data?} (replace_prop sarmalıyla aynı sonucu verir ve mevcut data'yı tasarım yedeği olarak taşır. Bu op prop'un TİPİNİ yerinde değiştirir. Taşıyıcı alan tipe göre: static → data, code ve translation → ref (runtime data'yı her turda ezer, anahtar yalnız ref'te yaşar). Eksik taşıyıcı istek gönderilmeden reddedilir; page_schema_validate aynı kararı verir) | reorder {parent ('root' ya da elementId), order: [çocuk elementId'lerinin TAMAMI, yeni sırada]} (yalnız DİZİ sırasını değiştirir; çizim sırası blocks.position'dır — görünen sırayı değiştirmek için konumları set_block/move_component ile de yazın) | set_state_binding {target, binding: {name, expression, targets}} (ada göre ekler/değiştirir) | remove_state_binding {target, name} | set_connect_data {target, path, ref, data?} (prop'u veri kaynağına BAĞLAR: değer { type: 'connectData', ref: <tanım>, data: <son satırlar> } olur; path düz prop 'rows', nesne içi 'config.series' ya da dizi öğesi 'series.1.data'; data verilmezse bağlı prop'un satırları korunur, bağlı olmayan prop'un statik satırları (iç içe sarmallar açılarak) son satır olarak taşınır — D-11; tanım şekli ve alt küme kuralları istek gitmeden denetlenir — PAGE-035 · CD-004; platform_guide('page') §Kural 1c-3) | remove_connect_data {target, path} (bağlantıyı kaldırır, değer static olur; son satırlar static öğelere sarılarak korunur) — bağlı prop'a replace_prop ile connectData sarmalı, set_binding ya da set_prop_type yazılmaz; replace_prop { value: [ham satırlar] } yalnız örnek satırları değiştirir (CD-005) | batch {gestureId?, label?, ops: [...]} (yalnız gruplamadır: gateway zarfı açar, içindeki op'lar tek tek yazılmış gibi denetlenip düz listeyle gider — PAGE-048; iç içe batch yok). target = elementId. Op'lar yazıldıkları SIRAYLA uygulanır (resize_component / set_prop_type / remove_event dahil — PAGE-050): aynı yamada eklenen düğüm SONRAKİ op'un hedefi olabilir; bir olayın bağını değiştirirken önce remove_event, sonra add_event yazın. İstek atomiktir: tek op reddedilirse hiçbiri yazılmaz.");
30
31
  /**
31
32
  * PAGE-008 — `set_event` uydurma bir olay adıyla 200 döner ve bağ SESSİZCE ölü kalır: motor olayı
32
33
  * komponentin yaydığı adla eşleştirir, eşleşme yoksa akış hiç çalışmaz. Kontrol yalnız hedefin
33
34
  * tipini ve o tipin katalogdaki olay listesini pozitif olarak çözebildiğinde konuşur (okunan ağaçta
34
35
  * olmayan hedefler — sayfa yamasında layout düğümleri — ve katalogda olay tanımlamayan tipler
35
36
  * doğrulanmadan geçer).
37
+ *
38
+ * Emekli tipte KAYITLI düğümde liste yerine geçen bileşenden okunur (`RETIRED_COMPONENTS`): kayıt
39
+ * defteri `table`'ı eski olaylarıyla tutar ama runtime o düğümü `datatable` ile çizer. Kayıtlı
40
+ * tipten okununca denetim tersine dönüyordu — yayılan `rowDoubleClick` reddediliyor, hiç yayılmayan
41
+ * `doubleClick` geçip ölü bağ yazılıyordu (inceleme 2026-10-13).
36
42
  */
37
43
  export function unknownEventOps(ops, typesByElementId, eventsBySlug) {
38
44
  return ops
@@ -42,10 +48,11 @@ export function unknownEventOps(ops, typesByElementId, eventsBySlug) {
42
48
  const target = String(op.target ?? '');
43
49
  const event = String(op.event ?? '');
44
50
  const type = typesByElementId.get(target);
45
- const declared = type ? eventsBySlug.get(type) : undefined;
51
+ const drawnAs = retiredComponent(type)?.replacedBy ?? type;
52
+ const declared = drawnAs ? eventsBySlug.get(drawnAs) : undefined;
46
53
  if (!event || !declared || declared.includes(event))
47
54
  return [];
48
- return [`'${target}' (${type}) komponenti '${event}' diye bir olay YAYMAZ. Yaydıkları: ${declared.join(', ')}. Platform bu op'u 200 ile kabul eder ama bağ sessizce ölü kalır — akış hiçbir zaman çalışmaz. Doğru adı component_get(slug:'${type}') çıktısındaki events listesinden alın.`];
55
+ return [`'${target}' (${drawnAs === type ? type : `${type} — emekli tip, runtime ${drawnAs} ile çizer`}) komponenti '${event}' diye bir olay YAYMAZ. Yaydıkları: ${declared.join(', ')}. Platform bu op'u 200 ile kabul eder ama bağ sessizce ölü kalır — akış hiçbir zaman çalışmaz. Doğru adı component_get(slug:'${type}') çıktısındaki events listesinden alın.`];
49
56
  });
50
57
  }
51
58
  /**
@@ -54,14 +61,15 @@ export function unknownEventOps(ops, typesByElementId, eventsBySlug) {
54
61
  * toplanır ve tek yanıtta döner. Yama yine ATOMİKTİR — birbirine bağlı op'lar (add_component +
55
62
  * set_event) yarım kalmasın diye geçenler de gönderilmez.
56
63
  */
57
- export function rejectedOpsMessage(unordered) {
64
+ export function rejectedOpsMessage(unordered, indexHint = '') {
58
65
  const rejected = [...unordered].sort((left, right) => left.index - right.index);
59
66
  const lines = rejected.map(entry => `#${entry.index} ${entry.op}${entry.target ? ` '${entry.target}'` : ''} [${entry.code}]: ${entry.message}`);
60
67
  const validationHint = rejected.some(entry => entry.code === 'PAGE-016')
61
68
  ? " PAGE-016 çalışan biçimi dört alanı da taşır: { type: 'required'|'email'|'regex'|'max-length'|'min-length'|'max-value'|'min-value'|'value-options', message: '<kullanıcıya görünen metin>', value: <eşik/desen/true>, isActive: true }."
62
69
  : '';
63
70
  const bodyHint = rejected.some(entry => entry.code === 'PAGE-037') ? ` PAGE-037 ${COMPONENT_BODY_RULE}` : '';
64
- return `Ön kontrol ${rejected.length} op'u reddetti; yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi). Düşen op'ları düzeltip TÜM listeyi tek çağrıda yeniden gönderin.${validationHint}${bodyHint}\n${lines.join('\n')}\nrejectedOps: ${JSON.stringify(rejected)}`;
71
+ const retiredHint = rejected.some(entry => entry.code === 'PAGE-051') ? ` PAGE-051 ${RETIRED_TYPE_RULE}` : '';
72
+ return `Ön kontrol ${rejected.length} op'u reddetti; yama atomik olduğu için HİÇBİR op gönderilmedi (istek gönderilmedi). Düşen op'ları düzeltip TÜM listeyi tek çağrıda yeniden gönderin.${indexHint}${validationHint}${bodyHint}${retiredHint}\n${lines.join('\n')}\nrejectedOps: ${JSON.stringify(rejected)}`;
65
73
  }
66
74
  export function collectTypes(nodes, into) {
67
75
  for (const node of nodes ?? []) {
@@ -70,29 +78,6 @@ export function collectTypes(nodes, into) {
70
78
  collectTypes(node.children, into);
71
79
  }
72
80
  }
73
- /**
74
- * TB-129 — `replace_prop` yalnız elementId taşır; stil anahtarı çevirisinin komponent TİPİNE
75
- * bakabilmesi için hedeflerin tipi okunan ağaçtan çıkarılır. Harita YALNIZ `style` prop'u yazan
76
- * bir op varsa kurulur: başka türlü fazladan bir okuma maliyeti doğardı.
77
- */
78
- export function styleTargetTypesFrom(ops, nodes) {
79
- if (!ops.some(entry => entry.op === 'replace_prop' && String(entry.prop ?? '') === 'style'))
80
- return undefined;
81
- const types = new Map();
82
- collectTypes(nodes, types);
83
- return types;
84
- }
85
- /** Aynı harita, ağacı tembel okuyan yazma yolu için; okuma düşerse çeviri eski davranışa döner. */
86
- export async function styleTargetTypes(ops, readHost) {
87
- if (!ops.some(entry => entry.op === 'replace_prop' && String(entry.prop ?? '') === 'style'))
88
- return undefined;
89
- try {
90
- return styleTargetTypesFrom(ops, (await readHost()).schema);
91
- }
92
- catch {
93
- return undefined;
94
- }
95
- }
96
81
  export function removeNodeFromTree(nodes, target, parent) {
97
82
  for (let index = 0; index < nodes.length; index++) {
98
83
  if (nodes[index].elementId === target)
@@ -210,9 +195,11 @@ export function resizeServerOps(op) {
210
195
  * Sunucu hedefi ve bloğu kendisi doğrular; burada okunan sayfadan yapılan kontrol ajana bağlamı
211
196
  * olan bir mesaj ve PAGE-019/020 uyarıları içindir. `size` yalnız verilen kenarı taşır: eksik
212
197
  * kenar okunan bloktan tamamlanır, yoksa sunucu 0 yazardı.
198
+ * `groups` op başına çevrilmiş sunucu op'larıdır (girdiyle aynı sırada): `serverOpsInOrder` her birini kendi yerine koyar.
199
+ * Uyarı metni yazıldığını İDDİA ETMEZ ("yazılır", "yazılıyor"): aynı plan kuru koşuda da koşar (PAGE-049, TOOL-060).
213
200
  */
214
201
  export function planResizes(schema, ops, readTool = 'page_schema_get') {
215
- const serverOps = [];
202
+ const groups = [];
216
203
  const applied = [];
217
204
  const warnings = [];
218
205
  for (const op of ops) {
@@ -249,13 +236,13 @@ export function planResizes(schema, ops, readTool = 'page_schema_get') {
249
236
  resolved = { ...op, size: merged };
250
237
  applied.push(`${target}.size=${JSON.stringify(merged)}`);
251
238
  if (inFlex) {
252
- warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazıldı ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
239
+ warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex kapsayıcı; orada yerleşimi 'blocks[].size' DEĞİL appearance (widthType/widthString/heightType/heightString) belirler. Boyut yazılır ama ekranda etkisi olmayabilir — flex'te ölçü vermek için aynı op'a 'appearance' koyun.`);
253
240
  }
254
241
  }
255
242
  if (states) {
256
243
  applied.push(`${target}.states=${JSON.stringify(states)}`);
257
244
  if (states.isWrapper === true) {
258
- warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazıldı — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
245
+ warnings.push(`PAGE-020 (uyarı): '${target}' düğümüne isWrapper:true yazılıyor — runtime sarmalayıcı düğüme 'min-height: 80vh' veriyor ve bu, sayfanın altına yüzlerce piksel ölü boşluk ekleyebilir (ölçüldü 09-05: dokuz düğüm ~1600px). style ile ezilemez; gerekmiyorsa isWrapper:false bırakın.`);
259
246
  }
260
247
  }
261
248
  if (appearance) {
@@ -264,9 +251,120 @@ export function planResizes(schema, ops, readTool = 'page_schema_get') {
264
251
  warnings.push(`PAGE-019 (uyarı): '${target}' düğümünün ebeveyni flex DEĞİL; ızgarada yerleşimi 'blocks[].size' belirler ve appearance genişlik/yükseklik alanları etkisiz kalabilir. Izgarada ölçü vermek için aynı op'a 'size' koyun.`);
265
252
  }
266
253
  }
267
- serverOps.push(...resizeServerOps(resolved));
254
+ groups.push(resizeServerOps(resolved));
255
+ }
256
+ return { serverOps: groups.flat(), groups, applied, warnings };
257
+ }
258
+ /** Gateway'in listeden ayırıp kendisi çevirdiği / planladığı op'lar. */
259
+ const EMULATED_OPS = new Set(['resize_component', 'set_prop_type', 'remove_event']);
260
+ /**
261
+ * Yamanın ÇAĞIRANIN SIRASI: `allOps` (ajanın yazdığı liste) ile `records`'u (öykünülen op'lar ayrıldıktan sonra kalan,
262
+ * gateway'in yerinde onardığı liste) tek sıraya dizer. `records` `allOps`'ta olmayan op taşıyabilir — kutu bölümü
263
+ * anahtarı (BX-002, `expandSectionToggles`) eklediklerini tetikleyen op'un hemen arkasına yerleştirir; onlar
264
+ * tetikleyenle birlikte kalır. Eşleme nesne kimliğiyledir, sayarak değil: sayarken eklenen op hizayı kaydırıyor ve
265
+ * `[replace_prop, set_prop_type]` yine ters gidiyordu (inceleme 2026-10-10).
266
+ */
267
+ export function callerSlots(allOps, records) {
268
+ const written = new Set(allOps);
269
+ const slots = [];
270
+ let cursor = 0;
271
+ for (const op of allOps) {
272
+ if (EMULATED_OPS.has(String(op.op))) {
273
+ slots.push({ emulated: op });
274
+ continue;
275
+ }
276
+ const at = records.indexOf(op, cursor);
277
+ if (at < 0)
278
+ continue;
279
+ let end = at + 1;
280
+ while (end < records.length && !written.has(records[end]))
281
+ end += 1;
282
+ for (let index = cursor; index < end; index += 1)
283
+ slots.push({ record: index });
284
+ cursor = end;
285
+ }
286
+ for (let index = cursor; index < records.length; index += 1)
287
+ slots.push({ record: index });
288
+ return slots;
289
+ }
290
+ /** `records` + `remove_event`'ler, çağıranın sırasıyla: akış silme planı (G-21) yamayı uygulanacağı sırayla benzetir. */
291
+ export function recordsWithEventRemovals(slots, records) {
292
+ return slots.flatMap(slot => {
293
+ if ('record' in slot)
294
+ return records.slice(slot.record, slot.record + 1);
295
+ return slot.emulated.op === 'remove_event' ? [slot.emulated] : [];
296
+ });
297
+ }
298
+ /**
299
+ * Platforma giden liste, ÇAĞIRANIN SIRASIYLA: gateway'in öykündüğü op (`resize_component`, `set_prop_type`) kendi
300
+ * yerinde çevrilmiş hâliyle, `remove_event` olduğu gibi durur; flex sıra çevirisinin eklediği taşımalar
301
+ * (`flexIndexOrder` `trailing`) sona gelir. Platform op'ları sırayla uygular (`PageSchemaMicroOpApplier.Apply`).
302
+ * 2026-10-10'a dek öykünülen op'lar listenin BAŞINA alınıyordu (ayrı istekle önceden yazıldıkları günden kalma):
303
+ * `[replace_prop, set_prop_type]` ters gidiyor, son yazan statik değer oluyor ve bağ sessizce kayboluyordu —
304
+ * yanıt yine `propTypes: static → code` diyordu (inceleme, `test/page-dry-run-parity.test.ts`).
305
+ *
306
+ * `sent`, `records`'un giden hâlidir ve onunla aynı sıradadır (`orderByIndex` kopyalasa da sırayı korur, eklediklerini
307
+ * sona yazar).
308
+ */
309
+ export function serverOpsInOrder(slots, parts) {
310
+ let records = 0;
311
+ let resized = 0;
312
+ let propTypes = 0;
313
+ const ordered = slots.flatMap(slot => {
314
+ if ('record' in slot) {
315
+ records += 1;
316
+ return parts.sent.slice(slot.record, slot.record + 1);
317
+ }
318
+ if (slot.emulated.op === 'resize_component')
319
+ return parts.resized[resized++] ?? [];
320
+ if (slot.emulated.op === 'set_prop_type')
321
+ return parts.propTypes.slice(propTypes, ++propTypes);
322
+ return [slot.emulated];
323
+ });
324
+ return [...ordered, ...parts.sent.slice(records)];
325
+ }
326
+ /**
327
+ * Öykünülen op'ların planı, ağacın O ANDAKİ hâline göre: yama sırayla benzetilir (ekleme, silme, taşıma, düzen prop'u,
328
+ * önceki `resize_component`'in yazdığı boyut) ve her `resize_component` / `set_prop_type` kendi sırasındaki ağaçta
329
+ * çözülür. Yalnız kayıtlı ağaca bakılırken `[add_component yeni, set_prop_type yeni]` "hedef bulunamadı" ile düşüyor,
330
+ * silinip yeniden eklenen düğümün eksik kenarı ESKİ düğümden tamamlanıyor, ardışık iki `resize_component`'in ikincisi
331
+ * birincinin yazdığı kenarı eziyordu. Öykünülen op yoksa ağaç kopyalanmaz.
332
+ */
333
+ export function planEmulatedOps(schema, slots, records, readTool = 'page_schema_get') {
334
+ const resized = { groups: [], applied: [], warnings: [] };
335
+ const propTypes = { serverOps: [], applied: [] };
336
+ if (!slots.some(slot => 'emulated' in slot && slot.emulated.op !== 'remove_event'))
337
+ return { resized, propTypes };
338
+ const tree = structuredClone(schema);
339
+ for (const slot of slots) {
340
+ if ('record' in slot) {
341
+ const record = records[slot.record];
342
+ if (record?.op === 'remove_component')
343
+ removeNodeFromTree(tree, String(record.target ?? ''));
344
+ else if (record)
345
+ simulateLayoutOps(tree, [record]);
346
+ continue;
347
+ }
348
+ if (slot.emulated.op === 'resize_component') {
349
+ const plan = planResizes(tree, [slot.emulated], readTool);
350
+ const group = plan.groups[0] ?? [];
351
+ resized.groups.push(group);
352
+ resized.applied.push(...plan.applied);
353
+ resized.warnings.push(...plan.warnings);
354
+ for (const written of group.filter(entry => entry.op === 'set_block')) {
355
+ const blocks = findNode(tree, String(written.target))?.blocks;
356
+ const block = blocks?.find(entry => Number(entry?.breakpoint) === Number(written.breakpoint));
357
+ if (block)
358
+ block.size = written.size;
359
+ }
360
+ }
361
+ if (slot.emulated.op === 'set_prop_type') {
362
+ const plan = planPropTypes(tree, [slot.emulated], readTool);
363
+ propTypes.serverOps.push(...plan.serverOps);
364
+ propTypes.applied.push(...plan.applied);
365
+ }
268
366
  }
269
- return { serverOps, applied, warnings };
367
+ return { resized, propTypes };
270
368
  }
271
369
  /**
272
370
  * `set_prop_type` (P0-2) 2026-09-14'ten beri platformun op'u; taşıyıcı kuralı
@@ -1,4 +1,4 @@
1
- import { invalidScreenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, assertComponentBlocks, blockSizeNote, heightTypeNote, sizeStringNote, droppableStatesNote } from '../knowledge/normalize.js';
1
+ import { invalidScreenEventNames, screenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, assertComponentBlocks, blockSizeNote, heightTypeNote, sizeStringNote, droppableStatesNote } from '../knowledge/normalize.js';
2
2
  import { catalogTypeRules, schemaCatalogErrors, schemaCatalogMessage } from '../knowledge/componentCatalog.js';
3
3
  import { FUNCTIONS_NAMESPACE } from '../knowledge/functionGuards.js';
4
4
  import { reservedExpressionName } from '../knowledge/expressionPrelude.js';
@@ -56,7 +56,7 @@ export function invalidComponentNames(nodes) {
56
56
  */
57
57
  export function assertScreenWriteGuards(kind, screen, platform) {
58
58
  const codes = GUARD_CODES[kind];
59
- const badEvents = invalidScreenEventNames(screen.events, codes.events);
59
+ const badEvents = invalidScreenEventNames(screen.events, codes.events, screenEventNames(kind, platform));
60
60
  if (badEvents.length > 0)
61
61
  throw new PlatformApiError(`Geçersiz ${codes.noun} olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
62
62
  const badWidgets = invalidWidgetTypeNodes(screen.schema, codes.widgets);
@@ -64,7 +64,7 @@ export function assertScreenWriteGuards(kind, screen, platform) {
64
64
  throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
65
65
  assertComponentNames(kind, screen.schema);
66
66
  assertComponentBlocks(screen.schema);
67
- const nested = nestedRepeaterNodes(screen.schema);
67
+ const nested = nestedRepeaterNodes(screen.schema, platform);
68
68
  if (nested.length > 0)
69
69
  throw new PlatformApiError(nestedRepeaterMessage(nested, platform), 400);
70
70
  }
@@ -2,11 +2,12 @@ import { PlatformApiError } from '../platform/errors.js';
2
2
  import { resolveNodeStyleReport } from '../knowledge/styleResolve.js';
3
3
  import { nodeNativeStyle } from '../knowledge/styleResolveNative.js';
4
4
  import { jsonArray } from '../platform/jsonArray.js';
5
+ import { prune } from '../platform/responses.js';
5
6
  import { z } from 'zod';
6
7
  export const hostParam = z.enum(['page', 'layout', 'component'])
7
8
  .describe("Akışın barındırıcısı: page | layout | component (widget de component'tir)");
8
9
  export const schemaParam = jsonArray(z.array(z.record(z.unknown())))
9
- .describe("Komponent ağacı (DesignComponentManagementSchema listesi): her düğümde elementId, name (= elementId), type, blocks zorunlu — blocks: [{ breakpoint: 1024, size: { w, h }, position: { x, y } }], gateway doldurmaz (PAGE-037). Verilmezse mevcut şema korunur");
10
+ .describe("Komponent ağacı (DesignComponentManagementSchema listesi): her düğümde elementId, name (= elementId), type, blocks zorunlu — blocks: [{ breakpoint: 1024, size: { w, h }, position: { x, y } }], gateway doldurmaz (PAGE-037). Oluşturmada katalogdan kaldırılan tipte düğüm (component_list'te retired: true) PAGE-051 ile reddedilir. Verilmezse mevcut şema korunur");
10
11
  export const eventsParam = jsonArray(z.array(z.record(z.unknown())))
11
12
  .describe("Sayfa/layout düzeyi olaylar: [{ name, value }] — value bir flow id'sidir");
12
13
  export const modeParam = z.enum(['summary', 'subtree', 'full']).optional().describe("summary (varsayılan): yalnız id/elementId/name/type ağacı — hedef bulmak için bunu kullanın. subtree: componentId ile tek alt ağaç. full: tüm şema.");
@@ -75,6 +76,44 @@ export function splitSchemaIntoParts(schema, budgetChars) {
75
76
  export function encodeSchemaCursor(version, partIndex) {
76
77
  return `${version}:${partIndex}`;
77
78
  }
79
+ /**
80
+ * ADR 0001 — `mode:'full'` okumanın sayfa ve layout ortak kapısı: sığan ağaç tam döner, sığmayan HATA
81
+ * verir (sessiz kırpma yok); `cursor` ile parçalar `items[]` olarak gelir, sürüm değişince 409.
82
+ * `allowTruncated:true` kırpmayı açıkça ister — global `shrink()` kırpar, `onTruncated` işaretler.
83
+ */
84
+ export function readFullSchema(read) {
85
+ const nodeCount = countSchemaNodes(read.nodes);
86
+ if (read.cursor !== undefined) {
87
+ const decoded = decodeSchemaCursor(read.cursor);
88
+ if (!decoded)
89
+ throw new PlatformApiError(`cursor '${read.cursor}' çözülemedi; cursor yalnız '0' ya da önceki yanıttaki nextCursor olabilir. cursor:'0' ile baştan başlayın.`, 400);
90
+ if (decoded.version !== undefined && decoded.version !== read.versionKey) {
91
+ throw new PlatformApiError(`${read.noun} bu cursor alındıktan sonra değişti (cursor sürümü '${decoded.version}', güncel sürüm '${read.versionKey}'); eski parçalarla yenileri birleştirilirse şema tutarsız olur. Toplanan parçaları atın ve cursor:'0' ile baştan okuyun.`, 409);
92
+ }
93
+ const parts = splitSchemaIntoParts(read.nodes, Math.floor(read.maxResponseBytes * 0.75));
94
+ const items = parts[decoded.partIndex];
95
+ if (!items)
96
+ throw new PlatformApiError(`cursor '${read.cursor}' ${decoded.partIndex}. parçayı istiyor ama şema ${parts.length} parça; son parçadan sonra nextCursor gelmez. cursor:'0' ile baştan başlayın.`, 400);
97
+ const nextIndex = decoded.partIndex + 1;
98
+ return {
99
+ ...read.base,
100
+ nodeCount,
101
+ part: { index: decoded.partIndex, count: parts.length, nodes: countSchemaNodes(items.map(item => item.node)) },
102
+ items,
103
+ ...(nextIndex < parts.length ? { nextCursor: encodeSchemaCursor(read.versionKey, nextIndex) } : {})
104
+ };
105
+ }
106
+ const full = { ...read.base, nodeCount, [read.field]: read.nodes };
107
+ const chars = (JSON.stringify(prune(full)) ?? '').length;
108
+ if (chars > read.maxResponseBytes) {
109
+ if (read.allowTruncated !== true) {
110
+ const kb = (value) => (value / 1024).toFixed(1);
111
+ throw new PlatformApiError(`Şema ${nodeCount} düğüm / ${kb(chars)} KB; tek yanıta sığmaz (bütçe ${kb(read.maxResponseBytes)} KB). Kırpılmış şema hangi düğümlerin düştüğünü söylemez ve geri yazılırsa ${read.noun === 'Sayfa' ? 'sayfa' : 'layout'} eksilir. cursor:'0' ile parça parça okuyun ya da allowTruncated:true verin (kırpılır, hangi düğümlerin düştüğü söylenmez). Hedef bulmak için mode:'summary' yeter.`, 400);
112
+ }
113
+ read.onTruncated?.();
114
+ }
115
+ return full;
116
+ }
78
117
  /** `'0'` sürümsüz ilk parçadır; `<sürüm>:<parça>` sürümü taşır. Çözülemeyen cursor `undefined`. */
79
118
  export function decodeSchemaCursor(cursor) {
80
119
  if (/^\d+$/.test(cursor))
@@ -99,8 +138,8 @@ export function summarize(node, styleSource, nativeTheme) {
99
138
  };
100
139
  // Mobil uygulama: stil CSS değil React Native nesnesidir (`shell` kutu, `text` metin).
101
140
  if (nativeTheme !== undefined) {
102
- const { shell, text, warnings } = nodeNativeStyle(node, nativeTheme, styleSource ?? undefined);
103
- summary.nativeStyle = { shell, text, ...(warnings.length > 0 ? { warnings } : {}) };
141
+ const { shell, text, motion, bound, warnings } = nodeNativeStyle(node, nativeTheme, styleSource ?? undefined);
142
+ summary.nativeStyle = { shell, text, ...(motion ? { motion } : {}), ...(bound ? { bound } : {}), ...(warnings.length > 0 ? { warnings } : {}) };
104
143
  }
105
144
  else if (styleSource !== undefined) {
106
145
  const report = resolveNodeStyleReport(node, styleSource ?? undefined);
@@ -1,14 +1,84 @@
1
- import { guid, projectParam, environmentParam, readOnly, updates } from './common.js';
1
+ import { guid, projectParam, environmentParam, readOnly, writes, updates } from './common.js';
2
2
  import { handle } from '../platform/responses.js';
3
+ import { PlatformApiError } from '../platform/errors.js';
3
4
  import { z } from 'zod';
4
5
  import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
5
6
  import { readProjectLanguages, translationLanguageFindings } from '../knowledge/languages.js';
6
7
  import { policyRules } from '../knowledge/rules.js';
8
+ import { readFile, writeFile } from 'node:fs/promises';
9
+ import { basename, extname, resolve } from 'node:path';
10
+ const isoLanguageCode = z.string().regex(/^[a-z]{2}$/, 'ISO 639-1 iki harfli küçük dil kodu (tr, en, de …); tr-TR gibi bölge ekli kod reddedilir');
7
11
  const translationKeyInput = z.object({
8
12
  key: z.string().min(1),
9
- values: z.record(z.string().regex(/^[a-z]{2}$/, 'ISO 639-1 iki harfli küçük dil kodu (tr, en, de …); tr-TR gibi bölge ekli kod reddedilir'), z.string()).describe("Dil kodu (ISO 639-1, iki küçük harf) → çeviri metni. Platform kodu yalnız 184 kodluk ISO sözlüğüne göre doğrular (TR-001); PROJENİN languages listesine göre doğrulamaz — o bağı gateway kurar (TR-002)"),
13
+ values: z.record(isoLanguageCode, z.string()).describe("Dil kodu (ISO 639-1, iki küçük harf) → çeviri metni. Platform kodu yalnız 184 kodluk ISO sözlüğüne göre doğrular (TR-001); PROJENİN languages listesine göre doğrulamaz — o bağı gateway kurar (TR-002)"),
10
14
  componentId: guid.optional().describe("Çeviriyi belirli bir komponente bağlar; o komponent silinince kaydı da silinir")
11
15
  });
16
+ /**
17
+ * ADR 0018 (zeyrek-backend/docs/adr/0018-ceviri-dosya-aktarimi.md) §4: çeviri dosyası platformda
18
+ * saklanmaz — içe aktarma ucu dosyanın kendisini multipart alır, dışa aktarma yalnız yerel yola
19
+ * yazılır. Sınır backend `TranslationConstants.ImportMaxFileBytes` ile aynıdır; aşan dosya gidip
20
+ * 422 almak yerine burada durur.
21
+ */
22
+ const translationFileMaxBytes = 10 * 1024 * 1024;
23
+ const xlsxContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
24
+ /**
25
+ * `translation_update { file }` kipi. Backend `POST /translations/{applicationId}/import`
26
+ * (`ImportTranslationsCommand`) dili kendisi projeye eklediği için TR-002 ön okuması yapılmaz.
27
+ */
28
+ async function importTranslationFile(platform, { project, environment, applicationId, file, targetLanguage, dryRun }) {
29
+ const absolute = resolve(file);
30
+ if (extname(absolute).toLowerCase() !== '.xlsx') {
31
+ throw new PlatformApiError(`translation_update: file bir .xlsx dosyası olmalı (${absolute}). Çeviri dosyası yalnız Excel biçiminde içe aktarılır; biçimi translation_export'un ürettiği dosyadır.`);
32
+ }
33
+ let bytes;
34
+ try {
35
+ bytes = await readFile(absolute);
36
+ }
37
+ catch (error) {
38
+ throw new PlatformApiError(`translation_update: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
39
+ }
40
+ if (bytes.byteLength > translationFileMaxBytes) {
41
+ throw new PlatformApiError(`translation_update: dosya ${bytes.byteLength} B, platformun 10 MB çeviri dosyası sınırının üstünde. Dosyada resim ya da ek sayfa varsa kaldırın; yalnız Key / kaynak / hedef sütunları gerekir.`);
42
+ }
43
+ const response = await platform.tenant('POST', project, `/translations/${applicationId}/import`, {
44
+ environment,
45
+ multipart: [
46
+ { name: 'File', filename: basename(absolute), contentType: xlsxContentType, content: bytes },
47
+ { name: 'TargetLanguage', value: targetLanguage },
48
+ { name: 'DryRun', value: dryRun ? 'true' : 'false' }
49
+ ]
50
+ });
51
+ const result = response?.data ?? {};
52
+ const notes = [];
53
+ if (result.languageAdded) {
54
+ notes.push(`Hedef dil '${result.targetLanguage}' projeye bütün ortamlarda eklendi; production'da çeviriler deployment'la gelir, o zamana kadar varsayılan dile düşer (TR-004).`);
55
+ }
56
+ else if (result.dryRun && result.targetLanguageInProject === false) {
57
+ notes.push(`Hedef dil '${result.targetLanguage}' projede yok; dryRun olmadan yapılan çağrı onu projeye bütün ortamlarda ekleyecek (TR-004).`);
58
+ }
59
+ if ((result.unknownKeyCount ?? 0) > 0) {
60
+ notes.push(`${result.unknownKeyCount} key application'da yok ve açılmadı (${(result.unknownKeys ?? []).join(', ')}). Yeni key için keys kipini kullanın.`);
61
+ }
62
+ if (result.applicationMismatch) {
63
+ notes.push('Dosya başka bir application\'dan dışa aktarılmış (_zeyrek meta); yalnız bu application\'da bulunan key\'ler yazıldı.');
64
+ }
65
+ return { ...result, ...(notes.length > 0 ? { note: notes.join(' ') } : {}) };
66
+ }
67
+ /**
68
+ * 38 S-6: platform PUT yanıtında uygulamanın BÜTÜN sözlüğünü döner (`ArrayBaseResponse<TranslationDto>`;
69
+ * 3. çağrıda 226 satır, ~25 bin karakter) — tek anahtar eklemek bile tam sözlüğü ajan bağlamına
70
+ * sokuyordu. Yanıt yazılanın özetidir; sözlüğün kendisi translation_list'tedir. Örnek sınırı
71
+ * `row_*` path yanıtındaki id örneğiyle aynıdır (37 S-4).
72
+ */
73
+ const writtenKeySample = 10;
74
+ function translationWriteSummary(keys, deletedKeys) {
75
+ return {
76
+ written: keys.length,
77
+ ...(deletedKeys.length > 0 ? { deleted: deletedKeys.length } : {}),
78
+ keys: keys.slice(0, writtenKeySample).map(entry => entry.key),
79
+ truncated: keys.length > writtenKeySample
80
+ };
81
+ }
12
82
  /**
13
83
  * Yazma öncesi projenin dil durumunu okur. Proje okunamazsa (uç düşer, yetki yoksa) kontrol
14
84
  * ATLANIR ve karar platforma kalır — `project_create`'in slug ön aramasındaki (PRJ-001) davranışın
@@ -30,24 +100,96 @@ export function registerTranslationTools(server, platform) {
30
100
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid },
31
101
  annotations: readOnly
32
102
  }, handle(config, ({ project, environment, applicationId }) => platform.tenant('GET', project, `/translations/${applicationId}`, { environment })));
103
+ server.registerTool('translation_export', {
104
+ title: 'Çevirileri Excel olarak dışa aktar',
105
+ description: "Application'ın bütün çeviri key'lerini çevirmene verilecek bir .xlsx dosyasına yazar: A Key · B kaynak dil (referans) · C hedef dil (hedefte değeri olan key'ler dolu gelir). Dosya YALNIZ outPath'e yazılır — Sürücü'ye yüklenmez, platformda iz kalmaz; içerik bağlama girmez, yanıt yol ve sayılardır ({ keys: application'daki key sayısı, targetMissing: hedefte boş olanlar }). Çevirmen C sütununu doldurduktan sonra dosyayı translation_update { file, targetLanguage, dryRun: true } ile önce sınayın, sonra dryRun olmadan içe aktarın; hedef dil projede yoksa içe aktarma onu projeye BÜTÜN ortamlarda ekler (TR-004). Ajan çeviriyi kendisi yapacaksa dosyaya gerek yok: translation_list → translation_update { keys }.",
106
+ inputSchema: {
107
+ project: projectParam,
108
+ environment: environmentParam,
109
+ applicationId: guid,
110
+ targetLanguage: isoLanguageCode.describe("Çevirmenin dolduracağı hedef dil (C sütunu); başlığa 'Target Language (ES)' biçiminde yalnız çevirmen için yazılır — içe aktarma başlığı okumaz, dili translation_update'in targetLanguage'ı belirler"),
111
+ sourceLanguage: isoLanguageCode.optional().describe('Çevirmene referans olacak kaynak dil (B sütunu); verilmezse projenin varsayılan dili'),
112
+ missingOnly: z.boolean().optional().describe("true: yalnız hedef dilde değeri olmayan ya da boş key'ler yazılır"),
113
+ outPath: z.string().min(1).describe("Dosyanın yazılacağı yerel yol, .xlsx ile biter (zorunlu). Yol gateway sürecinin çalıştığı makineye göre çözülür; var olan dosya ezilir")
114
+ },
115
+ annotations: writes
116
+ }, handle(config, async ({ project, environment, applicationId, targetLanguage, sourceLanguage, missingOnly, outPath }) => {
117
+ if (sourceLanguage === targetLanguage) {
118
+ throw new PlatformApiError(`translation_export: kaynak ve hedef dil aynı ('${targetLanguage}'). sourceLanguage'ı başka bir dil yapın ya da vermeyip projenin varsayılan diline bırakın.`);
119
+ }
120
+ const absolute = resolve(outPath);
121
+ if (extname(absolute).toLowerCase() !== '.xlsx') {
122
+ throw new PlatformApiError(`translation_export: outPath .xlsx ile bitmeli (${absolute}); çeviri dosyası yalnız Excel biçiminde üretilir.`);
123
+ }
124
+ const response = await platform.tenantWithHeaders('GET', project, `/translations/${applicationId}/export`, {
125
+ environment,
126
+ expectBinary: true,
127
+ query: { targetLanguage, ...(sourceLanguage ? { sourceLanguage } : {}), ...(missingOnly ? { missingOnly } : {}) }
128
+ });
129
+ const payload = response.body;
130
+ try {
131
+ await writeFile(absolute, payload.bytes);
132
+ }
133
+ catch (error) {
134
+ throw new PlatformApiError(`translation_export: dosya yazılamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Klasör var olmalı; yol gateway sürecinin çalıştığı makineye göre çözülür.`);
135
+ }
136
+ const listed = await platform.tenant('GET', project, `/translations/${applicationId}`, { environment });
137
+ const translations = listed?.data ?? [];
138
+ const keys = new Set(translations.map(translation => translation.key));
139
+ const translatedKeys = new Set(translations.filter(translation => translation.language === targetLanguage && (translation.value ?? '').trim() !== '').map(translation => translation.key));
140
+ const projectLanguages = readProjectLanguages(await platform.org('GET', `/p/${platform.resolveProject(project)}`).catch(() => undefined));
141
+ const fileName = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(response.headers['content-disposition'] ?? '')?.[1];
142
+ const notes = projectLanguages && !projectLanguages.languages.includes(targetLanguage)
143
+ ? [`Hedef dil '${targetLanguage}' projenin dil listesinde yok; dosya içe aktarılınca proje diline bütün ortamlarda eklenir (TR-004).`]
144
+ : [];
145
+ notes.push(`Çevirmen C sütununu doldurduktan sonra: translation_update { file: '<yol>', targetLanguage: '${targetLanguage}', dryRun: true } → sonra dryRun olmadan.`);
146
+ return {
147
+ outPath: absolute,
148
+ fileName: fileName ?? basename(absolute),
149
+ bytes: payload.bytes.byteLength,
150
+ keys: keys.size,
151
+ targetMissing: keys.size - translatedKeys.size,
152
+ sourceLanguage: sourceLanguage ?? projectLanguages?.defaultLanguage,
153
+ targetLanguage,
154
+ note: notes.join(' ')
155
+ };
156
+ }));
33
157
  server.registerTool('translation_update', {
34
158
  title: 'Çevirileri güncelle',
35
- description: "Bir ya da daha çok çeviri anahtarını toplu ekler/günceller (keys) ve/veya siler (deletedKeys) — tek çağrıda ikisi de verilebilir. 'keys' üzerinden yeni bir anahtar da açılabilir (upsert; verilmeyen diller silinmez, korunur). DİKKAT: çeviri yazmak tek başına YETMEZ — runtime bir etiketi ancak projenin languages listesi ve defaultLanguage kuruluysa çözer, aksi hâlde ekrana ham anahtar basar ve yazma yine 200 döner (ölçüldü 2026-09-09, S-15). Gateway yazmadan önce projeyi okur ve bağ kopuksa TR-002 caution'ı düşer; dilleri project_update(languages, defaultLanguage) ile kurun. Backend ön kontrolü (TR-001, block): anahtarlar boş olamaz ve tekil, dil kodları ISO 639-1 listesinden, deletedKeys boş olamaz.",
159
+ description: "Bir ya da daha çok çeviri anahtarını toplu ekler/günceller (keys) ve/veya siler (deletedKeys) — tek çağrıda ikisi de verilebilir. 'keys' üzerinden yeni bir anahtar da açılabilir (upsert; verilmeyen diller silinmez, korunur). DİKKAT: çeviri yazmak tek başına YETMEZ — runtime bir etiketi ancak projenin languages listesi ve defaultLanguage kuruluysa çözer, aksi hâlde ekrana ham anahtar basar ve yazma yine 200 döner (ölçüldü 2026-09-09, S-15). Gateway yazmadan önce projeyi okur ve bağ kopuksa TR-002 caution'ı düşer; dilleri project_update(languages, defaultLanguage) ile kurun. Backend ön kontrolü (TR-001, block): anahtarlar boş olamaz ve tekil, dil kodları ISO 639-1 listesinden, deletedKeys boş olamaz. Yanıt özettir: { written, deleted?, keys (yazılan ilk 10 anahtar), truncated } — platformun döndürdüğü tam sözlük bağlama taşınmaz, okumak için translation_list. DOSYA KİPİ: 'file' (yerel .xlsx yolu, translation_export biçimi) verilirse keys/deletedKeys verilmez; dosya platforma multipart gider ve SAKLANMAZ. C sütunu 'targetLanguage'ta verilen dile yazılır (zorunlu; başlık metni okunmaz — C1'deki '(ES)' gibi kod dili belirlemez): dolu hücre mevcut değeri EZER, boş hücre atlanır, application'da olmayan key açılmaz (unknownKeys); hedef dil projede yoksa projeye BÜTÜN ortamlarda eklenir (TR-004). 'dryRun: true' hiçbir şey yazmadan sayar. Yanıt: { targetLanguage, created, updated, unchanged, skippedEmpty, unknownKeyCount, unknownKeys, targetLanguageInProject, languageAdded, applicationMismatch, dryRun, note? }.",
36
160
  inputSchema: {
37
161
  project: projectParam,
38
162
  environment: environmentParam,
39
163
  applicationId: guid,
40
164
  keys: z.array(translationKeyInput).optional().describe('Eklenecek/güncellenecek anahtarlar'),
41
- deletedKeys: z.array(z.string().min(1)).optional().describe('Silinecek anahtar adları')
165
+ deletedKeys: z.array(z.string().min(1)).optional().describe('Silinecek anahtar adları'),
166
+ file: z.string().min(1).optional().describe("Çevirmenin doldurduğu yerel .xlsx yolu (en çok 10 MB): A Key, B kaynak dil (okunmaz), C hedef dil; başlık satırı okunmaz, dil targetLanguage'tan gelir. Gateway dosyayı okuyup içe aktarma ucuna gönderir, Sürücü'ye yüklemez. keys/deletedKeys ile birlikte verilmez"),
167
+ targetLanguage: isoLanguageCode.optional().describe("Yalnız file ile ve file verilince ZORUNLU: C sütununun yazılacağı dil (ISO 639-1, 'de'). Dosyanın başlığı dili belirlemez; projede yoksa dil projeye BÜTÜN ortamlarda eklenir (TR-004)"),
168
+ dryRun: z.boolean().optional().describe('Yalnız file ile: yazmadan created/updated/unchanged/skippedEmpty/unknownKeys sayımını ve dilin projeye eklenip eklenmeyeceğini döner')
42
169
  },
43
170
  annotations: updates
44
- }, handle(config, async ({ project, environment, applicationId, keys, deletedKeys }) => {
171
+ }, handle(config, async ({ project, environment, applicationId, keys, deletedKeys, file, targetLanguage, dryRun }) => {
172
+ if (file !== undefined && (keys?.length || deletedKeys?.length)) {
173
+ throw new PlatformApiError("translation_update: 'file' ile 'keys'/'deletedKeys' birlikte verilmez — dosyayı ayrı bir çağrıda içe aktarın, tek tek anahtarları başka bir çağrıda yazın.");
174
+ }
175
+ if (dryRun !== undefined && file === undefined) {
176
+ throw new PlatformApiError("translation_update: dryRun yalnız 'file' kipinde geçerlidir; keys/deletedKeys yazımının kuru koşusu yoktur.");
177
+ }
178
+ if (targetLanguage !== undefined && file === undefined) {
179
+ throw new PlatformApiError("translation_update: targetLanguage yalnız 'file' kipinde geçerlidir; keys yazımında dil her değerin kendi anahtarıdır (values: { de: '…' }).");
180
+ }
181
+ if (file !== undefined && targetLanguage === undefined) {
182
+ throw new PlatformApiError("translation_update: 'file' kipinde targetLanguage zorunludur — C sütununun hangi dile yazılacağını dosya değil istek söyler (ör. targetLanguage: 'de'). Dosyanın başlığındaki '(DE)' okunmaz.");
183
+ }
184
+ if (file !== undefined)
185
+ return importTranslationFile(platform, { project, environment, applicationId, file, targetLanguage: targetLanguage, dryRun: dryRun ?? false });
45
186
  if (!keys?.length && !deletedKeys?.length) {
46
187
  throw new Error("'keys' ya da 'deletedKeys' parametrelerinden en az biri dolu olmalı.");
47
188
  }
48
189
  const body = { keys: keys ?? [], deletedKeys: deletedKeys ?? [] };
49
190
  const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'translation', body, { operation: 'update' }), false);
50
191
  const languages = await languageFindings(platform, project, keys ?? []);
51
- return attachFindings(await platform.tenant('PUT', project, `/translations/${applicationId}`, { environment, body }), [...findings, ...languages]);
192
+ await platform.tenant('PUT', project, `/translations/${applicationId}`, { environment, body });
193
+ return attachFindings(translationWriteSummary(body.keys, body.deletedKeys), [...findings, ...languages]);
52
194
  }));
53
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appaflytech/zeyrek-ai-mcp",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "type": "module",
5
5
  "description": "Zeyrek (NgApps) platform APIs as MCP tools — data, logic and Studio design surface in one gateway",
6
6
  "license": "UNLICENSED",
@@ -60,4 +60,4 @@
60
60
  "typescript": "^5.7.2",
61
61
  "vitest": "^3.0.0"
62
62
  }
63
- }
63
+ }