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