@appaflytech/zeyrek-ai-mcp 0.0.21 → 0.0.23
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/dist/config.js +2 -0
- package/dist/knowledge/componentCatalog.js +6 -4
- package/dist/knowledge/componentEvents.js +12 -5
- package/dist/knowledge/mobileComponentCatalog.js +628 -0
- package/dist/knowledge/mobileComponentEvents.js +110 -0
- package/dist/knowledge/normalize.js +52 -11
- package/dist/knowledge/platform/lint.js +205 -0
- package/dist/knowledge/platform/mobile.js +78 -0
- package/dist/knowledge/platform/mobileRules.js +126 -0
- package/dist/knowledge/platform/mobileTabs.js +108 -0
- package/dist/knowledge/platform/profile.js +20 -0
- package/dist/knowledge/platform/resolve.js +122 -0
- package/dist/knowledge/platform/web.js +38 -0
- package/dist/knowledge/rules.js +56 -12
- package/dist/knowledge/styleResolve.js +1 -1
- package/dist/knowledge/styleResolveNative.js +410 -0
- package/dist/knowledge/themeResolve.js +3 -3
- package/dist/platform/capture/actions.js +333 -0
- package/dist/platform/capture/browser.js +1 -0
- package/dist/platform/{screenshot.js → capture/capture.js} +23 -509
- package/dist/platform/capture/index.js +10 -0
- package/dist/platform/capture/readiness.js +103 -0
- package/dist/platform/capture/summary.js +77 -0
- package/dist/platform/capture/target.js +5 -0
- package/dist/platform/capture/targets/studio-mobile.js +15 -0
- package/dist/platform/capture/targets/studio-web.js +5 -0
- package/dist/platform/domSummary.js +1 -1
- package/dist/resources/examples/mobile-app-shell-drawer.json +209 -0
- package/dist/resources/examples/mobile-app-shell-stack.json +123 -0
- package/dist/resources/examples/mobile-app-shell-tab.json +174 -0
- package/dist/resources/examples/mobile-list-detail-form.json +656 -0
- package/dist/resources/guides/component.md +6 -1
- package/dist/resources/guides/functions.md +1 -1
- package/dist/resources/guides/layout.md +11 -7
- package/dist/resources/guides/mobile.md +283 -0
- package/dist/resources/guides/page.md +69 -16
- package/dist/resources/guides/translation.md +1 -1
- package/dist/server.js +2 -0
- package/dist/tools/applications.js +60 -3
- package/dist/tools/components.js +50 -10
- package/dist/tools/designSchemaPatch.js +13 -4
- package/dist/tools/externalComponents.js +25 -4
- package/dist/tools/flows.js +9 -2
- package/dist/tools/layouts.js +41 -9
- package/dist/tools/mobile.js +203 -0
- package/dist/tools/pages.js +115 -35
- package/dist/tools/platformInfo.js +2 -1
- package/dist/tools/profileWrite.js +45 -0
- package/dist/tools/screenWrite.js +2 -2
- package/dist/tools/screens.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const MAX_MOBILE_TABS = 5;
|
|
2
|
+
const STYLES = new Set(['default', 'floating', 'custom']);
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Öğenin sayfa id'si: `{ page }` ya da düz metin (`items: ['id']`). */
|
|
10
|
+
function entryPage(entry) {
|
|
11
|
+
const item = record(entry);
|
|
12
|
+
return item ? text(item.page) : text(entry);
|
|
13
|
+
}
|
|
14
|
+
/** `Application.properties.mobile.tabs` — ham değer, doğrulanmamış. */
|
|
15
|
+
export function readMobileTabs(properties) {
|
|
16
|
+
return record(record(properties)?.mobile)?.tabs;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ham sekme ayarını çizilebilir biçime getirir; çizilecek çubuk yoksa `null` (yığın).
|
|
20
|
+
*
|
|
21
|
+
* - Nesne değilse, `enabled !== true` ise ya da geçerli öğe kalmadıysa → `null`.
|
|
22
|
+
* - `style` bilinmiyorsa `default`; `showLabels` boolean değilse `true`.
|
|
23
|
+
* - Öğe `{ page }` ya da düz sayfa id'sidir; sayfası yoksa düşer; `pages` verildiyse orada olmayan
|
|
24
|
+
* sayfa düşer; tekrar eden sayfa (ilki kalır) atılır; `MAX_MOBILE_TABS`'tan sonrası kırpılır.
|
|
25
|
+
* `label`/`icon` boş metinse yazılmaz.
|
|
26
|
+
* - Görünüm alanı yoktur: çubuk temadan çizilir; eski kayıtların `appearance`'ı okunmaz.
|
|
27
|
+
* - `customWidgetId` yalnız `custom` türde ve metinse yazılır.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeMobileTabs(tabs, pages) {
|
|
30
|
+
const source = record(tabs);
|
|
31
|
+
if (!source || source.enabled !== true)
|
|
32
|
+
return null;
|
|
33
|
+
const known = pages ? new Set(pages.map(page => page.id)) : undefined;
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const items = [];
|
|
36
|
+
for (const entry of Array.isArray(source.items) ? source.items : []) {
|
|
37
|
+
const item = record(entry);
|
|
38
|
+
const page = entryPage(entry);
|
|
39
|
+
if (!page || seen.has(page) || (known && !known.has(page)))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(page);
|
|
42
|
+
const label = text(item?.label);
|
|
43
|
+
const icon = text(item?.icon);
|
|
44
|
+
items.push({ page, ...(label ? { label } : {}), ...(icon ? { icon } : {}) });
|
|
45
|
+
if (items.length === MAX_MOBILE_TABS)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (items.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const style = (typeof source.style === 'string' && STYLES.has(source.style) ? source.style : 'default');
|
|
51
|
+
const customWidgetId = style === 'custom' ? text(source.customWidgetId) : undefined;
|
|
52
|
+
return {
|
|
53
|
+
enabled: true,
|
|
54
|
+
style,
|
|
55
|
+
items,
|
|
56
|
+
...(customWidgetId ? { customWidgetId } : {}),
|
|
57
|
+
showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* MOB-007 denetimi: ham ayarın normalizasyonda neyi kaybedeceğini ve tutarsızlıklarını söyler.
|
|
62
|
+
* Yazım yine platforma gider (D3); bulgular yalnız uyarıdır.
|
|
63
|
+
*/
|
|
64
|
+
export function mobileTabsProblems(tabs, context = {}) {
|
|
65
|
+
if (tabs === undefined || tabs === null)
|
|
66
|
+
return [];
|
|
67
|
+
const source = record(tabs);
|
|
68
|
+
if (!source)
|
|
69
|
+
return [`properties.mobile.tabs nesne olmalı (gelen: ${Array.isArray(tabs) ? 'dizi' : typeof tabs}) — sekme çubuğu çizilmez, uygulama yığınla açılır`];
|
|
70
|
+
const problems = [];
|
|
71
|
+
const rawItems = Array.isArray(source.items) ? source.items : undefined;
|
|
72
|
+
if (source.enabled !== undefined && typeof source.enabled !== 'boolean')
|
|
73
|
+
problems.push(`enabled boolean olmalı (gelen: ${JSON.stringify(source.enabled)}) — true olmayan her değer sekmeleri kapatır`);
|
|
74
|
+
if (source.style !== undefined && !(typeof source.style === 'string' && STYLES.has(source.style)))
|
|
75
|
+
problems.push(`style '${String(source.style)}' tanınmıyor (default | floating | custom) — 'default' çizilir`);
|
|
76
|
+
if (source.items !== undefined && !rawItems)
|
|
77
|
+
problems.push('items dizi olmalı — hiçbir sekme çizilmez');
|
|
78
|
+
const items = rawItems ?? [];
|
|
79
|
+
const pageIds = items.map(entryPage);
|
|
80
|
+
if (source.enabled === true && items.length === 0)
|
|
81
|
+
problems.push("enabled:true ama items boş — çubuk çizilmez; en az bir sayfa ekleyin ya da enabled:false yazın");
|
|
82
|
+
if (items.length > MAX_MOBILE_TABS)
|
|
83
|
+
problems.push(`${items.length} sekme var, en çok ${MAX_MOBILE_TABS} çizilir — fazlası kırpılır (${pageIds.slice(MAX_MOBILE_TABS).filter(Boolean).join(', ')})`);
|
|
84
|
+
const missingPage = pageIds.filter(id => !id).length;
|
|
85
|
+
if (missingPage > 0)
|
|
86
|
+
problems.push(`${missingPage} öğede page (sayfa id'si) yok — o öğeler düşer`);
|
|
87
|
+
const duplicates = pageIds.filter((id, index) => id && pageIds.indexOf(id) !== index);
|
|
88
|
+
if (duplicates.length > 0)
|
|
89
|
+
problems.push(`aynı sayfa birden çok sekmede: ${[...new Set(duplicates)].join(', ')} — tekrarlar atılır`);
|
|
90
|
+
if (context.pages) {
|
|
91
|
+
const known = new Set(context.pages.map(page => page.id));
|
|
92
|
+
const unknown = pageIds.filter((id) => Boolean(id) && !known.has(id));
|
|
93
|
+
if (unknown.length > 0)
|
|
94
|
+
problems.push(`var olmayan sayfa: ${[...new Set(unknown)].join(', ')} — page_list ile id'leri doğrulayın; bu öğeler düşer (item.page slug değil sayfa id'sidir)`);
|
|
95
|
+
}
|
|
96
|
+
const normalized = normalizeMobileTabs(tabs, context.pages ?? null);
|
|
97
|
+
if (normalized && context.homepageId && normalized.items[0]?.page !== context.homepageId) {
|
|
98
|
+
problems.push(`açılış sayfası (${context.homepageId}) ilk sekme değil — uygulama açılış sayfasıyla başlar ama çubukta başka sekme seçili görünür; açılış sayfasını items'ın başına alın`);
|
|
99
|
+
}
|
|
100
|
+
if (normalized?.style === 'custom') {
|
|
101
|
+
const widgetId = normalized.customWidgetId;
|
|
102
|
+
if (!widgetId)
|
|
103
|
+
problems.push("style:'custom' ama customWidgetId yok — özel sekme çubuğu widget'ı seçilmeli (component_list kind:'widget')");
|
|
104
|
+
else if (context.widgetIds && !context.widgetIds.has(widgetId))
|
|
105
|
+
problems.push(`customWidgetId '${widgetId}' bu mobil uygulamanın widget'ı değil — çubuk yerine uyarı çizilir`);
|
|
106
|
+
}
|
|
107
|
+
return problems;
|
|
108
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motorun kendi register ettiği adımlar (`@ng-apps/flow` `flow-engine.ts` initialize) — iki
|
|
3
|
+
* platformda da koşar. `end` ve `query` hiçbir barındırıcıda yoktur (normalizasyon `end`'i atar).
|
|
4
|
+
*/
|
|
5
|
+
export const ENGINE_STEP_TYPES = ['start', 'setter', 'code', 'condition', 'microservice', 'validation', 'flow-trigger', 'function'];
|
|
6
|
+
/**
|
|
7
|
+
* Eski adım tipi → koşan karşılığı. `navigate` ile `redirect`'in argümanı aynıdır; web `navigate`'i
|
|
8
|
+
* register etmez, yeni mobil runtime onu `redirect` takma adı olarak kaydeder — iki platformda da
|
|
9
|
+
* `redirect` kesin üstündür.
|
|
10
|
+
*/
|
|
11
|
+
export const STEP_ALIASES = Object.freeze({ navigate: 'redirect' });
|
|
12
|
+
/** Tipli stil grupları iki platformda da çizilir (web CSS'e, mobil RN stiline derler). */
|
|
13
|
+
export const TYPED_STYLE_GROUPS = ['surfaceStyle', 'typographyStyle', 'sectionStyle', 'headerStyle', 'valueStyle'];
|
|
14
|
+
/** Bulguyu mevcut `layoutWarnings[]` satır biçimine çevirir (`PAGE-010 (uyarı) — …` kalıbı). */
|
|
15
|
+
export function findingLine(finding) {
|
|
16
|
+
const level = finding.severity === 'info' ? 'bilgi' : 'uyarı';
|
|
17
|
+
const target = finding.target ? ` [${finding.target}]` : '';
|
|
18
|
+
const guide = finding.guide ? ` (${finding.guide})` : '';
|
|
19
|
+
return `${finding.code} (${level})${target} — ${finding.message}${guide}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mobileProfile } from './mobile.js';
|
|
2
|
+
import { webProfile } from './web.js';
|
|
3
|
+
export const APPLICATION_TYPE_TTL_MS = 10 * 60_000;
|
|
4
|
+
const caches = new WeakMap();
|
|
5
|
+
function cacheOf(platform) {
|
|
6
|
+
let cache = caches.get(platform);
|
|
7
|
+
if (!cache) {
|
|
8
|
+
cache = new Map();
|
|
9
|
+
caches.set(platform, cache);
|
|
10
|
+
}
|
|
11
|
+
return cache;
|
|
12
|
+
}
|
|
13
|
+
/** Backend `ApplicationType` JSON'da açıklama metniyle ('web' | 'mobile') döner; eski sayısal biçim de okunur. */
|
|
14
|
+
export function readApplicationPlatform(value) {
|
|
15
|
+
if (value === 1 || (typeof value === 'string' && value.toLowerCase() === 'mobile'))
|
|
16
|
+
return 'mobile';
|
|
17
|
+
if (value === 0 || (typeof value === 'string' && value.toLowerCase() === 'web'))
|
|
18
|
+
return 'web';
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export function profileFor(platform) {
|
|
22
|
+
return platform === 'mobile' ? mobileProfile : webProfile;
|
|
23
|
+
}
|
|
24
|
+
function factsOf(record) {
|
|
25
|
+
const platform = readApplicationPlatform(record.type);
|
|
26
|
+
const id = typeof record.id === 'string' ? record.id : undefined;
|
|
27
|
+
if (!platform || !id)
|
|
28
|
+
return undefined;
|
|
29
|
+
const homepage = record.homepage && typeof record.homepage === 'object' ? record.homepage.id : undefined;
|
|
30
|
+
const homepageId = typeof record.homepageId === 'string' ? record.homepageId : typeof homepage === 'string' ? homepage : undefined;
|
|
31
|
+
const properties = record.properties && typeof record.properties === 'object' && !Array.isArray(record.properties)
|
|
32
|
+
? record.properties
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
platform,
|
|
37
|
+
...(typeof record.slug === 'string' ? { slug: record.slug } : {}),
|
|
38
|
+
...(typeof record.name === 'string' ? { name: record.name } : {}),
|
|
39
|
+
...(typeof record.projectSlug === 'string' ? { projectSlug: record.projectSlug } : {}),
|
|
40
|
+
...(homepageId ? { homepageId } : {}),
|
|
41
|
+
...(properties ? { properties } : {})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function fresh(entry) {
|
|
45
|
+
return entry !== undefined && Date.now() - entry.at < APPLICATION_TYPE_TTL_MS;
|
|
46
|
+
}
|
|
47
|
+
export async function resolveApplication(platform, applicationId) {
|
|
48
|
+
const cache = cacheOf(platform);
|
|
49
|
+
const cached = cache.get(applicationId);
|
|
50
|
+
if (fresh(cached))
|
|
51
|
+
return cached.facts;
|
|
52
|
+
try {
|
|
53
|
+
const payload = await platform.org('GET', `/applications/${applicationId}`);
|
|
54
|
+
const facts = factsOf(((payload?.data ?? payload) ?? {}));
|
|
55
|
+
if (facts)
|
|
56
|
+
cache.set(applicationId, { facts, at: Date.now() });
|
|
57
|
+
return facts;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function resolveProfile(platform, applicationId) {
|
|
64
|
+
const application = await resolveApplication(platform, applicationId);
|
|
65
|
+
return application
|
|
66
|
+
? { profile: profileFor(application.platform), known: true, application }
|
|
67
|
+
: { profile: webProfile, known: false };
|
|
68
|
+
}
|
|
69
|
+
const SLUG_LOOKUP_PAGE = 50;
|
|
70
|
+
/**
|
|
71
|
+
* `page_screenshot` ve `page_runtime_get` uygulamayı slug ile tanır. Org listesi `searchTerm` ile
|
|
72
|
+
* daraltılır ve slug + proje slug'ı BİREBİR eşleşen kayıt alınır (arama ILIKE'tır, `shop` `shop-2`'yi
|
|
73
|
+
* de getirir). B1'den beri liste tip filtresi verilmeyince iki tipi de döner. Aynı slug'ı çok proje
|
|
74
|
+
* kullanıyorsa (`mobile`, `admin`) aranan kayıt ilk sayfanın dışında kalabilir: sayfa doluysa ve
|
|
75
|
+
* eşleşme yoksa arama proje kimliğiyle tekrarlanır — (proje, slug) tekildir.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveProfileBySlug(platform, projectSlug, applicationSlug) {
|
|
78
|
+
const cache = cacheOf(platform);
|
|
79
|
+
const key = `slug:${projectSlug}/${applicationSlug}`;
|
|
80
|
+
const cached = cache.get(key);
|
|
81
|
+
if (fresh(cached))
|
|
82
|
+
return { profile: profileFor(cached.facts.platform), known: true, application: cached.facts };
|
|
83
|
+
try {
|
|
84
|
+
const search = async (query) => {
|
|
85
|
+
const payload = await platform.org('GET', '/applications', { query: { searchTerm: applicationSlug, pageLength: SLUG_LOOKUP_PAGE, ...query } });
|
|
86
|
+
return Array.isArray(payload?.data) ? payload.data : [];
|
|
87
|
+
};
|
|
88
|
+
const matchIn = (rows) => rows.find(row => row.slug === applicationSlug && (row.projectSlug === undefined || row.projectSlug === projectSlug));
|
|
89
|
+
const rows = await search({});
|
|
90
|
+
let match = matchIn(rows);
|
|
91
|
+
if (!match && rows.length >= SLUG_LOOKUP_PAGE) {
|
|
92
|
+
const project = await platform.org('GET', `/p/${projectSlug}`);
|
|
93
|
+
const projectId = (project?.data ?? project)?.id;
|
|
94
|
+
if (typeof projectId === 'string')
|
|
95
|
+
match = matchIn(await search({ projectId }));
|
|
96
|
+
}
|
|
97
|
+
const facts = match ? factsOf(match) : undefined;
|
|
98
|
+
if (!facts)
|
|
99
|
+
return { profile: webProfile, known: false };
|
|
100
|
+
cache.set(key, { facts, at: Date.now() });
|
|
101
|
+
return { profile: profileFor(facts.platform), known: true, application: facts };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { profile: webProfile, known: false };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Yazma araçları uygulamayı değiştirdiğinde (tip, slug, properties) kaydı düşürür. */
|
|
108
|
+
export function forgetApplication(platform, applicationId) {
|
|
109
|
+
const cache = cacheOf(platform);
|
|
110
|
+
if (applicationId === undefined) {
|
|
111
|
+
cache.clear();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const [key, entry] of cache) {
|
|
115
|
+
if (key === applicationId || entry.facts.id === applicationId)
|
|
116
|
+
cache.delete(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Yanıta eklenen alan: tip biliniyorsa `applicationType`, bilinmiyorsa hiçbir şey. */
|
|
120
|
+
export function applicationTypeField(resolved) {
|
|
121
|
+
return resolved.known ? { applicationType: resolved.profile.platform } : {};
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ENGINE_STEP_TYPES, STEP_ALIASES } from './profile.js';
|
|
2
|
+
import { pageContainerNode } from '../normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* Web profili = gateway'in profil öncesi davranışı. Buradaki her değer bugün kodda bir yerde
|
|
5
|
+
* örtük olarak vardı; `test/platform-profile.test.ts` golden'ı bu profilin çıktıyı değiştirmediğini
|
|
6
|
+
* kilitler. Değer değiştirmek web davranışını değiştirir — golden kırmızıya döner.
|
|
7
|
+
*/
|
|
8
|
+
export const webProfile = Object.freeze({
|
|
9
|
+
platform: 'web',
|
|
10
|
+
catalogType: 'web',
|
|
11
|
+
layout: {
|
|
12
|
+
modes: new Set(['grid', 'flex', 'cssgrid']),
|
|
13
|
+
blocksMeaningful: true,
|
|
14
|
+
rootContainer: pageContainerNode,
|
|
15
|
+
orderBy: 'block-position'
|
|
16
|
+
},
|
|
17
|
+
appearance: {
|
|
18
|
+
ignoredKeys: new Set(),
|
|
19
|
+
sizeUnits: new Set(['px', '%', 'em', 'rem', 'calc'])
|
|
20
|
+
},
|
|
21
|
+
style: { freeStyle: true, hoverStyle: true, resolver: 'css' },
|
|
22
|
+
steps: {
|
|
23
|
+
registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share']),
|
|
24
|
+
aliases: STEP_ALIASES,
|
|
25
|
+
infoOnly: new Set()
|
|
26
|
+
},
|
|
27
|
+
shell: {
|
|
28
|
+
components: new Set(['navigations', 'sidebar', 'header']),
|
|
29
|
+
suggestions: {},
|
|
30
|
+
legacyShellNodes: new Set()
|
|
31
|
+
},
|
|
32
|
+
externalComponents: true,
|
|
33
|
+
capture: {
|
|
34
|
+
target: 'studio-web',
|
|
35
|
+
devices: { desktop: { width: 1280, height: 800, dpr: 1 } },
|
|
36
|
+
defaultDevice: 'desktop'
|
|
37
|
+
}
|
|
38
|
+
});
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -154,7 +154,7 @@ const flowBranchNames = {
|
|
|
154
154
|
*/
|
|
155
155
|
export const knownFlowStepTypes = new Set([
|
|
156
156
|
'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
|
|
157
|
-
'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function'
|
|
157
|
+
'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share'
|
|
158
158
|
]);
|
|
159
159
|
const flowFreeBranchTypes = new Set(['condition']);
|
|
160
160
|
/** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
|
|
@@ -167,16 +167,15 @@ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
|
|
|
167
167
|
// Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
|
|
168
168
|
const flowLiteralTextFields = new Map([
|
|
169
169
|
['alert', ['message', 'description']],
|
|
170
|
-
['confirm', ['message', 'title', 'confirmText', 'cancelText']]
|
|
170
|
+
['confirm', ['message', 'title', 'confirmText', 'cancelText']],
|
|
171
|
+
['share', ['title']]
|
|
171
172
|
]);
|
|
172
173
|
const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
|
|
173
|
-
// Barındırıcıya özel tipler
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// (`ApplicationType.cs`) ve gateway hangisi olduğunu bilmez. Mobil ayağı uygulama tarafında
|
|
179
|
-
// kapanır (`ScreenRuntime.tsx`), lint'te değil.
|
|
174
|
+
// Barındırıcıya özel tipler BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten
|
|
175
|
+
// `redirect`e çeviriyor (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da
|
|
176
|
+
// koşar, yani vaka uyarılmaz, ORTADAN KALKAR. Gateway uygulamanın tipini platform profilinden bilir
|
|
177
|
+
// ve barındırıcı sözlüğü `platform/*.ts`'tedir (`profile.steps`); mobil farkları orada `MOB-005`
|
|
178
|
+
// olarak konuşulur (`platform/lint.ts`). Yeni mobil runtime `confirm`/`download`'ı kaydeder.
|
|
180
179
|
const flowUnregisteredStepTypes = new Set(['end', 'query']);
|
|
181
180
|
const flowSetterIterateeTypes = new Set(['change', 'fill']);
|
|
182
181
|
function flowStepList(value) {
|
|
@@ -2367,7 +2366,7 @@ export const policyRules = [
|
|
|
2367
2366
|
id: 'FLOW-022',
|
|
2368
2367
|
module: 'flow',
|
|
2369
2368
|
appliesTo: ['create', 'update'],
|
|
2370
|
-
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu)",
|
|
2369
|
+
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu). share adımında (2026-09-25) yalnız `title` düz metindir: @ng-apps/flow resolveShareContent onu olduğu gibi verir, `message` ve `path` ifadedir",
|
|
2371
2370
|
check: definition => flowAllSteps(definition)
|
|
2372
2371
|
.flatMap((step) => {
|
|
2373
2372
|
const type = String(step.type ?? '');
|
|
@@ -2380,16 +2379,61 @@ export const policyRules = [
|
|
|
2380
2379
|
.map(field => ({
|
|
2381
2380
|
ruleId: 'FLOW-022',
|
|
2382
2381
|
severity: 'warn',
|
|
2383
|
-
message:
|
|
2382
|
+
message: type === 'share'
|
|
2383
|
+
? `'${flowStepLabel(step)}' share adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: başlık paylaşım penceresine olduğu gibi gider (iOS'ta e-posta konusu, Android'de seçici başlığı), ifade değerlendirilmez. Değişen içeriği ifade olan 'message' alanına yazın — ör. message: "'Şuna bak: ' + Page.properties.name"; bağlantı 'path' alanındadır.`
|
|
2384
|
+
: `'${flowStepLabel(step)}' ${type} adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: ${type}.ts task.argument.${field}'i doğrudan ekrana geçirir ve args hiçbir yerde değerlendirilmez (Runtime.svelte \`argument: step.args\`). Kullanıcı ifadenin sonucunu değil ifadenin KENDİSİNİ görür. Ucun mesajını göstermek için bir setter (ya da code) adımı kullanın — value { type: 'code', ref: 'Steps.error?.message' }; hata metninin nerede durduğu page.md §hata yolu'nda.`,
|
|
2384
2385
|
guide: 'ngapps://guides/page'
|
|
2385
2386
|
}));
|
|
2386
2387
|
})
|
|
2387
2388
|
},
|
|
2389
|
+
{
|
|
2390
|
+
id: 'FLOW-027',
|
|
2391
|
+
module: 'flow',
|
|
2392
|
+
appliesTo: ['create', 'update'],
|
|
2393
|
+
sourceRef: "share adımının argümanı StepManagementArgument'ın Title/Message/Path alanlarında saklanır (StepType.cs `share`, kök docs/adr/0001-share-akis-adimi.md); url/link/text gibi anahtarlar sözleşmede YOKTUR ve platform onları 200 ile sessizce düşürür — adım hiçbir şey paylaşmadan akış sürer. `message` ve `path` İFADEDİR (@ng-apps/flow resolveShareContent → runCode): tırnaksız 'https://…' JS'te etiket + yorumdur, undefined döner",
|
|
2394
|
+
check: definition => flowAllSteps(definition)
|
|
2395
|
+
.filter(step => step.type === 'share')
|
|
2396
|
+
.flatMap((step) => {
|
|
2397
|
+
const args = (step.args ?? {});
|
|
2398
|
+
const filled = (key) => typeof args[key] === 'string' && String(args[key]).trim().length > 0;
|
|
2399
|
+
const findings = [];
|
|
2400
|
+
const strayLinks = ['url', 'link', 'href'].filter(filled);
|
|
2401
|
+
const strayTexts = ['text', 'body', 'content'].filter(filled);
|
|
2402
|
+
if (strayLinks.length > 0) {
|
|
2403
|
+
findings.push({
|
|
2404
|
+
ruleId: 'FLOW-027',
|
|
2405
|
+
severity: filled('path') ? 'warn' : 'block',
|
|
2406
|
+
message: `'${flowStepLabel(step)}' share adımının bağlantısı args.${strayLinks.join('/args.')} alanında: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür ve adım bağlantısız kalır. Bağlantıyı args.path'e ifade olarak yazın — ör. "'https://ornek.app/urun/' + Page.queryParams.id".`,
|
|
2407
|
+
guide: 'ngapps://guides/page'
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
if (strayTexts.length > 0) {
|
|
2411
|
+
findings.push({
|
|
2412
|
+
ruleId: 'FLOW-027',
|
|
2413
|
+
severity: filled('message') ? 'warn' : 'block',
|
|
2414
|
+
message: `'${flowStepLabel(step)}' share adımının metni args.${strayTexts.join('/args.')} alanında: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu sessizce düşürür. Metni args.message'a ifade olarak yazın — ör. "'Şuna bak: ' + Page.properties.name".`,
|
|
2415
|
+
guide: 'ngapps://guides/page'
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
for (const key of ['message', 'path']) {
|
|
2419
|
+
const value = typeof args[key] === 'string' ? String(args[key]).trim() : '';
|
|
2420
|
+
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value)) {
|
|
2421
|
+
findings.push({
|
|
2422
|
+
ruleId: 'FLOW-027',
|
|
2423
|
+
severity: 'block',
|
|
2424
|
+
message: `'${flowStepLabel(step)}' share adımının '${key}' alanı tırnaksız bir adres: '${value}'. Bu alan İFADEDİR — tırnaksız adres JS'te etiket + yorumdur, undefined döner ve hiçbir şey paylaşılmaz. Dize olarak yazın: "'${value}'".`,
|
|
2425
|
+
guide: 'ngapps://guides/page'
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return findings;
|
|
2430
|
+
})
|
|
2431
|
+
},
|
|
2388
2432
|
{
|
|
2389
2433
|
id: 'FLOW-023',
|
|
2390
2434
|
module: 'flow',
|
|
2391
2435
|
appliesTo: ['create', 'update'],
|
|
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
|
|
2436
|
+
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",
|
|
2393
2437
|
check: definition => {
|
|
2394
2438
|
const steps = flowAllSteps(definition);
|
|
2395
2439
|
// 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
|
-
const rawProp = (props, name) => {
|
|
542
|
+
export 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;
|