@appaflytech/zeyrek-ai-mcp 0.0.10 → 0.0.12

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Zeyrek (NgApps) platformunu **MCP tool'ları** olarak sunan bağımsız gateway.
4
4
  oturumu Studio arayüzüne hiç girmeden proje kurabilir, veri modeli çıkarabilir, mikroservis ve
5
5
  workflow yazıp çalıştırabilir, sayfa tasarlayabilir.
6
6
 
7
- - **208 tool · 34 modül** — veri/mantık tarafı (project, application, datasource, entity, row,
7
+ - **212 tool · 34 modül** — veri/mantık tarafı (project, application, datasource, entity, row,
8
8
  import, microservice, query, workflow, automation, function, dto, view, dependency), Studio
9
9
  tasarım tarafı (page, **theme**, component, external component, flow, layout, variable, function,
10
10
  folder, file, access, translation), yönetim (template, organization, deployment) ve proje dallanması
@@ -140,9 +140,9 @@ ya da ikisi karışık (`core,page`). `platform` modülü (3 tool) her kümede v
140
140
  | Küme | Tool | Modüller |
141
141
  |---|---|---|
142
142
  | `core` | 136 | platform, project, application, datasource, entity, row, import, microservice, microfunction, query, workflow, automation, function, dto, view, dependency, environment, merge, activity, revision |
143
- | `studio` | 78 | platform, page, theme, component, externalComponent, flow, layout, variable, function, folder, file, import, access, translation, revision |
143
+ | `studio` | 82 | platform, page, theme, component, externalComponent, flow, layout, variable, function, folder, file, import, access, translation, revision |
144
144
  | `admin` | 27 | platform, template, organization, deployment, environment, merge, activity |
145
- | (boş) | **208** | 34 modülün tamamı |
145
+ | (boş) | **212** | 34 modülün tamamı (ölçüm 2026-09-16, `listTools()`) |
146
146
 
147
147
  Bilinmeyen bir modül/küme adı verilirse sunucu açılmaz, geçerli adları listeleyen bir hata yazar.
148
148
 
@@ -281,9 +281,14 @@ kural envanteri [`docs/08-kural-envanteri.md`](docs/08-kural-envanteri.md).
281
281
  Yayın otomatiktir: `main`'e `zeyrek-mcp/` değişikliği girince CI işi `publish:mcp:npm`
282
282
  ([`scripts/ci-publish-npm.sh`](scripts/ci-publish-npm.sh)) paketi üretip npm'deki son sürümle
283
283
  içerik olarak karşılaştırır; pakete giren dosyalar değiştiyse yeni patch sürümünü yayınlar.
284
- Sürümün kaynağı npm'dir, CI `package.json`'ı geri commit'lemez. Minor/major için
285
- `package.json`'daki sürümü elle yükseltip merge edin. Yayını o commit için atlamak: commit
286
- mesajına `DoNotDeploy` (iş elle tetiklenir hâle gelir).
284
+ Yayından sonra CI yeni sürümü `package.json` + `package-lock.json`'a yazıp
285
+ `chore: zeyrek-mcp X.Y.Z [skip ci]` bot commit'iyle `main`'e push eder (yeni pipeline
286
+ başlatmaz); yayın olmazsa commit de olmaz. Push CI_JOB_TOKEN ile yapılır; gereken GitLab
287
+ ayarları `.gitlab-ci.yml`'daki `publish:mcp:npm` notunda. Commit yazılamayıp iş kırmızı düşerse
288
+ push yetkisi olan biri işi Retry eder: yayın tekrarlanmaz, yalnız sürüm `main`'e yazılır.
289
+ Minor/major için `package.json`'daki
290
+ sürümü elle yükseltip merge edin. Yayını o commit için atlamak: commit mesajına `DoNotDeploy`
291
+ (iş elle tetiklenir hâle gelir).
287
292
 
288
293
  Yerelde deneme (yayın yapmaz; `npm ci` çalıştırdığı için bir kopyada ya da Linux container'da):
289
294
 
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * TASARIM ANLIK GÖRÜNTÜSÜ — bir tasarım dosyasının (Figma) TEK GEÇİŞTE okunup dosyaya alınmış
4
+ * hâli. `theme_discover`'ın girdisi budur; gateway Figma'ya kendisi gitmez (plan K1), süreç içi
5
+ * önbellek de tutmaz (HTTP taşımasında her POST yeni sunucu kurar — anlık görüntü DOSYADIR, K2).
6
+ *
7
+ * `.strict()` her seviyede: tanınmayan anahtar sessizce yaşamaz. Gerekçe tema şemasıyla aynı —
8
+ * ajan `variables` yerine `tokens` yazarsa bu bir yazım hatası değil, eşlemeye hiç girmeyen ve
9
+ * kimsenin görmediği bir veri kaybıdır. Şema onu istek anında adıyla reddeder.
10
+ *
11
+ * Ölçek: `textStyles.fontSize`, `components.*` uzunlukları ve `layout` px'tir; `effects.opacity`
12
+ * Figma'nın 0–1'idir (tema 0–100 ister, çeviri eşlemede — g-02). `variables` `get_variable_defs`
13
+ * çıktısının KENDİSİDİR: ad yeniden yazılmaz, değer olduğu gibi taşınır; eşleme adı okur.
14
+ */
15
+ const scalar = z.union([z.string(), z.number()]);
16
+ export const TEXT_STYLE_ROLES = ['h1', 'h2', 'h3', 'h4', 'label', 'p', 'small'];
17
+ export const SCREEN_ROLES = ['shell', 'list', 'form', 'detail', 'auth', 'other'];
18
+ /** `hints` değeri: bu Figma adı eşlemeye HİÇ girmesin (bilinçli karar; `unmapped` sebebi olarak döner). */
19
+ export const SNAPSHOT_HINT_IGNORE = 'ignore';
20
+ export const designSnapshotInput = z.object({
21
+ source: z.object({
22
+ kind: z.enum(['figma', 'manual']),
23
+ fileKey: z.string().optional(),
24
+ fileName: z.string().optional(),
25
+ nodeIds: z.array(z.string()).optional(),
26
+ fetchedAt: z.string().optional(),
27
+ calls: z.number().int().optional(),
28
+ characters: z.number().int().optional()
29
+ }).strict(),
30
+ variables: z.record(scalar),
31
+ variablesDark: z.record(scalar).optional(),
32
+ textStyles: z.array(z.object({
33
+ name: z.string(),
34
+ role: z.enum(TEXT_STYLE_ROLES).optional(),
35
+ fontFamily: z.string(),
36
+ fontSize: z.number(),
37
+ fontWeight: scalar.optional(),
38
+ lineHeight: z.number().optional()
39
+ }).strict()).optional(),
40
+ effects: z.array(z.object({
41
+ name: z.string(),
42
+ offsetX: z.number(),
43
+ offsetY: z.number(),
44
+ blur: z.number(),
45
+ spread: z.number(),
46
+ color: z.string(),
47
+ opacity: z.number()
48
+ }).strict()).optional(),
49
+ layout: z.object({
50
+ desktopWidth: z.number().optional(),
51
+ mobileWidth: z.number().optional(),
52
+ gutter: z.number().optional(),
53
+ sidebarWidth: z.number().optional(),
54
+ notes: z.array(z.string()).optional()
55
+ }).strict().optional(),
56
+ components: z.record(z.object({
57
+ background: z.string().optional(),
58
+ color: z.string().optional(),
59
+ border: z.string().optional(),
60
+ borderRadius: scalar.optional(),
61
+ paddingX: z.number().optional(),
62
+ paddingY: z.number().optional(),
63
+ height: z.number().optional(),
64
+ fontSize: z.number().optional(),
65
+ fontWeight: scalar.optional(),
66
+ shadow: z.string().optional()
67
+ }).strict()).optional(),
68
+ screens: z.array(z.object({
69
+ nodeId: z.string(),
70
+ name: z.string(),
71
+ role: z.enum(SCREEN_ROLES),
72
+ screenshot: z.string().optional(),
73
+ notes: z.array(z.string()).optional()
74
+ }).strict()).optional(),
75
+ /** Figma adı → tema yolu (`accent.muted`, `colors.blue.500`) ya da `ignore`; ajanın KALICI kararı. */
76
+ hints: z.record(z.string()).optional()
77
+ }).strict();
@@ -1,5 +1,25 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  const ERROR_STEP_NAME = 'gatewayOnError';
3
+ const ERROR_STEP_NAME_PATTERN = /^gatewayOnError\d*$/;
4
+ /**
5
+ * Global-error dalında `error` = `WorkflowError.ToObject()`: `{ code, message, detail, type,
6
+ * step: { id, name, type, data, input } }` (`ActivityContext.EnterGlobalErrorHandling`). Gövde
7
+ * `step`'i ALT KÜME olarak yazar, çünkü (1) `ResponseStep` global-error kipinde gövdedeki `step`
8
+ * `error.step` ile birebir aynıysa onu siler, (2) `step.input` adımın ifade değerlerini (parola
9
+ * dahil) taşır. Zincir önce `error.cause`'tan okunur: backend 2026-09-17'den beri her seviyeyi
10
+ * `{ code, message, detail, type, step: { name, type }, cause }` olarak orada verir — loop iterasyonu
11
+ * (`<loop>[<sıra>]`) ve istisnasız 2xx dışı dönen çocuk dahil (`WorkflowError.ToObject`). Eski backend'de
12
+ * `error.cause` yoktur; o zaman yalnız patlayan `workflow` adımının `step.data`'sı okunur: fırlatan
13
+ * çocukta `WorkflowError.ToResponseObject()`, 2xx dışı dönen çocukta çocuğun KENDİ yanıtı. İkincisi
14
+ * keyfî olabilir (elle yazılmış `({ err: error })` input taşır), bu yüzden `pick` her iki kaynağı seviye
15
+ * seviye beyaz listeye süzer: `code/message/detail/type/step{name,type}/cause`. Hiçbiri yoksa `cause`
16
+ * yazılmaz. Loop adımının `data`'sı tamamlanan öğelerdir — okunmaz. Canlı ölçüm 2026-09-16 (lokal `emrah/web`, üç seviyeli
17
+ * zincir + `/m/`). ES5: motor Jint 4 ama desteklenen yüzey belgesiz (RFQ W-4); null alan yazılmaz.
18
+ */
19
+ export const GATEWAY_ERROR_BODY_EXPRESSION = "(function () { function pick(source, depth) { if (!source || typeof source !== 'object') return null; var picked = {}; if (source.code) picked.code = source.code; if (source.message) picked.message = source.message; if (source.detail) picked.detail = source.detail; if (source.type) picked.type = source.type; if (source.step && source.step.name) { picked.step = { name: source.step.name }; if (source.step.type) picked.step.type = source.step.type; } if (depth < 8 && source.cause) { var inner = pick(source.cause, depth + 1); if (inner) picked.cause = inner; } return picked.code || picked.message || picked.step || picked.cause ? picked : null; } var body = { code: error.code, message: error.message }; if (error.detail) body.detail = error.detail; if (error.type) body.type = error.type; if (error.step) body.step = { name: error.step.name, type: error.step.type }; var cause = pick(error.cause, 0) || (error.step && error.step.type === 'workflow' ? pick(error.step.data, 0) : null); if (cause) body.cause = cause; return body; })()";
20
+ export const LEGACY_GATEWAY_ERROR_BODY_EXPRESSION = '({ code: error.code, message: error.message })';
21
+ const ERROR_STEP_DESCRIPTION = "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message, detail, type, step: { name, type } } döner; patlayan adım bir alt akışsa çocuğun hata gövdesi cause içinde iç içe gelir. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.";
22
+ const LEGACY_ERROR_STEP_DESCRIPTION = "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message } döner. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.";
3
23
  const INJECTED_KINDS = new Set(['workflow', 'middleware']);
4
24
  function asSteps(value) {
5
25
  return (Array.isArray(value) ? value : []).filter((step) => Boolean(step) && typeof step === 'object');
@@ -35,8 +55,9 @@ export function ensureWorkflowErrorPath(definition, kind) {
35
55
  if (steps.some(step => step !== startStep && (Array.isArray(step.branches) ? step.branches : []).some(branch => branch?.type === 'global-error')))
36
56
  return [];
37
57
  const branches = Array.isArray(startStep.branches) ? startStep.branches : [];
38
- if (branches.some(branch => branch?.type === 'global-error' && branch.nextStepId))
39
- return [];
58
+ const wired = branches.find(branch => branch?.type === 'global-error' && branch.nextStepId);
59
+ if (wired)
60
+ return upgradeLegacyErrorBody(steps, String(wired.nextStepId));
40
61
  const name = availableName(steps);
41
62
  const errorStepId = randomUUID();
42
63
  const unwired = branches.find(branch => branch?.type === 'global-error');
@@ -44,12 +65,12 @@ export function ensureWorkflowErrorPath(definition, kind) {
44
65
  id: errorStepId,
45
66
  name,
46
67
  type: 'response',
47
- description: "Gateway'in eklediği hata yolu: akışın herhangi bir adımı patlarsa buraya düşer ve 500 ile { code, message } döner. Kendi hata gövdeni istiyorsan bu adımın bodyExpression'ını değiştir.",
68
+ description: ERROR_STEP_DESCRIPTION,
48
69
  caution: "Bu adımı gateway ekledi; silersen akışın hata yolu kaybolur ve patlayan adım istemciye ham hata döndürür.",
49
70
  rule: {
50
71
  stepType: 'response',
51
72
  httpStatusCode: 500,
52
- bodyExpression: '({ code: error.code, message: error.message })'
73
+ bodyExpression: GATEWAY_ERROR_BODY_EXPRESSION
53
74
  },
54
75
  branches: []
55
76
  });
@@ -60,5 +81,27 @@ export function ensureWorkflowErrorPath(definition, kind) {
60
81
  branches.push({ id: randomUUID(), name: 'global-error', type: 'global-error', nextStepId: errorStepId });
61
82
  }
62
83
  startStep.branches = branches;
63
- return [`hata yolu eklendi: start adımına 'global-error' dalı + '${name}' response adımı (500, { code, message }). Hata yolu olmayan akış patladığında platform WorkflowStepFailedException fırlatır (HTTP 500 { code, message, detail, type }; zamanlanmış işte failed + "Step 'x' failed: …"); bu adım çıktıyı { code, message } ile denetim altına alır, zamanlanmış işte sonuç yine failed düşer. Kendi hata gövdenizi istiyorsanız bu adımın bodyExpression'ını değiştirin.`];
84
+ return [`hata yolu eklendi: start adımına 'global-error' dalı + '${name}' response adımı (500, { code, message, detail, type, step: { name, type }, cause }). Hata yolu olmayan akış patladığında platform WorkflowStepFailedException fırlatır (HTTP 500 { code, message, detail, type }, 2026-09-17 backend'inde step ve cause da; zamanlanmış işte failed + "Step 'x' failed: …"); bu adım aynı şekli ele alınmış 500 olarak döner — patlayan adımın adı ve tipi, alt akış ya da loop iterasyonu zinciri cause içinde — ve zamanlanmış işte sonuç yine failed düşer. Kendi hata gövdenizi istiyorsanız bu adımın bodyExpression'ını değiştirin.`];
85
+ }
86
+ /**
87
+ * Bu dosyanın 2026-09-16 öncesi sürümü hata adımına `{ code, message }` yazıyordu; o kayıtlar
88
+ * `detail`, `type` ve patlayan adımı dışarı vermez. Yalnız gateway'in KENDİ yazdığı ve kimsenin
89
+ * dokunmadığı adım yükseltilir: global-error dalının hedefi, adı `gatewayOnError[n]`, tipi
90
+ * `response`, ifadesi eski varsayılanla birebir aynı VE açıklaması eski gateway metni (ya da boş).
91
+ * Açıklama şartı, yükseltilmiş bir adımı aynı çağrıda bilerek eski gövdeye döndüren kullanıcıyı
92
+ * korur: yükseltme açıklamayı da yenilediği için geri dönüş bir daha ezilmez.
93
+ */
94
+ function upgradeLegacyErrorBody(steps, errorStepId) {
95
+ const target = steps.find(step => String(step.id ?? '') === errorStepId);
96
+ const rule = target?.rule;
97
+ if (!target || target.type !== 'response' || !ERROR_STEP_NAME_PATTERN.test(String(target.name ?? '')) || !rule)
98
+ return [];
99
+ if (typeof rule.bodyExpression !== 'string' || rule.bodyExpression.trim() !== LEGACY_GATEWAY_ERROR_BODY_EXPRESSION)
100
+ return [];
101
+ const untouchedDescription = target.description === undefined || target.description === null || String(target.description).trim() === '' || target.description === LEGACY_ERROR_STEP_DESCRIPTION;
102
+ if (!untouchedDescription)
103
+ return [];
104
+ rule.bodyExpression = GATEWAY_ERROR_BODY_EXPRESSION;
105
+ target.description = ERROR_STEP_DESCRIPTION;
106
+ return [`hata gövdesi genişletildi: '${String(target.name)}' adımının bodyExpression'ı gateway'in eski varsayılanıydı ({ code, message }); yeni varsayılan detail, type, patlayan adımın { name, type } bilgisini ve adım bir alt akışsa çocuğun hata gövdesini cause olarak da döner. Çocuk gövdesi yalnız code/message/detail/type/step{name,type}/cause alanlarına süzülür. Yanıta alan EKLENİR, mevcut code/message değişmez. Açıklaması ya da ifadesi elle değiştirilmiş hata adımına dokunulmaz; eski gövdede kalmak için bu adımın açıklamasını değiştirin.`];
64
107
  }
@@ -1905,6 +1905,53 @@ export function inertFlexPropWarning(entries) {
1905
1905
  + "sessizce yok sayılır (istek reddedilmez, prop katalogda vardır). Akış düzeni istiyorsan aynı düğüme "
1906
1906
  + "layout:'flex' de yaz, istemiyorsan bu prop'ları kaldır — platform_guide('page') Kural 1f-2."];
1907
1907
  }
1908
+ /**
1909
+ * Akış id'si → ona bağlı olaylar. Bağ üç yerde durur: düğümün `events[].value`'su, düğüm
1910
+ * prop'larındaki `type:'flow'` referansları (`event` alanı `prop:<ad>` olur) ve barındırıcının
1911
+ * düzey olayları (`target: 'page'` — `remove_event`'in aynı hedef adı). Akış tarafından bu
1912
+ * listeyi veren bir uç yok; 2026-09-16 raporunda uygulama kabuğunda hangi olayın hangi akışa
1913
+ * bağlı olduğu kaybolmuş, ikinci "zil rozeti" akışı sahipsiz kalmıştı.
1914
+ */
1915
+ export function flowBindings(schema, hostEvents) {
1916
+ const bindings = new Map();
1917
+ const add = (flowId, binding) => {
1918
+ if (flowId === '')
1919
+ return;
1920
+ bindings.set(flowId.toLowerCase(), [...(bindings.get(flowId.toLowerCase()) ?? []), binding]);
1921
+ };
1922
+ for (const event of asObjects(hostEvents)) {
1923
+ if (typeof event.value === 'string')
1924
+ add(event.value, { target: 'page', event: String(event.name ?? '') });
1925
+ }
1926
+ const visitProp = (target, propName, value) => {
1927
+ if (Array.isArray(value)) {
1928
+ for (const entry of value)
1929
+ visitProp(target, propName, entry);
1930
+ return;
1931
+ }
1932
+ if (!value || typeof value !== 'object')
1933
+ return;
1934
+ const record = value;
1935
+ if (record.type === 'flow' && typeof record.ref === 'string')
1936
+ add(record.ref, { target, event: `prop:${propName}` });
1937
+ for (const entry of Object.values(record))
1938
+ visitProp(target, propName, entry);
1939
+ };
1940
+ const visitNode = (node) => {
1941
+ const target = String(node.elementId ?? node.name ?? '');
1942
+ for (const event of asObjects(node.events)) {
1943
+ if (typeof event.value === 'string')
1944
+ add(event.value, { target, event: String(event.name ?? '') });
1945
+ }
1946
+ for (const [propName, value] of Object.entries((node.props ?? {})))
1947
+ visitProp(target, propName, value);
1948
+ for (const child of asObjects(node.children))
1949
+ visitNode(child);
1950
+ };
1951
+ for (const node of asObjects(schema))
1952
+ visitNode(node);
1953
+ return bindings;
1954
+ }
1908
1955
  /**
1909
1956
  * Bir yamanın SİLECEĞİ akışlar. Platform referanssız kalan akışı KALICI siler; uyarı yamadan sonra
1910
1957
  * geldiğinde "önce yedekle" tavsiyesi uygulanamaz hâle geliyordu (09-05 koşusu: sekiz adımlı giriş
@@ -0,0 +1,151 @@
1
+ import { readKnowledgeFile } from '../resources/knowledge.js';
2
+ import { PlatformApiError } from '../platform/errors.js';
3
+ import { discoverTheme } from './themeDiscover.js';
4
+ const ARCHETYPES = {
5
+ form: { example: 'page-form-upsert', flows: { mount: 'mountFlowCreateArgs', submit: 'submitFlowCreateArgs' } },
6
+ detail: { example: 'page-detail-profile', flows: { mount: 'mountFlowCreateArgs' } },
7
+ auth: { example: 'page-sign-in', flows: { submit: 'submitFlowCreateArgs' } }
8
+ };
9
+ const LIST_ARCHETYPE = 'list (kod içi iskelet) + flow-list-to-form';
10
+ const SHELL_EXAMPLE = 'layout-app-shell';
11
+ const LIST_EVENTS_EXAMPLE = 'flow-list-to-form';
12
+ const turkishMap = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
13
+ export function slugifyScreenName(name) {
14
+ return name.toLocaleLowerCase('tr')
15
+ .replace(/[çğıöşüâîû]/g, letter => turkishMap[letter] ?? letter)
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ }
19
+ const readExample = (name) => JSON.parse(readKnowledgeFile('examples', `${name}.json`));
20
+ const isPlaceholder = (value) => typeof value === 'string' && value.startsWith('<');
21
+ const staticProp = (data) => ({ type: 'static', data });
22
+ const node = (elementId, type, props, wrapper, block) => ({
23
+ elementId,
24
+ kind: 'component',
25
+ type,
26
+ name: elementId,
27
+ props,
28
+ states: { isSticky: false, isDroppable: wrapper, isScrollable: false, isWrapper: wrapper },
29
+ blocks: [{ breakpoint: 1024, size: { h: block.h, w: block.w }, position: { x: block.x, y: block.y } }],
30
+ children: [],
31
+ events: [],
32
+ validations: [],
33
+ stateBindings: []
34
+ });
35
+ /** Liste sayfası asgari iskeleti: araç çubuğu (Yeni) + kayıt tablosu; adlar `flow-list-to-form` ile aynı. */
36
+ function listSkeleton() {
37
+ return [
38
+ { op: 'add_component', parent: 'page_container', component: node('list_toolbar', 'container', { layout: staticProp('flex'), direction: staticProp('row') }, true, { h: 4, w: 12, x: 0, y: 0 }) },
39
+ { op: 'add_component', parent: 'list_toolbar', component: node('btn_new', 'button', { text: staticProp('Yeni') }, false, { h: 4, w: 3, x: 0, y: 0 }) },
40
+ { op: 'add_component', parent: 'page_container', component: node('records_table', 'table', {}, false, { h: 40, w: 12, x: 0, y: 4 }) }
41
+ ];
42
+ }
43
+ function pageArgs(screenName, base, options, caveats) {
44
+ const args = { ...base, name: screenName, slug: slugifyScreenName(screenName), browserTitle: screenName };
45
+ if (options.layoutId !== undefined) {
46
+ args.layoutId = options.layoutId;
47
+ }
48
+ else {
49
+ if (isPlaceholder(args.layoutId) || args.layoutId === undefined)
50
+ delete args.layoutId;
51
+ caveats.push("layoutId verilmedi — sayfa kabuksuz açılır; kabuk için 'shell' ekranını page_discover ile layout_create'e çevirip (ya da layout_list ile mevcut kabuğu bulup) pageCreateArgs.layoutId verin");
52
+ }
53
+ if (options.middlewareIds !== undefined) {
54
+ args.middlewareIds = options.middlewareIds;
55
+ }
56
+ else if (Array.isArray(args.middlewareIds) && args.middlewareIds.some(isPlaceholder)) {
57
+ delete args.middlewareIds;
58
+ caveats.push('middlewareIds verilmedi — örnek korumalı sayfa istiyor; auth middleware id için middleware_list, sonra pageCreateArgs.middlewareIds (bir-iki ucu bağlayıp gerisini açık bırakmak yasak)');
59
+ }
60
+ return args;
61
+ }
62
+ /** Tek aday preset bağlanır (`props.style = { $preset }`); birden çok aday karar ister (K4). */
63
+ function bindPresets(ops, presets, caveats) {
64
+ const bindings = [];
65
+ for (const op of ops) {
66
+ if (op.op !== 'add_component')
67
+ continue;
68
+ const component = op.component;
69
+ const type = typeof component?.type === 'string' ? component.type : undefined;
70
+ const elementId = typeof component?.elementId === 'string' ? component.elementId : undefined;
71
+ if (component === undefined || type === undefined || elementId === undefined)
72
+ continue;
73
+ const candidates = presets.filter(preset => preset === type || preset.startsWith(`${type}-`));
74
+ if (candidates.length === 1) {
75
+ component.props = { ...(component.props ?? {}), style: { $preset: candidates[0] } };
76
+ bindings.push({ elementId, type, preset: candidates[0] });
77
+ }
78
+ else if (candidates.length > 1) {
79
+ caveats.push(`${elementId} (${type}) için birden çok preset adayı var: ${candidates.join(', ')} — hangisi olacağına siz karar verin (page_schema_patch replace_prop style { $preset })`);
80
+ }
81
+ }
82
+ return bindings;
83
+ }
84
+ export function discoverPage(snapshot, screenRef, options = {}) {
85
+ const screens = snapshot.screens ?? [];
86
+ const screen = screens.find(entry => entry.nodeId === screenRef) ?? screens.find(entry => entry.name === screenRef);
87
+ if (screen === undefined) {
88
+ throw new PlatformApiError(`page_discover: '${screenRef}' anlık görüntünün screens listesinde yok (nodeId ya da name). Mevcut ekranlar: ${screens.map(entry => `${entry.name} (${entry.nodeId}, ${entry.role})`).join(' · ') || 'yok'}.`, 400);
89
+ }
90
+ const caveats = [];
91
+ const notes = [...(snapshot.layout?.notes ?? []), ...(screen.notes ?? [])];
92
+ const head = { screen: { nodeId: screen.nodeId, name: screen.name, role: screen.role }, notes };
93
+ const presets = Object.keys(discoverTheme(snapshot).theme.presets ?? {});
94
+ if (screen.role === 'shell') {
95
+ const example = readExample(SHELL_EXAMPLE);
96
+ return {
97
+ ...head,
98
+ archetype: SHELL_EXAMPLE,
99
+ layoutCreateArgs: { ...example.layoutCreate, name: screen.name },
100
+ schemaPatchOps: [],
101
+ flows: {},
102
+ presetBindings: [],
103
+ caveats: ['kabuk bir SAYFA değil LAYOUT kaydıdır; yan menü/üst şerit içeriğini ekran notlarına göre layout_schema_patch ile doldurun'],
104
+ next: 'layout_create(layoutCreateArgs) → layout_list ile id → diğer ekranların pageCreateArgs.layoutId'
105
+ };
106
+ }
107
+ if (screen.role === 'other') {
108
+ return {
109
+ ...head,
110
+ archetype: 'yok',
111
+ pageCreateArgs: pageArgs(screen.name, {}, options, caveats),
112
+ schemaPatchOps: [],
113
+ flows: {},
114
+ presetBindings: [],
115
+ caveats: [...caveats, "rol 'other' — arketip yok; ekran notlarına göre page_schema_patch ile elle kurun (platform_guide('page') §arketipler) ya da anlık görüntüde role list/form/detail/auth verin"],
116
+ next: "page_create(pageCreateArgs) → page_schema_patch(ops — elle) → page_screenshot"
117
+ };
118
+ }
119
+ if (screen.role === 'list') {
120
+ const events = readExample(LIST_EVENTS_EXAMPLE);
121
+ const ops = [...listSkeleton(), ...(events.schemaPatchOps ?? [])];
122
+ const presetBindings = bindPresets(ops, presets, caveats);
123
+ caveats.push("records_table props boş — kolonları ve veri kaynağını component_get('table') + flow-load-table örneğine göre verin; set_event op'larındaki '<… flow id>' yer tutucuları flow_create sonrası gerçek id ile değiştirilir");
124
+ return {
125
+ ...head,
126
+ archetype: LIST_ARCHETYPE,
127
+ pageCreateArgs: pageArgs(screen.name, {}, options, caveats),
128
+ schemaPatchOps: ops,
129
+ flows: { openRecord: events.openRecordFlowCreateArgs, newRecord: events.newRecordFlowCreateArgs },
130
+ presetBindings,
131
+ caveats,
132
+ next: 'page_create(pageCreateArgs) → flow_create(flows.openRecord, flows.newRecord) → set_event yer tutucularını flow id ile değiştir → page_schema_patch(schemaPatchOps) → page_screenshot'
133
+ };
134
+ }
135
+ const archetype = ARCHETYPES[screen.role];
136
+ const example = readExample(archetype.example);
137
+ const ops = structuredClone(example.schemaPatchOps ?? []);
138
+ const presetBindings = bindPresets(ops, presets, caveats);
139
+ const flows = Object.fromEntries(Object.entries(archetype.flows).map(([key, field]) => [key, example[field]]).filter(([, value]) => value !== undefined));
140
+ caveats.push(`iskelet '${archetype.example}' altın örneğinden — elementId'ler ve metinler örneğin domainindedir (${String(example.pageCreateArgs?.name ?? '')}); ekran notlarına göre alan adlarını, etiketleri ve akış adımlarını değiştirin`);
141
+ return {
142
+ ...head,
143
+ archetype: archetype.example,
144
+ pageCreateArgs: pageArgs(screen.name, example.pageCreateArgs ?? {}, options, caveats),
145
+ schemaPatchOps: ops,
146
+ flows,
147
+ presetBindings,
148
+ caveats,
149
+ next: 'page_create(pageCreateArgs) → page_schema_patch(pageId, schemaPatchOps) → flow_create(flows.*) ve set_event yer tutucularını flow id ile değiştir → page_screenshot'
150
+ };
151
+ }
@@ -47,6 +47,47 @@ function hasText(value) {
47
47
  function stepLabel(step) {
48
48
  return step.name || step.type || step.id || '?';
49
49
  }
50
+ const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
51
+ /**
52
+ * `workflow` adımının (ve `loop` içindeki `stepType: 'workflow'` çocuğunun) alt akışı çağıran
53
+ * kuralı. `WorkflowStep.cs` yalnız `rule.WorkflowId` + `rule.Expression` okur; girdiyi başka
54
+ * bir alanla göndermeye çalışan gövde ya kaydedilip okunmaz (şema üyeleri) ya da hiç
55
+ * kaydedilmez (şema dışı adlar, `unmappedMembers`) — iki durumda da alt akış SESSİZCE girdisiz
56
+ * koşar (RFQ koşusu W-7, 2026-09-14). Platform her adım kuralını `fields: []`,
57
+ * `customParameters: null` gibi boş varsayılanlarla geri okuttuğu için yalnız DOLU değer sayılır;
58
+ * aksi hâlde `workflow_get` gövdesiyle yapılan her `workflow_patch`/`update`/`preview` kendi
59
+ * kendine bloklanırdı.
60
+ */
61
+ const WORKFLOW_STEP_SAVED_BUT_UNREAD_KEYS = ['payloadExpression', 'customParameters', 'queryParameters', 'bodyExpression', 'fields'];
62
+ const WORKFLOW_STEP_UNMAPPED_INPUT_KEYS = ['parameters', 'parameter', 'input', 'payload', 'arguments', 'args', 'variables'];
63
+ function carriesValue(value) {
64
+ if (value === undefined || value === null)
65
+ return false;
66
+ if (Array.isArray(value))
67
+ return value.length > 0;
68
+ if (typeof value === 'string')
69
+ return value.trim() !== '';
70
+ return true;
71
+ }
72
+ function workflowCallSites(body) {
73
+ return workflowSteps(body).flatMap((step) => {
74
+ const rule = (step.rule ?? {});
75
+ if (step.type === 'workflow')
76
+ return [{ step, rule, label: stepLabel(step) }];
77
+ if (step.type === 'loop' && rule.stepType === 'workflow' && rule.stepRule && typeof rule.stepRule === 'object') {
78
+ return [{ step, rule: rule.stepRule, label: `${stepLabel(step)} (loop çocuğu)` }];
79
+ }
80
+ return [];
81
+ });
82
+ }
83
+ function literalShapeOf(expression) {
84
+ const head = expression.trim().replace(/^[(\s]+/, '');
85
+ if (head.startsWith('{'))
86
+ return 'object';
87
+ if (head.startsWith('['))
88
+ return 'array';
89
+ return undefined;
90
+ }
50
91
  /**
51
92
  * Start'tan başlayıp `global-error` dalları HARİÇ tüm dalları izleyerek erişilen adımlar.
52
93
  * Gateway her `workflow`/`middleware` tanımına global-error dalının ucunda bir `response` adımı
@@ -698,6 +739,45 @@ export const policyRules = [
698
739
  });
699
740
  }
700
741
  },
742
+ {
743
+ id: 'WF-032',
744
+ module: 'workflow',
745
+ appliesTo: ['create', 'update', 'preview'],
746
+ sourceRef: "WorkflowStep.cs EvaluateAsync: kural yalnız rule.WorkflowId ve rule.Expression okur; girdi workflow.HasParameter iken Expression'ın DataTransferObjectExpression.Eval'iyle bağlanır, başka hiçbir alan okunmaz. Studio WorkflowTrigger.svelte de yalnız bu iki alanı yazar ve expression'ı çağrılan akışın parameterScheme'inden nesne literali olarak doldurur. RFQ koşusu W-7 (2026-09-14): payloadExpression/customParameters kaydedildi, alt akış girdisiz koştu",
747
+ check: (definition, context) => workflowCallSites(workflowBody(definition, context.workflowKind)).flatMap(({ label, rule }) => {
748
+ const workflowId = rule.workflowId;
749
+ if (!hasText(workflowId)) {
750
+ return [{
751
+ ruleId: 'WF-032',
752
+ severity: 'block',
753
+ message: `'${label}' workflow adımında rule.workflowId yok — adım hangi akışı çağıracağını bilmez. workflow_list(applicationId, type: 'workflow') ile id seçin; girdi rule.expression'a nesne literali olarak yazılır (çağrılan akışın parametre DTO'suna göre).`,
754
+ guide: 'ngapps://guides/workflow'
755
+ }];
756
+ }
757
+ if (!GUID_PATTERN.test(String(workflowId))) {
758
+ return [{
759
+ ruleId: 'WF-032',
760
+ severity: 'block',
761
+ message: `'${label}' adımının rule.workflowId'si GUID değil: '${String(workflowId)}'. Platform alanı Guid'e çeviremediği için 400 döner; slug değil id verin (workflow_list).`,
762
+ guide: 'ngapps://guides/workflow'
763
+ }];
764
+ }
765
+ const savedButUnread = WORKFLOW_STEP_SAVED_BUT_UNREAD_KEYS.filter(key => carriesValue(rule[key]));
766
+ const unmapped = WORKFLOW_STEP_UNMAPPED_INPUT_KEYS.filter(key => carriesValue(rule[key]));
767
+ if (savedButUnread.length === 0 && unmapped.length === 0)
768
+ return [];
769
+ const fate = [
770
+ savedButUnread.length > 0 ? `${savedButUnread.map(key => `rule.${key}`).join(', ')} kaydedilir ama WorkflowStep okumaz` : '',
771
+ unmapped.length > 0 ? `${unmapped.map(key => `rule.${key}`).join(', ')} StepRuleDto'da yok, platform hiç kaydetmez (unmappedMembers)` : ''
772
+ ].filter(part => part !== '').join('; ');
773
+ return [{
774
+ ruleId: 'WF-032',
775
+ severity: 'block',
776
+ message: `'${label}' adımı alt akışa girdiyi yanlış alandan gönderiyor — ${fate}. WorkflowStep yalnız rule.workflowId ve rule.expression okur: iki durumda da alt akış boş girdiyle koşar ve 200 döner (RFQ koşusu W-7). Eşlemeyi rule.expression'a nesne literali olarak yazın — her anahtar çağrılan akışın parametre DTO'sundaki bir alan, her değer bir JS ifadesi: "({ title: input.title, source: 'telemetry' })". Aynı adlı alan code adımında GÖVDE, workflow/microfunction adımında GİRDİDİR. Alan listesini workflow_step_schema (parameterScheme) ya da workflow_get(<workflowId>).parameterDtoId → dto_get verir.`,
777
+ guide: 'ngapps://guides/workflow'
778
+ }];
779
+ })
780
+ },
701
781
  {
702
782
  id: 'WF-006',
703
783
  module: 'workflow',
@@ -2873,6 +2953,89 @@ export async function microfunctionStepFindings(platform, project, environment,
2873
2953
  }
2874
2954
  return applyPolicyMode(platform.config, findings, policyOverride);
2875
2955
  }
2956
+ /**
2957
+ * WF-032'nin ikinci yarısı. `rule.expression`'ın işe yarayıp yaramayacağı gövdeden görülemez:
2958
+ * `WorkflowStep` ifadeyi YALNIZ çağrılan akışın parametre DTO'su varken değerlendirir
2959
+ * (`workflow.HasParameter`); DTO'suz alt akışa yazılan ifade hiç koşmaz ve girdi boş gider.
2960
+ * Karar çağrılan akışın kaydında yaşar — gateway `workflow_get` ucunu okur ve bulguyu aynı
2961
+ * `policyFindings` kanalına koyar; `policyOverride` / `POLICY=warn` kaçışları aynen geçerlidir.
2962
+ *
2963
+ * Okuma başarısız olursa kural SUSAR (WF-MF-001 ile aynı gerekçe). Uygulama kimliği ne
2964
+ * parametrede ne gövdede varsa (automation yolu `entityId` ile çalışır) da susar: statik yarı
2965
+ * yine koşar, canlı yarı bir kolaylıktır.
2966
+ */
2967
+ export async function workflowStepFindings(platform, project, environment, applicationId, definition, kind, policyOverride = false) {
2968
+ if (platform.config.policyMode === 'off')
2969
+ return [];
2970
+ const application = hasText(applicationId)
2971
+ ? String(applicationId)
2972
+ : hasText(definition.applicationId) ? String(definition.applicationId) : undefined;
2973
+ if (application === undefined)
2974
+ return [];
2975
+ const sites = workflowCallSites(workflowBody(definition, kind))
2976
+ .filter(site => GUID_PATTERN.test(String(site.rule.workflowId ?? '')));
2977
+ if (sites.length === 0)
2978
+ return [];
2979
+ const guide = 'ngapps://guides/workflow';
2980
+ const records = new Map();
2981
+ const findings = [];
2982
+ for (const { label, rule } of sites) {
2983
+ const workflowId = String(rule.workflowId);
2984
+ if (!records.has(workflowId)) {
2985
+ try {
2986
+ const payload = await platform.tenant('GET', project, `/workflows/${application}/${workflowId}`, { environment });
2987
+ records.set(workflowId, (payload?.data ?? payload));
2988
+ }
2989
+ catch (error) {
2990
+ records.set(workflowId, error instanceof PlatformApiError && error.status === 404 ? 'missing' : 'unreadable');
2991
+ }
2992
+ }
2993
+ const record = records.get(workflowId);
2994
+ if (record === undefined || record === 'unreadable')
2995
+ continue;
2996
+ if (record === 'missing') {
2997
+ findings.push({
2998
+ ruleId: 'WF-032',
2999
+ severity: 'block',
3000
+ message: `'${label}' adımının workflowId'si (${workflowId}) bu ortamda YOK (platform kaydı yalnız id ile arar, uygulama kimliğine bakmaz). Adım kuralı step_rules.workflow_id ile kayda FK'lıdır: gövde gitseydi platform 400 dönerdi. workflow_list(applicationId: '${application}', type: 'workflow') ile bu ortamdaki akışları görün — alt akış başka bir dalda ya da ortamda kalmış, ya da henüz yaratılmamış olabilir.`,
3001
+ guide
3002
+ });
3003
+ continue;
3004
+ }
3005
+ const calleeName = String(record.name ?? record.slug ?? workflowId);
3006
+ const parameterDtoId = hasText(record.parameterDtoId) ? String(record.parameterDtoId) : undefined;
3007
+ const expression = hasText(rule.expression) ? String(rule.expression) : '';
3008
+ if (expression !== '' && parameterDtoId === undefined) {
3009
+ findings.push({
3010
+ ruleId: 'WF-032',
3011
+ severity: 'block',
3012
+ message: `'${label}' adımı '${calleeName}' akışına rule.expression ile girdi geçiriyor ama o akışın parametre DTO'su YOK (parameterDtoId boş). WorkflowStep ifadeyi yalnız workflow.HasParameter iken değerlendirir: bu hâliyle ifade HİÇ çalışmaz, alt akış boş girdiyle koşar ve 200 döner. Önce alt akışa girdi sözleşmesi verin — dto_create ile alan listesini tanımlayın, sonra workflow_patch(id: '${workflowId}', ops: [{ op: 'add', path: '/parameterDtoId', value: '<dtoId>' }, { op: 'add', path: '/parameterBehavior', value: 'object' }]); alt akışın adımları girdiyi input.<alan> ile okur.`,
3013
+ guide
3014
+ });
3015
+ continue;
3016
+ }
3017
+ if (expression === '' && parameterDtoId !== undefined) {
3018
+ findings.push({
3019
+ ruleId: 'WF-032',
3020
+ severity: 'warn',
3021
+ message: `'${label}' adımında rule.expression boş ama '${calleeName}' akışının parametre DTO'su var (parameterDtoId: ${parameterDtoId}). Alt akış input olarak boş nesne alır, kendi input.<alan> okumaları undefined döner. Girdiyi nesne literaliyle geçirin — alan listesini workflow_step_schema çıktısındaki parameterScheme ya da dto_get('${parameterDtoId}') verir.`,
3022
+ guide
3023
+ });
3024
+ continue;
3025
+ }
3026
+ const behavior = String(record.parameterBehavior ?? '').toLowerCase();
3027
+ const literal = literalShapeOf(expression);
3028
+ if (literal !== undefined && ((behavior === 'array' && literal === 'object') || (behavior === 'object' && literal === 'array'))) {
3029
+ findings.push({
3030
+ ruleId: 'WF-032',
3031
+ severity: 'warn',
3032
+ message: `'${label}' adımının ifadesi ${literal === 'object' ? 'nesne' : 'dizi'} literaliyle başlıyor ama '${calleeName}' akışının parameterBehavior'ı '${behavior}': alt akış input olarak ${behavior === 'array' ? 'DİZİ bekler; nesne alınca öğe okumaları' : 'NESNE bekler; dizi alınca input.<alan> okumaları'} undefined döner. İfadeyi davranışa uydurun ya da alt akışın parameterBehavior'ını değiştirin.`,
3033
+ guide
3034
+ });
3035
+ }
3036
+ }
3037
+ return applyPolicyMode(platform.config, findings, policyOverride);
3038
+ }
2876
3039
  const partialWriteOperations = new Set(['create', 'update', 'upsert']);
2877
3040
  const inputReferencePattern = /\binput\.([A-Za-z_$][\w$]*)/g;
2878
3041
  function inputReferencesOf(rule) {