@appaflytech/zeyrek-ai-mcp 0.0.6 → 0.0.8
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/.env.example +2 -2
- package/.env.production.example +8 -0
- package/README.md +9 -8
- package/dist/config.js +18 -3
- package/dist/knowledge/backendConstraints.js +88 -0
- package/dist/knowledge/componentCatalog.js +415 -68
- package/dist/knowledge/componentEvents.js +120 -25
- package/dist/knowledge/functionGuards.js +134 -0
- package/dist/knowledge/normalize.js +512 -25
- package/dist/knowledge/outlines.js +31 -0
- package/dist/knowledge/policy.js +14 -5
- package/dist/knowledge/propTypes.js +5 -4
- package/dist/knowledge/rules.js +453 -6
- package/dist/knowledge/styleResolve.js +6 -1
- package/dist/platform/csv.js +123 -0
- package/dist/platform/errors.js +62 -3
- package/dist/platform/http.js +11 -2
- package/dist/platform/responses.js +16 -4
- package/dist/platform/screenshot.js +98 -9
- package/dist/platform/strictArguments.js +32 -0
- package/dist/platform/writeDiff.js +6 -3
- package/dist/resources/examples/flow-call-function.json +66 -0
- package/dist/resources/examples/function-total.json +70 -0
- package/dist/resources/examples/microfunction-node-sum.json +56 -0
- package/dist/resources/examples/page-dynamic-form.json +1 -1
- package/dist/resources/examples/page-sign-in.json +2 -2
- package/dist/resources/guides/component.md +14 -2
- package/dist/resources/guides/entity.md +321 -17
- package/dist/resources/guides/expressions.md +9 -0
- package/dist/resources/guides/functions.md +262 -0
- package/dist/resources/guides/getting-started.md +7 -0
- package/dist/resources/guides/layout.md +6 -0
- package/dist/resources/guides/merge.md +231 -0
- package/dist/resources/guides/microfunction.md +166 -0
- package/dist/resources/guides/microservice.md +166 -7
- package/dist/resources/guides/modify-existing.md +23 -0
- package/dist/resources/guides/page.md +101 -22
- package/dist/resources/guides/translation.md +35 -10
- package/dist/resources/guides/workflow.md +51 -2
- package/dist/resources/schema-fingerprint.json +4 -4
- package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
- package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +3 -0
- package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
- package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
- package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
- package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
- package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
- package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +123 -2
- package/dist/resources/schemas/CreateWorkflowCommand.json +17 -3
- package/dist/resources/schemas/DataJobDto.json +267 -0
- package/dist/resources/schemas/ExportRowsCommand.json +172 -0
- package/dist/resources/schemas/Filter.json +3 -1
- package/dist/resources/schemas/GetRowsQuery.json +3 -1
- package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
- package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
- package/dist/resources/schemas/ImportPlanDto.json +260 -0
- package/dist/resources/schemas/ImportSampleDto.json +90 -0
- package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +123 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +17 -3
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
- package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
- package/dist/resources/schemas/RunImportCommand.json +62 -0
- package/dist/resources/schemas/StepDto.json +17 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +3 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +3 -1
- package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
- package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
- package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +123 -2
- package/dist/resources/schemas/UpdateWorkflowCommand.json +17 -3
- package/dist/server.js +17 -4
- package/dist/tools/activities.js +40 -0
- package/dist/tools/automations.js +14 -5
- package/dist/tools/common.js +16 -0
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/environments.js +52 -0
- package/dist/tools/files.js +51 -50
- package/dist/tools/flows.js +104 -5
- package/dist/tools/functions.js +331 -0
- package/dist/tools/importSchemas.js +79 -0
- package/dist/tools/imports.js +241 -0
- package/dist/tools/layouts.js +4 -4
- package/dist/tools/merges.js +113 -0
- package/dist/tools/microfunctions.js +155 -0
- package/dist/tools/microservices.js +112 -9
- package/dist/tools/pages.js +188 -66
- package/dist/tools/platformInfo.js +4 -1
- package/dist/tools/queries.js +72 -6
- package/dist/tools/revisions.js +94 -0
- package/dist/tools/rows.js +342 -22
- package/dist/tools/screenWrite.js +11 -2
- package/dist/tools/templates.js +32 -5
- package/dist/tools/workflows.js +28 -6
- package/package.json +3 -1
package/dist/knowledge/rules.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { applyPolicyMode } from './policy.js';
|
|
2
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
1
3
|
import { reservedTenantTables } from './reservedTables.js';
|
|
2
4
|
import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
|
|
5
|
+
import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
|
|
3
6
|
import { backendPolicyRules } from './backendConstraints.js';
|
|
4
7
|
const terminalStepTypes = new Set(['response', 'convert', 'revert']);
|
|
5
8
|
const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
|
|
@@ -14,6 +17,7 @@ const stepKindMatrix = {
|
|
|
14
17
|
workflow: ['workflow', 'automation', 'middleware'],
|
|
15
18
|
email: ['workflow', 'automation', 'middleware'],
|
|
16
19
|
'file-upload': ['workflow', 'automation', 'middleware'],
|
|
20
|
+
microfunction: ['workflow', 'automation', 'middleware'],
|
|
17
21
|
variable: ['middleware'],
|
|
18
22
|
'set-user': ['middleware'],
|
|
19
23
|
setcookie: ['middleware'],
|
|
@@ -97,8 +101,19 @@ const flowBranchNames = {
|
|
|
97
101
|
start: ['always'],
|
|
98
102
|
microservice: ['always', 'success'],
|
|
99
103
|
confirm: ['yes', 'no', 'always'],
|
|
104
|
+
function: ['always'],
|
|
100
105
|
end: []
|
|
101
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Gateway'in tanıdığı sayfa akışı adım tipleri — frontend `FlowTaskType` enum'unun aynası
|
|
109
|
+
* (`ng-apps-flow/src/flow-execution.enums.ts`). `flowBranchNames[type] ?? ['always']` varsayımı
|
|
110
|
+
* her tipi kabul ettiği için sürüklenme ancak bu AÇIK listeyle ölçülür
|
|
111
|
+
* (`test/flow-step-parity.test.ts`). Frontend'e yeni tip girince buraya da yazılır.
|
|
112
|
+
*/
|
|
113
|
+
export const knownFlowStepTypes = new Set([
|
|
114
|
+
'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
|
|
115
|
+
'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function'
|
|
116
|
+
]);
|
|
102
117
|
const flowFreeBranchTypes = new Set(['condition']);
|
|
103
118
|
/** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
|
|
104
119
|
const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
|
|
@@ -110,7 +125,7 @@ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
|
|
|
110
125
|
// Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
|
|
111
126
|
const flowLiteralTextFields = new Map([
|
|
112
127
|
['alert', ['message', 'description']],
|
|
113
|
-
['confirm', ['message']]
|
|
128
|
+
['confirm', ['message', 'title', 'confirmText', 'cancelText']]
|
|
114
129
|
]);
|
|
115
130
|
const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
|
|
116
131
|
// Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
|
|
@@ -205,7 +220,22 @@ function flowCodeExpressions(step) {
|
|
|
205
220
|
}
|
|
206
221
|
return expressions;
|
|
207
222
|
}
|
|
208
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Kod ifadesi taşıyan her alan: `flowCodeExpressions` + condition kurallarının `expression`ı.
|
|
225
|
+
* FLOW-025 (`Functions.<ad>` çağrı taraması) bunu tarar; FLOW-011'in taradığı küme değişmez.
|
|
226
|
+
*/
|
|
227
|
+
export function flowExpressionSites(step) {
|
|
228
|
+
const shaped = step;
|
|
229
|
+
const sites = flowCodeExpressions(shaped);
|
|
230
|
+
if (shaped.type === 'condition') {
|
|
231
|
+
for (const rule of asArray((shaped.args ?? {}).rules).filter((rule) => Boolean(rule) && typeof rule === 'object')) {
|
|
232
|
+
if (typeof rule.expression === 'string' && rule.expression.trim() !== '')
|
|
233
|
+
sites.push({ expression: rule.expression, origin: `rules['${String(rule.id ?? '?')}'].expression` });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return sites;
|
|
237
|
+
}
|
|
238
|
+
const writeOperations = new Set(['create', 'update', 'delete', 'upsert', 'bulk-create', 'bulk-update', 'bulk-delete']);
|
|
209
239
|
/** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
|
|
210
240
|
const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
|
|
211
241
|
const rolesSlugFieldId = '0199c35d-cfc8-77d4-9c20-29b857153147';
|
|
@@ -224,7 +254,7 @@ function writesRolesWithoutSlug(entityId, parameters) {
|
|
|
224
254
|
}
|
|
225
255
|
const rolesSlugMessage = "Built-in 'roles' entity'sine slug YAZMADAN rol satırı ÜRETİLİYOR (operation create/upsert). Insert 200 döner, satır role_list'te görünür ve page.roleIds ile bağlanır — ama yetkilendirme SLUG karşılaştırır: platformun get-user sorgusu roles+user_roles join'inden slug dizisi üretir ve Authorized(user.roles) o rol için hiçbir zaman tutmaz. fields'a slug'ı ekleyin ('super-admin' slug'ını vermeyin — platform süper yöneticiyi o slug ile arar). Upsert'te slug'ı İFADEYLE üretmeyin: UPDATE yolunda ifade yeniden değerlendirilir ve var olan rolün slug'ı sessizce değişir (MS-016) — üretilen slug'ı yalnız create ucunda yazın. Rolü kullanıcıya bağlamak ayrı bir user_roles satırıdır.";
|
|
226
256
|
const contractKeys = ['returnDtoId', 'parameterDtoId', 'returnBehavior', 'parameterBehavior'];
|
|
227
|
-
const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith']);
|
|
257
|
+
const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith', 'isnull', 'isnotnull']);
|
|
228
258
|
function flattenFilterTree(filters) {
|
|
229
259
|
return filters
|
|
230
260
|
.filter((filter) => Boolean(filter) && typeof filter === 'object')
|
|
@@ -316,6 +346,40 @@ function unreadSelectFields(step, steps) {
|
|
|
316
346
|
.map(field => String(field.fieldSlug ?? field.slug ?? ''))
|
|
317
347
|
.filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
|
|
318
348
|
}
|
|
349
|
+
const SQL_COMMENT_PATTERN = /--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
350
|
+
const QUERY_PARAMETER_TOKEN = String.raw `(?:\{\{\s*([A-Za-z_]\w*)\s*\}\}|(?<![:@\w'])@([A-Za-z_]\w*))(?:\s*::\s*\w+)*`;
|
|
351
|
+
const EMPTY_STRING_COMPARISON_PATTERN = new RegExp(`${QUERY_PARAMETER_TOKEN}\\s*=\\s*(?<!')''(?!')|(?<!')''\\s*=\\s*${QUERY_PARAMETER_TOKEN}`, 'g');
|
|
352
|
+
/**
|
|
353
|
+
* `rawQuery`'de bir parametreyi boş string'le EŞİTLİK üzerinden karşılaştıran yerlerin parametre
|
|
354
|
+
* adları (`@p = ''`, `@p::text = ''`, `'' = {{p}}`). Yorumlar önce atılır — platform da yorum
|
|
355
|
+
* içindeki belirteci parametre saymıyor. Yalnız `=` aranır: `@p <> ''` boş değerde NULL (yanlış)
|
|
356
|
+
* verir ve "doluysa" niyetiyle yazıldığında sonuç zaten doğrudur.
|
|
357
|
+
*/
|
|
358
|
+
function emptyStringComparisons(rawQuery) {
|
|
359
|
+
if (typeof rawQuery !== 'string')
|
|
360
|
+
return [];
|
|
361
|
+
const names = new Set();
|
|
362
|
+
for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(EMPTY_STRING_COMPARISON_PATTERN)) {
|
|
363
|
+
const name = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
364
|
+
if (name !== undefined)
|
|
365
|
+
names.add(name);
|
|
366
|
+
}
|
|
367
|
+
return [...names];
|
|
368
|
+
}
|
|
369
|
+
/** `rawQuery`'nin başvurduğu parametre adları (`@p`, `{{p}}`), yorumlar atılarak; ilk görülme sırasıyla. */
|
|
370
|
+
export function rawQueryParameterNames(rawQuery) {
|
|
371
|
+
if (typeof rawQuery !== 'string')
|
|
372
|
+
return [];
|
|
373
|
+
const names = new Set();
|
|
374
|
+
for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(new RegExp(QUERY_PARAMETER_TOKEN, 'g'))) {
|
|
375
|
+
const name = match[1] ?? match[2];
|
|
376
|
+
if (name !== undefined)
|
|
377
|
+
names.add(name);
|
|
378
|
+
}
|
|
379
|
+
return [...names];
|
|
380
|
+
}
|
|
381
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
382
|
+
const functionsGuide = 'ngapps://guides/functions';
|
|
319
383
|
export const policyRules = [
|
|
320
384
|
{
|
|
321
385
|
id: 'MS-001',
|
|
@@ -386,7 +450,7 @@ export const policyRules = [
|
|
|
386
450
|
id: 'MS-016',
|
|
387
451
|
module: 'microservice',
|
|
388
452
|
appliesTo: ['create', 'update', 'preview'],
|
|
389
|
-
sourceRef: "İK koşusu M-3: operation 'upsert' olan uçta expression'la yazılan alan UPDATE yolunda yeniden değerlendiriliyor; Incremental sicil alanı EMP-0005 → EMP-0006 diye sessizce değişti.
|
|
453
|
+
sourceRef: "İK koşusu M-3: operation 'upsert' olan uçta expression'la yazılan alan UPDATE yolunda yeniden değerlendiriliyor; Incremental sicil alanı EMP-0005 → EMP-0006 diye sessizce değişti. Alan bazlı writeOn (create|update|always) 2026-09-04'te eklendi (B-12), bu yüzden kural artık çözümü söyler ve writeOn:'create' taşıyan alanda susar",
|
|
390
454
|
check: definition => {
|
|
391
455
|
const parameters = (definition.sourceParameters ?? {});
|
|
392
456
|
if (String(parameters.operation ?? '') !== 'upsert')
|
|
@@ -394,13 +458,14 @@ export const policyRules = [
|
|
|
394
458
|
const generated = asArray(parameters.fields)
|
|
395
459
|
.map(entry => entry)
|
|
396
460
|
.filter(field => String(field.source ?? '') === 'expression')
|
|
461
|
+
.filter(field => String(field.writeOn ?? 'always').toLowerCase() !== 'create')
|
|
397
462
|
.map(field => String(field.alias ?? field.fieldId ?? '?'));
|
|
398
463
|
if (generated.length === 0)
|
|
399
464
|
return [];
|
|
400
465
|
return [{
|
|
401
466
|
ruleId: 'MS-016',
|
|
402
467
|
severity: 'warn',
|
|
403
|
-
message: `upsert ucunda expression kaynaklı alan var (${generated.join(', ')}): upsert UPDATE yolunda da bu ifadeyi yeniden değerlendirir, yani sicil/kod gibi ÜRETİLEN bir değer güncellemede sessizce değişir.
|
|
468
|
+
message: `upsert ucunda expression kaynaklı alan var (${generated.join(', ')}): upsert UPDATE yolunda da bu ifadeyi yeniden değerlendirir, yani sicil/kod gibi ÜRETİLEN bir değer güncellemede sessizce değişir. Alan yalnız oluşturmada yazılacaksa fields[] öğesine writeOn: 'create' koyun — upsert'in UPDATE dalı o alanı satıra hiç koymaz ve bu uyarı susar. writeOn yalnız operation:'upsert' ucunda geçerlidir (MS-017) ve bir 'kısmi güncelleme' bayrağı DEĞİLDİR: listelenen alan gövdede gelmezse yine NULL yazılır.`,
|
|
404
469
|
guide: 'ngapps://guides/microservice'
|
|
405
470
|
}];
|
|
406
471
|
}
|
|
@@ -419,6 +484,23 @@ export const policyRules = [
|
|
|
419
484
|
guide: 'ngapps://guides/microservice'
|
|
420
485
|
}]
|
|
421
486
|
},
|
|
487
|
+
{
|
|
488
|
+
id: 'QRY-007',
|
|
489
|
+
module: 'query',
|
|
490
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
491
|
+
sourceRef: "2026-09-15 bulgusu: pano sorgusu '(@tech = '' OR w.technician_id::text = @tech)' ile 0 satır döndü, hata yok. Kök neden Query.PrepareQueryParameters / NormalizePreviewParameters: boş ya da yalnız boşluktan oluşan değer NULL bağlanır, bu yüzden '@p = ''' hiçbir değerle doğru olmaz. Bulgunun 'dokunulmamış dropdown null gönderiyor' teşhisi yanlıştı — '' gönderen form da aynı sonucu alır. warn: kalıp hatalı ama yazmayı durdurmak gerekmiyor",
|
|
492
|
+
check: definition => {
|
|
493
|
+
const names = emptyStringComparisons(definition.rawQuery);
|
|
494
|
+
if (names.length === 0)
|
|
495
|
+
return [];
|
|
496
|
+
return [{
|
|
497
|
+
ruleId: 'QRY-007',
|
|
498
|
+
severity: 'warn',
|
|
499
|
+
message: `rawQuery parametreyi boş string'le karşılaştırıyor (${names.map(name => `@${name} = ''`).join(', ')}): platform boş ya da yalnız boşluktan oluşan parametre değerini NULL bağlar, NULL = '' hiçbir zaman doğru olmaz ve 'filtre boşsa hepsini getir' dalı ASLA çalışmaz — filtre boş geldiğinde sorgu hatasız 0 satır döner. Değerin form bileşeninden null ya da '' gelmesi fark etmez. Şöyle yazın: (@${names[0]} IS NULL OR kolon::text = @${names[0]}).`,
|
|
500
|
+
guide: 'ngapps://guides/microservice'
|
|
501
|
+
}];
|
|
502
|
+
}
|
|
503
|
+
},
|
|
422
504
|
{
|
|
423
505
|
id: 'MS-018',
|
|
424
506
|
module: 'microservice',
|
|
@@ -583,6 +665,38 @@ export const policyRules = [
|
|
|
583
665
|
});
|
|
584
666
|
}
|
|
585
667
|
},
|
|
668
|
+
{
|
|
669
|
+
id: 'WF-MF-001',
|
|
670
|
+
module: 'workflow',
|
|
671
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
672
|
+
sourceRef: "MicrofunctionStep.cs: rule.MicrofunctionId ile kayıt yüklenir, Status != Published ise FailedOutput(MicrofunctionNotPublishedException) — adım hata dalına düşer, akış 200 dönse bile iş yapılmaz. Ölçüldü 2026-09-10 (B4 doğrulaması: taslak fonksiyona bağlı uç 500 MicrofunctionNotPublishedException)",
|
|
673
|
+
check: definition => {
|
|
674
|
+
const body = workflowBody(definition);
|
|
675
|
+
return workflowSteps(body)
|
|
676
|
+
.filter(step => step.type === 'microfunction')
|
|
677
|
+
.flatMap((step) => {
|
|
678
|
+
const rule = (step.rule ?? {});
|
|
679
|
+
const microfunctionId = rule.microfunctionId;
|
|
680
|
+
if (!hasText(microfunctionId)) {
|
|
681
|
+
return [{
|
|
682
|
+
ruleId: 'WF-MF-001',
|
|
683
|
+
severity: 'block',
|
|
684
|
+
message: `'${stepLabel(step)}' microfunction adımında rule.microfunctionId yok — adım hangi fonksiyonu koşturacağını bilmez. microfunction_list(status: 'published') ile id seçin; argümanlar rule.expression'a yazılır (fonksiyonun parameterDto'su varsa o şemaya göre).`,
|
|
685
|
+
guide: 'ngapps://guides/microfunction'
|
|
686
|
+
}];
|
|
687
|
+
}
|
|
688
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(microfunctionId))) {
|
|
689
|
+
return [{
|
|
690
|
+
ruleId: 'WF-MF-001',
|
|
691
|
+
severity: 'block',
|
|
692
|
+
message: `'${stepLabel(step)}' adımının rule.microfunctionId'si GUID değil: '${String(microfunctionId)}'. Platform alanı Guid'e çeviremediği için 400 döner.`,
|
|
693
|
+
guide: 'ngapps://guides/microfunction'
|
|
694
|
+
}];
|
|
695
|
+
}
|
|
696
|
+
return [];
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
},
|
|
586
700
|
{
|
|
587
701
|
id: 'WF-006',
|
|
588
702
|
module: 'workflow',
|
|
@@ -1704,6 +1818,203 @@ export const policyRules = [
|
|
|
1704
1818
|
}];
|
|
1705
1819
|
})
|
|
1706
1820
|
},
|
|
1821
|
+
{
|
|
1822
|
+
id: 'FN-001',
|
|
1823
|
+
module: 'function',
|
|
1824
|
+
appliesTo: ['create', 'update'],
|
|
1825
|
+
sourceRef: "GlobalFunctionRules.cs IsValidName(name, source)/IsValidParameterName → 422 GlobalFunctionNameInvalid; function-source.ts isValidName — geçersiz ad iki dünyada da fonksiyonu ATLATIR. Kaynağa özel ayrılmış ad: studio 'Functions' (K1), workflow WorkflowReservedNames (motor globalleri)",
|
|
1826
|
+
check: definition => {
|
|
1827
|
+
const name = String(definition.name ?? '');
|
|
1828
|
+
const source = sourceOf(definition);
|
|
1829
|
+
const findings = [];
|
|
1830
|
+
const problem = nameProblem(definition.name, source);
|
|
1831
|
+
if (problem !== undefined) {
|
|
1832
|
+
findings.push({
|
|
1833
|
+
ruleId: 'FN-001',
|
|
1834
|
+
severity: 'block',
|
|
1835
|
+
message: source === 'workflow'
|
|
1836
|
+
? `Fonksiyon adı '${name}' geçersiz: ${problem}. Ad /^[A-Za-z_$][A-Za-z0-9_$]*$/ kalıbına uymalı, JavaScript ayrılmış sözcüğü ve workflow motorunun global adı (system, input, Now, crypto…) olmamalı — platform 422 GlobalFunctionNameInvalid döner.`
|
|
1837
|
+
: `Fonksiyon adı '${name}' geçersiz: ${problem}. Ad /^[A-Za-z_$][A-Za-z0-9_$]*$/ kalıbına uymalı, JavaScript ayrılmış sözcüğü ve 'Functions' olmamalı — platform 422 GlobalFunctionNameInvalid döner; kayıt bir yolla geçse bile runtime böyle bir adı atlar ve Functions.${name} tanımsız kalır.`,
|
|
1838
|
+
guide: functionsGuide
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
for (const parameter of asArray(definition.parameters).filter(isRecord)) {
|
|
1842
|
+
if (isValidParameterName(parameter.name))
|
|
1843
|
+
continue;
|
|
1844
|
+
findings.push({
|
|
1845
|
+
ruleId: 'FN-001',
|
|
1846
|
+
severity: 'block',
|
|
1847
|
+
message: `'${name}' fonksiyonunun parametre adı '${String(parameter.name ?? '')}' geçersiz (${nameProblem(parameter.name) ?? 'geçersiz'}) — platform 422 GlobalFunctionNameInvalid döner; runtime geçersiz parametre adlı fonksiyonu TÜMÜYLE atlar.`,
|
|
1848
|
+
guide: functionsGuide
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return findings;
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
id: 'FN-002',
|
|
1856
|
+
module: 'function',
|
|
1857
|
+
appliesTo: ['create', 'update'],
|
|
1858
|
+
sourceRef: 'CreateGlobalFunctionCommandValidator: parametre adı tekrarı → 422 GlobalFunctionParameterNameDuplicate; bildirim parametreleri konumsal bağlar, aynı ad ikinci değeri gölgeler',
|
|
1859
|
+
check: definition => {
|
|
1860
|
+
const names = asArray(definition.parameters).filter(isRecord).map(parameter => String(parameter.name ?? ''));
|
|
1861
|
+
const duplicates = [...new Set(names.filter((entry, index) => entry !== '' && names.indexOf(entry) !== index))];
|
|
1862
|
+
return duplicates.length === 0
|
|
1863
|
+
? []
|
|
1864
|
+
: [{
|
|
1865
|
+
ruleId: 'FN-002',
|
|
1866
|
+
severity: 'block',
|
|
1867
|
+
message: `'${String(definition.name ?? '?')}' fonksiyonunda parametre adı tekrar ediyor: ${duplicates.join(', ')} — platform 422 GlobalFunctionParameterNameDuplicate döner. Çağrı konumsaldır (K5); aynı ad iki konumu birden alamaz.`,
|
|
1868
|
+
guide: functionsGuide
|
|
1869
|
+
}];
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
id: 'FN-003',
|
|
1874
|
+
module: 'function',
|
|
1875
|
+
appliesTo: ['create', 'update'],
|
|
1876
|
+
sourceRef: 'K15: fonksiyon başka fonksiyonu çağırabilir, tanım sırası önemsiz — kendini çağıran gövde durma koşulu yoksa sonsuz özyinelemedir; runCode RangeError\'ı yutar, sunucuda Jint LimitRecursion henüz yok (B-16, Faz 9)',
|
|
1877
|
+
check: definition => {
|
|
1878
|
+
const name = String(definition.name ?? '');
|
|
1879
|
+
const source = sourceOf(definition);
|
|
1880
|
+
const body = typeof definition.body === 'string' ? definition.body : '';
|
|
1881
|
+
if (name === '' || !callsItself(name, body, source))
|
|
1882
|
+
return [];
|
|
1883
|
+
return [{
|
|
1884
|
+
ruleId: 'FN-003',
|
|
1885
|
+
severity: 'warn',
|
|
1886
|
+
message: `'${name}' gövdesi ${callPrefix(source)}${name}(…) ile KENDİNİ çağırıyor. Durma koşulu yoksa ${source === 'workflow' ? 'Jint sınırsız özyinelemeye girer (B-16 sertleştirmesi Faz 9\'da)' : "tarayıcıda 'Maximum call stack size exceeded' (runCode yutar, hedef prop yazılmaz)"}. Bilinçli özyinelemeyse yok sayın.`,
|
|
1887
|
+
guide: functionsGuide
|
|
1888
|
+
}];
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
id: 'FN-004',
|
|
1893
|
+
module: 'function',
|
|
1894
|
+
appliesTo: ['create', 'update'],
|
|
1895
|
+
sourceRef: "K6: isAsync istemci semantiğidir — function-source.ts compiles(): await taşıyan senkron gövde derlenmez ve atlanır; {{ }} inline bağlama Promise'i [object Promise] çizer. source:'workflow' senkron Jint motorunda koşar: GlobalFunction.ValidateState isAsync'i reddeder (422 GlobalFunctionAsyncNotAllowed)",
|
|
1896
|
+
check: definition => {
|
|
1897
|
+
const name = String(definition.name ?? '?');
|
|
1898
|
+
const body = typeof definition.body === 'string' ? definition.body : '';
|
|
1899
|
+
const isAsync = definition.isAsync === true;
|
|
1900
|
+
if (isAsync && sourceOf(definition) === 'workflow') {
|
|
1901
|
+
return [{
|
|
1902
|
+
ruleId: 'FN-004',
|
|
1903
|
+
severity: 'block',
|
|
1904
|
+
message: `'${name}' source:'workflow' ama isAsync=true — workflow motoru senkron çalışır, async bildirimin dönüşü Promise olur ve code adımı '[object Promise]' görür; platform 422 GlobalFunctionAsyncNotAllowed döner. isAsync:false yapın; await gerekiyorsa bu bir studio fonksiyonudur.`,
|
|
1905
|
+
guide: functionsGuide
|
|
1906
|
+
}];
|
|
1907
|
+
}
|
|
1908
|
+
if (isAsync && !mentionsAwait(body)) {
|
|
1909
|
+
return [{
|
|
1910
|
+
ruleId: 'FN-004',
|
|
1911
|
+
severity: 'warn',
|
|
1912
|
+
message: `'${name}' isAsync=true ama gövdede await yok — async gereksiz ve bedeli var: {{ }} inline bağlamada dönüş Promise'dir ve ekrana '[object Promise]' yazılır. await gerekmiyorsa isAsync:false yapın.`,
|
|
1913
|
+
guide: functionsGuide
|
|
1914
|
+
}];
|
|
1915
|
+
}
|
|
1916
|
+
if (!isAsync && mentionsAwait(body)) {
|
|
1917
|
+
return [{
|
|
1918
|
+
ruleId: 'FN-004',
|
|
1919
|
+
severity: 'warn',
|
|
1920
|
+
message: `'${name}' gövdesi await içeriyor ama isAsync=false — senkron function içinde await SyntaxError'dur: tarayıcı gövdeyi derleyemez ve fonksiyonu ATLAR (function-source.ts compiles), sunucu da derleyemediği gövdeyi atlar; Functions.${name} hiçbir yerde tanımlı olmaz. Ya isAsync:true yapın (o zaman yalnız tarayıcıda, function/code adımından çağrılır) ya da await'i kaldırın.`,
|
|
1921
|
+
guide: functionsGuide
|
|
1922
|
+
}];
|
|
1923
|
+
}
|
|
1924
|
+
return [];
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
id: 'FN-005',
|
|
1929
|
+
module: 'function',
|
|
1930
|
+
appliesTo: ['create'],
|
|
1931
|
+
sourceRef: "CreateGlobalFunctionCommandValidator When(source): workflow ⇒ applicationId/pageId/scope null (422 GlobalFunctionSourceWorkflowScopeForbidden); studio ⇒ applicationId ve scope zorunlu, scope=page ⇒ pageId (422 GlobalFunctionScopePageRequired), scope=application ⇒ pageId yok (422 GlobalFunctionScopeApplicationPageForbidden)",
|
|
1932
|
+
check: definition => {
|
|
1933
|
+
const name = String(definition.name ?? '?');
|
|
1934
|
+
const source = sourceOf(definition);
|
|
1935
|
+
const has = (key) => definition[key] !== undefined && definition[key] !== null && definition[key] !== '';
|
|
1936
|
+
const block = (message) => ({ ruleId: 'FN-005', severity: 'block', message, guide: functionsGuide });
|
|
1937
|
+
if (source === 'workflow') {
|
|
1938
|
+
const extras = ['applicationId', 'pageId', 'scope'].filter(has);
|
|
1939
|
+
return extras.length === 0
|
|
1940
|
+
? []
|
|
1941
|
+
: [block(`'${name}' source:'workflow' ama ${extras.join(', ')} verilmiş — workflow fonksiyonu TENANT genelidir, uygulamaya/kapsama/sayfaya bağlanmaz (platform 422 GlobalFunctionSourceWorkflowScopeForbidden). Alanları kaldırın; uygulamaya özel bir tarayıcı fonksiyonu istiyorsanız source:'studio'.`)];
|
|
1942
|
+
}
|
|
1943
|
+
if (!has('applicationId'))
|
|
1944
|
+
return [block(`'${name}' source:'studio' ama applicationId yok — studio fonksiyonu bir uygulamaya aittir (platform 422). application_list ile uygulama id'sini verin; tenant geneli bir SUNUCU fonksiyonu istiyorsanız source:'workflow'.`)];
|
|
1945
|
+
if (!has('scope'))
|
|
1946
|
+
return [block(`'${name}' source:'studio' ama scope yok — 'application' (her sayfada) ya da 'page' (tek sayfa, pageId zorunlu) seçin (platform 422).`)];
|
|
1947
|
+
if (definition.scope === 'page' && !has('pageId'))
|
|
1948
|
+
return [block(`'${name}' scope:'page' ama pageId yok — platform 422 GlobalFunctionScopePageRequired verir; page_list ile sayfa id'sini alın.`)];
|
|
1949
|
+
if (definition.scope === 'application' && has('pageId'))
|
|
1950
|
+
return [block(`'${name}' scope:'application' fonksiyonda pageId verilmez — platform 422 GlobalFunctionScopeApplicationPageForbidden verir; sayfaya bağlamak istiyorsanız scope:'page'.`)];
|
|
1951
|
+
return [];
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
id: 'FLOW-024',
|
|
1956
|
+
module: 'flow',
|
|
1957
|
+
appliesTo: ['create', 'update'],
|
|
1958
|
+
sourceRef: 'K9 + B-19: function adımında args.functionId zorunlu ve uygulamada var olmalı (CreateFlowCommandHandler ilk satırında 422 UiFunctionNotFound); tasks/function/function.ts önce id, sonra functionName ile çözer; parameters bildirim sırasına göre ada göre eşlenir',
|
|
1959
|
+
check: definition => flowAllSteps(definition)
|
|
1960
|
+
.filter(step => step.type === 'function')
|
|
1961
|
+
.flatMap(step => {
|
|
1962
|
+
const args = (step.args ?? {});
|
|
1963
|
+
const label = flowStepLabel(step);
|
|
1964
|
+
const findings = [];
|
|
1965
|
+
if (typeof args.functionId !== 'string' || !conditionRuleIdPattern.test(args.functionId)) {
|
|
1966
|
+
findings.push({
|
|
1967
|
+
ruleId: 'FLOW-024',
|
|
1968
|
+
severity: 'block',
|
|
1969
|
+
message: `'${label}' function adımında args.functionId ${args.functionId === undefined || args.functionId === null ? 'yok' : `geçersiz ('${String(args.functionId)}')`} — platform 422 UiFunctionNotFound döner. Gerçek id'yi function_list ile alın; runtime fonksiyonu ÖNCE id ile çözer, functionName yalnız yedektir (K9).`,
|
|
1970
|
+
guide: functionsGuide
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
if (typeof args.functionName !== 'string' || args.functionName.trim() === '') {
|
|
1974
|
+
findings.push({
|
|
1975
|
+
ruleId: 'FLOW-024',
|
|
1976
|
+
severity: 'warn',
|
|
1977
|
+
message: `'${label}' function adımında args.functionName boş — Studio adım özeti 'seçili fonksiyon yok' gösterir ve id çözülemezse ad yedeği de kalmaz. functionName: '<fonksiyon adı>' yazın.`,
|
|
1978
|
+
guide: functionsGuide
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
if (args.parameters !== undefined && args.parameters !== null && !Array.isArray(args.parameters)) {
|
|
1982
|
+
findings.push({
|
|
1983
|
+
ruleId: 'FLOW-024',
|
|
1984
|
+
severity: 'block',
|
|
1985
|
+
message: `'${label}' function adımında args.parameters dizi değil (${typeof args.parameters}) — şekil: [{ name: '<bildirimdeki ad>', value: { type: 'static'|'property'|'storage'|'state'|'code'|'variable'|'page'|'item', ref } }].`,
|
|
1986
|
+
guide: functionsGuide
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
const nameless = asArray(args.parameters).filter(isRecord).filter(parameter => typeof parameter.name !== 'string' || parameter.name.trim() === '');
|
|
1990
|
+
if (nameless.length > 0) {
|
|
1991
|
+
findings.push({
|
|
1992
|
+
ruleId: 'FLOW-024',
|
|
1993
|
+
severity: 'block',
|
|
1994
|
+
message: `'${label}' function adımında ${nameless.length} parametrenin name'i yok — eşleme bildirimdeki ada göre yapılır, adsız parametre hiçbir argümana bağlanmaz.`,
|
|
1995
|
+
guide: functionsGuide
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
return findings;
|
|
1999
|
+
})
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
id: 'WF-030',
|
|
2003
|
+
module: 'workflow',
|
|
2004
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
2005
|
+
sourceRef: "2026-09-10 kararı (Owner MR notu, zeyrek-backend docs/global-functions/02 §1): sunucu UI fonksiyonlarını YÜKLEMEZ — WorkflowContext'in 'Functions' ad alanı ve yükleme yolu kaldırıldı. Workflow/middleware/automation ifadesindeki Functions.<ad>(…) çağrısı Jint'te 'Functions is not defined' ile düşer (canlı ölçüldü: workflow_preview code adımı). Sunucuda paylaşılan hesap source:'workflow' fonksiyondur, çıplak adla çağrılır",
|
|
2006
|
+
check: (definition, context) => {
|
|
2007
|
+
const names = [...new Set(functionCallRefs(JSON.stringify(workflowBody(definition, context.workflowKind) ?? {})).map(ref => ref.name))];
|
|
2008
|
+
return names.length === 0
|
|
2009
|
+
? []
|
|
2010
|
+
: [{
|
|
2011
|
+
ruleId: 'WF-030',
|
|
2012
|
+
severity: 'block',
|
|
2013
|
+
message: `İfadeler ${names.map(name => `Functions.${name}(…)`).join(', ')} çağırıyor ama SUNUCUDA 'Functions' ad alanı YOKTUR (2026-09-10): motor kullanıcı fonksiyonlarını yüklemez, çağrı 'Functions is not defined' ile düşer ve adım hata yoluna girer. Sunucuda paylaşılan hesap source:'workflow' fonksiyondur — function_create source:'workflow' ile tanımlayıp ifadede çıplak adla çağırın ('${names[0]}(…)'). Kullanıcı fonksiyonları yalnız tarayıcıda çalışır ({{ }} bağlaması ve UI flow adımları).`,
|
|
2014
|
+
guide: functionsGuide
|
|
2015
|
+
}];
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
1707
2018
|
{
|
|
1708
2019
|
id: 'FLOW-005',
|
|
1709
2020
|
module: 'flow',
|
|
@@ -1954,7 +2265,7 @@ export const policyRules = [
|
|
|
1954
2265
|
id: 'FLOW-022',
|
|
1955
2266
|
module: 'flow',
|
|
1956
2267
|
appliesTo: ['create', 'update'],
|
|
1957
|
-
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(
|
|
2268
|
+
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)",
|
|
1958
2269
|
check: definition => flowAllSteps(definition)
|
|
1959
2270
|
.flatMap((step) => {
|
|
1960
2271
|
const type = String(step.type ?? '');
|
|
@@ -2479,3 +2790,139 @@ export const policyRules = [
|
|
|
2479
2790
|
// tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
|
|
2480
2791
|
...backendPolicyRules()
|
|
2481
2792
|
];
|
|
2793
|
+
/**
|
|
2794
|
+
* WF-MF-001'in ikinci yarısı. Bir adımın koşup koşmayacağı yalnız GÖVDEDEN anlaşılamaz: karar
|
|
2795
|
+
* seçilen microfunction'ın YAYIN DURUMUNA bağlıdır ve o durum platformda yaşar. Politika
|
|
2796
|
+
* kuralları senkron ve saf olduğu için bu denetim ayrı durur — gateway `microfunction_get`
|
|
2797
|
+
* ucunu okur, bulguyu aynı `policyFindings` kanalına koyar ve `policyOverride` / `POLICY=warn`
|
|
2798
|
+
* kaçışları (applyPolicyMode) aynen geçerli kalır.
|
|
2799
|
+
*
|
|
2800
|
+
* Okuma başarısız olursa (ağ, yetki, beklenmeyen gövde) kural SUSAR: ön-denetim bir kolaylıktır,
|
|
2801
|
+
* gateway'in kendi arızası yüzünden geçerli bir workflow yazılamaz hâle gelmemeli.
|
|
2802
|
+
*/
|
|
2803
|
+
export async function microfunctionStepFindings(platform, project, environment, definition, kind, policyOverride = false) {
|
|
2804
|
+
if (platform.config.policyMode === 'off')
|
|
2805
|
+
return [];
|
|
2806
|
+
const steps = workflowSteps(workflowBody(definition, kind))
|
|
2807
|
+
.filter(step => step.type === 'microfunction')
|
|
2808
|
+
.filter(step => hasText((step.rule ?? {}).microfunctionId));
|
|
2809
|
+
if (steps.length === 0)
|
|
2810
|
+
return [];
|
|
2811
|
+
const records = new Map();
|
|
2812
|
+
const findings = [];
|
|
2813
|
+
for (const step of steps) {
|
|
2814
|
+
const rule = (step.rule ?? {});
|
|
2815
|
+
const microfunctionId = String(rule.microfunctionId);
|
|
2816
|
+
if (!records.has(microfunctionId)) {
|
|
2817
|
+
try {
|
|
2818
|
+
const payload = await platform.tenant('GET', project, `/microfunctions/${microfunctionId}`, { environment });
|
|
2819
|
+
records.set(microfunctionId, (payload?.data ?? payload));
|
|
2820
|
+
}
|
|
2821
|
+
catch (error) {
|
|
2822
|
+
records.set(microfunctionId, error instanceof PlatformApiError && error.status === 404 ? 'missing' : 'unreadable');
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
const record = records.get(microfunctionId);
|
|
2826
|
+
if (record === undefined || record === 'unreadable')
|
|
2827
|
+
continue;
|
|
2828
|
+
if (record === 'missing') {
|
|
2829
|
+
findings.push({
|
|
2830
|
+
ruleId: 'WF-MF-001',
|
|
2831
|
+
severity: 'block',
|
|
2832
|
+
message: `'${stepLabel(step)}' adımının microfunctionId'si (${microfunctionId}) bu ortamda YOK. Adım kuralı step_rules.microfunction_id ile kayda FK'lıdır: gövde gitseydi platform 400 dönerdi. microfunction_list ile bu ortamdaki kayıtları görün — fonksiyon başka bir dalda/ortamda kalmış olabilir.`,
|
|
2833
|
+
guide: 'ngapps://guides/microfunction'
|
|
2834
|
+
});
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
const status = String(record.status ?? '');
|
|
2838
|
+
if (status === 'draft' || status === 'publish-failed') {
|
|
2839
|
+
findings.push({
|
|
2840
|
+
ruleId: 'WF-MF-001',
|
|
2841
|
+
severity: 'block',
|
|
2842
|
+
message: `'${stepLabel(step)}' adımı '${String(record.name ?? microfunctionId)}' fonksiyonunu seçiyor ama kaydın durumu '${status}'${status === 'publish-failed' && hasText(record.publishError) ? ` (publishError: ${String(record.publishError)})` : ''}. Step YALNIZ yayındaki kaydı koşturur: MicrofunctionStep kaydı yükleyip Status != Published görünce MicrofunctionNotPublishedException ile FAIL dalına düşer — akış kurulur, kaydedilir ve koşumda sessizce hata yoluna girer. Önce microfunction_publish çağırın ve microfunction_get ile 'published' olduğunu görün.`,
|
|
2843
|
+
guide: 'ngapps://guides/microfunction'
|
|
2844
|
+
});
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
if (status === 'publishing') {
|
|
2848
|
+
findings.push({
|
|
2849
|
+
ruleId: 'WF-MF-001',
|
|
2850
|
+
severity: 'warn',
|
|
2851
|
+
message: `'${stepLabel(step)}' adımının fonksiyonu ('${String(record.name ?? microfunctionId)}') şu anda yayınlanıyor. Yayın ASENKRONDUR: bu akış şimdi koşarsa adım fail dalına düşer. microfunction_get ile 'published' olduğunu doğrulayın (yayın başarısız olursa durum 'publish-failed' olur).`,
|
|
2852
|
+
guide: 'ngapps://guides/microfunction'
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
if (record.hasUnpublishedChanges === true) {
|
|
2856
|
+
findings.push({
|
|
2857
|
+
ruleId: 'WF-MF-001',
|
|
2858
|
+
severity: 'warn',
|
|
2859
|
+
message: `'${String(record.name ?? microfunctionId)}' fonksiyonu yayında ama üstüne yazılmış (hasUnpublishedChanges: true, revision ${String(record.revision ?? '?')} ≠ publishedRevision ${String(record.publishedRevision ?? '?')}). Runtime hâlâ ESKİ sürümü koşuyor — yeni kodu canlıya almak için microfunction_publish çağırın.`,
|
|
2860
|
+
guide: 'ngapps://guides/microfunction'
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
if (!hasText(rule.expression) && hasText(record.parameterDtoId)) {
|
|
2864
|
+
findings.push({
|
|
2865
|
+
ruleId: 'WF-MF-001',
|
|
2866
|
+
severity: 'warn',
|
|
2867
|
+
message: `'${stepLabel(step)}' adımında rule.expression boş ama '${String(record.name ?? microfunctionId)}' fonksiyonunun bir parametre DTO'su var (parameterDtoId: ${String(record.parameterDtoId)}). Fonksiyon args olarak boş nesne alır ve kendi alanlarını undefined görür. Argümanları ifadeyle geçirin — şemayı microfunction_get çıktısındaki parameterScheme verir.`,
|
|
2868
|
+
guide: 'ngapps://guides/microfunction'
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return applyPolicyMode(platform.config, findings, policyOverride);
|
|
2873
|
+
}
|
|
2874
|
+
const partialWriteOperations = new Set(['create', 'update', 'upsert']);
|
|
2875
|
+
const inputReferencePattern = /\binput\.([A-Za-z_$][\w$]*)/g;
|
|
2876
|
+
function inputReferencesOf(rule) {
|
|
2877
|
+
return asArray(rule.fields)
|
|
2878
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
2879
|
+
.flatMap(field => [...String(field.value ?? '').matchAll(inputReferencePattern)].map(match => match[1]))
|
|
2880
|
+
.filter((name) => typeof name === 'string');
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* WF-031 — yazma adımı, akışın giriş DTO'sunda OLMAYAN bir `input.x` alanını yazıyorsa uyarır.
|
|
2884
|
+
* Böyle bir ifade çalışma anında `undefined` döner, `Parse` onu `null` yapar ve platform kolonu
|
|
2885
|
+
* NULL yazar: satırdaki mevcut değer silinir, yanıt yine 200'dür (risk uygulaması Kanban taşıma
|
|
2886
|
+
* bulgusu, 2026-09-15 — `{ id, status }` gövdesi `title`/`owner_id`/`due_date`'i sildi).
|
|
2887
|
+
*
|
|
2888
|
+
* Kural yalnız DTO'da HİÇ olmayan alanı sayar: "DTO'da var ama çağıran göndermedi" vakası
|
|
2889
|
+
* tanımdan görülemez, onu rehber anlatır. DTO şeması okunamazsa (404/erişim) kural susar —
|
|
2890
|
+
* bir okuma hatası yazma çağrısını engellememelidir.
|
|
2891
|
+
*/
|
|
2892
|
+
export async function undefinedInputFieldFindings(platform, project, environment, definition, kind, policyOverride = false) {
|
|
2893
|
+
if (platform.config.policyMode === 'off')
|
|
2894
|
+
return [];
|
|
2895
|
+
const body = workflowBody(definition, kind);
|
|
2896
|
+
if (!hasText(body.parameterDtoId))
|
|
2897
|
+
return [];
|
|
2898
|
+
const candidates = workflowSteps(body)
|
|
2899
|
+
.filter(step => step.type === 'table' && step.rule)
|
|
2900
|
+
.filter(step => partialWriteOperations.has(String(step.rule.operation ?? '').toLowerCase()))
|
|
2901
|
+
.map(step => ({ step, references: [...new Set(inputReferencesOf(step.rule))] }))
|
|
2902
|
+
.filter(candidate => candidate.references.length > 0);
|
|
2903
|
+
if (candidates.length === 0)
|
|
2904
|
+
return [];
|
|
2905
|
+
let dtoFieldNames;
|
|
2906
|
+
try {
|
|
2907
|
+
const payload = await platform.tenant('GET', project, `/datatransferobjects/${String(body.parameterDtoId)}/scheme`, { environment });
|
|
2908
|
+
const items = asArray(payload?.data ?? payload).filter((item) => Boolean(item) && typeof item === 'object');
|
|
2909
|
+
dtoFieldNames = new Set(items.map(item => String(item.name ?? '')));
|
|
2910
|
+
}
|
|
2911
|
+
catch (error) {
|
|
2912
|
+
if (error instanceof PlatformApiError)
|
|
2913
|
+
return [];
|
|
2914
|
+
throw error;
|
|
2915
|
+
}
|
|
2916
|
+
const findings = candidates.flatMap(({ step, references }) => {
|
|
2917
|
+
const missing = references.filter(name => !dtoFieldNames.has(name));
|
|
2918
|
+
return missing.length > 0
|
|
2919
|
+
? [{
|
|
2920
|
+
ruleId: 'WF-031',
|
|
2921
|
+
severity: 'warn',
|
|
2922
|
+
message: `'${stepLabel(step)}' adımı ${missing.map(name => `input.${name}`).join(', ')} alanlarını yazıyor ama akışın giriş DTO'sunda bu alanlar YOK: ifade undefined döner, platform kolonu NULL yazar ve satırdaki mevcut değer SİLİNİR (yanıt yine 200). Yazma adımında SET listesi fields[]'tir, gövde değil — 'göndermediğim alan değişmez' (REST PATCH) sezgisi burada geçmez. İki doğru çıkış: (1) adımı EYLEM BAŞINA daraltın, fields[] yalnız bu eylemin gerçekten yazdığı alanları listelesin (kanban taşıma: yalnız status); (2) alanı DTO'ya ekleyin VE çağıranın her istekte gönderdiğinden emin olun — DTO'ya eklemek yalnız izin verir, göndermeyi garanti etmez. Alan kümesi ancak çağrı anında belliyse akışta oku-birleştir: önce table get, sonra "typeof input.x !== 'undefined' && input.x !== null ? input.x : read_row.data.x" (bedeli fazladan okuma ve son-yazan-kazanır).`,
|
|
2923
|
+
guide: 'ngapps://guides/workflow'
|
|
2924
|
+
}]
|
|
2925
|
+
: [];
|
|
2926
|
+
});
|
|
2927
|
+
return applyPolicyMode(platform.config, findings, policyOverride);
|
|
2928
|
+
}
|
|
@@ -419,6 +419,12 @@ export const TYPED_STYLE_COMPONENTS = {
|
|
|
419
419
|
// Taşma hedefi olmayan komponentte contentOverflow ölü kural üretirdi.
|
|
420
420
|
omit: { surfaceStyle: ['contentOverflow'] }
|
|
421
421
|
},
|
|
422
|
+
form: {
|
|
423
|
+
// Çerçeve <form> KÖKÜNDE: yüzey köke, taşma gövdeye (S-19, 2026-09-15).
|
|
424
|
+
// Genel düğüm yüzeyi yolu değeri kökün dışındaki kutuya yazıp ikinci çerçeve çiziyordu.
|
|
425
|
+
groups: ['surfaceStyle'],
|
|
426
|
+
ext: { overflowTarget: '&__body' }
|
|
427
|
+
},
|
|
422
428
|
icon: {
|
|
423
429
|
// Yalnız hover: surface çipin (chipShape/chipColor), typography da boyutun
|
|
424
430
|
// (size) ve rengin (color) çift kanalı olurdu — görsel kararlar semantik
|
|
@@ -439,7 +445,6 @@ export const TYPED_STYLE_COMPONENTS = {
|
|
|
439
445
|
export const NODE_SURFACE_TYPES = new Set([
|
|
440
446
|
'table',
|
|
441
447
|
'datatable',
|
|
442
|
-
'form',
|
|
443
448
|
'collapsible-container',
|
|
444
449
|
'drawer',
|
|
445
450
|
'modal',
|