jui-tools-mcp-server 2.12.10 → 2.14.0
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 +7 -4
- package/data/attribute_definitions.json +124 -74
- package/data/attribute_semantics.json +18 -1
- package/data/component_metadata.json +36 -7
- package/data/type_synonyms.json +167 -0
- package/dist/cli_runner.js +7 -1
- package/dist/index.js +4 -1
- package/dist/spec_loader.d.ts +25 -0
- package/dist/spec_loader.js +71 -0
- package/dist/tools/jui/jui_generate_converter.js +20 -5
- package/dist/tools/jui/jui_generate_project.js +11 -2
- package/dist/tools/spec/get_data_source.js +5 -2
- package/dist/tools/test/test_contracts_coverage.d.ts +10 -0
- package/dist/tools/test/test_contracts_coverage.js +95 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +21 -0
- package/package.json +2 -1
- package/scripts/fetch-definitions.js +1 -1
- package/scripts/fetch-manifest.js +6 -0
- package/src/cli_runner.ts +7 -1
- package/src/index.ts +4 -1
- package/src/spec_loader.ts +85 -0
- package/src/tools/jui/jui_generate_converter.ts +10 -2
- package/src/tools/jui/jui_generate_project.ts +13 -2
- package/src/tools/spec/get_data_source.ts +5 -2
- package/src/tools/test/test_contracts_coverage.ts +115 -0
- package/src/version.ts +22 -0
- package/tests/cli_runner.test.ts +38 -1
- package/tests/server_version.test.ts +51 -0
- package/tests/snapshot_completeness.test.ts +1 -0
- package/tests/staleness_is_content_not_mtime.test.ts +1 -1
- package/tests/tools_cli.test.ts +43 -0
- package/tests/tools_test.test.ts +103 -5
- package/tests/type_synonyms.test.ts +178 -0
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"omittedEntirely": "same as the default: top | start. Omitting gravity must render identically to naming the default explicitly",
|
|
86
86
|
"leafOwnFrameChannel": {
|
|
87
87
|
"default": "center",
|
|
88
|
-
"ruling": "2026-08-08 adjudication (gravity leaf-scope hotfix, v1.5.2/10.15.1): the container default above scopes to CONTAINERS — nodes whose content the framework itself lays out. A childless node's own-frame content (a fit image's bitmap inside its declared box, a custom component's internals, a text block smaller than its fixed box) is a SEPARATE channel whose default is Center: Compose's Image/extension alignment default, which kjui never overrides, and the position the android face measurably draws a shrunken autoShrink line in its fixed box. ios converged in SwiftJsonUI 245d059+4d41bf9 (codegen gravity_to_frame_alignment leaf scope; dynamic fixed-frame text drop). A DECLARED gravity or textAlign still applies to any node — this entry names only the omitted-spelling default. Implementations were aligned BEFORE this text landed; the entry records the ruling the code already obeys."
|
|
88
|
+
"ruling": "2026-08-08 adjudication (gravity leaf-scope hotfix, v1.5.2/10.15.1): the container default above scopes to CONTAINERS — nodes whose content the framework itself lays out. A childless node's own-frame content (a fit image's bitmap inside its declared box, a custom component's internals, a text block smaller than its fixed box) is a SEPARATE channel whose default is Center: Compose's Image/extension alignment default, which kjui never overrides, and the position the android face measurably draws a shrunken autoShrink line in its fixed box. ios converged in SwiftJsonUI 245d059+4d41bf9 (codegen gravity_to_frame_alignment leaf scope; dynamic fixed-frame text drop). A DECLARED gravity or textAlign still applies to any node — this entry names only the omitted-spelling default. Implementations were aligned BEFORE this text landed; the entry records the ruling the code already obeys. 2026-09-27 (4f ruling, round 17): a Label's text in a frame taller than it (a numeric, matchParent or minHeight height, or a weight along its column) had not been: Compose drew it at the frame's top whatever the gravity — kjui for a numeric height, KotlinJsonUI Dynamic for every frame. From jsonui-cli 1.9.0 both Compose paths place it by the gravity's vertical part (top, bottom, the centre for center / centerVertical) and in the centre when the gravity names none, inside the Label's background and padding, as iOS does (sjui 19bf7b94, SwiftJsonUI f776b88)."
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"shadow": {
|
|
@@ -352,6 +352,7 @@
|
|
|
352
352
|
],
|
|
353
353
|
"noOpWhereNoSeparators": "a container that draws no separators satisfies hideSeparator vacuously — that is a no-op by contract, not an unimplemented gap",
|
|
354
354
|
"orthogonality": "listStyle picks the chrome, hideSeparator hides the separators; neither overrides the other",
|
|
355
|
+
"update_2026_09_26": "The ruling below describes 2026-08-07. Two of its facts have moved: TableConverter no longer exists (Table, List and ListView draw as Collection — shared/core/type_synonyms.json), and the attribute descriptions no longer say which faces read these attributes or that the coverage rows are runtime-only — which faces implement them is the coverage ledger's to say, measured, not a declaration's. The contract itself (the no-op where there are no separators, the orthogonality, the vocabulary) is unchanged.",
|
|
355
356
|
"ruling": "2026-08-07 (plan 51-E), raised by B: the ios probe path is CollectionStackView, not SwiftUI's List, and a container that is not a list has no separators to hide — so what should `hideSeparator` draw there? NOTHING, and that is the contract, not a gap. 'Hide the separators' is vacuously satisfied where there are none. The scope is deliberately NOT narrowed: the axis here is the container's rendered SHAPE (Collection's own `layout`, `columns` and `orientation` decide whether it is list-shaped), and `platform` / `mode` cannot express that — narrowing by platform would say something false. More importantly, 'implement it' must NOT mean 'switch the container to a List': an attribute that reshapes the thing it modifies is the defect family this wave exists to remove. hideSeparator modifies a list; it does not create one. MEASUREMENT CONSEQUENCE, so the ledgers do not mis-file it: the fixture must use a list-shaped Collection to observe this at all. Inert on a non-list container is the CONTRACT being honoured, and the coverage rows for both attributes are `runtime-only` (ios dynamic implements them; no codegen face does) rather than `unimplemented`. listStyle: enumerated here from the only implementation that reads it (SwiftJsonUI TableConverter.applyListStyle) as plain / grouped / insetGrouped / sidebar with plain the default — B was right that a bare string could not be validated or discriminated, and B should not replace the hardcoded PlainListStyle() until the vocabulary exists, which it now does. THE TWO ARE ORTHOGONAL. TableConverter.swift:45-49 currently lets `hideSeparator: true` win and forces .plain, which makes a declared `listStyle: \"grouped\"` silently inert whenever separators are hidden — the same silently-inert-declaration shape again. `.listRowSeparator(.hidden)` applies to any list style, so there is no need to force plain. Owner: F.",
|
|
356
357
|
"observable": {}
|
|
357
358
|
},
|
|
@@ -378,6 +379,22 @@
|
|
|
378
379
|
"defaultRange": "0 .. 1",
|
|
379
380
|
"ruling": "2026-08-07 (plan 51-E), raised by E2 with all three converters measured: none of them had a declared default to read, so each invented one — sjui `max || 1`, kjui and rjui `max || 100`. A layout writing `value: 0.5` therefore sits mid-track on ios and at half a percent on android and web. IOS IS CANON: the default range is 0 .. 1. The ground is this SSoT's own convention for a unitless value on a track, not a head count. `Progress.progress` declares `min: 0, max: 1` and `common.opacity` declares `min: 0, max: 1`; Progress is the Slider's closest sibling — a number along a track — and 100 would make Slider the only such attribute in the SSoT that is not normalised. It also keeps the round-trip coherent: `Slider.value` is declared two-way, so under a 0..100 default the control writes 0..100 back into a viewmodel whose author was writing 0..1. Declared as `default` on minimum and maximum so no converter has to invent it again; `range` is the [min, max] shorthand, so its absence means [0, 1] too. CONSEQUENCE FOR WEB, so that fixing the range does not break the control: `Slider.step` is declared platform:react with no default, and E2 measured web rounding 0.5 up to 1 under the old 0..100 range. With a 0..1 range an implicit step of 1 collapses the slider to two positions, so the web default step must be continuous (HTML `step=\"any\"`) unless the layout declares one. Owner: A for the step, A and C for the range.",
|
|
380
381
|
"observable": {}
|
|
382
|
+
},
|
|
383
|
+
"collectionSpacing": {
|
|
384
|
+
"attributes": [
|
|
385
|
+
"Collection.lineSpacing",
|
|
386
|
+
"Collection.columnSpacing",
|
|
387
|
+
"Collection.itemSpacing"
|
|
388
|
+
],
|
|
389
|
+
"undeclaredDefault": "0 on every route (vertical list and stack, grid, horizontal lanes and pages, flow) and on every path (sjui, kjui and rjui codegen; SwiftJsonUI and KotlinJsonUI Dynamic). A declared value is drawn as declared, 0 included.",
|
|
390
|
+
"chains": {
|
|
391
|
+
"vertical": "between rows: lineSpacing, else itemSpacing, else 0",
|
|
392
|
+
"grid": "between rows: lineSpacing, else itemSpacing, else 0; between columns: columnSpacing, else itemSpacing, else 0 — columnSpacing never spaces the rows",
|
|
393
|
+
"horizontal": "along the scroll axis, pages included: lineSpacing, else itemSpacing, else 0; between lanes: columnSpacing, else itemSpacing, else 0",
|
|
394
|
+
"flow": "between the cells of a line: columnSpacing, else itemSpacing, else 0; between lines, and between the section blocks, each section its own wrap: lineSpacing, else itemSpacing, else 0"
|
|
395
|
+
},
|
|
396
|
+
"ruling": "2026-09-26 (4f, round 5 of the Collection audit). Until jsonui-cli 1.9.0 no declaration said what an undeclared Collection spacing draws, and the paths had each picked one: 0 everywhere except the flow route on iOS — sjui codegen 8 between cells, lines and section blocks; SwiftJsonUI Dynamic 8 on the lazy flow (its section blocks the ScrollView's own stack spacing) and `x > 0 ? x : 8` on the `lazy: none` flow, so a declared 0 drew 8. Android and web drew 0. The ruling: 0, flow included, on every route and every path. Declared HERE and not as a `default` in attribute_definitions.json, on purpose: a `default` there changes the attribute's kind — the conformance fixture generator writes the declared default as the fixture's value, the inert audit lists it, and attr codegen carries it into the generated tables — and the rule is per route, which one scalar cannot say. `sectionSpacing` is lineSpacing's alias (definitions `aliases`). Toolchain comments point here.",
|
|
397
|
+
"observable": {}
|
|
381
398
|
}
|
|
382
399
|
}
|
|
383
400
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"_description": "Per-component presentation metadata consumed by jsonui-mcp-server. Augments attribute_definitions.json (which defines the raw attribute schema, including binding_direction per attribute) with human descriptions, aliases, platform availability, platform-specific implementation notes, and agent-facing rules. Edit this file to change what `lookup_component` / `search_components` / `lookup_attribute` return from the MCP.",
|
|
3
3
|
"_schema": {
|
|
4
4
|
"description": "string",
|
|
5
|
+
"interactive": "boolean — REQUIRED on every type: whether a user can operate this component on its own (a control, an input, or a scrolling / embedding container). A tappable that holds one is not flattened into a single button for screen readers (shared/core/tap_accessibility.rb). Declared, never derived: Button, Radio and Web declare no two-way binding and are operable.",
|
|
5
6
|
"aliases": "string[]",
|
|
6
7
|
"platforms": {
|
|
7
8
|
"swift_generated": "boolean",
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
},
|
|
16
17
|
"Label": {
|
|
17
18
|
"description": "Text display component",
|
|
19
|
+
"interactive": false,
|
|
18
20
|
"aliases": [
|
|
19
21
|
"Text"
|
|
20
22
|
],
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
},
|
|
50
52
|
"TextField": {
|
|
51
53
|
"description": "Text input field",
|
|
54
|
+
"interactive": true,
|
|
52
55
|
"aliases": [
|
|
53
56
|
"EditText",
|
|
54
57
|
"Input"
|
|
@@ -86,6 +89,7 @@
|
|
|
86
89
|
},
|
|
87
90
|
"Button": {
|
|
88
91
|
"description": "Tappable button component",
|
|
92
|
+
"interactive": true,
|
|
89
93
|
"aliases": [],
|
|
90
94
|
"platforms": {
|
|
91
95
|
"swift_generated": true,
|
|
@@ -117,6 +121,7 @@
|
|
|
117
121
|
},
|
|
118
122
|
"Image": {
|
|
119
123
|
"description": "Local image component",
|
|
124
|
+
"interactive": false,
|
|
120
125
|
"aliases": [
|
|
121
126
|
"CircleImage",
|
|
122
127
|
"CircleImageView",
|
|
@@ -153,6 +158,7 @@
|
|
|
153
158
|
},
|
|
154
159
|
"NetworkImage": {
|
|
155
160
|
"description": "Remote/network image component",
|
|
161
|
+
"interactive": false,
|
|
156
162
|
"aliases": [],
|
|
157
163
|
"platforms": {
|
|
158
164
|
"swift_generated": true,
|
|
@@ -174,12 +180,12 @@
|
|
|
174
180
|
}
|
|
175
181
|
},
|
|
176
182
|
"rules": [
|
|
177
|
-
"Default contentMode is .center for matchParent (Swift)",
|
|
178
183
|
"Supports placeholder/error images via defaultImage"
|
|
179
184
|
]
|
|
180
185
|
},
|
|
181
186
|
"View": {
|
|
182
187
|
"description": "Container view (HStack/VStack/ZStack)",
|
|
188
|
+
"interactive": false,
|
|
183
189
|
"aliases": [],
|
|
184
190
|
"platforms": {
|
|
185
191
|
"swift_generated": true,
|
|
@@ -212,6 +218,7 @@
|
|
|
212
218
|
},
|
|
213
219
|
"ScrollView": {
|
|
214
220
|
"description": "Scrollable container",
|
|
221
|
+
"interactive": true,
|
|
215
222
|
"aliases": [],
|
|
216
223
|
"platforms": {
|
|
217
224
|
"swift_generated": true,
|
|
@@ -238,6 +245,7 @@
|
|
|
238
245
|
},
|
|
239
246
|
"Collection": {
|
|
240
247
|
"description": "List/grid collection with data binding",
|
|
248
|
+
"interactive": true,
|
|
241
249
|
"aliases": [
|
|
242
250
|
"Table",
|
|
243
251
|
"TableView",
|
|
@@ -270,11 +278,12 @@
|
|
|
270
278
|
"Sections support different cell types per section",
|
|
271
279
|
"cellIdProperty extracts unique keys from item data",
|
|
272
280
|
"scrollTo binding enables programmatic scrolling",
|
|
273
|
-
"lazy (default
|
|
281
|
+
"lazy (default 'lazy') is the outer container: 'lazy' virtualizes (LazyVStack/LazyColumn/LazyVerticalGrid with internal scroll); 'eager' draws every cell in a scroll container, with no virtualization; 'none' draws them with NO scroll container (use when nested in an already-scrollable parent). Sticky headers and paging require 'lazy'."
|
|
274
282
|
]
|
|
275
283
|
},
|
|
276
284
|
"Switch": {
|
|
277
285
|
"description": "Toggle switch (iOS style)",
|
|
286
|
+
"interactive": true,
|
|
278
287
|
"aliases": [],
|
|
279
288
|
"platforms": {
|
|
280
289
|
"swift_generated": true,
|
|
@@ -302,6 +311,7 @@
|
|
|
302
311
|
},
|
|
303
312
|
"Toggle": {
|
|
304
313
|
"description": "Toggle control (checkbox style)",
|
|
314
|
+
"interactive": true,
|
|
305
315
|
"aliases": [],
|
|
306
316
|
"platforms": {
|
|
307
317
|
"swift_generated": true,
|
|
@@ -328,6 +338,7 @@
|
|
|
328
338
|
},
|
|
329
339
|
"CheckBox": {
|
|
330
340
|
"description": "Checkbox input",
|
|
341
|
+
"interactive": true,
|
|
331
342
|
"aliases": [
|
|
332
343
|
"Check",
|
|
333
344
|
"Checkbox"
|
|
@@ -357,6 +368,7 @@
|
|
|
357
368
|
},
|
|
358
369
|
"Radio": {
|
|
359
370
|
"description": "Radio button group",
|
|
371
|
+
"interactive": true,
|
|
360
372
|
"aliases": [],
|
|
361
373
|
"platforms": {
|
|
362
374
|
"swift_generated": true,
|
|
@@ -382,6 +394,7 @@
|
|
|
382
394
|
},
|
|
383
395
|
"SelectBox": {
|
|
384
396
|
"description": "Dropdown/picker selection",
|
|
397
|
+
"interactive": true,
|
|
385
398
|
"aliases": [],
|
|
386
399
|
"platforms": {
|
|
387
400
|
"swift_generated": true,
|
|
@@ -407,6 +420,7 @@
|
|
|
407
420
|
},
|
|
408
421
|
"Segment": {
|
|
409
422
|
"description": "Segmented control",
|
|
423
|
+
"interactive": true,
|
|
410
424
|
"aliases": [],
|
|
411
425
|
"platforms": {
|
|
412
426
|
"swift_generated": true,
|
|
@@ -433,6 +447,7 @@
|
|
|
433
447
|
},
|
|
434
448
|
"Slider": {
|
|
435
449
|
"description": "Slider/range input",
|
|
450
|
+
"interactive": true,
|
|
436
451
|
"aliases": [],
|
|
437
452
|
"platforms": {
|
|
438
453
|
"swift_generated": true,
|
|
@@ -454,12 +469,13 @@
|
|
|
454
469
|
},
|
|
455
470
|
"rules": [
|
|
456
471
|
"value/bind is two-way binding",
|
|
457
|
-
"
|
|
472
|
+
"minimum / maximum define the range (default 0 and 1: with neither set the range is 0 .. 1); minimumValue / minValue and maximumValue / maxValue are alias spellings",
|
|
458
473
|
"step defines discrete steps"
|
|
459
474
|
]
|
|
460
475
|
},
|
|
461
476
|
"Progress": {
|
|
462
477
|
"description": "Progress bar/indicator",
|
|
478
|
+
"interactive": false,
|
|
463
479
|
"aliases": [],
|
|
464
480
|
"platforms": {
|
|
465
481
|
"swift_generated": true,
|
|
@@ -485,6 +501,7 @@
|
|
|
485
501
|
},
|
|
486
502
|
"Indicator": {
|
|
487
503
|
"description": "Activity indicator / loading spinner",
|
|
504
|
+
"interactive": false,
|
|
488
505
|
"aliases": [],
|
|
489
506
|
"platforms": {
|
|
490
507
|
"swift_generated": true,
|
|
@@ -508,11 +525,12 @@
|
|
|
508
525
|
},
|
|
509
526
|
"CircleView": {
|
|
510
527
|
"description": "Circular shape view",
|
|
528
|
+
"interactive": false,
|
|
511
529
|
"aliases": [],
|
|
512
530
|
"platforms": {
|
|
513
531
|
"swift_generated": false,
|
|
514
532
|
"swift_dynamic": false,
|
|
515
|
-
"kotlin_generated":
|
|
533
|
+
"kotlin_generated": true,
|
|
516
534
|
"kotlin_dynamic": true,
|
|
517
535
|
"react": true
|
|
518
536
|
},
|
|
@@ -531,6 +549,7 @@
|
|
|
531
549
|
},
|
|
532
550
|
"GradientView": {
|
|
533
551
|
"description": "Gradient background view",
|
|
552
|
+
"interactive": false,
|
|
534
553
|
"aliases": [],
|
|
535
554
|
"platforms": {
|
|
536
555
|
"swift_generated": true,
|
|
@@ -556,6 +575,7 @@
|
|
|
556
575
|
},
|
|
557
576
|
"Blur": {
|
|
558
577
|
"description": "Blur effect view",
|
|
578
|
+
"interactive": false,
|
|
559
579
|
"aliases": [],
|
|
560
580
|
"platforms": {
|
|
561
581
|
"swift_generated": true,
|
|
@@ -581,6 +601,7 @@
|
|
|
581
601
|
},
|
|
582
602
|
"IconLabel": {
|
|
583
603
|
"description": "Icon with label combination",
|
|
604
|
+
"interactive": false,
|
|
584
605
|
"aliases": [],
|
|
585
606
|
"platforms": {
|
|
586
607
|
"swift_generated": true,
|
|
@@ -606,6 +627,7 @@
|
|
|
606
627
|
},
|
|
607
628
|
"Web": {
|
|
608
629
|
"description": "Web view / embedded browser",
|
|
630
|
+
"interactive": true,
|
|
609
631
|
"aliases": [],
|
|
610
632
|
"platforms": {
|
|
611
633
|
"swift_generated": true,
|
|
@@ -626,11 +648,13 @@
|
|
|
626
648
|
}
|
|
627
649
|
},
|
|
628
650
|
"rules": [
|
|
629
|
-
"url attribute specifies the web page to load"
|
|
651
|
+
"url attribute specifies the web page to load",
|
|
652
|
+
"Error face + retry (iOS / Android only): bind onLoadFailed to a ViewModel method that hides the Web and shows the error, and bind reloadToken to an Int the retry method increments — the Web reloads its own url each time the value changes. Rewriting url to the same value does not reload."
|
|
630
653
|
]
|
|
631
654
|
},
|
|
632
655
|
"SafeAreaView": {
|
|
633
656
|
"description": "Safe area respecting container",
|
|
657
|
+
"interactive": false,
|
|
634
658
|
"aliases": [],
|
|
635
659
|
"platforms": {
|
|
636
660
|
"swift_generated": true,
|
|
@@ -653,6 +677,7 @@
|
|
|
653
677
|
},
|
|
654
678
|
"TabView": {
|
|
655
679
|
"description": "Tab-based navigation container",
|
|
680
|
+
"interactive": true,
|
|
656
681
|
"aliases": [],
|
|
657
682
|
"platforms": {
|
|
658
683
|
"swift_generated": true,
|
|
@@ -678,6 +703,7 @@
|
|
|
678
703
|
},
|
|
679
704
|
"TextView": {
|
|
680
705
|
"description": "Multi-line text input",
|
|
706
|
+
"interactive": true,
|
|
681
707
|
"aliases": [],
|
|
682
708
|
"platforms": {
|
|
683
709
|
"swift_generated": true,
|
|
@@ -704,11 +730,12 @@
|
|
|
704
730
|
},
|
|
705
731
|
"EditText": {
|
|
706
732
|
"description": "Text input (Android naming convention)",
|
|
733
|
+
"interactive": true,
|
|
707
734
|
"aliases": [],
|
|
708
735
|
"platforms": {
|
|
709
736
|
"swift_generated": true,
|
|
710
737
|
"swift_dynamic": true,
|
|
711
|
-
"kotlin_generated":
|
|
738
|
+
"kotlin_generated": true,
|
|
712
739
|
"kotlin_dynamic": true,
|
|
713
740
|
"react": true
|
|
714
741
|
},
|
|
@@ -724,11 +751,12 @@
|
|
|
724
751
|
},
|
|
725
752
|
"Input": {
|
|
726
753
|
"description": "Generic input (React naming convention)",
|
|
754
|
+
"interactive": true,
|
|
727
755
|
"aliases": [],
|
|
728
756
|
"platforms": {
|
|
729
757
|
"swift_generated": true,
|
|
730
758
|
"swift_dynamic": true,
|
|
731
|
-
"kotlin_generated":
|
|
759
|
+
"kotlin_generated": true,
|
|
732
760
|
"kotlin_dynamic": true,
|
|
733
761
|
"react": true
|
|
734
762
|
},
|
|
@@ -744,6 +772,7 @@
|
|
|
744
772
|
},
|
|
745
773
|
"Embed": {
|
|
746
774
|
"description": "Embeds another screen as a region of this layout. The embedded screen owns its own ViewModel — independent from the parent VM.",
|
|
775
|
+
"interactive": true,
|
|
747
776
|
"aliases": [],
|
|
748
777
|
"platforms": {
|
|
749
778
|
"swift_generated": true,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_description": [
|
|
3
|
+
"Type-spelling synonyms: a node `type` that is not a section of attribute_definitions.json, mapped to the section that declares it (`canonical`).",
|
|
4
|
+
"The one table. Validation and the renderers are to read it, and hold no list of their own; which of them do is measured by their own tests (the type_synonyms_draw specs, SwiftJsonUI's TypeSynonymsDrawTests), not recorded here. It does not apply to sjui's UIKit mode, a separate renderer with its own set of UIKit components and its own spellings for them (Table is its SJUITableView); that mode draws what it has.",
|
|
5
|
+
"A renderer draws a synonym as its canonical type — except where the entry names `render_as`: the spelling draws differently from its canonical type (CircleImage clips to a circle), and the renderer draws `render_as` with the converter of that name, while validation still reads the canonical section. A type an app registers as its own component is the app's, whatever its spelling, and is never rewritten.",
|
|
6
|
+
"Any other key of an entry is an attribute the spelling itself means, of the canonical section: HStack and Row are a View with `orientation: horizontal`, VStack and Column one with `vertical`. A renderer adds it when the node does not set it. When the node sets it otherwise (an HStack with `orientation: vertical`), the node's value is drawn and the renderer warns, naming both. ZStack and Box add nothing: a View without orientation is already the stack they mean.",
|
|
7
|
+
"Not here: EditText / Input / Check / Toggle. They are sections of attribute_definitions.json that point at their canonical section with `_alias_of`; the exact-match lookup finds them first. Spacer / Divider / Triangle are neither declared nor synonyms, and draw as undeclared types."
|
|
8
|
+
],
|
|
9
|
+
"synonyms": {
|
|
10
|
+
"Text": {
|
|
11
|
+
"canonical": "Label"
|
|
12
|
+
},
|
|
13
|
+
"MultiLineEditText": {
|
|
14
|
+
"canonical": "TextView"
|
|
15
|
+
},
|
|
16
|
+
"Textarea": {
|
|
17
|
+
"canonical": "TextView"
|
|
18
|
+
},
|
|
19
|
+
"ImageView": {
|
|
20
|
+
"canonical": "Image"
|
|
21
|
+
},
|
|
22
|
+
"Img": {
|
|
23
|
+
"canonical": "Image"
|
|
24
|
+
},
|
|
25
|
+
"NetworkImageView": {
|
|
26
|
+
"canonical": "NetworkImage"
|
|
27
|
+
},
|
|
28
|
+
"CircleImage": {
|
|
29
|
+
"canonical": "Image",
|
|
30
|
+
"render_as": "CircleImage"
|
|
31
|
+
},
|
|
32
|
+
"CircleImageView": {
|
|
33
|
+
"canonical": "Image",
|
|
34
|
+
"render_as": "CircleImage"
|
|
35
|
+
},
|
|
36
|
+
"AsyncImage": {
|
|
37
|
+
"canonical": "NetworkImage"
|
|
38
|
+
},
|
|
39
|
+
"Spinner": {
|
|
40
|
+
"canonical": "SelectBox"
|
|
41
|
+
},
|
|
42
|
+
"DatePicker": {
|
|
43
|
+
"canonical": "SelectBox"
|
|
44
|
+
},
|
|
45
|
+
"Select": {
|
|
46
|
+
"canonical": "SelectBox"
|
|
47
|
+
},
|
|
48
|
+
"Picker": {
|
|
49
|
+
"canonical": "SelectBox"
|
|
50
|
+
},
|
|
51
|
+
"Checkbox": {
|
|
52
|
+
"canonical": "CheckBox"
|
|
53
|
+
},
|
|
54
|
+
"RadioButton": {
|
|
55
|
+
"canonical": "Radio"
|
|
56
|
+
},
|
|
57
|
+
"RadioGroup": {
|
|
58
|
+
"canonical": "Radio"
|
|
59
|
+
},
|
|
60
|
+
"SegmentedControl": {
|
|
61
|
+
"canonical": "Segment"
|
|
62
|
+
},
|
|
63
|
+
"TabLayout": {
|
|
64
|
+
"canonical": "Segment"
|
|
65
|
+
},
|
|
66
|
+
"TabGroup": {
|
|
67
|
+
"canonical": "Segment"
|
|
68
|
+
},
|
|
69
|
+
"SeekBar": {
|
|
70
|
+
"canonical": "Slider"
|
|
71
|
+
},
|
|
72
|
+
"Range": {
|
|
73
|
+
"canonical": "Slider"
|
|
74
|
+
},
|
|
75
|
+
"ProgressBar": {
|
|
76
|
+
"canonical": "Progress"
|
|
77
|
+
},
|
|
78
|
+
"ActivityIndicator": {
|
|
79
|
+
"canonical": "Indicator"
|
|
80
|
+
},
|
|
81
|
+
"Loading": {
|
|
82
|
+
"canonical": "Indicator"
|
|
83
|
+
},
|
|
84
|
+
"LinearLayout": {
|
|
85
|
+
"canonical": "View"
|
|
86
|
+
},
|
|
87
|
+
"RelativeLayout": {
|
|
88
|
+
"canonical": "View"
|
|
89
|
+
},
|
|
90
|
+
"FrameLayout": {
|
|
91
|
+
"canonical": "View"
|
|
92
|
+
},
|
|
93
|
+
"HStack": {
|
|
94
|
+
"canonical": "View",
|
|
95
|
+
"orientation": "horizontal"
|
|
96
|
+
},
|
|
97
|
+
"VStack": {
|
|
98
|
+
"canonical": "View",
|
|
99
|
+
"orientation": "vertical"
|
|
100
|
+
},
|
|
101
|
+
"ZStack": {
|
|
102
|
+
"canonical": "View"
|
|
103
|
+
},
|
|
104
|
+
"Div": {
|
|
105
|
+
"canonical": "View"
|
|
106
|
+
},
|
|
107
|
+
"Box": {
|
|
108
|
+
"canonical": "View"
|
|
109
|
+
},
|
|
110
|
+
"Container": {
|
|
111
|
+
"canonical": "View"
|
|
112
|
+
},
|
|
113
|
+
"Column": {
|
|
114
|
+
"canonical": "View",
|
|
115
|
+
"orientation": "vertical"
|
|
116
|
+
},
|
|
117
|
+
"Row": {
|
|
118
|
+
"canonical": "View",
|
|
119
|
+
"orientation": "horizontal"
|
|
120
|
+
},
|
|
121
|
+
"ConstraintLayout": {
|
|
122
|
+
"canonical": "View"
|
|
123
|
+
},
|
|
124
|
+
"Scroll": {
|
|
125
|
+
"canonical": "ScrollView"
|
|
126
|
+
},
|
|
127
|
+
"CollectionView": {
|
|
128
|
+
"canonical": "Collection"
|
|
129
|
+
},
|
|
130
|
+
"RecyclerView": {
|
|
131
|
+
"canonical": "Collection"
|
|
132
|
+
},
|
|
133
|
+
"Table": {
|
|
134
|
+
"canonical": "Collection"
|
|
135
|
+
},
|
|
136
|
+
"TableView": {
|
|
137
|
+
"canonical": "Collection"
|
|
138
|
+
},
|
|
139
|
+
"List": {
|
|
140
|
+
"canonical": "Collection"
|
|
141
|
+
},
|
|
142
|
+
"Grid": {
|
|
143
|
+
"canonical": "Collection"
|
|
144
|
+
},
|
|
145
|
+
"LazyGrid": {
|
|
146
|
+
"canonical": "Collection"
|
|
147
|
+
},
|
|
148
|
+
"ListView": {
|
|
149
|
+
"canonical": "Collection"
|
|
150
|
+
},
|
|
151
|
+
"LazyColumn": {
|
|
152
|
+
"canonical": "Collection"
|
|
153
|
+
},
|
|
154
|
+
"Gradient": {
|
|
155
|
+
"canonical": "GradientView"
|
|
156
|
+
},
|
|
157
|
+
"BlurView": {
|
|
158
|
+
"canonical": "Blur"
|
|
159
|
+
},
|
|
160
|
+
"WebView": {
|
|
161
|
+
"canonical": "Web"
|
|
162
|
+
},
|
|
163
|
+
"Iframe": {
|
|
164
|
+
"canonical": "Web"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/cli_runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
|
|
|
2
2
|
export async function runCli(command, args, options) {
|
|
3
3
|
const timeout = options.timeout ?? 60_000;
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
|
-
execFile(command, args, {
|
|
5
|
+
const child = execFile(command, args, {
|
|
6
6
|
cwd: options.cwd,
|
|
7
7
|
timeout,
|
|
8
8
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -22,6 +22,12 @@ export async function runCli(command, args, options) {
|
|
|
22
22
|
stderr: stderr || "",
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
+
// Nothing is ever written to the child's stdin: end it now, so a CLI that
|
|
26
|
+
// reads stdin — an overwrite prompt — sees end-of-file instead of waiting
|
|
27
|
+
// for a line that never comes. Through 2.13.1 the pipe stayed open, and such
|
|
28
|
+
// a child waited until the timeout above killed it (`jui g converter` on
|
|
29
|
+
// an existing component, measured 2026-09-26).
|
|
30
|
+
child.stdin?.end();
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
export function formatResult(result) {
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { SpecLoader, mcpRootFromImportMetaUrl } from "./spec_loader.js";
|
|
5
5
|
import { installAutoReload } from "./auto_reload.js";
|
|
6
6
|
import { ServerConfig } from "./config.js";
|
|
7
|
+
import { packageVersion } from "./version.js";
|
|
7
8
|
// --- Group A: Component Spec Lookup ---
|
|
8
9
|
import { register as registerLookupComponent } from "./tools/spec/lookup_component.js";
|
|
9
10
|
import { register as registerLookupAttribute } from "./tools/spec/lookup_attribute.js";
|
|
@@ -55,13 +56,14 @@ import { register as registerTestReport } from "./tools/test/test_report.js";
|
|
|
55
56
|
import { register as registerTestMockGenerate } from "./tools/test/test_mock_generate.js";
|
|
56
57
|
import { register as registerTestArtifactsPull } from "./tools/test/test_artifacts_pull.js";
|
|
57
58
|
import { register as registerTestArtifactsStatus } from "./tools/test/test_artifacts_status.js";
|
|
59
|
+
import { register as registerTestContractsCoverage } from "./tools/test/test_contracts_coverage.js";
|
|
58
60
|
function log(message) {
|
|
59
61
|
console.error(`[jui-tools] ${new Date().toISOString()} ${message}`);
|
|
60
62
|
}
|
|
61
63
|
log("Server starting...");
|
|
62
64
|
const server = new McpServer({
|
|
63
65
|
name: "jui-tools",
|
|
64
|
-
version:
|
|
66
|
+
version: packageVersion(),
|
|
65
67
|
});
|
|
66
68
|
// Initialize spec loader for Group A
|
|
67
69
|
// Resolves attribute_definitions.json via 4-layer fallback:
|
|
@@ -131,6 +133,7 @@ registerTestReport(server, config);
|
|
|
131
133
|
registerTestMockGenerate(server, config);
|
|
132
134
|
registerTestArtifactsPull(server, config);
|
|
133
135
|
registerTestArtifactsStatus(server, config);
|
|
136
|
+
registerTestContractsCoverage(server, config);
|
|
134
137
|
log(`Registered 41 tools (7 spec + 6 context + 8 jui + 9 doc + 3 api + 8 test)`);
|
|
135
138
|
async function main() {
|
|
136
139
|
const transport = new StdioServerTransport();
|
package/dist/spec_loader.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export interface ReloadRecord {
|
|
|
58
58
|
ok: boolean;
|
|
59
59
|
error?: string;
|
|
60
60
|
}
|
|
61
|
+
export interface TypeSynonym {
|
|
62
|
+
spelling: string;
|
|
63
|
+
canonical: string;
|
|
64
|
+
renderAs?: string;
|
|
65
|
+
implies?: Record<string, unknown>;
|
|
66
|
+
source: string;
|
|
67
|
+
}
|
|
61
68
|
export interface DataSourceInfo {
|
|
62
69
|
attributeDefinitions: FileInfo;
|
|
63
70
|
componentMetadata: FileInfo;
|
|
@@ -66,6 +73,7 @@ export interface DataSourceInfo {
|
|
|
66
73
|
attributeSemantics: FileInfo | null;
|
|
67
74
|
platformSemantics: FileInfo | null;
|
|
68
75
|
coverage: FileInfo | null;
|
|
76
|
+
typeSynonyms: FileInfo | null;
|
|
69
77
|
componentCount: number;
|
|
70
78
|
commonAttributeCount: number;
|
|
71
79
|
}
|
|
@@ -92,6 +100,7 @@ export declare class SpecLoader {
|
|
|
92
100
|
private screenIdentity;
|
|
93
101
|
private commonAttributes;
|
|
94
102
|
private aliasMap;
|
|
103
|
+
private typeSynonyms;
|
|
95
104
|
private metadata;
|
|
96
105
|
private dataSource;
|
|
97
106
|
/** When this process read the files. Data is cached in memory from then
|
|
@@ -156,6 +165,22 @@ export declare class SpecLoader {
|
|
|
156
165
|
listComponents(): string[];
|
|
157
166
|
getDataSource(): DataSourceInfo;
|
|
158
167
|
private load;
|
|
168
|
+
/**
|
|
169
|
+
* Type spellings the build accepts for a section they are not
|
|
170
|
+
* (shared/core/type_synonyms.json, jsonui-cli 1.9.0 — the one table the
|
|
171
|
+
* tools and the dynamic runtimes read). Until then lookup_component knew
|
|
172
|
+
* only `_alias_of` sections and metadata aliases, and answered "not found"
|
|
173
|
+
* for `ProgressBar`, which the build draws as a Progress.
|
|
174
|
+
*
|
|
175
|
+
* A section, an `_alias_of` section or a metadata alias of the same
|
|
176
|
+
* spelling wins: the exact match is found first, as in the tools. An entry
|
|
177
|
+
* whose canonical is not a section here is skipped.
|
|
178
|
+
*
|
|
179
|
+
* Absent (older jsonui-cli): no synonyms. Present but not JSON: the load
|
|
180
|
+
* throws, so an automatic reload keeps the last content that parsed rather
|
|
181
|
+
* than swapping in a loader that silently lost every synonym.
|
|
182
|
+
*/
|
|
183
|
+
private loadTypeSynonyms;
|
|
159
184
|
private normalizeMetadata;
|
|
160
185
|
private getMetadata;
|
|
161
186
|
private buildComponentSpec;
|