@appaflytech/zeyrek-ai-mcp 0.0.22 → 0.0.24

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.
Files changed (50) hide show
  1. package/.env.example +15 -0
  2. package/dist/config.js +9 -0
  3. package/dist/index.js +2 -0
  4. package/dist/knowledge/componentCatalog.js +4 -2
  5. package/dist/knowledge/componentEvents.js +3 -3
  6. package/dist/knowledge/mobileComponentCatalog.js +308 -50
  7. package/dist/knowledge/mobileComponentEvents.js +130 -3
  8. package/dist/knowledge/normalize.js +16 -6
  9. package/dist/knowledge/platform/lint.js +110 -3
  10. package/dist/knowledge/platform/mobile.js +1 -1
  11. package/dist/knowledge/platform/mobileRules.js +70 -11
  12. package/dist/knowledge/platform/web.js +1 -1
  13. package/dist/knowledge/rules.js +106 -11
  14. package/dist/knowledge/styleResolve.js +49 -3
  15. package/dist/knowledge/styleResolveNative.js +104 -4
  16. package/dist/observability/trace.js +1 -1
  17. package/dist/platform/argumentAliases.js +128 -0
  18. package/dist/platform/bodyFile.js +103 -0
  19. package/dist/platform/capture/capture.js +29 -5
  20. package/dist/platform/capture/console-log.js +80 -0
  21. package/dist/platform/capture/index.js +1 -0
  22. package/dist/platform/diagnostics.js +10 -4
  23. package/dist/platform/http.js +23 -7
  24. package/dist/platform/responses.js +6 -2
  25. package/dist/platform/schemaReadMemory.js +60 -0
  26. package/dist/platform/secrets.js +9 -0
  27. package/dist/platform/strictArguments.js +68 -12
  28. package/dist/platform/tokenVault.js +187 -4
  29. package/dist/resources/guides/expressions.md +1 -1
  30. package/dist/resources/guides/microservice.md +9 -2
  31. package/dist/resources/guides/middleware.md +5 -1
  32. package/dist/resources/guides/mobile.md +165 -10
  33. package/dist/resources/guides/page.md +161 -41
  34. package/dist/resources/guides/theme.md +4 -1
  35. package/dist/resources/guides/workflow.md +7 -1
  36. package/dist/resources/schema-fingerprint.json +2 -2
  37. package/dist/server.js +1 -1
  38. package/dist/tools/datasources.js +1 -1
  39. package/dist/tools/designSchemaPatch.js +4 -3
  40. package/dist/tools/externalComponents.js +1 -1
  41. package/dist/tools/flows.js +70 -4
  42. package/dist/tools/layouts.js +5 -5
  43. package/dist/tools/microservices.js +26 -10
  44. package/dist/tools/pages.js +192 -36
  45. package/dist/tools/platformInfo.js +4 -1
  46. package/dist/tools/rows.js +7 -1
  47. package/dist/tools/schemaOps.js +1 -1
  48. package/dist/tools/screenWrite.js +4 -3
  49. package/dist/tools/screens.js +81 -0
  50. package/package.json +1 -1
@@ -137,11 +137,14 @@ export function readsBeforeGuard(source, root) {
137
137
  * code.ts:49). Eşleşme yoksa flow-engine.ts:79 sessizce durur. Studio tarafında da zincir
138
138
  * `sourceBranch === 'always'` ile çizilir (EventActionsStart.svelte:47, EventActionsItem.svelte:298).
139
139
  * condition dalları rule id'sidir (serbest), microservice'te 'success' yalnız eski şema için
140
- * yaşatılır (microservice.ts:144).
140
+ * yaşatılır (microservice.ts:174 `isHasSuccess`). 'error' dalı 2026-09-26'dan beri motorun
141
+ * KENDİSİ tanır (ADR 0004, ZM-07/g-02): bağlıysa hata anında `always` hiç koşmaz, sürer
142
+ * (microservice.ts:182 `hasBranch`) — eski motor tanımadığı dalı hiç aramadığı için bu ekleme
143
+ * geriye dönük zararsızdır.
141
144
  */
142
145
  const flowBranchNames = {
143
146
  start: ['always'],
144
- microservice: ['always', 'success'],
147
+ microservice: ['always', 'success', 'error'],
145
148
  confirm: ['yes', 'no', 'always'],
146
149
  function: ['always'],
147
150
  end: []
@@ -154,7 +157,7 @@ const flowBranchNames = {
154
157
  */
155
158
  export const knownFlowStepTypes = new Set([
156
159
  'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
157
- 'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function'
160
+ 'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share'
158
161
  ]);
159
162
  const flowFreeBranchTypes = new Set(['condition']);
160
163
  /** `condition.types.ts` ConditionArgumentRuleType — motorun tanıdığı üçlü. */
@@ -167,7 +170,8 @@ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
167
170
  // Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
168
171
  const flowLiteralTextFields = new Map([
169
172
  ['alert', ['message', 'description']],
170
- ['confirm', ['message', 'title', 'confirmText', 'cancelText']]
173
+ ['confirm', ['message', 'title', 'confirmText', 'cancelText']],
174
+ ['share', ['title']]
171
175
  ]);
172
176
  const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
173
177
  // Barındırıcıya özel tipler BURADA kural üretmez. `navigate`i gateway normalizasyonu zaten
@@ -294,25 +298,71 @@ function writesRolesWithoutSlug(entityId, parameters) {
294
298
  }
295
299
  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.";
296
300
  const contractKeys = ['returnDtoId', 'parameterDtoId', 'returnBehavior', 'parameterBehavior'];
297
- const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith', 'isnull', 'isnotnull']);
301
+ /**
302
+ * Filtre işleci sözlüğü — backend `OperatorType` enum'u (paketlenmiş `Filter` şeması; eşitliği
303
+ * `test/filter-operators.test.ts` kilitler). Studio filtre ARAYÜZÜNÜN sözlüğü (`notContains`, `between`…,
304
+ * `componentCatalog.ts`) ayrı bir kavramdır: arayüz onları bu işleçlere çevirir.
305
+ */
306
+ export const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith', 'isnull', 'isnotnull']);
307
+ /** Ajanların sık yazdığı yanlış adlar → doğru işleç. Yalnız ÖNERİLİR, sessizce çevrilmez. */
308
+ const filterOperatorSuggestions = {
309
+ ne: 'neq', '!=': 'neq', '<>': 'neq', noteq: 'neq', notequal: 'neq', notequals: 'neq', not_eq: 'neq',
310
+ '=': 'eq', '==': 'eq', equal: 'eq', equals: 'eq',
311
+ '<': 'lt', '<=': 'lte', '>': 'gt', '>=': 'gte',
312
+ like: 'contains', ilike: 'icontains', nin: 'notin', 'not in': 'notin', not_in: 'notin'
313
+ };
298
314
  function flattenFilterTree(filters) {
299
315
  return filters
300
316
  .filter((filter) => Boolean(filter) && typeof filter === 'object')
301
317
  .flatMap(filter => [filter, ...flattenFilterTree(asArray(filter.children))]);
302
318
  }
319
+ /**
320
+ * Bilinmeyen işleçler, öneriyle: `status: 'ne' (neq mı demek istedin?)`. Grup çocukları da okunur. Ad
321
+ * büyük/küçük harfe DUYARLI eşlenir — backend de öyle eşler (`GetValueFromDescription` `==`); `NEQ`
322
+ * bilinmeyendir ve küçük harfli adı önerilir.
323
+ */
324
+ function unknownFilterOperators(filters) {
325
+ return flattenFilterTree(filters)
326
+ .filter(filter => filter.operator !== undefined && filter.operator !== null && !filterOperators.has(String(filter.operator)))
327
+ .map(filter => {
328
+ const operator = String(filter.operator);
329
+ const lower = operator.toLowerCase();
330
+ const suggestion = filterOperators.has(lower) ? lower : filterOperatorSuggestions[lower];
331
+ return `${String(filter.fieldSlug ?? filter.fieldId ?? '?')}: '${operator}'${suggestion ? ` (${suggestion} mı demek istedin?)` : ''}`;
332
+ });
333
+ }
334
+ const unknownOperatorConsequence = "Bilinmeyen işleç yola ve backend sürümüne göre ya 400 alır ya sessizce 'eq'e düşer ve sorgu yanlış sonuç döner (2026-09-27'den sonraki backend uç çağrısının `filters` parametresini 400 ile reddeder; workflow tablo adımı hâlâ sessizce 'eq' yapar); ad büyük/küçük harfe duyarlıdır — doğru işleci yazın";
303
335
  function unknownOperatorFindings(ruleId, owner, filters) {
304
- const unknownOperators = flattenFilterTree(filters)
305
- .filter(filter => filter.operator !== undefined && filter.operator !== null && !filterOperators.has(String(filter.operator).toLowerCase()))
306
- .map(filter => `${String(filter.fieldSlug ?? filter.fieldId ?? '?')}: '${String(filter.operator)}'`);
336
+ const unknownOperators = unknownFilterOperators(filters);
307
337
  return unknownOperators.length > 0
308
338
  ? [{
309
339
  ruleId,
310
340
  severity: 'block',
311
- message: `${owner}: bilinmeyen filtre operatörü — ${unknownOperators.join(', ')}. Platform bilinmeyen operatörü 400 ile REDDETMEZ, sessizce 'eq'e düşürür (EnumExtensions.GetValueFromDescription null → default) ve uç testten geçip yanlış çalışır. Geçerli sözlük: ${[...filterOperators].join(' · ')} ('like' yok — metin araması için 'contains'/'icontains').`,
341
+ message: `${owner}: bilinmeyen filtre işleci — ${unknownOperators.join(', ')}. ${unknownOperatorConsequence}. Geçerli sözlük: ${[...filterOperators].join(' · ')} ('like' yok — metin araması için 'contains'/'icontains').`,
312
342
  guide: 'ngapps://guides/microservice'
313
343
  }]
314
344
  : [];
315
345
  }
346
+ /**
347
+ * Çalışma anında gelen filtre (`row_list.filters`, `microservice_execute` `filters` parametresi): dizi ya
348
+ * da JSON dizesi. Bilinmeyen işleç varsa hata METNİ döner — araç isteği göndermeden 400 verir
349
+ * (plans/mobil-plantcare-acigi g-03). Ayrıştırılamayan değer backend'e bırakılır.
350
+ */
351
+ export function filterOperatorError(owner, filters) {
352
+ let parsed = filters;
353
+ if (typeof filters === 'string') {
354
+ try {
355
+ parsed = JSON.parse(filters);
356
+ }
357
+ catch {
358
+ return undefined;
359
+ }
360
+ }
361
+ const unknownOperators = Array.isArray(parsed) ? unknownFilterOperators(parsed) : [];
362
+ return unknownOperators.length > 0
363
+ ? `${owner}: bilinmeyen filtre işleci — ${unknownOperators.join(', ')}. İstek GÖNDERİLMEDİ. ${unknownOperatorConsequence}. Geçerli sözlük: ${[...filterOperators].join(' · ')}.`
364
+ : undefined;
365
+ }
316
366
  function isReadOperation(parameters) {
317
367
  const operation = String(parameters?.operation ?? '').toLowerCase();
318
368
  return !writeOperations.has(operation);
@@ -2365,7 +2415,7 @@ export const policyRules = [
2365
2415
  id: 'FLOW-022',
2366
2416
  module: 'flow',
2367
2417
  appliesTo: ['create', 'update'],
2368
- 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)",
2418
+ sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu). share adımında (2026-09-25) yalnız `title` düz metindir: @ng-apps/flow resolveShareContent onu olduğu gibi verir, `message` ve `path` ifadedir",
2369
2419
  check: definition => flowAllSteps(definition)
2370
2420
  .flatMap((step) => {
2371
2421
  const type = String(step.type ?? '');
@@ -2378,11 +2428,56 @@ export const policyRules = [
2378
2428
  .map(field => ({
2379
2429
  ruleId: 'FLOW-022',
2380
2430
  severity: 'warn',
2381
- 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.`,
2431
+ message: type === 'share'
2432
+ ? `'${flowStepLabel(step)}' share adımının '${field}' alanı ifade içeriyor ama bu alan DÜZ METİNDİR: başlık paylaşım penceresine olduğu gibi gider (iOS'ta e-posta konusu, Android'de seçici başlığı), ifade değerlendirilmez. Değişen içeriği ifade olan 'message' alanına yazın — ör. message: "'Şuna bak: ' + Page.properties.name"; bağlantı 'path' alanındadır.`
2433
+ : `'${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.`,
2382
2434
  guide: 'ngapps://guides/page'
2383
2435
  }));
2384
2436
  })
2385
2437
  },
2438
+ {
2439
+ id: 'FLOW-027',
2440
+ module: 'flow',
2441
+ appliesTo: ['create', 'update'],
2442
+ sourceRef: "share adımının argümanı StepManagementArgument'ın Title/Message/Path alanlarında saklanır (StepType.cs `share`, kök docs/adr/0001-share-akis-adimi.md); url/link/text gibi anahtarlar sözleşmede YOKTUR ve platform onları 200 ile sessizce düşürür — adım hiçbir şey paylaşmadan akış sürer. `message` ve `path` İFADEDİR (@ng-apps/flow resolveShareContent → runCode): tırnaksız 'https://…' JS'te etiket + yorumdur, undefined döner",
2443
+ check: definition => flowAllSteps(definition)
2444
+ .filter(step => step.type === 'share')
2445
+ .flatMap((step) => {
2446
+ const args = (step.args ?? {});
2447
+ const filled = (key) => typeof args[key] === 'string' && String(args[key]).trim().length > 0;
2448
+ const findings = [];
2449
+ const strayLinks = ['url', 'link', 'href'].filter(filled);
2450
+ const strayTexts = ['text', 'body', 'content'].filter(filled);
2451
+ if (strayLinks.length > 0) {
2452
+ findings.push({
2453
+ ruleId: 'FLOW-027',
2454
+ severity: filled('path') ? 'warn' : 'block',
2455
+ message: `'${flowStepLabel(step)}' share adımının bağlantısı args.${strayLinks.join('/args.')} alanında: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür ve adım bağlantısız kalır. Bağlantıyı args.path'e ifade olarak yazın — ör. "'https://ornek.app/urun/' + Page.queryParams.id".`,
2456
+ guide: 'ngapps://guides/page'
2457
+ });
2458
+ }
2459
+ if (strayTexts.length > 0) {
2460
+ findings.push({
2461
+ ruleId: 'FLOW-027',
2462
+ severity: filled('message') ? 'warn' : 'block',
2463
+ message: `'${flowStepLabel(step)}' share adımının metni args.${strayTexts.join('/args.')} alanında: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu sessizce düşürür. Metni args.message'a ifade olarak yazın — ör. "'Şuna bak: ' + Page.properties.name".`,
2464
+ guide: 'ngapps://guides/page'
2465
+ });
2466
+ }
2467
+ for (const key of ['message', 'path']) {
2468
+ const value = typeof args[key] === 'string' ? String(args[key]).trim() : '';
2469
+ if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value)) {
2470
+ findings.push({
2471
+ ruleId: 'FLOW-027',
2472
+ severity: 'block',
2473
+ message: `'${flowStepLabel(step)}' share adımının '${key}' alanı tırnaksız bir adres: '${value}'. Bu alan İFADEDİR — tırnaksız adres JS'te etiket + yorumdur, undefined döner ve hiçbir şey paylaşılmaz. Dize olarak yazın: "'${value}'".`,
2474
+ guide: 'ngapps://guides/page'
2475
+ });
2476
+ }
2477
+ }
2478
+ return findings;
2479
+ })
2480
+ },
2386
2481
  {
2387
2482
  id: 'FLOW-023',
2388
2483
  module: 'flow',
@@ -154,6 +154,24 @@ const resolveShadow = (shadow) => {
154
154
  return undefined;
155
155
  return SHADOW_VALUES[shadow] ?? String(shadow);
156
156
  };
157
+ /**
158
+ * Döndür / ölçekle / kaydır alanlarının kabul aralığı — frontend `style-groups/transform-fields.ts`
159
+ * `TRANSFORM_RANGES`'in kopyası (altın vakalar kilitler). Aralık dışı ve birimli değer yazılmaz.
160
+ */
161
+ export const TRANSFORM_RANGES = {
162
+ rotate: [-360, 360],
163
+ scale: [10, 500],
164
+ translateX: [-1000, 1000],
165
+ translateY: [-1000, 1000]
166
+ };
167
+ /** Sayı ya da sayısal metin, aralığın içinde; aksi `undefined` — frontend `transformValue`. */
168
+ export const transformValue = (value, field) => {
169
+ if (typeof value !== 'number' && (typeof value !== 'string' || value.trim() === ''))
170
+ return undefined;
171
+ const parsed = Number(value);
172
+ const [min, max] = TRANSFORM_RANGES[field];
173
+ return Number.isFinite(parsed) && parsed >= min && parsed <= max ? parsed : undefined;
174
+ };
157
175
  /**
158
176
  * Grupların alan listeleri — frontend `style-groups/style-group-schemas.ts`
159
177
  * `STYLE_GROUP_FIELDS`'in kopyası. Parite testi `components.json`'daki gerçek
@@ -170,7 +188,11 @@ export const STYLE_GROUP_FIELDS = {
170
188
  'borderLeft',
171
189
  'shadow',
172
190
  'contentOverflow',
173
- 'opacity'
191
+ 'opacity',
192
+ 'rotate',
193
+ 'scale',
194
+ 'translateX',
195
+ 'translateY'
174
196
  ],
175
197
  typographyStyle: [
176
198
  'textColor',
@@ -220,6 +242,21 @@ export function composeTypedStyle(input, ext) {
220
242
  if (opacity !== undefined && opacity >= 0 && opacity < 100) {
221
243
  groups.opacity = String(opacity / 100);
222
244
  }
245
+ // Döndür / ölçekle / kaydır (ADR 0005): kaydır → döndür → ölçekle, köken merkez;
246
+ // ölçek yüzdedir, nötr değer ve aralık dışı yazılmaz — frontend `composeSurfaceTransform`.
247
+ const transform = [];
248
+ const x = transformValue(surface.translateX, 'translateX') ?? 0;
249
+ const y = transformValue(surface.translateY, 'translateY') ?? 0;
250
+ if (x !== 0 || y !== 0)
251
+ transform.push(`translate(${x}px, ${y}px)`);
252
+ const rotate = transformValue(surface.rotate, 'rotate');
253
+ if (rotate !== undefined && rotate !== 0)
254
+ transform.push(`rotate(${rotate}deg)`);
255
+ const scale = transformValue(surface.scale, 'scale');
256
+ if (scale !== undefined && scale !== 100)
257
+ transform.push(`scale(${scale / 100})`);
258
+ if (transform.length)
259
+ groups.transform = transform.join(' ');
223
260
  // `ROOT_TARGET` (`&`) kökün kendisidir: kendi derleyicisi olmayan kaplarda
224
261
  // yüzey düğüm kutusuna yazılır ve kaydırılan da odur.
225
262
  if (isSet(surface.contentOverflow) && ext?.overflowTarget) {
@@ -251,8 +288,10 @@ export function composeTypedStyle(input, ext) {
251
288
  assign(rules, 'borderColor', hover.borderColor);
252
289
  assign(rules, 'boxShadow', resolveShadow(hover.shadow));
253
290
  const lift = asDimension(hover.lift);
254
- if (lift !== undefined)
255
- rules.transform = `translateY(calc(-1 * ${lift}))`;
291
+ // Kaldırma düğümün kendi transform'unu ezmez, önüne eklenir.
292
+ if (lift !== undefined) {
293
+ rules.transform = `translateY(calc(-1 * ${lift}))${typeof groups.transform === 'string' ? ` ${groups.transform}` : ''}`;
294
+ }
256
295
  if (Object.keys(rules).length) {
257
296
  const duration = asNumber(hover.transitionMs) ?? 160;
258
297
  groups.transition = `background-color ${duration}ms ease, border-color ${duration}ms ease, box-shadow ${duration}ms ease, transform ${duration}ms ease`;
@@ -500,6 +539,13 @@ export const NODE_SURFACE_TYPES = new Set([
500
539
  'qm-sidebar',
501
540
  'auth-layout'
502
541
  ]);
542
+ /**
543
+ * `style`'ı kendi içeriğinin CSS'i olarak alan tipler: `html`in `style`'ı uzun metindir ve yazarın
544
+ * işaretlemesine kapsamlı girer — web'de emotion sınıfı (`html.functions.ts` `resolveHtmlStyleClass`),
545
+ * mobilde korumalı belgenin stil bloğu (plans/mobil-plantcare-acigi f-20). "Düz CSS string style"
546
+ * (PAGE-014) ve "ham CSS çizilmez" (MOB-002) uyarıları bu tiplerde yanlıştır.
547
+ */
548
+ export const DOCUMENT_STYLE_TYPES = new Set(['html']);
503
549
  const NODE_SURFACE_EXTENSION = { overflowTarget: ROOT_TARGET };
504
550
  /** Düğüm kutusuna inen yüzey; kap tipi değilse ya da grup boşsa yok. */
505
551
  export function composeNodeSurfaceStyle(type, surfaceStyle) {
@@ -1,4 +1,4 @@
1
- import { rawProp } from './styleResolve.js';
1
+ import { TRANSFORM_RANGES, rawProp, transformValue } from './styleResolve.js';
2
2
  import { mergeTypeScale, resolveTheme } from './themeResolve.js';
3
3
  /* ---- css-value.ts ---- */
4
4
  const warn = (context, message) => {
@@ -45,6 +45,46 @@ export function parseFontFamily(value) {
45
45
  const first = String(value).split(',')[0].trim().replace(/^["']|["']$/g, '');
46
46
  return first || null;
47
47
  }
48
+ const GENERIC_FONT_FAMILIES = new Set([
49
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
50
+ 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong'
51
+ ]);
52
+ /** CSS generik ailesi mi (`sans-serif`, `monospace`…) — tırnaklanırsa aile adı sanılır. */
53
+ export function isGenericFontFamily(name) {
54
+ return GENERIC_FONT_FAMILIES.has(name.trim().toLowerCase());
55
+ }
56
+ /**
57
+ * Yığının yedek generiği: son generik ad, yoksa `sans-serif`. Temanın yığını `"Figtree",
58
+ * sans-serif` biçimindedir; tek ad yazılan `typographyStyle` yedeğini buradan miras alır (ZM-10).
59
+ */
60
+ export function fontFallback(stack) {
61
+ const generic = String(stack ?? '').split(',').map(part => part.trim().replace(/^["']|["']$/g, '')).filter(isGenericFontFamily).pop();
62
+ return generic ?? 'sans-serif';
63
+ }
64
+ /** Yığının yalın ilk adı (tırnaksız); yükleyicinin istediği ad budur (ZM-10, f-05). */
65
+ export function firstFontFamily(value) {
66
+ return parseFontFamily(value) ?? '';
67
+ }
68
+ /**
69
+ * Font adı platforma göre çözülür (ZM-10, K3). Frontend'de bu WEB/cihaz diye ikiye ayrılır
70
+ * (`font-family.ts` / `font-family.native.ts`, Metro `.native`'i seçer) ama bu ayna TEK koldur:
71
+ * `test/style-resolve-native.test.ts` frontend'in `theme-native.ts`'ini düz Node `import()` ile
72
+ * okur ve Node/Vite çözümü platform uzantısını (`.native.ts`) hiç görmez — parite testinin
73
+ * kıyasladığı gerçek davranış bu yüzden HER ZAMAN web kolu (`font-family.ts`); cihazdaki yalın-ad
74
+ * davranışı yalnız Metro bundle'ında oluşur ve bu aynanın kapsamı dışındadır (g-02 parity fix).
75
+ * Tek ad generik yedeksiz kalırsa RN web'de font yüklenemediğinde tarayıcı varsayılanı (Times)
76
+ * çizilir; bu yüzden tek ad tırnaklanıp temanın generiğiyle yığına çevrilir, yazarın kendi yığını
77
+ * (virgüllü) aynen geçer.
78
+ */
79
+ export function resolveFontFamily(value, fallback) {
80
+ const text = value.trim();
81
+ if (text === '' || text.includes(','))
82
+ return text;
83
+ const name = firstFontFamily(text);
84
+ if (isGenericFontFamily(name))
85
+ return name;
86
+ return `"${name}", ${fallback}`;
87
+ }
48
88
  export function parseLineHeight(value, fontSize, context) {
49
89
  if (value === undefined || value === null || value === '')
50
90
  return null;
@@ -202,6 +242,9 @@ export function resolveThemeNative(theme, mode = 'light') {
202
242
  if (narrow)
203
243
  scale[`${level}Mobile`] = px(narrow, fontSize);
204
244
  }
245
+ const bodyStack = vars['--ng-font'] ?? tokens.fontFamily;
246
+ const headingStack = vars['--ng-font-heading'] ?? bodyStack;
247
+ const fallback = fontFallback(bodyStack);
205
248
  return {
206
249
  mode,
207
250
  vars,
@@ -214,8 +257,11 @@ export function resolveThemeNative(theme, mode = 'light') {
214
257
  pill: px(vars['--ng-radius-pill'], 999)
215
258
  },
216
259
  font: {
217
- family: vars['--ng-font'] ?? tokens.fontFamily,
218
- headingFamily: vars['--ng-font-heading'] ?? tokens.fontFamily,
260
+ family: resolveFontFamily(bodyStack, fallback),
261
+ headingFamily: resolveFontFamily(headingStack, fallback),
262
+ source: firstFontFamily(bodyStack),
263
+ headingSource: firstFontFamily(headingStack),
264
+ fallback,
219
265
  size: fontSize,
220
266
  weight: vars['--ng-font-weight'] ?? tokens.fontWeight,
221
267
  scale
@@ -302,6 +348,55 @@ const composeBorders = (surface, shell, theme, context) => {
302
348
  shell.borderStyle = style;
303
349
  }
304
350
  };
351
+ /** Yüzey grubunun bilinen alanları; kalanı uyarıya yazılır (frontend `SURFACE_KEYS`). MOB-002 de buradan okur. */
352
+ export const SURFACE_KEYS = new Set([
353
+ 'background',
354
+ 'radius',
355
+ 'borderTop',
356
+ 'borderRight',
357
+ 'borderBottom',
358
+ 'borderLeft',
359
+ 'shadow',
360
+ 'contentOverflow',
361
+ 'opacity',
362
+ 'rotate',
363
+ 'scale',
364
+ 'translateX',
365
+ 'translateY',
366
+ 'borderWidth',
367
+ 'borderStyle',
368
+ 'borderColor'
369
+ ]);
370
+ /**
371
+ * Döndür / ölçekle / kaydır (ADR 0005): web ile aynı sıra ve aralık, RN dizisi. Dolu ama geçersiz
372
+ * değer (birimli metin, aralık dışı) uyarıya yazılır — frontend `composeTransform`.
373
+ */
374
+ const composeTransform = (surface, shell, warnings) => {
375
+ const read = (field) => {
376
+ const raw = surface[field];
377
+ if (!isSet(raw))
378
+ return undefined;
379
+ const value = transformValue(raw, field);
380
+ if (value === undefined) {
381
+ const [min, max] = TRANSFORM_RANGES[field];
382
+ warnings?.push(`surfaceStyle.${field}: ${String(raw)} geçersiz — ${min}…${max} arası sayı, atlandı`);
383
+ }
384
+ return value;
385
+ };
386
+ const transform = [];
387
+ const x = read('translateX') ?? 0;
388
+ const y = read('translateY') ?? 0;
389
+ if (x !== 0 || y !== 0)
390
+ transform.push({ translateX: x }, { translateY: y });
391
+ const rotate = read('rotate');
392
+ if (rotate !== undefined && rotate !== 0)
393
+ transform.push({ rotate: `${rotate}deg` });
394
+ const scale = read('scale');
395
+ if (scale !== undefined && scale !== 100)
396
+ transform.push({ scale: scale / 100 });
397
+ if (transform.length)
398
+ shell.transform = transform;
399
+ };
305
400
  const composeSurface = (surface, shell, theme, context) => {
306
401
  const background = asText(surface.background, theme);
307
402
  if (background)
@@ -325,12 +420,17 @@ const composeSurface = (surface, shell, theme, context) => {
325
420
  if (overflow === 'auto')
326
421
  context.warnings?.push('contentOverflow: auto — RN kaydırmayı bileşenin kendisi açar, kutu hidden yapıldı');
327
422
  }
423
+ composeTransform(surface, shell, context.warnings);
424
+ for (const [key, value] of Object.entries(surface)) {
425
+ if (!SURFACE_KEYS.has(key) && isSet(value))
426
+ context.warnings?.push(`surfaceStyle.${key}: mobilde karşılığı yok, atlandı`);
427
+ }
328
428
  };
329
429
  const composeTypography = (typography, text, theme, context) => {
330
430
  const color = asText(typography.textColor, theme);
331
431
  if (color)
332
432
  text.color = color;
333
- const family = parseFontFamily(asText(typography.fontFamily, theme));
433
+ const family = resolveFontFamily(asText(typography.fontFamily, theme) ?? '', theme.font.fallback);
334
434
  if (family)
335
435
  text.fontFamily = family;
336
436
  const fontSize = parsePanelLength(typeof typography.fontSize === 'string' ? resolveThemeToken(typography.fontSize, theme) : typography.fontSize, context);
@@ -351,7 +351,7 @@ export function logGatewayEvent(message) {
351
351
  */
352
352
  export function logCapabilityGap(gap) {
353
353
  const lines = [
354
- `┌─ ${clockOf(new Date())} · KABİLİYET BOŞLUĞU · ${gap.severity}${gap.module ? ` · ${gap.module}` : ''}`,
354
+ `┌─ ${clockOf(new Date())} · KABİLİYET BOŞLUĞU · ${gap.severity}${gap.module ? ` · ${gap.module}` : ''}${gap.project ? ` · proje ${gap.project}` : ''}`,
355
355
  `│ hedef : ${condense(gap.goal, maxBodyChars)}`,
356
356
  `│ engel : ${condense(gap.blocker, maxBodyChars)}`
357
357
  ];
@@ -0,0 +1,128 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { PlatformApiError } from './errors.js';
3
+ import { RefResolver, isGuid } from './refs.js';
4
+ const text = (value) => typeof value === 'string' ? value : String(value ?? '');
5
+ const hostAlias = (from, host) => ({
6
+ tool: '*',
7
+ from,
8
+ when: (shape, args) => 'host' in shape && 'hostId' in shape && !(from in shape) && args.host === undefined && args.hostId === undefined,
9
+ apply: async (value) => ({ set: { host, hostId: value }, note: `${from} → host: '${host}', hostId: ${text(value)}` })
10
+ });
11
+ export const ARGUMENT_ALIASES = [
12
+ hostAlias('pageId', 'page'),
13
+ hostAlias('layoutId', 'layout'),
14
+ hostAlias('componentId', 'component'),
15
+ {
16
+ tool: '*',
17
+ from: 'project',
18
+ when: (shape, args) => 'projectId' in shape && !('project' in shape) && args.projectId === undefined,
19
+ apply: async (value, _args, { platform }) => {
20
+ if (isGuid(text(value)))
21
+ return { set: { projectId: text(value) }, note: `project → projectId: ${text(value)}` };
22
+ // Önce slug (`/p/{slug}`), sonra ad: liste arama sonucunda slug ya da ad birebir (büyük/küçük harf duyarsız).
23
+ const bySlug = await platform.org('GET', `/p/${encodeURIComponent(text(value))}`).catch(() => undefined);
24
+ const wanted = text(value).toLocaleLowerCase('tr');
25
+ const listed = bySlug?.data?.id
26
+ ? undefined
27
+ : await platform.org('GET', '/projects', { query: { searchTerm: text(value), pageLength: 50 } }).catch(() => undefined);
28
+ const projectId = bySlug?.data?.id ?? listed?.data?.find(project => [project.slug, project.name].some(candidate => candidate?.toLocaleLowerCase('tr') === wanted))?.id;
29
+ if (!projectId)
30
+ throw new PlatformApiError(`'${text(value)}' diye bir proje yok (slug ya da ad); projectId'yi project_list ile bulun.`, 400);
31
+ return { set: { projectId }, note: `project '${text(value)}' → projectId: ${projectId}` };
32
+ }
33
+ },
34
+ {
35
+ tool: '*',
36
+ from: 'id',
37
+ when: (shape, args) => 'microservice' in shape && 'application' in shape && !('id' in shape) && args.microservice === undefined,
38
+ apply: async (value, args, { platform }) => {
39
+ if (!isGuid(text(value)))
40
+ return { set: { microservice: text(value) }, note: `id → microservice: '${text(value)}'` };
41
+ if (typeof args.application !== 'string')
42
+ throw new PlatformApiError("id bir microservice GUID'i; slug'ını çözmek için 'application' (uygulama slug'ı) da verin.", 400);
43
+ const resolver = new RefResolver(platform, args.project, args.environment);
44
+ const applicationId = await resolver.applicationId(args.application);
45
+ const payload = await platform.tenant('GET', args.project, `/microservices/${applicationId}/${text(value)}`, { environment: args.environment });
46
+ const slug = payload?.data?.slug;
47
+ if (!slug)
48
+ throw new PlatformApiError(`'${args.application}' uygulamasında ${text(value)} id'li microservice yok; microservice_list ile slug'ı bulun.`, 400);
49
+ return { set: { microservice: slug }, note: `id ${text(value)} → microservice: '${slug}'` };
50
+ }
51
+ },
52
+ {
53
+ tool: 'entity_get',
54
+ from: 'applicationId',
55
+ apply: async (value, args, { platform }) => {
56
+ const project = args.project;
57
+ const environment = args.environment;
58
+ const resolver = new RefResolver(platform, project, environment);
59
+ const applicationId = await resolver.applicationId(text(value));
60
+ const entity = await resolver.entity(text(args.id));
61
+ const payload = await platform.tenant('GET', project, `/entities/${entity.datasourceId}/${entity.entityId}`, { environment });
62
+ const linked = (payload?.data?.applications ?? []).some(link => [link.id, link.applicationId].some(id => id?.toLowerCase() === applicationId.toLowerCase()));
63
+ if (!linked) {
64
+ throw new PlatformApiError(`'${text(args.id)}' entity'si '${text(value)}' uygulamasına bağlı değil; entity_get uygulamaya göre değil entity'ye göre çalışır — applicationId'yi kaldırın ya da uygulamanın entity'lerini entity_list {applicationId} ile listeleyin.`, 400);
65
+ }
66
+ return { set: { datasourceId: entity.datasourceId }, note: `applicationId '${text(value)}' → datasourceId: ${entity.datasourceId} (entity '${text(args.id)}' bu uygulamaya bağlı)` };
67
+ }
68
+ },
69
+ {
70
+ tool: '*',
71
+ from: 'applicationId',
72
+ when: (shape, args) => shape.applicationId !== undefined && typeof args.applicationId === 'string' && !shape.applicationId.safeParse(args.applicationId).success,
73
+ apply: async (value, args, { platform }) => {
74
+ const applicationId = await new RefResolver(platform, args.project, args.environment).applicationId(text(value));
75
+ return { set: { applicationId }, note: `applicationId '${text(value)}' → ${applicationId}` };
76
+ }
77
+ }
78
+ ];
79
+ export async function applyArgumentAliases(tool, args, context) {
80
+ if (!args || typeof args !== 'object' || Array.isArray(args))
81
+ return { args, applied: [] };
82
+ let current = { ...args };
83
+ const applied = [];
84
+ for (const alias of ARGUMENT_ALIASES) {
85
+ if (alias.tool !== '*' && alias.tool !== tool)
86
+ continue;
87
+ if (!(alias.from in current))
88
+ continue;
89
+ if (!(alias.when ? alias.when(context.shape, current) : !(alias.from in context.shape)))
90
+ continue;
91
+ const outcome = await alias.apply(current[alias.from], current, context);
92
+ const { [alias.from]: _replaced, ...rest } = current;
93
+ current = { ...rest, ...outcome.set };
94
+ applied.push(outcome.note);
95
+ }
96
+ return { args: current, applied };
97
+ }
98
+ /**
99
+ * Takma adı olmayan bilinmeyen argüman için "bunu mu demek istedin": okunan alanlar arasından büyük/küçük
100
+ * harf duyarsız düzenleme uzaklığı 2'yi geçmeyen en yakın ad (`hostID` → `hostId`, `views` → `view`).
101
+ */
102
+ export function closestArgument(key, accepted) {
103
+ const distance = (left, right) => {
104
+ const row = Array.from({ length: right.length + 1 }, (_, index) => index);
105
+ for (let i = 1; i <= left.length; i++) {
106
+ let diagonal = row[0] ?? 0;
107
+ row[0] = i;
108
+ for (let j = 1; j <= right.length; j++) {
109
+ const above = row[j] ?? 0;
110
+ row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
111
+ diagonal = above;
112
+ }
113
+ }
114
+ return row[right.length] ?? 0;
115
+ };
116
+ const ranked = accepted
117
+ .map(name => ({ name, score: distance(key.toLowerCase(), name.toLowerCase()) }))
118
+ .filter(entry => entry.score <= 2)
119
+ .sort((left, right) => left.score - right.score);
120
+ return ranked[0]?.name;
121
+ }
122
+ const activeNotes = new AsyncLocalStorage();
123
+ export function runWithArgumentNotes(notes, run) {
124
+ return activeNotes.run(notes, run);
125
+ }
126
+ export function argumentNotes() {
127
+ return activeNotes.getStore() ?? { aliasesApplied: [] };
128
+ }