@appaflytech/zeyrek-ai-mcp 0.0.21 → 0.0.22

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 (51) hide show
  1. package/dist/config.js +2 -0
  2. package/dist/knowledge/componentCatalog.js +3 -3
  3. package/dist/knowledge/componentEvents.js +9 -2
  4. package/dist/knowledge/mobileComponentCatalog.js +543 -0
  5. package/dist/knowledge/mobileComponentEvents.js +98 -0
  6. package/dist/knowledge/normalize.js +52 -11
  7. package/dist/knowledge/platform/lint.js +205 -0
  8. package/dist/knowledge/platform/mobile.js +78 -0
  9. package/dist/knowledge/platform/mobileRules.js +123 -0
  10. package/dist/knowledge/platform/mobileTabs.js +108 -0
  11. package/dist/knowledge/platform/profile.js +20 -0
  12. package/dist/knowledge/platform/resolve.js +122 -0
  13. package/dist/knowledge/platform/web.js +38 -0
  14. package/dist/knowledge/rules.js +6 -8
  15. package/dist/knowledge/styleResolve.js +1 -1
  16. package/dist/knowledge/styleResolveNative.js +410 -0
  17. package/dist/knowledge/themeResolve.js +3 -3
  18. package/dist/platform/capture/actions.js +333 -0
  19. package/dist/platform/capture/browser.js +1 -0
  20. package/dist/platform/{screenshot.js → capture/capture.js} +23 -509
  21. package/dist/platform/capture/index.js +10 -0
  22. package/dist/platform/capture/readiness.js +103 -0
  23. package/dist/platform/capture/summary.js +77 -0
  24. package/dist/platform/capture/target.js +5 -0
  25. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  26. package/dist/platform/capture/targets/studio-web.js +5 -0
  27. package/dist/platform/domSummary.js +1 -1
  28. package/dist/resources/examples/mobile-app-shell-drawer.json +209 -0
  29. package/dist/resources/examples/mobile-app-shell-stack.json +123 -0
  30. package/dist/resources/examples/mobile-app-shell-tab.json +174 -0
  31. package/dist/resources/examples/mobile-list-detail-form.json +656 -0
  32. package/dist/resources/guides/component.md +6 -1
  33. package/dist/resources/guides/functions.md +1 -1
  34. package/dist/resources/guides/layout.md +11 -7
  35. package/dist/resources/guides/mobile.md +264 -0
  36. package/dist/resources/guides/page.md +55 -15
  37. package/dist/resources/guides/translation.md +1 -1
  38. package/dist/server.js +2 -0
  39. package/dist/tools/applications.js +60 -3
  40. package/dist/tools/components.js +50 -10
  41. package/dist/tools/designSchemaPatch.js +13 -4
  42. package/dist/tools/externalComponents.js +25 -4
  43. package/dist/tools/flows.js +9 -2
  44. package/dist/tools/layouts.js +41 -9
  45. package/dist/tools/mobile.js +203 -0
  46. package/dist/tools/pages.js +115 -35
  47. package/dist/tools/platformInfo.js +2 -1
  48. package/dist/tools/profileWrite.js +45 -0
  49. package/dist/tools/screenWrite.js +2 -2
  50. package/dist/tools/screens.js +9 -3
  51. package/package.json +1 -1
@@ -1,25 +1,40 @@
1
1
  import { guid, projectParam, environmentParam, readOnly, writes, updates, destructive } from './common.js';
2
- import { normalizeComponentDefaults, componentDefaultsNote, clampBlockSizes, blockSizeNote, normalizeHeightTypes, heightTypeNote, normalizeSizeStrings, sizeStringNote, withNormalization } from '../knowledge/normalize.js';
2
+ import { normalizeComponentDefaults, componentDefaultsNote, clampBlockSizes, blockSizeNote, normalizeHeightTypes, heightTypeNote, normalizeSizeStrings, sizeStringNote, normalizeDroppableStates, withNormalization } from '../knowledge/normalize.js';
3
3
  import { eventContractsFor, eventPayloadNote } from '../knowledge/componentEvents.js';
4
4
  import { componentPresetDefaults, readProjectTheme } from '../knowledge/themeSchema.js';
5
5
  import { knowledgeFor, knowledgeNote, APPEARANCE_TOKEN_SETS, STATE_FLAG_NOTES, INTERNAL_ROLES_BY_TYPE } from '../knowledge/componentCatalog.js';
6
+ import { MOBILE_STATE_FLAG_NOTES, mobileComponentKnowledge } from '../knowledge/mobileComponentCatalog.js';
7
+ import { mobileComponentEventContracts } from '../knowledge/mobileComponentEvents.js';
6
8
  import { componentUpdateBody, fetchHost, schemaParam, unwrap } from './screens.js';
7
9
  import { PlatformApiError } from '../platform/errors.js';
8
10
  import { handle } from '../platform/responses.js';
11
+ import { applicationTypeField, resolveProfile } from '../knowledge/platform/resolve.js';
12
+ import { mobileComponentNotes } from '../knowledge/platform/mobileRules.js';
9
13
  import { z } from 'zod';
10
- const typeParam = z.enum(['web', 'mobile']).optional().describe('Hedef platform; varsayılan web');
14
+ const typeParam = z.enum(['web', 'mobile']).optional().describe('Hedef platform (ComponentType); verilmezse uygulamanın tipi — mobil uygulamada mobile');
11
15
  const kindParam = z.enum(['component', 'widget']).optional()
12
16
  .describe("component: sayfaya konan tekil eleman. widget: kendi şeması olan kapsayıcı (container/modal/drawer)");
13
17
  const widgetTypeParam = z.enum(['container', 'modal', 'drawer']).optional()
14
18
  .describe("kind:'widget' iken zorunlu");
19
+ /**
20
+ * Uç `type` verilmezse WEB kataloğunu döner (`GetComponentsQuery` `Type ?? Web`); mobil uygulamanın
21
+ * paleti, doğrulayıcısı ve widget'ları `type=mobile` satırlarıdır. Profil mobil derse sorgu tipi
22
+ * taşır; web'de sorgu profil öncesiyle aynıdır (G1 kilidi).
23
+ */
15
24
  async function listComponents(platform, project, environment, applicationId, scope) {
16
25
  const path = scope === 'all' ? `/components/${applicationId}/all` : `/components/${applicationId}`;
26
+ const catalogType = scope === 'all' ? 'web' : (await resolveProfile(platform, applicationId)).profile.catalogType;
17
27
  const response = await platform.tenant('GET', project, path, {
18
28
  environment,
19
- query: scope === 'all' ? undefined : { pageIndex: 0, pageLength: 999 }
29
+ query: scope === 'all' ? undefined : { pageIndex: 0, pageLength: 999, ...(catalogType === 'mobile' ? { type: 'mobile' } : {}) }
20
30
  });
21
31
  return response?.data ?? [];
22
32
  }
33
+ /** Uygulamanın kayıtlı bileşen slug'ları (mobil profilin MOB-003 denetimi). */
34
+ export async function fetchCatalogSlugs(platform, project, environment, applicationId) {
35
+ const components = await listComponents(platform, project, environment, applicationId);
36
+ return new Set(components.map(component => String(component.slug ?? '')).filter(slug => slug.length > 0));
37
+ }
23
38
  /**
24
39
  * Yerleşim normalizasyonlarının ikisi de aynı katalog listesinden beslenir; tek okumada
25
40
  * çıkarılır ki yazma yolları fazladan istek atmasın. Uygulamanın kendi sözlüğü okunur
@@ -38,12 +53,17 @@ export async function fetchComponentLayoutRules(platform, project, environment,
38
53
  export function componentLayoutRules(components) {
39
54
  const maxSizes = new Map();
40
55
  const autoHeights = new Map();
56
+ const droppable = new Map();
41
57
  for (const component of components) {
42
58
  const slug = typeof component.slug === 'string' ? component.slug : undefined;
43
59
  if (!slug)
44
60
  continue;
45
61
  if (typeof component.isDefaultAutoHeight === 'boolean')
46
62
  autoHeights.set(slug, component.isDefaultAutoHeight);
63
+ // Kap bilgisi sözlükten gelir; düğümün `states.isDroppable`ı onu izler (Studio paleti de
64
+ // aynı alanı katalogdan yazar). `true` gören tip kap sayılır.
65
+ if (typeof component.isDroppable === 'boolean')
66
+ droppable.set(slug, component.isDroppable);
47
67
  const blocks = (component.definition ?? {}).blocks;
48
68
  if (!Array.isArray(blocks))
49
69
  continue;
@@ -58,7 +78,7 @@ export function componentLayoutRules(components) {
58
78
  });
59
79
  }
60
80
  }
61
- return { maxSizes, autoHeights };
81
+ return { maxSizes, autoHeights, droppable };
62
82
  }
63
83
  /**
64
84
  * Katalog okuması başarısız olsa da yazma isteği gitmeli, ama iki normalizasyonun düşüş davranışı
@@ -69,14 +89,21 @@ export function componentLayoutRules(components) {
69
89
  * varsayılanını (`fixed`, `DesignComponentManagementSchema.cs:24`) kabul etmek demektir; en fazla
70
90
  * sekiz tipte (auth-layout/html/image/layout/qrcode/table/text-editor/video) fazladan yükseklik
71
91
  * üretir, atlamak ise her tipte içerik kırpar.
92
+ * - `states.isDroppable` atlanır — katalog okunamadığında hangi tipin kap olduğu bilinmez ve
93
+ * uydurulmaz; düğüm bugünkü hâliyle yazılır.
72
94
  */
73
95
  export async function applyCatalogLayoutRules(platform, project, environment, applicationId, nodes) {
74
96
  try {
75
97
  const rules = await fetchComponentLayoutRules(platform, project, environment, applicationId);
76
- return { clamped: clampBlockSizes(nodes, rules.maxSizes), heights: normalizeHeightTypes(nodes, rules.autoHeights), sizes: normalizeSizeStrings(nodes) };
98
+ return {
99
+ clamped: clampBlockSizes(nodes, rules.maxSizes),
100
+ heights: normalizeHeightTypes(nodes, rules.autoHeights),
101
+ sizes: normalizeSizeStrings(nodes),
102
+ states: normalizeDroppableStates(nodes, rules.droppable)
103
+ };
77
104
  }
78
105
  catch {
79
- return { clamped: [], heights: normalizeHeightTypes(nodes, new Map()), sizes: normalizeSizeStrings(nodes) };
106
+ return { clamped: [], heights: normalizeHeightTypes(nodes, new Map()), sizes: normalizeSizeStrings(nodes), states: [] };
80
107
  }
81
108
  }
82
109
  /**
@@ -116,6 +143,7 @@ export function registerComponentTools(server, platform) {
116
143
  }, handle(config, async ({ project, environment, applicationId, scope, includeHidden }) => {
117
144
  const components = await listComponents(platform, project, environment, applicationId, scope);
118
145
  return {
146
+ ...applicationTypeField(await resolveProfile(platform, applicationId)),
119
147
  components: components
120
148
  .filter(component => includeHidden === true || component.isHide !== true)
121
149
  .map(component => ({
@@ -161,6 +189,15 @@ export function registerComponentTools(server, platform) {
161
189
  : [];
162
190
  const componentSlug = typeof component.slug === 'string' ? component.slug : undefined;
163
191
  const knowledge = knowledgeFor(componentSlug);
192
+ // Mobil uygulamada kayıt mobil kataloğun satırıdır (`listComponents` type=mobile); davranış
193
+ // notları web runtime'ından gelir (`knowledge` — mobil şema web sınıfını genişletir), bileşenin
194
+ // mobil farkları `mobileKnowledge`'ta, bütün mobil bileşenlere geçen notlar `mobileNotes`'ta (G4).
195
+ // Web ikizi olmayan bileşende (`app-bar`, `tooltip`…) bilgi yalnız `mobileKnowledge`'tadır; olay
196
+ // yükleri de önce mobil sözlükten okunur (`mobileComponentEvents.ts`).
197
+ const mobile = (await resolveProfile(platform, applicationId)).profile.platform === 'mobile';
198
+ const mobileKnowledge = mobile && componentSlug !== undefined && Object.hasOwn(mobileComponentKnowledge, componentSlug)
199
+ ? mobileComponentKnowledge[componentSlug]
200
+ : undefined;
164
201
  // S-3 — `defaults` HAM katalogdur; Studio'da bırakılan düğüm ise temanın bileşen preseti
165
202
  // kaplanmış katalogdan kurulur (`withThemeComponentDefaults`). Tema okunamazsa (uç düşer,
166
203
  // yetki yoksa) alan yanıta HİÇ girmez: uydurulmuş bir varsayılan yanlış bilgidir.
@@ -177,7 +214,7 @@ export function registerComponentTools(server, platform) {
177
214
  isDroppable: component.isDroppable,
178
215
  props: definition.props,
179
216
  defaults: definition.defaults,
180
- events: eventContractsFor(componentSlug, eventNames),
217
+ events: eventContractsFor(componentSlug, eventNames, mobile ? mobileComponentEventContracts : undefined),
181
218
  eventPayloadNote,
182
219
  ...(knowledge
183
220
  ? {
@@ -188,7 +225,7 @@ export function registerComponentTools(server, platform) {
188
225
  },
189
226
  ...(knowledge.guide ? { guide: knowledge.guide } : {})
190
227
  }
191
- : { knowledgeNote }),
228
+ : mobileKnowledge ? {} : { knowledgeNote }),
192
229
  ...(componentSlug && INTERNAL_ROLES_BY_TYPE[componentSlug]
193
230
  ? {
194
231
  internalRoles: INTERNAL_ROLES_BY_TYPE[componentSlug],
@@ -196,8 +233,9 @@ export function registerComponentTools(server, platform) {
196
233
  }
197
234
  : {}),
198
235
  ...(themeDefaults ? { themeDefaults } : {}),
236
+ ...(mobile ? { platform: 'mobile', ...(mobileKnowledge ? { mobileKnowledge } : {}), mobileNotes: mobileComponentNotes(componentSlug) } : {}),
199
237
  appearanceValues: APPEARANCE_TOKEN_SETS,
200
- stateFlags: STATE_FLAG_NOTES,
238
+ stateFlags: mobile ? { ...STATE_FLAG_NOTES, ...MOBILE_STATE_FLAG_NOTES } : STATE_FLAG_NOTES,
201
239
  validations: definition.validations,
202
240
  sizes: definition.blocks
203
241
  };
@@ -234,11 +272,13 @@ export function registerComponentTools(server, platform) {
234
272
  const catalog = (body.schema ?? []).length > 0
235
273
  ? await applyCatalogLayoutRules(platform, project, environment, applicationId, body.schema)
236
274
  : { clamped: [], heights: [], sizes: [] };
275
+ // Mobil uygulamanın widget'ı mobil katalog satırıdır; tip verilmezse uygulamanın tipi.
276
+ const defaultType = body.type ?? (await resolveProfile(platform, applicationId)).profile.catalogType;
237
277
  return platform.tenant('POST', project, `/components/${applicationId}`, {
238
278
  environment,
239
279
  body: {
240
280
  kind: 'component',
241
- type: 'web',
281
+ type: defaultType,
242
282
  categories: [],
243
283
  definition: {},
244
284
  schema: [],
@@ -8,6 +8,8 @@ import { fetchComponentEventNames } from './components.js';
8
8
  import { PlatformApiError } from '../platform/errors.js';
9
9
  import { handle } from '../platform/responses.js';
10
10
  import { slotWarning } from './layouts.js';
11
+ import { applicationTypeField, resolveProfile } from '../knowledge/platform/resolve.js';
12
+ import { boundFlowSteps, screenProfileWarnings } from './profileWrite.js';
11
13
  import { z } from 'zod';
12
14
  const HOSTS = {
13
15
  layout: {
@@ -101,12 +103,18 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
101
103
  const nested = simulatable ? nestedRepeaterNodes(simulated) : [];
102
104
  if (nested.length > 0)
103
105
  throw new PlatformApiError(nestedRepeaterMessage(nested), 400);
106
+ const resolved = await resolveProfile(platform, applicationId);
104
107
  const reshaped = simulatable || opRecords.some(entry => entry.op === 'remove_component');
108
+ const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, {
109
+ kind: spec.kind,
110
+ ops: [...opRecords, ...propTypeOps],
111
+ steps: await boundFlowSteps(platform, resolved, project, environment, host, applicationId, id, opRecords)
112
+ });
105
113
  const layoutWarnings = reshaped
106
114
  ? [
107
- ...verticalGapWarnings(simulated, spec.codes.gap, touched),
115
+ ...(resolved.profile.layout.blocksMeaningful ? verticalGapWarnings(simulated, spec.codes.gap, touched) : []),
108
116
  ...stringStyleWarning(stringStyleNodes(simulated)),
109
- ...inertFlexPropWarning(inertFlexPropNodes(simulated)),
117
+ ...(resolved.profile.layout.modes.has('grid') ? inertFlexPropWarning(inertFlexPropNodes(simulated)) : []),
110
118
  ...flexSizeWarnings(simulated, spec.codes.flexMismatch, spec.codes.flexInvalid),
111
119
  ...(host === 'layout' ? slotWarning(simulated) : [])
112
120
  ]
@@ -133,8 +141,9 @@ export function registerDesignSchemaPatchTool(server, platform, host) {
133
141
  response.deletedFlows = deletedFlows;
134
142
  if (flowWarnings.length > 0)
135
143
  response.flowWarnings = flowWarnings;
136
- if (layoutWarnings.length > 0)
137
- response.layoutWarnings = layoutWarnings;
144
+ if (layoutWarnings.length > 0 || profileWarnings.length > 0)
145
+ response.layoutWarnings = [...layoutWarnings, ...profileWarnings];
146
+ Object.assign(response, applicationTypeField(resolved));
138
147
  const contentOps = opRecords.filter(entry => entry.op === 'add_component' || entry.op === 'replace_prop');
139
148
  let opDrops = emptyWriteDiff();
140
149
  if (contentOps.length > 0) {
@@ -7,6 +7,23 @@ import { z } from 'zod';
7
7
  import { evaluatePolicy, enforcePolicy } from '../knowledge/policy.js';
8
8
  import { policyRules } from '../knowledge/rules.js';
9
9
  import { EXTERNAL_COMPONENT_PROTOCOL_VERSION } from '../knowledge/backendConstraints.js';
10
+ import { resolveProfile } from '../knowledge/platform/resolve.js';
11
+ import { findingLine } from '../knowledge/platform/profile.js';
12
+ /**
13
+ * MOB-006 — harici bileşen protokolü v1 tarayıcıda koşan `.mjs` custom element'tir; mobil
14
+ * uygulamanın native çizeri onu yükleyemez (mobil protokol v2 Faz 3). Profil kapalı derse çağrı
15
+ * platforma GİTMEZ: yüklenen paket hiçbir ekranda çizilmeyecekti.
16
+ */
17
+ async function assertExternalComponentsAllowed(platform, applicationId, tool) {
18
+ const resolved = await resolveProfile(platform, applicationId);
19
+ if (resolved.profile.externalComponents)
20
+ return;
21
+ throw new PlatformApiError(`${tool}: ${findingLine({
22
+ code: 'MOB-006',
23
+ severity: 'warn',
24
+ message: "harici bileşen mobil uygulamada desteklenmiyor — protokol v1 tarayıcıda koşan .mjs custom element'tir, native çizer onu yükleyemez (mobil protokol v2 Faz 3). Çağrı platforma gönderilmedi; mobil ekranı katalog bileşenleriyle kurun (component_list)."
25
+ })}`, 400);
26
+ }
10
27
  /** Platformun köprü protokolü; scaffold ve publish bu sürüme göre üretir/doğrular (backend ExternalComponentRules.CurrentProtocolVersion) */
11
28
  const PROTOCOL_VERSION = EXTERNAL_COMPONENT_PROTOCOL_VERSION;
12
29
  /** Backend `ExternalComponentRules.MaxBundleSizeBytes` ile aynı sınır */
@@ -199,6 +216,7 @@ export function registerExternalComponentTools(server, platform) {
199
216
  },
200
217
  annotations: writes
201
218
  }, handle(config, async ({ project, environment, applicationId, bundlePath, manifestPath, version }) => {
219
+ await assertExternalComponentsAllowed(platform, applicationId, 'external_component_publish');
202
220
  const manifestFile = resolve(manifestPath);
203
221
  const bundleFile = resolve(bundlePath);
204
222
  let manifest;
@@ -262,10 +280,13 @@ export function registerExternalComponentTools(server, platform) {
262
280
  acknowledgeWarnings: z.boolean().optional().describe('Doğrulama uyarılarını okuyup kabul ettiğinizi bildirir; uyarısı olan sürümlerde zorunlu')
263
281
  },
264
282
  annotations: updates
265
- }, handle(config, ({ project, environment, applicationId, slug, version, acknowledgeWarnings }) => platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/activate`, {
266
- environment,
267
- body: { version, acknowledgeWarnings: acknowledgeWarnings === true }
268
- })));
283
+ }, handle(config, async ({ project, environment, applicationId, slug, version, acknowledgeWarnings }) => {
284
+ await assertExternalComponentsAllowed(platform, applicationId, 'external_component_activate');
285
+ return platform.tenant('POST', project, `/components/${applicationId}/external/${slug}/activate`, {
286
+ environment,
287
+ body: { version, acknowledgeWarnings: acknowledgeWarnings === true }
288
+ });
289
+ }));
269
290
  server.registerTool('external_component_versions', {
270
291
  title: 'Paket sürümlerini listele',
271
292
  description: 'Bir harici komponentin yayınlanmış paket sürümlerini, hangisinin aktif olduğunu ve her sürümün doğrulama raporunu listeler. Geri alınacak sürümü buradan seçin.',
@@ -13,6 +13,7 @@ import { policyRules, flowExpressionSites } from '../knowledge/rules.js';
13
13
  import { functionCallRefs } from '../knowledge/functionGuards.js';
14
14
  import { existingMatch, ifExistsParam, platformRename, withRename } from '../platform/ifExists.js';
15
15
  import { randomUUID } from 'node:crypto';
16
+ import { flowProfileWarnings } from './profileWrite.js';
16
17
  import { z } from 'zod';
17
18
  /**
18
19
  * Akış yazması yanıtında gövdeyi geri döndürmüyor, bu yüzden diff için akışı bir kez okuruz.
@@ -382,6 +383,7 @@ export function registerFlowTools(server, platform) {
382
383
  // Diff YALNIZ çağıranın yazdığı alanları kapsar; `id` ve barındırıcı bağı (`pageId` vb.)
383
384
  // gövdeye gateway koyar, kullanıcının düzeltebileceği bir şey değildir.
384
385
  const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, writtenFlowId, { name: flow.name, slug: flow.slug, steps: flow.steps, errorSteps: flow.errorSteps }, ifExists === 'duplicate');
386
+ const platformWarnings = await flowProfileWarnings(platform, applicationId, [...(flow.steps ?? []), ...(flow.errorSteps ?? [])]);
385
387
  return attachFindings(withWriteDiff(withRename(withNormalization({
386
388
  host,
387
389
  hostId,
@@ -393,6 +395,7 @@ export function registerFlowTools(server, platform) {
393
395
  steps: steps.length
394
396
  },
395
397
  ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
398
+ ...(platformWarnings.length > 0 ? { platformWarnings } : {}),
396
399
  hint: "Akışı bir olaya bağlamak için: page_schema_patch (layout'ta layout_schema_patch, widget'ta component_schema_patch) ops:[{op:'set_event', target:<elementId>, event:'<component_get events listesindeki ad>', eventValue:<flow id>}]; olayda zaten akış bağlıysa ikinciyi add_event ile ekleyin (set_event bağlı akışı değiştirir, PAGE-034). Olay adları komponente özeldir ve 'on' ÖNEKİ ALMAZ (button → 'click', form → 'submit', table → 'mount'/'select'/'sort'…); uydurma ad 200 döner ama bağ sessizce ölü kalır — gateway PAGE-008 ile yakalar."
397
400
  }, normalized), drops.renamed), drops), findings);
398
401
  }));
@@ -446,7 +449,9 @@ export function registerFlowTools(server, platform) {
446
449
  ...(steps === undefined ? {} : { steps }),
447
450
  ...(errorSteps === undefined ? {} : { errorSteps })
448
451
  });
449
- return attachFindings(withWriteDiff(withNormalization({ host, hostId, flow: flowSummary(flow), ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}) }, normalized), drops), findings);
452
+ const platformWarnings = await flowProfileWarnings(platform, applicationId, [...(flow.steps ?? []), ...(flow.errorSteps ?? [])]);
453
+ return attachFindings(withWriteDiff(withNormalization({ host, hostId, flow: flowSummary(flow), ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
454
+ ...(platformWarnings.length > 0 ? { platformWarnings } : {}) }, normalized), drops), findings);
450
455
  }));
451
456
  server.registerTool('flow_patch', {
452
457
  title: "Akışa patch uygula",
@@ -495,6 +500,7 @@ export function registerFlowTools(server, platform) {
495
500
  });
496
501
  const written = patchResult(saved, flow);
497
502
  const drops = await flowWriteDrops(platform, host, project, environment, applicationId, hostId, flowId, { steps: flow.steps, errorSteps: flow.errorSteps });
503
+ const platformWarnings = await flowProfileWarnings(platform, applicationId, [...(flow.steps ?? []), ...(flow.errorSteps ?? [])]);
498
504
  return attachFindings(withWriteDiff(withNormalization({
499
505
  host,
500
506
  hostId,
@@ -502,7 +508,8 @@ export function registerFlowTools(server, platform) {
502
508
  newVersion: written.newVersion,
503
509
  patchTargets: resolved.targets,
504
510
  flow: flowSummary(written.data),
505
- ...(parameterWarnings.length > 0 ? { parameterWarnings } : {})
511
+ ...(parameterWarnings.length > 0 ? { parameterWarnings } : {}),
512
+ ...(platformWarnings.length > 0 ? { platformWarnings } : {})
506
513
  }, [...normalized, ...resolved.notes.map(note => `patch yolu seçiciden indekse çözüldü: ${note}`)]), drops), findings);
507
514
  }));
508
515
  server.registerTool('flow_delete', {
@@ -9,6 +9,8 @@ import { applyPropTypesToSchema, isSetPropTypeOp, setPropTypeErrors } from '../k
9
9
  import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
10
10
  import { PlatformApiError } from '../platform/errors.js';
11
11
  import { handle } from '../platform/responses.js';
12
+ import { applicationTypeField, resolveProfile } from '../knowledge/platform/resolve.js';
13
+ import { screenProfileWarnings } from './profileWrite.js';
12
14
  import { z } from 'zod';
13
15
  const patchOperationsParam = z.array(z.record(z.unknown())).min(1)
14
16
  .describe("JSON Patch listesi (RFC 6902): { op, path, value }. Yol seçici alabilir — '/views/0/children[elementId=baslik]/props/content/data'.");
@@ -46,7 +48,13 @@ export function registerLayoutTools(server, platform) {
46
48
  description: "Layout'u komponent ağacı (views), widget'ları, akışları ve olaylarıyla getirir.",
47
49
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, id: guid },
48
50
  annotations: readOnly
49
- }, handle(config, ({ project, environment, applicationId, id }) => platform.tenant('GET', project, `/layouts/${applicationId}/${id}`, { environment })));
51
+ }, handle(config, async ({ project, environment, applicationId, id }) => {
52
+ const response = await platform.tenant('GET', project, `/layouts/${applicationId}/${id}`, { environment });
53
+ const typeField = applicationTypeField(await resolveProfile(platform, applicationId));
54
+ return response && typeof response === 'object' && !Array.isArray(response) && Object.keys(typeField).length > 0
55
+ ? { ...response, ...typeField }
56
+ : response;
57
+ }));
50
58
  server.registerTool('layout_schema_get', {
51
59
  title: "Layout komponent ağacını oku",
52
60
  description: "Layout'un komponent ağacını (views) okur — page_schema_get'in layout karşılığı. Ağaçta hedefli değişiklik için layout_schema_patch (sayfa ile aynı micro-op sözlüğü, PATCH /layouts/{app}/{id}/schema). Ad/parametre/olay alanlarında ya da düz JSON Patch yolu gerektiğinde layout_patch — o araç buradaki updatedAt'i expectedUpdatedAt olarak kabul eder. Tam ağacı kendiniz yazacaksanız layout_update.",
@@ -61,20 +69,21 @@ export function registerLayoutTools(server, platform) {
61
69
  annotations: readOnly
62
70
  }, handle(config, async ({ project, environment, applicationId, id, mode, componentId }) => {
63
71
  const layout = await fetchHost(platform, 'layout', project, environment, applicationId, id);
72
+ const typeField = applicationTypeField(await resolveProfile(platform, applicationId));
64
73
  const audit = layout.audit;
65
74
  const updatedAt = audit?.updatedAt ?? audit?.createdAt;
66
75
  const views = (layout.views ?? []);
67
76
  if (mode === 'full')
68
- return { updatedAt, views };
77
+ return { updatedAt, ...typeField, views };
69
78
  if (mode === 'subtree') {
70
79
  if (!componentId)
71
80
  throw new PlatformApiError("mode='subtree' için componentId zorunludur.");
72
81
  const subtree = findNode(views, componentId);
73
82
  if (!subtree)
74
83
  throw new PlatformApiError(`Layout'ta '${componentId}' id'li bir komponent yok; layout_schema_get'i summary kipinde çağırıp hedefi doğrulayın.`);
75
- return { updatedAt, views: subtree };
84
+ return { updatedAt, ...typeField, views: subtree };
76
85
  }
77
- return { updatedAt, views: views.map(node => summarize(node)) };
86
+ return { updatedAt, ...typeField, views: views.map(node => summarize(node)) };
78
87
  }));
79
88
  server.registerTool('layout_create', {
80
89
  title: 'Layout oluştur',
@@ -91,18 +100,27 @@ export function registerLayoutTools(server, platform) {
91
100
  annotations: writes
92
101
  }, handle(config, async ({ project, environment, applicationId, name, views, events, parameters }) => {
93
102
  assertScreenWriteGuards('layout', { schema: views, events });
103
+ const resolved = await resolveProfile(platform, applicationId);
94
104
  const fixed = normalizeComponentDefaults(views);
95
105
  const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, views);
96
- const gaps = [...verticalGapWarnings(views ?? [], 'LAY-003 (uyarı)'), ...flexSizeWarnings(views ?? [], 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'), ...spacingLayerWarnings(views ?? [], 'LAY-010 (uyarı)', 'LAY-011 (uyarı)')];
106
+ const gaps = [
107
+ ...(resolved.profile.layout.blocksMeaningful ? verticalGapWarnings(views ?? [], 'LAY-003 (uyarı)') : []),
108
+ ...flexSizeWarnings(views ?? [], 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'),
109
+ ...spacingLayerWarnings(views ?? [], 'LAY-010 (uyarı)', 'LAY-011 (uyarı)'),
110
+ ...await screenProfileWarnings(platform, resolved, project, environment, applicationId, { kind: 'layout', nodes: views ?? [] })
111
+ ];
97
112
  const response = await platform.tenant('POST', project, `/layouts/${applicationId}`, {
98
113
  environment,
99
114
  body: { name, views: views ?? [], events: events ?? [], parameters: parameters ?? null }
100
115
  });
101
116
  const drops = writeDiff({ name, views: views ?? [], events: events ?? [] }, response);
102
117
  const normalized = withWriteDiff(withNormalization(response, [...componentDefaultsNote(fixed), ...catalogNotes, ...slotWarning(views)]), drops);
103
- if (gaps.length === 0 || !normalized || typeof normalized !== 'object')
118
+ const typeField = applicationTypeField(resolved);
119
+ if (!normalized || typeof normalized !== 'object')
104
120
  return normalized;
105
- return { ...normalized, layoutWarnings: gaps };
121
+ if (gaps.length === 0)
122
+ return Object.keys(typeField).length === 0 ? normalized : { ...normalized, ...typeField };
123
+ return { ...normalized, ...typeField, layoutWarnings: gaps };
106
124
  }));
107
125
  server.registerTool('layout_update', {
108
126
  title: 'Layout güncelle',
@@ -126,7 +144,15 @@ export function registerLayoutTools(server, platform) {
126
144
  const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, overrides.views);
127
145
  const body = layoutUpdateBody({ ...current, applicationId }, overrides);
128
146
  await platform.tenant('PUT', project, `/layouts/${applicationId}/${id}`, { environment, body });
129
- const gaps = overrides.views === undefined ? [] : [...verticalGapWarnings(body.views, 'LAY-003 (uyarı)'), ...flexSizeWarnings(body.views, 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'), ...spacingLayerWarnings(body.views, 'LAY-010 (uyarı)', 'LAY-011 (uyarı)')];
147
+ const resolved = overrides.views === undefined ? undefined : await resolveProfile(platform, applicationId);
148
+ const gaps = overrides.views === undefined || !resolved
149
+ ? []
150
+ : [
151
+ ...(resolved.profile.layout.blocksMeaningful ? verticalGapWarnings(body.views, 'LAY-003 (uyarı)') : []),
152
+ ...flexSizeWarnings(body.views, 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'),
153
+ ...spacingLayerWarnings(body.views, 'LAY-010 (uyarı)', 'LAY-011 (uyarı)'),
154
+ ...await screenProfileWarnings(platform, resolved, project, environment, applicationId, { kind: 'layout', nodes: body.views })
155
+ ];
130
156
  const summary = { id, changed: Object.keys(overrides), preserved: { viewNodes: body.views.length } };
131
157
  if (gaps.length > 0)
132
158
  summary.layoutWarnings = gaps;
@@ -193,7 +219,13 @@ export function registerLayoutTools(server, platform) {
193
219
  body: { baseVersion: baseVersionOf(document, 'layout_get'), ops: netOps }
194
220
  });
195
221
  const written = patchResult(saved, finalDocument);
196
- const gaps = [...verticalGapWarnings(body.views, 'LAY-003 (uyarı)'), ...flexSizeWarnings(body.views, 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'), ...spacingLayerWarnings(body.views, 'LAY-010 (uyarı)', 'LAY-011 (uyarı)')];
222
+ const profile = await resolveProfile(platform, applicationId);
223
+ const gaps = [
224
+ ...(profile.profile.layout.blocksMeaningful ? verticalGapWarnings(body.views, 'LAY-003 (uyarı)') : []),
225
+ ...flexSizeWarnings(body.views, 'LAY-007 (uyarı)', 'LAY-008 (uyarı)'),
226
+ ...spacingLayerWarnings(body.views, 'LAY-010 (uyarı)', 'LAY-011 (uyarı)'),
227
+ ...await screenProfileWarnings(platform, profile, project, environment, applicationId, { kind: 'layout', nodes: body.views })
228
+ ];
197
229
  const summary = {
198
230
  id,
199
231
  applied: patchOps.length,
@@ -0,0 +1,203 @@
1
+ import { z } from 'zod';
2
+ import { environmentParam, guid, projectParam, writes } from './common.js';
3
+ import { fetchHost, pageUpdateBody, unwrap } from './screens.js';
4
+ import { fetchCatalogSlugs } from './components.js';
5
+ import { mobileTabsWarnings } from './applications.js';
6
+ import { PlatformApiError } from '../platform/errors.js';
7
+ import { handle } from '../platform/responses.js';
8
+ import { ensurePageContainer } from '../knowledge/normalize.js';
9
+ import { mobileProfile } from '../knowledge/platform/mobile.js';
10
+ import { MAX_MOBILE_TABS } from '../knowledge/platform/mobileTabs.js';
11
+ import { forgetApplication } from '../knowledge/platform/resolve.js';
12
+ import { readKnowledgeFile } from '../resources/knowledge.js';
13
+ /** Ana sayfa: açıkça verilen, yoksa ilk sekme, yoksa ilk sayfa. */
14
+ function homepageSlug(input) {
15
+ return input.homepage ?? input.tabs?.[0]?.pageSlug ?? input.pages[0].pageSlug;
16
+ }
17
+ export function mobileAppInputErrors(input) {
18
+ const errors = [];
19
+ const slugs = input.pages.map(page => page.pageSlug);
20
+ const repeated = slugs.filter((slug, index) => slugs.indexOf(slug) !== index);
21
+ if (input.pages.length === 0)
22
+ errors.push('pages en az bir sayfa ister');
23
+ if (repeated.length > 0)
24
+ errors.push(`pages içinde tekrar eden slug var: ${[...new Set(repeated)].join(', ')}`);
25
+ if (input.tabs !== undefined) {
26
+ const tabSlugs = input.tabs.map(tab => tab.pageSlug);
27
+ if (input.tabs.length === 0)
28
+ errors.push("tabs verildiyse en az bir öğe taşır; sekme istemiyorsan alanı hiç gönderme (yığın)");
29
+ if (input.tabs.length > MAX_MOBILE_TABS)
30
+ errors.push(`tabs en fazla ${MAX_MOBILE_TABS} öğe taşır (${input.tabs.length} verildi) — fazlası yığına açılan sayfa olarak kalmalı`);
31
+ for (const slug of tabSlugs.filter(slug => !slugs.includes(slug)))
32
+ errors.push(`tabs '${slug}' pages içinde yok — sekme yalnız kurulan bir sayfayı gösterebilir`);
33
+ if (tabSlugs.some((slug, index) => tabSlugs.indexOf(slug) !== index))
34
+ errors.push('tabs içinde tekrar eden sayfa var');
35
+ }
36
+ if (input.homepage !== undefined && !slugs.includes(input.homepage))
37
+ errors.push(`homepage '${input.homepage}' pages içinde yok`);
38
+ return errors;
39
+ }
40
+ export function buildMobileAppPlan(input) {
41
+ const home = homepageSlug(input);
42
+ const others = input.pages.filter(page => page.pageSlug !== home);
43
+ return [
44
+ { step: 'application_create', tool: 'application_create', summary: "type:'mobile' uygulama; backend ana sayfayı kendisi açar" },
45
+ { step: 'layout_create', tool: 'layout_create', summary: `kabuk: app-bar + slot${input.drawer ? ' + drawer widget (app-bar.menuTarget)' : ''}` },
46
+ ...others.map((page, index) => ({ step: `page_create#${index + 1}`, tool: 'page_create', summary: `'${page.pageSlug}' (${page.pageName}), layoutId ile` })),
47
+ { step: 'page_update#homepage', tool: 'page_update', summary: `backend'in ana sayfası → '${home}' (ad, slug, layoutId)` },
48
+ ...(input.tabs ? [{ step: 'application_update', tool: 'application_update', summary: `properties.mobile.tabs: ${input.tabs.map(tab => tab.pageSlug).join(' · ')}` }] : [])
49
+ ];
50
+ }
51
+ /** Kabuk ağacı örnek dosyasından okunur — örnek ile aracın kurduğu layout tek kaynaktır. */
52
+ function shellViews(input) {
53
+ const example = JSON.parse(readKnowledgeFile('examples', input.drawer ? 'mobile-app-shell-drawer.json' : 'mobile-app-shell-stack.json'));
54
+ const views = structuredClone(example.layoutCreate.views);
55
+ const appBar = views[0]?.children?.find(node => node.type === 'app-bar');
56
+ if (appBar && input.title)
57
+ appBar.props.title = { type: 'static', ref: null, data: input.title };
58
+ return views;
59
+ }
60
+ const SHELL_TYPES = ['app-bar', 'slot'];
61
+ export function registerMobileTools(server, platform) {
62
+ const config = platform.config;
63
+ server.registerTool('mobile_app_create', {
64
+ title: 'Mobil uygulamayı kabuğuyla kur',
65
+ description: "Mobil uygulamayı (type:'mobile'), kabuk layout'unu (app-bar + slot; drawer:true ise çekmece widget'ı), sayfalarını ve isteğe bağlı sekme ayarını (properties.mobile.tabs) TEK çağrıda kurar. Sıra: application_create → layout_create → page_create (sayfalar layoutId ile doğar) → backend'in açtığı ana sayfa seçilen ana sayfaya dönüştürülür (yetim sayfa kalmaz) → application_update (yalnız tabs verildiyse). tabs yoksa gezinme yığındır. dryRun:true planı döner, hiçbir şey yazmaz. Platformda işlem yoktur: hata olursa yanıt completed (kurulanlar), failed (düşen adım) ve remaining (kalanlar) taşır — kalanları tekil araçlarla tamamlayın. Kabuk bileşenleri (app-bar, slot) mobil katalogda yoksa layout atlanır ve uyarı döner. Rehber: platform_guide('mobile').",
66
+ inputSchema: {
67
+ project: projectParam,
68
+ environment: environmentParam,
69
+ projectId: guid,
70
+ slug: z.string().min(1),
71
+ name: z.string().min(1),
72
+ color: z.string().optional(),
73
+ pages: z.array(z.object({ pageSlug: z.string().min(1), pageName: z.string().min(1) })).min(1),
74
+ tabs: z.array(z.object({ pageSlug: z.string().min(1), label: z.string().optional(), icon: z.string().optional() })).optional()
75
+ .describe(`1-${MAX_MOBILE_TABS} sekme; her pageSlug pages içinden. Verilmezse sekme çubuğu yok (yığın). label 't:<anahtar>' ile yazılırsa çeviri sözlüğünden çözülür (platform_guide('mobile') §Çok dil).`),
76
+ drawer: z.boolean().optional().describe("true: layout'a drawer widget'ı + app-bar.menuTarget (çekmece)"),
77
+ title: z.string().optional().describe('app-bar başlığı; boşsa her sayfanın adı yazılır'),
78
+ homepage: z.string().optional().describe('Ana sayfanın pageSlug\'ı; varsayılan ilk sekme, sekme yoksa ilk sayfa'),
79
+ dryRun: z.boolean().optional()
80
+ },
81
+ annotations: writes
82
+ }, handle(config, async ({ project, environment, projectId, slug, name, color, pages, tabs, drawer, title, homepage, dryRun }) => {
83
+ const input = { pages, ...(tabs ? { tabs } : {}), ...(drawer ? { drawer } : {}), ...(homepage ? { homepage } : {}), ...(title ? { title } : {}) };
84
+ const errors = mobileAppInputErrors(input);
85
+ if (errors.length > 0)
86
+ throw new PlatformApiError(`mobile_app_create: ${errors.join(' · ')}. Hiçbir şey yazılmadı.`, 400);
87
+ const plan = buildMobileAppPlan(input);
88
+ if (dryRun)
89
+ return { dryRun: true, plan };
90
+ const completed = [];
91
+ const skipped = new Set();
92
+ const warnings = [];
93
+ const home = homepageSlug(input);
94
+ const pageIds = new Map();
95
+ let applicationId;
96
+ let homepageId;
97
+ let layoutId;
98
+ let current = 'application_create';
99
+ try {
100
+ const created = unwrap(await platform.org('POST', `/applications/${projectId}`, {
101
+ environment,
102
+ body: { slug, name, type: 'mobile', ...(color ? { color } : {}) }
103
+ }));
104
+ applicationId = String(created.id ?? '');
105
+ homepageId = typeof created.homepageId === 'string'
106
+ ? created.homepageId
107
+ : String(created.homepage?.id ?? '');
108
+ forgetApplication(platform);
109
+ if (!applicationId || !homepageId)
110
+ throw new PlatformApiError('application_create yanıtı uygulama ya da ana sayfa kimliği taşımıyor — application_get ile kontrol edin.');
111
+ completed.push({ step: 'application_create', id: applicationId });
112
+ current = 'layout_create';
113
+ const catalog = await fetchCatalogSlugs(platform, project, environment, applicationId);
114
+ const missing = SHELL_TYPES.filter(type => !catalog.has(type));
115
+ if (missing.length > 0) {
116
+ skipped.add('layout_create');
117
+ warnings.push(`MOB-003 (uyarı) — kabuk bileşenleri mobil katalogda yok (${missing.join(', ')}): layout KURULMADI, sayfalar layout'suz açıldı. Katalog yazıldıktan sonra kabuğu ngapps://examples/mobile-app-shell-${input.drawer ? 'drawer' : input.tabs ? 'tab' : 'stack'} ile layout_create + page_update(layoutId) olarak kurun.`);
118
+ }
119
+ else {
120
+ const layout = unwrap(await platform.tenant('POST', project, `/layouts/${applicationId}`, {
121
+ environment,
122
+ body: { name: `${name} kabuğu`, views: shellViews(input), events: [], parameters: null }
123
+ }));
124
+ layoutId = String(layout.id ?? '');
125
+ completed.push({ step: 'layout_create', id: layoutId });
126
+ }
127
+ const others = pages.filter(page => page.pageSlug !== home);
128
+ for (const [index, page] of others.entries()) {
129
+ current = `page_create#${index + 1}`;
130
+ const saved = unwrap(await platform.tenant('POST', project, `/pages/${applicationId}`, {
131
+ environment,
132
+ body: {
133
+ name: page.pageName,
134
+ slug: page.pageSlug,
135
+ browserTitle: '',
136
+ browserTitleKey: '',
137
+ layoutId: layoutId ?? null,
138
+ roleIds: [],
139
+ middlewareIds: [],
140
+ events: [],
141
+ schema: ensurePageContainer([], mobileProfile.layout.rootContainer).schema
142
+ }
143
+ }));
144
+ pageIds.set(page.pageSlug, String(saved.id ?? ''));
145
+ completed.push({ step: current, id: String(saved.id ?? '') });
146
+ }
147
+ current = 'page_update#homepage';
148
+ const homePage = pages.find(page => page.pageSlug === home);
149
+ const existing = await fetchHost(platform, 'page', project, environment, applicationId, homepageId);
150
+ await platform.tenant('PUT', project, `/pages/${applicationId}/${homepageId}`, {
151
+ environment,
152
+ body: pageUpdateBody(existing, { name: homePage.pageName, slug: homePage.pageSlug, layoutId: layoutId ?? null })
153
+ });
154
+ pageIds.set(home, homepageId);
155
+ completed.push({ step: current, id: homepageId });
156
+ if (tabs) {
157
+ current = 'application_update';
158
+ const properties = {
159
+ mobile: {
160
+ tabs: {
161
+ enabled: true,
162
+ style: 'default',
163
+ showLabels: true,
164
+ items: tabs.map(tab => ({ page: pageIds.get(tab.pageSlug), ...(tab.label ? { label: tab.label } : {}), ...(tab.icon ? { icon: tab.icon } : {}) }))
165
+ }
166
+ }
167
+ };
168
+ warnings.push(...await mobileTabsWarnings(platform, applicationId, platform.resolveProject(project), homepageId, properties));
169
+ await platform.org('PUT', `/applications/${applicationId}`, {
170
+ environment,
171
+ body: { slug, name, homepageId, ...(color ? { color } : {}), properties }
172
+ });
173
+ forgetApplication(platform, applicationId);
174
+ completed.push({ step: current });
175
+ }
176
+ }
177
+ catch (error) {
178
+ if (completed.length === 0)
179
+ throw error;
180
+ const done = new Set(completed.map(entry => entry.step));
181
+ return {
182
+ applicationId,
183
+ ...(layoutId ? { layoutId } : {}),
184
+ completed,
185
+ failed: { step: current, error: error instanceof Error ? error.message : String(error) },
186
+ remaining: plan.map(step => step.step).filter(step => !done.has(step) && !skipped.has(step) && step !== current),
187
+ ...(warnings.length > 0 ? { warnings } : {}),
188
+ note: 'Kurulum yarıda kaldı: completed kurulanları, remaining eksik adımları söyler. Kalanları tekil araçlarla tamamlayın (page_create, page_update, application_update) — mobile_app_create aynı slug ile tekrar çağrılırsa uygulamayı ikinci kez açmaya çalışır.'
189
+ };
190
+ }
191
+ return {
192
+ applicationId,
193
+ homepageId,
194
+ ...(layoutId ? { layoutId } : {}),
195
+ applicationType: 'mobile',
196
+ pages: pages.map(page => ({ slug: page.pageSlug, name: page.pageName, id: pageIds.get(page.pageSlug), ...(page.pageSlug === home ? { homepage: true } : {}) })),
197
+ ...(tabs ? { tabs: tabs.map(tab => tab.pageSlug) } : {}),
198
+ completed,
199
+ ...(warnings.length > 0 ? { warnings } : {}),
200
+ next: "Ekranları page_schema_patch ile doldurun; doğrulama page_screenshot (mobilde varsayılan device: 'phone'). platform_guide('mobile')."
201
+ };
202
+ }));
203
+ }