@appaflytech/zeyrek-ai-mcp 0.0.20 → 0.0.21

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 (65) hide show
  1. package/README.md +14 -13
  2. package/dist/config.js +0 -2
  3. package/dist/knowledge/componentCatalog.js +940 -215
  4. package/dist/knowledge/componentEvents.js +368 -75
  5. package/dist/knowledge/normalize.js +66 -63
  6. package/dist/knowledge/rules.js +29 -6
  7. package/dist/knowledge/styleResolve.js +1 -1
  8. package/dist/knowledge/themeResolve.js +3 -3
  9. package/dist/platform/diagnostics.js +85 -0
  10. package/dist/platform/domSummary.js +1 -1
  11. package/dist/platform/ifExists.js +129 -0
  12. package/dist/platform/responses.js +2 -1
  13. package/dist/platform/{capture/capture.js → screenshot.js} +509 -23
  14. package/dist/platform/strictArguments.js +22 -2
  15. package/dist/platform/writeDiff.js +58 -31
  16. package/dist/platform/writeView.js +17 -0
  17. package/dist/resources/guides/component.md +9 -6
  18. package/dist/resources/guides/functions.md +32 -3
  19. package/dist/resources/guides/getting-started.md +35 -0
  20. package/dist/resources/guides/layout.md +7 -11
  21. package/dist/resources/guides/page.md +49 -71
  22. package/dist/resources/guides/translation.md +5 -6
  23. package/dist/resources/schema-fingerprint.json +2 -2
  24. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +18 -0
  25. package/dist/server.js +0 -2
  26. package/dist/tools/applications.js +3 -60
  27. package/dist/tools/components.js +10 -50
  28. package/dist/tools/designSchemaPatch.js +13 -18
  29. package/dist/tools/externalComponents.js +4 -25
  30. package/dist/tools/flows.js +38 -51
  31. package/dist/tools/functions.js +20 -10
  32. package/dist/tools/layouts.js +9 -41
  33. package/dist/tools/microservices.js +39 -11
  34. package/dist/tools/pages.js +95 -127
  35. package/dist/tools/platformInfo.js +1 -2
  36. package/dist/tools/schemaOps.js +2 -2
  37. package/dist/tools/screenWrite.js +2 -2
  38. package/dist/tools/screens.js +3 -9
  39. package/dist/tools/workflows.js +39 -12
  40. package/package.json +7 -2
  41. package/dist/knowledge/mobileComponentCatalog.js +0 -331
  42. package/dist/knowledge/mobileComponentEvents.js +0 -50
  43. package/dist/knowledge/platform/lint.js +0 -205
  44. package/dist/knowledge/platform/mobile.js +0 -78
  45. package/dist/knowledge/platform/mobileRules.js +0 -121
  46. package/dist/knowledge/platform/mobileTabs.js +0 -108
  47. package/dist/knowledge/platform/profile.js +0 -20
  48. package/dist/knowledge/platform/resolve.js +0 -122
  49. package/dist/knowledge/platform/web.js +0 -38
  50. package/dist/knowledge/styleResolveNative.js +0 -410
  51. package/dist/platform/capture/actions.js +0 -333
  52. package/dist/platform/capture/browser.js +0 -1
  53. package/dist/platform/capture/index.js +0 -10
  54. package/dist/platform/capture/readiness.js +0 -103
  55. package/dist/platform/capture/summary.js +0 -77
  56. package/dist/platform/capture/target.js +0 -5
  57. package/dist/platform/capture/targets/studio-mobile.js +0 -15
  58. package/dist/platform/capture/targets/studio-web.js +0 -5
  59. package/dist/resources/examples/mobile-app-shell-drawer.json +0 -209
  60. package/dist/resources/examples/mobile-app-shell-stack.json +0 -123
  61. package/dist/resources/examples/mobile-app-shell-tab.json +0 -174
  62. package/dist/resources/examples/mobile-list-detail-form.json +0 -656
  63. package/dist/resources/guides/mobile.md +0 -215
  64. package/dist/tools/mobile.js +0 -203
  65. package/dist/tools/profileWrite.js +0 -45
@@ -2,7 +2,6 @@ import { logStep } from '../observability/trace.js';
2
2
  import { PlatformApiError } from '../platform/errors.js';
3
3
  import { ensureWorkflowErrorPath } from './errorPath.js';
4
4
  import { TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
- import { STEP_ALIASES } from './platform/profile.js';
6
5
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
7
6
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
8
7
  function asArray(value) {
@@ -270,11 +269,12 @@ export function normalizeWorkflowDefinition(definition) {
270
269
  */
271
270
  /**
272
271
  * Adım tipi sözlüğü backend'in, KOŞAN tipler barındırıcının: motor yedi tip register eder, web
273
- * `Runtime.svelte` altı, mobil `runtime-tasks.ts` yedi tip ekler ve aynı sayfa kaydı ikisinde de
272
+ * `Runtime.svelte` altı, mobil `ScreenRuntime.tsx` beş tip ekler ve aynı sayfa kaydı ikisinde de
274
273
  * koşar. Register edilmemiş bir tipe gelen akış sessizce ölür (`flow-engine.ts` `process()`
275
274
  * callback bulamazsa next/completed/stopped hiç çağrılmaz), üstelik `run()` promise'i
276
275
  * çözülmediği için `__ngAppsFlowActivity.running` sıfıra inmez. İki hâl tamamen türetilebilir
277
- * olduğu için lint değil NORMALİZASYON; `FLOW-023` kalan hâlleri uyarır.
276
+ * olduğu için lint değil NORMALİZASYON — kalanı (`confirm`/`download` mobilde yok) yalnız
277
+ * uygulama tarafında kapanabilir, `FLOW-023` onları uyarır.
278
278
  */
279
279
  function normalizeFlowHostGaps(steps, list) {
280
280
  const fixes = [];
@@ -288,15 +288,12 @@ function normalizeFlowHostGaps(steps, list) {
288
288
  .map(step => String(step.stepId ?? ''))
289
289
  .filter(id => id.length > 0));
290
290
  for (const step of list) {
291
- // navigate ile redirect'in argümanı AYNI ({ path, pageId, query, targetBlank, presentation })
292
- // ama navigate'i yalnız mobil register eder; redirect ise iki barındırıcıda da koşar (mobilde
293
- // aynı navigate uygulamasına eşlenir), yani redirect kesin üstün. `presentation`
294
- // (push | modal | replace) yalnız mobil barındırıcıda okunur; web yok sayar.
295
- const alias = STEP_ALIASES[String(step.type ?? '')];
296
- if (alias) {
297
- const original = String(step.type);
298
- step.type = alias;
299
- fixes.push(`'${label(step)}': type ${original} → ${alias} yazıldı — argüman şekli aynı ({ path, pageId, query, targetBlank, presentation }) ama ${original}'i web barındırıcısı register etmez, ${alias} ikisinde de koşar`);
291
+ // navigate ile redirect'in argümanı AYNI ({ path, pageId, query, targetBlank }) ama navigate'i
292
+ // yalnız mobil register eder; redirect ise iki barındırıcıda da koşar (mobilde aynı navigate
293
+ // uygulamasına eşlenir), yani redirect kesin üstün.
294
+ if (step.type === 'navigate') {
295
+ step.type = 'redirect';
296
+ fixes.push(`'${label(step)}': type navigate → redirect yazıldı — argüman şekli aynı ({ path, pageId, query, targetBlank }) ama navigate'i web barındırıcısı register etmez, redirect ikisinde de koşar`);
300
297
  }
301
298
  if (endStepIds.size === 0)
302
299
  continue;
@@ -653,44 +650,6 @@ export function styleKeyNote(fixed) {
653
650
  + 'değer kaydedilir ama hiç uygulanmaz ve tek işareti tarayıcı konsoludur (F-06). '
654
651
  + "Seçici anahtarlara ('&__body', ':hover', '--token') dokunulmadı."];
655
652
  }
656
- /**
657
- * Kabın `states.isDroppable`ı KATALOĞUN kararıdır. Gateway düğümü kurarken bu alanı
658
- * yazmıyordu (`DEFAULT_STATES` hepsini `false` bırakır) ve Studio kanvası düğümü kap saymıyordu:
659
- * imleç `scroll-view`un İÇİNDEYKEN bırakma planı sayfa kabına düşüyor, çizgi kabın üst kenarına
660
- * çiziliyordu (ölçüldü 2026-09-21, `test/mobile app` › `Satislar`). Aynı tipi paletten ekleyen
661
- * Studio katalog değerini yazdığı için tek şemada iki türlü davranan kap oluyordu.
662
- *
663
- * Değer uygulamanın KENDİ sözlüğünden gelir (`listComponents`; mobil uygulamada `type=mobile`
664
- * satırları), yani kural platformdan bağımsızdır ve kuruluşun kendi komponentlerini de kapsar.
665
- * Yalnız `true` yazılır: katalog bir tipi kap saymıyorsa düğüme dokunulmaz.
666
- */
667
- export function normalizeDroppableStates(nodes, droppable) {
668
- const fixed = [];
669
- if (droppable.size === 0)
670
- return fixed;
671
- for (const node of asObjects(nodes)) {
672
- const type = typeof node.type === 'string' ? node.type : '';
673
- if (droppable.get(type) === true) {
674
- // `states` normalde `normalizeComponentDefaults`tan geçmiş olur; gelmediyse burada da
675
- // eksiksiz kurulur — yarım `states` platformda NullReference üretir.
676
- if (!node.states || typeof node.states !== 'object' || Array.isArray(node.states)) {
677
- node.states = { ...DEFAULT_STATES };
678
- }
679
- const states = node.states;
680
- if (states.isDroppable !== true) {
681
- states.isDroppable = true;
682
- fixed.push(`'${String(node.elementId ?? node.name ?? type)}' (${type})`);
683
- }
684
- }
685
- fixed.push(...normalizeDroppableStates(node.children, droppable));
686
- }
687
- return fixed;
688
- }
689
- export function droppableStatesNote(fixed) {
690
- return fixed.length === 0
691
- ? []
692
- : [`states.isDroppable katalogdan yazıldı (${fixed.join(' · ')}) — kataloğun kap saydığı düğüm Studio kanvasında da kap olmalı; alanı bilerek kapatmak için resize_component/set_states kullanın`];
693
- }
694
653
  export function normalizeComponentDefaults(nodes) {
695
654
  const fixed = [];
696
655
  for (const node of asObjects(nodes)) {
@@ -925,11 +884,11 @@ export function pageContainerNode() {
925
884
  * çekilirlerse sayfa içeriğinin akışına girer ve katman olarak açılmazlar.
926
885
  */
927
886
  const ROOT_LAYER_TYPES = new Set(['modal', 'drawer']);
928
- export function ensurePageContainer(schema, rootContainer = pageContainerNode) {
887
+ export function ensurePageContainer(schema) {
929
888
  const nodes = asObjects(schema);
930
889
  if (nodes.some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID))
931
890
  return { schema: nodes, fixes: [] };
932
- const container = rootContainer();
891
+ const container = pageContainerNode();
933
892
  const layers = nodes.filter(node => ROOT_LAYER_TYPES.has(String(node.type ?? '')));
934
893
  container.children = nodes.filter(node => !ROOT_LAYER_TYPES.has(String(node.type ?? '')));
935
894
  const moved = container.children.length;
@@ -2082,32 +2041,76 @@ export function orphanedFlowPlan(schema, pageEvents, ops) {
2082
2041
  if (op.op !== 'set_event' && op.op !== 'remove_event')
2083
2042
  continue;
2084
2043
  const eventName = String(op.event ?? '');
2044
+ const eventValue = typeof op.eventValue === 'string' ? op.eventValue : undefined;
2045
+ const boundTo = (events) => asObjects(events)
2046
+ .filter(event => String(event.name ?? '') === eventName && typeof event.value === 'string' && event.value !== '');
2047
+ // Bir olaya birden fazla akış bağlı olabilir. Değersiz `remove_event` o durumda platformda
2048
+ // `ambiguous-event` ile düşer ve hiçbir bağ kalkmaz; değerli olan yalnız o akışı çözer.
2049
+ const removedBy = (named) => eventValue === undefined
2050
+ ? (named.length === 1 ? named : [])
2051
+ : named.filter(event => event.value === eventValue);
2085
2052
  // `remove_event target:'page'` düğümü değil barındırıcının düzey olaylarını hedefler
2086
2053
  // (`PageSchemaMicroOpApplier.PageTarget`); düğüm aramasına düşseydi plan boş kalır ve yalnız
2087
2054
  // oraya bağlı akış kapıdan geçip kalıcı silinirdi. Düzey olayı olmayan widget'ta applier da
2088
2055
  // düğüm aramasına düşer.
2089
2056
  if (op.op === 'remove_event' && target === 'page' && Array.isArray(pageEvents)) {
2090
2057
  const hostEvents = asObjects(pageEvents);
2091
- const removed = hostEvents.find(event => String(event.name ?? '') === eventName);
2092
- if (typeof removed?.value !== 'string' || removed.value === '')
2093
- continue;
2094
- const surviving = new Set(hostEvents.filter(event => event !== removed && typeof event.value === 'string').map(event => String(event.value)));
2058
+ const removed = removedBy(boundTo(hostEvents));
2059
+ const surviving = new Set(hostEvents.filter(event => !removed.includes(event) && typeof event.value === 'string').map(event => String(event.value)));
2095
2060
  collectFlowRefsExcept(nodes, '', surviving);
2096
- if (!surviving.has(removed.value))
2097
- plan.push({ target, op: 'remove_event', flowIds: [removed.value] });
2061
+ const flowIds = [...new Set(removed.map(event => String(event.value)))].filter(flowId => !surviving.has(flowId));
2062
+ if (flowIds.length > 0)
2063
+ plan.push({ target, op: 'remove_event', flowIds });
2098
2064
  continue;
2099
2065
  }
2100
2066
  const node = findNodeByElementId(nodes, target);
2101
- const current = asObjects(node?.events).find(event => String(event.name ?? '') === eventName);
2102
- const currentValue = typeof current?.value === 'string' ? current.value : '';
2103
- if (currentValue === '' || currentValue === String(op.eventValue ?? ''))
2067
+ const named = boundTo(node?.events);
2068
+ // `set_event` adın BÜTÜN bağlarını tek kayda indirir; yeni değerden farklı her akış bağını kaybeder.
2069
+ const losing = op.op === 'set_event' ? named.filter(event => event.value !== eventValue) : removedBy(named);
2070
+ if (losing.length === 0)
2104
2071
  continue;
2105
2072
  const surviving = survivingRefs(target, eventName);
2106
- if (!surviving.has(currentValue))
2107
- plan.push({ target, op: String(op.op), flowIds: [currentValue] });
2073
+ for (const event of named) {
2074
+ if (!losing.includes(event))
2075
+ surviving.add(String(event.value));
2076
+ }
2077
+ const flowIds = [...new Set(losing.map(event => String(event.value)))].filter(flowId => !surviving.has(flowId));
2078
+ if (flowIds.length > 0)
2079
+ plan.push({ target, op: String(op.op), flowIds });
2108
2080
  }
2109
2081
  return plan;
2110
2082
  }
2083
+ /**
2084
+ * PAGE-034 — dolu bir olaya farklı akışla `set_event` gelmesi. `set_event` adın bütün bağlarını
2085
+ * TEK kayda indirir, yani önceki akış bağını sessizce kaybeder; akış başka yerden de
2086
+ * referanslıysa `orphanedFlowPlan` da susar ve kayıp hiç görünmez (maddelerim 4: makale önerisi
2087
+ * akışı, akıllı öneri bağlanınca çalışmaz oldu). Olaya ikinci akış eklemek `add_event`'tir; bilerek
2088
+ * değiştirmek için op'a `replaceBoundFlows: true` verilir. Okunan ağaçta olmayan hedef denetlenmez.
2089
+ */
2090
+ export function boundEventOps(schema, ops) {
2091
+ const nodes = asObjects(schema);
2092
+ return asObjects(ops)
2093
+ .filter(op => op.op === 'set_event' && op.replaceBoundFlows !== true)
2094
+ .flatMap(op => {
2095
+ const target = String(op.target ?? '');
2096
+ const event = String(op.event ?? '');
2097
+ const eventValue = typeof op.eventValue === 'string' ? op.eventValue : '';
2098
+ const bound = asObjects(findNodeByElementId(nodes, target)?.events)
2099
+ .filter(entry => String(entry.name ?? '') === event && typeof entry.value === 'string' && entry.value !== '')
2100
+ .map(entry => String(entry.value));
2101
+ const replaced = [...new Set(bound)].filter(flowId => flowId !== eventValue);
2102
+ if (replaced.length === 0)
2103
+ return [];
2104
+ return [`PAGE-034: '${target}' komponentinin '${event}' olayına zaten bağlı akış var (${replaced.join(', ')}). set_event olayın BÜTÜN bağlarını tek akışa indirir ve bu akışlar olay tetiklenince artık çalışmaz. `
2105
+ + `Olaya bir akış daha eklemek için { op: 'add_event', target: '${target}', event: '${event}', eventValue: '${eventValue}' } kullanın — bağlı akışların hepsi paralel koşar. `
2106
+ + `Mevcut bağı bilerek değiştirmek istiyorsanız aynı op'a replaceBoundFlows: true ekleyin.`];
2107
+ });
2108
+ }
2109
+ /** Gateway'e ait op alanları platforma gitmez. */
2110
+ export function stripGatewayEventFields(ops) {
2111
+ for (const op of ops)
2112
+ delete op.replaceBoundFlows;
2113
+ }
2111
2114
  export function orphanedFlowMessage(plan) {
2112
2115
  const lines = plan.map(entry => `'${entry.target}' (${entry.op}) → ${entry.flowIds.join(', ')}`);
2113
2116
  return `İstek gönderilmedi: bu yama ${plan.reduce((total, entry) => total + entry.flowIds.length, 0)} akışı sayfada referanssız bırakıyor ve platform referanssız akışı KALICI siler — ${lines.join(' · ')}. `
@@ -170,11 +170,13 @@ const flowLiteralTextFields = new Map([
170
170
  ['confirm', ['message', 'title', 'confirmText', 'cancelText']]
171
171
  ]);
172
172
  const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
173
- // Barındırıcıya özel tipler BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten
174
- // `redirect`e çeviriyor (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da
175
- // koşar, yani vaka uyarılmaz, ORTADAN KALKAR. Gateway uygulamanın tipini platform profilinden bilir
176
- // ve barındırıcı sözlüğü `platform/*.ts`'tedir (`profile.steps`); mobil farkları orada `MOB-005`
177
- // olarak konuşulur (`platform/lint.ts`). Yeni mobil runtime `confirm`/`download`'ı kaydeder.
173
+ // Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
174
+ // BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten `redirect`e çeviriyor
175
+ // (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da koşar, yani vaka
176
+ // uyarılmaz, ORTADAN KALKAR. Geriye kalan `confirm`/`download` tek başına kusur değildir: sayfa
177
+ // tek bir uygulamaya aittir (`Page.ApplicationId`), uygulamanın tipi web ya da mobile'dır
178
+ // (`ApplicationType.cs`) ve gateway hangisi olduğunu bilmez. Mobil ayağı uygulama tarafında
179
+ // kapanır (`ScreenRuntime.tsx`), lint'te değil.
178
180
  const flowUnregisteredStepTypes = new Set(['end', 'query']);
179
181
  const flowSetterIterateeTypes = new Set(['change', 'fill']);
180
182
  function flowStepList(value) {
@@ -420,6 +422,8 @@ export function rawQueryParameterNames(rawQuery) {
420
422
  }
421
423
  const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
422
424
  const functionsGuide = 'ngapps://guides/functions';
425
+ /** FN-006: bu kadar dolu satırdan uzun gövde yorumsuz kalmaz; tek satırlık `return` yorum istemez. */
426
+ const FUNCTION_COMMENT_MIN_LINES = 5;
423
427
  export const policyRules = [
424
428
  {
425
429
  id: 'MS-001',
@@ -2030,6 +2034,25 @@ export const policyRules = [
2030
2034
  return [];
2031
2035
  }
2032
2036
  },
2037
+ {
2038
+ id: 'FN-006',
2039
+ module: 'function',
2040
+ appliesTo: ['create', 'update'],
2041
+ sourceRef: "Kullanıcı isteği 2026-09-18: ajan fonksiyon yazarken gövdeye ne yaptığını söyleyen yorum koyar; Studio editörü yorumu yeşil italik çizer. Gövde kendi satırına sarılır (function-source.ts, GlobalFunction.cs AppendLine), '//' kapanış parantezini yutmaz",
2042
+ check: definition => {
2043
+ const name = String(definition.name ?? '?');
2044
+ const body = typeof definition.body === 'string' ? definition.body : '';
2045
+ const lineCount = body.split('\n').filter(line => line.trim() !== '').length;
2046
+ if (lineCount < FUNCTION_COMMENT_MIN_LINES || /(^|\s)\/\/|\/\*/m.test(body))
2047
+ return [];
2048
+ return [{
2049
+ ruleId: 'FN-006',
2050
+ severity: 'warn',
2051
+ message: `'${name}' gövdesi ${lineCount} satır ama hiç yorum yok. Başa ne yaptığını, parametreleri ve dönüşü söyleyen kısa bir '//' yorumu, açık olmayan adımların üstüne de birer satır yorum ekleyin; kodu tekrar eden yorum yazmayın.`,
2052
+ guide: functionsGuide
2053
+ }];
2054
+ }
2055
+ },
2033
2056
  {
2034
2057
  id: 'FLOW-024',
2035
2058
  module: 'flow',
@@ -2366,7 +2389,7 @@ export const policyRules = [
2366
2389
  id: 'FLOW-023',
2367
2390
  module: 'flow',
2368
2391
  appliesTo: ['create', 'update'],
2369
- sourceRef: "Adım tipi sözlüğü backend'indir (StepType.cs) ama koşan tipler barındırıcıdan gelir: flow-engine.ts start·setter·code·condition·microservice·validation·flow-trigger register eder, web Runtime.svelte alert·confirm·redirect·download·storage·setcookie ekler, mobil @ng-apps/ui-native runtime-tasks.ts alert·confirm·redirect·navigate·storage·setcookie·download ekler. navigate web'de YOKTUR; end ve query hiçbir barındırıcıda yoktur (Studio kataloğu da bunu visibility ile söyler: navigate 'mobile', End eklenebilir adımlar arasında değil — EventActionsAdd.svelte). Register edilmemiş tipe gelen akış SESSİZCE durur: flow-engine.ts process() callback bulamazsa hiçbir şey yapmaz (next yok, completed/stopped yok, hata yok, log yok), run() Promise'i çözülmez ve flowRunFinished() hiç koşmaz — __ngAppsFlowActivity.running sıfıra inmez, page_screenshot'ın hazır bekleyişi her eylemde bütçesini sonuna kadar yakar",
2392
+ sourceRef: "Adım tipi sözlüğü backend'indir (StepType.cs) ama koşan tipler barındırıcıdan gelir: flow-engine.ts start·setter·code·condition·microservice·validation·flow-trigger register eder, web Runtime.svelte alert·confirm·redirect·download·storage·setcookie ekler, mobil ScreenRuntime.tsx alert·redirect·navigate·storage·setcookie ekler. navigate web'de, confirm/download mobilde YOKTUR; end ve query hiçbir barındırıcıda yoktur (Studio kataloğu da bunu visibility ile söyler: navigate 'mobile', confirm/download 'web'; End eklenebilir adımlar arasında değil — EventActionsAdd.svelte). Register edilmemiş tipe gelen akış SESSİZCE durur: flow-engine.ts process() callback bulamazsa hiçbir şey yapmaz (next yok, completed/stopped yok, hata yok, log yok), run() Promise'i çözülmez ve flowRunFinished() hiç koşmaz — __ngAppsFlowActivity.running sıfıra inmez, page_screenshot'ın hazır bekleyişi her eylemde bütçesini sonuna kadar yakar",
2370
2393
  check: definition => {
2371
2394
  const steps = flowAllSteps(definition);
2372
2395
  // Studio her akış tuvaline dekoratif bir End düğümü koyar ve onu sildirmez; zararlı olan
@@ -539,7 +539,7 @@ const unwrapProp = (value) => {
539
539
  result[key] = unwrapProp(item);
540
540
  return result;
541
541
  };
542
- export const rawProp = (props, name) => {
542
+ const rawProp = (props, name) => {
543
543
  const prop = props?.[name];
544
544
  // İfadeye bağlı prop'un değeri çalışma anında oluşur, burada çözülemez.
545
545
  return prop && typeof prop === 'object' && 'data' in prop ? unwrapProp(prop.data) : undefined;
@@ -332,8 +332,8 @@ export function buildSpacingScaleVars(scale) {
332
332
  .join('\n\t');
333
333
  }
334
334
  /**
335
- * Bir seviyenin dar ekran karşılığının masaüstüne oranı — bugünkü breakpoint değerlerinden türer.
336
- * Kullanıcı tek çarpan ayarlar, dar ekran ona orantılı hareket eder.
335
+ * Bir seviyenin mobil karşılığının masaüstüne oranı — bugünkü breakpoint değerlerinden türer.
336
+ * Kullanıcı tek çarpan ayarlar, mobil ona orantılı hareket eder.
337
337
  */
338
338
  const TYPE_SCALE_MOBILE_RATIO = {
339
339
  h1: 1.7 / 2.25,
@@ -363,7 +363,7 @@ export function mergeTypeScale(overrides) {
363
363
  }
364
364
  return result;
365
365
  }
366
- /** Ölçekten `--ng-font-size-*` değişkenlerini üretir (masaüstü + dar ekran). */
366
+ /** Ölçekten `--ng-font-size-*` değişkenlerini üretir (masaüstü + mobil). */
367
367
  export function buildTypeScaleVars(scale) {
368
368
  const lines = [];
369
369
  for (const [level, factor] of Object.entries(scale)) {
@@ -0,0 +1,85 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { z } from 'zod';
3
+ const activeMode = new AsyncLocalStorage();
4
+ const MAX_NOTES = 8;
5
+ const MAX_NOTE_CHARS = 240;
6
+ const MAX_DROPS = 5;
7
+ export const diagnosticsParam = z.enum(['compact', 'full']).optional().describe("Yanıttaki gatewayNormalized / silentlyDropped listelerinin boyu. compact (varsayılan): aynı not tekilleşir, uzun not kısalır (en fazla 8); aynı yolun düşmeleri tek satırda sayıyla toplanır (en fazla 5 grup); droppedCount her zaman tam sayıdır. full: listelerin tamamı.");
8
+ export function runWithDiagnostics(mode, run) {
9
+ return activeMode.run(mode, run);
10
+ }
11
+ /**
12
+ * Tanılama listeleri her yazma çağrısında yanıta girer ve aynı gerekçe cümlesi çağrı başına tekrar
13
+ * eder. Kırpma yalnız GÖRÜNÜMÜ küçültür: `silentlyDropped` "kaydedildi mi" sorusunun tek kanıtı olduğu
14
+ * için liste hiçbir zaman sessizce boşalmaz — toplam sayı ve kalan satır sayısı yanıtta kalır, tam
15
+ * liste gateway günlüğünde (`sessiz düşme` / `düzeltme` adımları) durur.
16
+ *
17
+ * Kip bağlamı olmayan çağrı (okuma ve silme tool'ları, doğrudan `toolResult` kullanan testler) tam
18
+ * listeyi görür; kısaltma yalnız `diagnostics` parametresini taşıyan yazma tool'larında devreye girer.
19
+ */
20
+ export function compactDiagnostics(payload) {
21
+ if (activeMode.getStore() !== 'compact')
22
+ return payload;
23
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
24
+ return payload;
25
+ const record = payload;
26
+ const notes = Array.isArray(record.gatewayNormalized) ? record.gatewayNormalized.map(String) : undefined;
27
+ const drops = Array.isArray(record.silentlyDropped) ? record.silentlyDropped.map(String) : undefined;
28
+ if (notes === undefined && drops === undefined)
29
+ return payload;
30
+ const hidden = [];
31
+ const result = { ...record };
32
+ if (notes !== undefined) {
33
+ const counts = new Map();
34
+ for (const note of notes)
35
+ counts.set(note, (counts.get(note) ?? 0) + 1);
36
+ const unique = [...counts.entries()].map(([note, count]) => {
37
+ const shortened = note.length > MAX_NOTE_CHARS ? `${note.slice(0, MAX_NOTE_CHARS - 1)}…` : note;
38
+ return count > 1 ? `${shortened} (×${count})` : shortened;
39
+ });
40
+ const shortenedCount = notes.filter(note => note.length > MAX_NOTE_CHARS).length;
41
+ const kept = unique.slice(0, MAX_NOTES);
42
+ result.gatewayNormalized = unique.length > MAX_NOTES ? [...kept, `… ${unique.length - MAX_NOTES} not daha`] : kept;
43
+ if (unique.length > MAX_NOTES)
44
+ hidden.push(`${unique.length - MAX_NOTES} not`);
45
+ if (shortenedCount > 0)
46
+ hidden.push(`${shortenedCount} notun gerekçesi kısaltıldı`);
47
+ }
48
+ if (drops !== undefined) {
49
+ const droppedCount = typeof record.droppedCount === 'number' ? record.droppedCount : drops.length;
50
+ const grouped = groupDrops(drops, record.droppedCountCapped === true || droppedCount > drops.length);
51
+ const kept = grouped.slice(0, MAX_DROPS);
52
+ if (grouped.length !== drops.length)
53
+ hidden.push('benzer düşme satırları toplandı');
54
+ if (grouped.length > MAX_DROPS)
55
+ hidden.push(`${grouped.length - MAX_DROPS} düşme grubu`);
56
+ result.silentlyDropped = grouped.length > MAX_DROPS ? [...kept, `… ${grouped.length - MAX_DROPS} grup daha`] : kept;
57
+ result.droppedCount = droppedCount;
58
+ }
59
+ if (hidden.length === 0)
60
+ return result;
61
+ return { ...result, diagnosticsNote: "Kısaltıldı; yazmayı tekrarlamayın. Tam liste: sonraki çağrıda diagnostics:'full'." };
62
+ }
63
+ /**
64
+ * Aynı yolun farklı öğelerindeki düşmeler (`selectFields[fieldId=a].path`, `selectFields[fieldId=b].path`, …)
65
+ * aynı sorunu tekrar eder. Seçici ve indeks `[*]`'a indirgenir, grup tek satırda sayıyla ve bir örnekle
66
+ * döner. Listedeki HER satır kayıptır (sıra uyarısı ve kesme bilgisi `silentlyDroppedNotes`'tadır ve
67
+ * buraya hiç girmez); micro-op satırları (`add_component 'x': …`) da kendi yolu altında gruplanır.
68
+ * Liste kesildiyse ya da sayım tavana vurduysa grup sayısı "en az" diye yazılır.
69
+ */
70
+ function groupDrops(drops, partial) {
71
+ const groups = new Map();
72
+ for (const line of drops) {
73
+ const separator = line.indexOf(': ');
74
+ const path = separator === -1 ? line : line.slice(0, separator);
75
+ const outcome = line.includes(' ↔ ') ? (line.endsWith('kaydedilen yok') ? ' · kaydedilmedi' : ' · farklı kaydedildi') : '';
76
+ const pattern = `${path.replace(/\[[^\]]*\]/g, '[*]')}${outcome}`;
77
+ groups.set(pattern, [...(groups.get(pattern) ?? []), line]);
78
+ }
79
+ return [...groups.entries()].map(([pattern, lines]) => {
80
+ if (lines.length === 1)
81
+ return lines[0];
82
+ const example = lines[0].length > 160 ? `${lines[0].slice(0, 159)}…` : lines[0];
83
+ return `${pattern}: ${partial ? 'en az ' : ''}${lines.length} öğe — ör. ${example}`;
84
+ });
85
+ }
@@ -7,7 +7,7 @@
7
7
  * (card) · GİZLİ" 200 karakterdir.
8
8
  *
9
9
  * Katman saftır: tarayıcıdan gelen anlık görüntüyü doğrular ve biçimler. Toplama işi
10
- * `capture/summary.ts` `domSummaryScript`'te, sunum `pages.ts`'te.
10
+ * `screenshot.ts` `domSummaryScript`'te, sunum `pages.ts`'te.
11
11
  */
12
12
  function text(value) {
13
13
  return typeof value === 'string' && value.length > 0 ? value : undefined;
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { PlatformApiError } from './errors.js';
3
+ /**
4
+ * `ifExists` parametresi. Aynı ad ya da slug'la ikinci kaydın `"<ad> (1)"` olarak açılması platformun
5
+ * ürün kararıdır (akış, sayfa, workflow ve microservice servisleri aynı sayaç kuralını uygular) ve
6
+ * değişmez. MCP ajanı çağrıyı tekrar edebildiği için varsayılan "yazma, mevcut id'yi söyle"dir.
7
+ */
8
+ export function ifExistsParam(noun, updateNote) {
9
+ return z.enum(['error', 'update', 'duplicate']).default('error').describe(`Aynı ad ya da slug'da (büyük/küçük harf duyarsız) ${noun} varsa ne olacağı. 'error' (varsayılan): yazmaz, mevcut kaydın id'sini döner. 'update': ${updateNote}. 'duplicate': platformun ürün davranışı — ikinci kayıt açılır, platform ada/slug'a ' (1)' sayacını ekler (Studio'daki 'New … (1)' ile aynı kural).`);
10
+ }
11
+ const upper = (value) => String(value ?? '').toUpperCase();
12
+ const isPresent = (value) => value !== undefined && value !== null && String(value) !== '';
13
+ /**
14
+ * Var-mı kapısı (FLOW-026 ve kardeşleri). Aday araması platformda harfe duyarlıdır; gateway bilinçli
15
+ * olarak harf duyarsız karşılaştırır. Var-mı okuması başarısız olursa çağrı düşer — okuyamadığı
16
+ * listeyi boş sayan kontrol tam da zaman aşımında kopya üretirdi.
17
+ */
18
+ export async function existingMatch(options) {
19
+ const { code, noun, where, name, slug, id, idParam, ifExists } = options;
20
+ const slugCounter = (options.slugSuffix ?? 'dash') === 'dash' ? "'-1'" : "' (1)'";
21
+ if (ifExists === 'duplicate')
22
+ return undefined;
23
+ if (!name && !slug)
24
+ return undefined;
25
+ let records;
26
+ try {
27
+ records = await options.list();
28
+ }
29
+ catch (error) {
30
+ throw new PlatformApiError(`${code}: mevcut ${noun} kayıtları okunamadı, ${noun} YAZILMADI (${error instanceof Error ? error.message : String(error)}). Liste okunmadan yazmak aynı adla ikinci kayıt açabilir; ${options.listTool} ile tekrar deneyin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate' verin.`);
31
+ }
32
+ const matches = records.filter(record => (name !== undefined && name !== '' && upper(record.name) === upper(name)) ||
33
+ (slug !== undefined && slug !== '' && upper(record.slug) === upper(slug)));
34
+ const [match] = matches;
35
+ if (match === undefined)
36
+ return undefined;
37
+ const describe = (record) => `${String(record.id)} ('${String(record.name)}', slug '${String(record.slug)}')`;
38
+ const copyPattern = name
39
+ ? new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\(\\d+\\)$`, 'i')
40
+ : undefined;
41
+ const copies = copyPattern ? records.filter(record => copyPattern.test(String(record.name ?? ''))) : [];
42
+ const copyNote = copies.length > 0
43
+ ? ` ${where[0].toUpperCase()}${where.slice(1)} bu adın sayaçlı eşleri de var: ${copies.map(describe).join(' · ')} — ${options.copyCheck}.`
44
+ : '';
45
+ if (ifExists === 'error') {
46
+ throw new PlatformApiError(`${code}: ${where} aynı ad ya da slug'da ${noun} zaten var: ${matches.map(describe).join(' · ')}. İstek gönderilmedi — gönderilseydi platform ürün kuralı gereği ikinci bir ${noun} açardı (çakışan ada ' (1)', slug'a ${slugCounter} sayacı eklenerek; yalnız harf farkı varsa sayaçsız). Mevcut kaydı güncellemek için aynı çağrıyı ifExists:'update' ile tekrarlayın; bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
47
+ }
48
+ if (matches.length > 1) {
49
+ throw new PlatformApiError(`${code}: ad ve slug farklı kayıtlara çıkıyor (${matches.map(describe).join(' · ')}); hangisinin güncelleneceği belirsiz, istek gönderilmedi. ${options.updateTool} kullanın.${copyNote}`, 409);
50
+ }
51
+ // Eşleşme tipten bağımsızdır (platform sayacı tipe bakmaz), ama 'update' tipi farklı kaydın üstüne
52
+ // yazarsa kaydın türü değişir: backend PUT'ta tipi de yazar ve middleware automation'a dönüşür (R-1).
53
+ const kind = options.kind;
54
+ if (kind !== undefined && isPresent(kind.value) && isPresent(match[kind.key]) && upper(match[kind.key]) !== upper(kind.value)) {
55
+ throw new PlatformApiError(`${code}: aynı ad ya da slug'daki mevcut kayıt ${describe(match)} ${kind.key}='${String(match[kind.key])}'; gönderilen gövde ${kind.key}='${String(kind.value)}'. ifExists:'update' kaydın türünü değiştirirdi — istek gönderilmedi. Farklı ad/slug seçin ya da bilerek ikinci kayıt istiyorsanız ifExists:'duplicate'.${copyNote}`, 409);
56
+ }
57
+ if (id !== undefined && id.toLowerCase() !== String(match.id).toLowerCase()) {
58
+ throw new PlatformApiError(`${code}: verilen ${idParam} (${id}) aynı adlı mevcut kayıtla (${describe(match)}) çelişiyor; istek gönderilmedi. Mevcut kaydı güncellemek için ${idParam}'yi vermeyin ya da ${String(match.id)} verin.`, 409);
59
+ }
60
+ return match;
61
+ }
62
+ const LIST_PAGE_LENGTH = 200;
63
+ const LIST_PAGE_LIMIT = 50;
64
+ /**
65
+ * Uygulama düzeyindeki liste uçlarını (`GET /microservices|workflows|pages/{app}`) sonuna kadar okur.
66
+ * Arama terimi kullanılmaz: platformun `searchTerm`'ü slug'ı kapsamayabilir ve eksik liste kopya açar.
67
+ *
68
+ * Platform sayfaları kararlı sırada vermiyor (R-3): bir kayıt iki sayfada gelip başka biri hiç
69
+ * gelmeyebilir. İlk yanıttaki `meta.totalData` okunan TEKİL id sayısıyla tutmazsa liste tutarsızdır ve
70
+ * hata atılır — çağıran yazmaz. `meta` taşımayan eski yanıtta denetim yapılamaz.
71
+ */
72
+ export async function listAll(platform, project, environment, path) {
73
+ const records = [];
74
+ let expected;
75
+ for (let pageIndex = 0; pageIndex < LIST_PAGE_LIMIT; pageIndex++) {
76
+ const payload = await platform.tenant('GET', project, path, { environment, query: { pageIndex, pageLength: LIST_PAGE_LENGTH } });
77
+ if (!Array.isArray(payload?.data))
78
+ throw new PlatformApiError('liste yanıtında data dizisi yok');
79
+ if (pageIndex === 0 && typeof payload.meta?.totalData === 'number')
80
+ expected = payload.meta.totalData;
81
+ records.push(...payload.data);
82
+ if (payload.data.length < LIST_PAGE_LENGTH || (expected !== undefined && records.length >= expected)) {
83
+ const unique = new Set(records.map(record => String(record.id).toLowerCase())).size;
84
+ if (expected !== undefined && unique !== expected) {
85
+ throw new PlatformApiError(`liste tutarsız okundu: platform ${expected} kayıt dedi, sayfalardan ${unique} tekil kayıt geldi (${records.length - unique} tekrar) — sayfa sırası kararlı değil`);
86
+ }
87
+ return records;
88
+ }
89
+ }
90
+ throw new PlatformApiError(`liste ${LIST_PAGE_LENGTH * LIST_PAGE_LIMIT} kaydı aştı, tamamı okunamadı`);
91
+ }
92
+ /**
93
+ * `ifExists:'duplicate'` yazmasında platform ada ' (n)', slug'a '-n' (akışta ' (n)') ekler. Bu fark kayıp
94
+ * değildir (Gözlem 1): kalıba uyan alan karşılaştırmadan ÖNCE gönderilen gövdede kaydedilen değerle
95
+ * değiştirilir ve `renamedByPlatform`'a gider. Kalıba uymayan fark gövdede kalır, yani yine kayıptır.
96
+ */
97
+ export function platformRename(sent, saved, slugSuffix) {
98
+ const record = saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {};
99
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ const renamed = {};
101
+ const adjusted = { ...sent };
102
+ for (const key of ['name', 'slug']) {
103
+ const given = sent[key];
104
+ const stored = record[key];
105
+ if (typeof given !== 'string' || typeof stored !== 'string' || given === stored)
106
+ continue;
107
+ const pattern = key === 'slug' && slugSuffix === 'dash'
108
+ ? new RegExp(`^${escape(given)}-\\d+$`)
109
+ : new RegExp(`^${escape(given)} \\(\\d+\\)$`);
110
+ if (!pattern.test(stored))
111
+ continue;
112
+ renamed[key] = stored;
113
+ adjusted[key] = stored;
114
+ }
115
+ return Object.keys(renamed).length === 0 ? { sent: adjusted } : { sent: adjusted, renamed };
116
+ }
117
+ export function withRename(payload, renamed) {
118
+ if (renamed === undefined || !payload || typeof payload !== 'object' || Array.isArray(payload))
119
+ return payload;
120
+ return { ...payload, renamedByPlatform: renamed };
121
+ }
122
+ /**
123
+ * Yazma yanıtına `outcome` ekler; ajan kaydın yeni mi açıldığını yoksa güncellendiğini mi tek alandan okur.
124
+ */
125
+ export function withOutcome(payload, existing) {
126
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
127
+ return payload;
128
+ return { ...payload, outcome: existing === undefined ? 'created' : 'updated' };
129
+ }
@@ -1,3 +1,4 @@
1
+ import { compactDiagnostics } from './diagnostics.js';
1
2
  import { logError, logStep } from '../observability/trace.js';
2
3
  import { PlatformApiError, describeErrorBody } from './errors.js';
3
4
  import { PolicyBlockError } from '../knowledge/policy.js';
@@ -139,7 +140,7 @@ function asStructured(payload) {
139
140
  return { data: payload };
140
141
  }
141
142
  export function toolResult(payload, config, expectFailure) {
142
- const pruned = prune(payload);
143
+ const pruned = prune(compactDiagnostics(payload));
143
144
  const { payload: safe, masked, tokenRefs } = maskSecrets(pruned);
144
145
  const runtimeErrors = runtimeErrorSummary(safe);
145
146
  if (runtimeErrors.length > 0) {