@appaflytech/zeyrek-ai-mcp 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/knowledge/componentCatalog.js +95 -104
- package/dist/knowledge/componentEvents.js +34 -18
- package/dist/knowledge/mobileComponentCatalog.js +119 -20
- package/dist/knowledge/mobileComponentEvents.js +41 -0
- package/dist/knowledge/motionResolve.js +252 -0
- package/dist/knowledge/normalize.js +117 -71
- package/dist/knowledge/pageDiscover.js +2 -2
- package/dist/knowledge/platform/batchOps.js +70 -0
- package/dist/knowledge/platform/heroTag.js +153 -0
- package/dist/knowledge/platform/lint.js +128 -6
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
- package/dist/knowledge/platform/mobileRules.js +18 -0
- package/dist/knowledge/platform/pageTransition.js +57 -0
- package/dist/knowledge/platform/web.js +1 -1
- package/dist/knowledge/rules.js +217 -24
- package/dist/knowledge/styleResolve.js +0 -1
- package/dist/knowledge/styleResolveNative.js +88 -4
- package/dist/platform/capture/actions.js +27 -1
- package/dist/platform/capture/capture.js +10 -13
- package/dist/platform/capture/persistent-browser.js +130 -0
- package/dist/platform/capture/summary.js +4 -1
- package/dist/resources/examples/flow-list-to-form.json +8 -10
- package/dist/resources/examples/flow-load-table.json +1 -1
- package/dist/resources/examples/mobile-app-shell-drawer.json +1 -0
- package/dist/resources/examples/mobile-list-detail-form.json +7 -3
- package/dist/resources/examples/mobile-scan-code.json +285 -0
- package/dist/resources/guides/component.md +3 -3
- package/dist/resources/guides/entity.md +16 -0
- package/dist/resources/guides/expressions.md +8 -7
- package/dist/resources/guides/merge.md +98 -18
- package/dist/resources/guides/microservice.md +3 -3
- package/dist/resources/guides/middleware.md +2 -0
- package/dist/resources/guides/mobile-layout.md +2 -1
- package/dist/resources/guides/mobile-publish.md +1 -0
- package/dist/resources/guides/mobile-steps.md +67 -0
- package/dist/resources/guides/mobile.md +51 -70
- package/dist/resources/guides/page.md +229 -137
- package/dist/resources/guides/translation.md +67 -1
- package/dist/resources/guides/workflow.md +31 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
- package/dist/tools/activities.js +3 -1
- package/dist/tools/applications.js +16 -2
- package/dist/tools/components.js +12 -6
- package/dist/tools/designSchemaPatch.js +18 -13
- package/dist/tools/entities.js +41 -4
- package/dist/tools/layouts.js +29 -9
- package/dist/tools/merges.js +29 -9
- package/dist/tools/mobile.js +15 -1
- package/dist/tools/pages.js +156 -168
- package/dist/tools/platformInfo.js +3 -2
- package/dist/tools/profileWrite.js +2 -2
- package/dist/tools/queries.js +1 -1
- package/dist/tools/rows.js +14 -3
- package/dist/tools/schemaOps.js +132 -34
- package/dist/tools/screenWrite.js +3 -3
- package/dist/tools/screens.js +42 -3
- package/dist/tools/translations.js +148 -6
- package/package.json +2 -2
package/dist/knowledge/rules.js
CHANGED
|
@@ -5,6 +5,7 @@ import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
|
|
|
5
5
|
import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
|
|
6
6
|
import { backendPolicyRules } from './backendConstraints.js';
|
|
7
7
|
import { accessPolicyRules } from './access.js';
|
|
8
|
+
import { isGuid } from '../platform/refs.js';
|
|
8
9
|
import { themePolicyRules } from './themeRules.js';
|
|
9
10
|
import { processPolicyRules } from './process.js';
|
|
10
11
|
import { mobileStorePolicyRules } from './mobileStore.js';
|
|
@@ -153,11 +154,13 @@ export function readsBeforeGuard(source, root) {
|
|
|
153
154
|
* yaşatılır (microservice.ts:174 `isHasSuccess`). 'error' dalı 2026-09-26'dan beri motorun
|
|
154
155
|
* KENDİSİ tanır (ADR 0004, ZM-07/g-02): bağlıysa hata anında `always` hiç koşmaz, sürer
|
|
155
156
|
* (microservice.ts:182 `hasBranch`) — eski motor tanımadığı dalı hiç aramadığı için bu ekleme
|
|
156
|
-
* geriye dönük zararsızdır.
|
|
157
|
+
* geriye dönük zararsızdır. `validation` tüm kurallar geçince 'success', biri düşünce 'error' dalına,
|
|
158
|
+
* ardından her durumda 'always'e gider (validation.ts; kök ADR 0014, g-18).
|
|
157
159
|
*/
|
|
158
160
|
const flowBranchNames = {
|
|
159
161
|
start: ['always'],
|
|
160
162
|
microservice: ['always', 'success', 'error'],
|
|
163
|
+
validation: ['always', 'success', 'error'],
|
|
161
164
|
confirm: ['yes', 'no', 'always'],
|
|
162
165
|
function: ['always'],
|
|
163
166
|
haptic: ['always'],
|
|
@@ -169,6 +172,8 @@ const flowBranchNames = {
|
|
|
169
172
|
permission: ['always'],
|
|
170
173
|
'open-settings': ['always'],
|
|
171
174
|
'ad-show': ['always', 'error'],
|
|
175
|
+
back: ['always'],
|
|
176
|
+
'scan-code': ['always', 'error'],
|
|
172
177
|
end: []
|
|
173
178
|
};
|
|
174
179
|
/**
|
|
@@ -181,7 +186,7 @@ export const knownFlowStepTypes = new Set([
|
|
|
181
186
|
'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
|
|
182
187
|
'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
|
|
183
188
|
'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
|
|
184
|
-
'permission', 'open-settings', 'ad-show'
|
|
189
|
+
'permission', 'open-settings', 'ad-show', 'back', 'scan-code'
|
|
185
190
|
]);
|
|
186
191
|
/**
|
|
187
192
|
* Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
|
|
@@ -195,8 +200,20 @@ export const locationAccuracies = new Set(['low', 'balanced', 'high']);
|
|
|
195
200
|
* ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
|
|
196
201
|
*/
|
|
197
202
|
export const adPlacementNamePattern = /^[a-z][A-Za-z0-9]{0,39}$/;
|
|
203
|
+
/**
|
|
204
|
+
* `scan-code` adımının kod türleri — `@ng-apps/flow` `SCAN_CODE_TYPES`'ın aynası (kök ADR 0017); altın dosya
|
|
205
|
+
* `scan-code-golden.json` ile kilitlidir (`test/flow-step-parity.test.ts`), backend `ScanCodeType` aynı kümeyle 422 döner.
|
|
206
|
+
* `upc_a` bilerek yok: UPC-A etiketi iki platformda `ean13` ve on üç haneyle döner.
|
|
207
|
+
*/
|
|
208
|
+
export const scanCodeTypes = new Set(['aztec', 'codabar', 'code39', 'code93', 'code128', 'datamatrix', 'ean8', 'ean13', 'itf14', 'pdf417', 'qr', 'upc_e']);
|
|
198
209
|
/** `permission` adımının kipleri — `@ng-apps/flow` `PERMISSION_MODES`'un aynası (kök ADR 0010); tür adını gateway bilmez, backend doğrular. */
|
|
199
210
|
export const permissionStepModes = new Set(['request', 'check']);
|
|
211
|
+
/**
|
|
212
|
+
* `validation` adımının kural tipleri — `@ng-apps/flow` `VALIDATION_RULE_TYPES`'ın aynası (`test/flow-step-parity.test.ts`
|
|
213
|
+
* bağlar). Backend kümeyi doğrulamaz (kök ADR 0014); tanınmayan tip runtime'da `validate()` default dalında GEÇER sayılır.
|
|
214
|
+
*/
|
|
215
|
+
export const validationRuleTypes = new Set(['required', 'email', 'regex', 'min-length', 'max-length', 'min-value', 'max-value']);
|
|
216
|
+
const validationParameterlessTypes = new Set(['required', 'email']);
|
|
200
217
|
const flowFreeBranchTypes = new Set(['condition']);
|
|
201
218
|
/** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
|
|
202
219
|
const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
|
|
@@ -204,16 +221,22 @@ const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
|
|
|
204
221
|
// backend'de bir Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; }`), yani
|
|
205
222
|
// okunabilir bir ad iki uçta birden tutmaz: istek System.Guid çevrimiyle reddedilir.
|
|
206
223
|
const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
224
|
+
// Metin şablonu alanları (kök ADR 0003 §Metin şablonu, K4 2026-10-08): iki barındırıcı (web alert.ts/confirm.ts,
|
|
225
|
+
// cihaz runtime-tasks.ts + device.functions.ts) metni @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan
|
|
226
|
+
// geçirir — yalnız `{{ }}` içi çözülür, dışındaki çıplak ifade ekranda harfi harfine görünür.
|
|
227
|
+
const flowTemplateTextFields = new Map([
|
|
210
228
|
['alert', ['message', 'description']],
|
|
211
229
|
['confirm', ['message', 'title', 'confirmText', 'cancelText']],
|
|
212
|
-
['
|
|
213
|
-
['notification-schedule', ['name', 'title', 'path']],
|
|
230
|
+
['notification-schedule', ['name', 'title']],
|
|
214
231
|
['notification-cancel', ['name']]
|
|
215
232
|
]);
|
|
233
|
+
// Düz metin alanları: ne ifade ne şablon çözülür (`share.title` resolveShareContent'te, bildirim `path`'i K4 kapsamı dışında).
|
|
234
|
+
const flowLiteralTextFields = new Map([
|
|
235
|
+
['share', ['title']],
|
|
236
|
+
['notification-schedule', ['path']]
|
|
237
|
+
]);
|
|
216
238
|
const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
|
|
239
|
+
const flowTemplateSegments = /\{\{[\s\S]*?\}\}/g;
|
|
217
240
|
// Barındırıcıya özel tipler BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten
|
|
218
241
|
// `redirect`e çeviriyor (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da
|
|
219
242
|
// koşar, yani vaka uyarılmaz, ORTADAN KALKAR. Gateway uygulamanın tipini platform profilinden bilir
|
|
@@ -1843,7 +1866,7 @@ export const policyRules = [
|
|
|
1843
1866
|
id: 'ENT-006',
|
|
1844
1867
|
module: 'entity',
|
|
1845
1868
|
appliesTo: ['create', 'update'],
|
|
1846
|
-
sourceRef: 'sms oturumu: kullanıcının intprimarykey/guidprimarykey referans tabloları — PK identity + properties.generationType:0 (Always) ile otomatik üretilir; dbType↔commonType eşleşmesi zorunlu',
|
|
1869
|
+
sourceRef: 'sms oturumu: kullanıcının intprimarykey/guidprimarykey referans tabloları — PK identity + properties.generationType:0 (Always) ile otomatik üretilir; dbType↔commonType eşleşmesi zorunlu. 38 S-1: PostgreSqlEntityConnector.ValidateEntityAsync identity alanında GenerationType 0/1 değilse InvalidGenerationTypeException (400) atar — create ve update',
|
|
1847
1870
|
check: definition => {
|
|
1848
1871
|
const primaryKeys = asArray(definition.fields)
|
|
1849
1872
|
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
@@ -1853,7 +1876,15 @@ export const policyRules = [
|
|
|
1853
1876
|
const properties = (key.properties ?? {});
|
|
1854
1877
|
const dbType = String(key.dbType ?? '');
|
|
1855
1878
|
const expected = dbType === 'UUID' ? 'guid' : ['INT2', 'INT4', 'INT8'].includes(dbType) ? 'number' : null;
|
|
1856
|
-
if (key.identity
|
|
1879
|
+
if (key.identity === true && properties.generationType !== 0 && properties.generationType !== 1) {
|
|
1880
|
+
findings.push({
|
|
1881
|
+
ruleId: 'ENT-006',
|
|
1882
|
+
severity: 'block',
|
|
1883
|
+
message: `'${key.slug ?? key.name}' primary key'i identity ama properties.generationType ${properties.generationType === undefined ? 'yok' : `'${String(properties.generationType)}'`} — platform 400 "Please select a valid generation type for '${key.slug ?? key.name}'" döner. Alana properties: { generationType: 0 } (Always) ya da 1 (ByDefault) ekleyin.`,
|
|
1884
|
+
guide: 'ngapps://guides/entity'
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
else if (key.identity !== true) {
|
|
1857
1888
|
findings.push({
|
|
1858
1889
|
ruleId: 'ENT-006',
|
|
1859
1890
|
severity: 'warn',
|
|
@@ -2507,7 +2538,7 @@ export const policyRules = [
|
|
|
2507
2538
|
: [{
|
|
2508
2539
|
ruleId: 'FLOW-021',
|
|
2509
2540
|
severity: 'warn',
|
|
2510
|
-
message: "errorSteps yalnız 'start' adımı taşıyor — motor bunu hata yolu SAYMAZ (flow-engine.ts hasErrorFlow: start dışında en az bir adım arar). Hata yolu kurulmuş görünür ama uç 4xx dönünce onError false döner, akış 'always' dalından hata gövdesiyle devam eder ve kullanıcıya hiçbir mesaj ulaşmaz. start'ın ardına en az bir adım bağlayın:
|
|
2541
|
+
message: "errorSteps yalnız 'start' adımı taşıyor — motor bunu hata yolu SAYMAZ (flow-engine.ts hasErrorFlow: start dışında en az bir adım arar). Hata yolu kurulmuş görünür ama uç 4xx dönünce onError false döner, akış 'always' dalından hata gövdesiyle devam eder ve kullanıcıya hiçbir mesaj ulaşmaz. start'ın ardına en az bir adım bağlayın: alert yeter — 'message' alanı metin şablonudur (FLOW-022), ucun kendi mesajı '{{ Steps.error.message }}' ile gösterilir; {{ }} dışında yazılan ifadeyi kullanıcı harfi harfine görür. Mesajı bir bileşene yazacaksan setter (value.type:'code' → Steps.error.message).",
|
|
2511
2542
|
guide: 'ngapps://guides/page'
|
|
2512
2543
|
}];
|
|
2513
2544
|
}
|
|
@@ -2516,26 +2547,32 @@ export const policyRules = [
|
|
|
2516
2547
|
id: 'FLOW-022',
|
|
2517
2548
|
module: 'flow',
|
|
2518
2549
|
appliesTo: ['create', 'update'],
|
|
2519
|
-
sourceRef: "
|
|
2550
|
+
sourceRef: "Metin şablonu (kök docs/adr/0003-cihaz-yetenegi-adimlari.md §Metin şablonu, plans/mobil-acik-isler K4, 2026-10-08): alert.ts/confirm.ts (web) ve ui-native runtime-tasks.ts + @ng-apps/flow device.functions.ts (cihaz, önizleme) `alert.message/description`, `confirm.message/title/confirmText/cancelText`, `notification-schedule.name/title`, `notification-cancel.name`'i @ng-apps/flow `resolveStepText` → `runPropertyCode`'dan geçirir: yalnız `{{ }}` içi çözülür (çeviri `t('anahtar')`, `state`, `Steps`, `item`), dışındaki çıplak ifade ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu; 38 S-12, 37 S-11). Düz metin kalanlar: share `title` (resolveShareContent olduğu gibi verir; `message` ve `path` ifadedir) ve notification-schedule `path` (K4 kapsamı dışı) — orada `{{ }}` de çözülmez",
|
|
2520
2551
|
check: definition => flowAllSteps(definition)
|
|
2521
2552
|
.flatMap((step) => {
|
|
2522
2553
|
const type = String(step.type ?? '');
|
|
2523
|
-
const fields = flowLiteralTextFields.get(type);
|
|
2524
|
-
if (!fields)
|
|
2525
|
-
return [];
|
|
2526
2554
|
const args = (step.args ?? {});
|
|
2527
|
-
|
|
2528
|
-
|
|
2555
|
+
const label = flowStepLabel(step);
|
|
2556
|
+
const text = (field) => (typeof args[field] === 'string' ? args[field] : '');
|
|
2557
|
+
const templateFindings = (flowTemplateTextFields.get(type) ?? [])
|
|
2558
|
+
.filter(field => flowExpressionLooking.test(text(field).replace(flowTemplateSegments, '')))
|
|
2559
|
+
.map(field => ({
|
|
2560
|
+
ruleId: 'FLOW-022',
|
|
2561
|
+
severity: 'warn',
|
|
2562
|
+
message: `'${label}' ${type} adımının '${field}' alanı {{ }} dışında ifade içeriyor: bu alan METİN ŞABLONUDUR (kök ADR 0003 §Metin şablonu) — yalnız {{ }} içi çözülür, dışındaki ifade kullanıcıya harfi harfine görünür. İfadeyi sarın: '{{ Steps.error.message }}', 'ilac-{{ item.id }}', '{{ t('anahtar') }}' (çeviri).`,
|
|
2563
|
+
guide: 'ngapps://guides/page'
|
|
2564
|
+
}));
|
|
2565
|
+
const literalFindings = (flowLiteralTextFields.get(type) ?? [])
|
|
2566
|
+
.filter(field => flowExpressionLooking.test(text(field)) || text(field).includes('{{'))
|
|
2529
2567
|
.map(field => ({
|
|
2530
2568
|
ruleId: 'FLOW-022',
|
|
2531
2569
|
severity: 'warn',
|
|
2532
|
-
message: type
|
|
2533
|
-
? `'${
|
|
2534
|
-
: type
|
|
2535
|
-
? `'${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.`
|
|
2536
|
-
: `'${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.`,
|
|
2570
|
+
message: type === 'share'
|
|
2571
|
+
? `'${label}' share adımının '${field}' alanı ifade ya da {{ }} 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ığı), ne ifade ne şablon çözülür. 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.`
|
|
2572
|
+
: `'${label}' ${type} adımının '${field}' alanı ifade ya da {{ }} içeriyor ama bu alan DÜZ METİNDİR (kök ADR 0003): açılacak sayfanın slug'ı olduğu gibi kullanılır, ne ifade ne şablon çözülür. Değişen kısmı ifade olan 'query' (sayfanın sorgusu) alanına yazın.`,
|
|
2537
2573
|
guide: 'ngapps://guides/page'
|
|
2538
2574
|
}));
|
|
2575
|
+
return [...templateFindings, ...literalFindings];
|
|
2539
2576
|
})
|
|
2540
2577
|
},
|
|
2541
2578
|
{
|
|
@@ -2680,6 +2717,119 @@ export const policyRules = [
|
|
|
2680
2717
|
return findings;
|
|
2681
2718
|
})
|
|
2682
2719
|
},
|
|
2720
|
+
{
|
|
2721
|
+
id: 'FLOW-042',
|
|
2722
|
+
module: 'flow',
|
|
2723
|
+
appliesTo: ['create', 'update'],
|
|
2724
|
+
sourceRef: "Kod tarama adımının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0017-kod-tarama-akis-adimi.md): scan-code.value = izinli kod türleri (sözlükten virgüllü küme, DÜZ METİN; boş → hepsi), scan-code.title = ekrandaki yönerge (metin şablonu, FLOW-022). Backend FlowDtoValidator her adı ScanCodeType kümesiyle BİREBİR karşılaştırır (büyük harf ve ifade sarmalı 422, FlowStepScanCodeTypeUnknown); çalışma zamanı sözlük dışı adda ekranı açmaz, `invalid` döner. upc_a sözlükte yoktur: UPC-A etiketi iki platformda ean13 ve on üç haneyle (başında 0) döner. types / formats / barcodeTypes anahtarları sözleşmede yok, 200 ile sessizce düşer ve adım bütün türleri okur.",
|
|
2725
|
+
check: definition => flowAllSteps(definition)
|
|
2726
|
+
.filter(step => step.type === 'scan-code')
|
|
2727
|
+
.flatMap((step) => {
|
|
2728
|
+
const args = (step.args ?? {});
|
|
2729
|
+
const raw = typeof args.value === 'string' ? args.value : '';
|
|
2730
|
+
const names = raw.split(',').map(name => name.trim()).filter(name => name !== '');
|
|
2731
|
+
const unknown = names.filter(name => !scanCodeTypes.has(name));
|
|
2732
|
+
const stray = ['types', 'formats', 'barcodeTypes', 'codeTypes', 'type'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
|
|
2733
|
+
const allowed = [...scanCodeTypes].join(', ');
|
|
2734
|
+
const findings = [];
|
|
2735
|
+
if (unknown.length > 0) {
|
|
2736
|
+
const unquoted = raw.replace(/^['"`](.*)['"`]$/, '$1');
|
|
2737
|
+
const lowered = unknown.filter(name => scanCodeTypes.has(name.toLowerCase()));
|
|
2738
|
+
findings.push({
|
|
2739
|
+
ruleId: 'FLOW-042',
|
|
2740
|
+
severity: 'block',
|
|
2741
|
+
message: unquoted !== raw && unquoted.split(',').every(name => scanCodeTypes.has(name.trim()))
|
|
2742
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türleri tırnaklı yazılmış: ${raw}. Bu alan DÜZ METİNDİR, ifade değil — args.value: '${unquoted}' yazın.`
|
|
2743
|
+
: /\{\{|\}\}|[()[\]]|\bstate\b|\bSteps\b/.test(raw)
|
|
2744
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türleri ifade olarak yazılmış: ${raw}. Bu alan DÜZ METİNDİR ve olduğu gibi okunur (platform 422 döner) — türleri virgülle yazın ya da hepsi için boş bırakın. Kullanılabilir: ${allowed}.`
|
|
2745
|
+
: unknown.includes('upc_a')
|
|
2746
|
+
? `'${flowStepLabel(step)}' scan-code adımında 'upc_a' sözlükte yok: UPC-A etiketi iki platformda 'ean13' türüyle ve on üç haneyle (başında 0) döner — 'ean13' yazın (platform 422 döner). Kullanılabilir: ${allowed}.`
|
|
2747
|
+
: lowered.length > 0
|
|
2748
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türü büyük harfle yazılmış: ${lowered.join(', ')}. Adlar küçük harftir ve birebir karşılaştırılır (platform 422 döner) — ${lowered.map(name => `'${name.toLowerCase()}'`).join(', ')} yazın.`
|
|
2749
|
+
: `'${flowStepLabel(step)}' scan-code adımının kod türü tanınmıyor: ${unknown.join(', ')}; platform 422 döner. Kullanılabilir: ${allowed} (boş → hepsi).`,
|
|
2750
|
+
guide: 'ngapps://guides/mobile-steps'
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
if (stray.length > 0) {
|
|
2754
|
+
findings.push({
|
|
2755
|
+
ruleId: 'FLOW-042',
|
|
2756
|
+
severity: names.length > 0 ? 'warn' : 'block',
|
|
2757
|
+
message: `'${flowStepLabel(step)}' scan-code adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür ve adım bütün kod türlerini okur. Türler args.value'ya virgüllü düz metin olarak yazılır (ör. 'qr,ean13'). Kullanılabilir: ${allowed}.`,
|
|
2758
|
+
guide: 'ngapps://guides/mobile-steps'
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
return findings;
|
|
2762
|
+
})
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
id: 'FLOW-041',
|
|
2766
|
+
module: 'flow',
|
|
2767
|
+
appliesTo: ['create', 'update'],
|
|
2768
|
+
sourceRef: "validation adımının kuralları StepManagementArgument.validations listesindedir (kök docs/adr/0014-validation-kurallari-cocuk-tabloda.md §5, 37 S-13): { id?, component (tasarım bileşeninin elementId'si; boş = null, sayfada olmayan 404), property, type, errorMessage (düz metin ya da t: anahtarı), settings (tek parametre, runtime Object.values(settings)[0]'ı okur) }. Backend type kümesini doğrulamaz; @ng-apps/flow validation.ts validate() tanımadığı tipte true döner, yani yazım hatalı kural sessizce GEÇER. Liste boşsa her çalıştırma success dalına gider. Komponent doğrulamasının { message, value } şekli bu sözleşmede YOKTUR ve 200 ile sessizce düşer",
|
|
2769
|
+
check: definition => flowAllSteps(definition)
|
|
2770
|
+
.filter(step => step.type === 'validation')
|
|
2771
|
+
.flatMap((step) => {
|
|
2772
|
+
const label = flowStepLabel(step);
|
|
2773
|
+
const validations = (step.args ?? {}).validations;
|
|
2774
|
+
if (validations !== undefined && validations !== null && !Array.isArray(validations)) {
|
|
2775
|
+
return [{
|
|
2776
|
+
ruleId: 'FLOW-041',
|
|
2777
|
+
severity: 'block',
|
|
2778
|
+
message: `'${label}' validation adımının args.validations alanı bir DİZİ olmalı: [{ component, property, type, errorMessage, settings }] (kök ADR 0014). Ör. [{ component: 'email', property: 'value', type: 'email', errorMessage: 't:validation.email', settings: {} }].`,
|
|
2779
|
+
guide: 'ngapps://guides/page'
|
|
2780
|
+
}];
|
|
2781
|
+
}
|
|
2782
|
+
const rules = (Array.isArray(validations) ? validations : []);
|
|
2783
|
+
if (rules.length === 0) {
|
|
2784
|
+
return [{
|
|
2785
|
+
ruleId: 'FLOW-041',
|
|
2786
|
+
severity: 'warn',
|
|
2787
|
+
message: `'${label}' validation adımında kural yok: args.validations boş olduğu için adım her çalıştırmada 'success' dalına gider, hiçbir alan denetlenmez. Kuralları args.validations'a yazın (kök ADR 0014).`,
|
|
2788
|
+
guide: 'ngapps://guides/page'
|
|
2789
|
+
}];
|
|
2790
|
+
}
|
|
2791
|
+
return rules.flatMap((rule, index) => {
|
|
2792
|
+
const type = typeof rule?.type === 'string' ? rule.type : '';
|
|
2793
|
+
const settings = rule?.settings && typeof rule.settings === 'object' ? Object.values(rule.settings) : [];
|
|
2794
|
+
const stray = ['message', 'value', 'name', 'isActive'].filter(key => rule?.[key] !== undefined);
|
|
2795
|
+
const at = `args.validations[${index}]`;
|
|
2796
|
+
const findings = [];
|
|
2797
|
+
if (!validationRuleTypes.has(type)) {
|
|
2798
|
+
findings.push({
|
|
2799
|
+
ruleId: 'FLOW-041',
|
|
2800
|
+
severity: 'block',
|
|
2801
|
+
message: `'${label}' validation adımında ${at}.type '${type}' tanınmıyor — beklenen: '${[...validationRuleTypes].join("' | '")}'. Platform tipi doğrulamadan kaydeder ve runtime tanımadığı tipi GEÇTİ sayar: kural hiçbir şeyi denetlemez.`,
|
|
2802
|
+
guide: 'ngapps://guides/page'
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
else if (!validationParameterlessTypes.has(type) && (settings.length === 0 || settings[0] === null || settings[0] === '')) {
|
|
2806
|
+
findings.push({
|
|
2807
|
+
ruleId: 'FLOW-041',
|
|
2808
|
+
severity: 'block',
|
|
2809
|
+
message: `'${label}' validation adımında ${at} ('${type}') parametresiz: runtime eşiği settings'in İLK değerinden okur (${type === 'regex' ? "{ pattern: '^\\\\d+$' }" : '{ value: 8 }'}); değer yoksa karşılaştırma undefined ile yapılır ve kural her çalıştırmada düşer.`,
|
|
2810
|
+
guide: 'ngapps://guides/page'
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
if (typeof rule?.property !== 'string' || rule.property.trim() === '') {
|
|
2814
|
+
findings.push({
|
|
2815
|
+
ruleId: 'FLOW-041',
|
|
2816
|
+
severity: 'block',
|
|
2817
|
+
message: `'${label}' validation adımında ${at}.property yok: runtime bileşenin hangi prop'unu okuyacağını bu alandan bilir (çoğunlukla 'value'). Yoksa okunan değer undefined olur.`,
|
|
2818
|
+
guide: 'ngapps://guides/page'
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
if (stray.length > 0) {
|
|
2822
|
+
findings.push({
|
|
2823
|
+
ruleId: 'FLOW-041',
|
|
2824
|
+
severity: 'warn',
|
|
2825
|
+
message: `'${label}' validation adımında ${at}.${stray.join('/')} alanı var: bu komponent doğrulamasının şeklidir, adım kuralının sözleşmesinde YOK ve platform onu 200 ile sessizce düşürür. Mesaj errorMessage'a, eşik settings'e yazılır — ör. { component: 'sifre', property: 'value', type: 'min-length', errorMessage: 'En az 8 karakter', settings: { value: 8 } }.`,
|
|
2826
|
+
guide: 'ngapps://guides/page'
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
return findings;
|
|
2830
|
+
});
|
|
2831
|
+
})
|
|
2832
|
+
},
|
|
2683
2833
|
{
|
|
2684
2834
|
id: 'FLOW-038',
|
|
2685
2835
|
module: 'flow',
|
|
@@ -2741,7 +2891,7 @@ export const policyRules = [
|
|
|
2741
2891
|
id: 'FLOW-030',
|
|
2742
2892
|
module: 'flow',
|
|
2743
2893
|
appliesTo: ['create', 'update'],
|
|
2744
|
-
sourceRef: "Yerel bildirim adımlarının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0003-cihaz-yetenegi-adimlari.md): notification-schedule name = kimlik (
|
|
2894
|
+
sourceRef: "Yerel bildirim adımlarının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0003-cihaz-yetenegi-adimlari.md): notification-schedule name = kimlik ve title = başlık METİN ŞABLONU (yalnız {{ }} içi çözülür — 2026-10-08, FLOW-022; aynı kimlik eskisinin yerini alır), message ve value İFADE, path sayfa (düz metin); notification-cancel name = kimlik, metin şablonu ('*' hepsi). Zamanın türü value'nun sonucunun şeklinden okunur (@ng-apps/flow notificationTrigger): 'HH:mm' her gün, { weekday: 1-7, time: 'HH:mm' } her hafta (1 = Pazartesi), ISO tarih-saat bir kez. Eksik name/value backend FlowDtoValidator'da 400 alır; id/at/daily/weekly/body gibi anahtarlar sözleşmede YOKTUR ve platform onları 200 ile sessizce düşürür. GUID değerli args.id platformun kendi StepArgument kimliğidir (okunan akıştan geri yazım) ve işaretlenmez — 37 S-21",
|
|
2745
2895
|
check: definition => flowAllSteps(definition)
|
|
2746
2896
|
.filter(step => step.type === 'notification-schedule' || step.type === 'notification-cancel')
|
|
2747
2897
|
.flatMap((step) => {
|
|
@@ -2749,13 +2899,16 @@ export const policyRules = [
|
|
|
2749
2899
|
const text = (key) => typeof args[key] === 'string' ? String(args[key]).trim() : '';
|
|
2750
2900
|
const findings = [];
|
|
2751
2901
|
const schedule = step.type === 'notification-schedule';
|
|
2902
|
+
// 37 S-21: StepArgument bir AggregateRoot<Guid>'dir; okunan akış args.id'yi GUID olarak
|
|
2903
|
+
// taşır ve geri yazımda o platformun kimliğidir. Yalnız GUID olmayan id ajanın uydurmasıdır.
|
|
2752
2904
|
const stray = ['id', 'identifier', 'at', 'daily', 'weekly', 'time', 'body', 'text']
|
|
2753
|
-
.filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '')
|
|
2905
|
+
.filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '')
|
|
2906
|
+
.filter(key => key !== 'id' || !isGuid(String(args.id)));
|
|
2754
2907
|
if (!text('name')) {
|
|
2755
2908
|
findings.push({
|
|
2756
2909
|
ruleId: 'FLOW-030',
|
|
2757
2910
|
severity: 'block',
|
|
2758
|
-
message: `'${flowStepLabel(step)}' ${step.type} adımının kimliği yok: args.name
|
|
2911
|
+
message: `'${flowStepLabel(step)}' ${step.type} adımının kimliği yok: args.name metin olarak yazılmalı (ör. 'sulama'; kayıt başına 'ilac-{{ item.id }}')${schedule ? ' — aynı kimlikle yeniden planlama eskisinin yerini alır' : " — '*' bütün planlı bildirimleri siler"}. Platform eksik kimliği 400 ile reddeder.`,
|
|
2759
2912
|
guide: 'ngapps://guides/mobile'
|
|
2760
2913
|
});
|
|
2761
2914
|
}
|
|
@@ -3262,6 +3415,46 @@ export const policyRules = [
|
|
|
3262
3415
|
return findings;
|
|
3263
3416
|
}
|
|
3264
3417
|
},
|
|
3418
|
+
/**
|
|
3419
|
+
* WF-035 — 38 S-10: `query.fail` gateway'in `gatewayOnError` adımına bağlanınca bozuk id'li
|
|
3420
|
+
* istek 500 `JavaScriptException: error is not defined` döndü. Hedef adım global-error dalının
|
|
3421
|
+
* da hedefi olabilir; o yoldan gelince `error` kurulur, bağlı fail yolundan gelince kurulmaz —
|
|
3422
|
+
* bu yüzden hedefin global-error hedefi olması uyarıyı susturmaz.
|
|
3423
|
+
*/
|
|
3424
|
+
{
|
|
3425
|
+
id: 'WF-035',
|
|
3426
|
+
module: 'workflow',
|
|
3427
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
3428
|
+
sourceRef: "IStep.cs FailedOutput: global-error dalına yalnız fail dalı BAĞLANMAMIŞSA geçilir ve `error` değişkenini yalnız ActivityContext.EnterGlobalErrorHandling kurar. Bağlı fail dalında sonraki adımlar patlayan adımın hatasını kendi değişkeninden okur: ActivityContext.CreateJavascriptEngine her adımı adıyla { data, meta, error: { code, message, detail, type, cause } } olarak koyar. Ölçüm 38 S-10 (2026-09-30): query.fail → gatewayOnError, bozuk id → 500 'error is not defined'",
|
|
3429
|
+
check: (definition, context) => {
|
|
3430
|
+
const steps = workflowSteps(workflowBody(definition, context.workflowKind));
|
|
3431
|
+
const byId = new Map(steps.filter(step => step.id).map(step => [String(step.id), step]));
|
|
3432
|
+
const readsGlobalError = /(?<![\w$.])error\s*(?:\?\.|\.(?=[A-Za-z_$])|\[)/;
|
|
3433
|
+
const strings = (value) => {
|
|
3434
|
+
if (typeof value === 'string')
|
|
3435
|
+
return [value];
|
|
3436
|
+
if (Array.isArray(value))
|
|
3437
|
+
return value.flatMap(strings);
|
|
3438
|
+
if (value && typeof value === 'object')
|
|
3439
|
+
return Object.values(value).flatMap(strings);
|
|
3440
|
+
return [];
|
|
3441
|
+
};
|
|
3442
|
+
return steps.flatMap((step) => asArray(step.branches)
|
|
3443
|
+
.map(branch => branch)
|
|
3444
|
+
.filter(branch => branch?.type === 'fail' && Boolean(branch.nextStepId))
|
|
3445
|
+
.map(branch => byId.get(String(branch.nextStepId)))
|
|
3446
|
+
.filter((target) => Boolean(target) && strings(target.rule).some(text => readsGlobalError.test(text)))
|
|
3447
|
+
.map(target => {
|
|
3448
|
+
const name = stepLabel(step);
|
|
3449
|
+
return {
|
|
3450
|
+
ruleId: 'WF-035',
|
|
3451
|
+
severity: 'warn',
|
|
3452
|
+
message: `'${name}' adımının fail dalı '${stepLabel(target)}' adımına bağlı ve o adım 'error.*' okuyor. 'error' YALNIZ global-error dalında (fail dalı bağlanmamışken) kurulur; bağlı fail dalından gelindiğinde tanımsızdır ve adım 500 'JavaScriptException: error is not defined' ile düşer. Patlayan adımın hatasını kendi değişkeninden okuyun: ${name}.error.message · ${name}.error.code (ayrıca detail, type, cause; çıktı ${name}.data). Ya da fail dalını bağlamayın: hata global-error dalına düşer ve 'error' orada kurulur. Global-error hedefiyle aynı adımı paylaşmak yerine fail dalına ayrı bir response adımı bağlayın (ör. 404).`,
|
|
3453
|
+
guide: 'ngapps://guides/workflow'
|
|
3454
|
+
};
|
|
3455
|
+
}));
|
|
3456
|
+
}
|
|
3457
|
+
},
|
|
3265
3458
|
{
|
|
3266
3459
|
id: 'WF-021',
|
|
3267
3460
|
module: 'workflow',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { motionFieldBinding, resolveMotion } from './motionResolve.js';
|
|
1
2
|
import { TRANSFORM_RANGES, rawProp, resolveNodeStyleLayers, transformValue } from './styleResolve.js';
|
|
2
3
|
import { mergeTypeScale, resolveTheme } from './themeResolve.js';
|
|
3
4
|
/* ---- css-value.ts ---- */
|
|
@@ -440,6 +441,8 @@ const composeSurface = (surface, shell, theme, context) => {
|
|
|
440
441
|
context.warnings?.push(`surfaceStyle.${key}: mobilde karşılığı yok, atlandı`);
|
|
441
442
|
}
|
|
442
443
|
};
|
|
444
|
+
/** Tipografi grubunun `composeTypography`'nin okuduğu alanları (frontend derleyicisiyle parite: `test/style-resolve-native.test.ts`). */
|
|
445
|
+
export const TYPOGRAPHY_KEYS = new Set(['textColor', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'textAlign', 'letterSpacing', 'lineHeight']);
|
|
443
446
|
const composeTypography = (typography, text, theme, context) => {
|
|
444
447
|
const color = asText(typography.textColor, theme, context.warnings, 'typographyStyle.textColor');
|
|
445
448
|
if (color)
|
|
@@ -510,20 +513,101 @@ export function composeNativeStyle(input, theme) {
|
|
|
510
513
|
* Bir düğümün mobil çizerdeki stili (`NativeRenderer` → `nativeNodeStyle`, `renderer.functions.ts`). Temanın katmanı
|
|
511
514
|
* (`componentStyles[tip]` ← `$preset`) web'in çözücüsünden gelir ve TABANDIR; panel grupları üstüne, düğümün serbest
|
|
512
515
|
* `style`'ı son söz; `$preset` işareti RN stiline inmez (plans/mobil-plantcare-acigi f-16 — 2026-09-28'e dek çizer bu
|
|
513
|
-
* katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok
|
|
516
|
+
* katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok: grubu bağlı düğümde
|
|
517
|
+
* grup (tasarım yedeği taşısa da) stile girmez; yüzey ve tipografi grubunun TEK alanı bağlıysa (`{ type: 'code', ref }`,
|
|
518
|
+
* frontend ADR 0020) o alan stile girmez ve `bound`'da `<grup>.<alan>` diye listelenir — yedeği çizerin verdiği değer
|
|
519
|
+
* değildir (yedeksiz sarmal `"[object Object]"` yazılıyordu, inceleme 2026-10-09). Çizerin TANIMADIĞI alan bağlı olsa da
|
|
520
|
+
* bağlı alan değildir (`bound`'a girmez; yüzeyde derleyicinin "karşılığı yok" uyarısı düşer). `ref`'i boş `code` sarmalı
|
|
521
|
+
* bağ değildir: verisi ham değer okunur; verisiz sarmal alanı yok sayar (çizer `mapSchemaPropToRawProp` ile aynı).
|
|
522
|
+
*
|
|
523
|
+
* `motion`, kabuğun `motionStyle`'dan uyguladığı harekettir (`NativeRenderer` → `nodeMotionOf`, `/dev` ve cihaz):
|
|
524
|
+
* yalnız hareketi olan düğümde yazılır. Kanvas ve "hareketi azalt" onu dondurur — rapor dondurulmamış çözümdür.
|
|
514
525
|
*/
|
|
515
526
|
export function nodeNativeStyle(node, theme, source) {
|
|
516
527
|
const props = node.props;
|
|
528
|
+
const bound = [];
|
|
517
529
|
const group = (name) => {
|
|
518
530
|
const value = rawProp(props, name);
|
|
519
531
|
return isPlainObject(value) ? value : null;
|
|
520
532
|
};
|
|
533
|
+
const evaluated = (name, known) => {
|
|
534
|
+
const value = isBound(props?.[name]) ? null : group(name);
|
|
535
|
+
if (!value)
|
|
536
|
+
return value;
|
|
537
|
+
const kept = { ...value };
|
|
538
|
+
for (const [field, wrapper] of fieldWrappers(props?.[name])) {
|
|
539
|
+
if (!isExpression(wrapper)) {
|
|
540
|
+
if (!('data' in wrapper))
|
|
541
|
+
delete kept[field];
|
|
542
|
+
}
|
|
543
|
+
else if (known.has(field)) {
|
|
544
|
+
delete kept[field];
|
|
545
|
+
bound.push(`${name}.${field}`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// Tanınmayan alan: sarmalı değer olarak kalır ki derleyicinin kendi "karşılığı yok" uyarısı düşsün.
|
|
549
|
+
kept[field] = wrapper;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return kept;
|
|
553
|
+
};
|
|
521
554
|
const layers = resolveNodeStyleLayers(rawProp(props, 'style'), source, node.type);
|
|
522
|
-
|
|
555
|
+
const composed = composeNativeStyle({
|
|
523
556
|
themeStyle: layers.theme,
|
|
524
|
-
surfaceStyle:
|
|
525
|
-
typographyStyle:
|
|
557
|
+
surfaceStyle: evaluated('surfaceStyle', SURFACE_KEYS),
|
|
558
|
+
typographyStyle: evaluated('typographyStyle', TYPOGRAPHY_KEYS),
|
|
526
559
|
hoverStyle: group('hoverStyle'),
|
|
527
560
|
style: layers.own
|
|
528
561
|
}, theme);
|
|
562
|
+
const style = bound.length > 0 ? { ...composed, bound } : composed;
|
|
563
|
+
const resolved = nodeMotion(props?.motionStyle);
|
|
564
|
+
if (!resolved)
|
|
565
|
+
return style;
|
|
566
|
+
const { warnings, ...motion } = resolved;
|
|
567
|
+
const reported = { ...style, warnings: [...style.warnings, ...warnings] };
|
|
568
|
+
// Hareket çıkmadıysa (yalnız atlanan alanlar) `motion` yazılmaz; uyarıları yine listededir.
|
|
569
|
+
return Object.keys(motion).length > 0 ? { ...reported, motion } : reported;
|
|
570
|
+
}
|
|
571
|
+
const RUNNING = { design: false, reduceMotion: false };
|
|
572
|
+
/**
|
|
573
|
+
* İfadeye bağlı GRUP zarfı (`code`, `translation`): değeri çalışma anında oluşur. Zarfın `data`'sı tasarım zamanı
|
|
574
|
+
* yedeğidir (kanvasın gösterdiği değer), çizerin uyguladığı değer değil — rapora girmez.
|
|
575
|
+
*/
|
|
576
|
+
const isBound = (value) => isPlainObject(value) && (value.type === 'code' || value.type === 'translation');
|
|
577
|
+
/**
|
|
578
|
+
* Statik grubun sarmallı alanları (`{ type, data?, ref? }`), ham hâlleriyle. Tasarım şeması statik sarmala
|
|
579
|
+
* `type: 'static'` yazar, yayınlanan sayfanın yükü yazmaz (backend `AppProp`, 2026-10-04'ten beri; statik alanda
|
|
580
|
+
* `type: null` gelir) — ikisi de statiktir (frontend `inner-bindings.ts` `fieldsOf` ile aynı kural). Yalnız yazılı
|
|
581
|
+
* `type`'ı tanıyan ilk sürüm gerçek yükte bağlı alanı hiç görmüyordu (ölçüldü 2026-10-09).
|
|
582
|
+
*/
|
|
583
|
+
const fieldWrappers = (prop) => Object.entries(isPlainObject(prop) && (prop.type ?? 'static') === 'static' && isPlainObject(prop.data) ? prop.data : {})
|
|
584
|
+
.filter((entry) => isPlainObject(entry[1]) && typeof entry[1].type === 'string');
|
|
585
|
+
/** İfadeye bağlı alan sarmalı: çizer onu çalışma anında değerlendirir (frontend ADR 0020, `inner-bindings.ts` `isCodeBinding`). */
|
|
586
|
+
const isExpression = (wrapper) => wrapper.type === 'code' && typeof wrapper.ref === 'string' && wrapper.ref !== '';
|
|
587
|
+
/**
|
|
588
|
+
* Düğümün ham `motionStyle` prop'undan (zarfıyla) çalışma anı çözümü. Bağlı grup atlanır. Grubun içindeki tek alan
|
|
589
|
+
* ifadeye bağlıysa (`code`) değeri çalışma anında oluşur: çözüme girmez, `bound`'da adıyla listelenir — tasarım yedeği
|
|
590
|
+
* çizerin uyguladığı değer değildir. Çözülmeyen yazım (şablon metni, `code` dışı bağ) uyarısıyla listenin başındadır;
|
|
591
|
+
* çizerin ham değer olarak okuduğu verisi varsa çözüme o girer (`motionFieldBinding`).
|
|
592
|
+
*/
|
|
593
|
+
function nodeMotion(prop) {
|
|
594
|
+
if (!isPlainObject(prop) || isBound(prop) || !isPlainObject(prop.data))
|
|
595
|
+
return null;
|
|
596
|
+
const unresolved = [];
|
|
597
|
+
const bound = [];
|
|
598
|
+
const written = Object.fromEntries(Object.entries(prop.data).flatMap(([field, value]) => {
|
|
599
|
+
const binding = motionFieldBinding(field, value);
|
|
600
|
+
if (!binding)
|
|
601
|
+
return [[field, value]];
|
|
602
|
+
if (binding.kind === 'expression') {
|
|
603
|
+
bound.push(field);
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
unresolved.push(binding.warning);
|
|
607
|
+
return binding.fallback === undefined ? [] : [[field, { type: 'static', data: binding.fallback }]];
|
|
608
|
+
}));
|
|
609
|
+
const resolved = resolveMotion(rawProp({ motionStyle: { type: 'static', data: written } }, 'motionStyle'), RUNNING);
|
|
610
|
+
if (!resolved && unresolved.length === 0 && bound.length === 0)
|
|
611
|
+
return null;
|
|
612
|
+
return { ...resolved, ...(bound.length > 0 ? { bound } : {}), warnings: [...unresolved, ...(resolved?.warnings ?? [])] };
|
|
529
613
|
}
|
|
@@ -20,11 +20,20 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
20
20
|
// Seçeneklerin arandığı kök: web'de kutunun kabı (`scope`), mobilde belge (liste `Modal` katmanında).
|
|
21
21
|
const optionRoot = captureTarget.optionsOutsideNode ? 'document' : 'scope';
|
|
22
22
|
if (action.action === 'click') {
|
|
23
|
+
// `<elementId>__press` basış yüzeyinin KENDİSİDİR (frontend K2 sözleşmesi): içinde düğme taşıyan yüzeyde
|
|
24
|
+
// (`flip-card` — web'de iç içe `<button>` olmasın diye `div` çizilir) ilk düğmeye inilmez.
|
|
25
|
+
const pressSurface = String(action.target ?? '').endsWith(`${INTERNAL_ROLE_SEPARATOR}press`);
|
|
23
26
|
return `(function () {
|
|
24
27
|
var count = performance.getEntriesByType('resource').length;
|
|
25
28
|
var node = document.querySelector(${selector});
|
|
26
29
|
if (!node) return { found: false, applied: false, requests: count };
|
|
27
|
-
|
|
30
|
+
// g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
|
|
31
|
+
// Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
|
|
32
|
+
// çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
|
|
33
|
+
// denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
|
|
34
|
+
var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
|
|
35
|
+
var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
|
|
36
|
+
: (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
|
|
28
37
|
// S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
|
|
29
38
|
// ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.
|
|
30
39
|
if (target.closest && target.closest('[role="tablist"]') && !node.matches('[role="tab"]')) {
|
|
@@ -354,15 +363,32 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
354
363
|
if (!field) return finish({ found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count });
|
|
355
364
|
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
356
365
|
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
366
|
+
// K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
|
|
367
|
+
// bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
|
|
368
|
+
// (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
|
|
369
|
+
var focusField = function () {
|
|
370
|
+
field.focus();
|
|
371
|
+
if (document.activeElement === field) return;
|
|
372
|
+
field.dispatchEvent(new FocusEvent('focus'));
|
|
373
|
+
field.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
|
374
|
+
};
|
|
375
|
+
var blurField = function () {
|
|
376
|
+
if (document.activeElement === field) { field.blur(); return; }
|
|
377
|
+
field.dispatchEvent(new FocusEvent('blur'));
|
|
378
|
+
field.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
|
379
|
+
};
|
|
380
|
+
focusField();
|
|
357
381
|
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
358
382
|
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
359
383
|
if (field.checked !== shouldCheck) field.click();
|
|
384
|
+
blurField();
|
|
360
385
|
return finish({ found: true, applied: true, requests: count });
|
|
361
386
|
}
|
|
362
387
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
363
388
|
setter.call(field, ${value});
|
|
364
389
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
365
390
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
391
|
+
blurField();
|
|
366
392
|
return finish({ found: true, applied: true, requests: count });
|
|
367
393
|
})()`;
|
|
368
394
|
}
|
|
@@ -2,6 +2,7 @@ import { PlatformApiError } from '../errors.js';
|
|
|
2
2
|
import { parseDomSummary } from '../domSummary.js';
|
|
3
3
|
import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../../knowledge/componentCatalog.js';
|
|
4
4
|
import { logStep } from '../../observability/trace.js';
|
|
5
|
+
import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfileRoot } from './persistent-browser.js';
|
|
5
6
|
import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
|
|
6
7
|
import { domSummaryScript } from './summary.js';
|
|
7
8
|
import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
|
|
@@ -280,22 +281,18 @@ export class StudioScreenshotter {
|
|
|
280
281
|
catch {
|
|
281
282
|
throw new PlatformApiError('playwright-core paketi yüklü değil — zeyrek-mcp dizininde `npm install playwright-core` çalıştırın, ardından tarayıcı için `npx playwright-core install chromium`.');
|
|
282
283
|
}
|
|
283
|
-
logStep('tarayıcı →',
|
|
284
|
+
logStep('tarayıcı →', `chromium başlatılıyor (ilk çağrı — sonraki çağrılar önbellekteki tarayıcıyı kullanır) · profil ${screenshotProfileRoot()}`);
|
|
285
|
+
const open = persistentContextOpener(chromium, this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {});
|
|
286
|
+
const browser = new PersistentScreenshotBrowser(open);
|
|
284
287
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
...(this.config.screenshotBrowserPath ? { executablePath: this.config.screenshotBrowserPath } : {})
|
|
288
|
-
});
|
|
288
|
+
// İlk bağlam burada açılır ki başlatma hatası eskisi gibi `launch()`'tan dönsün.
|
|
289
|
+
await (await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 })).close();
|
|
289
290
|
}
|
|
290
291
|
catch (firstError) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
const message = firstError instanceof Error ? firstError.message : String(firstError);
|
|
296
|
-
throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
|
|
297
|
-
}
|
|
292
|
+
const message = firstError instanceof Error ? firstError.message : String(firstError);
|
|
293
|
+
throw new PlatformApiError(`Chromium başlatılamadı: ${message}. \`npx playwright-core install chromium\` ile indirin; alternatif olarak sistemdeki tarayıcının yolunu SCREENSHOT_BROWSER_PATH ile verin (alpine imajında \`apk add chromium\` + /usr/bin/chromium-browser).`);
|
|
298
294
|
}
|
|
295
|
+
return browser;
|
|
299
296
|
}
|
|
300
297
|
/**
|
|
301
298
|
* Bekleme stratejisi bilinçli olarak `networkidle` DEĞİL: Studio'nun kök layout'u
|
|
@@ -331,7 +328,7 @@ export class StudioScreenshotter {
|
|
|
331
328
|
before.note = action.target === '__tabs' || action.target.startsWith(`__tabs${INTERNAL_ROLE_SEPARATOR}`)
|
|
332
329
|
? `'${action.target}' sayfada yok: bu sayfa sekme çubuğunda değil ya da uygulamada sekmeler kapalı (application_get → properties.mobile.tabs). Sekme öğesinin kimliği __tabs__item-<sayfa slug'ı>.`
|
|
333
330
|
: action.target.includes(INTERNAL_ROLE_SEPARATOR)
|
|
334
|
-
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}.
|
|
331
|
+
? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
|
|
335
332
|
: `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
|
|
336
333
|
}
|
|
337
334
|
// Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
|