@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6

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 (101) hide show
  1. package/.env.example +8 -3
  2. package/.env.production.example +2 -0
  3. package/README.md +229 -193
  4. package/dist/config.js +20 -2
  5. package/dist/knowledge/backendConstraints.js +897 -0
  6. package/dist/knowledge/componentCatalog.js +196 -42
  7. package/dist/knowledge/coverage.js +43 -0
  8. package/dist/knowledge/flowGuards.js +30 -0
  9. package/dist/knowledge/guideSections.js +49 -0
  10. package/dist/knowledge/hydrate.js +97 -0
  11. package/dist/knowledge/languageCodes.js +36 -0
  12. package/dist/knowledge/languages.js +105 -0
  13. package/dist/knowledge/layoutSummary.js +10 -0
  14. package/dist/knowledge/localization.js +51 -0
  15. package/dist/knowledge/normalize.js +449 -16
  16. package/dist/knowledge/outlines.js +136 -12
  17. package/dist/knowledge/propTypes.js +80 -0
  18. package/dist/knowledge/rules.js +618 -43
  19. package/dist/knowledge/schemaGuard.js +199 -0
  20. package/dist/knowledge/styleResolve.js +63 -4
  21. package/dist/observability/trace.js +27 -3
  22. package/dist/platform/errors.js +46 -4
  23. package/dist/platform/http.js +33 -4
  24. package/dist/platform/patch.js +27 -0
  25. package/dist/platform/refs.js +104 -0
  26. package/dist/platform/responses.js +9 -2
  27. package/dist/platform/screenshot.js +527 -16
  28. package/dist/platform/secrets.js +40 -0
  29. package/dist/platform/tokenVault.js +89 -0
  30. package/dist/platform/writeDiff.js +281 -0
  31. package/dist/resources/examples/layout-app-shell.json +1 -1
  32. package/dist/resources/examples/microservice-table-list.json +2 -4
  33. package/dist/resources/examples/page-dynamic-form.json +2 -2
  34. package/dist/resources/examples/page-flex-sections.json +99 -0
  35. package/dist/resources/examples/page-sign-in.json +3 -2
  36. package/dist/resources/guides/component.md +69 -8
  37. package/dist/resources/guides/entity.md +404 -16
  38. package/dist/resources/guides/expressions.md +44 -2
  39. package/dist/resources/guides/external-component.md +9 -0
  40. package/dist/resources/guides/getting-started.md +77 -7
  41. package/dist/resources/guides/layout.md +53 -10
  42. package/dist/resources/guides/microservice.md +226 -13
  43. package/dist/resources/guides/middleware.md +89 -1
  44. package/dist/resources/guides/page.md +383 -46
  45. package/dist/resources/guides/translation.md +126 -8
  46. package/dist/resources/guides/view.md +16 -1
  47. package/dist/resources/guides/workflow.md +198 -27
  48. package/dist/resources/knowledge.js +5 -0
  49. package/dist/resources/rules.js +27 -0
  50. package/dist/resources/schema-fingerprint.json +8 -0
  51. package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
  52. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  53. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  54. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  55. package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
  56. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  57. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  58. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  59. package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
  60. package/dist/resources/schemas/GetRowsQuery.json +3 -0
  61. package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
  62. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
  64. package/dist/resources/schemas/StepDto.json +23 -3
  65. package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
  66. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  67. package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
  69. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  70. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  71. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  72. package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
  73. package/dist/server.js +4 -0
  74. package/dist/tools/access.js +3 -3
  75. package/dist/tools/applications.js +1 -1
  76. package/dist/tools/automations.js +63 -30
  77. package/dist/tools/common.js +39 -0
  78. package/dist/tools/components.js +16 -7
  79. package/dist/tools/datasources.js +83 -21
  80. package/dist/tools/deployments.js +34 -2
  81. package/dist/tools/dtos.js +25 -12
  82. package/dist/tools/entities.js +113 -28
  83. package/dist/tools/externalComponents.js +9 -3
  84. package/dist/tools/files.js +20 -6
  85. package/dist/tools/flows.js +110 -11
  86. package/dist/tools/folders.js +7 -2
  87. package/dist/tools/globalFunctions.js +63 -0
  88. package/dist/tools/layouts.js +103 -29
  89. package/dist/tools/microservices.js +229 -38
  90. package/dist/tools/organizations.js +5 -6
  91. package/dist/tools/pages.js +365 -100
  92. package/dist/tools/platformInfo.js +152 -11
  93. package/dist/tools/projects.js +183 -33
  94. package/dist/tools/queries.js +32 -10
  95. package/dist/tools/rows.js +159 -45
  96. package/dist/tools/screenWrite.js +59 -0
  97. package/dist/tools/screens.js +28 -0
  98. package/dist/tools/translations.js +24 -8
  99. package/dist/tools/views.js +20 -10
  100. package/dist/tools/workflows.js +33 -16
  101. package/package.json +6 -3
@@ -0,0 +1,199 @@
1
+ import { unstoredKeys } from '../platform/writeDiff.js';
2
+ import { logStep } from '../observability/trace.js';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readFileSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ const schemasDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'schemas');
7
+ const documents = new Map();
8
+ /**
9
+ * Yazma gövdesindeki TANINMAYAN anahtarı platform sessizce yok sayar: `isUnique` gönderilen bir
10
+ * index UNIQUE OLMADAN kurulur, istek 200 döner (ölçüldü 2026-09-07, `ENT-013`).
11
+ *
12
+ * Backend bunun için `X-Unmapped-Members` başlığını yazıyor ama kanal olarak KULLANILAMAZ:
13
+ * yalnız İLK eşleşmeyen anahtarı bildiriyor ve gateway'in GET→PUT gidiş-dönüşü gövdeye yedi
14
+ * yapısal anahtar koyuyor (`slug` · `datasourceId` · `primaryKey` · `type` · `applications` ·
15
+ * `automationCount` · `audit`). Sırada `slug` önce geldiği için başlık sıradan bir `entity_patch`
16
+ * çağrısında hep `slug` der ve ajanın yazdığı `indexes[0].isUnique` ASLA görünmez — kanal
17
+ * sinyali maskeler (g-03 Adım 0 ölçümü).
18
+ *
19
+ * Bu yüzden denetim gateway'in kendi taşıdığı şemalardan yapılır: çevrimdışı çalışır, TÜM
20
+ * anahtarları görür ve `introducedUnmappedMembers` sayesinde ajanın yazdığını gateway'in
21
+ * taşıdığından ayırabilir.
22
+ */
23
+ function schemaDocument(command) {
24
+ if (documents.has(command))
25
+ return documents.get(command);
26
+ // Ad dosya yoluna giriyor: dizin dışına çıkan ya da uzantı taşıyan bir değer okunmaz.
27
+ const document = /^[A-Za-z0-9_]+$/.test(command)
28
+ ? (() => {
29
+ try {
30
+ return JSON.parse(readFileSync(join(schemasDirectory, `${command}.json`), 'utf8'));
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ })()
36
+ : undefined;
37
+ documents.set(command, document);
38
+ return document;
39
+ }
40
+ /** `$ref` ya da tek elemanlı `allOf: [{ $ref }]` sarmalının hedef adı; ikincisi Swashbuckle'ın kısıt/nullable eklenmiş referans için ürettiği biçimdir. */
41
+ function referenceName(node) {
42
+ if (!node || typeof node !== 'object')
43
+ return undefined;
44
+ const record = node;
45
+ if (typeof record.$ref === 'string')
46
+ return record.$ref.replace('#/components/schemas/', '');
47
+ const allOf = record.allOf;
48
+ if (Array.isArray(allOf) && allOf.length === 1)
49
+ return referenceName(allOf[0]);
50
+ return undefined;
51
+ }
52
+ function resolve(document, node) {
53
+ if (!node || typeof node !== 'object')
54
+ return undefined;
55
+ const record = node;
56
+ const reference = record.$ref;
57
+ if (typeof reference === 'string') {
58
+ // Üretilen şemalarda tek biçim var: '#/components/schemas/<tam ad>'.
59
+ const name = reference.replace('#/components/schemas/', '');
60
+ const components = document.components?.schemas;
61
+ const target = components?.[name];
62
+ return target && typeof target === 'object' ? target : undefined;
63
+ }
64
+ // FluentValidation kısıtları swagger'a aktığından beri (2026-09-08) referanslı bir özellik
65
+ // `allOf: [{ $ref }]` + kendi anahtarları (nullable, minLength…) şeklinde gelebiliyor; hedef
66
+ // çözülür, özelliğin kendi anahtarları üstüne yazılır.
67
+ const allOf = record.allOf;
68
+ if (Array.isArray(allOf) && allOf.length === 1) {
69
+ const inner = resolve(document, allOf[0]);
70
+ const { allOf: _dropped, ...own } = record;
71
+ return inner ? { ...inner, ...own } : undefined;
72
+ }
73
+ return record;
74
+ }
75
+ function walk(document, schema, value, path, into) {
76
+ if (!schema || !value || typeof value !== 'object')
77
+ return;
78
+ if (Array.isArray(value)) {
79
+ const items = resolve(document, schema.items);
80
+ value.forEach((entry, index) => walk(document, items, entry, `${path}[${index}]`, into));
81
+ return;
82
+ }
83
+ const properties = schema.properties;
84
+ if (!properties)
85
+ return;
86
+ for (const [key, entry] of Object.entries(value)) {
87
+ const child = path === '' ? key : `${path}.${key}`;
88
+ // `additionalProperties: false` olmayan bir düğümde serbest anahtar meşrudur (ör. `properties`
89
+ // alanı gibi şemasız sözlükler) — orada susmak yanlış alarmı önler.
90
+ if (!Object.hasOwn(properties, key)) {
91
+ // `writeDiff`'in gerekçeli istisnaları burada da geçerli: gateway'in bilerek gönderdiği,
92
+ // platformun `fieldId`'den çözüp saklamadığı kolaylık alanları hata değildir. Tek küme
93
+ // tutulur — ikiye ayrılırsa ayrışırlar.
94
+ if (schema.additionalProperties === false && !unstoredKeys.has(key))
95
+ into.push(child);
96
+ continue;
97
+ }
98
+ walk(document, resolve(document, properties[key]), entry, child, into);
99
+ }
100
+ }
101
+ /**
102
+ * `command` üretilen şema dosyasının adıdır (`ngapps://schemas/<command>`); bilinmeyen ad
103
+ * sessizce boş liste döner — şema yoksa iddia da yoktur.
104
+ */
105
+ export function unmappedMembers(command, body) {
106
+ const document = schemaDocument(command);
107
+ if (!document)
108
+ return [];
109
+ const found = [];
110
+ walk(document, document.schema, body, '', found);
111
+ return found;
112
+ }
113
+ /**
114
+ * Yamanın GETİRDİĞİ eşleşmeyen anahtarları döner. Gateway'in GET'ten taşıdığı yapısal alanlar iki
115
+ * tarafta da bulunduğu için elenir; geriye yalnız çağıranın eklediği kalır.
116
+ */
117
+ export function introducedUnmappedMembers(command, before, after) {
118
+ const existing = new Set(unmappedMembers(command, before));
119
+ return unmappedMembers(command, after).filter(member => !existing.has(member));
120
+ }
121
+ /**
122
+ * Yanıt yan kanalı (`runtimeErrors` · `gatewayMasked` · `silentlyDropped` ile aynı desen).
123
+ * Alan adı `secretKeyPattern` ile eşleşmemeli, yoksa değeri sessizce `***` olur — GW-001 dersi;
124
+ * `entities.test.ts` bunu yanıt üzerinde doğruluyor.
125
+ */
126
+ export function withUnmappedMembers(payload, members) {
127
+ if (members.length === 0 || !payload || typeof payload !== 'object')
128
+ return payload;
129
+ logStep('şema ✕', `${members.length} anahtar komut şemasında yok — platform onları sessizce yok sayar`, members);
130
+ return {
131
+ ...payload,
132
+ unmappedMembers: members,
133
+ unmappedMembersNote: "Bu anahtarlar gönderdiğiniz komutun şemasında YOK: platform onları hata vermeden YOK SAYAR, yani o alanlar kaydedilmedi. Doğru adlar için ngapps://schemas kaynağına bakın."
134
+ };
135
+ }
136
+ const numericKinds = ['minLength', 'maxLength', 'minimum', 'maximum', 'minItems'];
137
+ function limitOf(path, schema, required) {
138
+ const limit = { path };
139
+ if (required)
140
+ limit.required = true;
141
+ for (const kind of numericKinds) {
142
+ if (typeof schema[kind] === 'number')
143
+ limit[kind] = schema[kind];
144
+ }
145
+ if (typeof schema.pattern === 'string' && schema.pattern !== '')
146
+ limit.pattern = schema.pattern;
147
+ return Object.keys(limit).length > 1 ? limit : undefined;
148
+ }
149
+ function collectSchemaLimits(document, schema, path, chain, into) {
150
+ const properties = schema?.properties;
151
+ if (!schema || !properties)
152
+ return;
153
+ const required = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
154
+ for (const [key, raw] of Object.entries(properties)) {
155
+ const childPath = path === '' ? key : `${path}.${key}`;
156
+ const child = resolve(document, raw);
157
+ if (!child)
158
+ continue;
159
+ const limit = limitOf(childPath, child, required.has(key));
160
+ if (limit)
161
+ into.push(limit);
162
+ if (child.type === 'array') {
163
+ const items = resolve(document, child.items);
164
+ if (!items)
165
+ continue;
166
+ const itemLimit = limitOf(`${childPath}[]`, items, false);
167
+ if (itemLimit)
168
+ into.push(itemLimit);
169
+ const itemsName = referenceName(child.items);
170
+ // Özyineli tip (FieldDto.fields → FieldDto) bir kez yürünür; katalog da yalnız ilk seviyeyi tanır.
171
+ if (itemsName !== undefined && chain.includes(itemsName))
172
+ continue;
173
+ collectSchemaLimits(document, items, `${childPath}[]`, itemsName !== undefined ? [...chain, itemsName] : chain, into);
174
+ continue;
175
+ }
176
+ const childName = referenceName(raw);
177
+ if (childName !== undefined && chain.includes(childName))
178
+ continue;
179
+ collectSchemaLimits(document, child, childPath, childName !== undefined ? [...chain, childName] : chain, into);
180
+ }
181
+ }
182
+ /** Şema dokümanındaki (`{ schema, components }`) bütün alan kısıtlarını katalog yol notasyonuyla (`fields[].name`) döner. */
183
+ export function schemaLimitsOf(document) {
184
+ if (!document)
185
+ return [];
186
+ const found = [];
187
+ const rootName = referenceName(document.schema);
188
+ collectSchemaLimits(document, resolve(document, document.schema), '', rootName !== undefined ? [rootName] : [], found);
189
+ return found;
190
+ }
191
+ /**
192
+ * Paketlenmiş swagger şemasından FluentValidation kaynaklı kısıtlar (`MicroElements.Swashbuckle.FluentValidation`,
193
+ * backend 2026-09-08): NotEmpty → required (+ minLength 1), MaximumLength → maxLength, Matches → pattern,
194
+ * InclusiveBetween → minimum/maximum. `npm run generate-schemas` yenilenmeden eski şemada kısıt yoktur ve
195
+ * bu liste boş döner — o durumda `backendConstraints.ts` kataloğu tek başına konuşur.
196
+ */
197
+ export function schemaLimits(command) {
198
+ return schemaLimitsOf(schemaDocument(command));
199
+ }
@@ -156,7 +156,8 @@ export const STYLE_GROUP_FIELDS = {
156
156
  'borderBottom',
157
157
  'borderLeft',
158
158
  'shadow',
159
- 'contentOverflow'
159
+ 'contentOverflow',
160
+ 'opacity'
160
161
  ],
161
162
  typographyStyle: [
162
163
  'textColor',
@@ -171,6 +172,8 @@ export const STYLE_GROUP_FIELDS = {
171
172
  ],
172
173
  hoverStyle: ['background', 'borderColor', 'shadow', 'lift', 'transitionMs']
173
174
  };
175
+ /** `overflowTarget` için "kökün kendisi" işareti (frontend `ROOT_TARGET` kopyası). */
176
+ export const ROOT_TARGET = '&';
174
177
  export function composeTypedStyle(input, ext) {
175
178
  const groups = {};
176
179
  if (isPlainObject(input.surfaceStyle)) {
@@ -198,8 +201,19 @@ export function composeTypedStyle(input, ext) {
198
201
  groups.border = `1px ${surface.borderStyle || 'solid'} ${surface.borderColor || DEFAULT_DIVIDER_COLOR}`;
199
202
  }
200
203
  }
204
+ // Opaklık yüzdedir (eski Surface bloğuyla aynı sözleşme): 100 ve boş
205
+ // yazılmaz, aralık dışı yok sayılır.
206
+ const opacity = asNumber(surface.opacity);
207
+ if (opacity !== undefined && opacity >= 0 && opacity < 100) {
208
+ groups.opacity = String(opacity / 100);
209
+ }
210
+ // `ROOT_TARGET` (`&`) kökün kendisidir: kendi derleyicisi olmayan kaplarda
211
+ // yüzey düğüm kutusuna yazılır ve kaydırılan da odur.
201
212
  if (isSet(surface.contentOverflow) && ext?.overflowTarget) {
202
- nest(groups, ext.overflowTarget, { overflow: surface.contentOverflow });
213
+ if (ext.overflowTarget === ROOT_TARGET)
214
+ groups.overflow = surface.contentOverflow;
215
+ else
216
+ nest(groups, ext.overflowTarget, { overflow: surface.contentOverflow });
203
217
  }
204
218
  }
205
219
  if (isPlainObject(input.typographyStyle)) {
@@ -414,6 +428,44 @@ export const TYPED_STYLE_COMPONENTS = {
414
428
  omit: { hoverStyle: ['borderColor'] }
415
429
  }
416
430
  };
431
+ /**
432
+ * Kendi derleyicisi OLMAYAN kap bileşenleri (2026-09-04): şemalarında
433
+ * `surfaceStyle` grubu var ama bileşen prop'u okumaz — render katmanı yüzeyi
434
+ * düğüm kutusuna (`.ng-app__node-item`) genel yolla yazar, taşma kökün
435
+ * kendisine gider (`packages/ng-apps-ui/src/style-groups/node-surface.ts`).
436
+ * Bu tipler `TYPED_STYLE_COMPONENTS`'a GİRMEZ: `style` kaçış kanalı onlarda
437
+ * eski birleşik kaskadla (tema+preset+own) çözülmeye devam eder.
438
+ */
439
+ export const NODE_SURFACE_TYPES = new Set([
440
+ 'table',
441
+ 'datatable',
442
+ 'form',
443
+ 'collapsible-container',
444
+ 'drawer',
445
+ 'modal',
446
+ 'popover',
447
+ 'tabs',
448
+ 'stepper',
449
+ 'repeater',
450
+ 'html',
451
+ 'file-viewer',
452
+ 'document-editor',
453
+ 'layout',
454
+ 'layout-region',
455
+ 'layout-shell',
456
+ 'qm-content',
457
+ 'qm-sidebar',
458
+ 'auth-layout'
459
+ ]);
460
+ const NODE_SURFACE_EXTENSION = { overflowTarget: ROOT_TARGET };
461
+ /** Düğüm kutusuna inen yüzey; kap tipi değilse ya da grup boşsa yok. */
462
+ export function composeNodeSurfaceStyle(type, surfaceStyle) {
463
+ if (!type || !NODE_SURFACE_TYPES.has(type))
464
+ return undefined;
465
+ if (!isPlainObject(surfaceStyle))
466
+ return undefined;
467
+ return composeTypedStyle({ surfaceStyle }, NODE_SURFACE_EXTENSION);
468
+ }
417
469
  /** text derlemesi — testler ve dış tüketiciler için sarmalayıcı. */
418
470
  export function composeTextStyle(input) {
419
471
  return composeTypedStyle(input, TYPED_STYLE_COMPONENTS.text.ext);
@@ -467,9 +519,16 @@ export function resolveNodeStyleReport(node, source) {
467
519
  own: isPlainObject(ownStyle) ? Object.keys(ownStyle).filter(key => key !== STYLE_PRESET_KEY).length > 0 : Boolean(ownStyle)
468
520
  };
469
521
  if (!config) {
470
- // Kayıtsız tip: eski birleşik kaskad.
522
+ // Kayıtsız tip: eski birleşik kaskad. Kap tiplerinde tipli yüzey grubu
523
+ // düğüm kutusuna ayrıca iner; rapor onu kaskadın ÜSTÜNE koyar (kutu,
524
+ // bileşenin kökünü sarar).
471
525
  const cascaded = resolveNodeStyle(ownStyle, source, node.type);
472
- return cascaded === undefined ? { styleLayers: layers } : { resolvedStyle: cascaded, styleLayers: layers };
526
+ const nodeSurface = composeNodeSurfaceStyle(node.type, rawProp(props, 'surfaceStyle'));
527
+ const typedLayers = nodeSurface ? { ...layers, typedGroups: ['surfaceStyle'] } : layers;
528
+ const resolved = cascaded === undefined && nodeSurface === undefined
529
+ ? undefined
530
+ : deepMergeStyle((cascaded ?? {}), nodeSurface ?? {});
531
+ return resolved === undefined ? { styleLayers: typedLayers } : { resolvedStyle: resolved, styleLayers: typedLayers };
473
532
  }
474
533
  // Tipli komponent gruplar boşken de tipli yoldan geçer: uzantı emekli
475
534
  // prop'ları okuyabilir (text'in isBold/align/color takma adı) ve K4 sırası
@@ -1,11 +1,11 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
+ import { secretKeyPattern } from '../platform/secrets.js';
4
5
  const defaultDirectory = join(process.cwd(), 'logs');
5
6
  const activeTrace = new AsyncLocalStorage();
6
7
  const activeRequest = new AsyncLocalStorage();
7
8
  const inFlight = new Set();
8
- const secretKeyPattern = /pass|secret|token|authorization|api[-_]?key|credential|connectionstring/i;
9
9
  const detailIndent = 26;
10
10
  let level = 'off';
11
11
  let logFile;
@@ -180,6 +180,25 @@ function renderTrace(trace, outcome) {
180
180
  lines.push('');
181
181
  return `${lines.join('\n')}\n`;
182
182
  }
183
+ /**
184
+ * `runtimeErrors` kayıtlarını DÜZ METNE çevirir. Eskiden `runtimeErrors.map(String)` yazılıyordu
185
+ * ve nesne `[object Object]`e düşüyordu: 2026-09-06 koşusunda tool mesajı hangi adımın neden
186
+ * düştüğünü söylemiyordu, oysa doğru cümle aynı yanıtın içinde duruyordu (`S-4`).
187
+ *
188
+ * Yalnız İLK hata yazılır ve kalan sayılır — mesaj bir özet satırıdır, liste değil. Kullanıcı
189
+ * verisi taşıyabildiği için cümle 300 karakterde kesilir; tamamı günlüktedir.
190
+ */
191
+ export function describeRuntimeErrors(errors) {
192
+ const first = (errors[0] ?? {});
193
+ const where = [first.step?.name, first.element ? `${first.element}${first.property ? `.${first.property}` : ''}` : undefined]
194
+ .filter(Boolean)
195
+ .join(' → ');
196
+ const message = typeof first.message === 'string' && first.message.length > 0
197
+ ? first.message
198
+ : JSON.stringify(first);
199
+ const rest = errors.length > 1 ? ` (+${errors.length - 1} hata daha)` : '';
200
+ return `${where ? `${where} — ` : ''}${clip(message, 300)}${rest} · tamamı günlükte`;
201
+ }
183
202
  function describeResult(result) {
184
203
  const call = result;
185
204
  const text = Array.isArray(call?.content)
@@ -187,9 +206,14 @@ function describeResult(result) {
187
206
  : '';
188
207
  if (call?.isError)
189
208
  return { ok: false, summary: clip(text, 400) };
190
- const runtimeErrors = call?.structuredContent?.runtimeErrors;
209
+ const structured = call?.structuredContent;
210
+ const runtimeErrors = structured?.runtimeErrors;
191
211
  if (Array.isArray(runtimeErrors) && runtimeErrors.length > 0) {
192
- return { ok: false, summary: `çalışma anı hatası (platform HTTP 200 döndü): ${clip(runtimeErrors.map(String).join(' | '), 400)}` };
212
+ // Kasıtlı negatif test bir kusur değil bir KANIT: çağıran beklediğini söylediyse satır
213
+ // başarılı sayılır ve ölçüm aracının plansız hata oranını şişirmez (S-4).
214
+ return structured?.expectedFailure === true
215
+ ? { ok: true, summary: `beklenen çalışma anı hatası (negatif test): ${describeRuntimeErrors(runtimeErrors)}` }
216
+ : { ok: false, summary: `çalışma anı hatası (platform HTTP 200 döndü): ${describeRuntimeErrors(runtimeErrors)}` };
193
217
  }
194
218
  const effect = summarizeEffect(text);
195
219
  return { ok: true, summary: `${Buffer.byteLength(text, 'utf8')} B yanıt${effect ? ` · ${effect}` : ''}` };
@@ -45,8 +45,12 @@ function collectValidationMessages(payload) {
45
45
  * Sayfa şeması patch'inin üç reddi de "hiçbir op uygulanmadı" anlamına gelir (uygulayıcı önce tüm
46
46
  * op'ları bellekte işler, tek hata bile tüm komutu düşürür) — ajanın kısmi uygulama varsayıp
47
47
  * planın kalanını göndermesi bu yüzden yanlış olur.
48
+ *
49
+ * Aynı duvar 2026-09-08'den beri sayfa/layout KURMA yolunda da koşuyor (P1-2: layout hattı aynı
50
+ * hatalı prop'ları sessizce kaydediyordu). Orada op listesi yoktur ve `page_schema_validate` de
51
+ * uygulanmaz — ipucu bu yüzden yola göre ayrışır.
48
52
  */
49
- function pageSchemaHint(code) {
53
+ function pageSchemaHint(code, path) {
50
54
  if (code === 'PageSchemaVersionConflictException') {
51
55
  return ' Sayfa siz okuduktan sonra değişti ve HİÇBİR op uygulanmadı. page_schema_get ile yeniden okuyup planı tazeleyin, yeni `version` değerini `baseVersion` olarak gönderin.';
52
56
  }
@@ -54,10 +58,42 @@ function pageSchemaHint(code) {
54
58
  return ' Sayfa şeması başka bir aktörün kilidinde (kilit sahibi mesajda; kilit 60 sn sonra kendiliğinden düşer). Bekleyip tekrar deneyin — aynı anda iki yazar olduğunda kaybeden taraf sessizce ezilmesin diye kilit var.';
55
59
  }
56
60
  if (code === 'PageSchemaValidationException') {
57
- return ' Op listesi doğrulama duvarına takıldı ve HİÇBİR op uygulanmadı (hepsi ya da hiçbiri). Alan hatalarını düzeltip tek çağrıda tekrar gönderin; planı önce page_schema_validate ile kuru koşturabilirsiniz.';
61
+ return path.endsWith('/schema')
62
+ ? ' Op listesi doğrulama duvarına takıldı ve HİÇBİR op uygulanmadı (hepsi ya da hiçbiri). Alan hatalarını düzeltip tek çağrıda tekrar gönderin; planı önce page_schema_validate ile kuru koşturabilirsiniz.'
63
+ : ' Gövde doğrulama duvarına takıldı ve HİÇBİR ŞEY KAYDEDİLMEDİ. Yukarıdaki her satır bir düğüm/prop adresi ve kabul edilen değer listesi taşır — prop adlarını component_get ile doğrulayıp gövdeyi tek çağrıda düzeltin.';
58
64
  }
59
65
  return '';
60
66
  }
67
+ /**
68
+ * Toplu satır yazma tek transaction: platform hangi satırın düştüğünü mesajda söyler
69
+ * (`Row {n} could not be written: …`), ama "geri kalanı yazıldı mı" sorusunu söylemez. Ajan
70
+ * kısmi yazma varsayıp yalnız düşen satırı yeniden göndermesin diye all-or-nothing burada yazılı.
71
+ */
72
+ function bulkRowHint(code) {
73
+ if (code === 'BulkRowFailedException') {
74
+ return " Toplu yazma TEK TRANSACTION: bu çağrıdan HİÇBİR satır yazılmadı (yalnız id sekansı ilerledi). Mesajdaki satır numarası 0'dan başlar ve gönderdiğiniz `rows` dizisinin indeksidir. O satırı düzeltip TÜM diziyi yeniden gönderin.";
75
+ }
76
+ if (code === 'BulkRowLimitException') {
77
+ return ' Diziyi 1000 satırlık parçalara bölüp sırayla gönderin; her parça kendi transaction\'ında yazılır.';
78
+ }
79
+ return '';
80
+ }
81
+ /**
82
+ * PATCH uçlarında `baseVersion` zorunlu ve kayıt okumanızdan beri değiştiyse HİÇBİR op uygulanmaz.
83
+ * Ajanın "tekrar dene" refleksi burada yanlış: aynı ops'u yeni sürümle göndermek başkasının
84
+ * yazdığını ezer, önce yeniden okunmalı.
85
+ */
86
+ function versionConflictHint(code, path) {
87
+ if (code !== 'AggregateVersionConflictException')
88
+ return '';
89
+ const readTool = /\/entities(\/|$)/.test(path) ? 'entity_get'
90
+ : /\/workflows(\/|$)/.test(path) ? 'workflow_get'
91
+ : /\/flows(\/|$)/.test(path) ? 'flow_get'
92
+ : /\/layouts(\/|$)/.test(path) ? 'layout_get'
93
+ : /\/microservices(\/|$)/.test(path) ? 'microservice_get'
94
+ : 'ilgili *_get';
95
+ return ` Kayıt siz okuduktan sonra değişti ve HİÇBİR op uygulanmadı (mesajdaki beklenen/mevcut sürüm). ${readTool} ile yeniden okuyup patch'i güncel hâle göre kurun, yeni \`version\` değerini \`baseVersion\` olarak gönderin — aynı ops'u eski sürümle tekrar göndermek aradaki değişikliği ezer.`;
96
+ }
61
97
  /**
62
98
  * Bilinen platform bug'larını ajanın tool yüzeyinden teşhis edebilmesi için desen eşlemesi
63
99
  * (klinik turu dersi: teşhis DB'ye/koda inmeden yapılabilmeli — görev G-09).
@@ -65,6 +101,12 @@ function pageSchemaHint(code) {
65
101
  function diagnoseKnownFailure(status, message, code, context) {
66
102
  if (status !== 500)
67
103
  return '';
104
+ // 2026-09-08'e kadar ExceptionHandler bu istisnayı eşlemiyordu ve slug çakışması 500 dönüyordu
105
+ // (3. koşu S-1 / P3-7); yeni build 409 verir, gateway de POST'tan önce project_list ile arar
106
+ // (PRJ-001). Bu dal eski build'e karşı kalır.
107
+ if (code === 'DatabaseNameAlreadyExistsException' || message.includes('already has this database name')) {
108
+ return " Sebep: proje slug'ı bu organizasyonda zaten kullanımda (veritabanı adı zeyrek_<org>_<slug> tekildir). Çalışan backend bu durumu 409 yerine 500 ile bildiren eski bir build; başka bir slug seçin ya da project_list ile mevcut projeyi bulun (PRJ-001).";
109
+ }
68
110
  if (context.path.includes('/row') && message.includes('Sequence contains no matching element')) {
69
111
  return " Muhtemel sebep: entity bir application'a bağlı değil — entity_patch ile applicationIds ekleyin (PLT-004).";
70
112
  }
@@ -72,7 +114,7 @@ function diagnoseKnownFailure(status, message, code, context) {
72
114
  return " Bilinen sınır (K-005/P-01): m2m, çoklu dosya ya da çoklu enum alanı entity_create gövdesindeyken platform junction KAYDINI açar ama fiziksel tabloyu kurmaz. Alanı entity_patch ile yeniden ekleyin; gateway'in entity_create'i bunu artık kendisi iki aşamaya bölüyor, elle kurulmuş eski kayıtlar etkilenir.";
73
115
  }
74
116
  if (message.includes('cannot be tracked') || message.includes('already being tracked')) {
75
- return " Bilinen sınır (K-006/P-02): operation:'get' tipli microservice'te PUT, izlenen Application grafiği yüzünden 500 veriyor. Geçici yol: microservice_delete + microservice_create ile yeniden kurun (gövdeyi önce microservice_get ile alın).";
117
+ return " Bilinen sınır (K-006/P-02): operation:'get' tipli microservice'te PUT, izlenen Application grafiği yüzünden 500 veriyordu. Regresyon 2026-08-16'da düzeltildi ve 2026-09-01'de canlı doğrulandı (İK koşusu M-9) — bu hatayı hâlâ görüyorsanız çalışan sürüm düzeltmeden eskidir. O sürümde geçici yol: microservice_delete + microservice_create ile yeniden kurun (gövdeyi önce microservice_get ile alın).";
76
118
  }
77
119
  if (code === 'RowValidationFailed' || message.includes('RowValidationFailed')) {
78
120
  return ' Gövdedeki alanlar doğru görünüyorsa muhtemel sebep istemcinin body alanını düşürmesidir (K-002): microservice_execute/debug çağrısında body bir NESNE ya da nesne DİZİSİ olmalı; boş gövdeyle gönderildiyse istemci şemayı desteklemiyor olabilir.';
@@ -117,7 +159,7 @@ export function mapPlatformError(status, payload, context) {
117
159
  : '';
118
160
  const validationMessages = collectValidationMessages(body);
119
161
  const detail = validationMessages.length > 0 ? ` → ${validationMessages.join(' ; ')}` : '';
120
- return new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code)}`, status, code);
162
+ return new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${bulkRowHint(code)}${versionConflictHint(code, context.path)}`, status, code);
121
163
  }
122
164
  const fallbackValidationMessages = collectValidationMessages(body);
123
165
  const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
@@ -5,6 +5,17 @@ import { TokenProvider } from './token.js';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { request } from 'undici';
7
7
  export const requestAuthStorage = new AsyncLocalStorage();
8
+ const hiddenResponseHeaders = new Set(['set-cookie', 'authorization', 'proxy-authorization']);
9
+ /**
10
+ * Sayfalı tablo akışları `Total-Count` header'ına bağlı ama execute ailesi yalnız gövdeyi
11
+ * döndürüyordu; sözleşme hiçbir tool'dan doğrulanamıyordu (İK koşusu M-8). Gizli header'lar
12
+ * her koşulda elenir.
13
+ */
14
+ export function responseHeadersForTool(raw) {
15
+ return Object.fromEntries(Object.entries(raw)
16
+ .map(([name, value]) => [name.toLowerCase(), Array.isArray(value) ? value.join(', ') : value])
17
+ .filter((entry) => typeof entry[1] === 'string' && !hiddenResponseHeaders.has(entry[0])));
18
+ }
8
19
  function buildMultipart(parts, boundary) {
9
20
  const chunks = [];
10
21
  for (const part of parts) {
@@ -70,6 +81,18 @@ export class Platform {
70
81
  });
71
82
  return outcome.body;
72
83
  }
84
+ /**
85
+ * `tenant` gibi davranır (4xx/5xx atar) ama yanıt başlıklarını da verir. `tenantRaw` bu iş için
86
+ * kullanılamaz: o hata durumunda atmaz, yani satır yazma yolunda 400'ü başarı gibi döndürürdü.
87
+ * Satır araçları başlığa bakmak zorunda — `X-Unmapped-Members` platformun `localizations`
88
+ * alanını tanımadığını söyleyen tek işaret (sözleşme D6, gateway eski platformda companion'a düşer).
89
+ */
90
+ async tenantWithHeaders(method, project, path, options = {}) {
91
+ return this.send('tenant', method, `/${this.resolveProject(project)}${path}`, {
92
+ ...options,
93
+ environment: this.resolveEnvironment(options.environment)
94
+ });
95
+ }
73
96
  async tenantRaw(method, project, path, options = {}) {
74
97
  return this.send('tenant', method, `/${this.resolveProject(project)}${path}`, {
75
98
  ...options,
@@ -95,12 +118,16 @@ export class Platform {
95
118
  const url = new URL(`${baseUrl}${path}`);
96
119
  appendQuery(url, options.query);
97
120
  const callerHeaders = Object.fromEntries(Object.entries(options.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
121
+ const authorization = callerHeaders.authorization ?? (options.anonymous ? undefined : await this.authorizationHeader());
98
122
  const headers = {
99
123
  host: `${this.config.organization}.${this.config.hostSuffix}`,
100
124
  accept: 'application/json',
101
- ...(callerHeaders.authorization || options.anonymous ? {} : { authorization: await this.authorizationHeader() }),
125
+ ...(authorization === undefined ? {} : { authorization }),
102
126
  ...(options.environment ? { 'x-environment': options.environment } : {}),
103
- ...callerHeaders
127
+ ...callerHeaders,
128
+ ...(options.mirrorAuthorizationCookie && authorization !== undefined && callerHeaders.cookie === undefined
129
+ ? { cookie: `NG_AccessToken=${authorization.replace(/^Bearer /, '')}` }
130
+ : {})
104
131
  };
105
132
  let body;
106
133
  if (options.multipart) {
@@ -127,6 +154,7 @@ export class Platform {
127
154
  let statusCode;
128
155
  let payload;
129
156
  let responseBytes = 0;
157
+ let responseHeaders = {};
130
158
  try {
131
159
  const response = await request(url, {
132
160
  method: method,
@@ -135,6 +163,7 @@ export class Platform {
135
163
  signal: AbortSignal.timeout(this.config.requestTimeoutMs)
136
164
  });
137
165
  statusCode = response.statusCode;
166
+ responseHeaders = responseHeadersForTool(response.headers);
138
167
  if (options.expectBinary && statusCode < 400) {
139
168
  const bytes = Buffer.from(await response.body.arrayBuffer());
140
169
  responseBytes = bytes.byteLength;
@@ -164,9 +193,9 @@ export class Platform {
164
193
  return this.send(api, method, path, options, raw, true);
165
194
  }
166
195
  if (raw)
167
- return { httpStatus: statusCode, body: payload ?? null };
196
+ return { httpStatus: statusCode, body: payload ?? null, headers: responseHeaders };
168
197
  if (statusCode >= 400)
169
198
  throw mapPlatformError(statusCode, payload, context);
170
- return { httpStatus: statusCode, body: payload ?? { success: true } };
199
+ return { httpStatus: statusCode, body: payload ?? { success: true }, headers: responseHeaders };
171
200
  }
172
201
  }
@@ -0,0 +1,27 @@
1
+ export function patchResult(response, fallback) {
2
+ const envelope = response && typeof response === 'object'
3
+ ? (response.data ?? response)
4
+ : {};
5
+ const written = envelope.data && typeof envelope.data === 'object'
6
+ ? envelope.data
7
+ : fallback;
8
+ return {
9
+ // Sayı olarak gelen bir newVersion JSON.parse sırasında ZATEN yuvarlanmıştır
10
+ // (6.4e16 > MAX_SAFE_INTEGER): geri gönderilirse her patch 409 verir, boş bırakmak dürüst.
11
+ newVersion: typeof envelope.newVersion === 'string' ? envelope.newVersion : '',
12
+ applied: typeof envelope.applied === 'number' ? envelope.applied : 0,
13
+ data: written
14
+ };
15
+ }
16
+ /**
17
+ * `baseVersion` PATCH gövdesinde ZORUNLU. Okunan kayıtta `version` yoksa platform 422 döndürür;
18
+ * ajanın hatayı gateway'e değil eksik alana bağlayabilmesi için burada erken ve açık patlıyoruz.
19
+ */
20
+ export function baseVersionOf(definition, readTool) {
21
+ const version = definition.version;
22
+ if (typeof version === 'string' && version.length > 0)
23
+ return version;
24
+ if (typeof version === 'number')
25
+ return String(version);
26
+ throw new Error(`Okunan kayıtta 'version' alanı yok; PATCH 'baseVersion' olmadan gönderilemez. ${readTool} çıktısında 'version' görünmüyorsa platform sürümü bu ucu desteklemiyordur.`);
27
+ }