jui-tools-mcp-server 2.7.0 → 2.10.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 +2 -1
- package/data/platform_semantics.json +70 -0
- package/data/screen_identity.json +1 -1
- package/dist/index.js +8 -2
- package/dist/spec_loader.d.ts +5 -0
- package/dist/spec_loader.js +55 -0
- package/dist/tools/context/list_screen_specs.js +24 -5
- package/dist/tools/context/search_specs.d.ts +3 -0
- package/dist/tools/context/search_specs.js +113 -0
- package/dist/tools/doc/doc_validate_spec.js +1 -1
- package/dist/tools/spec/get_platform_rules.d.ts +3 -0
- package/dist/tools/spec/get_platform_rules.js +18 -0
- package/dist/tools/test/test_generate_branch_tests.d.ts +3 -0
- package/dist/tools/test/test_generate_branch_tests.js +76 -0
- package/package.json +1 -1
- package/scripts/fetch-manifest.js +7 -0
- package/src/index.ts +8 -2
- package/src/spec_loader.ts +80 -0
- package/src/tools/context/list_screen_specs.ts +23 -5
- package/src/tools/context/search_specs.ts +124 -0
- package/src/tools/doc/doc_validate_spec.ts +1 -1
- package/src/tools/spec/get_platform_rules.ts +26 -0
- package/src/tools/test/test_generate_branch_tests.ts +72 -0
- package/tests/snapshot_completeness.test.ts +1 -0
- package/tests/spec_loader.test.ts +55 -0
- package/tests/tools_context.test.ts +114 -0
- package/tests/tools_test.test.ts +66 -1
package/README.md
CHANGED
|
@@ -107,13 +107,14 @@ bash ~/.jsonui-mcp-server/uninstall.sh
|
|
|
107
107
|
| `list_api_models` | 生成済み DTO/Domain 一覧(orphan 検出付き) | `jui ls api-models --json` |
|
|
108
108
|
| `preview_api_model_sync` | swagger→DTO/Domain 同期の dry-run プレビュー(書き込みなし) | `jui g api --dry-run --json` |
|
|
109
109
|
|
|
110
|
-
### Group F: jsonui-test CLI (
|
|
110
|
+
### Group F: jsonui-test CLI (9)
|
|
111
111
|
|
|
112
112
|
| Tool | Description | CLI |
|
|
113
113
|
|------|-------------|-----|
|
|
114
114
|
| `test_validate` | テストファイル(screen/flow + description)のバリデーション。既定では CLI 同様に flatten-install 副作用あり(`no_install=true` で検証のみ) | `jsonui-test validate [--no-install]` |
|
|
115
115
|
| `test_generate_screen` | screen テストテンプレート生成 | `jsonui-test generate test screen` |
|
|
116
116
|
| `test_generate_flow` | flow テストテンプレート生成 | `jsonui-test generate test flow` |
|
|
117
|
+
| `test_generate_branch_tests` | spec の branchContracts から分岐テスト生成(web/android/ios) | `jsonui-test generate branch-tests` |
|
|
117
118
|
| `test_generate_description` | テストケースの description JSON 生成(screen/flow) | `jsonui-test generate description` |
|
|
118
119
|
| `test_report` | results JSON → JUnit XML / HTML レポート変換 | `jsonui-test report` |
|
|
119
120
|
| `test_mock_generate` | OpenAPI からモック scaffold(`check=true` で drift レポートのみ・書き込みなし) | `jsonui-test mock generate [--check]` |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"title": "Platform-scoped layout semantics",
|
|
4
|
+
"purpose": "How to make a node render differently (or not at all) on one platform: the node-level 'platform' directive, the layout-root 'platforms' whitelist, and how they differ from 'responsive'. Canonical implementation: jui_tools/jui_cli/core/platform_resolver.py — this file restates it for lookup and must follow it, never lead it.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"platform", "platforms", "ios only", "android only", "web only",
|
|
7
|
+
"per-platform", "override", "platform-specific", "platform limited",
|
|
8
|
+
"プラットフォーム", "限定", "出し分け", "iosだけ", "androidだけ", "webだけ",
|
|
9
|
+
"プラットフォーム別", "機種別"
|
|
10
|
+
],
|
|
11
|
+
"nodeDirective": {
|
|
12
|
+
"attribute": "platform",
|
|
13
|
+
"consumedAt": "distribution (jui build). The key never appears in distributed per-platform output; platform-specific emit is produced from it.",
|
|
14
|
+
"objectForm": {
|
|
15
|
+
"summary": "Per-platform attribute overrides, merged into the node for the matching platform only. Override wins over the base value; merge is per top-level key (the override value replaces the base value for that key wholesale — it is not deep-merged into it).",
|
|
16
|
+
"example": {
|
|
17
|
+
"prompt": "Change fontColor only on iOS",
|
|
18
|
+
"layout": {
|
|
19
|
+
"type": "Label",
|
|
20
|
+
"text": "@{title}",
|
|
21
|
+
"fontColor": "#333333",
|
|
22
|
+
"platform": { "ios": { "fontColor": "#FFD700" } }
|
|
23
|
+
},
|
|
24
|
+
"resolved": {
|
|
25
|
+
"ios": { "type": "Label", "text": "@{title}", "fontColor": "#FFD700" },
|
|
26
|
+
"android": { "type": "Label", "text": "@{title}", "fontColor": "#333333" },
|
|
27
|
+
"web": { "type": "Label", "text": "@{title}", "fontColor": "#333333" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"multiPlatformExample": {
|
|
31
|
+
"prompt": "Different heights everywhere, plus a web-only extra attribute",
|
|
32
|
+
"layout": {
|
|
33
|
+
"type": "View",
|
|
34
|
+
"height": 200,
|
|
35
|
+
"platform": {
|
|
36
|
+
"ios": { "height": 220 },
|
|
37
|
+
"android": { "height": 180 },
|
|
38
|
+
"web": { "height": "100vh", "maxWidth": 1200 }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"detection": "A dict counts as an override map ONLY when every key is one of 'ios' / 'android' / 'web'. Any other key (e.g. a stray 'class') makes the whole value a non-override, so nothing merges — keep override maps pure.",
|
|
43
|
+
"nesting": "Override values are themselves resolved recursively, so an override may contain child nodes with their own 'platform' directives."
|
|
44
|
+
},
|
|
45
|
+
"stringForm": {
|
|
46
|
+
"summary": "Node filter: the node (and its whole subtree) is DROPPED on platforms that do not match. Comma-separated tokens allowed.",
|
|
47
|
+
"example": {
|
|
48
|
+
"prompt": "Render this node on Android only",
|
|
49
|
+
"layout": { "type": "View", "platform": "android", "child": [] }
|
|
50
|
+
},
|
|
51
|
+
"tokens": {
|
|
52
|
+
"ios": ["ios", "swift", "swiftui", "uikit"],
|
|
53
|
+
"android": ["android", "kotlin", "java", "compose", "xml"],
|
|
54
|
+
"web": ["web", "typescript", "javascript", "react"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"layoutRootPlatforms": {
|
|
59
|
+
"attribute": "platforms",
|
|
60
|
+
"summary": "Layout-root array whitelisting the distribution targets of the ENTIRE file (e.g. [\"ios\", \"android\"]). Distinct from the node-level 'platform' directive: 'platforms' picks which platforms get the file at all; 'platform' shapes a single node per platform."
|
|
61
|
+
},
|
|
62
|
+
"vsResponsive": {
|
|
63
|
+
"summary": "'responsive' is resolved at RUNTIME by each platform's framework (window size class / orientation); 'platform' is resolved at BUILD time and is invisible at runtime. Use 'platform' for per-OS differences, 'responsive' for per-form-factor differences. The resolver leaves 'responsive' untouched."
|
|
64
|
+
},
|
|
65
|
+
"authoringGuidance": {
|
|
66
|
+
"spec": "Screen specs stay platform-agnostic; when a node intentionally differs per platform, note the difference in the spec text of the section that owns the node (do not fork the spec per platform). Large screens split into sub-specs list them in the parent's 'subSpecs' array — the sub-spec that owns the node carries the note.",
|
|
67
|
+
"precedence": "Per-node 'platform' override > inline base attribute > 'style'-provided value. Distribution bakes the override into the node as a plain inline attribute; style merge (applied later, at codegen or dynamic runtime) always yields to inline attributes.",
|
|
68
|
+
"dynamicFace": "Distribution has already resolved 'platform' by the time a layout reaches a device, so the dynamic face never sees the key either — declaring it costs nothing at runtime on any face."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"appOwnedScreens": {
|
|
88
|
-
"description": "Screens the app owns outright — a hand-written page with no JsonUI layout behind it. They are real navigation destinations (a test navigates to them and asserts there), so they must be declarable, otherwise the screen-unknown rule rejects a legitimate id.",
|
|
88
|
+
"description": "Screens the app owns outright — a hand-written page with no JsonUI layout behind it. They are real navigation destinations (a test navigates to them and asserts there), so they must be declarable, otherwise the screen-unknown rule rejects a legitimate id. Having no layout is what they ARE, not an omission: `jui verify`'s spec-coverage check looks for a layout that generates a screen nobody declared, so these fall outside it — requiring a spec would also leave no way to comply, since there is no layout root to carry a `role` and a spec for a screen with nothing to generate describes nothing.",
|
|
89
89
|
"declaration": {
|
|
90
90
|
"location": "jui.config.json",
|
|
91
91
|
"key": "test.appOwnedScreens",
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { register as registerGetBindingRules } from "./tools/spec/get_binding_ru
|
|
|
12
12
|
import { register as registerGetScreenIdentity } from "./tools/spec/get_screen_identity.js";
|
|
13
13
|
import { register as registerGetPlatformMapping } from "./tools/spec/get_platform_mapping.js";
|
|
14
14
|
import { register as registerGetDataSource } from "./tools/spec/get_data_source.js";
|
|
15
|
+
import { register as registerGetPlatformRules } from "./tools/spec/get_platform_rules.js";
|
|
15
16
|
// --- Group B: Project Context ---
|
|
16
17
|
import { register as registerGetProjectConfig } from "./tools/context/get_project_config.js";
|
|
17
18
|
import { register as registerListScreenSpecs } from "./tools/context/list_screen_specs.js";
|
|
@@ -19,6 +20,7 @@ import { register as registerListComponentSpecs } from "./tools/context/list_com
|
|
|
19
20
|
import { register as registerListLayouts } from "./tools/context/list_layouts.js";
|
|
20
21
|
import { register as registerReadSpecFile } from "./tools/context/read_spec_file.js";
|
|
21
22
|
import { register as registerReadLayoutFile } from "./tools/context/read_layout_file.js";
|
|
23
|
+
import { register as registerSearchSpecs } from "./tools/context/search_specs.js";
|
|
22
24
|
// --- Group C: jui CLI ---
|
|
23
25
|
import { register as registerJuiInit } from "./tools/jui/jui_init.js";
|
|
24
26
|
import { register as registerJuiGenerateProject } from "./tools/jui/jui_generate_project.js";
|
|
@@ -46,6 +48,7 @@ import { register as registerDocRulesShow } from "./tools/doc/doc_rules_show.js"
|
|
|
46
48
|
import { register as registerTestValidate } from "./tools/test/test_validate.js";
|
|
47
49
|
import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
|
|
48
50
|
import { register as registerTestGenerateFlow } from "./tools/test/test_generate_flow.js";
|
|
51
|
+
import { register as registerTestGenerateBranchTests } from "./tools/test/test_generate_branch_tests.js";
|
|
49
52
|
import { register as registerTestGenerateDescription } from "./tools/test/test_generate_description.js";
|
|
50
53
|
import { register as registerTestReport } from "./tools/test/test_report.js";
|
|
51
54
|
import { register as registerTestMockGenerate } from "./tools/test/test_mock_generate.js";
|
|
@@ -70,7 +73,7 @@ log(`component_metadata: [${source.componentMetadata.layer}] ${source.compone
|
|
|
70
73
|
log(`Loaded ${source.componentCount} components, ${source.commonAttributeCount} common attributes`);
|
|
71
74
|
// Initialize server config for Groups B/C/D
|
|
72
75
|
const config = new ServerConfig();
|
|
73
|
-
// Register Group A: Component Spec Lookup (
|
|
76
|
+
// Register Group A: Component Spec Lookup (9 tools)
|
|
74
77
|
registerLookupComponent(server, loader);
|
|
75
78
|
registerLookupAttribute(server, loader);
|
|
76
79
|
registerSearchComponents(server, loader);
|
|
@@ -79,13 +82,15 @@ registerGetBindingRules(server, loader);
|
|
|
79
82
|
registerGetScreenIdentity(server, loader);
|
|
80
83
|
registerGetPlatformMapping(server, loader);
|
|
81
84
|
registerGetDataSource(server, loader);
|
|
82
|
-
|
|
85
|
+
registerGetPlatformRules(server, loader);
|
|
86
|
+
// Register Group B: Project Context (7 tools)
|
|
83
87
|
registerGetProjectConfig(server, config);
|
|
84
88
|
registerListScreenSpecs(server, config);
|
|
85
89
|
registerListComponentSpecs(server, config);
|
|
86
90
|
registerListLayouts(server, config);
|
|
87
91
|
registerReadSpecFile(server, config);
|
|
88
92
|
registerReadLayoutFile(server, config);
|
|
93
|
+
registerSearchSpecs(server, config);
|
|
89
94
|
// Register Group C: jui CLI (8 tools)
|
|
90
95
|
registerJuiInit(server, config);
|
|
91
96
|
registerJuiGenerateProject(server, config);
|
|
@@ -116,6 +121,7 @@ registerPreviewApiModelSync(server, config);
|
|
|
116
121
|
registerTestValidate(server, config);
|
|
117
122
|
registerTestGenerateScreen(server, config);
|
|
118
123
|
registerTestGenerateFlow(server, config);
|
|
124
|
+
registerTestGenerateBranchTests(server, config);
|
|
119
125
|
registerTestGenerateDescription(server, config);
|
|
120
126
|
registerTestReport(server, config);
|
|
121
127
|
registerTestMockGenerate(server, config);
|
package/dist/spec_loader.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface DataSourceInfo {
|
|
|
38
38
|
screenIdentity: FileInfo | null;
|
|
39
39
|
bindingSemantics: FileInfo | null;
|
|
40
40
|
attributeSemantics: FileInfo | null;
|
|
41
|
+
platformSemantics: FileInfo | null;
|
|
41
42
|
coverage: FileInfo | null;
|
|
42
43
|
componentCount: number;
|
|
43
44
|
commonAttributeCount: number;
|
|
@@ -60,6 +61,7 @@ export declare class SpecLoader {
|
|
|
60
61
|
private commonAttributesRaw;
|
|
61
62
|
private bindingSemantics;
|
|
62
63
|
private attributeSemantics;
|
|
64
|
+
private platformSemantics;
|
|
63
65
|
private coverageGaps;
|
|
64
66
|
private screenIdentity;
|
|
65
67
|
private commonAttributes;
|
|
@@ -75,6 +77,8 @@ export declare class SpecLoader {
|
|
|
75
77
|
getAttribute(name: string): any;
|
|
76
78
|
private withImplementationGaps;
|
|
77
79
|
searchComponents(query: string): any[];
|
|
80
|
+
/** Keyword-indexed topical guides, each backed by a dedicated MCP tool. */
|
|
81
|
+
private topicGuides;
|
|
78
82
|
getModifierOrder(platform?: string): any;
|
|
79
83
|
/**
|
|
80
84
|
* The canonical screen-identity asset, or null when this checkout predates
|
|
@@ -93,6 +97,7 @@ export declare class SpecLoader {
|
|
|
93
97
|
getChangedSinceLoad(): string[];
|
|
94
98
|
getScreenIdentity(): any;
|
|
95
99
|
getBindingRules(): any;
|
|
100
|
+
getPlatformRules(): any;
|
|
96
101
|
getPlatformMapping(category?: string): any;
|
|
97
102
|
listComponents(): string[];
|
|
98
103
|
getDataSource(): DataSourceInfo;
|
package/dist/spec_loader.js
CHANGED
|
@@ -35,6 +35,7 @@ export class SpecLoader {
|
|
|
35
35
|
commonAttributesRaw = {};
|
|
36
36
|
bindingSemantics = null;
|
|
37
37
|
attributeSemantics = null;
|
|
38
|
+
platformSemantics = null;
|
|
38
39
|
/// `${component}.${attribute}` -> [{platform, reason, note}] from the
|
|
39
40
|
/// coverage ledger. The ledger tracks pairs that ARE declared (platform
|
|
40
41
|
/// field includes them) but that the platform's codegen does not consume —
|
|
@@ -159,8 +160,40 @@ export class SpecLoader {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
// Topic-guide index: task vocabulary ("iOSだけ色を変える", "ios only",
|
|
164
|
+
// "platform override") does not contain any component or attribute
|
|
165
|
+
// name, so the loops above can't surface the mechanism. Each guide
|
|
166
|
+
// declares its own keywords (en + ja) in its semantics file; a hit
|
|
167
|
+
// pins a pointer to the dedicated tool at the top of the results.
|
|
168
|
+
for (const guide of this.topicGuides()) {
|
|
169
|
+
const hit = guide.keywords.some((kw) => q.includes(kw) || (q.length >= 3 && kw.includes(q)));
|
|
170
|
+
if (hit) {
|
|
171
|
+
results.push({
|
|
172
|
+
guide: guide.topic,
|
|
173
|
+
score: 100,
|
|
174
|
+
matches: [guide.summary],
|
|
175
|
+
nextTool: guide.tool,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
162
179
|
return results.sort((a, b) => b.score - a.score);
|
|
163
180
|
}
|
|
181
|
+
/** Keyword-indexed topical guides, each backed by a dedicated MCP tool. */
|
|
182
|
+
topicGuides() {
|
|
183
|
+
const guides = [];
|
|
184
|
+
if (this.platformSemantics) {
|
|
185
|
+
const kws = Array.isArray(this.platformSemantics.keywords)
|
|
186
|
+
? this.platformSemantics.keywords
|
|
187
|
+
: [];
|
|
188
|
+
guides.push({
|
|
189
|
+
topic: "platform_rules",
|
|
190
|
+
tool: "get_platform_rules",
|
|
191
|
+
summary: "Per-platform styling/visibility (e.g. change fontColor on iOS only): node-level 'platform' directive (object form = per-platform overrides, string form = platform filter), root 'platforms' whitelist, vs 'responsive'. Call get_platform_rules for the full semantics with examples.",
|
|
192
|
+
keywords: kws.map((k) => k.toLowerCase()),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return guides;
|
|
196
|
+
}
|
|
164
197
|
getModifierOrder(platform) {
|
|
165
198
|
if (platform)
|
|
166
199
|
return MODIFIER_ORDER[platform] || null;
|
|
@@ -189,6 +222,7 @@ export class SpecLoader {
|
|
|
189
222
|
this.dataSource.screenIdentity,
|
|
190
223
|
this.dataSource.bindingSemantics,
|
|
191
224
|
this.dataSource.attributeSemantics,
|
|
225
|
+
this.dataSource.platformSemantics,
|
|
192
226
|
this.dataSource.coverage,
|
|
193
227
|
].filter((f) => f != null);
|
|
194
228
|
const changed = [];
|
|
@@ -222,6 +256,14 @@ export class SpecLoader {
|
|
|
222
256
|
semantics: this.bindingSemantics,
|
|
223
257
|
};
|
|
224
258
|
}
|
|
259
|
+
getPlatformRules() {
|
|
260
|
+
// Canonical platform-scoping semantics (SSoT:
|
|
261
|
+
// shared/core/platform_semantics.json — restates
|
|
262
|
+
// jui_tools/jui_cli/core/platform_resolver.py). Null on jsonui-cli
|
|
263
|
+
// checkouts that predate the file; the tool reports that instead of
|
|
264
|
+
// inventing rules.
|
|
265
|
+
return this.platformSemantics;
|
|
266
|
+
}
|
|
225
267
|
getPlatformMapping(category) {
|
|
226
268
|
if (category)
|
|
227
269
|
return PLATFORM_MAPPING[category] || null;
|
|
@@ -262,6 +304,18 @@ export class SpecLoader {
|
|
|
262
304
|
catch {
|
|
263
305
|
this.attributeSemantics = null;
|
|
264
306
|
}
|
|
307
|
+
// Platform-scoped layout semantics (node 'platform' directive, root
|
|
308
|
+
// 'platforms', vs 'responsive'). Optional for the same backward-compat
|
|
309
|
+
// reason.
|
|
310
|
+
let platformSemanticsResolution = null;
|
|
311
|
+
try {
|
|
312
|
+
const platSemResolution = this.resolveFile("shared/core/platform_semantics.json", "data/platform_semantics.json");
|
|
313
|
+
this.platformSemantics = JSON.parse(readFileSync(platSemResolution.path, "utf-8"));
|
|
314
|
+
platformSemanticsResolution = platSemResolution;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
this.platformSemantics = null;
|
|
318
|
+
}
|
|
265
319
|
// Canonical screen identity (screen-identity track). Optional so older
|
|
266
320
|
// jsonui-cli checkouts without the file still load.
|
|
267
321
|
try {
|
|
@@ -337,6 +391,7 @@ export class SpecLoader {
|
|
|
337
391
|
screenIdentity: screenIdentityResolution,
|
|
338
392
|
bindingSemantics: bindingResolution,
|
|
339
393
|
attributeSemantics: attributeSemanticsResolution,
|
|
394
|
+
platformSemantics: platformSemanticsResolution,
|
|
340
395
|
coverage: coverageResolution,
|
|
341
396
|
componentCount,
|
|
342
397
|
commonAttributeCount: Object.keys(this.commonAttributesRaw).filter((k) => !k.startsWith("_")).length,
|
|
@@ -13,21 +13,40 @@ export function register(server, config) {
|
|
|
13
13
|
return { content: [{ type: "text", text: `Spec directory not found: ${specDir}` }] };
|
|
14
14
|
}
|
|
15
15
|
const files = readdirSync(specDir).filter((f) => f.endsWith(".spec.json"));
|
|
16
|
-
const specs =
|
|
16
|
+
const specs = [];
|
|
17
|
+
for (const file of files) {
|
|
17
18
|
try {
|
|
18
19
|
const data = JSON.parse(readFileSync(join(specDir, file), "utf-8"));
|
|
19
|
-
|
|
20
|
+
specs.push({
|
|
20
21
|
file,
|
|
21
22
|
name: data.metadata?.name || file.replace(".spec.json", ""),
|
|
22
23
|
displayName: data.metadata?.displayName || "",
|
|
23
24
|
type: data.type || "screen_spec",
|
|
24
25
|
layoutFile: data.metadata?.layoutFile || null,
|
|
25
|
-
};
|
|
26
|
+
});
|
|
27
|
+
// A parent spec splits one screen across sub-spec files (its
|
|
28
|
+
// 'subSpecs' array). Surface them inline so a session can see
|
|
29
|
+
// the split and read_spec_file the right part without first
|
|
30
|
+
// opening the parent.
|
|
31
|
+
if (Array.isArray(data.subSpecs)) {
|
|
32
|
+
for (const sub of data.subSpecs) {
|
|
33
|
+
if (!sub?.file)
|
|
34
|
+
continue;
|
|
35
|
+
specs.push({
|
|
36
|
+
file: sub.file,
|
|
37
|
+
name: sub.name || sub.file,
|
|
38
|
+
displayName: sub.description || "",
|
|
39
|
+
type: "screen_sub_spec",
|
|
40
|
+
parent: file,
|
|
41
|
+
layoutFile: data.metadata?.layoutFile || null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
catch {
|
|
28
|
-
|
|
47
|
+
specs.push({ file, name: file.replace(".spec.json", ""), displayName: "", type: "unknown", layoutFile: null });
|
|
29
48
|
}
|
|
30
|
-
}
|
|
49
|
+
}
|
|
31
50
|
return { content: [{ type: "text", text: JSON.stringify(specs, null, 2) }] };
|
|
32
51
|
}
|
|
33
52
|
catch (e) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
3
|
+
import { join, relative } from "path";
|
|
4
|
+
const MAX_RESULTS = 30;
|
|
5
|
+
const SNIPPET_LEN = 200;
|
|
6
|
+
/** Recursively collect *.spec.json / *.component.json under dir. */
|
|
7
|
+
function collectSpecFiles(dir, out = []) {
|
|
8
|
+
for (const entry of readdirSync(dir)) {
|
|
9
|
+
const p = join(dir, entry);
|
|
10
|
+
if (statSync(p).isDirectory()) {
|
|
11
|
+
collectSpecFiles(p, out);
|
|
12
|
+
}
|
|
13
|
+
else if (entry.endsWith(".spec.json") || entry.endsWith(".component.json")) {
|
|
14
|
+
out.push(p);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Walk a parsed spec and report the JSON paths whose keys or string values
|
|
21
|
+
* contain the query. One entry per path, with a short snippet.
|
|
22
|
+
*/
|
|
23
|
+
function findMatches(node, q, path, out) {
|
|
24
|
+
if (out.length >= MAX_RESULTS)
|
|
25
|
+
return;
|
|
26
|
+
if (typeof node === "string") {
|
|
27
|
+
if (node.toLowerCase().includes(q)) {
|
|
28
|
+
out.push({ path, snippet: node.slice(0, SNIPPET_LEN) });
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(node)) {
|
|
33
|
+
node.forEach((v, i) => findMatches(v, q, `${path}[${i}]`, out));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (node && typeof node === "object") {
|
|
37
|
+
for (const [k, v] of Object.entries(node)) {
|
|
38
|
+
const childPath = path ? `${path}.${k}` : k;
|
|
39
|
+
if (k.toLowerCase().includes(q)) {
|
|
40
|
+
const snippet = typeof v === "string"
|
|
41
|
+
? v.slice(0, SNIPPET_LEN)
|
|
42
|
+
: JSON.stringify(v)?.slice(0, SNIPPET_LEN) ?? "";
|
|
43
|
+
out.push({ path: childPath, snippet });
|
|
44
|
+
if (out.length >= MAX_RESULTS)
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
findMatches(v, q, childPath, out);
|
|
48
|
+
if (out.length >= MAX_RESULTS)
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function register(server, config) {
|
|
54
|
+
server.tool("search_specs", "Keyword-search across ALL project spec files (screen specs, sub-specs in subdirectories, component specs). Returns file + JSON path + snippet per match — the fast way to find which spec (or which sub-spec of a split screen) covers a topic", {
|
|
55
|
+
query: z.string().describe("Search keyword (matched case-insensitively against keys and string values)"),
|
|
56
|
+
project_dir: z.string().optional().describe("Project directory (overrides JUI_PROJECT_DIR env)"),
|
|
57
|
+
}, async ({ query, project_dir }) => {
|
|
58
|
+
try {
|
|
59
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
60
|
+
const projectConfig = config.readProjectConfig(projectDir);
|
|
61
|
+
const q = query.toLowerCase();
|
|
62
|
+
if (!q.trim()) {
|
|
63
|
+
return { content: [{ type: "text", text: "Empty query." }] };
|
|
64
|
+
}
|
|
65
|
+
const dirs = new Set();
|
|
66
|
+
for (const field of ["spec_directory", "component_spec_directory"]) {
|
|
67
|
+
try {
|
|
68
|
+
const d = config.resolveDir(projectConfig, field, projectDir);
|
|
69
|
+
if (existsSync(d))
|
|
70
|
+
dirs.add(d);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* directory not configured */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (dirs.size === 0) {
|
|
77
|
+
return { content: [{ type: "text", text: "No spec directories found." }] };
|
|
78
|
+
}
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const dir of dirs) {
|
|
81
|
+
for (const filePath of collectSpecFiles(dir)) {
|
|
82
|
+
if (results.length >= MAX_RESULTS)
|
|
83
|
+
break;
|
|
84
|
+
let matches = [];
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
87
|
+
findMatches(data, q, "", matches);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (matches.length > 0) {
|
|
93
|
+
results.push({
|
|
94
|
+
file: relative(dir, filePath),
|
|
95
|
+
matchCount: matches.length,
|
|
96
|
+
matches: matches.slice(0, 5),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (results.length === 0) {
|
|
102
|
+
return { content: [{ type: "text", text: `No matches for '${query}'.` }] };
|
|
103
|
+
}
|
|
104
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { runCli } from "../../cli_runner.js";
|
|
4
4
|
export function register(server, config) {
|
|
5
|
-
server.tool("doc_validate_spec", "Validate a screen specification JSON file against the schema", {
|
|
5
|
+
server.tool("doc_validate_spec", "Validate a screen specification JSON file against the schema. Also expands `@canonical` marks in dataFlow method params/returnType against the project's OpenAPI documents, so a mark that names no operation is reported here as an error rather than reaching `jui build`.", {
|
|
6
6
|
file: z.string().describe("Spec file path (e.g., 'login.spec.json' or full path)"),
|
|
7
7
|
project_dir: z.string().optional().describe("Project directory (overrides JUI_PROJECT_DIR env)"),
|
|
8
8
|
}, async (params) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function register(server, loader) {
|
|
2
|
+
server.tool("get_platform_rules", "Get platform-scoping rules for layouts: how to change an attribute on one platform only (node-level 'platform' object form, e.g. iOS-only fontColor), how to drop a node per platform (string form + language tokens), the layout-root 'platforms' whitelist, precedence, and how this differs from 'responsive'", {}, async () => {
|
|
3
|
+
const result = loader.getPlatformRules();
|
|
4
|
+
if (!result) {
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
text: "platform_semantics.json not found — the connected jsonui-cli checkout predates it. Fall back to lookup_attribute('platform') for the short definition.",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runCli, formatResult } from "../../cli_runner.js";
|
|
3
|
+
export function register(server, config) {
|
|
4
|
+
server.tool("test_generate_branch_tests", "Generate unit-level branch tests from a screen spec's branchContracts " +
|
|
5
|
+
"(vitest / Kotlin JUnit4+Robolectric / Swift XCTest). Only the HTTP " +
|
|
6
|
+
"boundary is mocked — ViewModel, UseCase, Repository and response " +
|
|
7
|
+
"decoding all run for real. The generated test file and runtime are " +
|
|
8
|
+
"@generated; the harness skeleton is emitted once and then owned by " +
|
|
9
|
+
"the project. Screens with no branchContracts section are an error, " +
|
|
10
|
+
"not a no-op", {
|
|
11
|
+
screen: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Screen name in snake_case (resolves <spec_directory>/<screen>.spec.json)"),
|
|
14
|
+
platform: z
|
|
15
|
+
.enum(["web", "android", "ios"])
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Target platform (default web). android additionally requires " +
|
|
18
|
+
"`package`; ios additionally requires `module`"),
|
|
19
|
+
package: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Kotlin package for the generated sources — required for platform 'android'"),
|
|
23
|
+
module: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("App module name for @testable import — required for platform 'ios'"),
|
|
27
|
+
spec: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Explicit spec file path (overrides spec_directory resolution)"),
|
|
31
|
+
out_dir: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Output directory for the @generated test + runtime files"),
|
|
35
|
+
harness_dir: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Project-owned harness directory (skeleton written only when absent)"),
|
|
39
|
+
mocks_dir: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Directory scanned for *.mock.json scenario files"),
|
|
43
|
+
project_dir: z.string().optional().describe("Project directory (overrides JUI_PROJECT_DIR env)"),
|
|
44
|
+
}, async (params) => {
|
|
45
|
+
try {
|
|
46
|
+
const projectDir = config.resolveProjectDir(params.project_dir);
|
|
47
|
+
const args = ["generate", "branch-tests", params.screen];
|
|
48
|
+
if (params.platform) {
|
|
49
|
+
args.push("--platform", params.platform);
|
|
50
|
+
}
|
|
51
|
+
if (params.package) {
|
|
52
|
+
args.push("--package", params.package);
|
|
53
|
+
}
|
|
54
|
+
if (params.module) {
|
|
55
|
+
args.push("--module", params.module);
|
|
56
|
+
}
|
|
57
|
+
if (params.spec) {
|
|
58
|
+
args.push("--spec", params.spec);
|
|
59
|
+
}
|
|
60
|
+
if (params.out_dir) {
|
|
61
|
+
args.push("--out-dir", params.out_dir);
|
|
62
|
+
}
|
|
63
|
+
if (params.harness_dir) {
|
|
64
|
+
args.push("--harness-dir", params.harness_dir);
|
|
65
|
+
}
|
|
66
|
+
if (params.mocks_dir) {
|
|
67
|
+
args.push("--mocks-dir", params.mocks_dir);
|
|
68
|
+
}
|
|
69
|
+
const result = await runCli("jsonui-test", args, { cwd: projectDir });
|
|
70
|
+
return { content: [{ type: "text", text: formatResult(result) }] };
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -33,6 +33,13 @@ export const FILES = [
|
|
|
33
33
|
remote: "shared/core/attribute_semantics.json",
|
|
34
34
|
local: "attribute_semantics.json",
|
|
35
35
|
},
|
|
36
|
+
// Canonical platform-scoping semantics (node 'platform' directive, root
|
|
37
|
+
// 'platforms', vs 'responsive') — backs get_platform_rules and the
|
|
38
|
+
// search-side topic-guide index.
|
|
39
|
+
{
|
|
40
|
+
remote: "shared/core/platform_semantics.json",
|
|
41
|
+
local: "platform_semantics.json",
|
|
42
|
+
},
|
|
36
43
|
// Canonical binding-resolution semantics. This was bundled in data/ but
|
|
37
44
|
// missing from FILES, so the snapshot only ever refreshed by hand and
|
|
38
45
|
// silently went stale — the same trap the screen-identity asset would
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { register as registerGetBindingRules } from "./tools/spec/get_binding_ru
|
|
|
14
14
|
import { register as registerGetScreenIdentity } from "./tools/spec/get_screen_identity.js";
|
|
15
15
|
import { register as registerGetPlatformMapping } from "./tools/spec/get_platform_mapping.js";
|
|
16
16
|
import { register as registerGetDataSource } from "./tools/spec/get_data_source.js";
|
|
17
|
+
import { register as registerGetPlatformRules } from "./tools/spec/get_platform_rules.js";
|
|
17
18
|
|
|
18
19
|
// --- Group B: Project Context ---
|
|
19
20
|
import { register as registerGetProjectConfig } from "./tools/context/get_project_config.js";
|
|
@@ -22,6 +23,7 @@ import { register as registerListComponentSpecs } from "./tools/context/list_com
|
|
|
22
23
|
import { register as registerListLayouts } from "./tools/context/list_layouts.js";
|
|
23
24
|
import { register as registerReadSpecFile } from "./tools/context/read_spec_file.js";
|
|
24
25
|
import { register as registerReadLayoutFile } from "./tools/context/read_layout_file.js";
|
|
26
|
+
import { register as registerSearchSpecs } from "./tools/context/search_specs.js";
|
|
25
27
|
|
|
26
28
|
// --- Group C: jui CLI ---
|
|
27
29
|
import { register as registerJuiInit } from "./tools/jui/jui_init.js";
|
|
@@ -53,6 +55,7 @@ import { register as registerDocRulesShow } from "./tools/doc/doc_rules_show.js"
|
|
|
53
55
|
import { register as registerTestValidate } from "./tools/test/test_validate.js";
|
|
54
56
|
import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
|
|
55
57
|
import { register as registerTestGenerateFlow } from "./tools/test/test_generate_flow.js";
|
|
58
|
+
import { register as registerTestGenerateBranchTests } from "./tools/test/test_generate_branch_tests.js";
|
|
56
59
|
import { register as registerTestGenerateDescription } from "./tools/test/test_generate_description.js";
|
|
57
60
|
import { register as registerTestReport } from "./tools/test/test_report.js";
|
|
58
61
|
import { register as registerTestMockGenerate } from "./tools/test/test_mock_generate.js";
|
|
@@ -89,7 +92,7 @@ log(
|
|
|
89
92
|
// Initialize server config for Groups B/C/D
|
|
90
93
|
const config = new ServerConfig();
|
|
91
94
|
|
|
92
|
-
// Register Group A: Component Spec Lookup (
|
|
95
|
+
// Register Group A: Component Spec Lookup (9 tools)
|
|
93
96
|
registerLookupComponent(server, loader);
|
|
94
97
|
registerLookupAttribute(server, loader);
|
|
95
98
|
registerSearchComponents(server, loader);
|
|
@@ -98,14 +101,16 @@ registerGetBindingRules(server, loader);
|
|
|
98
101
|
registerGetScreenIdentity(server, loader);
|
|
99
102
|
registerGetPlatformMapping(server, loader);
|
|
100
103
|
registerGetDataSource(server, loader);
|
|
104
|
+
registerGetPlatformRules(server, loader);
|
|
101
105
|
|
|
102
|
-
// Register Group B: Project Context (
|
|
106
|
+
// Register Group B: Project Context (7 tools)
|
|
103
107
|
registerGetProjectConfig(server, config);
|
|
104
108
|
registerListScreenSpecs(server, config);
|
|
105
109
|
registerListComponentSpecs(server, config);
|
|
106
110
|
registerListLayouts(server, config);
|
|
107
111
|
registerReadSpecFile(server, config);
|
|
108
112
|
registerReadLayoutFile(server, config);
|
|
113
|
+
registerSearchSpecs(server, config);
|
|
109
114
|
|
|
110
115
|
// Register Group C: jui CLI (8 tools)
|
|
111
116
|
registerJuiInit(server, config);
|
|
@@ -140,6 +145,7 @@ registerPreviewApiModelSync(server, config);
|
|
|
140
145
|
registerTestValidate(server, config);
|
|
141
146
|
registerTestGenerateScreen(server, config);
|
|
142
147
|
registerTestGenerateFlow(server, config);
|
|
148
|
+
registerTestGenerateBranchTests(server, config);
|
|
143
149
|
registerTestGenerateDescription(server, config);
|
|
144
150
|
registerTestReport(server, config);
|
|
145
151
|
registerTestMockGenerate(server, config);
|