@atezer/figma-mcp-bridge 1.9.4 → 1.9.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.
@@ -6,7 +6,185 @@
6
6
 
7
7
  // v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
8
8
  // Keep in sync with package.json and src/core/version.ts.
9
- var FMCP_PLUGIN_VERSION = '1.9.4';
9
+ var FMCP_PLUGIN_VERSION = '1.9.6';
10
+
11
+ /**
12
+ * v1.9.6: Post-execute scan — figma_execute sonrasında oluşturulan node'ları
13
+ * otomatik tarayıp unbound fill/padding/radius/text-style tespit eder.
14
+ * Skill direktifi "return { createdNodeIds: [...] }" diyor — plugin bunu okuyup
15
+ * subtree'yi tarar. Violations varsa response'a _postExecuteScan field'ı eklenir,
16
+ * server bunu _DESIGN_SYSTEM_VIOLATIONS_BLOCKING'e dönüştürür.
17
+ */
18
+ async function postExecuteScan(result) {
19
+ var nodeIds = [];
20
+ if (result && typeof result === 'object') {
21
+ if (Array.isArray(result.createdNodeIds)) nodeIds = result.createdNodeIds;
22
+ else if (Array.isArray(result.nodeIds)) nodeIds = result.nodeIds;
23
+ else if (Array.isArray(result.ids)) nodeIds = result.ids;
24
+ else if (typeof result.frameId === 'string') nodeIds = [result.frameId];
25
+ else if (typeof result.rootId === 'string') nodeIds = [result.rootId];
26
+ else if (typeof result.nodeId === 'string') nodeIds = [result.nodeId];
27
+ }
28
+ if (nodeIds.length === 0) return null;
29
+
30
+ var violations = [];
31
+ var maxViolations = 10;
32
+ var maxNodes = 500; // safety cap per subtree
33
+ var totalChecked = 0;
34
+
35
+ for (var i = 0; i < nodeIds.length && violations.length < maxViolations; i++) {
36
+ var root;
37
+ try { root = await figma.getNodeByIdAsync(nodeIds[i]); } catch (e) { continue; }
38
+ if (!root) continue;
39
+ var stack = [root];
40
+ var visited = 0;
41
+ while (stack.length > 0 && visited < maxNodes) {
42
+ var n = stack.pop(); visited++; totalChecked++;
43
+ if (!n) continue;
44
+
45
+ // Fill check (skip instances — their bindings come from main component)
46
+ if (Array.isArray(n.fills) && n.type !== 'INSTANCE') {
47
+ for (var fi = 0; fi < n.fills.length; fi++) {
48
+ var fl = n.fills[fi];
49
+ if (fl && fl.visible !== false && fl.type === 'SOLID') {
50
+ var fbv = n.boundVariables && n.boundVariables.fills;
51
+ if (!(fbv && (Array.isArray(fbv) ? fbv[fi] : true)) && violations.length < maxViolations) {
52
+ violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_FILL', hint: 'setBoundVariableForPaint cagrisi eksik' });
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ if ((n.type === 'FRAME' || n.type === 'COMPONENT') && n.boundVariables !== undefined) {
59
+ var padProps = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'];
60
+ for (var j = 0; j < padProps.length && violations.length < maxViolations; j++) {
61
+ if (typeof n[padProps[j]] === 'number' && n[padProps[j]] > 0 && !(n.boundVariables && n.boundVariables[padProps[j]])) {
62
+ violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_PADDING', prop: padProps[j], value: n[padProps[j]] });
63
+ }
64
+ }
65
+ var radProps = ['cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius'];
66
+ for (var k = 0; k < radProps.length && violations.length < maxViolations; k++) {
67
+ if (typeof n[radProps[k]] === 'number' && n[radProps[k]] > 0 && !(n.boundVariables && n.boundVariables[radProps[k]])) {
68
+ violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_RADIUS', prop: radProps[k], value: n[radProps[k]] });
69
+ }
70
+ }
71
+ if (typeof n.itemSpacing === 'number' && n.itemSpacing > 0 && !(n.boundVariables && n.boundVariables.itemSpacing) && violations.length < maxViolations) {
72
+ violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_ITEMSPACING', value: n.itemSpacing });
73
+ }
74
+ }
75
+
76
+ if (n.type === 'TEXT' && !(n.textStyleId && typeof n.textStyleId === 'string' && n.textStyleId !== '') && violations.length < maxViolations) {
77
+ violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_TEXTSTYLE', sample: String(n.characters || '').slice(0, 30) });
78
+ }
79
+
80
+ if (n.children) {
81
+ for (var ci = 0; ci < n.children.length; ci++) stack.push(n.children[ci]);
82
+ }
83
+ }
84
+ }
85
+
86
+ return {
87
+ scanned: true,
88
+ rootsScanned: nodeIds.length,
89
+ totalChecked: totalChecked,
90
+ violationCount: violations.length,
91
+ violations: violations,
92
+ passed: violations.length === 0,
93
+ hint: violations.length > 0
94
+ ? '❌ v1.9.6 post-execute scan: ' + violations.length + ' unbound node tespit edildi. Kodu duzelt — her node icin setBoundVariable/setTextStyleIdAsync cagrisi eksik.'
95
+ : '✅ v1.9.6 post-execute scan: Tum node\'lar bound.',
96
+ };
97
+ }
98
+
99
+ // v1.9.5: Metadata summary builder for returnMode='summary' (screenshotsuz)
100
+ async function buildNodeSummary(node, maxDepth) {
101
+ var colorUsage = {}; // role → count
102
+ var textRoleCount = {}; // styleId → { count, samples }
103
+ var unboundHexCount = 0;
104
+ var totalNodes = 0;
105
+
106
+ function walk(n, depth) {
107
+ totalNodes++;
108
+ // Fill bound token tracking
109
+ if (Array.isArray(n.fills)) {
110
+ for (var fi = 0; fi < n.fills.length; fi++) {
111
+ var fl = n.fills[fi];
112
+ if (fl && fl.visible !== false && fl.type === 'SOLID') {
113
+ var fbv = n.boundVariables && n.boundVariables.fills;
114
+ var bound = fbv && (Array.isArray(fbv) ? fbv[fi] : true);
115
+ if (bound) {
116
+ var role = (Array.isArray(fbv) ? fbv[fi] : fbv).id || 'unknown';
117
+ colorUsage[role] = (colorUsage[role] || 0) + 1;
118
+ } else {
119
+ unboundHexCount++;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (n.type === 'TEXT') {
125
+ var styleId = n.textStyleId || 'inline';
126
+ if (!textRoleCount[styleId]) textRoleCount[styleId] = { count: 0, samples: [] };
127
+ textRoleCount[styleId].count++;
128
+ if (textRoleCount[styleId].samples.length < 3) {
129
+ textRoleCount[styleId].samples.push(String(n.characters || '').slice(0, 30));
130
+ }
131
+ }
132
+ if (n.children && depth < 3) { // deep walk but capped
133
+ for (var ci = 0; ci < n.children.length; ci++) walk(n.children[ci], depth + 1);
134
+ }
135
+ }
136
+
137
+ walk(node, 0);
138
+
139
+ function collectSections(n, depth) {
140
+ if (depth > maxDepth) return null;
141
+ var s = {
142
+ name: n.name,
143
+ type: n.type,
144
+ size: n.width && n.height ? Math.round(n.width) + 'x' + Math.round(n.height) : null,
145
+ childrenCount: n.children ? n.children.length : 0,
146
+ };
147
+ if (n.children && n.children.length > 0 && depth < maxDepth) {
148
+ var items = [];
149
+ for (var i = 0; i < n.children.length && i < 20; i++) {
150
+ items.push(collectSections(n.children[i], depth + 1));
151
+ }
152
+ s.children = items;
153
+ }
154
+ return s;
155
+ }
156
+
157
+ // Resolve variable names (best-effort)
158
+ var dominantColors = [];
159
+ var colorEntries = Object.keys(colorUsage).map(function (k) { return { role: k, usage: colorUsage[k] }; });
160
+ colorEntries.sort(function (a, b) { return b.usage - a.usage; });
161
+ for (var ci2 = 0; ci2 < Math.min(colorEntries.length, 5); ci2++) {
162
+ dominantColors.push({ variableId: colorEntries[ci2].role, usageCount: colorEntries[ci2].usage, bound: true });
163
+ }
164
+ if (unboundHexCount > 0) {
165
+ dominantColors.push({ variableId: '<hardcoded>', usageCount: unboundHexCount, bound: false });
166
+ }
167
+
168
+ var textRoles = [];
169
+ var trKeys = Object.keys(textRoleCount);
170
+ for (var tk = 0; tk < trKeys.length; tk++) {
171
+ textRoles.push({ styleId: trKeys[tk], count: textRoleCount[trKeys[tk]].count, samples: textRoleCount[trKeys[tk]].samples });
172
+ }
173
+
174
+ return {
175
+ nodeId: node.id,
176
+ name: node.name,
177
+ type: node.type,
178
+ dimensions: { width: Math.round(node.width || 0), height: Math.round(node.height || 0) },
179
+ layoutMode: node.layoutMode || 'NONE',
180
+ childrenCount: node.children ? node.children.length : 0,
181
+ totalNodes: totalNodes,
182
+ sections: collectSections(node, 0),
183
+ dominantColors: dominantColors,
184
+ textRoles: textRoles.slice(0, 10),
185
+ hint: "v1.9.5 summary: Metadata ozeti, gorsel yok. Layout karari icin yeter. Pixel dogrulama gerekirse returnMode='file' veya 'regions'.",
186
+ };
187
+ }
10
188
 
11
189
  // Console log buffer for figma_get_console_logs (no CDP)
12
190
  var __consoleLogBuffer = [];
@@ -552,6 +730,17 @@ figma.ui.onmessage = async (msg) => {
552
730
  safeResult = { __serializationError: true, message: 'Result could not be serialized: ' + (serErr.message || String(serErr)), resultType: typeof result };
553
731
  }
554
732
 
733
+ // v1.9.6: Post-execute scan — oluşturulan node'larda unbound tespit
734
+ var postScan = null;
735
+ try {
736
+ postScan = await postExecuteScan(result);
737
+ if (postScan && postScan.violationCount > 0) {
738
+ console.warn('🌉 [F-MCP v1.9.6] Post-execute scan: ' + postScan.violationCount + ' unbound node detected');
739
+ }
740
+ } catch (psErr) {
741
+ console.warn('🌉 [F-MCP v1.9.6] Post-execute scan failed:', psErr && psErr.message ? psErr.message : String(psErr));
742
+ }
743
+
555
744
  figma.ui.postMessage({
556
745
  type: 'EXECUTE_CODE_RESULT',
557
746
  requestId: msg.requestId,
@@ -562,7 +751,8 @@ figma.ui.onmessage = async (msg) => {
562
751
  fileContext: {
563
752
  fileName: figma.root.name,
564
753
  fileKey: figma.fileKey || null
565
- }
754
+ },
755
+ _postExecuteScan: postScan // v1.9.6: BLOCKING signal source for server
566
756
  });
567
757
 
568
758
  } catch (error) {
@@ -2985,64 +3175,140 @@ figma.ui.onmessage = async (msg) => {
2985
3175
  // ============================================================================
2986
3176
  else if (msg.type === 'CAPTURE_SCREENSHOT') {
2987
3177
  try {
2988
- console.log('🌉 [F-MCP ATezer Bridge] Capturing screenshot for node:', msg.nodeId);
3178
+ console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Capturing screenshot for node:', msg.nodeId, 'returnMode:', msg.returnMode);
2989
3179
 
2990
3180
  var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
2991
3181
  if (!node) {
2992
3182
  throw new Error('Node not found: ' + msg.nodeId);
2993
3183
  }
2994
3184
 
2995
- // Verify node supports export
2996
- if (!('exportAsync' in node)) {
2997
- throw new Error('Node type ' + node.type + ' does not support export');
2998
- }
2999
-
3000
- // v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
3001
- // msg.scale, msg.jpegQuality from the request payload (passed through
3002
- // captureScreenshot connector → bridge → here).
3185
+ // v1.8.0: Default JPG@1x q70 for context safety.
3003
3186
  var format = (msg.format || 'JPG').toUpperCase();
3004
3187
  var scale = msg.scale != null ? msg.scale : 1;
3005
3188
  var jpegQuality = msg.jpegQuality != null ? msg.jpegQuality : 70;
3189
+ // v1.9.5: returnMode (default 'file' which server translates to base64 + save)
3190
+ var returnMode = msg.returnMode || 'base64'; // plugin default stays base64; server wraps to file
3191
+ var regionStrategy = msg.regionStrategy || 'children';
3192
+ var maxRegions = msg.maxRegions != null ? msg.maxRegions : 8;
3006
3193
 
3007
- var exportSettings = {
3008
- format: format,
3009
- constraint: { type: 'SCALE', value: scale }
3010
- };
3011
- // JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
3012
- if (format === 'JPG') {
3013
- exportSettings.quality = jpegQuality;
3194
+ function buildExportSettings() {
3195
+ var s = { format: format, constraint: { type: 'SCALE', value: scale } };
3196
+ if (format === 'JPG') s.quality = jpegQuality;
3197
+ return s;
3014
3198
  }
3015
3199
 
3016
- // Export the node
3017
- var bytes = await node.exportAsync(exportSettings);
3200
+ // ---------- v1.9.5 MODE: summary ----------
3201
+ // No export. Just metadata.
3202
+ if (returnMode === 'summary') {
3203
+ var rootSummary = await buildNodeSummary(node, /*depth*/ 2);
3204
+ figma.ui.postMessage({
3205
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3206
+ requestId: msg.requestId,
3207
+ success: true,
3208
+ mode: 'summary',
3209
+ summary: rootSummary,
3210
+ });
3211
+ return;
3212
+ }
3018
3213
 
3019
- // Convert to base64
3020
- var base64 = figma.base64Encode(bytes);
3214
+ // ---------- v1.9.5 MODE: regions (children strategy) ----------
3215
+ if (returnMode === 'regions' && regionStrategy === 'children') {
3216
+ if (!node.children || node.children.length === 0) {
3217
+ figma.ui.postMessage({
3218
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3219
+ requestId: msg.requestId,
3220
+ success: false,
3221
+ error: "Node has no children for regions mode. Use 'file' or 'summary' instead.",
3222
+ });
3223
+ return;
3224
+ }
3225
+ var regions = [];
3226
+ var children = node.children.slice(0, maxRegions);
3227
+ for (var ri = 0; ri < children.length; ri++) {
3228
+ var child = children[ri];
3229
+ if (!('exportAsync' in child)) continue;
3230
+ try {
3231
+ var cbytes = await child.exportAsync(buildExportSettings());
3232
+ var cbase64 = figma.base64Encode(cbytes);
3233
+ regions.push({
3234
+ name: child.name,
3235
+ nodeId: child.id,
3236
+ type: child.type,
3237
+ image: {
3238
+ base64: cbase64,
3239
+ format: format,
3240
+ scale: scale,
3241
+ byteLength: cbytes.length,
3242
+ width: child.width,
3243
+ height: child.height,
3244
+ },
3245
+ });
3246
+ } catch (childErr) {
3247
+ regions.push({
3248
+ name: child.name,
3249
+ nodeId: child.id,
3250
+ error: childErr && childErr.message ? childErr.message : String(childErr),
3251
+ });
3252
+ }
3253
+ }
3254
+ figma.ui.postMessage({
3255
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3256
+ requestId: msg.requestId,
3257
+ success: true,
3258
+ mode: 'regions',
3259
+ regionStrategy: 'children',
3260
+ regions: regions,
3261
+ totalRegions: regions.length,
3262
+ rootSummary: {
3263
+ nodeId: node.id,
3264
+ name: node.name,
3265
+ dimensions: { width: node.width, height: node.height },
3266
+ childrenCount: node.children.length,
3267
+ capped: node.children.length > maxRegions,
3268
+ },
3269
+ });
3270
+ return;
3271
+ }
3272
+
3273
+ // ---------- v1.9.5 MODE: regions (slices strategy) — NOT SUPPORTED YET ----------
3274
+ if (returnMode === 'regions' && regionStrategy === 'slices') {
3275
+ // Figma exportAsync does not support crop regions natively.
3276
+ // Fallback: return a warning + single full image export.
3277
+ figma.ui.postMessage({
3278
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3279
+ requestId: msg.requestId,
3280
+ success: false,
3281
+ error: "regionStrategy='slices' not supported in v1.9.5. Use regionStrategy='children' or returnMode='summary'.",
3282
+ });
3283
+ return;
3284
+ }
3021
3285
 
3022
- // Get node bounds for context
3023
- var bounds = null;
3024
- if ('absoluteBoundingBox' in node) {
3025
- bounds = node.absoluteBoundingBox;
3286
+ // ---------- DEFAULT: full single export (base64 or file — server post-processes) ----------
3287
+ if (!('exportAsync' in node)) {
3288
+ throw new Error('Node type ' + node.type + ' does not support export');
3026
3289
  }
3027
3290
 
3028
- console.log('🌉 [F-MCP ATezer Bridge] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
3291
+ var bytes = await node.exportAsync(buildExportSettings());
3292
+ var base64 = figma.base64Encode(bytes);
3293
+ var bounds = ('absoluteBoundingBox' in node) ? node.absoluteBoundingBox : null;
3294
+
3295
+ console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
3029
3296
 
3030
3297
  figma.ui.postMessage({
3031
3298
  type: 'CAPTURE_SCREENSHOT_RESULT',
3032
3299
  requestId: msg.requestId,
3033
3300
  success: true,
3301
+ mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
3034
3302
  image: {
3035
3303
  base64: base64,
3036
3304
  format: format,
3037
3305
  scale: scale,
3038
3306
  jpegQuality: format === 'JPG' ? jpegQuality : null,
3039
3307
  byteLength: bytes.length,
3040
- node: {
3041
- id: node.id,
3042
- name: node.name,
3043
- type: node.type
3044
- },
3045
- bounds: bounds
3308
+ width: node.width,
3309
+ height: node.height,
3310
+ node: { id: node.id, name: node.name, type: node.type },
3311
+ bounds: bounds,
3046
3312
  }
3047
3313
  });
3048
3314
 
@@ -424,7 +424,7 @@
424
424
  window.__figmaFileName = null;
425
425
  // v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
426
426
  // Keep in sync with package.json, src/core/version.ts, f-mcp-plugin/code.js.
427
- var FMCP_PLUGIN_VERSION = '1.9.4';
427
+ var FMCP_PLUGIN_VERSION = '1.9.6';
428
428
 
429
429
  // v1.9.2+ Startup log — Figma plugin iframe'de yeni kod yüklendiğini doğrulamak için.
430
430
  // Console'da bu log görünüyorsa plugin güncel. Görünmüyorsa iframe eski kopyayı tutuyor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atezer/figma-mcp-bridge",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "F-MCP ATezer: MCP server and Figma plugin bridge for Claude/Cursor. No REST token required.",
5
5
  "type": "module",
6
6
  "main": "dist/local-plugin-only.js",
@@ -184,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
184
184
 
185
185
  16. **Her Promise'i `await` et.**
186
186
 
187
+ 17. **v1.9.5 Concise Query Rule — tek satır boyut/metadata sorguları.**
188
+
189
+ Boyut, renk, layout sorgulamak için **20+ satır JS YAZMA**. Tek satır yeter:
190
+
191
+ ```js
192
+ // İYİ — tek satır, minimal context
193
+ return await figma.getNodeByIdAsync(id).then(n => ({ w: n.width, h: n.height, children: n.children?.length }));
194
+
195
+ // İYİ — toplu (birden fazla node)
196
+ return Promise.all(ids.map(async id => {
197
+ const n = await figma.getNodeByIdAsync(id);
198
+ return { id, name: n?.name, size: n ? [n.width, n.height] : null };
199
+ }));
200
+
201
+ // KÖTÜ — 30 satır "defensive" kod boyut sorgusu için
202
+ // await figma.loadAllPagesAsync(); for (var pg of ...) { if (...) ... }
203
+ ```
204
+
205
+ **Kural:** Tek nodeId için size → 1 satır. Multi-node metadata → 1 Promise.all. Complex traversal sadece üretim mantığında gerekli — keşifte değil.
206
+
187
207
  ## 2. Sayfa Kuralları
188
208
 
189
209
  ```js
@@ -226,6 +226,48 @@ FOR each input in required_inputs:
226
226
 
227
227
  Missing list'teki tüm soruları **TEK** `AskUserQuestion` çağrısında toplayıp sor. Max 4 soru/çağrı (AskUserQuestion limiti). 5+ soru gerekiyorsa 2 ayrı çağrı yap, ama ÖNCE kritik 4'ü sor, sonra kalanları.
228
228
 
229
+ ### v1.9.5 Elicitation Kuralı (SERT)
230
+
231
+ - **Maks 1 `AskUserQuestion` çağrısı** tüm oturum boyunca. Tekrar soru sormak yasak — state'ten veya context'ten çıkar.
232
+ - **"devam et" / "tamam" / "ok" / "yap" sonrası soru YASAK.** Kullanıcı onay verdi → üretime geç.
233
+ - User prompt'u anlamlıysa (spesifik ekran adı, boyut, DS veriliyorsa) **hiç sorma**, direkt planla ve göster.
234
+ - **"Sen seç"** cevabı alırsan: mantıklı default kullan (iPhone 17, mobile, single variant, active DS).
235
+ - Sorulan her soru ≤30 kelime — uzun elicitation yasak.
236
+ - Soruyu sormanın context maliyeti ≥ yanıtın değeri mi? Değilse sorma.
237
+
238
+ ### v1.9.6 Negative Intent Detection (KRITIK)
239
+
240
+ Kullanıcı şu paternlerle **negatif/dışlayıcı** talimat verebilir — bunları ZORUNLU parse et:
241
+
242
+ | Kullanıcı paterni | Anlam | Nasıl handle et |
243
+ |---|---|---|
244
+ | "X'i atla" / "X'i kullanma" / "X'ten bahsetme" | X referansını DIŞLA | `exclude_references: ["X"]` state'e yaz, o node/page/frame'e referans verme |
245
+ | "X'e bakma" / "X yok say" / "X'i unut" | X sıfır-referans | Aynı |
246
+ | "Y benzetme" / "Y gibi olmasın" | Y anti-pattern | `anti_pattern_refs: ["Y"]` — Y'ye ters tasarım yap |
247
+ | "X dışında" / "X hariç" | Whitelist exclusion | `exclude_references: ["X"]` |
248
+ | "Sıfırdan" / "baştan" / "yeni tasarım" | Mevcut iterasyonları atla | `start_fresh: true`, ideation/iterasyon referansları atla |
249
+
250
+ **Örnek:**
251
+
252
+ ```
253
+ Kullanıcı: "SUI Alt 3'e bakma, sıfırdan yeni bir Anasayfa tasarla"
254
+
255
+ PARSE:
256
+ - exclude_references: ["SUI Alt 3", "241:11896"] (SUI Alt 3 frame ID'si de dahil)
257
+ - start_fresh: true
258
+ - target_screen: "Anasayfa"
259
+ - DS: active-ds.md'den çek
260
+
261
+ SONUÇ:
262
+ - SUI Alt 3 sayfası referans alınmaz
263
+ - Mevcut "SUI Alt 3 — Anasayfa" frame'i açılmaz, screenshot alınmaz
264
+ - ideation sayfası (ilham) kullanılabilir ama SUI Alt 3 bölümü atlanır
265
+ ```
266
+
267
+ **Anti-pattern:** Kullanıcı "SUI Alt 3 atla" dedikten sonra onu screenshot alıp referans göstermek. v1.9.6'da bu explicit yasak — pre-flight'ta `exclude_references` kontrolü yapılır.
268
+
269
+ **State persist:** `exclude_references` last-intent.md'ye yazılır; aynı oturum boyunca geçerli.
270
+
229
271
  **Örnek — generate-figma-screen:**
230
272
 
231
273
  ```
@@ -144,6 +144,35 @@ Benchmark/görselden DEĞER alma YASAK. Sadece NİYET: layout yönü, hiyerarşi
144
144
  2. **Content** — DS instance yerleşimi → screenshot → onay
145
145
  3. **Polish** — spacing, states, edge cases → son screenshot → audit
146
146
 
147
+ ### v1.9.5 Discovery Budget Rule (SERT)
148
+
149
+ - **Maks 3 discovery çağrısı** (figma_get_*, figma_search_*, figma_execute read-only) sonra plan sun.
150
+ - Plan kullanıcıya 1-2 cümle + varsa özet: "Şu ekran/section'ları oluşturacağım: [liste]. Onay veriyor musun?"
151
+ - Kullanıcı onay verdikten sonra **mutation** aşamasına geç (figma_execute createFrame/setFills/setBoundVariable) — discovery counter reset olur.
152
+ - Plugin 8 çağrıdan sonra `_warnings: ["DISCOVERY_BUDGET_WARNING..."]` döner — görünce üretime geçmek zorundasın.
153
+ - 12 çağrıdan sonra `_DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true` döner — **skip edilemez**, plan sun veya dur.
154
+
155
+ ### v1.9.5 Screenshot Method Selection (KARAR AĞACI)
156
+
157
+ Ekran yakalama isteği olduğunda şu ağacı takip et:
158
+
159
+ ```
160
+ İhtiyaç ne?
161
+ ├── "Planlama yapacağım, layout anlamak istiyorum" → returnMode: "summary" (metadata, screenshotsuz)
162
+ ├── "Kullanıcıya son halini göstereyim" → returnMode: "file" (1 screenshot dosyaya)
163
+ ├── "Büyük/scroll'lu ekran, bölümleri görmek istiyorum" → returnMode: "regions", regionStrategy: "children"
164
+ ├── "Üretim sonrası hızlı validation" → returnMode: "file"
165
+ ├── "Spesifik region (örn Hero)" → single-node file veya regions children maxRegions=3
166
+ └── "Base64 context'te gerekli (nadiren)" → returnMode: "base64" (explicit, _warning ile)
167
+ ```
168
+
169
+ **Budget farkındalığı:**
170
+ - Bir oturumda >3 farklı nodeId için screenshot → 4. için zorunlu `summary` veya `regions`
171
+ - Response'da `_warnings: ["CONTEXT_NEAR_LIMIT"]` veya `"DISCOVERY_BUDGET_..."` görürsen → sonraki screenshot zorunlu `summary`
172
+ - Pixel-perfect değil, **region-perfect** düşün: "Hero'yu göreyim, gerekirse Actions'ı da" — hepsi birden değil
173
+
174
+ **Kritik:** Screenshot YASAK değil. "Yasak" yerine **doğru yöntem** kullan. Her mode'un kullanım amacı var.
175
+
147
176
  ### Error Recovery
148
177
 
149
178
  | Hata | Aksiyon |