@appaflytech/zeyrek-ai-mcp 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/knowledge/componentCatalog.js +68 -7
- package/dist/knowledge/componentEvents.js +33 -0
- package/dist/knowledge/mobileComponentCatalog.js +5 -5
- package/dist/knowledge/normalize.js +30 -12
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/web.js +1 -1
- package/dist/knowledge/rules.js +166 -24
- package/dist/knowledge/styleResolve.js +0 -1
- package/dist/platform/capture/actions.js +24 -1
- package/dist/platform/capture/summary.js +4 -1
- package/dist/resources/examples/mobile-app-shell-drawer.json +1 -0
- package/dist/resources/examples/mobile-list-detail-form.json +3 -0
- package/dist/resources/guides/entity.md +16 -0
- package/dist/resources/guides/expressions.md +7 -6
- package/dist/resources/guides/microservice.md +1 -1
- package/dist/resources/guides/middleware.md +2 -0
- package/dist/resources/guides/mobile.md +28 -19
- package/dist/resources/guides/page.md +47 -16
- package/dist/resources/guides/translation.md +41 -1
- package/dist/resources/guides/workflow.md +31 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/tools/entities.js +41 -4
- package/dist/tools/layouts.js +22 -7
- package/dist/tools/mobile.js +15 -1
- package/dist/tools/pages.js +28 -42
- package/dist/tools/queries.js +1 -1
- package/dist/tools/rows.js +14 -3
- package/dist/tools/screenWrite.js +2 -2
- package/dist/tools/screens.js +39 -0
- package/dist/tools/translations.js +148 -6
- package/package.json +1 -1
package/dist/tools/pages.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { guid, projectParam, environmentParam, pagingParams, asUserIdParam, readOnly, writes, updates, destructive, microOps, isBranchName, branchSlug } from './common.js';
|
|
2
|
-
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, assertAddComponentIds, assertComponentBlocks, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID } from '../knowledge/normalize.js';
|
|
2
|
+
import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, assertAddComponentIds, assertComponentBlocks, validationNote, componentDefaultsNote, replacePropNote, ensurePageContainer, retargetRootAdds, rootAddsNote, flexSizeWarnings, spacingLayerWarnings, reorderSchemaBlocks, reorderNote, nestedRepeaterNodes, nestedRepeaterMessage, stringStyleNodes, stringStyleWarning, inertFlexPropNodes, inertFlexPropWarning, FLEX_LAYOUT_PROP_NAMES, resetPageContainerPosition, pageContainerPositionWarning, verticalGapWarnings, flexIndexOrder, flexIndexOrderNote, withNormalization, parseVariableEntries, PAGE_CONTAINER_ELEMENT_ID, screenEventsNeedPlatform } from '../knowledge/normalize.js';
|
|
3
3
|
import { functionSignature } from '../knowledge/functionGuards.js';
|
|
4
4
|
import { attachFindings } from '../knowledge/policy.js';
|
|
5
|
-
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath, countSchemaNodes, missingElementIds,
|
|
5
|
+
import { fetchHost, pageUpdateBody, schemaParam, eventsParam, unwrap, modeParam, summarize, findNode, flowPath, countSchemaNodes, missingElementIds, readFullSchema } from './screens.js';
|
|
6
6
|
import { fetchComponentEventNames } from './components.js';
|
|
7
7
|
import { assertComponentNames, assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog } from './screenWrite.js';
|
|
8
8
|
import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, styleTargetTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
|
|
@@ -10,7 +10,7 @@ import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/cove
|
|
|
10
10
|
import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
|
|
11
11
|
import { PlatformApiError } from '../platform/errors.js';
|
|
12
12
|
import { existingMatch, ifExistsParam, listAll, withOutcome } from '../platform/ifExists.js';
|
|
13
|
-
import { handle, toolResult, toolError
|
|
13
|
+
import { handle, toolResult, toolError } from '../platform/responses.js';
|
|
14
14
|
import { expressionErrorsNote, runtimeErrorsNote, StudioScreenshotter } from '../platform/capture/index.js';
|
|
15
15
|
import { captureTargetFor } from '../platform/capture/target.js';
|
|
16
16
|
import { diffSummaries, summaryLines, summaryStats } from '../platform/domSummary.js';
|
|
@@ -267,7 +267,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
267
267
|
project: projectParam,
|
|
268
268
|
environment: environmentParam,
|
|
269
269
|
applicationSlug: z.string().min(1).describe("Uygulamanın slug'ı (application_list / project_overview)"),
|
|
270
|
-
pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list)"),
|
|
270
|
+
pageSlug: z.string().min(1).describe("Sayfanın slug'ı (page_list); sayfa parametresi sorguyla verilebilir: 'ilac-detay?id=7' (language '&lang=' olarak eklenir)"),
|
|
271
271
|
viewport: z.object({
|
|
272
272
|
width: z.number().int().min(320).max(3840),
|
|
273
273
|
height: z.number().int().min(240).max(2160)
|
|
@@ -359,7 +359,14 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
359
359
|
throw new PlatformApiError("page_screenshot: asUserId ile cookies birlikte verilemez — uygulama oturum çerezi (ZY_App_…) geçerliyse platform başlığı değil çerezin sahibini özne yapar ve karenin kimin gözüyle çekildiği belirsiz kalır.");
|
|
360
360
|
}
|
|
361
361
|
const eye = await impersonationHeaders(platform, 'page_screenshot', project, environment, { asUserId, asUser, anonymous }, { runtime: true });
|
|
362
|
-
|
|
362
|
+
// 37 S-17 / 38 S-13: sorgulu slug (`ilac-detay?id=7`) dizeyle birleşince dil ikinci bir '?' ile
|
|
363
|
+
// ekleniyordu ve runtime `id`'yi '7?lang=tr' okudu; sayfa isteği işareti de sorguyu taşıdığı
|
|
364
|
+
// için sayfanın kendi veri isteğiyle hiç eşleşmiyordu.
|
|
365
|
+
const address = new URL(`${config.studioBaseUrl}/${lane}/${projectSlug}/${applicationSlug}/${pageSlug}`);
|
|
366
|
+
if (languageChoice)
|
|
367
|
+
address.searchParams.set('lang', languageChoice.code);
|
|
368
|
+
const url = address.toString();
|
|
369
|
+
const pagePath = pageSlug.split(/[?#]/, 1)[0];
|
|
363
370
|
// Hedef ve cihaz uygulamanın platform profilinden (G3): mobil uygulama react-native-web
|
|
364
371
|
// çizimidir, varsayılan kare telefon ön ayarıdır. Tip okunamazsa web davranışı aynen.
|
|
365
372
|
const resolved = await resolveProfileBySlug(platform, projectSlug, applicationSlug);
|
|
@@ -384,7 +391,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
384
391
|
const cookieNames = [...new Set([...(identity === 'anonymous' ? [] : ['NG_AccessToken']), ...extraCookies.map(cookie => cookie.name)])];
|
|
385
392
|
const shot = await screenshotter.capture({
|
|
386
393
|
url,
|
|
387
|
-
apiPathMarker: `/${applicationSlug}/p/${
|
|
394
|
+
apiPathMarker: `/${applicationSlug}/p/${pagePath}`,
|
|
388
395
|
token: identityToken,
|
|
389
396
|
...(eye ? { headers: eye } : {}),
|
|
390
397
|
...(extraCookies.length > 0 ? { cookies: extraCookies } : {}),
|
|
@@ -657,41 +664,17 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
657
664
|
throw new PlatformApiError(`cursor yalnız mode:'full' ile çalışır (verilen mode: '${mode ?? 'summary'}'); parçalı okuma tüm şema içindir. mode:'full' ile cursor:'0' verin ya da cursor'ı kaldırın.`, 400);
|
|
658
665
|
}
|
|
659
666
|
if (mode === 'full') {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const items = parts[decoded.partIndex];
|
|
672
|
-
if (!items)
|
|
673
|
-
throw new PlatformApiError(`cursor '${cursor}' ${decoded.partIndex}. parçayı istiyor ama şema ${parts.length} parça; son parçadan sonra nextCursor gelmez. cursor:'0' ile baştan başlayın.`, 400);
|
|
674
|
-
const nextIndex = decoded.partIndex + 1;
|
|
675
|
-
return {
|
|
676
|
-
version,
|
|
677
|
-
updatedAt,
|
|
678
|
-
...typeField,
|
|
679
|
-
nodeCount,
|
|
680
|
-
part: { index: decoded.partIndex, count: parts.length, nodes: countSchemaNodes(items.map(item => item.node)) },
|
|
681
|
-
items,
|
|
682
|
-
...(nextIndex < parts.length ? { nextCursor: encodeSchemaCursor(versionKey, nextIndex) } : {})
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
const full = { version, updatedAt, ...typeField, nodeCount, schema };
|
|
686
|
-
const chars = (JSON.stringify(prune(full)) ?? '').length;
|
|
687
|
-
if (chars > config.maxResponseBytes) {
|
|
688
|
-
if (allowTruncated !== true) {
|
|
689
|
-
const kb = (value) => (value / 1024).toFixed(1);
|
|
690
|
-
throw new PlatformApiError(`Şema ${nodeCount} düğüm / ${kb(chars)} KB; tek yanıta sığmaz (bütçe ${kb(config.maxResponseBytes)} KB). Kırpılmış şema hangi düğümlerin düştüğünü söylemez ve geri yazılırsa sayfa eksilir. cursor:'0' ile parça parça okuyun ya da allowTruncated:true verin (kırpılır, hangi düğümlerin düştüğü söylenmez). Hedef bulmak için mode:'summary' yeter.`, 400);
|
|
691
|
-
}
|
|
692
|
-
schemaReadMemory.markTruncated(readKey(project, environment, pageId, version));
|
|
693
|
-
}
|
|
694
|
-
return full;
|
|
667
|
+
return readFullSchema({
|
|
668
|
+
noun: 'Sayfa',
|
|
669
|
+
field: 'schema',
|
|
670
|
+
nodes: page.schema ?? [],
|
|
671
|
+
versionKey: String(version ?? ''),
|
|
672
|
+
base: { version, updatedAt, ...typeField },
|
|
673
|
+
cursor,
|
|
674
|
+
allowTruncated,
|
|
675
|
+
maxResponseBytes: config.maxResponseBytes,
|
|
676
|
+
onTruncated: () => schemaReadMemory.markTruncated(readKey(project, environment, pageId, version))
|
|
677
|
+
});
|
|
695
678
|
}
|
|
696
679
|
if (mode === 'subtree') {
|
|
697
680
|
if (!componentId)
|
|
@@ -1258,7 +1241,10 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
|
|
|
1258
1241
|
const overrides = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== undefined));
|
|
1259
1242
|
const named = fillComponentNames(overrides.schema);
|
|
1260
1243
|
const resolved = overrides.schema === undefined ? undefined : await resolveProfile(platform, applicationId);
|
|
1261
|
-
|
|
1244
|
+
// Kök ADR 0015: `focus` yalnız mobil sayfada geçerli — şemasız olay yazımında da uygulamanın platformu okunur.
|
|
1245
|
+
const eventPlatform = resolved?.profile.platform
|
|
1246
|
+
?? (screenEventsNeedPlatform(overrides.events) ? (await resolveProfile(platform, applicationId)).profile.platform : undefined);
|
|
1247
|
+
assertScreenWriteGuards('page', { schema: overrides.schema, events: overrides.events }, eventPlatform);
|
|
1262
1248
|
const current = await fetchHost(platform, 'page', project, environment, applicationId, pageId);
|
|
1263
1249
|
if (overrides.schema !== undefined && allowNodeLoss !== true) {
|
|
1264
1250
|
const storedSchema = current.schema;
|
package/dist/tools/queries.js
CHANGED
|
@@ -73,7 +73,7 @@ export function registerQueryTools(server, platform) {
|
|
|
73
73
|
}, handle(config, ({ project, environment, id }) => platform.tenant('GET', project, `/queries/${id}`, { environment })));
|
|
74
74
|
server.registerTool('query_create', {
|
|
75
75
|
title: 'Query oluştur',
|
|
76
|
-
description: "Yeni query tanımlar (ham SQL). definition = CreateQueryCommand gövdesi. AGREGASYON VE RAPORLAMANIN YOLU BUDUR (SUM/COUNT/AVG/GROUP BY, dönemsel özet, KPI) — view_* bir TAM METİN ARAMA indeksidir, özet üretmez (09-02 M-3). VAR OLAN SATIRLARDAN TÜRETİLEN SATIR YAZMANIN DA YOLU BUDUR: 'INSERT … SELECT' kaydedip query kaynaklı bir uçtan çalıştırmak, geçmiş/hareket/junction tablosunu TEK çağrıda doldurur ve hiçbir id elle taşınmaz (ölçüldü 2026-09-07; reçete platform_guide('entity') §'Toplu ve türetilmiş satır'). Kayıt sırasında gövde bir kez ÇALIŞTIRILIR ama geri alınır — yazan sorguyu kaydetmek satır bırakmaz
|
|
76
|
+
description: "Yeni query tanımlar (ham SQL). definition = CreateQueryCommand gövdesi. AGREGASYON VE RAPORLAMANIN YOLU BUDUR (SUM/COUNT/AVG/GROUP BY, dönemsel özet, KPI) — view_* bir TAM METİN ARAMA indeksidir, özet üretmez (09-02 M-3). VAR OLAN SATIRLARDAN TÜRETİLEN SATIR YAZMANIN DA YOLU BUDUR: 'INSERT … SELECT' kaydedip query kaynaklı bir uçtan çalıştırmak, geçmiş/hareket/junction tablosunu TEK çağrıda doldurur ve hiçbir id elle taşınmaz (ölçüldü 2026-09-07; reçete platform_guide('entity') §'Toplu ve türetilmiş satır'). Kayıt sırasında gövde bir kez ÇALIŞTIRILIR ama geri alınır — yazan sorguyu kaydetmek satır bırakmaz; 'returnScheme' yalnız sonuç kümesi DÖNMEYEN yazan sorguda boştur — 'INSERT/UPDATE/DELETE … RETURNING' ya da sonda bir SELECT taşıyan gövdede dönen kolonlarla DOLU gelir (37 S-5). json/jsonb kolonu returnScheme'de 'string' tiplenir ve değeri JSON METNİ gelir (preview'da da çalışma anında da); akışta JSON.parse ile açın. Şema tipi önizlemede değil KAYITTA sabitlenir (37 S-2, b-25 ertelendi). Kayıtlı query'nin parametreleri parameterScheme ile tanımlanır ve runtime'da bağlanır; tüketimi source:'query' microservice'idir. PARAMETRENİN DEĞERİ HER ZAMAN ÇAĞRIDAN GELİR (body → query-string → header): parametreye ifade kaynağı verilemez (parameters[].source komut şemasında yoktur, platform sessizce atar) ve middleware'in kurduğu user bu yola girmez. Kimlik/tenant kapsamı sunucu tarafında zorlanacaksa sorguyu bir workflow query adımından çağırın — reçete platform_guide('microservice') §'Kimlik kapsaması × uç kaynağı'. BOŞ STRING parametre NULL'a çevrilir: '= @p' koşulu hiçbir satırla eşleşmez ve sorgu sessizce 0 satır döner — 'filtre yok' için (@p IS NULL OR ...) yazın ya da sentinel kullanın; (@p = '' OR ...) kalıbı hiçbir zaman eşleşmez ve gateway `QRY-007` uyarısı düşürür. Yorum içindeki belirteç parametre sayılmaz. SQL çalıştırılamazsa kayıt YİNE de oluşur: returnScheme boş kalır ve yanıttaki message nedeni söyler — SQL'i düzeltip query_update ile kaydedin. SORGUNUN KENDİ AÇIKLAMASI VAR: `description` alanını doldurun — sorgu listesinde ad ve slug dışında ipucu yoktur ve ucun açıklaması (MS-014) bunun yerine geçmez, çünkü bir sorgu birden çok uçtan tüketilir; boş bırakılırsa gateway `QRY-006` uyarısı düşürür (yazmayı durdurmaz). " + constraintSummary('CreateQueryCommand'),
|
|
77
77
|
inputSchema: { project: projectParam, environment: environmentParam, definition: definitionParam, policyOverride: policyOverrideParam },
|
|
78
78
|
annotations: writes
|
|
79
79
|
}, handle(config, async ({ project, environment, definition, policyOverride }) => {
|
package/dist/tools/rows.js
CHANGED
|
@@ -53,9 +53,20 @@ const bulkBatchSize = 1000;
|
|
|
53
53
|
const maxPathBytes = 50 * 1024 * 1024;
|
|
54
54
|
const maxPathRows = 100_000;
|
|
55
55
|
const pathParam = z.string().min(1).optional()
|
|
56
|
-
.describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
|
|
56
|
+
.describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar; yanıt { written, batches, ids (platform döndürdüyse ilk 10), truncated } — tam id listesi için row_list. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
|
|
57
57
|
const relationsByNameParam = z.boolean().optional()
|
|
58
58
|
.describe("İlişki alanlarına id yerine GÖRÜNEN AD yazmayı açar: 'departman: \"Satış\"' hedef tablonun display ya da benzersiz alanında aranır ve id'ye çevrilir. Yalnız toplu yolda (rows / path) çalışır. Eşleşmeyen değer 422 döndürür ve hangi satırın hangi alanı olduğunu söyler — sessizce boş bırakmaz.");
|
|
59
|
+
/**
|
|
60
|
+
* 37 S-4: `path` "satırlar ajan bağlamından hiç geçmez" der, ama toplu uç her partide yazdığı id'leri
|
|
61
|
+
* döndürünce yanıt dosyanın satır sayısı kadar id taşıyordu (82 satır → `ids[82]`). Dosya yolunda
|
|
62
|
+
* yanıt sayıdır; id'lerden yalnız ilk birkaçı örnek olarak kalır, tamamı row_list'tedir.
|
|
63
|
+
*/
|
|
64
|
+
const fileIdSample = 10;
|
|
65
|
+
function withIdSample(outcome) {
|
|
66
|
+
if (!outcome.ids)
|
|
67
|
+
return outcome;
|
|
68
|
+
return { ...outcome, ids: outcome.ids.slice(0, fileIdSample), truncated: outcome.ids.length > fileIdSample };
|
|
69
|
+
}
|
|
59
70
|
function collectIds(payload, into) {
|
|
60
71
|
const data = payload?.data;
|
|
61
72
|
if (Array.isArray(data))
|
|
@@ -315,7 +326,7 @@ export function registerRowTools(server, platform) {
|
|
|
315
326
|
const outcome = await writeInBatches(platform, 'POST', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
|
|
316
327
|
if (fileRows)
|
|
317
328
|
notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
|
|
318
|
-
return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
|
|
329
|
+
return attachFindings(withUnmappedMembers(withNormalization(withResolved(fileRows ? withIdSample(outcome) : outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
|
|
319
330
|
}
|
|
320
331
|
const body = { row, ...(wantedLocalizations ? { localizations: wantedLocalizations } : {}), ...(rowContext ? { rowContext } : {}) };
|
|
321
332
|
const drift = unmappedMembers('CreateRowCommand', body);
|
|
@@ -384,7 +395,7 @@ export function registerRowTools(server, platform) {
|
|
|
384
395
|
const outcome = await writeInBatches(platform, 'PUT', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
|
|
385
396
|
if (fileRows)
|
|
386
397
|
notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
|
|
387
|
-
return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
|
|
398
|
+
return attachFindings(withUnmappedMembers(withNormalization(withResolved(fileRows ? withIdSample(outcome) : outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
|
|
388
399
|
}
|
|
389
400
|
const body = { row, ...(localizations ? { localizations } : {}), ...(rowContext ? { rowContext } : {}) };
|
|
390
401
|
const drift = unmappedMembers('UpdateRowCommand', body);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { invalidScreenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, assertComponentBlocks, blockSizeNote, heightTypeNote, sizeStringNote, droppableStatesNote } from '../knowledge/normalize.js';
|
|
1
|
+
import { invalidScreenEventNames, screenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, assertComponentBlocks, blockSizeNote, heightTypeNote, sizeStringNote, droppableStatesNote } from '../knowledge/normalize.js';
|
|
2
2
|
import { catalogTypeRules, schemaCatalogErrors, schemaCatalogMessage } from '../knowledge/componentCatalog.js';
|
|
3
3
|
import { FUNCTIONS_NAMESPACE } from '../knowledge/functionGuards.js';
|
|
4
4
|
import { reservedExpressionName } from '../knowledge/expressionPrelude.js';
|
|
@@ -56,7 +56,7 @@ export function invalidComponentNames(nodes) {
|
|
|
56
56
|
*/
|
|
57
57
|
export function assertScreenWriteGuards(kind, screen, platform) {
|
|
58
58
|
const codes = GUARD_CODES[kind];
|
|
59
|
-
const badEvents = invalidScreenEventNames(screen.events, codes.events);
|
|
59
|
+
const badEvents = invalidScreenEventNames(screen.events, codes.events, screenEventNames(kind, platform));
|
|
60
60
|
if (badEvents.length > 0)
|
|
61
61
|
throw new PlatformApiError(`Geçersiz ${codes.noun} olayı (istek gönderilmedi): ${badEvents.join(' · ')}`, 400);
|
|
62
62
|
const badWidgets = invalidWidgetTypeNodes(screen.schema, codes.widgets);
|
package/dist/tools/screens.js
CHANGED
|
@@ -2,6 +2,7 @@ import { PlatformApiError } from '../platform/errors.js';
|
|
|
2
2
|
import { resolveNodeStyleReport } from '../knowledge/styleResolve.js';
|
|
3
3
|
import { nodeNativeStyle } from '../knowledge/styleResolveNative.js';
|
|
4
4
|
import { jsonArray } from '../platform/jsonArray.js';
|
|
5
|
+
import { prune } from '../platform/responses.js';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
export const hostParam = z.enum(['page', 'layout', 'component'])
|
|
7
8
|
.describe("Akışın barındırıcısı: page | layout | component (widget de component'tir)");
|
|
@@ -75,6 +76,44 @@ export function splitSchemaIntoParts(schema, budgetChars) {
|
|
|
75
76
|
export function encodeSchemaCursor(version, partIndex) {
|
|
76
77
|
return `${version}:${partIndex}`;
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* ADR 0001 — `mode:'full'` okumanın sayfa ve layout ortak kapısı: sığan ağaç tam döner, sığmayan HATA
|
|
81
|
+
* verir (sessiz kırpma yok); `cursor` ile parçalar `items[]` olarak gelir, sürüm değişince 409.
|
|
82
|
+
* `allowTruncated:true` kırpmayı açıkça ister — global `shrink()` kırpar, `onTruncated` işaretler.
|
|
83
|
+
*/
|
|
84
|
+
export function readFullSchema(read) {
|
|
85
|
+
const nodeCount = countSchemaNodes(read.nodes);
|
|
86
|
+
if (read.cursor !== undefined) {
|
|
87
|
+
const decoded = decodeSchemaCursor(read.cursor);
|
|
88
|
+
if (!decoded)
|
|
89
|
+
throw new PlatformApiError(`cursor '${read.cursor}' çözülemedi; cursor yalnız '0' ya da önceki yanıttaki nextCursor olabilir. cursor:'0' ile baştan başlayın.`, 400);
|
|
90
|
+
if (decoded.version !== undefined && decoded.version !== read.versionKey) {
|
|
91
|
+
throw new PlatformApiError(`${read.noun} bu cursor alındıktan sonra değişti (cursor sürümü '${decoded.version}', güncel sürüm '${read.versionKey}'); eski parçalarla yenileri birleştirilirse şema tutarsız olur. Toplanan parçaları atın ve cursor:'0' ile baştan okuyun.`, 409);
|
|
92
|
+
}
|
|
93
|
+
const parts = splitSchemaIntoParts(read.nodes, Math.floor(read.maxResponseBytes * 0.75));
|
|
94
|
+
const items = parts[decoded.partIndex];
|
|
95
|
+
if (!items)
|
|
96
|
+
throw new PlatformApiError(`cursor '${read.cursor}' ${decoded.partIndex}. parçayı istiyor ama şema ${parts.length} parça; son parçadan sonra nextCursor gelmez. cursor:'0' ile baştan başlayın.`, 400);
|
|
97
|
+
const nextIndex = decoded.partIndex + 1;
|
|
98
|
+
return {
|
|
99
|
+
...read.base,
|
|
100
|
+
nodeCount,
|
|
101
|
+
part: { index: decoded.partIndex, count: parts.length, nodes: countSchemaNodes(items.map(item => item.node)) },
|
|
102
|
+
items,
|
|
103
|
+
...(nextIndex < parts.length ? { nextCursor: encodeSchemaCursor(read.versionKey, nextIndex) } : {})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const full = { ...read.base, nodeCount, [read.field]: read.nodes };
|
|
107
|
+
const chars = (JSON.stringify(prune(full)) ?? '').length;
|
|
108
|
+
if (chars > read.maxResponseBytes) {
|
|
109
|
+
if (read.allowTruncated !== true) {
|
|
110
|
+
const kb = (value) => (value / 1024).toFixed(1);
|
|
111
|
+
throw new PlatformApiError(`Şema ${nodeCount} düğüm / ${kb(chars)} KB; tek yanıta sığmaz (bütçe ${kb(read.maxResponseBytes)} KB). Kırpılmış şema hangi düğümlerin düştüğünü söylemez ve geri yazılırsa ${read.noun === 'Sayfa' ? 'sayfa' : 'layout'} eksilir. cursor:'0' ile parça parça okuyun ya da allowTruncated:true verin (kırpılır, hangi düğümlerin düştüğü söylenmez). Hedef bulmak için mode:'summary' yeter.`, 400);
|
|
112
|
+
}
|
|
113
|
+
read.onTruncated?.();
|
|
114
|
+
}
|
|
115
|
+
return full;
|
|
116
|
+
}
|
|
78
117
|
/** `'0'` sürümsüz ilk parçadır; `<sürüm>:<parça>` sürümü taşır. Çözülemeyen cursor `undefined`. */
|
|
79
118
|
export function decodeSchemaCursor(cursor) {
|
|
80
119
|
if (/^\d+$/.test(cursor))
|
|
@@ -1,14 +1,84 @@
|
|
|
1
|
-
import { guid, projectParam, environmentParam, readOnly, updates } from './common.js';
|
|
1
|
+
import { guid, projectParam, environmentParam, readOnly, writes, updates } from './common.js';
|
|
2
2
|
import { handle } from '../platform/responses.js';
|
|
3
|
+
import { PlatformApiError } from '../platform/errors.js';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/policy.js';
|
|
5
6
|
import { readProjectLanguages, translationLanguageFindings } from '../knowledge/languages.js';
|
|
6
7
|
import { policyRules } from '../knowledge/rules.js';
|
|
8
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { basename, extname, resolve } from 'node:path';
|
|
10
|
+
const isoLanguageCode = z.string().regex(/^[a-z]{2}$/, 'ISO 639-1 iki harfli küçük dil kodu (tr, en, de …); tr-TR gibi bölge ekli kod reddedilir');
|
|
7
11
|
const translationKeyInput = z.object({
|
|
8
12
|
key: z.string().min(1),
|
|
9
|
-
values: z.record(
|
|
13
|
+
values: z.record(isoLanguageCode, z.string()).describe("Dil kodu (ISO 639-1, iki küçük harf) → çeviri metni. Platform kodu yalnız 184 kodluk ISO sözlüğüne göre doğrular (TR-001); PROJENİN languages listesine göre doğrulamaz — o bağı gateway kurar (TR-002)"),
|
|
10
14
|
componentId: guid.optional().describe("Çeviriyi belirli bir komponente bağlar; o komponent silinince kaydı da silinir")
|
|
11
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* ADR 0018 (zeyrek-backend/docs/adr/0018-ceviri-dosya-aktarimi.md) §4: çeviri dosyası platformda
|
|
18
|
+
* saklanmaz — içe aktarma ucu dosyanın kendisini multipart alır, dışa aktarma yalnız yerel yola
|
|
19
|
+
* yazılır. Sınır backend `TranslationConstants.ImportMaxFileBytes` ile aynıdır; aşan dosya gidip
|
|
20
|
+
* 422 almak yerine burada durur.
|
|
21
|
+
*/
|
|
22
|
+
const translationFileMaxBytes = 10 * 1024 * 1024;
|
|
23
|
+
const xlsxContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
24
|
+
/**
|
|
25
|
+
* `translation_update { file }` kipi. Backend `POST /translations/{applicationId}/import`
|
|
26
|
+
* (`ImportTranslationsCommand`) dili kendisi projeye eklediği için TR-002 ön okuması yapılmaz.
|
|
27
|
+
*/
|
|
28
|
+
async function importTranslationFile(platform, { project, environment, applicationId, file, targetLanguage, dryRun }) {
|
|
29
|
+
const absolute = resolve(file);
|
|
30
|
+
if (extname(absolute).toLowerCase() !== '.xlsx') {
|
|
31
|
+
throw new PlatformApiError(`translation_update: file bir .xlsx dosyası olmalı (${absolute}). Çeviri dosyası yalnız Excel biçiminde içe aktarılır; biçimi translation_export'un ürettiği dosyadır.`);
|
|
32
|
+
}
|
|
33
|
+
let bytes;
|
|
34
|
+
try {
|
|
35
|
+
bytes = await readFile(absolute);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new PlatformApiError(`translation_update: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
|
|
39
|
+
}
|
|
40
|
+
if (bytes.byteLength > translationFileMaxBytes) {
|
|
41
|
+
throw new PlatformApiError(`translation_update: dosya ${bytes.byteLength} B, platformun 10 MB çeviri dosyası sınırının üstünde. Dosyada resim ya da ek sayfa varsa kaldırın; yalnız Key / kaynak / hedef sütunları gerekir.`);
|
|
42
|
+
}
|
|
43
|
+
const response = await platform.tenant('POST', project, `/translations/${applicationId}/import`, {
|
|
44
|
+
environment,
|
|
45
|
+
multipart: [
|
|
46
|
+
{ name: 'File', filename: basename(absolute), contentType: xlsxContentType, content: bytes },
|
|
47
|
+
{ name: 'TargetLanguage', value: targetLanguage },
|
|
48
|
+
{ name: 'DryRun', value: dryRun ? 'true' : 'false' }
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
const result = response?.data ?? {};
|
|
52
|
+
const notes = [];
|
|
53
|
+
if (result.languageAdded) {
|
|
54
|
+
notes.push(`Hedef dil '${result.targetLanguage}' projeye bütün ortamlarda eklendi; production'da çeviriler deployment'la gelir, o zamana kadar varsayılan dile düşer (TR-004).`);
|
|
55
|
+
}
|
|
56
|
+
else if (result.dryRun && result.targetLanguageInProject === false) {
|
|
57
|
+
notes.push(`Hedef dil '${result.targetLanguage}' projede yok; dryRun olmadan yapılan çağrı onu projeye bütün ortamlarda ekleyecek (TR-004).`);
|
|
58
|
+
}
|
|
59
|
+
if ((result.unknownKeyCount ?? 0) > 0) {
|
|
60
|
+
notes.push(`${result.unknownKeyCount} key application'da yok ve açılmadı (${(result.unknownKeys ?? []).join(', ')}). Yeni key için keys kipini kullanın.`);
|
|
61
|
+
}
|
|
62
|
+
if (result.applicationMismatch) {
|
|
63
|
+
notes.push('Dosya başka bir application\'dan dışa aktarılmış (_zeyrek meta); yalnız bu application\'da bulunan key\'ler yazıldı.');
|
|
64
|
+
}
|
|
65
|
+
return { ...result, ...(notes.length > 0 ? { note: notes.join(' ') } : {}) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 38 S-6: platform PUT yanıtında uygulamanın BÜTÜN sözlüğünü döner (`ArrayBaseResponse<TranslationDto>`;
|
|
69
|
+
* 3. çağrıda 226 satır, ~25 bin karakter) — tek anahtar eklemek bile tam sözlüğü ajan bağlamına
|
|
70
|
+
* sokuyordu. Yanıt yazılanın özetidir; sözlüğün kendisi translation_list'tedir. Örnek sınırı
|
|
71
|
+
* `row_*` path yanıtındaki id örneğiyle aynıdır (37 S-4).
|
|
72
|
+
*/
|
|
73
|
+
const writtenKeySample = 10;
|
|
74
|
+
function translationWriteSummary(keys, deletedKeys) {
|
|
75
|
+
return {
|
|
76
|
+
written: keys.length,
|
|
77
|
+
...(deletedKeys.length > 0 ? { deleted: deletedKeys.length } : {}),
|
|
78
|
+
keys: keys.slice(0, writtenKeySample).map(entry => entry.key),
|
|
79
|
+
truncated: keys.length > writtenKeySample
|
|
80
|
+
};
|
|
81
|
+
}
|
|
12
82
|
/**
|
|
13
83
|
* Yazma öncesi projenin dil durumunu okur. Proje okunamazsa (uç düşer, yetki yoksa) kontrol
|
|
14
84
|
* ATLANIR ve karar platforma kalır — `project_create`'in slug ön aramasındaki (PRJ-001) davranışın
|
|
@@ -30,24 +100,96 @@ export function registerTranslationTools(server, platform) {
|
|
|
30
100
|
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid },
|
|
31
101
|
annotations: readOnly
|
|
32
102
|
}, handle(config, ({ project, environment, applicationId }) => platform.tenant('GET', project, `/translations/${applicationId}`, { environment })));
|
|
103
|
+
server.registerTool('translation_export', {
|
|
104
|
+
title: 'Çevirileri Excel olarak dışa aktar',
|
|
105
|
+
description: "Application'ın bütün çeviri key'lerini çevirmene verilecek bir .xlsx dosyasına yazar: A Key · B kaynak dil (referans) · C hedef dil (hedefte değeri olan key'ler dolu gelir). Dosya YALNIZ outPath'e yazılır — Sürücü'ye yüklenmez, platformda iz kalmaz; içerik bağlama girmez, yanıt yol ve sayılardır ({ keys: application'daki key sayısı, targetMissing: hedefte boş olanlar }). Çevirmen C sütununu doldurduktan sonra dosyayı translation_update { file, targetLanguage, dryRun: true } ile önce sınayın, sonra dryRun olmadan içe aktarın; hedef dil projede yoksa içe aktarma onu projeye BÜTÜN ortamlarda ekler (TR-004). Ajan çeviriyi kendisi yapacaksa dosyaya gerek yok: translation_list → translation_update { keys }.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
project: projectParam,
|
|
108
|
+
environment: environmentParam,
|
|
109
|
+
applicationId: guid,
|
|
110
|
+
targetLanguage: isoLanguageCode.describe("Çevirmenin dolduracağı hedef dil (C sütunu); başlığa 'Target Language (ES)' biçiminde yalnız çevirmen için yazılır — içe aktarma başlığı okumaz, dili translation_update'in targetLanguage'ı belirler"),
|
|
111
|
+
sourceLanguage: isoLanguageCode.optional().describe('Çevirmene referans olacak kaynak dil (B sütunu); verilmezse projenin varsayılan dili'),
|
|
112
|
+
missingOnly: z.boolean().optional().describe("true: yalnız hedef dilde değeri olmayan ya da boş key'ler yazılır"),
|
|
113
|
+
outPath: z.string().min(1).describe("Dosyanın yazılacağı yerel yol, .xlsx ile biter (zorunlu). Yol gateway sürecinin çalıştığı makineye göre çözülür; var olan dosya ezilir")
|
|
114
|
+
},
|
|
115
|
+
annotations: writes
|
|
116
|
+
}, handle(config, async ({ project, environment, applicationId, targetLanguage, sourceLanguage, missingOnly, outPath }) => {
|
|
117
|
+
if (sourceLanguage === targetLanguage) {
|
|
118
|
+
throw new PlatformApiError(`translation_export: kaynak ve hedef dil aynı ('${targetLanguage}'). sourceLanguage'ı başka bir dil yapın ya da vermeyip projenin varsayılan diline bırakın.`);
|
|
119
|
+
}
|
|
120
|
+
const absolute = resolve(outPath);
|
|
121
|
+
if (extname(absolute).toLowerCase() !== '.xlsx') {
|
|
122
|
+
throw new PlatformApiError(`translation_export: outPath .xlsx ile bitmeli (${absolute}); çeviri dosyası yalnız Excel biçiminde üretilir.`);
|
|
123
|
+
}
|
|
124
|
+
const response = await platform.tenantWithHeaders('GET', project, `/translations/${applicationId}/export`, {
|
|
125
|
+
environment,
|
|
126
|
+
expectBinary: true,
|
|
127
|
+
query: { targetLanguage, ...(sourceLanguage ? { sourceLanguage } : {}), ...(missingOnly ? { missingOnly } : {}) }
|
|
128
|
+
});
|
|
129
|
+
const payload = response.body;
|
|
130
|
+
try {
|
|
131
|
+
await writeFile(absolute, payload.bytes);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new PlatformApiError(`translation_export: dosya yazılamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Klasör var olmalı; yol gateway sürecinin çalıştığı makineye göre çözülür.`);
|
|
135
|
+
}
|
|
136
|
+
const listed = await platform.tenant('GET', project, `/translations/${applicationId}`, { environment });
|
|
137
|
+
const translations = listed?.data ?? [];
|
|
138
|
+
const keys = new Set(translations.map(translation => translation.key));
|
|
139
|
+
const translatedKeys = new Set(translations.filter(translation => translation.language === targetLanguage && (translation.value ?? '').trim() !== '').map(translation => translation.key));
|
|
140
|
+
const projectLanguages = readProjectLanguages(await platform.org('GET', `/p/${platform.resolveProject(project)}`).catch(() => undefined));
|
|
141
|
+
const fileName = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(response.headers['content-disposition'] ?? '')?.[1];
|
|
142
|
+
const notes = projectLanguages && !projectLanguages.languages.includes(targetLanguage)
|
|
143
|
+
? [`Hedef dil '${targetLanguage}' projenin dil listesinde yok; dosya içe aktarılınca proje diline bütün ortamlarda eklenir (TR-004).`]
|
|
144
|
+
: [];
|
|
145
|
+
notes.push(`Çevirmen C sütununu doldurduktan sonra: translation_update { file: '<yol>', targetLanguage: '${targetLanguage}', dryRun: true } → sonra dryRun olmadan.`);
|
|
146
|
+
return {
|
|
147
|
+
outPath: absolute,
|
|
148
|
+
fileName: fileName ?? basename(absolute),
|
|
149
|
+
bytes: payload.bytes.byteLength,
|
|
150
|
+
keys: keys.size,
|
|
151
|
+
targetMissing: keys.size - translatedKeys.size,
|
|
152
|
+
sourceLanguage: sourceLanguage ?? projectLanguages?.defaultLanguage,
|
|
153
|
+
targetLanguage,
|
|
154
|
+
note: notes.join(' ')
|
|
155
|
+
};
|
|
156
|
+
}));
|
|
33
157
|
server.registerTool('translation_update', {
|
|
34
158
|
title: 'Çevirileri güncelle',
|
|
35
|
-
description: "Bir ya da daha çok çeviri anahtarını toplu ekler/günceller (keys) ve/veya siler (deletedKeys) — tek çağrıda ikisi de verilebilir. 'keys' üzerinden yeni bir anahtar da açılabilir (upsert; verilmeyen diller silinmez, korunur). DİKKAT: çeviri yazmak tek başına YETMEZ — runtime bir etiketi ancak projenin languages listesi ve defaultLanguage kuruluysa çözer, aksi hâlde ekrana ham anahtar basar ve yazma yine 200 döner (ölçüldü 2026-09-09, S-15). Gateway yazmadan önce projeyi okur ve bağ kopuksa TR-002 caution'ı düşer; dilleri project_update(languages, defaultLanguage) ile kurun. Backend ön kontrolü (TR-001, block): anahtarlar boş olamaz ve tekil, dil kodları ISO 639-1 listesinden, deletedKeys boş olamaz.",
|
|
159
|
+
description: "Bir ya da daha çok çeviri anahtarını toplu ekler/günceller (keys) ve/veya siler (deletedKeys) — tek çağrıda ikisi de verilebilir. 'keys' üzerinden yeni bir anahtar da açılabilir (upsert; verilmeyen diller silinmez, korunur). DİKKAT: çeviri yazmak tek başına YETMEZ — runtime bir etiketi ancak projenin languages listesi ve defaultLanguage kuruluysa çözer, aksi hâlde ekrana ham anahtar basar ve yazma yine 200 döner (ölçüldü 2026-09-09, S-15). Gateway yazmadan önce projeyi okur ve bağ kopuksa TR-002 caution'ı düşer; dilleri project_update(languages, defaultLanguage) ile kurun. Backend ön kontrolü (TR-001, block): anahtarlar boş olamaz ve tekil, dil kodları ISO 639-1 listesinden, deletedKeys boş olamaz. Yanıt özettir: { written, deleted?, keys (yazılan ilk 10 anahtar), truncated } — platformun döndürdüğü tam sözlük bağlama taşınmaz, okumak için translation_list. DOSYA KİPİ: 'file' (yerel .xlsx yolu, translation_export biçimi) verilirse keys/deletedKeys verilmez; dosya platforma multipart gider ve SAKLANMAZ. C sütunu 'targetLanguage'ta verilen dile yazılır (zorunlu; başlık metni okunmaz — C1'deki '(ES)' gibi kod dili belirlemez): dolu hücre mevcut değeri EZER, boş hücre atlanır, application'da olmayan key açılmaz (unknownKeys); hedef dil projede yoksa projeye BÜTÜN ortamlarda eklenir (TR-004). 'dryRun: true' hiçbir şey yazmadan sayar. Yanıt: { targetLanguage, created, updated, unchanged, skippedEmpty, unknownKeyCount, unknownKeys, targetLanguageInProject, languageAdded, applicationMismatch, dryRun, note? }.",
|
|
36
160
|
inputSchema: {
|
|
37
161
|
project: projectParam,
|
|
38
162
|
environment: environmentParam,
|
|
39
163
|
applicationId: guid,
|
|
40
164
|
keys: z.array(translationKeyInput).optional().describe('Eklenecek/güncellenecek anahtarlar'),
|
|
41
|
-
deletedKeys: z.array(z.string().min(1)).optional().describe('Silinecek anahtar adları')
|
|
165
|
+
deletedKeys: z.array(z.string().min(1)).optional().describe('Silinecek anahtar adları'),
|
|
166
|
+
file: z.string().min(1).optional().describe("Çevirmenin doldurduğu yerel .xlsx yolu (en çok 10 MB): A Key, B kaynak dil (okunmaz), C hedef dil; başlık satırı okunmaz, dil targetLanguage'tan gelir. Gateway dosyayı okuyup içe aktarma ucuna gönderir, Sürücü'ye yüklemez. keys/deletedKeys ile birlikte verilmez"),
|
|
167
|
+
targetLanguage: isoLanguageCode.optional().describe("Yalnız file ile ve file verilince ZORUNLU: C sütununun yazılacağı dil (ISO 639-1, 'de'). Dosyanın başlığı dili belirlemez; projede yoksa dil projeye BÜTÜN ortamlarda eklenir (TR-004)"),
|
|
168
|
+
dryRun: z.boolean().optional().describe('Yalnız file ile: yazmadan created/updated/unchanged/skippedEmpty/unknownKeys sayımını ve dilin projeye eklenip eklenmeyeceğini döner')
|
|
42
169
|
},
|
|
43
170
|
annotations: updates
|
|
44
|
-
}, handle(config, async ({ project, environment, applicationId, keys, deletedKeys }) => {
|
|
171
|
+
}, handle(config, async ({ project, environment, applicationId, keys, deletedKeys, file, targetLanguage, dryRun }) => {
|
|
172
|
+
if (file !== undefined && (keys?.length || deletedKeys?.length)) {
|
|
173
|
+
throw new PlatformApiError("translation_update: 'file' ile 'keys'/'deletedKeys' birlikte verilmez — dosyayı ayrı bir çağrıda içe aktarın, tek tek anahtarları başka bir çağrıda yazın.");
|
|
174
|
+
}
|
|
175
|
+
if (dryRun !== undefined && file === undefined) {
|
|
176
|
+
throw new PlatformApiError("translation_update: dryRun yalnız 'file' kipinde geçerlidir; keys/deletedKeys yazımının kuru koşusu yoktur.");
|
|
177
|
+
}
|
|
178
|
+
if (targetLanguage !== undefined && file === undefined) {
|
|
179
|
+
throw new PlatformApiError("translation_update: targetLanguage yalnız 'file' kipinde geçerlidir; keys yazımında dil her değerin kendi anahtarıdır (values: { de: '…' }).");
|
|
180
|
+
}
|
|
181
|
+
if (file !== undefined && targetLanguage === undefined) {
|
|
182
|
+
throw new PlatformApiError("translation_update: 'file' kipinde targetLanguage zorunludur — C sütununun hangi dile yazılacağını dosya değil istek söyler (ör. targetLanguage: 'de'). Dosyanın başlığındaki '(DE)' okunmaz.");
|
|
183
|
+
}
|
|
184
|
+
if (file !== undefined)
|
|
185
|
+
return importTranslationFile(platform, { project, environment, applicationId, file, targetLanguage: targetLanguage, dryRun: dryRun ?? false });
|
|
45
186
|
if (!keys?.length && !deletedKeys?.length) {
|
|
46
187
|
throw new Error("'keys' ya da 'deletedKeys' parametrelerinden en az biri dolu olmalı.");
|
|
47
188
|
}
|
|
48
189
|
const body = { keys: keys ?? [], deletedKeys: deletedKeys ?? [] };
|
|
49
190
|
const findings = enforcePolicy(evaluatePolicy(config, policyRules, 'translation', body, { operation: 'update' }), false);
|
|
50
191
|
const languages = await languageFindings(platform, project, keys ?? []);
|
|
51
|
-
|
|
192
|
+
await platform.tenant('PUT', project, `/translations/${applicationId}`, { environment, body });
|
|
193
|
+
return attachFindings(translationWriteSummary(body.keys, body.deletedKeys), [...findings, ...languages]);
|
|
52
194
|
}));
|
|
53
195
|
}
|
package/package.json
CHANGED