@appaflytech/zeyrek-ai-mcp 0.0.41 → 0.0.43

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 (49) hide show
  1. package/dist/knowledge/componentCatalog.js +27 -97
  2. package/dist/knowledge/componentEvents.js +1 -18
  3. package/dist/knowledge/mobileComponentCatalog.js +114 -15
  4. package/dist/knowledge/mobileComponentEvents.js +41 -0
  5. package/dist/knowledge/motionResolve.js +252 -0
  6. package/dist/knowledge/normalize.js +87 -59
  7. package/dist/knowledge/pageDiscover.js +2 -2
  8. package/dist/knowledge/platform/batchOps.js +70 -0
  9. package/dist/knowledge/platform/heroTag.js +153 -0
  10. package/dist/knowledge/platform/lint.js +128 -6
  11. package/dist/knowledge/platform/mobile.js +1 -1
  12. package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
  13. package/dist/knowledge/platform/mobileRules.js +18 -0
  14. package/dist/knowledge/platform/pageTransition.js +57 -0
  15. package/dist/knowledge/platform/web.js +1 -1
  16. package/dist/knowledge/rules.js +52 -1
  17. package/dist/knowledge/styleResolveNative.js +88 -4
  18. package/dist/platform/capture/actions.js +4 -1
  19. package/dist/platform/capture/capture.js +10 -13
  20. package/dist/platform/capture/persistent-browser.js +130 -0
  21. package/dist/resources/examples/flow-list-to-form.json +8 -10
  22. package/dist/resources/examples/flow-load-table.json +1 -1
  23. package/dist/resources/examples/mobile-list-detail-form.json +4 -3
  24. package/dist/resources/examples/mobile-scan-code.json +285 -0
  25. package/dist/resources/guides/component.md +3 -3
  26. package/dist/resources/guides/expressions.md +2 -2
  27. package/dist/resources/guides/merge.md +98 -18
  28. package/dist/resources/guides/microservice.md +2 -2
  29. package/dist/resources/guides/mobile-layout.md +2 -1
  30. package/dist/resources/guides/mobile-publish.md +1 -0
  31. package/dist/resources/guides/mobile-steps.md +67 -0
  32. package/dist/resources/guides/mobile.md +27 -55
  33. package/dist/resources/guides/page.md +182 -121
  34. package/dist/resources/guides/translation.md +26 -0
  35. package/dist/resources/schema-fingerprint.json +2 -2
  36. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
  37. package/dist/tools/activities.js +3 -1
  38. package/dist/tools/applications.js +16 -2
  39. package/dist/tools/components.js +12 -6
  40. package/dist/tools/designSchemaPatch.js +18 -13
  41. package/dist/tools/layouts.js +7 -2
  42. package/dist/tools/merges.js +29 -9
  43. package/dist/tools/pages.js +129 -127
  44. package/dist/tools/platformInfo.js +3 -2
  45. package/dist/tools/profileWrite.js +2 -2
  46. package/dist/tools/schemaOps.js +132 -34
  47. package/dist/tools/screenWrite.js +1 -1
  48. package/dist/tools/screens.js +3 -3
  49. package/package.json +2 -2
@@ -1,11 +1,11 @@
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, screenEventsNeedPlatform } from '../knowledge/normalize.js';
2
+ import { orphanedFlowWarnings, orphanedFlowPlan, orphanedFlowMessage, boundEventOps, stripGatewayEventFields, staleSetterWarnings, lostSetterTargets, fillComponentNames, componentNameNote, normalizeComponentDefaults, normalizeSchemaOps, schemaOpValidationErrors, codePropTemplateErrors, addComponentIdErrors, missingBlocksNodes, retiredTypeNodes, assertNoRetiredNodes, 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
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
- import { assertComponentNames, assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog } from './screenWrite.js';
8
- import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, styleTargetTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, resizeServerOps, planResizes, planPropTypes, rejectedOpsMessage } from './schemaOps.js';
7
+ import { assertScreenWriteGuards, screenCatalogNotes, assertScreenCatalog } from './screenWrite.js';
8
+ import { opsParam, writeViewParam, outlineWriteResponse, unknownEventOps, collectTypes, simulateLayoutOps, applySkeletonOps, reshapesSkeleton, planEventRemovals, callerSlots, planEmulatedOps, recordsWithEventRemovals, serverOpsInOrder, rejectedOpsMessage } from './schemaOps.js';
9
9
  import { formBindingWarnings, formSubmitButtonWarnings } from '../knowledge/coverage.js';
10
10
  import { emptyWriteDiff, schemaOpDrops, unwrapProps, withWriteDiff, writeDiff } from '../platform/writeDiff.js';
11
11
  import { PlatformApiError } from '../platform/errors.js';
@@ -30,6 +30,9 @@ import { boxSectionNote, collectIds, ensureBoxSections, expandSectionToggles } f
30
30
  import { designSnapshotInput } from '../knowledge/designSnapshot.js';
31
31
  import { discoverPage } from '../knowledge/pageDiscover.js';
32
32
  import { applicationTypeField, resolveProfile, resolveProfileBySlug } from '../knowledge/platform/resolve.js';
33
+ import { batchEnvelopeNote, batchIndexHint, openBatchEnvelopes } from '../knowledge/platform/batchOps.js';
34
+ import { heroTags } from '../knowledge/platform/heroTag.js';
35
+ import { profileLintReadsSavedNodes } from '../knowledge/platform/lint.js';
33
36
  import { resolveThemeNative } from '../knowledge/styleResolveNative.js';
34
37
  import { boundFlowSteps, pageLayoutWarnings, screenProfileWarnings } from './profileWrite.js';
35
38
  import { applyMobileSpacingDefaults, applyMobileSpacingToOps, emptyMobileSpacingFix, mobileSpacingNote } from '../knowledge/platform/mobileSpacing.js';
@@ -76,7 +79,8 @@ async function fetchPage(platform, project, environment, applicationId, pageId)
76
79
  * Platformun sayfa micro-op sözlüğü (`PageSchemaMicroOpApplier.KnownOps`). Gateway'in sunucuya
77
80
  * ilettiği op'lar yalnız bunlardır; `test/page-micro-op-parity.test.ts` listeyi backend ve
78
81
  * `@ng-apps/screen-builder` ile karşılaştırır. `resize_component` gateway adıdır ve sunucuya
79
- * `set_block` + `set_appearance` + `set_states` olarak çevrilir.
82
+ * `set_block` + `set_appearance` + `set_states` olarak çevrilir. `batch` sözlüktedir ama iletilmez:
83
+ * zarf araç girişinde açılır (PAGE-048).
80
84
  */
81
85
  export const SERVER_PAGE_OPS = [
82
86
  'replace_prop', 'set_binding', 'set_prop_type', 'add_component', 'remove_component', 'move_component', 'reorder', 'set_block',
@@ -171,7 +175,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
171
175
  }));
172
176
  server.registerTool('page_runtime_get', {
173
177
  title: 'Sayfanın runtime çıktısını getir',
174
- description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları (variableKeys: middleware değişkenleri, pageVariableKeys/appVariableKeys: state.page/state.app anahtarları) + kullanıcı fonksiyonu imzaları (appFunctions/pageFunctions — bir ifadeye Functions.<ad>(…) yazmadan önce adı ve parametre sırasını buradan alın; gövde taşınmaz, function_get ile okunur). Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır. Kompleks projede yanıta 'accessDecision' eklenir (allow | deny | not-enforced | unknown, mode, izin, reason, next): sayfa reddi page.view kararından mı geldi, sıradaki adım ne.",
178
+ description: "Çıkan uygulamanın sayfa için aldığı GERÇEK paketi döner (GET /{project}/{application}/p/{slug} — sayfanın ham serve yolu budur; Studio preview ve runtime aynı ucu tüketir): sayfa şeması + layout views'ü + sayfa olayları + flow'lar + değişken anahtarları (variableKeys: middleware değişkenleri, pageVariableKeys/appVariableKeys: state.page/state.app anahtarları) + kullanıcı fonksiyonu imzaları (appFunctions/pageFunctions — bir ifadeye Functions.<ad>(…) yazmadan önce adı ve parametre sırasını buradan alın; gövde taşınmaz, function_get ile okunur). Kurduğun sayfanın SON hâlini doğrulamanın yolu budur (layout-slot yerleşimi dahil — Kural 1f). DİKKAT: sayfaya bağlı middleware'ler bu çağrıda GERÇEKTEN koşar; middleware reddederse yanıt middlewareBlocked ile döner — korumalı sayfada bu beklenen davranıştır ve kendisi bir doğrulamadır. Kompleks projede yanıta 'accessDecision' eklenir (allow | deny | not-enforced | unknown, mode, izin, reason, next): sayfa reddi page.view kararından mı geldi, sıradaki adım ne. Mobil sayfada heroTag taşıyan düğümler `heroTags`'te listelenir (layout'unkiler dahil; statik etiket metniyle, ifadeye bağlı olan `bound: true` ile): paylaşılan öğe çiftini kurarken iki sayfanın özetini buradan karşılaştırın (platform_guide('page') §Kural 1g-3).",
175
179
  inputSchema: {
176
180
  project: projectParam,
177
181
  environment: environmentParam,
@@ -224,6 +228,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
224
228
  const nativeTheme = resolveStyles && runtimeApplication.type === 'mobile' ? resolveThemeNative((theme ?? undefined)) : undefined;
225
229
  // Mobilde de tema katmanı (componentStyles ← $preset) çizerin tabanıdır (f-16); rapor aynı kaynağı okur.
226
230
  const nodeStyleSource = styleSource;
231
+ // Paylaşılan öğe etiketleri (frontend ADR 0022, MOB-030): iki sayfanın özeti karşılaştırılarak çift kurulur.
232
+ // Layout'un düğümleri de cihazda çizilir (başlıktaki logo); listede önce onlar gelir.
233
+ const pageHeroTags = runtimeApplication.type === 'mobile' ? [...heroTags(layout?.views), ...heroTags(body.schema)] : [];
227
234
  return {
228
235
  ...(decision ? { accessDecision: decision } : {}),
229
236
  page: { id: body.id, name: body.name, slug: body.slug, browserTitle: body.browserTitle, browserTitleKey: body.browserTitleKey, isHomepage: body.isHomepage },
@@ -231,6 +238,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
231
238
  events: body.events ?? [],
232
239
  layout: layout ? { id: layout.id, name: layout.name, views: asNodes(layout.views).map(node => summarize(node, nodeStyleSource, nativeTheme)) } : null,
233
240
  schema: asNodes(body.schema).map(node => summarize(node, nodeStyleSource, nativeTheme)),
241
+ ...(pageHeroTags.length > 0 ? { heroTags: pageHeroTags } : {}),
234
242
  ...(nativeTheme ? { styleResolver: 'native' } : {}),
235
243
  widgets: named(body.widgets),
236
244
  flows: named(body.flows),
@@ -251,7 +259,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
251
259
  }
252
260
  : {}),
253
261
  note: nativeTheme
254
- ? "Mobil uygulama: nativeStyle, mobil çizerin düğüme verdiği React Native stilidir — shell kutunun, text içine akan metnin stili (RN'de tipografi miras alınmaz); warnings mobilde çizilmeyen değerlerdir (MOB-002). Tema componentStyles ← $preset katmanı TABANDIR (panel grupları üstüne biner, serbest style son söz); mobil katalogda style prop'u olmadığı için $preset'e yeni başvuru yazılamaz, componentStyles tipin bütün düğümlerine iner."
262
+ ? "Mobil uygulama: nativeStyle, mobil çizerin düğüme verdiği React Native stilidir — shell kutunun, text içine akan metnin stili (RN'de tipografi miras alınmaz); warnings mobilde çizilmeyen değerlerdir (MOB-002; hareket alanları MOB-028). bound (yalnız varsa) değeri çalışma anında oluşan, ifadeye bağlı stil alanlarıdır (<grup>.<alan>) — stile girmezler. motion (yalnız hareketli düğümde) motionStyle'ın /dev ve cihazdaki çözümüdür; kanvas ve page_screenshot'ın donuk karesi uygulamaz. Tema componentStyles ← $preset katmanı TABANDIR (panel grupları üstüne biner, serbest style son söz); mobil katalogda style prop'u olmadığı için $preset'e yeni başvuru yazılamaz, componentStyles tipin bütün düğümlerine iner."
255
263
  : resolveStyles
256
264
  ? "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f). resolvedStyle = componentStyles → preset → düğüm stili kaskadı (aibox'ta tipli grupların derlemesi TABAN katmandır); styleLayers hangi katmanların konuştuğunu söyler — styleLayers.componentPreset ise CSS DEĞİL, düğümün hangi bileşen preseti versiyonundan kurulduğunun işaretidir."
257
265
  : "schema, layout'taki slot'un İÇİNE yerleşerek çizilir (Kural 1f); tam gövde gerekirse view:'full'. Bir düğümün ekranda GERÇEKTEN aldığı stili görmek için resolveStyles:true."
@@ -692,98 +700,20 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
692
700
  inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid },
693
701
  annotations: readOnly
694
702
  }, handle(config, ({ project, environment, applicationId, pageId }) => platform.tenant('GET', project, `/pages/${applicationId}/${pageId}/ai-context`, { environment })));
695
- server.registerTool('page_schema_validate', {
696
- title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
697
- description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. Gövdeye page_schema_patch ile AYNI normalizasyonu uygular (Prop sarmalı, validation biçimi) — kuru koşu ile yazma aynı gövdeyi görür.",
698
- inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için") },
699
- annotations: readOnly
700
- }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations }) => {
701
- const allOps = microOps('page_schema_validate', ops, operations).map(op => op);
702
- const propTypeIssues = setPropTypeErrors(allOps.filter(entry => entry.op === 'set_prop_type'));
703
- if (propTypeIssues.length > 0)
704
- throw new PlatformApiError(`set_prop_type gövdesi eksik (istek gönderilmedi): ${propTypeIssues.join(' · ')}`, 400);
705
- // Kuru koşu ile yazma AYNI gövdeyi görür (P0-2 Step 3): resize_component burada da sunucu
706
- // op'larına çevrilir, platform dokunulan düğümleri aynı kapsamla doğrular.
707
- const opRecords = allOps.filter(entry => entry.op !== 'resize_component');
708
- const resolved = await resolveProfile(platform, applicationId);
709
- // ARC-143: kuru koşu page_schema_patch'in add_component kapısını (PAGE-009 · 011 · 012 · 013)
710
- // atlıyordu; ayrılmış adlı komponent validate'ten geçip yamada reddediliyordu.
711
- assertScreenWriteGuards('page', { schema: opRecords.filter(entry => entry.op === 'add_component').map(entry => entry.component) }, resolved.profile.platform);
712
- const readPage = () => fetchPage(platform, project, environment, applicationId, pageId);
713
- const normalized = normalizeSchemaOps(opRecords, await styleTargetTypes(opRecords, readPage));
714
- const added = opRecords.filter(entry => entry.op === 'add_component').map(entry => entry.component).filter(Boolean);
715
- // page_schema_patch'in sırası: kimlik ve blocks op başına (PAGE-037), sonra ad (PAGE-009).
716
- assertAddComponentIds(opRecords);
717
- assertComponentBlocks(added);
718
- assertComponentNames('page', added);
719
- const validationIssues = [
720
- ...schemaOpValidationErrors(opRecords).map(issue => `PAGE-016 — ${issue}`),
721
- ...codePropTemplateErrors(opRecords).map(issue => `PAGE-032 — ${issue}`)
722
- ];
723
- const spacing = await mobileSpacingForOps(resolved.profile.platform, opRecords, () => fetchPage(platform, project, environment, applicationId, pageId));
724
- const ordering = resolved.profile.layout.orderBy === 'block-position'
725
- ? await orderByIndex(opRecords, () => fetchPage(platform, project, environment, applicationId, pageId))
726
- : { ops: opRecords, moved: [], warnings: [] };
727
- const writesStyle = resolved.profile.platform !== 'web' && opRecords.some(entry => (entry.op === 'replace_prop' || entry.op === 'set_binding') && entry.prop === 'style');
728
- const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, {
729
- kind: 'page',
730
- ops: opRecords,
731
- savedNodes: writesStyle ? await fetchPage(platform, project, environment, applicationId, pageId).then(page => page.schema, () => undefined) : undefined
732
- });
733
- // page_schema_patch'in Connect Data duvarı kuru koşuda da koşar: patch'in reddedeceği op burada görünür.
734
- const connectedProps = opRecords.some(entry => ['set_binding', 'set_prop_type', 'replace_prop', 'set_connect_data', 'remove_connect_data'].includes(String(entry.op)))
735
- ? await fetchPage(platform, project, environment, applicationId, pageId).then(page => connectedPropKeys(page.schema), () => null)
736
- : null;
737
- const connectDataRejections = opRecords.flatMap((entry, index) => connectDataOpIssues(entry, connectedProps).map(issue => ({
738
- index,
739
- op: String(entry.op ?? ''),
740
- target: typeof entry.target === 'string' ? entry.target : null,
741
- code: issue.code,
742
- message: issue.message
743
- })));
744
- // Yazmadaki gibi katalog kuralları GİDEN listeye: kuru koşu kırpılmamış boyutu doğrulayıp
745
- // yazmanın geçireceği gövdeye hata veriyor, notu da vermiyordu (Y-13 C).
746
- const catalogNotes = await screenCatalogNotes(platform, project, environment, applicationId, ordering.ops.filter(entry => entry.op === 'add_component').map(entry => entry.component));
747
- const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
748
- environment,
749
- body: { ops: [...allOps.filter(entry => entry.op === 'resize_component').flatMap(resizeServerOps), ...ordering.ops] }
750
- })) ?? {});
751
- const skeletonWarnings = await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, () => fetchPage(platform, project, environment, applicationId, pageId), opRecords, 'audit');
752
- if (validationIssues.length > 0)
753
- response.validationWarnings = validationIssues;
754
- if (ordering.warnings.length > 0 || profileWarnings.length > 0 || skeletonWarnings.length > 0)
755
- response.layoutWarnings = [...ordering.warnings, ...profileWarnings, ...skeletonWarnings];
756
- if (connectDataRejections.length > 0)
757
- response.rejectedOps = connectDataRejections;
758
- Object.assign(response, applicationTypeField(resolved));
759
- return withNormalization(response, [
760
- ...componentNameNote(normalized.names),
761
- ...componentDefaultsNote(normalized.components),
762
- ...validationNote(normalized.validations),
763
- ...catalogNotes,
764
- ...replacePropNote(normalized.props),
765
- ...flexIndexOrderNote(ordering.moved),
766
- ...mobileSpacingNote(spacing)
767
- ]);
768
- }));
769
- server.registerTool('page_schema_patch', {
770
- title: 'Sayfa şemasına patch uygula',
771
- description: "Micro-op'ları doğrulama duvarından geçirip TEK PATCH ile kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. İstek atomiktir: bir op reddedilirse hiçbiri yazılmaz. baseVersion (page_schema_get çıktısındaki version ya da önceki yamanın newVersion'ı) verilirse sayfa o sürümden beri değişmişse 409 döner ve hiçbir op uygulanmaz (stale plan koruması); verilmezse gateway yalnız bu çağrıda zaten okuduğu sayfanın sürümünü gönderir. Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler. Gateway ön kontrolleri (PAGE-006/008/016/032, set_prop_type gövdesi, move_component size) op başına toplanır: düşen HER op tek hata yanıtında rejectedOps[] ({ index, op, target, code, message }) ile listelenir ve hiçbir op gönderilmez — hepsini düzeltip listeyi bir kez yeniden gönderin.",
772
- inputSchema: {
773
- project: projectParam,
774
- environment: environmentParam,
775
- applicationId: guid,
776
- pageId: guid,
777
- ops: opsParam.optional(),
778
- operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
779
- baseVersion: z.string().optional().describe("page_schema_get çıktısındaki version ya da önceki page_schema_patch yanıtındaki newVersion; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir op uygulanmaz"),
780
- expectedUpdatedAt: z.string().optional().describe("Eski koruma: page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz. baseVersion tercih edilir"),
781
- allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
782
- view: writeViewParam
783
- },
784
- annotations: updates
785
- }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, baseVersion, expectedUpdatedAt, allowFlowDeletion, view }) => {
786
- const allOps = microOps('page_schema_patch', ops, operations).map(op => op);
703
+ /**
704
+ * Sayfa şemasına yazımın PLANI — `page_schema_patch` ve `page_schema_validate` aynısını koşar: ön kontroller, onarım,
705
+ * öykünülen op'ların çevrilmesi, platforma gidecek liste, uyarılar. Kuru koşu yazımla AYNI kararı verir (yazımın
706
+ * reddedeceği yamayı aynı mesajla reddeder) ve platforma AYNI gövdeyi doğrulatır; fark yalnız gönderilen istekte.
707
+ *
708
+ * İki işleyici ayrı yazılmıştı ve ayrışmıştı (TOOL-059, ölçüldü 2026-10-10, gerçek uç): kuru koşu
709
+ * `remove_component page_container`'a `valid: true` diyor, yazım aynı op'u PAGE-006 ile reddediyordu; kuru koşu
710
+ * PAGE-034 · PAGE-008 · MOVE-SIZE kapılarını, akış silme planını (G-21), yama sonrası iç içe tekrarlayıcıyı (PAGE-013),
711
+ * kök eklemenin taşınmasını ve `resize_component` planını da koşmuyordu. Emsal: ARC-143, `page-meta.test.ts`
712
+ * "validate = yazma". Regresyon: `test/page-dry-run-parity.test.ts`.
713
+ */
714
+ const planPageWrite = async (tool, { project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion }) => {
715
+ const opened = openBatchEnvelopes(tool, microOps(tool, ops, operations).map(op => op));
716
+ const allOps = opened.ops;
787
717
  const removeEventOps = allOps.filter(entry => entry.op === 'remove_event');
788
718
  const resizeOps = allOps.filter(entry => entry.op === 'resize_component');
789
719
  const propTypeOps = allOps.filter(entry => entry.op === 'set_prop_type');
@@ -826,14 +756,16 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
826
756
  const toggles = expandSectionToggles(opRecords, schema, taken);
827
757
  sectionNotes = boxSectionNote([...created, ...toggles.created], toggles.removed);
828
758
  }
829
- const normalized = normalizeSchemaOps(opRecords, await styleTargetTypes(opRecords, readPage));
759
+ const normalized = normalizeSchemaOps(opRecords);
830
760
  for (const entry of opRecords)
831
761
  reject('PAGE-016', entry, schemaOpValidationErrors([entry]));
832
762
  for (const entry of opRecords)
833
763
  reject('PAGE-032', entry, codePropTemplateErrors([entry]));
834
764
  for (const entry of opRecords)
835
765
  reject('PAGE-037', entry, [...addComponentIdErrors([entry]), ...(entry.op === 'add_component' ? missingBlocksNodes([entry.component]) : [])]);
836
- const touchesConnection = opRecords.some(entry => entry.op === 'set_binding' || entry.op === 'set_prop_type' || entry.op === 'replace_prop' || entry.op === 'remove_connect_data');
766
+ for (const entry of opRecords)
767
+ reject('PAGE-051', entry, entry.op === 'add_component' ? retiredTypeNodes([entry.component]) : []);
768
+ const touchesConnection = propTypeOps.length > 0 || opRecords.some(entry => entry.op === 'set_binding' || entry.op === 'replace_prop' || entry.op === 'remove_connect_data');
837
769
  let connectedProps = null;
838
770
  if (touchesConnection) {
839
771
  try {
@@ -843,7 +775,8 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
843
775
  connectedProps = null;
844
776
  }
845
777
  }
846
- for (const entry of opRecords) {
778
+ // `set_prop_type` ayrı listededir; bağlı prop'u ezen yazımı (CD-005) o da yapar.
779
+ for (const entry of [...opRecords, ...propTypeOps]) {
847
780
  for (const issue of connectDataOpIssues(entry, connectedProps))
848
781
  reject(issue.code, entry, [issue.message]);
849
782
  }
@@ -864,7 +797,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
864
797
  reject('PAGE-008', entry, unknownEventOps([entry], typesByElementId, eventsBySlug));
865
798
  }
866
799
  if (rejected.length > 0)
867
- throw new PlatformApiError(rejectedOpsMessage(rejected), 400);
800
+ throw new PlatformApiError(rejectedOpsMessage(rejected, batchIndexHint(opened)), 400);
868
801
  stripGatewayEventFields(opRecords);
869
802
  const wantsRootAdd = opRecords.some(entry => entry.op === 'add_component' && (entry.parent === undefined || entry.parent === null || entry.parent === 'root'));
870
803
  let retargeted = [];
@@ -882,16 +815,17 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
882
815
  ? await orderByIndex(opRecords, readPage)
883
816
  : { ops: opRecords, moved: [], warnings: [] };
884
817
  const sentOps = ordering.ops;
818
+ // Bu noktadan sonra `opRecords` yeni op almaz (bölüm anahtarı yukarıda eklendi): çağıranın sırası sabittir.
819
+ const slots = callerSlots(allOps, opRecords);
885
820
  // Akış silen op'lar yamadan ÖNCE hesaplanır: uyarı sonradan geldiğinde akış zaten kalıcı
886
821
  // silinmiş oluyordu ve "önce yedekle" tavsiyesi uygulanamıyordu (09-05 koşusu, G-21).
887
822
  let deletedFlows = [];
888
823
  // remove_event opRecords'ta değil, ayrı listede durur; koşul yalnız opRecords'a baktığı sürece
889
824
  // yalnız olay kaldıran bir yama bu korumayı hiç tetiklemiyordu (2026-09-14).
890
- // `batch` içindeki silme de planı gerektirir; açma `orphanedFlowPlan`'da.
891
- if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event', 'batch'].includes(String(entry.op)))) {
825
+ if ([...opRecords, ...removeEventOps].some(entry => ['remove_component', 'set_event', 'remove_event'].includes(String(entry.op)))) {
892
826
  try {
893
827
  const page = await readPage();
894
- deletedFlows = orphanedFlowPlan(page.schema, page.events, [...opRecords, ...removeEventOps]);
828
+ deletedFlows = orphanedFlowPlan(page.schema, page.events, recordsWithEventRemovals(slots, opRecords));
895
829
  }
896
830
  catch {
897
831
  deletedFlows = [];
@@ -942,7 +876,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
942
876
  const touched = simulateLayoutOps(simulated, sentOps);
943
877
  // PAGE-013 op'lar UYGULANDIKTAN SONRAKİ ağaca bakar: tek başına eklenen düğüm masum
944
878
  // görünür, iç içeliği ancak hedef parent zinciri belli olunca anlarsın.
945
- nestedAfterOps = nestedRepeaterNodes(simulated);
879
+ nestedAfterOps = nestedRepeaterNodes(simulated, resolved.profile.platform);
946
880
  layoutWarnings = [
947
881
  ...(blocksMeaningful ? pageContainerPositionWarning(page.schema) : []),
948
882
  ...(blocksMeaningful ? verticalGapWarnings(simulated, 'PAGE-010 (uyarı)', touched) : []),
@@ -959,25 +893,100 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
959
893
  if (nestedAfterOps.length > 0)
960
894
  throw new PlatformApiError(nestedRepeaterMessage(nestedAfterOps, resolved.profile.platform), 400);
961
895
  const removedEvents = planEventRemovals(removeEventOps);
962
- const resized = resizeOps.length > 0 ? planResizes((await readPage()).schema ?? [], resizeOps) : { serverOps: [], applied: [], warnings: [] };
963
- const propTypes = propTypeOps.length > 0 ? planPropTypes((await readPage()).schema ?? [], propTypeOps) : { serverOps: [], applied: [] };
964
- const serverOps = [...removeEventOps, ...resized.serverOps, ...propTypes.serverOps, ...sentOps];
965
- const writesStyle = resolved.profile.platform !== 'web' && opRecords.some(entry => (entry.op === 'replace_prop' || entry.op === 'set_binding') && entry.prop === 'style');
896
+ // Öykünülen op'lar ağacın o andaki hâline göre planlanır ve çağıranın yazdığı yerde gider (PAGE-050).
897
+ const { resized, propTypes } = planEmulatedOps(resizeOps.length > 0 || propTypeOps.length > 0 ? (await readPage()).schema ?? [] : [], slots, opRecords);
898
+ const serverOps = serverOpsInOrder(slots, { sent: sentOps, resized: resized.groups, propTypes: propTypes.serverOps });
966
899
  const profileWarnings = await screenProfileWarnings(platform, resolved, project, environment, applicationId, {
967
900
  kind: 'page',
968
901
  ops: [...opRecords, ...propTypeOps],
969
- savedNodes: writesStyle ? await readPage().then(page => page.schema, () => undefined) : undefined,
902
+ // Kayıtlı ağaç: `style` yazımında hedefin tipi, `heroTag` yazımında sayfanın öteki etiketleri (MOB-030) için.
903
+ savedNodes: resolved.profile.platform !== 'web' && profileLintReadsSavedNodes(opRecords) ? await readPage().then(page => page.schema, () => undefined) : undefined,
970
904
  steps: await boundFlowSteps(platform, resolved, project, environment, 'page', applicationId, pageId, opRecords)
971
905
  });
972
- const skeletonWarnings = reshapesSkeleton(opRecords)
973
- ? await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, readPage, sentOps, 'new')
974
- : [];
906
+ // Kuru koşu hiçbir şey yazmaz: iskelet bulgularının tamamını döner; yazım yalnız bu yamayla YENİ geleni.
907
+ const skeletonWarnings = tool === 'page_schema_validate'
908
+ ? await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, readPage, sentOps, 'audit')
909
+ : reshapesSkeleton(opRecords)
910
+ ? await simulatedPageLayoutWarnings(platform, resolved, project, environment, applicationId, readPage, sentOps, 'new')
911
+ : [];
975
912
  layoutWarnings = [...ordering.warnings, ...layoutWarnings, ...profileWarnings, ...skeletonWarnings];
913
+ return {
914
+ opRecords,
915
+ sentOps,
916
+ serverOps,
917
+ resolved,
918
+ removedEvents,
919
+ resized,
920
+ propTypes,
921
+ deletedFlows,
922
+ formWarnings,
923
+ flowWarnings,
924
+ flowsBeforePatch,
925
+ layoutWarnings,
926
+ readVersion: () => (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined),
927
+ notes: [
928
+ ...batchEnvelopeNote(opened),
929
+ ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
930
+ ...componentNameNote(normalized.names),
931
+ ...sectionNotes,
932
+ ...componentDefaultsNote(normalized.components),
933
+ ...validationNote(normalized.validations),
934
+ ...catalogNotes,
935
+ ...replacePropNote(normalized.props),
936
+ ...flexIndexOrderNote(ordering.moved),
937
+ ...mobileSpacingNote(spacing)
938
+ ]
939
+ };
940
+ };
941
+ server.registerTool('page_schema_validate', {
942
+ title: 'Sayfa şemasını doğrula (kuru çalıştırma)',
943
+ description: "Micro-op'ları kaydetmeden şemaya uygular ve doğrulama hatalarını (suggestions ile) döndürür; page_schema_patch öncesi planı test etmek için. page_schema_patch'in PLANINI koşar (PAGE-049): aynı ön kontroller — yazımın reddedeceği yamayı aynı mesajla, istek göndermeden reddeder — aynı onarım ve platforma aynı gövde; yazımın döneceği uyarılar (layoutWarnings, formWarnings, flowWarnings) burada da görünür.",
944
+ inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, pageId: guid, ops: opsParam.optional(), operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"), allowFlowDeletion: z.boolean().optional().describe('page_schema_patch\'e vereceğiniz değer: true ise akışı referanssız bırakan yama kuru koşuda da reddedilmez. Varsayılan false — yazım onu reddeder, kuru koşu da (G-21).') },
945
+ annotations: readOnly
946
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, allowFlowDeletion }) => {
947
+ const plan = await planPageWrite('page_schema_validate', { project, environment, applicationId, pageId, ops, operations, allowFlowDeletion });
948
+ const response = ((await platform.tenant('POST', project, `/pages/${applicationId}/${pageId}/schema/validate`, {
949
+ environment,
950
+ body: { ops: plan.serverOps }
951
+ })) ?? {});
952
+ // Yazımın yanıtında dönecek uyarılar kuru koşuda da görünür: plan aynı, yalnız istek farklı.
953
+ if (plan.deletedFlows.length > 0)
954
+ response.deletedFlows = plan.deletedFlows;
955
+ if (plan.resized.warnings.length > 0)
956
+ response.resizeWarnings = plan.resized.warnings;
957
+ if (plan.formWarnings.length > 0)
958
+ response.formWarnings = plan.formWarnings;
959
+ if (plan.flowWarnings.length > 0)
960
+ response.flowWarnings = plan.flowWarnings;
961
+ if (plan.layoutWarnings.length > 0)
962
+ response.layoutWarnings = plan.layoutWarnings;
963
+ Object.assign(response, applicationTypeField(plan.resolved));
964
+ return withNormalization(response, plan.notes);
965
+ }));
966
+ server.registerTool('page_schema_patch', {
967
+ title: 'Sayfa şemasına patch uygula',
968
+ description: "Micro-op'ları doğrulama duvarından geçirip TEK PATCH ile kaydeder; açık studio canvas'ları SignalR ile canlı güncellenir. İstek atomiktir: bir op reddedilirse hiçbiri yazılmaz. baseVersion (page_schema_get çıktısındaki version ya da önceki yamanın newVersion'ı) verilirse sayfa o sürümden beri değişmişse 409 döner ve hiçbir op uygulanmaz (stale plan koruması); verilmezse gateway yalnız bu çağrıda zaten okuduğu sayfanın sürümünü gönderir. Yanıttaki appliedPatch, her op'un şemada tam olarak hangi JSON Pointer'a dokunduğunu söyler; gövdesi VARSAYILAN OLARAK iskelettir (eklenen düğüm id/type/children'a iner) — tam gövde için view:'full'. Op'un gerçekten kaydedilip kaydedilmediğini gövde değil silentlyDropped söyler. Gateway ön kontrolleri (PAGE-006/008/016/032/037/051, set_prop_type gövdesi, move_component size) op başına toplanır: düşen HER op tek hata yanıtında rejectedOps[] ({ index, op, target, code, message }) ile listelenir ve hiçbir op gönderilmez — hepsini düzeltip listeyi bir kez yeniden gönderin.",
969
+ inputSchema: {
970
+ project: projectParam,
971
+ environment: environmentParam,
972
+ applicationId: guid,
973
+ pageId: guid,
974
+ ops: opsParam.optional(),
975
+ operations: opsParam.optional().describe("'ops' ile aynı alan (eşanlamlı) — entity/microservice/workflow patch'leriyle aynı adı kullanabilmeniz için"),
976
+ baseVersion: z.string().optional().describe("page_schema_get çıktısındaki version ya da önceki page_schema_patch yanıtındaki newVersion; sayfa o sürümden beri değiştiyse 409 döner ve hiçbir op uygulanmaz"),
977
+ expectedUpdatedAt: z.string().optional().describe("Eski koruma: page_schema_get çıktısındaki updatedAt; eşleşmezse patch uygulanmaz. baseVersion tercih edilir"),
978
+ allowFlowDeletion: z.boolean().optional().describe("true: bu yama bir akışı referanssız bırakacaksa yine de uygulanır. Varsayılan false — platform referanssız akışı KALICI siler ve gateway isteği önden reddeder (G-21). Silinen akışlar yanıtta deletedFlows ile listelenir."),
979
+ view: writeViewParam
980
+ },
981
+ annotations: updates
982
+ }, handle(config, async ({ project, environment, applicationId, pageId, ops, operations, baseVersion, expectedUpdatedAt, allowFlowDeletion, view }) => {
983
+ const plan = await planPageWrite('page_schema_patch', { project, environment, applicationId, pageId, ops, operations, expectedUpdatedAt, allowFlowDeletion });
984
+ const { opRecords, sentOps, serverOps, resolved, removedEvents, resized, propTypes, deletedFlows, formWarnings, flowWarnings, flowsBeforePatch, layoutWarnings } = plan;
976
985
  // Tek istek, tek sürüm: öykünülen op'lar 2026-09-14'e kadar ayrı GET + tam PUT çiftleriydi ve
977
986
  // yamadan ÖNCE yazılıyordu — PATCH reddedilse bile kalıcıydılar. Artık hepsi aynı atomik PATCH'te.
978
987
  // baseVersion yalnız biliniyorsa gider (ajanın verdiği ya da bu çağrıda zaten okunan sayfanınki):
979
988
  // sürümü öğrenmek için ayrı GET atılmaz, platform satırı her yamada kilitler.
980
- const sentVersion = baseVersion ?? (typeof cachedPage?.version === 'string' ? cachedPage.version : undefined);
989
+ const sentVersion = baseVersion ?? plan.readVersion();
981
990
  const response = ((await platform.tenant('PATCH', project, `/pages/${applicationId}/${pageId}/schema`, {
982
991
  environment,
983
992
  body: { ops: serverOps, baseVersion: sentVersion }
@@ -1049,17 +1058,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1049
1058
  }
1050
1059
  }
1051
1060
  Object.assign(response, applicationTypeField(resolved));
1052
- return withWriteDiff(withNormalization(response, [
1053
- ...rootAddsNote(retargeted, PAGE_CONTAINER_ELEMENT_ID),
1054
- ...componentNameNote(normalized.names),
1055
- ...sectionNotes,
1056
- ...componentDefaultsNote(normalized.components),
1057
- ...validationNote(normalized.validations),
1058
- ...catalogNotes,
1059
- ...replacePropNote(normalized.props),
1060
- ...flexIndexOrderNote(ordering.moved),
1061
- ...mobileSpacingNote(spacing)
1062
- ]), opDrops);
1061
+ return withWriteDiff(withNormalization(response, plan.notes), opDrops);
1063
1062
  }));
1064
1063
  server.registerTool('page_discover', {
1065
1064
  title: 'Tasarım ekranından sayfa iskeleti öner',
@@ -1166,6 +1165,9 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1166
1165
  summary.layoutWarnings = updateWarnings;
1167
1166
  return withNormalization(summary, schema === undefined ? [] : [...container.fixes, ...skeletonNotes, ...componentNameNote(named), ...reset, ...componentDefaultsNote(fixed), ...catalogNotes, ...reorderNote(moved), ...mobileSpacingNote(spacing)]);
1168
1167
  }
1168
+ // PAGE-051 yalnız gerçekten OLUŞTURAN dalda: `ifExists: 'update'` var olan sayfada page_update gibi
1169
+ // davranır ve kayıtlı emekli düğüm yazılabilir kalmalıdır.
1170
+ assertNoRetiredNodes(container.schema);
1169
1171
  if (templateRoot && layoutId === undefined)
1170
1172
  skeletonNotes = shellLessPageSkeleton(templateRoot);
1171
1173
  const response = await platform.tenant('POST', project, `/pages/${applicationId}`, {
@@ -1343,7 +1345,7 @@ export function registerPageTools(server, platform, injectedScreenshotter, injec
1343
1345
  */
1344
1346
  async function mobileSpacingForOps(platformName, ops, readSaved) {
1345
1347
  const fix = emptyMobileSpacingFix();
1346
- if (platformName !== 'mobile' || !ops.some(entry => entry.op === 'add_component' || entry.op === 'batch'))
1348
+ if (platformName !== 'mobile' || !ops.some(entry => entry.op === 'add_component'))
1347
1349
  return fix;
1348
1350
  const saved = await readSaved().then(page => page.schema, () => undefined);
1349
1351
  if (saved !== undefined)
@@ -66,8 +66,9 @@ const guideRouting = {
66
66
  entity: "alan tipleri, ilişkiler, hazır entity'ler",
67
67
  page: 'sayfa/flow işleri; prop\'u veri kaynağına bağlama (connectData, Kural 1c-3); flex düzen prop grubu ve boyut modları (Kural 1f-2)',
68
68
  layout: 'yerleşim kabuğu: tek slot, views, dar ekranda yığılma, widthType fill/fixed/auto',
69
- mobile: "mobil UYGULAMA (type:'mobile'): kurulum sırası (mobile_app_create), flex-only yerleşim, tipli stil, sekmeler (properties.mobile.tabs) ve çekmece, adım sözlüğü, MOB-* bulguları, page_screenshot device",
70
- 'mobile-publish': "mobil uygulamanın mağaza yayını: mobile_store_* / mobile_build_* akışı, ön kontrol P-01…P-11 (push kimlikleri dahil), derleme hata kodları, insanın Studio'da yaptıkları",
69
+ mobile: "mobil UYGULAMA (type:'mobile'): kurulum sırası (mobile_app_create), flex-only yerleşim, tipli stil, sekmeler (properties.mobile.tabs) ve çekmece, sayfa mount/focus olayları, MOB-* bulguları, page_screenshot device",
70
+ 'mobile-steps': "mobil uygulamada cihaza dokunan akış adımları (redirect, alert, confirm, download, storage, setcookie, share, haptic, location, notification-*, permission, open-settings, back, ad-show, scan-code): mobildeki davranış, args sözleşmesi, Steps.<adım> sonucu; bildirim, izin, reklam ve konum tarifleri",
71
+ 'mobile-publish': "mobil uygulamanın mağaza yayını: mobile_store_* / mobile_build_* akışı, ön kontrol P-01…P-14 (push kimlikleri, izinler, reklam ve kod tarama dahil), derleme hata kodları, insanın Studio'da yaptıkları",
71
72
  'mobile-layout': 'mobil ekranın mizanpajı — tek dil: güvenli alan, tek kaydırıcı, 16 dp kenar, gap ritmi, saydam/yüzeyli grup, genişlik, yarıçap/yazı ölçeği; MOB-015…021 reçeteleri ve layoutReport hiza ölçümü',
72
73
  component: 'komponent modeli, olay sözleşmeleri, appearance boyut sözlüğü (widthString/heightString)',
73
74
  html: 'hazır komponent yetmediğinde tek seferlik kaçış kapısı (Mustache, fireEvent, script kuralları)',
@@ -48,8 +48,8 @@ async function pageShell(platform, project, environment, applicationId, layoutId
48
48
  }
49
49
  }
50
50
  /**
51
- * `set_event` ile bağlanan akışların adımları (MOB-005). Akış gövdeleri şemada değil ayrı uçtadır;
52
- * okunamazsa bulgu üretilmez, yazım yine geçer.
51
+ * `set_event` ile bağlanan akışların adımları (MOB-005). Akış gövdeleri şemada değil ayrı uçtadır; okunamazsa bulgu
52
+ * üretilmez, yazım yine geçer. `ops` araç girişinde açılmış listedir (PAGE-048): `batch` zarfı buraya ulaşmaz.
53
53
  */
54
54
  export async function boundFlowSteps(platform, resolved, project, environment, host, applicationId, hostId, ops) {
55
55
  if (resolved.profile.platform === 'web')