jui-tools-mcp-server 2.15.4 → 2.15.7

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/README.md CHANGED
@@ -55,7 +55,7 @@ bash ~/.jsonui-mcp-server/uninstall.sh
55
55
  | `lookup_component` | コンポーネント仕様を取得(属性、binding、プラットフォーム別詳細) |
56
56
  | `lookup_attribute` | 属性定義を取得(型、binding対応、使用コンポーネント) |
57
57
  | `search_components` | キーワードでコンポーネント/属性を検索 |
58
- | `get_modifier_order` | プラットフォーム別 modifier 適用順序 |
58
+ | `get_modifier_order` | プラットフォーム別 modifier 適用順序(Swift は jsonui-cli の `sjui_tools/lib/swiftui/views/modifier_order.json` から導出。Kotlin / React は手書きで、`source` にそう出る) |
59
59
  | `get_binding_rules` | binding 構文ルール(two-way / read-only) |
60
60
  | `get_platform_mapping` | プラットフォーム間の値変換マッピング |
61
61
  | `get_screen_identity` | スクリーン正準定義(screen とは何か・id 導出・表示判定述語) |
@@ -284,7 +284,7 @@
284
284
  },
285
285
  "responsive": {
286
286
  "type": "object",
287
- "description": "Size class overrides. Keys: compact, medium, regular, landscape, or hyphenated combos (e.g. regular-landscape). Values: object of attribute overrides for that size class."
287
+ "description": "Size class or window-width overrides. Keys: compact, medium, regular, landscape, or hyphenated combos (e.g. regular-landscape); or window-width conditions minWidth:N (the window is at least N wide) and maxWidth:N (narrower than N; N is not included, unlike jsonui-test-runner's maxWidth test condition), N a whole number of pt / dp / CSS px. One block uses size classes or width conditions, never both; every maxWidth's N is at most every minWidth's N; no landscape combination for a width condition. The one key that holds and ranks highest wins alone (size classes: regular-landscape > medium-landscape > compact-landscape > landscape > regular > medium > compact; width: the smallest maxWidth, else the largest minWidth). Values: object of attribute overrides."
288
288
  },
289
289
  "visibility": {
290
290
  "type": [
@@ -1169,7 +1169,7 @@
1169
1169
  "equalSpacing",
1170
1170
  "equalCentering"
1171
1171
  ],
1172
- "description": "Child distribution for orientation-bearing containers. SwiftUI: Spacer/weight synthesis; Compose: Arrangement.",
1172
+ "description": "Child distribution for orientation-bearing containers. SwiftUI: Spacer/weight synthesis; Compose: Arrangement. `fill` and `fillEqually` distribute SIZE: a child whose width (in a vertical stack, its height) is `wrapContent` grows, and the growing children share the axis \u2014 `fill` grows each from its content, `fillEqually` makes them equal shares of what is left. Only a child that declares a numeric size keeps it. `equalSpacing` and `equalCentering` leave the children's sizes alone and distribute the free space between them (equal gaps / equal centre-to-centre tracks).",
1173
1173
  "platform": [
1174
1174
  "swift",
1175
1175
  "kotlin"
@@ -3544,7 +3544,7 @@
3544
3544
  "equalSpacing",
3545
3545
  "equalCentering"
3546
3546
  ],
3547
- "description": "Child distribution mode for stacks",
3547
+ "description": "Child distribution mode for stacks. `fill` and `fillEqually` distribute SIZE: a child whose width (in a vertical stack, its height) is `wrapContent` grows, and the growing children share the axis \u2014 `fill` grows each from its content, `fillEqually` makes them equal shares of what is left. Only a child that declares a numeric size keeps it. `equalSpacing` and `equalCentering` leave the children's sizes alone and distribute the free space between them (equal gaps / equal centre-to-centre tracks).",
3548
3548
  "mode": [
3549
3549
  "swiftui",
3550
3550
  "compose",
@@ -4043,7 +4043,7 @@
4043
4043
  },
4044
4044
  "scrollTo": {
4045
4045
  "type": "binding",
4046
- "description": "Binding that requests a programmatic scroll. The declared DATA CLASS is a plain value, and it decides what the value names, on every path (4f ruling 2026-09-27, round 14; jsonui-cli 1.9.0): an `Int` is a cell's index, a `String` a cell's key. cellIdProperty decides what a cell's key is — its `cellId`, else its cellIdProperty value — and not the class: a Collection with cellIdProperty may declare an `Int`, and one without it a `String` (its cells' cellIds are the keys). What it names across sections (4f ruling 2026-09-27, jsonui-cli 1.9.0; one rule on every path — sjui, kjui and rjui codegen, SwiftJsonUI and KotlinJsonUI Dynamic): an `Int` is the index of a CELL counted across all the drawn sections in order, cells only — a section's header or footer, and a grid's filler, is not counted (the same n as the paging address `<id>_item_<n>`); a `String` is the FIRST cell, in section order, whose key (its `cellId`, else its cellIdProperty value) it is, when two cells share the key — in two sections or in one; a cell with no key has none, and no String names it. A value that names no cell scrolls nowhere. Kotlin only (legacy): a String that is no cell's key and reads `<digits>` or `<digits>#<anything>` scrolls to lazy item <digits>, with a DEBUG warning, on the lazy lists; a container that is not one (the flow, the EAGER one, the wrapContent Column) has no lazy item and does not read it, and iOS and web do not read it. (A consuming Android screen asks its keyed, reverse-laid-out list for its bottom-most item with `\"0#<time>\"`; read as a cell, 0 would be section 0's first cell, which is not the bottom-most item when a later section has cells.) The request is a CHANGE of the value (4f ruling 2026-09-27, jsonui-cli 1.9.0): the value a Collection is drawn with scrolls nowhere, a value arriving where there was none is a change like any other, and sending the same value again does not re-scroll. It is deliberately NOT `PassthroughSubject<Int, Never>`, which this description used to prescribe: PassthroughSubject is a Combine type, and naming a Swift transport in the cross-platform SSoT is what made consumers write it into their data section, where kjui's map_to_kotlin_type passes unknown classes through verbatim and the Kotlin build dies on it. How the request is delivered is each platform's own business — iOS keys `.onChange(of:)` on the value inside a ScrollViewReader, Compose a LaunchedEffect, web a useEffect that compares the value with the one it last saw. STATUS 2026-09-27 (jsonui-cli 1.9.0): every path reads the plain value — sjui and SwiftJsonUI Dynamic, kjui and KotlinJsonUI Dynamic, rjui. On iOS and web it reaches the cells of every route with a scroll container of its own: the lists (the class-list shape's included), the grids, the flows, the horizontal Collections and the pagers — a pager turns to the page the value names, its currentPage when one is bound. On Compose (kjui codegen and KotlinJsonUI Dynamic) it reaches the lazy lists and grids, the horizontal rows, the pagers and a flow that scrolls in its own bounds (4f ruling 2026-09-27, round 11), and the EAGER container (kjui's CollectionStack EAGER, KotlinJsonUI Dynamic's Column and Row) and the Column of a vertical Collection whose height wraps its content (see `lazy`), by the same rule (4f ruling 2026-09-27, round 12); until jsonui-cli 1.9.0 neither scrolled to a cell. Until 1.9.0 kjui codegen read every value as its text and scrolled to that LAZY ITEM index - headers, footers and grid fillers counted -, with cellIdProperty looked an Int up as a key first (so an Int never became a cell there) and read a String without cellIdProperty as the index, scrolled on the value the list was first drawn with, landed bottom and center with the target's top edge at the viewport's bottom edge and middle, drew no scroll on the flow and the pager, and a key two cells shared took the lazy list down (\"Key ... was already used\"); KotlinJsonUI Dynamic read only a SharedFlow<Int>, so the plain value never scrolled, landed center and bottom at the top, and drew no scroll on the flow and the pager. Until 1.9.0 web's grid was no scroll container (a grid of a declared height drew its rows past its box and a scrollTo moved nothing); on iOS the flow (sjui codegen and SwiftJsonUI Dynamic), SwiftJsonUI Dynamic's sectioned List, the class-list List and the pager drew no scroll at all; and a String on a Collection with no cellIdProperty never met a cellId — web read a string of digits there as an index. Until 1.9.0 sjui codegen took what the value names from cellIdProperty, not from the declared class: an `Int` with cellIdProperty did not compile on the class-list List and the pager and scrolled nowhere on the other routes; and its cell ids with cellIdProperty let a String reach a cell with no key (its id was \"\\(index)\"), and gave two cells of one section with one key one id, the cell it reached SwiftUI's choice. A Collection with `lazy: none` has no scroll container of its own and scrolls nowhere. Until 2026-09-27 this paragraph named the SwiftUI Dynamic renderer as the one that could not read a plain value; it was the KotlinJsonUI Dynamic renderer, which reads it today."
4046
+ "description": "Binding that requests a programmatic scroll. The declared DATA CLASS is a plain value, and it decides what the value names, on every path (4f ruling 2026-09-27, round 14; jsonui-cli 1.9.0): an `Int` is a cell's index, a `String` a cell's key. cellIdProperty decides what a cell's key is — its `cellId`, else its cellIdProperty value — and not the class: a Collection with cellIdProperty may declare an `Int`, and one without it a `String` (its cells' cellIds are the keys). What it names across sections (4f ruling 2026-09-27, jsonui-cli 1.9.0; one rule on every path — sjui, kjui and rjui codegen, SwiftJsonUI and KotlinJsonUI Dynamic): an `Int` is the index of a CELL counted across all the drawn sections in order, cells only — a section's header or footer, and a grid's filler, is not counted (the same n as the paging address `<id>_item_<n>`); a `String` is the FIRST cell, in section order, whose key (its `cellId`, else its cellIdProperty value) it is, when two cells share the key — in two sections or in one; a cell with no key has none, and no String names it. A value that names no cell scrolls nowhere. Kotlin only (legacy): a String that is no cell's key and reads `<digits>` or `<digits>#<anything>` scrolls to lazy item <digits>, with a DEBUG warning, on the lazy lists; a container that is not one (the flow, the EAGER one, the wrapContent Column) has no lazy item and does not read it, and iOS and web do not read it. (A consuming Android screen asks its keyed, reverse-laid-out list for its bottom-most item with `\"0#<time>\"`; read as a cell, 0 would be section 0's first cell, which is not the bottom-most item when a later section has cells.) The request is a CHANGE of the value (4f ruling 2026-09-27, jsonui-cli 1.9.0): the value a Collection is drawn with scrolls nowhere, a value arriving where there was none is a change like any other, and sending the same value again does not re-scroll. A landing in flight is left alone by a change to a value that names no cell — \"\", null, a key no cell has, an index out of range — and replaced by a change to a value that names a cell, which lands on its own (jsonui-cli 1.9.26, kjui-collection-scrollto-landing-cancelled-by-next-value: a consumer that names a cell and puts the value back to \"\" 200 ms later, to be able to name it again, cut every Compose landing longer than that; iOS's `.onChange(of:)` never did). It is deliberately NOT `PassthroughSubject<Int, Never>`, which this description used to prescribe: PassthroughSubject is a Combine type, and naming a Swift transport in the cross-platform SSoT is what made consumers write it into their data section, where kjui's map_to_kotlin_type passes unknown classes through verbatim and the Kotlin build dies on it. How the request is delivered is each platform's own business — iOS keys `.onChange(of:)` on the value inside a ScrollViewReader, Compose a LaunchedEffect, web a useEffect that compares the value with the one it last saw. STATUS 2026-09-27 (jsonui-cli 1.9.0): every path reads the plain value — sjui and SwiftJsonUI Dynamic, kjui and KotlinJsonUI Dynamic, rjui. On iOS and web it reaches the cells of every route with a scroll container of its own: the lists (the class-list shape's included), the grids, the flows, the horizontal Collections and the pagers — a pager turns to the page the value names, its currentPage when one is bound. On Compose (kjui codegen and KotlinJsonUI Dynamic) it reaches the lazy lists and grids, the horizontal rows, the pagers and a flow that scrolls in its own bounds (4f ruling 2026-09-27, round 11), and the EAGER container (kjui's CollectionStack EAGER, KotlinJsonUI Dynamic's Column and Row) and the Column of a vertical Collection whose height wraps its content (see `lazy`), by the same rule (4f ruling 2026-09-27, round 12); until jsonui-cli 1.9.0 neither scrolled to a cell. Until 1.9.0 kjui codegen read every value as its text and scrolled to that LAZY ITEM index - headers, footers and grid fillers counted -, with cellIdProperty looked an Int up as a key first (so an Int never became a cell there) and read a String without cellIdProperty as the index, scrolled on the value the list was first drawn with, landed bottom and center with the target's top edge at the viewport's bottom edge and middle, drew no scroll on the flow and the pager, and a key two cells shared took the lazy list down (\"Key ... was already used\"); KotlinJsonUI Dynamic read only a SharedFlow<Int>, so the plain value never scrolled, landed center and bottom at the top, and drew no scroll on the flow and the pager. Until 1.9.0 web's grid was no scroll container (a grid of a declared height drew its rows past its box and a scrollTo moved nothing); on iOS the flow (sjui codegen and SwiftJsonUI Dynamic), SwiftJsonUI Dynamic's sectioned List, the class-list List and the pager drew no scroll at all; and a String on a Collection with no cellIdProperty never met a cellId — web read a string of digits there as an index. Until 1.9.0 sjui codegen took what the value names from cellIdProperty, not from the declared class: an `Int` with cellIdProperty did not compile on the class-list List and the pager and scrolled nowhere on the other routes; and its cell ids with cellIdProperty let a String reach a cell with no key (its id was \"\\(index)\"), and gave two cells of one section with one key one id, the cell it reached SwiftUI's choice. A Collection with `lazy: none` has no scroll container of its own and scrolls nowhere. Until 2026-09-27 this paragraph named the SwiftUI Dynamic renderer as the one that could not read a plain value; it was the KotlinJsonUI Dynamic renderer, which reads it today."
4047
4047
  },
4048
4048
  "scrollAnchor": {
4049
4049
  "type": "string",
@@ -210,7 +210,7 @@
210
210
  "edgeSpacerGate": "equalCentering's edge spacers are suppressed ONLY on a wrapContent axis (declared size nil): with no free space a Spacer would expand the container and swallow the trailing padding (the measured round-3 bug). A FIXED numeric size has free space for the ratio and KEEPS its edge spacers — the older widthExpands gate (matchParent/-1/.infinity) was too narrow for it (F, e8f99c7, decision 2)",
211
211
  "note": "Pinned 2026-08-06 (E's proposal, orchestrator-approved: implement this release, B codegen + F dynamic) so the two ios paths emit ONE counting instead of deriving Spacer arithmetic twice — two paths growing children separately is exactly what collapsed distribution's SIZE half before c7d2dfb. Spacer counting suffices because HStack/VStack split free space equally among unconstrained Spacers: the 1:2 end:between count ratio IS space-around's half-gap. No custom Layout, no measurement pass. Acceptance is already wired: when this lands, run 6 must delete collapsedPairs' ios row and turn the two value_discrimination receiving rows stale. Regression-test discipline from F's landing: a partial reversion (between 2->1 only) builds an equal-three-spacer structure the first sample points slipped past — pin BOTH sides of the computed boundary (55 in the 300pt fixture) so partial and full reversion both fail."
212
212
  },
213
- "explicitChildSizeWins": "an explicit child width/height beats distribution's size half — size topic's explicit > bounds > fill",
213
+ "explicitChildSizeWins": "an explicit child width/height beats distribution's size half — size topic's explicit > bounds > fill. Explicit = a numeric size. wrapContent (and an undeclared axis, which the validator refuses) is a child fill grows. User ruling 2026-10-07 (relayed by the orchestrator): distribution(fill / fillEqually)で `width: wrapContent` の子は伸ばす。「明示された大きさ」は数値だけ。理由: width は required なので、wrapContent まで明示に入れると、正しい layout で fill が伸ばせる子が無くなる。 Measured before the ruling: KotlinJsonUI 2.43.5 Dynamic drew wrapContent children of a fill row at their content width (View/ and common/ distribution__fill / __fillequally, 2026-10-07); sjui codegen and KotlinJsonUI Dynamic's own fill decision already read only a numeric size as explicit.",
214
214
  "collapsedPairs": {},
215
215
  "observableNote": "MEASURED 2026-08-08 (run 31243724782 + local-env): uniformly active — the divergence this entry recorded was fixed in-wave (ios Blur bridge / ios highlighted swap / ios distribution fill / ios loadingImage via pending.invalid / border emit on all faces)."
216
216
  },
@@ -368,6 +368,19 @@
368
368
  "observable": {},
369
369
  "overflowHitTesting": "hit testing follows CLIPPING, not the parent frame: an unclipped overflow is drawn and therefore tappable; a clipped one is neither"
370
370
  },
371
+ "idBox": {
372
+ "attributes": [
373
+ "common.id"
374
+ ],
375
+ "box": "an id names its element's box: inside the margin, including the padding, moved by the offset",
376
+ "ruling": "2026-10-06, user: 「マージンはそのビューの要素じゃない」 (the margin is not the element), 「パディングはその要素だからね」 (the padding is the element), and the id box moves with the offset (Android's measured result, accepted as the target). This is the box a UI test driver taps and reads (XCUIElement.frame on iOS, accessibility bounds on Android). iOS measured it against the layout box in SwiftJsonUI ConformanceHost IdBoxProbeUITests and CellIdBoxProbeUITests; containers met it from SwiftJsonUI 10.29.7 / jsonui-cli 1.9.18, when the 0.5pt single-child merge anchor moved inside the offset and the margins.",
377
+ "platformConventions": {
378
+ "androidButtonTouchTarget": "Android: a Button's id box is at least 48dp high (Material's minimum touch target), centred on the declared box — (25,6,100,48) for a 100 x 40 Button. A platform convention, not a defect (2026-10-06). iOS keeps the declared 40.",
379
+ "iosTextFieldInputArea": "iOS: a TextField's id box is the input field itself, inside the padding — (33,19,84,22) for a 100 x 40 field with padding 8. SwiftUI's TextField cannot be made larger than its line, and every placement that gives the padded box either takes the id off the input (an Other container with no value, which a text assertion cannot read) or cannot be typed into (measured 2026-10-06, six placements). With a symmetric padding the box's centre, where a tap aims, is the same. User ruling 2026-10-06: recorded as an iOS convention, not fixed. The id stays on the input field."
380
+ },
381
+ "collectionCellRoot": "A cell's root is addressed as `{collectionId}_item_{N}`. An id declared on the root is not used under the item address: the item address and the root's id would name the same element, which carries one identifier, so the root's own id is not found (iOS Dynamic and codegen, Android, measured 2026-10-06). The item's box is the root's box by the rule above. User ruling 2026-10-06.",
382
+ "observable": {}
383
+ },
371
384
  "sliderRange": {
372
385
  "attributes": [
373
386
  "Slider.minimum",
@@ -0,0 +1,57 @@
1
+ {
2
+ "_about": [
3
+ "The order sjui codegen emits SwiftUI modifiers in, for SwiftJsonUI to hold its Dynamic runtime to (DynamicModifierHelper.standardOrder).",
4
+ "bag: ModifierBag::MODIFIER_ORDER (swiftui/views/modifier_bag.rb), the order emit_all writes the registered keys in.",
5
+ "after_bag: what BaseViewConverter writes after the bag, in order: the accessibilityIdentifier, then .disabled again outside it (apply_outer_disabled).",
6
+ "shared_slots: keys that carry more than one attribute family, in the order their entries are written: :padding gets padding first, then insets (SpacingHelper#apply_insets appends to :padding after apply_padding).",
7
+ "spec/swiftui/views/modifier_order_json_spec.rb holds this file to the constant and to generated code. Change them together; SwiftJsonUI copies this file byte for byte and its CI compares the copy with this file at its pinned jsonui-cli ref."
8
+ ],
9
+ "bag": [
10
+ "component_specific",
11
+ "padding",
12
+ "frame_constraints",
13
+ "fixed_size",
14
+ "frame_size",
15
+ "background",
16
+ "gradient",
17
+ "glass",
18
+ "corner_radius",
19
+ "border",
20
+ "shadow",
21
+ "clip_to_bounds",
22
+ "foreground_color",
23
+ "opacity",
24
+ "hidden",
25
+ "accessibility_anchor",
26
+ "conformance_frame",
27
+ "on_click",
28
+ "on_long_press",
29
+ "on_pan",
30
+ "on_pinch",
31
+ "allows_hit_testing",
32
+ "offset",
33
+ "margin",
34
+ "tint_color",
35
+ "on_value_change",
36
+ "on_text_change",
37
+ "on_appear",
38
+ "on_disappear",
39
+ "confirmation_dialog",
40
+ "alert",
41
+ "safe_area_insets",
42
+ "tag",
43
+ "z_index",
44
+ "accessibility_id",
45
+ "disabled"
46
+ ],
47
+ "after_bag": [
48
+ "accessibility_identifier",
49
+ "disabled"
50
+ ],
51
+ "shared_slots": {
52
+ "padding": [
53
+ "padding",
54
+ "insets"
55
+ ]
56
+ }
57
+ }
@@ -10,7 +10,6 @@ export declare function categorizeCommonAttributes(commonAttrs: Record<string, a
10
10
  export declare const MODIFIER_ORDER: {
11
11
  description: string;
12
12
  swift: {
13
- order: string[];
14
13
  criticalRules: string[];
15
14
  };
16
15
  kotlin: {
@@ -94,32 +94,11 @@ export function categorizeCommonAttributes(commonAttrs) {
94
94
  // ----- modifier order (platform rendering) -------------------------------
95
95
  export const MODIFIER_ORDER = {
96
96
  description: "Platform-specific modifier application order. Order affects rendering.",
97
+ // Swift's order is not restated here: SpecLoader.getModifierOrder reads it
98
+ // from jsonui-cli's sjui_tools/lib/swiftui/views/modifier_order.json. The
99
+ // list that stood here had drifted from it (margins before offset, onClick
100
+ // after the margins). Only the prose rules stay.
97
101
  swift: {
98
- order: [
99
- "centerAlignment",
100
- "edgeAlignment",
101
- "padding",
102
- "frameConstraints (min/max)",
103
- "frameSize (width/height)",
104
- "insets/insetHorizontal",
105
- "background",
106
- "cornerRadius",
107
- "border",
108
- "margins",
109
- "alpha/opacity",
110
- "shadow",
111
- "clipping",
112
- "offset",
113
- "visibility (hidden/opacity ternary)",
114
- "safeAreaInsets",
115
- "disabled",
116
- "tag (for TabView)",
117
- "tintColor",
118
- "onClick/onTapGesture",
119
- "lifecycle (onAppear/onDisappear)",
120
- "confirmationDialog",
121
- "accessibilityIdentifier",
122
- ],
123
102
  criticalRules: [
124
103
  "background MUST come after padding (background includes padding area)",
125
104
  "border MUST come after cornerRadius (for rounded border)",
@@ -74,6 +74,7 @@ export interface DataSourceInfo {
74
74
  platformSemantics: FileInfo | null;
75
75
  coverage: FileInfo | null;
76
76
  typeSynonyms: FileInfo | null;
77
+ swiftModifierOrder: FileInfo | null;
77
78
  componentCount: number;
78
79
  commonAttributeCount: number;
79
80
  }
@@ -98,6 +99,7 @@ export declare class SpecLoader {
98
99
  private platformSemantics;
99
100
  private coverageGaps;
100
101
  private screenIdentity;
102
+ private swiftModifierOrder;
101
103
  private commonAttributes;
102
104
  private aliasMap;
103
105
  private typeSynonyms;
@@ -136,7 +138,17 @@ export declare class SpecLoader {
136
138
  searchComponents(query: string): any[];
137
139
  /** Keyword-indexed topical guides, each backed by a dedicated MCP tool. */
138
140
  private topicGuides;
141
+ /**
142
+ * The Swift order is read from jsonui-cli's modifier_order.json, the order
143
+ * sjui codegen emits and SwiftJsonUI's Dynamic chain is tested against. It
144
+ * was a hand-written list here (derived.ts), and it had drifted: margins
145
+ * before offset, onClick after the margins — while jsonui-cli 1.9.20 puts
146
+ * the tap inside the offset and the margins (ticket mcp-get-modifier-order-
147
+ * swift-is-a-hand-written-list-that-drifted). Kotlin and React have no such
148
+ * file in jsonui-cli; they stay hand-written and say so (`source`).
149
+ */
139
150
  getModifierOrder(platform?: string): any;
151
+ private swiftModifierOrderEntry;
140
152
  /**
141
153
  * The canonical screen-identity asset, or null when this checkout predates
142
154
  * it. Served verbatim: it is a declaration, not something to restate.
@@ -57,6 +57,7 @@ export class SpecLoader {
57
57
  /// the note explains why (backlog, alias normalization, runtime-only, ...).
58
58
  coverageGaps = null;
59
59
  screenIdentity = null;
60
+ swiftModifierOrder = null;
60
61
  commonAttributes = null;
61
62
  aliasMap = new Map();
62
63
  /// Lower-cased spelling -> its entry in type_synonyms.json, for the
@@ -274,10 +275,45 @@ export class SpecLoader {
274
275
  }
275
276
  return guides;
276
277
  }
278
+ /**
279
+ * The Swift order is read from jsonui-cli's modifier_order.json, the order
280
+ * sjui codegen emits and SwiftJsonUI's Dynamic chain is tested against. It
281
+ * was a hand-written list here (derived.ts), and it had drifted: margins
282
+ * before offset, onClick after the margins — while jsonui-cli 1.9.20 puts
283
+ * the tap inside the offset and the margins (ticket mcp-get-modifier-order-
284
+ * swift-is-a-hand-written-list-that-drifted). Kotlin and React have no such
285
+ * file in jsonui-cli; they stay hand-written and say so (`source`).
286
+ */
277
287
  getModifierOrder(platform) {
288
+ const swift = this.swiftModifierOrderEntry();
289
+ const all = { ...MODIFIER_ORDER, swift };
290
+ for (const key of ["kotlin", "react"]) {
291
+ all[key] = { source: "hand-written in jsonui-mcp-server (src/data/derived.ts); jsonui-cli has no order file for it", ...MODIFIER_ORDER[key] };
292
+ }
278
293
  if (platform)
279
- return MODIFIER_ORDER[platform] || null;
280
- return MODIFIER_ORDER;
294
+ return all[platform] || null;
295
+ return all;
296
+ }
297
+ swiftModifierOrderEntry() {
298
+ const doc = this.swiftModifierOrder;
299
+ const file = this.dataSource?.swiftModifierOrder;
300
+ if (!doc || !file || !Array.isArray(doc.bag)) {
301
+ return {
302
+ source: "unavailable: sjui_tools/lib/swiftui/views/modifier_order.json did not resolve",
303
+ order: null,
304
+ };
305
+ }
306
+ const afterBag = Array.isArray(doc.after_bag) ? doc.after_bag : [];
307
+ return {
308
+ source: `jsonui-cli sjui_tools/lib/swiftui/views/modifier_order.json (${file.layer}: ${file.path})`,
309
+ // The bag in the order it is written, then what is written after it.
310
+ order: [...doc.bag, ...afterBag.map((k) => `${k} (after the bag)`)],
311
+ bag: doc.bag,
312
+ afterBag,
313
+ sharedSlots: doc.shared_slots ?? {},
314
+ about: doc._about ?? [],
315
+ criticalRules: MODIFIER_ORDER.swift?.criticalRules ?? [],
316
+ };
281
317
  }
282
318
  /**
283
319
  * The canonical screen-identity asset, or null when this checkout predates
@@ -311,6 +347,7 @@ export class SpecLoader {
311
347
  this.dataSource.platformSemantics,
312
348
  this.dataSource.coverage,
313
349
  this.dataSource.typeSynonyms,
350
+ this.dataSource.swiftModifierOrder,
314
351
  ].filter((f) => f != null);
315
352
  const changed = [];
316
353
  for (const file of tracked) {
@@ -480,6 +517,17 @@ export class SpecLoader {
480
517
  componentCount++;
481
518
  }
482
519
  const typeSynonymsResolution = this.loadTypeSynonyms();
520
+ // sjui's SwiftUI modifier order (get_modifier_order's Swift answer).
521
+ // Optional for the same backward-compat reason.
522
+ let swiftModifierOrderResolution = null;
523
+ try {
524
+ const orderResolution = this.resolveFile("sjui_tools/lib/swiftui/views/modifier_order.json", "data/modifier_order.json");
525
+ this.swiftModifierOrder = JSON.parse(readFileSync(orderResolution.path, "utf-8"));
526
+ swiftModifierOrderResolution = orderResolution;
527
+ }
528
+ catch {
529
+ this.swiftModifierOrder = null;
530
+ }
483
531
  this.dataSource = {
484
532
  attributeDefinitions: attrResolution,
485
533
  componentMetadata: metaResolution,
@@ -489,6 +537,7 @@ export class SpecLoader {
489
537
  platformSemantics: platformSemanticsResolution,
490
538
  coverage: coverageResolution,
491
539
  typeSynonyms: typeSynonymsResolution,
540
+ swiftModifierOrder: swiftModifierOrderResolution,
492
541
  componentCount,
493
542
  commonAttributeCount: Object.keys(this.commonAttributesRaw).filter((k) => !k.startsWith("_")).length,
494
543
  };
package/docs/design.md CHANGED
@@ -74,7 +74,11 @@ jsonui-cli files — it encodes fixed cross-platform conventions:
74
74
  lifecycle / accessibility / binding buckets for the lookup tools.
75
75
 
76
76
  These are served directly by the `get_modifier_order`,
77
- `get_binding_rules`, and `get_platform_mapping` tools.
77
+ `get_binding_rules`, and `get_platform_mapping` tools — except Swift's
78
+ modifier order, which `get_modifier_order` reads from jsonui-cli's
79
+ `sjui_tools/lib/swiftui/views/modifier_order.json` (bundled as
80
+ `data/modifier_order.json`): the hand-written list that stood in
81
+ `MODIFIER_ORDER.swift` had drifted from what sjui emits.
78
82
 
79
83
  ---
80
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jui-tools-mcp-server",
3
- "version": "2.15.4",
3
+ "version": "2.15.7",
4
4
  "description": "MCP server for JsonUI project management - wraps jui and jsonui-doc CLIs plus component spec lookup",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -44,7 +44,7 @@ const DATA_DIR = join(PROJECT_ROOT, "data");
44
44
  // 3. `npm run fetch-definitions` and commit the data/ diff together with
45
45
  // this pin in one commit
46
46
  // JSONUI_CLI_RAW_BASE still overrides the whole base URL for manual testing.
47
- const JSONUI_CLI_TAG = "v1.9.16";
47
+ const JSONUI_CLI_TAG = "v1.9.26";
48
48
  const BASE_URL =
49
49
  process.env.JSONUI_CLI_RAW_BASE ||
50
50
  `https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/${JSONUI_CLI_TAG}`;
@@ -65,6 +65,12 @@ export const FILES = [
65
65
  remote: "shared/core/type_synonyms.json",
66
66
  local: "type_synonyms.json",
67
67
  },
68
+ // sjui's SwiftUI modifier order: get_modifier_order serves its Swift
69
+ // answer from it rather than from a hand-kept list.
70
+ {
71
+ remote: "sjui_tools/lib/swiftui/views/modifier_order.json",
72
+ local: "modifier_order.json",
73
+ },
68
74
  {
69
75
  remote: "conformance/coverage.json",
70
76
  local: "coverage.json",
@@ -100,32 +100,11 @@ export function categorizeCommonAttributes(commonAttrs: Record<string, any>): an
100
100
  export const MODIFIER_ORDER = {
101
101
  description:
102
102
  "Platform-specific modifier application order. Order affects rendering.",
103
+ // Swift's order is not restated here: SpecLoader.getModifierOrder reads it
104
+ // from jsonui-cli's sjui_tools/lib/swiftui/views/modifier_order.json. The
105
+ // list that stood here had drifted from it (margins before offset, onClick
106
+ // after the margins). Only the prose rules stay.
103
107
  swift: {
104
- order: [
105
- "centerAlignment",
106
- "edgeAlignment",
107
- "padding",
108
- "frameConstraints (min/max)",
109
- "frameSize (width/height)",
110
- "insets/insetHorizontal",
111
- "background",
112
- "cornerRadius",
113
- "border",
114
- "margins",
115
- "alpha/opacity",
116
- "shadow",
117
- "clipping",
118
- "offset",
119
- "visibility (hidden/opacity ternary)",
120
- "safeAreaInsets",
121
- "disabled",
122
- "tag (for TabView)",
123
- "tintColor",
124
- "onClick/onTapGesture",
125
- "lifecycle (onAppear/onDisappear)",
126
- "confirmationDialog",
127
- "accessibilityIdentifier",
128
- ],
129
108
  criticalRules: [
130
109
  "background MUST come after padding (background includes padding area)",
131
110
  "border MUST come after cornerRadius (for rounded border)",
@@ -126,6 +126,11 @@ export interface DataSourceInfo {
126
126
  /// section they are not (ProgressBar, HStack, Text …). Optional: absent on
127
127
  /// jsonui-cli before 1.9.0.
128
128
  typeSynonyms: FileInfo | null;
129
+ /// sjui_tools/lib/swiftui/views/modifier_order.json — the order sjui
130
+ /// codegen emits SwiftUI modifiers in, which SwiftJsonUI's Dynamic chain
131
+ /// is held to. get_modifier_order serves its Swift order from it. Optional:
132
+ /// absent on jsonui-cli before 1.9.x.
133
+ swiftModifierOrder: FileInfo | null;
129
134
  componentCount: number;
130
135
  commonAttributeCount: number;
131
136
  }
@@ -170,6 +175,7 @@ export class SpecLoader {
170
175
  /// the note explains why (backlog, alias normalization, runtime-only, ...).
171
176
  private coverageGaps: Map<string, any[]> | null = null;
172
177
  private screenIdentity: Record<string, any> | null = null;
178
+ private swiftModifierOrder: Record<string, any> | null = null;
173
179
  private commonAttributes: any = null;
174
180
  private aliasMap: Map<string, string> = new Map();
175
181
  /// Lower-cased spelling -> its entry in type_synonyms.json, for the
@@ -420,9 +426,45 @@ export class SpecLoader {
420
426
  return guides;
421
427
  }
422
428
 
429
+ /**
430
+ * The Swift order is read from jsonui-cli's modifier_order.json, the order
431
+ * sjui codegen emits and SwiftJsonUI's Dynamic chain is tested against. It
432
+ * was a hand-written list here (derived.ts), and it had drifted: margins
433
+ * before offset, onClick after the margins — while jsonui-cli 1.9.20 puts
434
+ * the tap inside the offset and the margins (ticket mcp-get-modifier-order-
435
+ * swift-is-a-hand-written-list-that-drifted). Kotlin and React have no such
436
+ * file in jsonui-cli; they stay hand-written and say so (`source`).
437
+ */
423
438
  getModifierOrder(platform?: string): any {
424
- if (platform) return (MODIFIER_ORDER as any)[platform] || null;
425
- return MODIFIER_ORDER;
439
+ const swift = this.swiftModifierOrderEntry();
440
+ const all: Record<string, any> = { ...MODIFIER_ORDER, swift };
441
+ for (const key of ["kotlin", "react"]) {
442
+ all[key] = { source: "hand-written in jsonui-mcp-server (src/data/derived.ts); jsonui-cli has no order file for it", ...(MODIFIER_ORDER as any)[key] };
443
+ }
444
+ if (platform) return all[platform] || null;
445
+ return all;
446
+ }
447
+
448
+ private swiftModifierOrderEntry(): Record<string, any> {
449
+ const doc = this.swiftModifierOrder;
450
+ const file = this.dataSource?.swiftModifierOrder;
451
+ if (!doc || !file || !Array.isArray(doc.bag)) {
452
+ return {
453
+ source: "unavailable: sjui_tools/lib/swiftui/views/modifier_order.json did not resolve",
454
+ order: null,
455
+ };
456
+ }
457
+ const afterBag: string[] = Array.isArray(doc.after_bag) ? doc.after_bag : [];
458
+ return {
459
+ source: `jsonui-cli sjui_tools/lib/swiftui/views/modifier_order.json (${file.layer}: ${file.path})`,
460
+ // The bag in the order it is written, then what is written after it.
461
+ order: [...doc.bag, ...afterBag.map((k: string) => `${k} (after the bag)`)],
462
+ bag: doc.bag,
463
+ afterBag,
464
+ sharedSlots: doc.shared_slots ?? {},
465
+ about: doc._about ?? [],
466
+ criticalRules: (MODIFIER_ORDER as any).swift?.criticalRules ?? [],
467
+ };
426
468
  }
427
469
 
428
470
  /**
@@ -458,6 +500,7 @@ export class SpecLoader {
458
500
  this.dataSource.platformSemantics,
459
501
  this.dataSource.coverage,
460
502
  this.dataSource.typeSynonyms,
503
+ this.dataSource.swiftModifierOrder,
461
504
  ].filter((f): f is FileInfo => f != null);
462
505
 
463
506
  const changed: string[] = [];
@@ -673,6 +716,22 @@ export class SpecLoader {
673
716
 
674
717
  const typeSynonymsResolution = this.loadTypeSynonyms();
675
718
 
719
+ // sjui's SwiftUI modifier order (get_modifier_order's Swift answer).
720
+ // Optional for the same backward-compat reason.
721
+ let swiftModifierOrderResolution: FileInfo | null = null;
722
+ try {
723
+ const orderResolution = this.resolveFile(
724
+ "sjui_tools/lib/swiftui/views/modifier_order.json",
725
+ "data/modifier_order.json"
726
+ );
727
+ this.swiftModifierOrder = JSON.parse(
728
+ readFileSync(orderResolution.path, "utf-8")
729
+ ) as Record<string, any>;
730
+ swiftModifierOrderResolution = orderResolution;
731
+ } catch {
732
+ this.swiftModifierOrder = null;
733
+ }
734
+
676
735
  this.dataSource = {
677
736
  attributeDefinitions: attrResolution,
678
737
  componentMetadata: metaResolution,
@@ -682,6 +741,7 @@ export class SpecLoader {
682
741
  platformSemantics: platformSemanticsResolution,
683
742
  coverage: coverageResolution,
684
743
  typeSynonyms: typeSynonymsResolution,
744
+ swiftModifierOrder: swiftModifierOrderResolution,
685
745
  componentCount,
686
746
  commonAttributeCount: Object.keys(this.commonAttributesRaw).filter(
687
747
  (k) => !k.startsWith("_")
@@ -101,31 +101,6 @@ exports[`derived constants (pinned) > MODIFIER_ORDER snapshot 1`] = `
101
101
  "Image: .resizable() must come first, then .aspectRatio",
102
102
  "Label: .leading alignment added to frame for textAlign to work",
103
103
  ],
104
- "order": [
105
- "centerAlignment",
106
- "edgeAlignment",
107
- "padding",
108
- "frameConstraints (min/max)",
109
- "frameSize (width/height)",
110
- "insets/insetHorizontal",
111
- "background",
112
- "cornerRadius",
113
- "border",
114
- "margins",
115
- "alpha/opacity",
116
- "shadow",
117
- "clipping",
118
- "offset",
119
- "visibility (hidden/opacity ternary)",
120
- "safeAreaInsets",
121
- "disabled",
122
- "tag (for TabView)",
123
- "tintColor",
124
- "onClick/onTapGesture",
125
- "lifecycle (onAppear/onDisappear)",
126
- "confirmationDialog",
127
- "accessibilityIdentifier",
128
- ],
129
104
  },
130
105
  }
131
106
  `;
@@ -78,7 +78,10 @@ describe("derived constants (pinned)", () => {
78
78
  "react",
79
79
  "swift",
80
80
  ]);
81
- expect(MODIFIER_ORDER.swift.order.length).toBeGreaterThan(0);
81
+ // Swift's order is read from jsonui-cli's modifier_order.json
82
+ // (SpecLoader.getModifierOrder); a list here is the drift that ticket
83
+ // mcp-get-modifier-order-swift-is-a-hand-written-list-that-drifted removed.
84
+ expect((MODIFIER_ORDER.swift as Record<string, unknown>).order).toBeUndefined();
82
85
  expect(MODIFIER_ORDER.swift.criticalRules.length).toBeGreaterThan(0);
83
86
  expect(MODIFIER_ORDER.kotlin.order.length).toBeGreaterThan(0);
84
87
  });
package/tests/helpers.ts CHANGED
@@ -108,8 +108,19 @@ export interface CliDatasetOptions {
108
108
  omitComponentMetadata?: boolean;
109
109
  /** Optional conformance/coverage.json fixture (implementation-gap ledger). */
110
110
  coverage?: Record<string, unknown>;
111
+ /** Optional sjui_tools/lib/swiftui/views/modifier_order.json fixture. */
112
+ modifierOrder?: Record<string, unknown>;
111
113
  }
112
114
 
115
+ /** A modifier order whose slot names exist nowhere else: an answer that
116
+ * carries them came from the file, not from a list kept in the server. */
117
+ export const FIXTURE_MODIFIER_ORDER = {
118
+ _about: ["test fixture — not real data"],
119
+ bag: ["fixture_slot_alpha", "fixture_slot_beta", "fixture_slot_gamma"],
120
+ after_bag: ["fixture_after_one"],
121
+ shared_slots: { fixture_slot_alpha: ["fixture_slot_alpha", "fixture_extra"] },
122
+ };
123
+
113
124
  /** Coverage-ledger fixture: one gap on Label.fontSize [web] + one on common.onClick [android]. */
114
125
  export const FIXTURE_COVERAGE = {
115
126
  schemaVersion: 1,
@@ -149,6 +160,9 @@ export function makeCliDataset(root: string, opts: CliDatasetOptions = {}): void
149
160
  if (opts.coverage) {
150
161
  writeJson(join(root, "conformance", "coverage.json"), opts.coverage);
151
162
  }
163
+ if (opts.modifierOrder) {
164
+ writeJson(join(root, "sjui_tools", "lib", "swiftui", "views", "modifier_order.json"), opts.modifierOrder);
165
+ }
152
166
  }
153
167
 
154
168
  /** Write a fixture bundled snapshot layout: <mcpRoot>/data/*.json */
@@ -166,6 +180,9 @@ export function makeBundledDataset(root: string, opts: CliDatasetOptions = {}):
166
180
  opts.componentMetadata ?? FIXTURE_COMPONENT_METADATA
167
181
  );
168
182
  }
183
+ if (opts.modifierOrder) {
184
+ writeJson(join(dataDir, "modifier_order.json"), opts.modifierOrder);
185
+ }
169
186
  }
170
187
 
171
188
  // ----- fixture JsonUI project ---------------------------------------------
@@ -48,6 +48,7 @@ function reportedFiles(source: DataSourceInfo) {
48
48
  source.platformSemantics,
49
49
  source.coverage,
50
50
  source.typeSynonyms,
51
+ source.swiftModifierOrder,
51
52
  ].filter((f): f is NonNullable<typeof f> => f != null);
52
53
  }
53
54
 
@@ -23,6 +23,7 @@ import {
23
23
  import {
24
24
  cleanupTempDirs,
25
25
  createToolHarness,
26
+ FIXTURE_MODIFIER_ORDER,
26
27
  makeBundledDataset,
27
28
  makeTempDir,
28
29
  type ToolHarness,
@@ -42,9 +43,11 @@ function buildHarness(loader: SpecLoader): ToolHarness {
42
43
  return harness;
43
44
  }
44
45
 
45
- function makeFixtureLoader(opts: { staleDays?: number } = {}): SpecLoader {
46
+ function makeFixtureLoader(opts: { staleDays?: number; modifierOrder?: Record<string, unknown> | null } = {}): SpecLoader {
46
47
  const mcpRoot = makeTempDir("mcp-root");
47
- makeBundledDataset(mcpRoot);
48
+ makeBundledDataset(mcpRoot, {
49
+ modifierOrder: opts.modifierOrder === null ? undefined : (opts.modifierOrder ?? FIXTURE_MODIFIER_ORDER),
50
+ });
48
51
  if (opts.staleDays) {
49
52
  const when = new Date(Date.now() - opts.staleDays * 24 * 60 * 60 * 1000);
50
53
  for (const f of ["attribute_definitions.json", "component_metadata.json"]) {
@@ -156,16 +159,43 @@ describe("search_components", () => {
156
159
  });
157
160
 
158
161
  describe("get_modifier_order", () => {
159
- it("returns the full table when platform is omitted", async () => {
160
- const result = JSON.parse(await harness.call("get_modifier_order"));
161
- expect(result).toEqual(JSON.parse(JSON.stringify(MODIFIER_ORDER)));
162
+ // The Swift order is read from jsonui-cli's modifier_order.json. The
163
+ // fixture's slot names exist nowhere else, so an answer carrying them came
164
+ // from the file — a list kept in the server cannot pass (ticket mcp-get-
165
+ // modifier-order-swift-is-a-hand-written-list-that-drifted).
166
+ it("serves the Swift order from modifier_order.json: the bag, then what follows it", async () => {
167
+ const result = JSON.parse(
168
+ await harness.call("get_modifier_order", { platform: "swift" })
169
+ );
170
+ expect(result.bag).toEqual(FIXTURE_MODIFIER_ORDER.bag);
171
+ expect(result.afterBag).toEqual(FIXTURE_MODIFIER_ORDER.after_bag);
172
+ expect(result.order).toEqual([
173
+ ...FIXTURE_MODIFIER_ORDER.bag,
174
+ "fixture_after_one (after the bag)",
175
+ ]);
176
+ expect(result.sharedSlots).toEqual(FIXTURE_MODIFIER_ORDER.shared_slots);
177
+ expect(result.source).toContain("sjui_tools/lib/swiftui/views/modifier_order.json");
178
+ expect(result.criticalRules).toEqual(MODIFIER_ORDER.swift.criticalRules);
162
179
  });
163
180
 
164
- it("returns a single platform section", async () => {
181
+ it("says the Swift order is unavailable when the file does not resolve, and invents none", async () => {
182
+ harness = buildHarness(makeFixtureLoader({ modifierOrder: null }));
165
183
  const result = JSON.parse(
166
184
  await harness.call("get_modifier_order", { platform: "swift" })
167
185
  );
168
- expect(result).toEqual(JSON.parse(JSON.stringify(MODIFIER_ORDER.swift)));
186
+ expect(result.order).toBeNull();
187
+ expect(result.source).toMatch(/^unavailable/);
188
+ });
189
+
190
+ it("returns the full table when platform is omitted, Kotlin and React marked hand-written", async () => {
191
+ const result = JSON.parse(await harness.call("get_modifier_order"));
192
+ expect(Object.keys(result).sort()).toEqual(["description", "kotlin", "react", "swift"]);
193
+ expect(result.swift.bag).toEqual(FIXTURE_MODIFIER_ORDER.bag);
194
+ for (const key of ["kotlin", "react"] as const) {
195
+ expect(result[key].source).toMatch(/^hand-written/);
196
+ const { source, ...rest } = result[key];
197
+ expect(rest).toEqual(JSON.parse(JSON.stringify(MODIFIER_ORDER[key])));
198
+ }
169
199
  });
170
200
 
171
201
  it("rejects unknown platforms at the schema layer", async () => {