@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7
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 +9 -1
- package/README.md +14 -38
- package/dist/config.js +36 -3
- package/dist/knowledge/backendConstraints.js +982 -0
- package/dist/knowledge/componentCatalog.js +549 -92
- package/dist/knowledge/componentEvents.js +120 -25
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -0
- package/dist/knowledge/functionGuards.js +134 -0
- package/dist/knowledge/guideSections.js +49 -0
- package/dist/knowledge/hydrate.js +97 -0
- package/dist/knowledge/languageCodes.js +36 -0
- package/dist/knowledge/languages.js +105 -0
- package/dist/knowledge/layoutSummary.js +10 -0
- package/dist/knowledge/localization.js +51 -0
- package/dist/knowledge/normalize.js +500 -16
- package/dist/knowledge/outlines.js +167 -12
- package/dist/knowledge/policy.js +14 -5
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +961 -44
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/csv.js +123 -0
- package/dist/platform/errors.js +107 -6
- package/dist/platform/http.js +34 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +25 -6
- package/dist/platform/screenshot.js +544 -16
- package/dist/platform/secrets.js +40 -0
- package/dist/platform/tokenVault.js +89 -0
- package/dist/platform/writeDiff.js +281 -0
- package/dist/resources/examples/flow-call-function.json +66 -0
- package/dist/resources/examples/function-total.json +70 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/microfunction-node-sum.json +56 -0
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +3 -3
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +4 -3
- package/dist/resources/guides/component.md +81 -8
- package/dist/resources/guides/entity.md +712 -20
- package/dist/resources/guides/expressions.md +47 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/functions.md +262 -0
- package/dist/resources/guides/getting-started.md +82 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/merge.md +231 -0
- package/dist/resources/guides/microfunction.md +166 -0
- package/dist/resources/guides/microservice.md +287 -15
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/modify-existing.md +23 -0
- package/dist/resources/guides/page.md +402 -49
- package/dist/resources/guides/translation.md +150 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +199 -28
- package/dist/resources/knowledge.js +5 -0
- package/dist/resources/rules.js +27 -0
- package/dist/resources/schema-fingerprint.json +8 -0
- package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
- package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
- package/dist/resources/schemas/CreateEntityCommand.json +52 -12
- 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 +145 -3
- package/dist/resources/schemas/CreateQueryCommand.json +56 -3
- package/dist/resources/schemas/CreateRowCommand.json +38 -0
- package/dist/resources/schemas/CreateViewCommand.json +13 -10
- package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
- 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 +6 -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 +135 -1
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
- 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 +34 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
- 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 +147 -4
- package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
- package/dist/resources/schemas/UpdateRowCommand.json +40 -0
- package/dist/resources/schemas/UpdateViewCommand.json +15 -10
- package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
- package/dist/server.js +17 -1
- package/dist/tools/access.js +3 -3
- package/dist/tools/activities.js +40 -0
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +76 -34
- package/dist/tools/common.js +55 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/environments.js +52 -0
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +57 -42
- package/dist/tools/flows.js +205 -13
- package/dist/tools/folders.js +7 -2
- package/dist/tools/functions.js +331 -0
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/importSchemas.js +79 -0
- package/dist/tools/imports.js +241 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/merges.js +113 -0
- package/dist/tools/microfunctions.js +155 -0
- package/dist/tools/microservices.js +270 -40
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +421 -104
- package/dist/tools/platformInfo.js +155 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/revisions.js +94 -0
- package/dist/tools/rows.js +484 -50
- package/dist/tools/screenWrite.js +68 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/templates.js +32 -5
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +56 -21
- package/package.json +7 -2
package/dist/knowledge/rules.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { applyPolicyMode } from './policy.js';
|
|
2
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
1
3
|
import { reservedTenantTables } from './reservedTables.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
import { headersAccessNames, isHeadersAccessGuarded } from './flowGuards.js';
|
|
5
|
+
import { callsItself, functionCallRefs, isValidParameterName, mentionsAwait, nameProblem, sourceOf, callPrefix } from './functionGuards.js';
|
|
6
|
+
import { backendPolicyRules } from './backendConstraints.js';
|
|
7
|
+
const terminalStepTypes = new Set(['response', 'convert', 'revert']);
|
|
8
|
+
const DESCRIPTION_EXEMPT_STEPS = new Set(['start']);
|
|
4
9
|
const stepKindMatrix = {
|
|
5
10
|
start: ['workflow', 'automation', 'middleware'],
|
|
6
11
|
condition: ['workflow', 'automation', 'middleware'],
|
|
@@ -12,14 +17,14 @@ const stepKindMatrix = {
|
|
|
12
17
|
workflow: ['workflow', 'automation', 'middleware'],
|
|
13
18
|
email: ['workflow', 'automation', 'middleware'],
|
|
14
19
|
'file-upload': ['workflow', 'automation', 'middleware'],
|
|
20
|
+
microfunction: ['workflow', 'automation', 'middleware'],
|
|
15
21
|
variable: ['middleware'],
|
|
16
22
|
'set-user': ['middleware'],
|
|
17
23
|
setcookie: ['middleware'],
|
|
18
24
|
redirect: ['middleware'],
|
|
19
25
|
response: ['workflow', 'middleware'],
|
|
20
26
|
convert: ['workflow'],
|
|
21
|
-
revert: ['automation']
|
|
22
|
-
end: ['workflow', 'automation', 'middleware']
|
|
27
|
+
revert: ['automation']
|
|
23
28
|
};
|
|
24
29
|
function asArray(value) {
|
|
25
30
|
return Array.isArray(value) ? value : [];
|
|
@@ -96,9 +101,41 @@ const flowBranchNames = {
|
|
|
96
101
|
start: ['always'],
|
|
97
102
|
microservice: ['always', 'success'],
|
|
98
103
|
confirm: ['yes', 'no', 'always'],
|
|
104
|
+
function: ['always'],
|
|
99
105
|
end: []
|
|
100
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
|
+
]);
|
|
101
117
|
const flowFreeBranchTypes = new Set(['condition']);
|
|
118
|
+
/** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
|
|
119
|
+
const conditionRuleTypes = new Set(['if', 'else-if', 'else']);
|
|
120
|
+
// Sayfa akışında kuralın id'si hem dal adıdır (condition.ts `engine.next(rule.id)`) hem de
|
|
121
|
+
// backend'de bir Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; }`), yani
|
|
122
|
+
// okunabilir bir ad iki uçta birden tutmaz: istek System.Guid çevrimiyle reddedilir.
|
|
123
|
+
const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
124
|
+
// alert.ts ve confirm.ts `task.argument.message`'ı doğrudan toast/dialog'a geçirir; argüman
|
|
125
|
+
// Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
|
|
126
|
+
const flowLiteralTextFields = new Map([
|
|
127
|
+
['alert', ['message', 'description']],
|
|
128
|
+
['confirm', ['message']]
|
|
129
|
+
]);
|
|
130
|
+
const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
|
|
131
|
+
// Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
|
|
132
|
+
// BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten `redirect`e çeviriyor
|
|
133
|
+
// (normalize.ts `normalizeFlowHostGaps`) — `redirect` iki barındırıcıda da koşar, yani vaka
|
|
134
|
+
// uyarılmaz, ORTADAN KALKAR. Geriye kalan `confirm`/`download` tek başına kusur değildir: sayfa
|
|
135
|
+
// tek bir uygulamaya aittir (`Page.ApplicationId`), uygulamanın tipi web ya da mobile'dır
|
|
136
|
+
// (`ApplicationType.cs`) ve gateway hangisi olduğunu bilmez. Mobil ayağı uygulama tarafında
|
|
137
|
+
// kapanır (`ScreenRuntime.tsx`), lint'te değil.
|
|
138
|
+
const flowUnregisteredStepTypes = new Set(['end', 'query']);
|
|
102
139
|
const flowSetterIterateeTypes = new Set(['change', 'fill']);
|
|
103
140
|
function flowStepList(value) {
|
|
104
141
|
return asArray(value).filter((step) => Boolean(step) && typeof step === 'object');
|
|
@@ -109,8 +146,20 @@ function flowAllSteps(definition) {
|
|
|
109
146
|
function flowStepLabel(step) {
|
|
110
147
|
return step.stepId || step.name || step.type || '?';
|
|
111
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Dal listesi İKİ biçimde gelebilir ve motor ikisini de aynı şekle eşler (Runtime.svelte
|
|
151
|
+
* `mapStepBranches`, ScreenRuntime.tsx aynısı): dizi `[{ name, nextStepId }]` ya da nesne
|
|
152
|
+
* `{ <dal adı>: <nextStepId> }`. Studio'nun kendi varsayılan şeması nesne biçimini kullanır
|
|
153
|
+
* (`event-actions.constants.ts` DEFAULT_SCHEMA `branches: { always: '' }`), bu yüzden lint
|
|
154
|
+
* nesne biçimini görmezse Studio'dan gelen gövdeler bütün dal kurallarını atlar.
|
|
155
|
+
*/
|
|
112
156
|
function flowBranches(step) {
|
|
113
|
-
|
|
157
|
+
const branches = step.branches;
|
|
158
|
+
if (branches && typeof branches === 'object' && !Array.isArray(branches)) {
|
|
159
|
+
return Object.entries(branches)
|
|
160
|
+
.map(([name, nextStepId]) => ({ name, nextStepId: nextStepId ?? null }));
|
|
161
|
+
}
|
|
162
|
+
return asArray(branches).filter((branch) => Boolean(branch) && typeof branch === 'object');
|
|
114
163
|
}
|
|
115
164
|
function flowIteratees(step) {
|
|
116
165
|
return asArray((step.args ?? {}).iteratees).filter((iteratee) => Boolean(iteratee) && typeof iteratee === 'object');
|
|
@@ -171,10 +220,41 @@ function flowCodeExpressions(step) {
|
|
|
171
220
|
}
|
|
172
221
|
return expressions;
|
|
173
222
|
}
|
|
174
|
-
/**
|
|
175
|
-
|
|
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
|
+
}
|
|
176
238
|
const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
|
|
177
|
-
|
|
239
|
+
/** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
|
|
240
|
+
const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
|
|
241
|
+
const rolesSlugFieldId = '0199c35d-cfc8-77d4-9c20-29b857153147';
|
|
242
|
+
function writesRolesWithoutSlug(entityId, parameters) {
|
|
243
|
+
if (String(entityId ?? '').toLowerCase() !== rolesEntityId)
|
|
244
|
+
return false;
|
|
245
|
+
const operation = String(parameters?.operation ?? '').toLowerCase();
|
|
246
|
+
// Yalnız SATIR ÜRETEN yollar: 'update' var olan satırın slug'ını zaten taşır, orada slug
|
|
247
|
+
// yazdırmak eski slug'a bakan yetki ifadelerini kırar (aynı tuzağın upsert ayağı MS-016).
|
|
248
|
+
if (operation !== 'create' && operation !== 'upsert')
|
|
249
|
+
return false;
|
|
250
|
+
return !asArray(parameters?.fields)
|
|
251
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
252
|
+
.some(field => String(field.fieldId ?? '').toLowerCase() === rolesSlugFieldId
|
|
253
|
+
|| String(field.fieldSlug ?? field.slug ?? '') === 'slug');
|
|
254
|
+
}
|
|
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.";
|
|
256
|
+
const contractKeys = ['returnDtoId', 'parameterDtoId', 'returnBehavior', 'parameterBehavior'];
|
|
257
|
+
const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith', 'isnull', 'isnotnull']);
|
|
178
258
|
function flattenFilterTree(filters) {
|
|
179
259
|
return filters
|
|
180
260
|
.filter((filter) => Boolean(filter) && typeof filter === 'object')
|
|
@@ -266,6 +346,8 @@ function unreadSelectFields(step, steps) {
|
|
|
266
346
|
.map(field => String(field.fieldSlug ?? field.slug ?? ''))
|
|
267
347
|
.filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
|
|
268
348
|
}
|
|
349
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
350
|
+
const functionsGuide = 'ngapps://guides/functions';
|
|
269
351
|
export const policyRules = [
|
|
270
352
|
{
|
|
271
353
|
id: 'MS-001',
|
|
@@ -300,6 +382,95 @@ export const policyRules = [
|
|
|
300
382
|
guide: 'ngapps://guides/microservice'
|
|
301
383
|
}]
|
|
302
384
|
},
|
|
385
|
+
{
|
|
386
|
+
id: 'MS-015',
|
|
387
|
+
module: 'microservice',
|
|
388
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
389
|
+
sourceRef: "TableMicroserviceExecutor.ProcessFilterExpressions (TableMicroserviceExecutor.cs:495-510) filter.Value'yu Source'a BAKMADAN engine.Evaluate'e verir; query-string/body/header kaynaklı bir filtreye value yazmak çalışma anında 'JavaScriptException: <value> is not defined' → HTTP 500 üretir. Bu kaynaklarda değer çağrıdan gelir, parametre adı alan slug'ından türer (İK koşusu M-2)",
|
|
390
|
+
check: definition => {
|
|
391
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
392
|
+
const offenders = [];
|
|
393
|
+
const pending = [...asArray(parameters.filters)];
|
|
394
|
+
while (pending.length > 0) {
|
|
395
|
+
const filter = pending.shift();
|
|
396
|
+
if (!filter || typeof filter !== 'object')
|
|
397
|
+
continue;
|
|
398
|
+
pending.push(...asArray(filter.children));
|
|
399
|
+
const source = String(filter.source ?? '');
|
|
400
|
+
const value = filter.value;
|
|
401
|
+
if (source !== 'query-string' && source !== 'body' && source !== 'header')
|
|
402
|
+
continue;
|
|
403
|
+
if (value === null || value === undefined || String(value).trim() === '')
|
|
404
|
+
continue;
|
|
405
|
+
offenders.push(`${String(filter.alias ?? '?')} · source: '${source}' · value: ${JSON.stringify(value)}`);
|
|
406
|
+
}
|
|
407
|
+
if (offenders.length === 0)
|
|
408
|
+
return [];
|
|
409
|
+
return [{
|
|
410
|
+
ruleId: 'MS-015',
|
|
411
|
+
severity: 'block',
|
|
412
|
+
message: `query-string/body/header kaynaklı filtreye 'value' yazılmış (${offenders.join(' | ')}): platform filtre değerini kaynağa bakmadan JS olarak değerlendiriyor, bu yüzden uç çağrıldığında 'JavaScriptException: … is not defined' ile 500 döner. Bu kaynaklarda 'value' HİÇ verilmez — parametre adı alan slug'ından türer; sabit bir ön-süzme istiyorsanız source: 'expression' kullanın.`,
|
|
413
|
+
guide: 'ngapps://guides/microservice'
|
|
414
|
+
}];
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: 'MS-016',
|
|
419
|
+
module: 'microservice',
|
|
420
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
421
|
+
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. Platformda alan bazlı writeOn (create|update|always) YOK — bu kural o gelene kadarki tek uyarı",
|
|
422
|
+
check: definition => {
|
|
423
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
424
|
+
if (String(parameters.operation ?? '') !== 'upsert')
|
|
425
|
+
return [];
|
|
426
|
+
const generated = asArray(parameters.fields)
|
|
427
|
+
.map(entry => entry)
|
|
428
|
+
.filter(field => String(field.source ?? '') === 'expression')
|
|
429
|
+
.map(field => String(field.alias ?? field.fieldId ?? '?'));
|
|
430
|
+
if (generated.length === 0)
|
|
431
|
+
return [];
|
|
432
|
+
return [{
|
|
433
|
+
ruleId: 'MS-016',
|
|
434
|
+
severity: 'warn',
|
|
435
|
+
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 yazmak istiyorsanız create ve update için iki ayrı uç kurun — platformda alan bazlı 'yalnız create'te yaz' ifadesi henüz yok.`,
|
|
436
|
+
guide: 'ngapps://guides/microservice'
|
|
437
|
+
}];
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'QRY-006',
|
|
442
|
+
module: 'query',
|
|
443
|
+
appliesTo: ['create', 'update'],
|
|
444
|
+
sourceRef: "2026-09-09 koşusu S-1: query_create gövdesine yazılan description komut şemasında YOKTU ve sessizce düştü; alan backend'e eklendi (Query.Description + 20260909075142_AddQueryDescription). MS-014'ün query karşılığı, ama warn: uç açıklaması sorgununkinin yerine geçmez ve block bugün açıklamasız yazan her çağrıyı kırardı",
|
|
445
|
+
check: definition => hasText(definition.description)
|
|
446
|
+
? []
|
|
447
|
+
: [{
|
|
448
|
+
ruleId: 'QRY-006',
|
|
449
|
+
severity: 'warn',
|
|
450
|
+
message: "Sorgunun 'description' alanı boş. 1-2 cümleyle bu sorgunun NE DÖNDÜRDÜĞÜNÜ yazın — sorgu listesinde ad ve slug dışında hiçbir ipucu yok ve aynı datasource üzerinde onlarca ham SQL birikiyor. Ucun kendi açıklaması (MS-014) bunun yerine geçmez: bir sorgu birden çok uçtan tüketilebilir ve uç sözleşmeyi anlatırken sorgu veriyi anlatır.",
|
|
451
|
+
guide: 'ngapps://guides/microservice'
|
|
452
|
+
}]
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'MS-018',
|
|
456
|
+
module: 'microservice',
|
|
457
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
458
|
+
sourceRef: "2026-09-09 koşusu S-2: microservice_create gövdesine yazılan returnDtoId sessizce düştü — platform 200 döndü, uç sözleşmesiz kaldı. Alan Create/UpdateMicroserviceCommand şemasında YOK (additionalProperties:false); sözleşme Workflow.ReturnDtoId'ye ait (WF-002)",
|
|
459
|
+
check: definition => {
|
|
460
|
+
const written = contractKeys.filter(key => definition[key] !== undefined && definition[key] !== null);
|
|
461
|
+
if (written.length === 0)
|
|
462
|
+
return [];
|
|
463
|
+
const workflowId = String(definition.workflowId ?? '');
|
|
464
|
+
const dtoId = String(definition.returnDtoId ?? definition.parameterDtoId ?? '<dtoId>');
|
|
465
|
+
const patch = `workflow_patch(id: '${workflowId === '' ? '<workflowId>' : workflowId}', ops: [{ op: 'add', path: '/returnDtoId', value: '${dtoId}' }, { op: 'add', path: '/returnBehavior', value: 'object' }])`;
|
|
466
|
+
return [{
|
|
467
|
+
ruleId: 'MS-018',
|
|
468
|
+
severity: 'block',
|
|
469
|
+
message: `Uç komutunda DTO sözleşmesi alanı YOK — gövdeye yazılan ${written.join(', ')} platforma gitse 200 dönerdi ve alan SESSİZCE düşerdi; uç sözleşmesiz kalır (ölçüldü 2026-09-09, S-2). table/query/view kaynaklı ucun yanıt şekli kaynağın KENDİSİNDEN türer (selectFields / sorgunun returnScheme'i / view indeksi); açık yanıt sözleşmesi WORKFLOW kavramıdır. Reçete: ucu source:'workflow' + workflowId ile kurun, DTO'yu workflow'a bağlayın — ${patch}. WF-002: type:'workflow' akışta returnDtoId ZORUNLUDUR, returnBehavior'ı boş bırakırsanız gateway 'object' ile doldurur. Sözleşme yalnız 2xx yanıtlarda uygulanır (WF-026). Uç gövdesinden bu alanları çıkarıp yeniden çağırın.`,
|
|
470
|
+
guide: 'ngapps://guides/microservice'
|
|
471
|
+
}];
|
|
472
|
+
}
|
|
473
|
+
},
|
|
303
474
|
{
|
|
304
475
|
id: 'MS-002',
|
|
305
476
|
module: 'microservice',
|
|
@@ -423,17 +594,57 @@ export const policyRules = [
|
|
|
423
594
|
const kind = context.workflowKind ?? body.type;
|
|
424
595
|
if (!kind)
|
|
425
596
|
return [];
|
|
426
|
-
return workflowSteps(body)
|
|
427
|
-
.filter(step => {
|
|
597
|
+
return workflowSteps(body).flatMap(step => {
|
|
428
598
|
const allowedKinds = stepKindMatrix[String(step.type)];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
599
|
+
if (!allowedKinds) {
|
|
600
|
+
return [{
|
|
601
|
+
ruleId: 'WF-005',
|
|
602
|
+
severity: 'block',
|
|
603
|
+
message: `'${stepLabel(step)}' adımındaki '${step.type}' geçerli bir workflow step tipi değil. Geçerli tipler: ${Object.keys(stepKindMatrix).join(', ')}. Akışı bitirmek için ayrı bir end adımı eklemeyin; son dalda nextStepId: null kullanın.`,
|
|
604
|
+
guide: 'ngapps://guides/workflow'
|
|
605
|
+
}];
|
|
606
|
+
}
|
|
607
|
+
if (allowedKinds.includes(kind))
|
|
608
|
+
return [];
|
|
609
|
+
return [{
|
|
610
|
+
ruleId: 'WF-005',
|
|
611
|
+
severity: 'block',
|
|
612
|
+
message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware.`,
|
|
613
|
+
guide: 'ngapps://guides/workflow'
|
|
614
|
+
}];
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: 'WF-MF-001',
|
|
620
|
+
module: 'workflow',
|
|
621
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
622
|
+
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)",
|
|
623
|
+
check: definition => {
|
|
624
|
+
const body = workflowBody(definition);
|
|
625
|
+
return workflowSteps(body)
|
|
626
|
+
.filter(step => step.type === 'microfunction')
|
|
627
|
+
.flatMap((step) => {
|
|
628
|
+
const rule = (step.rule ?? {});
|
|
629
|
+
const microfunctionId = rule.microfunctionId;
|
|
630
|
+
if (!hasText(microfunctionId)) {
|
|
631
|
+
return [{
|
|
632
|
+
ruleId: 'WF-MF-001',
|
|
633
|
+
severity: 'block',
|
|
634
|
+
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).`,
|
|
635
|
+
guide: 'ngapps://guides/microfunction'
|
|
636
|
+
}];
|
|
637
|
+
}
|
|
638
|
+
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))) {
|
|
639
|
+
return [{
|
|
640
|
+
ruleId: 'WF-MF-001',
|
|
641
|
+
severity: 'block',
|
|
642
|
+
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.`,
|
|
643
|
+
guide: 'ngapps://guides/microfunction'
|
|
644
|
+
}];
|
|
645
|
+
}
|
|
646
|
+
return [];
|
|
647
|
+
});
|
|
437
648
|
}
|
|
438
649
|
},
|
|
439
650
|
{
|
|
@@ -524,7 +735,32 @@ export const policyRules = [
|
|
|
524
735
|
return [{
|
|
525
736
|
ruleId: 'WF-024',
|
|
526
737
|
severity: 'block',
|
|
527
|
-
message: `Açıklama eksik — ${parts.join('; ')}. Her adıma ve akışın kendisine 1-2 cümle 'description' yazın: Studio kanvasında adımın yanında ⓘ ikonu olarak görünür ve akışı devralan geliştiricinin tek okuma kaynağıdır. NE YAPTIĞINI değil NEDEN olduğunu yazın: 'assignments tablosunu status=Aktif ile okur' mekanizmadır ve zaten koddan okunuyor; 'kullanıcının asal pozisyonunu seçer, birden fazla aktif atama varsa is_primary olanı tercih eder' niyettir. start
|
|
738
|
+
message: `Açıklama eksik — ${parts.join('; ')}. Her adıma ve akışın kendisine 1-2 cümle 'description' yazın: Studio kanvasında adımın yanında ⓘ ikonu olarak görünür ve akışı devralan geliştiricinin tek okuma kaynağıdır. NE YAPTIĞINI değil NEDEN olduğunu yazın: 'assignments tablosunu status=Aktif ile okur' mekanizmadır ve zaten koddan okunuyor; 'kullanıcının asal pozisyonunu seçer, birden fazla aktif atama varsa is_primary olanı tercih eder' niyettir. start adımı muaftır.`,
|
|
739
|
+
guide: 'ngapps://guides/workflow'
|
|
740
|
+
}];
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
id: 'WF-025',
|
|
745
|
+
module: 'workflow',
|
|
746
|
+
appliesTo: ['create', 'update'],
|
|
747
|
+
sourceRef: "WorkflowService.UpdateAsync adımları silip yeniden yazar; gövdede olmayan bir adıma bakan branches.next_step_id INSERT'te FK'ya takılır. Ölçüldü 2026-09-08 (workflow_patch ile bir adım silindi, kalan dal ona bakıyordu): 400 'This record is in use. Related table: Steps, column: Next Step Id'. Flow tarafındaki karşılığı FLOW-005",
|
|
748
|
+
check: (definition, context) => {
|
|
749
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
750
|
+
const steps = workflowSteps(body);
|
|
751
|
+
if (steps.length === 0)
|
|
752
|
+
return [];
|
|
753
|
+
const stepIds = new Set(steps.map(step => String(step.id ?? '')).filter(id => id !== ''));
|
|
754
|
+
const dangling = steps.flatMap(step => asArray(step.branches)
|
|
755
|
+
.map(branch => ({ branch: branch, nextStepId: String(branch.nextStepId ?? '') }))
|
|
756
|
+
.filter(entry => entry.nextStepId !== '' && !stepIds.has(entry.nextStepId))
|
|
757
|
+
.map(entry => `${stepLabel(step)} · '${entry.branch.name ?? entry.branch.type}' → ${entry.nextStepId}`));
|
|
758
|
+
if (dangling.length === 0)
|
|
759
|
+
return [];
|
|
760
|
+
return [{
|
|
761
|
+
ruleId: 'WF-025',
|
|
762
|
+
severity: 'block',
|
|
763
|
+
message: `Hedefi gövdede olmayan dal var: ${dangling.join(', ')}. Workflow dalının 'nextStepId'si adımın GUID'idir ve platform onu silinmiş adıma bağlayamaz — istek 400 'This record is in use … Next Step Id' ile döner. Adımı silmeden ÖNCE ona bakan dalların 'nextStepId' değerini null yapın (aynı patch çağrısında iki op olarak gönderilebilir).`,
|
|
528
764
|
guide: 'ngapps://guides/workflow'
|
|
529
765
|
}];
|
|
530
766
|
}
|
|
@@ -547,6 +783,32 @@ export const policyRules = [
|
|
|
547
783
|
: [];
|
|
548
784
|
}
|
|
549
785
|
},
|
|
786
|
+
{
|
|
787
|
+
id: 'WF-025',
|
|
788
|
+
module: 'workflow',
|
|
789
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
790
|
+
sourceRef: "Ölçüm 2026-09-07 (canlı, emrah/development): kayıtlı sorgunun parameterScheme'i doluyken library query adımı rule.queryParameters'ı HİÇ okumadı, rule.expression dalına düştü ve parametreler null bağlandı — çağrı 200 + 0 satır döndü, hiçbir katman uyarmadı (teşhis microservice_debug: adımın input girdisi ifadesiz/değersiz). Depo kodu düzeltilmiş (QueryStep.ExecuteLibraryAsync, fe7b923a — önce ruleParameters, expression yedek) ama canlı backend o commit'ten geride: B-63. Kural, dağıtım güncellenince düşer",
|
|
791
|
+
check: (definition, context) => {
|
|
792
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
793
|
+
const unboundSteps = workflowSteps(body)
|
|
794
|
+
.filter(step => step.type === 'query')
|
|
795
|
+
.filter(step => {
|
|
796
|
+
const rule = (step.rule ?? {});
|
|
797
|
+
if (String(rule.queryStepSourceType ?? 'library') === 'custom')
|
|
798
|
+
return false;
|
|
799
|
+
return asArray(rule.queryParameters).length > 0 && String(rule.expression ?? '').trim() === '';
|
|
800
|
+
})
|
|
801
|
+
.map(stepLabel);
|
|
802
|
+
return unboundSteps.length > 0
|
|
803
|
+
? [{
|
|
804
|
+
ruleId: 'WF-025',
|
|
805
|
+
severity: 'warn',
|
|
806
|
+
message: `Query adımı yalnız 'queryParameters' ile bağlanmış (${unboundSteps.join(', ')}) ve 'rule.expression' boş. Kayıtlı sorgunun parametreleri bildirilmişse ÇALIŞAN MOTOR BU DİZİYİ OKUMUYOR: parametreler null bağlanıyor ve çağrı 200 + SIFIR SATIR dönüyor — hata yok, uyarı yok (ölçüldü 2026-09-07; düzeltme depoda var, canlı dağıtım geride — B-63). Aynı bağlamayı 'rule.expression'a nesne literali olarak da yazın: "{ year: input.year, company_id: user.company_id }". Kapsam parametresini middleware'den okumanın yolu da budur (platform_guide('workflow') §'Query adımı').`,
|
|
807
|
+
guide: 'ngapps://guides/workflow'
|
|
808
|
+
}]
|
|
809
|
+
: [];
|
|
810
|
+
}
|
|
811
|
+
},
|
|
550
812
|
{
|
|
551
813
|
id: 'WF-010',
|
|
552
814
|
module: 'workflow',
|
|
@@ -687,6 +949,28 @@ export const policyRules = [
|
|
|
687
949
|
return [];
|
|
688
950
|
}
|
|
689
951
|
},
|
|
952
|
+
{
|
|
953
|
+
id: 'AUT-002',
|
|
954
|
+
module: 'workflow',
|
|
955
|
+
appliesTo: ['create', 'update'],
|
|
956
|
+
sourceRef: "2026-09-05 ölçümü + 2026-09-07 ürün sahibi cevabı (S-7): revert yapısı gereği exception fırlatır ve İKİ koşum yolunda da içinde bulunduğu transaction'ı geri alır — fark, o transaction'ın tetikleyen satır yazmasını kapsayıp kapsamamasıdır. isQueue:false → RowConsumer/AutomationConsumer, automation KENDİ transaction'ında (QueuedEventConsumer) satır COMMIT olduktan sonra koşar; akışın yazdıkları geri alınır, olay 'failed' damgalanır (beklenen iz, kusur değil), çağıran 200'ü çoktan almıştır. isQueue:true → TriggerAutomationWhenRowEventHandlerBase, satır işlemiyle AYNI transaction'da koşar; çağırana 400 { code: revertType, message: revertMessage } döner ve tetikleyen satır da geri alınır. Backend adlandırması: Entity.cs:735-736 GetSyncEventQueueAutomations = IsQueue==true",
|
|
957
|
+
check: (definition, context) => {
|
|
958
|
+
if (context.workflowKind !== 'automation')
|
|
959
|
+
return [];
|
|
960
|
+
const automation = definition.automation;
|
|
961
|
+
if (!automation || typeof automation !== 'object' || automation.isQueue === true)
|
|
962
|
+
return [];
|
|
963
|
+
const steps = [...asArray(definition.steps), ...asArray(automation.steps)];
|
|
964
|
+
if (!steps.some(step => step?.type === 'revert'))
|
|
965
|
+
return [];
|
|
966
|
+
return [{
|
|
967
|
+
ruleId: 'AUT-002',
|
|
968
|
+
severity: 'warn',
|
|
969
|
+
message: "Bu automation 'revert' taşıyor ve isQueue:false — akış RabbitMQ üzerinden ASYNC koşar. O yolda revert kendi transaction'ını geri alır (akışın yazdıkları kalmaz) ve olay 'failed' damgalanır; bu beklenen davranıştır, kapının çalıştığının izidir. Kapsam dışında kalan tek şey tetikleyen satırdır: o revert'e varılmadan COMMIT olmuş ve çağıran 200 almıştır. İş kuralı ÇAĞIRANI reddetmeli ve tetikleyen satır da geri alınmalıysa isQueue:true + queueOrder verin — o yol satır işlemiyle aynı transaction'da koşar ve çağırana 400 { code: revertType, message: revertMessage } döner.",
|
|
970
|
+
guide: 'ngapps://guides/workflow'
|
|
971
|
+
}];
|
|
972
|
+
}
|
|
973
|
+
},
|
|
690
974
|
{
|
|
691
975
|
id: 'MS-004',
|
|
692
976
|
module: 'microservice',
|
|
@@ -935,7 +1219,7 @@ export const policyRules = [
|
|
|
935
1219
|
id: 'ENT-004',
|
|
936
1220
|
module: 'entity',
|
|
937
1221
|
appliesTo: ['create', 'update'],
|
|
938
|
-
sourceRef:
|
|
1222
|
+
sourceRef: "ik-hrm teslim notu PLT-002: display alanı tekil. Backend'de de doğrulayıcı kuralı: CreateEntityCommandValidator / UpdateEntityCommandValidator Fields.Count(Display) <= 1 → 422 DuplicateValue 'Display' (2026-09-08 madenciliği; warn → block, çünkü gövde gitseydi aynı red gelirdi)",
|
|
939
1223
|
check: definition => {
|
|
940
1224
|
const displayFields = asArray(definition.fields)
|
|
941
1225
|
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
@@ -943,13 +1227,142 @@ export const policyRules = [
|
|
|
943
1227
|
return displayFields.length > 1
|
|
944
1228
|
? [{
|
|
945
1229
|
ruleId: 'ENT-004',
|
|
946
|
-
severity: '
|
|
947
|
-
message: `Birden çok display alanı var (${displayFields.map(field => field.slug ?? field.name).join(', ')}) — entity başına yalnız BİR display alanı olabilir.`,
|
|
1230
|
+
severity: 'block',
|
|
1231
|
+
message: `Birden çok display alanı var (${displayFields.map(field => field.slug ?? field.name).join(', ')}) — entity başına yalnız BİR display alanı olabilir; platform 422 "Duplicate value: 'Display'" döner.`,
|
|
948
1232
|
guide: 'ngapps://guides/entity'
|
|
949
1233
|
}]
|
|
950
1234
|
: [];
|
|
951
1235
|
}
|
|
952
1236
|
},
|
|
1237
|
+
{
|
|
1238
|
+
id: 'ENT-013',
|
|
1239
|
+
module: 'entity',
|
|
1240
|
+
appliesTo: ['create', 'update'],
|
|
1241
|
+
sourceRef: "EntityIndexDto.cs: alan adları `unique` ve `fieldIds`, doğrulayıcı FieldIds NotEmpty + tekrarsız, ad önek hariç ≤32; Create/UpdateEntityCommandValidator ad benzersizliğini büyük/küçük harf duyarsız arıyor. B-57 (2026-09-06): 422 yalnız İLK yanlış anahtarı söylediği için `fields`→`fieldIds` öğrenildi ama `isUnique` öğrenilemedi — platform onu yok sayıp index'i UNIQUE OLMADAN kurdu",
|
|
1242
|
+
check: definition => {
|
|
1243
|
+
const indexes = asArray(definition.indexes).filter((index) => Boolean(index) && typeof index === 'object');
|
|
1244
|
+
if (indexes.length === 0)
|
|
1245
|
+
return [];
|
|
1246
|
+
const findings = [];
|
|
1247
|
+
const guide = 'ngapps://guides/entity';
|
|
1248
|
+
const knownFieldIds = new Set(asArray(definition.fields)
|
|
1249
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
1250
|
+
.map(field => field.id)
|
|
1251
|
+
.filter((id) => typeof id === 'string' && id !== ''));
|
|
1252
|
+
const nameCounts = new Map();
|
|
1253
|
+
indexes.forEach((index, position) => {
|
|
1254
|
+
const name = typeof index.name === 'string' ? index.name : '';
|
|
1255
|
+
const label = name === '' ? `indexes[${position}]` : `'${name}'`;
|
|
1256
|
+
if (Object.hasOwn(index, 'isUnique')) {
|
|
1257
|
+
findings.push({
|
|
1258
|
+
ruleId: 'ENT-013',
|
|
1259
|
+
severity: 'block',
|
|
1260
|
+
message: `${label} index'inde 'isUnique' anahtarı var — platform bu adı TANIMAZ, sessizce yok sayar ve index UNIQUE OLMADAN kurulur. Doğru anahtar 'unique'.`,
|
|
1261
|
+
guide
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (Object.hasOwn(index, 'fields')) {
|
|
1265
|
+
findings.push({
|
|
1266
|
+
ruleId: 'ENT-013',
|
|
1267
|
+
severity: 'block',
|
|
1268
|
+
message: `${label} index'inde 'fields' anahtarı var — doğru anahtar 'fieldIds' ve alan ID'si ister, slug değil.`,
|
|
1269
|
+
guide
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
const fieldIds = asArray(index.fieldIds).filter((id) => typeof id === 'string' && id !== '');
|
|
1274
|
+
if (fieldIds.length === 0) {
|
|
1275
|
+
findings.push({
|
|
1276
|
+
ruleId: 'ENT-013',
|
|
1277
|
+
severity: 'block',
|
|
1278
|
+
message: `${label} index'inin 'fieldIds' alanı boş — platform 422 döner ("'Field Ids' must not be empty"). Alan id'lerini entity_get view:'outline' çıktısından alın.`,
|
|
1279
|
+
guide
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (new Set(fieldIds).size !== fieldIds.length) {
|
|
1283
|
+
findings.push({
|
|
1284
|
+
ruleId: 'ENT-013',
|
|
1285
|
+
severity: 'block',
|
|
1286
|
+
message: `${label} index'inde aynı alan birden çok kez var — 'fieldIds' tekrar edemez.`,
|
|
1287
|
+
guide
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const foreign = knownFieldIds.size === 0 ? [] : fieldIds.filter(id => !knownFieldIds.has(id));
|
|
1291
|
+
if (foreign.length > 0) {
|
|
1292
|
+
findings.push({
|
|
1293
|
+
ruleId: 'ENT-013',
|
|
1294
|
+
severity: 'warn',
|
|
1295
|
+
message: `${label} index'i gövdedeki hiçbir alana ait olmayan id taşıyor (${foreign.join(', ')}). Alan id'lerini platform ÜRETİR: entity'yi önce kurup id'leri okuyun ya da 'fields[].id'leri kendiniz verip aynı guid'leri kullanın. Yollarsanız platform yanıltıcı bir mesajla reddeder: "This record is in use. Related table: 'Fields', column: 'Field Id'." — ortada bağımlılık yoktur, alan yoktur.`,
|
|
1296
|
+
guide
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (name === '') {
|
|
1301
|
+
findings.push({
|
|
1302
|
+
ruleId: 'ENT-013',
|
|
1303
|
+
severity: 'block',
|
|
1304
|
+
message: `indexes[${position}] adsız — 'name' zorunludur.`,
|
|
1305
|
+
guide
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
// Uzunluk sınırı ÖNEK HARİÇ ölçülür: platform ada `IX_<5 hane>_` ekler ve okuma yolu
|
|
1310
|
+
// önekli adı döndürür, yani bir gidiş-dönüş gövdesi sırf önek yüzünden ihlal etmemeli.
|
|
1311
|
+
if (name.replace(/^IX_[0-9a-fA-F]{5}_/, '').length > 32) {
|
|
1312
|
+
findings.push({
|
|
1313
|
+
ruleId: 'ENT-013',
|
|
1314
|
+
severity: 'block',
|
|
1315
|
+
message: `${label} index adı çok uzun — platformun eklediği 'IX_<5 hane>_' öneki hariç en fazla 32 karakter olabilir.`,
|
|
1316
|
+
guide
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
const key = name.toLocaleLowerCase('tr');
|
|
1320
|
+
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
|
|
1321
|
+
});
|
|
1322
|
+
for (const [name, count] of nameCounts) {
|
|
1323
|
+
if (count < 2)
|
|
1324
|
+
continue;
|
|
1325
|
+
findings.push({
|
|
1326
|
+
ruleId: 'ENT-013',
|
|
1327
|
+
severity: 'block',
|
|
1328
|
+
message: `'${name}' adı ${count} index'te birden kullanılmış — index adları entity içinde büyük/küçük harf duyarsız benzersizdir.`,
|
|
1329
|
+
guide
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return findings;
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
id: 'ENT-011',
|
|
1337
|
+
module: 'entity',
|
|
1338
|
+
appliesTo: ['create', 'update'],
|
|
1339
|
+
sourceRef: "FieldDto.cs:170-178 When(IsLocalized): PrimaryKey/Identity/Unique false, RelationType None, CommonType Text zorunlu — beşi de platform validator'ında hard 400 (OnlyTextCanBeLocalizedException). İstek gitmeden yakalanır",
|
|
1340
|
+
check: definition => {
|
|
1341
|
+
const findings = [];
|
|
1342
|
+
for (const field of asArray(definition.fields).filter((entry) => Boolean(entry) && typeof entry === 'object')) {
|
|
1343
|
+
if (field.isLocalized !== true)
|
|
1344
|
+
continue;
|
|
1345
|
+
const name = String(field.slug ?? field.name ?? '?');
|
|
1346
|
+
const relationType = String(field.relationType ?? 'none').toLowerCase();
|
|
1347
|
+
const conflicts = [
|
|
1348
|
+
field.primaryKey === true ? 'primaryKey' : null,
|
|
1349
|
+
field.identity === true ? 'identity' : null,
|
|
1350
|
+
field.unique === true ? 'unique' : null,
|
|
1351
|
+
relationType !== 'none' && relationType !== '' ? `relationType: '${field.relationType}'` : null,
|
|
1352
|
+
field.commonType !== undefined && field.commonType !== 'text' ? `commonType: '${field.commonType}'` : null
|
|
1353
|
+
].filter((entry) => entry !== null);
|
|
1354
|
+
if (conflicts.length === 0)
|
|
1355
|
+
continue;
|
|
1356
|
+
findings.push({
|
|
1357
|
+
ruleId: 'ENT-011',
|
|
1358
|
+
severity: 'block',
|
|
1359
|
+
message: `'${name}' alanı isLocalized ama ${conflicts.join(', ')} taşıyor — platform yalnız düz METİN alanını çok dilli yapar. Bu gövde 400 ile reddedilir (FieldDto validator'ı isLocalized alanında primaryKey/identity/unique false, relationType 'none' ve commonType 'text' ister). Çeviriyi metin alanında tutun; anahtar, benzersizlik ve ilişki ana tabloda kalır. Ayrıntı: platform_guide('entity') §'Çoklu dilli alan'.`,
|
|
1360
|
+
guide: 'ngapps://guides/entity'
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
return findings;
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
953
1366
|
{
|
|
954
1367
|
id: 'MS-007',
|
|
955
1368
|
module: 'microservice',
|
|
@@ -1355,6 +1768,203 @@ export const policyRules = [
|
|
|
1355
1768
|
}];
|
|
1356
1769
|
})
|
|
1357
1770
|
},
|
|
1771
|
+
{
|
|
1772
|
+
id: 'FN-001',
|
|
1773
|
+
module: 'function',
|
|
1774
|
+
appliesTo: ['create', 'update'],
|
|
1775
|
+
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)",
|
|
1776
|
+
check: definition => {
|
|
1777
|
+
const name = String(definition.name ?? '');
|
|
1778
|
+
const source = sourceOf(definition);
|
|
1779
|
+
const findings = [];
|
|
1780
|
+
const problem = nameProblem(definition.name, source);
|
|
1781
|
+
if (problem !== undefined) {
|
|
1782
|
+
findings.push({
|
|
1783
|
+
ruleId: 'FN-001',
|
|
1784
|
+
severity: 'block',
|
|
1785
|
+
message: source === 'workflow'
|
|
1786
|
+
? `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.`
|
|
1787
|
+
: `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.`,
|
|
1788
|
+
guide: functionsGuide
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
for (const parameter of asArray(definition.parameters).filter(isRecord)) {
|
|
1792
|
+
if (isValidParameterName(parameter.name))
|
|
1793
|
+
continue;
|
|
1794
|
+
findings.push({
|
|
1795
|
+
ruleId: 'FN-001',
|
|
1796
|
+
severity: 'block',
|
|
1797
|
+
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.`,
|
|
1798
|
+
guide: functionsGuide
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
return findings;
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
id: 'FN-002',
|
|
1806
|
+
module: 'function',
|
|
1807
|
+
appliesTo: ['create', 'update'],
|
|
1808
|
+
sourceRef: 'CreateGlobalFunctionCommandValidator: parametre adı tekrarı → 422 GlobalFunctionParameterNameDuplicate; bildirim parametreleri konumsal bağlar, aynı ad ikinci değeri gölgeler',
|
|
1809
|
+
check: definition => {
|
|
1810
|
+
const names = asArray(definition.parameters).filter(isRecord).map(parameter => String(parameter.name ?? ''));
|
|
1811
|
+
const duplicates = [...new Set(names.filter((entry, index) => entry !== '' && names.indexOf(entry) !== index))];
|
|
1812
|
+
return duplicates.length === 0
|
|
1813
|
+
? []
|
|
1814
|
+
: [{
|
|
1815
|
+
ruleId: 'FN-002',
|
|
1816
|
+
severity: 'block',
|
|
1817
|
+
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.`,
|
|
1818
|
+
guide: functionsGuide
|
|
1819
|
+
}];
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
id: 'FN-003',
|
|
1824
|
+
module: 'function',
|
|
1825
|
+
appliesTo: ['create', 'update'],
|
|
1826
|
+
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)',
|
|
1827
|
+
check: definition => {
|
|
1828
|
+
const name = String(definition.name ?? '');
|
|
1829
|
+
const source = sourceOf(definition);
|
|
1830
|
+
const body = typeof definition.body === 'string' ? definition.body : '';
|
|
1831
|
+
if (name === '' || !callsItself(name, body, source))
|
|
1832
|
+
return [];
|
|
1833
|
+
return [{
|
|
1834
|
+
ruleId: 'FN-003',
|
|
1835
|
+
severity: 'warn',
|
|
1836
|
+
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.`,
|
|
1837
|
+
guide: functionsGuide
|
|
1838
|
+
}];
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
id: 'FN-004',
|
|
1843
|
+
module: 'function',
|
|
1844
|
+
appliesTo: ['create', 'update'],
|
|
1845
|
+
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)",
|
|
1846
|
+
check: definition => {
|
|
1847
|
+
const name = String(definition.name ?? '?');
|
|
1848
|
+
const body = typeof definition.body === 'string' ? definition.body : '';
|
|
1849
|
+
const isAsync = definition.isAsync === true;
|
|
1850
|
+
if (isAsync && sourceOf(definition) === 'workflow') {
|
|
1851
|
+
return [{
|
|
1852
|
+
ruleId: 'FN-004',
|
|
1853
|
+
severity: 'block',
|
|
1854
|
+
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.`,
|
|
1855
|
+
guide: functionsGuide
|
|
1856
|
+
}];
|
|
1857
|
+
}
|
|
1858
|
+
if (isAsync && !mentionsAwait(body)) {
|
|
1859
|
+
return [{
|
|
1860
|
+
ruleId: 'FN-004',
|
|
1861
|
+
severity: 'warn',
|
|
1862
|
+
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.`,
|
|
1863
|
+
guide: functionsGuide
|
|
1864
|
+
}];
|
|
1865
|
+
}
|
|
1866
|
+
if (!isAsync && mentionsAwait(body)) {
|
|
1867
|
+
return [{
|
|
1868
|
+
ruleId: 'FN-004',
|
|
1869
|
+
severity: 'warn',
|
|
1870
|
+
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.`,
|
|
1871
|
+
guide: functionsGuide
|
|
1872
|
+
}];
|
|
1873
|
+
}
|
|
1874
|
+
return [];
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
id: 'FN-005',
|
|
1879
|
+
module: 'function',
|
|
1880
|
+
appliesTo: ['create'],
|
|
1881
|
+
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)",
|
|
1882
|
+
check: definition => {
|
|
1883
|
+
const name = String(definition.name ?? '?');
|
|
1884
|
+
const source = sourceOf(definition);
|
|
1885
|
+
const has = (key) => definition[key] !== undefined && definition[key] !== null && definition[key] !== '';
|
|
1886
|
+
const block = (message) => ({ ruleId: 'FN-005', severity: 'block', message, guide: functionsGuide });
|
|
1887
|
+
if (source === 'workflow') {
|
|
1888
|
+
const extras = ['applicationId', 'pageId', 'scope'].filter(has);
|
|
1889
|
+
return extras.length === 0
|
|
1890
|
+
? []
|
|
1891
|
+
: [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'.`)];
|
|
1892
|
+
}
|
|
1893
|
+
if (!has('applicationId'))
|
|
1894
|
+
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'.`)];
|
|
1895
|
+
if (!has('scope'))
|
|
1896
|
+
return [block(`'${name}' source:'studio' ama scope yok — 'application' (her sayfada) ya da 'page' (tek sayfa, pageId zorunlu) seçin (platform 422).`)];
|
|
1897
|
+
if (definition.scope === 'page' && !has('pageId'))
|
|
1898
|
+
return [block(`'${name}' scope:'page' ama pageId yok — platform 422 GlobalFunctionScopePageRequired verir; page_list ile sayfa id'sini alın.`)];
|
|
1899
|
+
if (definition.scope === 'application' && has('pageId'))
|
|
1900
|
+
return [block(`'${name}' scope:'application' fonksiyonda pageId verilmez — platform 422 GlobalFunctionScopeApplicationPageForbidden verir; sayfaya bağlamak istiyorsanız scope:'page'.`)];
|
|
1901
|
+
return [];
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
id: 'FLOW-024',
|
|
1906
|
+
module: 'flow',
|
|
1907
|
+
appliesTo: ['create', 'update'],
|
|
1908
|
+
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',
|
|
1909
|
+
check: definition => flowAllSteps(definition)
|
|
1910
|
+
.filter(step => step.type === 'function')
|
|
1911
|
+
.flatMap(step => {
|
|
1912
|
+
const args = (step.args ?? {});
|
|
1913
|
+
const label = flowStepLabel(step);
|
|
1914
|
+
const findings = [];
|
|
1915
|
+
if (typeof args.functionId !== 'string' || !conditionRuleIdPattern.test(args.functionId)) {
|
|
1916
|
+
findings.push({
|
|
1917
|
+
ruleId: 'FLOW-024',
|
|
1918
|
+
severity: 'block',
|
|
1919
|
+
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).`,
|
|
1920
|
+
guide: functionsGuide
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
if (typeof args.functionName !== 'string' || args.functionName.trim() === '') {
|
|
1924
|
+
findings.push({
|
|
1925
|
+
ruleId: 'FLOW-024',
|
|
1926
|
+
severity: 'warn',
|
|
1927
|
+
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.`,
|
|
1928
|
+
guide: functionsGuide
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
if (args.parameters !== undefined && args.parameters !== null && !Array.isArray(args.parameters)) {
|
|
1932
|
+
findings.push({
|
|
1933
|
+
ruleId: 'FLOW-024',
|
|
1934
|
+
severity: 'block',
|
|
1935
|
+
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 } }].`,
|
|
1936
|
+
guide: functionsGuide
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
const nameless = asArray(args.parameters).filter(isRecord).filter(parameter => typeof parameter.name !== 'string' || parameter.name.trim() === '');
|
|
1940
|
+
if (nameless.length > 0) {
|
|
1941
|
+
findings.push({
|
|
1942
|
+
ruleId: 'FLOW-024',
|
|
1943
|
+
severity: 'block',
|
|
1944
|
+
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.`,
|
|
1945
|
+
guide: functionsGuide
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
return findings;
|
|
1949
|
+
})
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
id: 'WF-030',
|
|
1953
|
+
module: 'workflow',
|
|
1954
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1955
|
+
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",
|
|
1956
|
+
check: (definition, context) => {
|
|
1957
|
+
const names = [...new Set(functionCallRefs(JSON.stringify(workflowBody(definition, context.workflowKind) ?? {})).map(ref => ref.name))];
|
|
1958
|
+
return names.length === 0
|
|
1959
|
+
? []
|
|
1960
|
+
: [{
|
|
1961
|
+
ruleId: 'WF-030',
|
|
1962
|
+
severity: 'block',
|
|
1963
|
+
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ı).`,
|
|
1964
|
+
guide: functionsGuide
|
|
1965
|
+
}];
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1358
1968
|
{
|
|
1359
1969
|
id: 'FLOW-005',
|
|
1360
1970
|
module: 'flow',
|
|
@@ -1460,6 +2070,199 @@ export const policyRules = [
|
|
|
1460
2070
|
});
|
|
1461
2071
|
}
|
|
1462
2072
|
},
|
|
2073
|
+
{
|
|
2074
|
+
id: 'FLOW-019',
|
|
2075
|
+
module: 'flow',
|
|
2076
|
+
appliesTo: ['create', 'update'],
|
|
2077
|
+
sourceRef: "code.ts:18 kod gövdesini `task.argument?.value` alanından okur ve boşsa adımı NO-OP sayıp engine.next('always') çağırır. Gövde `args.value` alanındadır; `args.expression`/`args.script` gibi uydurma anahtarlar StepManagementArgument sözleşmesinde YOKTUR ve platform onları sessizce düşürür (ölçüldü 2026-09-05, Corecruiter koşusu S-19: akış koştu, filtre hiçbir şey yapmadı, teşhis ~10 dk sürdü)",
|
|
2078
|
+
check: definition => flowAllSteps(definition)
|
|
2079
|
+
.filter(step => step.type === 'code')
|
|
2080
|
+
.flatMap((step) => {
|
|
2081
|
+
const args = (step.args ?? {});
|
|
2082
|
+
const body = typeof args.value === 'string' ? args.value.trim() : '';
|
|
2083
|
+
if (body.length > 0)
|
|
2084
|
+
return [];
|
|
2085
|
+
const strays = ['expression', 'script', 'code', 'body', 'source']
|
|
2086
|
+
.filter(key => typeof args[key] === 'string' && String(args[key]).trim().length > 0);
|
|
2087
|
+
return [{
|
|
2088
|
+
ruleId: 'FLOW-019',
|
|
2089
|
+
severity: 'block',
|
|
2090
|
+
message: `'${flowStepLabel(step)}' code adımının gövdesi yok. Kod JS METNİ olarak args.value alanında durur — runtime onu \`(async () => { <value> })()\` içinde koşturur; alan boşken adım hata vermeden NO-OP olur ve akış hiçbir şey yapmadan devam eder.${strays.length > 0 ? ` Gövdeyi args.${strays.join('/args.')} alanına yazmışsınız: bu anahtar StepManagementArgument sözleşmesinde YOK ve platform onu sessizce düşürür — args.value olarak yazın.` : ''} Değeri bir komponentten okuyacaksanız code adımına hiç gerek yoktur: parametreye value { type: 'property', ref: { source: 'component', element, property } } verin.`,
|
|
2091
|
+
guide: 'ngapps://guides/page'
|
|
2092
|
+
}];
|
|
2093
|
+
})
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
id: 'FLOW-020',
|
|
2097
|
+
module: 'flow',
|
|
2098
|
+
appliesTo: ['create', 'update'],
|
|
2099
|
+
sourceRef: "ng-apps-flow/tasks/condition/condition.ts + condition.types.ts: gövde `task.argument.rules: [{ id, expression, type: 'if'|'else-if'|'else' }]`; process() YALNIZ rules[0].type==='if' iken başlar, dal adı rule.id'dir (engine.next(rule.id) → Runtime.svelte:330 branch.name→branch.id → flow-engine.ts:93), 'else' yalnız expression BOŞKEN fallback'tir, eşleşmeyen kural rules[index+1]'e geçer. rule.id backend'de Guid'dir (StepManagementSchema.cs `public Guid Id { get; set; } = Guid.CreateVersion7()`): okunabilir ad System.Guid çevrimiyle reddedilir, id hiç verilmezse backend kendi GUID'ini üretir ve branches[].name ölü kalır. 'always' yalnız EŞLEŞEN dalın completed/stopped'ından çağrılır — rules boş / rules[0].type!=='if' / hiçbir kural tutmuyor ve boş ifadeli else yok hâllerinde engine.next('always') HİÇ çalışmaz. Backend workflow'unun rule.items[].branchId şekli (StepArgument.cs StepRule) sayfa akışında HİÇ okunmaz — 2026-09-06·2 koşusu S-12: istek 200, adım kaydedildi, Studio çizdi, motor kuralı hiç değerlendirmedi",
|
|
2100
|
+
check: definition => flowAllSteps(definition)
|
|
2101
|
+
.filter(step => step.type === 'condition')
|
|
2102
|
+
.flatMap((step) => {
|
|
2103
|
+
const args = (step.args ?? {});
|
|
2104
|
+
const rules = asArray(args.rules).filter((rule) => Boolean(rule) && typeof rule === 'object');
|
|
2105
|
+
const label = flowStepLabel(step);
|
|
2106
|
+
const shape = "args.rules: [{ id: '<TAZE GUID>', expression: '<JS ifadesi>', type: 'if' }, { id: '<TAZE GUID>', expression: '', type: 'else' }] — id GERÇEK bir GUID olmalı (backend Guid'e çevirir) ve her rule.id için branches: [{ name: '<aynı GUID>', nextStepId: '<hedef stepId>' }]";
|
|
2107
|
+
if (rules.length === 0) {
|
|
2108
|
+
const workflowShape = args.rule !== undefined || args.items !== undefined || step.rule !== undefined;
|
|
2109
|
+
return [{
|
|
2110
|
+
ruleId: 'FLOW-020',
|
|
2111
|
+
severity: 'block',
|
|
2112
|
+
message: workflowShape
|
|
2113
|
+
? `'${label}' condition adımı BACKEND WORKFLOW şeklini taşıyor (rule/items). Sayfa akışının motoru başka bir motordur ve yalnız 'args.rules' okur (condition.ts); workflow şekli 200 ile kaydedilir, Studio çizer ve kural HİÇ değerlendirilmez — akış sessizce dalsız kalır. Doğru gövde: ${shape}`
|
|
2114
|
+
: `'${label}' condition adımında 'args.rules' yok ya da boş. Motor kural listesi boşken hiçbir dala girmez ve akış o adımda sessizce biter (condition.ts). Doğru gövde: ${shape}`,
|
|
2115
|
+
guide: 'ngapps://guides/page'
|
|
2116
|
+
}];
|
|
2117
|
+
}
|
|
2118
|
+
const findings = [];
|
|
2119
|
+
const first = rules[0];
|
|
2120
|
+
if (first.type !== 'if') {
|
|
2121
|
+
findings.push({
|
|
2122
|
+
ruleId: 'FLOW-020',
|
|
2123
|
+
severity: 'block',
|
|
2124
|
+
message: `'${label}' condition adımının İLK kuralı type:'${String(first.type ?? '')}' — motor zinciri yalnız rules[0].type === 'if' iken başlatır (condition.ts), aksi hâlde adım hiçbir dala girmez ve akış sessizce biter. Zincirin başına 'if' kuralını koyun; devamı 'else-if', kapanışı boş ifadeli 'else' olur.`,
|
|
2125
|
+
guide: 'ngapps://guides/page'
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
// YALNIZ `name` — motor dalı adıyla arar (Runtime.svelte:330 `branch.name || ''`).
|
|
2129
|
+
// `id` GUID'dir; kümeye karıştırılırsa hiçbir rule id'si tutmaz ve kural sahte blok üretir.
|
|
2130
|
+
const branchNames = new Set(flowBranches(step)
|
|
2131
|
+
.map(branch => (typeof branch.name === 'string' ? branch.name : ''))
|
|
2132
|
+
.filter(name => name.length > 0));
|
|
2133
|
+
for (const rule of rules) {
|
|
2134
|
+
const id = typeof rule.id === 'string' ? rule.id : '';
|
|
2135
|
+
const type = String(rule.type ?? '');
|
|
2136
|
+
const expression = typeof rule.expression === 'string' ? rule.expression : '';
|
|
2137
|
+
const ruleLabel = id || type || '?';
|
|
2138
|
+
if (id.length === 0) {
|
|
2139
|
+
findings.push({
|
|
2140
|
+
ruleId: 'FLOW-020',
|
|
2141
|
+
severity: 'block',
|
|
2142
|
+
message: `'${label}' condition adımının '${type || '?'}' kuralında 'id' yok. Dal adı kuralın id'sidir (engine.next(rule.id)); id vermezseniz backend kendi GUID'ini üretir (StepManagementSchema.cs \`Guid.CreateVersion7()\`), yazdığınız branches[].name ona ASLA eşleşmez ve dal sessizce ölür. Taze bir GUID üretip hem rule.id'ye hem branches[].name'e aynısını yazın.`,
|
|
2143
|
+
guide: 'ngapps://guides/page'
|
|
2144
|
+
});
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
if (!conditionRuleIdPattern.test(id)) {
|
|
2148
|
+
findings.push({
|
|
2149
|
+
ruleId: 'FLOW-020',
|
|
2150
|
+
severity: 'block',
|
|
2151
|
+
message: `'${label}' condition adımının '${ruleLabel}' kuralında id GUID değil. Backend bu alanı Guid'e çevirir (StepManagementSchema.cs) ve okunabilir bir ad isteği \`The JSON value could not be converted to System.Guid\` ile reddeder. Dal adı da aynı GUID olmak zorunda olduğu için iki yerde birden okunabilir ad kullanamazsınız: taze bir GUID üretip hem rule.id'ye hem branches[].name'e yazın.`,
|
|
2152
|
+
guide: 'ngapps://guides/page'
|
|
2153
|
+
});
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (!conditionRuleTypes.has(type)) {
|
|
2157
|
+
findings.push({
|
|
2158
|
+
ruleId: 'FLOW-020',
|
|
2159
|
+
severity: 'block',
|
|
2160
|
+
message: `'${label}' condition adımının '${ruleLabel}' kuralında type:'${type}' geçersiz — kabul edilen küme 'if' · 'else-if' · 'else' (condition.types.ts).`,
|
|
2161
|
+
guide: 'ngapps://guides/page'
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (type === 'else' && expression.trim().length > 0) {
|
|
2165
|
+
findings.push({
|
|
2166
|
+
ruleId: 'FLOW-020',
|
|
2167
|
+
severity: 'warn',
|
|
2168
|
+
message: `'${label}' condition adımının '${ruleLabel}' kuralı type:'else' ama 'expression' dolu. Motor else dalını YALNIZ ifade boşken fallback sayar (condition.ts \`!rule.expression && rule.type === 'else'\`); dolu else normal bir karşılaştırmaya döner. Hiçbir kural tutmazsa akış dalsız biter ve 'always' dalı DA koşmaz — motor always'i yalnız eşleşen dalın completed/stopped'ından çağırır. Fallback istiyorsanız expression'ı boş bırakın.`,
|
|
2169
|
+
guide: 'ngapps://guides/page'
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (branchNames.size > 0 && !branchNames.has(id)) {
|
|
2173
|
+
findings.push({
|
|
2174
|
+
ruleId: 'FLOW-020',
|
|
2175
|
+
severity: 'block',
|
|
2176
|
+
message: `'${label}' condition adımının '${ruleLabel}' kuralının id'si hiçbir branches[].name ile eşleşmiyor. Motor dalı adıyla arar (Runtime.svelte:330 branch.name → branch.id, flow-engine.ts:93) — eşleşme yoksa kural tutsa bile akış o dalda durur. branches'a { name: '${id}', nextStepId: '<hedef stepId>' } ekleyin.`,
|
|
2177
|
+
guide: 'ngapps://guides/page'
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Hiç 'else' kuralı yoksa fallback de yoktur. Dolu ifadeli else zaten yukarıda
|
|
2182
|
+
// uyarıldığı için burada YALNIZ else'siz zinciri raporluyoruz — aynı derdi iki kez
|
|
2183
|
+
// söylemeyelim.
|
|
2184
|
+
if (!rules.some(rule => String(rule.type ?? '') === 'else')) {
|
|
2185
|
+
findings.push({
|
|
2186
|
+
ruleId: 'FLOW-020',
|
|
2187
|
+
severity: 'warn',
|
|
2188
|
+
message: `'${label}' condition adımında 'else' kuralı yok. Hiçbir ifade tutmazsa motor zinciri sessizce bitirir: akış dalsız kalır ve 'always' dalı DA koşmaz — always yalnız eşleşen dalın completed/stopped'ından tetiklenir (condition.ts). Yükleniyor bayrağını always ile kapatıyorsanız bayrak açık kalır. Zinciri boş ifadeli bir { type: 'else', expression: '' } kuralıyla kapatın.`,
|
|
2189
|
+
guide: 'ngapps://guides/page'
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
return findings;
|
|
2193
|
+
})
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
id: 'FLOW-021',
|
|
2197
|
+
module: 'flow',
|
|
2198
|
+
appliesTo: ['create', 'update'],
|
|
2199
|
+
sourceRef: "flow-engine.ts hasErrorFlow(): `flow.errorTasks.some(t => t.type !== 'start')` — yalnız start taşıyan errorSteps hata yolu SAYILMAZ, onError false döner ve akış hata gövdesiyle 'always' dalından devam eder (2026-09-06·2 koşusu S-13)",
|
|
2200
|
+
check: definition => {
|
|
2201
|
+
const errorSteps = flowStepList(definition.errorSteps);
|
|
2202
|
+
if (errorSteps.length === 0)
|
|
2203
|
+
return [];
|
|
2204
|
+
return errorSteps.some(step => step.type !== 'start')
|
|
2205
|
+
? []
|
|
2206
|
+
: [{
|
|
2207
|
+
ruleId: 'FLOW-021',
|
|
2208
|
+
severity: 'warn',
|
|
2209
|
+
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: ucun kendi mesajını göstereceksen setter (value.type:'code' → Steps.error.message), sabit bir uyarı yetiyorsa alert — alert'in 'message' alanı DÜZ METİNDİR (alert.ts task.argument.message'ı doğrudan toast'a geçirir), oraya ifade yazarsanız kullanıcı ifadenin kendisini görür.",
|
|
2210
|
+
guide: 'ngapps://guides/page'
|
|
2211
|
+
}];
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
id: 'FLOW-022',
|
|
2216
|
+
module: 'flow',
|
|
2217
|
+
appliesTo: ['create', 'update'],
|
|
2218
|
+
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(task.argument.message)`. 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)",
|
|
2219
|
+
check: definition => flowAllSteps(definition)
|
|
2220
|
+
.flatMap((step) => {
|
|
2221
|
+
const type = String(step.type ?? '');
|
|
2222
|
+
const fields = flowLiteralTextFields.get(type);
|
|
2223
|
+
if (!fields)
|
|
2224
|
+
return [];
|
|
2225
|
+
const args = (step.args ?? {});
|
|
2226
|
+
return fields
|
|
2227
|
+
.filter(field => typeof args[field] === 'string' && flowExpressionLooking.test(args[field]))
|
|
2228
|
+
.map(field => ({
|
|
2229
|
+
ruleId: 'FLOW-022',
|
|
2230
|
+
severity: 'warn',
|
|
2231
|
+
message: `'${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.`,
|
|
2232
|
+
guide: 'ngapps://guides/page'
|
|
2233
|
+
}));
|
|
2234
|
+
})
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
id: 'FLOW-023',
|
|
2238
|
+
module: 'flow',
|
|
2239
|
+
appliesTo: ['create', 'update'],
|
|
2240
|
+
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",
|
|
2241
|
+
check: definition => {
|
|
2242
|
+
const steps = flowAllSteps(definition);
|
|
2243
|
+
// Studio her akış tuvaline dekoratif bir End düğümü koyar ve onu sildirmez; zararlı olan
|
|
2244
|
+
// düğümün VARLIĞI değil, bir dalın oraya İŞARET ETMESİDİR — motor o adıma girer ve çıkamaz.
|
|
2245
|
+
const targeted = new Set(steps.flatMap(step => flowBranches(step)
|
|
2246
|
+
.map(branch => String(branch.nextStepId ?? ''))
|
|
2247
|
+
.filter(id => id.length > 0)));
|
|
2248
|
+
const silentStop = "Register edilmemiş bir 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. Akışın kalanı ölür ve run() Promise'i çözülmediği için flowRunFinished() hiç koşmaz; __ngAppsFlowActivity.running sıfıra inmez ve page_screenshot'ın hazır bekleyişi her eylemde bütçesini sonuna kadar yakar.";
|
|
2249
|
+
return steps.flatMap((step) => {
|
|
2250
|
+
const type = String(step.type ?? '');
|
|
2251
|
+
if (flowUnregisteredStepTypes.has(type)) {
|
|
2252
|
+
const reachable = type !== 'end' || targeted.has(String(step.stepId ?? ''));
|
|
2253
|
+
if (!reachable)
|
|
2254
|
+
return [];
|
|
2255
|
+
return [{
|
|
2256
|
+
ruleId: 'FLOW-023',
|
|
2257
|
+
severity: 'block',
|
|
2258
|
+
message: `'${flowStepLabel(step)}' adımının tipi '${type}' ve bir dal oraya işaret ediyor — bu tipi HİÇBİR barındırıcı register etmiyor (ne flow-engine.ts, ne web Runtime.svelte, ne mobil ScreenRuntime.tsx); Studio da onu eklenebilir adımlar arasında göstermez. ${silentStop} Akışı bitirmek için ayrı bir adım gerekmez: son adımın dalına nextStepId: null verin. Dekoratif bir '${type}' düğümü tutmak serbesttir, yeter ki hiçbir dal ona işaret etmesin.`,
|
|
2259
|
+
guide: 'ngapps://guides/page'
|
|
2260
|
+
}];
|
|
2261
|
+
}
|
|
2262
|
+
return [];
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
},
|
|
1463
2266
|
{
|
|
1464
2267
|
id: 'FLOW-017',
|
|
1465
2268
|
module: 'flow',
|
|
@@ -1624,31 +2427,32 @@ export const policyRules = [
|
|
|
1624
2427
|
const microserviceStepsByName = new Map(steps
|
|
1625
2428
|
.filter(step => step.type === 'microservice' && step.name)
|
|
1626
2429
|
.map(step => [String(step.name), step]));
|
|
1627
|
-
return steps.flatMap(step =>
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
const name = match[1] ?? match[2] ?? '';
|
|
2430
|
+
return steps.flatMap(step => {
|
|
2431
|
+
const seen = new Set();
|
|
2432
|
+
return flowCodeExpressions(step).flatMap(({ expression, origin }) => headersAccessNames(expression).flatMap((name) => {
|
|
2433
|
+
if (seen.has(name))
|
|
2434
|
+
return [];
|
|
1633
2435
|
const source = microserviceStepsByName.get(name);
|
|
1634
2436
|
if (source && (source.args ?? {}).isRaw === true) {
|
|
1635
|
-
|
|
2437
|
+
seen.add(name);
|
|
2438
|
+
return [{
|
|
2439
|
+
ruleId: 'FLOW-011',
|
|
2440
|
+
severity: 'block',
|
|
2441
|
+
message: `'${flowStepLabel(step)}' adımının ${origin} ifadesi Steps.${name}.headers okuyor ama '${name}' adımı isRaw=true ile çalışıyor — o zaman storage ham response.data'dır, 'headers' diye bir alan HİÇ yazılmaz (microservice.ts:88). İfade her koşuda TypeError verir. isRaw'ı kaldırın (normalize false'a çeker) ya da header okumaktan vazgeçin.`,
|
|
2442
|
+
guide: 'ngapps://guides/page'
|
|
2443
|
+
}];
|
|
2444
|
+
}
|
|
2445
|
+
if (isHeadersAccessGuarded(expression, name))
|
|
2446
|
+
return [];
|
|
2447
|
+
seen.add(name);
|
|
2448
|
+
return [{
|
|
1636
2449
|
ruleId: 'FLOW-011',
|
|
1637
|
-
severity: '
|
|
1638
|
-
message: `'${flowStepLabel(step)}' adımının ${origin}
|
|
2450
|
+
severity: 'warn',
|
|
2451
|
+
message: `'${flowStepLabel(step)}' adımının ${origin} ifadesinde Steps.${name}.headers guard'sız okunuyor. İstek HATA aldığında storage { error, data } olarak yazılır ve 'headers' alanı yoktur (microservice.ts:132) → TypeError; runCode undefined döner (prop'a NaN yazılır), ifade hatası toast'ı düşer ve flow-engine.ts:53 akışı onError'a çevirip durdurabilir. Guard'lı yazın: Number(Steps.${name}?.headers?.["total-count"] ?? 0) ya da Steps.${name}.headers && … önkoşulu.`,
|
|
1639
2452
|
guide: 'ngapps://guides/page'
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
findings.push({
|
|
1644
|
-
ruleId: 'FLOW-011',
|
|
1645
|
-
severity: 'warn',
|
|
1646
|
-
message: `'${flowStepLabel(step)}' adımının ${origin} ifadesinde Steps.${name}.headers guard'sız okunuyor. İstek HATA aldığında storage { error, data } olarak yazılır ve 'headers' alanı yoktur (microservice.ts:132) → TypeError; runCode undefined döner (prop'a NaN yazılır), ifade hatası toast'ı düşer ve flow-engine.ts:53 akışı onError'a çevirip durdurabilir. Guard'lı yazın: Number(Steps.${name}?.headers?.["total-count"] ?? 0).`,
|
|
1647
|
-
guide: 'ngapps://guides/page'
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
return findings;
|
|
1651
|
-
}));
|
|
2453
|
+
}];
|
|
2454
|
+
}));
|
|
2455
|
+
});
|
|
1652
2456
|
}
|
|
1653
2457
|
},
|
|
1654
2458
|
{
|
|
@@ -1881,6 +2685,35 @@ export const policyRules = [
|
|
|
1881
2685
|
}];
|
|
1882
2686
|
}
|
|
1883
2687
|
},
|
|
2688
|
+
{
|
|
2689
|
+
id: 'ROL-002',
|
|
2690
|
+
module: 'microservice',
|
|
2691
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
2692
|
+
sourceRef: "Ürün sahibi cevabı 2026-09-07 (S-16): rol yönetimi entity + microservice yoludur. Kaynak: roles kolonları id · name (UNIQUE ix_roles_name) · slug (TenantContextModelSnapshot); yetkilendirme slug'a bakar (QueryConstants.GetUserQuery ARRAY(SELECT r.slug …), Authorized(user.roles)); slug'ı role_list DÖNDÜRMEZ (RoleDto = Id + Name)",
|
|
2693
|
+
check: definition => {
|
|
2694
|
+
const parameters = definition.sourceParameters;
|
|
2695
|
+
if (definition.source !== 'table')
|
|
2696
|
+
return [];
|
|
2697
|
+
return writesRolesWithoutSlug(parameters?.entityId ?? definition.entityId, parameters)
|
|
2698
|
+
? [{ ruleId: 'ROL-002', severity: 'warn', message: rolesSlugMessage, guide: 'ngapps://guides/entity' }]
|
|
2699
|
+
: [];
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2702
|
+
{
|
|
2703
|
+
id: 'ROL-002',
|
|
2704
|
+
module: 'workflow',
|
|
2705
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
2706
|
+
sourceRef: "ROL-002'nin workflow ayağı: table step de aynı tabloya yazar (rule.entityId + rule.fields)",
|
|
2707
|
+
check: (definition, context) => workflowSteps(workflowBody(definition, context.workflowKind))
|
|
2708
|
+
.filter(step => step.type === 'table' && step.rule)
|
|
2709
|
+
.filter(step => writesRolesWithoutSlug(step.rule.entityId, step.rule))
|
|
2710
|
+
.map(step => ({
|
|
2711
|
+
ruleId: 'ROL-002',
|
|
2712
|
+
severity: 'warn',
|
|
2713
|
+
message: `'${stepLabel(step)}' table adımı: ${rolesSlugMessage}`,
|
|
2714
|
+
guide: 'ngapps://guides/entity'
|
|
2715
|
+
}))
|
|
2716
|
+
},
|
|
1884
2717
|
{
|
|
1885
2718
|
id: 'MW-003',
|
|
1886
2719
|
module: 'workflow',
|
|
@@ -1902,5 +2735,89 @@ export const policyRules = [
|
|
|
1902
2735
|
guide: 'ngapps://guides/middleware'
|
|
1903
2736
|
}];
|
|
1904
2737
|
}
|
|
1905
|
-
}
|
|
2738
|
+
},
|
|
2739
|
+
// Backend doğrulayıcılarının aynası (ENT-015…018, MS-017, WF-026, QRY/VIEW/DTO/ROW/DS/PRJ/TR/FLD/FILE/EXT):
|
|
2740
|
+
// tek liste `backendConstraints.ts`'te durur, buradan yalnız politika motoruna katılır.
|
|
2741
|
+
...backendPolicyRules()
|
|
1906
2742
|
];
|
|
2743
|
+
/**
|
|
2744
|
+
* WF-MF-001'in ikinci yarısı. Bir adımın koşup koşmayacağı yalnız GÖVDEDEN anlaşılamaz: karar
|
|
2745
|
+
* seçilen microfunction'ın YAYIN DURUMUNA bağlıdır ve o durum platformda yaşar. Politika
|
|
2746
|
+
* kuralları senkron ve saf olduğu için bu denetim ayrı durur — gateway `microfunction_get`
|
|
2747
|
+
* ucunu okur, bulguyu aynı `policyFindings` kanalına koyar ve `policyOverride` / `POLICY=warn`
|
|
2748
|
+
* kaçışları (applyPolicyMode) aynen geçerli kalır.
|
|
2749
|
+
*
|
|
2750
|
+
* Okuma başarısız olursa (ağ, yetki, beklenmeyen gövde) kural SUSAR: ön-denetim bir kolaylıktır,
|
|
2751
|
+
* gateway'in kendi arızası yüzünden geçerli bir workflow yazılamaz hâle gelmemeli.
|
|
2752
|
+
*/
|
|
2753
|
+
export async function microfunctionStepFindings(platform, project, environment, definition, kind, policyOverride = false) {
|
|
2754
|
+
if (platform.config.policyMode === 'off')
|
|
2755
|
+
return [];
|
|
2756
|
+
const steps = workflowSteps(workflowBody(definition, kind))
|
|
2757
|
+
.filter(step => step.type === 'microfunction')
|
|
2758
|
+
.filter(step => hasText((step.rule ?? {}).microfunctionId));
|
|
2759
|
+
if (steps.length === 0)
|
|
2760
|
+
return [];
|
|
2761
|
+
const records = new Map();
|
|
2762
|
+
const findings = [];
|
|
2763
|
+
for (const step of steps) {
|
|
2764
|
+
const rule = (step.rule ?? {});
|
|
2765
|
+
const microfunctionId = String(rule.microfunctionId);
|
|
2766
|
+
if (!records.has(microfunctionId)) {
|
|
2767
|
+
try {
|
|
2768
|
+
const payload = await platform.tenant('GET', project, `/microfunctions/${microfunctionId}`, { environment });
|
|
2769
|
+
records.set(microfunctionId, (payload?.data ?? payload));
|
|
2770
|
+
}
|
|
2771
|
+
catch (error) {
|
|
2772
|
+
records.set(microfunctionId, error instanceof PlatformApiError && error.status === 404 ? 'missing' : 'unreadable');
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const record = records.get(microfunctionId);
|
|
2776
|
+
if (record === undefined || record === 'unreadable')
|
|
2777
|
+
continue;
|
|
2778
|
+
if (record === 'missing') {
|
|
2779
|
+
findings.push({
|
|
2780
|
+
ruleId: 'WF-MF-001',
|
|
2781
|
+
severity: 'block',
|
|
2782
|
+
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.`,
|
|
2783
|
+
guide: 'ngapps://guides/microfunction'
|
|
2784
|
+
});
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const status = String(record.status ?? '');
|
|
2788
|
+
if (status === 'draft' || status === 'publish-failed') {
|
|
2789
|
+
findings.push({
|
|
2790
|
+
ruleId: 'WF-MF-001',
|
|
2791
|
+
severity: 'block',
|
|
2792
|
+
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.`,
|
|
2793
|
+
guide: 'ngapps://guides/microfunction'
|
|
2794
|
+
});
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
if (status === 'publishing') {
|
|
2798
|
+
findings.push({
|
|
2799
|
+
ruleId: 'WF-MF-001',
|
|
2800
|
+
severity: 'warn',
|
|
2801
|
+
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).`,
|
|
2802
|
+
guide: 'ngapps://guides/microfunction'
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
if (record.hasUnpublishedChanges === true) {
|
|
2806
|
+
findings.push({
|
|
2807
|
+
ruleId: 'WF-MF-001',
|
|
2808
|
+
severity: 'warn',
|
|
2809
|
+
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.`,
|
|
2810
|
+
guide: 'ngapps://guides/microfunction'
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
if (!hasText(rule.expression) && hasText(record.parameterDtoId)) {
|
|
2814
|
+
findings.push({
|
|
2815
|
+
ruleId: 'WF-MF-001',
|
|
2816
|
+
severity: 'warn',
|
|
2817
|
+
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.`,
|
|
2818
|
+
guide: 'ngapps://guides/microfunction'
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return applyPolicyMode(platform.config, findings, policyOverride);
|
|
2823
|
+
}
|