@wootsup/mcp 0.3.0 → 0.4.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +14 -5
  2. package/dist/catalog/build-catalog.d.ts +31 -0
  3. package/dist/catalog/build-catalog.js +68 -0
  4. package/dist/catalog/build-catalog.js.map +1 -0
  5. package/dist/index.js +37 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/modules/apimapper/auto-layout.d.ts +21 -0
  8. package/dist/modules/apimapper/auto-layout.js +54 -0
  9. package/dist/modules/apimapper/auto-layout.js.map +1 -0
  10. package/dist/modules/apimapper/client.d.ts +54 -4
  11. package/dist/modules/apimapper/client.js +145 -14
  12. package/dist/modules/apimapper/client.js.map +1 -1
  13. package/dist/modules/apimapper/connections-format.d.ts +31 -1
  14. package/dist/modules/apimapper/connections-format.js +97 -5
  15. package/dist/modules/apimapper/connections-format.js.map +1 -1
  16. package/dist/modules/apimapper/connections.d.ts +9 -7
  17. package/dist/modules/apimapper/connections.js +225 -58
  18. package/dist/modules/apimapper/connections.js.map +1 -1
  19. package/dist/modules/apimapper/credentials.js +86 -14
  20. package/dist/modules/apimapper/credentials.js.map +1 -1
  21. package/dist/modules/apimapper/elicitation.d.ts +29 -0
  22. package/dist/modules/apimapper/elicitation.js +62 -0
  23. package/dist/modules/apimapper/elicitation.js.map +1 -1
  24. package/dist/modules/apimapper/example-extract.d.ts +13 -0
  25. package/dist/modules/apimapper/example-extract.js +111 -0
  26. package/dist/modules/apimapper/example-extract.js.map +1 -0
  27. package/dist/modules/apimapper/filter-operators.d.ts +24 -0
  28. package/dist/modules/apimapper/filter-operators.js +103 -0
  29. package/dist/modules/apimapper/filter-operators.js.map +1 -0
  30. package/dist/modules/apimapper/flows-format.js +92 -22
  31. package/dist/modules/apimapper/flows-format.js.map +1 -1
  32. package/dist/modules/apimapper/flows.d.ts +8 -7
  33. package/dist/modules/apimapper/flows.js +216 -44
  34. package/dist/modules/apimapper/flows.js.map +1 -1
  35. package/dist/modules/apimapper/gateway/advanced-read-tool.d.ts +9 -0
  36. package/dist/modules/apimapper/gateway/advanced-read-tool.js +172 -0
  37. package/dist/modules/apimapper/gateway/advanced-read-tool.js.map +1 -0
  38. package/dist/modules/apimapper/gateway/advanced-tool.js +39 -130
  39. package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
  40. package/dist/modules/apimapper/gateway/collect-module-tools.d.ts +17 -0
  41. package/dist/modules/apimapper/gateway/collect-module-tools.js +44 -0
  42. package/dist/modules/apimapper/gateway/collect-module-tools.js.map +1 -0
  43. package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
  44. package/dist/modules/apimapper/gateway/essentials.js +19 -7
  45. package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
  46. package/dist/modules/apimapper/gateway/gateway-shared.d.ts +21 -0
  47. package/dist/modules/apimapper/gateway/gateway-shared.js +124 -0
  48. package/dist/modules/apimapper/gateway/gateway-shared.js.map +1 -0
  49. package/dist/modules/apimapper/gateway/test-support.d.ts +1 -17
  50. package/dist/modules/apimapper/gateway/test-support.js +4 -33
  51. package/dist/modules/apimapper/gateway/test-support.js.map +1 -1
  52. package/dist/modules/apimapper/get-skill-cores.d.ts +4 -0
  53. package/dist/modules/apimapper/get-skill-cores.js +220 -0
  54. package/dist/modules/apimapper/get-skill-cores.js.map +1 -0
  55. package/dist/modules/apimapper/get-skill.d.ts +1 -1
  56. package/dist/modules/apimapper/get-skill.js +30 -3
  57. package/dist/modules/apimapper/get-skill.js.map +1 -1
  58. package/dist/modules/apimapper/graph-builder.d.ts +85 -2
  59. package/dist/modules/apimapper/graph-builder.js +151 -15
  60. package/dist/modules/apimapper/graph-builder.js.map +1 -1
  61. package/dist/modules/apimapper/graph.js +115 -15
  62. package/dist/modules/apimapper/graph.js.map +1 -1
  63. package/dist/modules/apimapper/index.js +25 -13
  64. package/dist/modules/apimapper/index.js.map +1 -1
  65. package/dist/modules/apimapper/jmespath-test.d.ts +4 -0
  66. package/dist/modules/apimapper/jmespath-test.js +152 -0
  67. package/dist/modules/apimapper/jmespath-test.js.map +1 -0
  68. package/dist/modules/apimapper/library.js +131 -8
  69. package/dist/modules/apimapper/library.js.map +1 -1
  70. package/dist/modules/apimapper/list-footer.d.ts +27 -0
  71. package/dist/modules/apimapper/list-footer.js +57 -0
  72. package/dist/modules/apimapper/list-footer.js.map +1 -0
  73. package/dist/modules/apimapper/local-sources.js +88 -31
  74. package/dist/modules/apimapper/local-sources.js.map +1 -1
  75. package/dist/modules/apimapper/mcp-client-identity.d.ts +32 -0
  76. package/dist/modules/apimapper/mcp-client-identity.js +70 -0
  77. package/dist/modules/apimapper/mcp-client-identity.js.map +1 -0
  78. package/dist/modules/apimapper/merge-constants.d.ts +6 -0
  79. package/dist/modules/apimapper/merge-constants.js +26 -0
  80. package/dist/modules/apimapper/merge-constants.js.map +1 -0
  81. package/dist/modules/apimapper/node-schema.d.ts +52 -2
  82. package/dist/modules/apimapper/node-schema.js +95 -4
  83. package/dist/modules/apimapper/node-schema.js.map +1 -1
  84. package/dist/modules/apimapper/onboarding.d.ts +29 -0
  85. package/dist/modules/apimapper/onboarding.js +117 -9
  86. package/dist/modules/apimapper/onboarding.js.map +1 -1
  87. package/dist/modules/apimapper/read-cache.d.ts +16 -3
  88. package/dist/modules/apimapper/read-cache.js +59 -4
  89. package/dist/modules/apimapper/read-cache.js.map +1 -1
  90. package/dist/modules/apimapper/render/index.js +26 -5
  91. package/dist/modules/apimapper/render/index.js.map +1 -1
  92. package/dist/modules/apimapper/resource-id.d.ts +13 -0
  93. package/dist/modules/apimapper/resource-id.js +69 -0
  94. package/dist/modules/apimapper/resource-id.js.map +1 -0
  95. package/dist/modules/apimapper/tool-result.d.ts +20 -0
  96. package/dist/modules/apimapper/tool-result.js +67 -5
  97. package/dist/modules/apimapper/tool-result.js.map +1 -1
  98. package/dist/modules/apimapper/toolslist-size.d.ts +10 -10
  99. package/dist/modules/apimapper/toolslist-size.js +29 -18
  100. package/dist/modules/apimapper/toolslist-size.js.map +1 -1
  101. package/dist/modules/apimapper/types.d.ts +13 -0
  102. package/dist/modules/apimapper/types.js +1 -1
  103. package/dist/modules/apimapper/types.js.map +1 -1
  104. package/dist/modules/apimapper/whitelist-drift.js +16 -1
  105. package/dist/modules/apimapper/whitelist-drift.js.map +1 -1
  106. package/dist/modules/apimapper/workflows.js +221 -32
  107. package/dist/modules/apimapper/workflows.js.map +1 -1
  108. package/dist/modules/apimapper/yootheme-binding.js +103 -22
  109. package/dist/modules/apimapper/yootheme-binding.js.map +1 -1
  110. package/dist/platform/index.js +7 -0
  111. package/dist/platform/index.js.map +1 -1
  112. package/dist/proxy/bridge.d.ts +35 -0
  113. package/dist/proxy/bridge.js +129 -0
  114. package/dist/proxy/bridge.js.map +1 -0
  115. package/dist/proxy/mode.d.ts +9 -0
  116. package/dist/proxy/mode.js +20 -0
  117. package/dist/proxy/mode.js.map +1 -0
  118. package/dist/setup/probe-auth.d.ts +51 -0
  119. package/dist/setup/probe-auth.js +141 -0
  120. package/dist/setup/probe-auth.js.map +1 -0
  121. package/dist/setup-cli.d.ts +9 -0
  122. package/dist/setup-cli.js +34 -0
  123. package/dist/setup-cli.js.map +1 -1
  124. package/dist/sites/loader.d.ts +7 -0
  125. package/dist/sites/loader.js +16 -1
  126. package/dist/sites/loader.js.map +1 -1
  127. package/dist/skill-instructions.d.ts +14 -1
  128. package/dist/skill-instructions.js +30 -6
  129. package/dist/skill-instructions.js.map +1 -1
  130. package/manifest.json +2 -2
  131. package/package.json +3 -2
  132. package/skills/apimapper/SKILL.md +78 -3
  133. package/skills/apimapper/reference/dynamize-existing-layout.md +158 -0
  134. package/skills/apimapper/reference/jmespath-cookbook.md +241 -0
  135. package/skills/apimapper/reference/jmespath-pitfalls.md +81 -0
  136. package/skills/apimapper/reference/library-template-discovery.md +1 -1
  137. package/skills/apimapper/reference/merge-two-sources-on-key.md +117 -12
  138. package/skills/apimapper/reference/oauth.md +143 -52
  139. package/skills/apimapper/reference/troubleshooting.md +2 -2
  140. package/skills/apimapper/reference/yootheme-source-to-builder-handoff.md +348 -0
  141. package/skills/apimapper/reference/yootheme.md +75 -44
@@ -0,0 +1,348 @@
1
+ # Handoff: API Mapper source → YOOtheme Builder element
2
+
3
+ This is the bridge between the two halves of the job: you have a **published API Mapper flow** (built and published on THIS server), and you want it to actually drive a YOOtheme Builder element (Grid / List / Slider / **Map**) on the page. The element-side work happens on the **yt-builder-mcp** server, through its `yootheme_builder_*` tools. This topic walks the exact call sequence and the three things that bite cold agents: the `*_item` binding level, the `field_mappings` full-replace semantics, and the `map_item.location` `'lat,lng'` contract.
4
+
5
+ > **Two servers.** Steps 1–2 are API Mapper (`apimapper_*`). Steps 3–5 are yt-builder-mcp (`yootheme_builder_*`). `apimapper_yootheme_binding_for_flow` is the only tool that spans the boundary — it tells you the exact source name + field names the yt-builder bind call needs.
6
+
7
+ ## The end-to-end sequence
8
+
9
+ ### Step 1 — Publish the flow (API Mapper)
10
+
11
+ ```jsonc
12
+ apimapper_flow_full_recompile_publish({ flow_id: "flow_abc" })
13
+ ```
14
+
15
+ This registers the flow as a YOOtheme source and flushes the schema cache. **Always** use this (not a bare compile) — derived/Transform fields are invisible downstream until a recompile-publish.
16
+
17
+ ### Step 2 — Discover the source name + bindable fields (API Mapper)
18
+
19
+ ```jsonc
20
+ apimapper_yootheme_binding_for_flow({ flow_id: "flow_abc" })
21
+ // → {
22
+ // source_name_singular: "apiMapperFlowAbc",
23
+ // source_name_list: "apiMapperFlowAbcList", // the Multi-Items target
24
+ // fields: [ { name: "title", type: "String" }, { name: "location", ... }, ... ],
25
+ // example_field_mappings: { title: "name", content: "popup_body", ... },
26
+ // ready_for_binding: true
27
+ // }
28
+ ```
29
+
30
+ - `source_name_list` is the **Multi-Items** target (grid / list / slider / map — one container, N children).
31
+ - `source_name_singular` is the single-item wrapper (rare; for a single record).
32
+ - `ready_for_binding: false` means the flow isn't compiled or its output node has no schema fields — fix that before binding.
33
+
34
+ ### Step 3 — Read the element's ETag (yt-builder-mcp)
35
+
36
+ The bind tool is a write and requires an ETag (optimistic lock):
37
+
38
+ ```jsonc
39
+ yootheme_builder_get_etag({ template_id: "tpl_home" })
40
+ // → { etag: "W/\"…\"" }
41
+ ```
42
+
43
+ ### Step 4 — Bind the source on the `*_item` CHILD (yt-builder-mcp)
44
+
45
+ `yootheme_builder_element_bind_source` is a **first-class** tool in `tools/list` (promoted to L1 by F116). Call it **directly**:
46
+
47
+ ```jsonc
48
+ yootheme_builder_element_bind_source({
49
+ template_id: "tpl_home",
50
+ element_path: "…/grid_item", // the *_item child, NOT the grid container
51
+ source_name: "apiMapperFlowAbcList", // the LIST name from step 2
52
+ bindingLevel: "item",
53
+ field_mappings: {
54
+ title: "name",
55
+ content: "popup_body",
56
+ image: "photo"
57
+ },
58
+ etag: "W/\"…\""
59
+ })
60
+ ```
61
+
62
+ > **Older server builds (pre-F116).** If your yt-builder-mcp host plugin predates the F116 promotion, the bind tool is still an *advanced* tool and a direct call returns `unknown tool`. On those builds, reach it through the gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_element_bind_source", arguments: { … } })`. On a current build the gateway wrap returns `unknown_tool` — call the tool directly.
63
+
64
+ ### Step 5 — Publish the page and verify
65
+
66
+ Publish via the yt-builder page tools, then confirm the element renders N children with real field values (not defaults / blanks).
67
+
68
+ ## Rule 1 — Bind on the `*_item` child, never the container
69
+
70
+ For "1 container with N dynamic children", bind `source` + Multi-Items on the **`*_item` child**, never on the container. YT Pro clones the source-bearing element N times as siblings of its parent — binding on a `grid` produces N stacked grids, not 1 grid with N children.
71
+
72
+ | Container | Bind target (the child) |
73
+ |-----------|-------------------------|
74
+ | `grid` | `grid_item` |
75
+ | `list` | `list_item` |
76
+ | `slider` | `slider_item` |
77
+ | `slideshow` | `slideshow_item` |
78
+ | `map` | `map_item` |
79
+
80
+ `bindingLevel: "item"` makes a container `element_path` auto-resolve to its first `*_item` child; `bindingLevel: "container"` binds on the container and the response carries a cloning warning. When the target is already a leaf under a bound `*_item`, the tool auto-uses the YT-Pro INHERIT form (`__node_item__` sentinel) — a standalone query there renders empty on YT5/Joomla.
81
+
82
+ ## Rule 2 — `field_mappings` is a FULL REPLACE, not a merge (F43)
83
+
84
+ Each `yootheme_builder_element_bind_source` call writes `source.props` **from scratch** out of the `field_mappings` you pass — it does **not** merge with the prior binding. Whatever you omit is dropped. So:
85
+
86
+ - To refine one mapping, re-send **all** the mappings you want to keep, every call.
87
+ - A bind call with `field_mappings: { content: "popup_body" }` after one with `{ title: "name", content: "old" }` leaves the element bound to `content` only — `title` is gone.
88
+
89
+ Treat `field_mappings` as the complete desired prop→field map, not a patch.
90
+
91
+ ## Rule 3 — Map needs ONE `'lat,lng'` location field (A9 / F39)
92
+
93
+ A `map_item` positions each pin from a **single** `location` field formatted `"lat,lng"`. Source APIs almost always deliver `lat` and `lng` as **separate** numeric fields, so you must synthesize the combined field in a Transform node BEFORE publishing:
94
+
95
+ ```jsonc
96
+ // Transform projection (API Mapper side, step 0 — before step 1):
97
+ [*].{
98
+ title: name,
99
+ location: join(',', [to_string(lat), to_string(lng)]), // "37.77,-122.41"
100
+ popup_body: name // or a composed derived field — see below
101
+ }
102
+ ```
103
+
104
+ Then bind `location` on the `map_item`:
105
+
106
+ ```jsonc
107
+ yootheme_builder_element_bind_source({
108
+ template_id, element_path: "…/map_item", source_name: "apiMapperFlowAbcList",
109
+ bindingLevel: "item",
110
+ field_mappings: { location: "location", title: "title", content: "popup_body" },
111
+ etag
112
+ })
113
+ ```
114
+
115
+ > **Silent zero-pin trap.** If `location` is left unbound, the Map renders structurally fine and the inspector reports it "correct" — but **zero pins** appear. Always bind `location`, and verify pins after publish.
116
+
117
+ ### Rich popups beyond the 4 slots (F42)
118
+
119
+ A `map_item` popup has only `title` / `meta` / `content` / `image`. For more facets, compose them into one **derived field** in the Transform (`popup_body` above), then bind it to `content` — see `apimapper_get_skill({ topic: "yootheme" })` for the full derived-field recipe.
120
+
121
+ ## Rule 4 — Schema-changing Transform edits force recompile → re-bind (F44)
122
+
123
+ Any Transform edit that adds or renames a field (a new derived `location`, a new `popup_body`) is **invisible** to `apimapper_yootheme_binding_for_flow` and to the yt-builder bind call until you re-publish. The loop is:
124
+
125
+ ```
126
+ edit Transform → apimapper_flow_full_recompile_publish →
127
+ apimapper_yootheme_binding_for_flow (re-read fields) → re-bind (full field_mappings)
128
+ ```
129
+
130
+ Skipping the recompile leaves you binding against the OLD schema — the new field name resolves to nothing and the element renders blank.
131
+
132
+ ## Two binding shapes — pick the one that matches the job
133
+
134
+ Everything above is the **`field_mappings` shape**: you bind a flow into a *fresh* Builder element (a blank grid / list / map you just dropped in) and let `element_bind_source` author the `source.props` for you. That is the right shape for "build me a new grid from this API".
135
+
136
+ There is a SECOND shape you reach for when the goal is **"dynamize an EXISTING, already-designed theme layout"** — the customer hand-built a beautiful section (match rows with logos, badges, a red highlight row) and just wants those *same* rows fed from your flow, pixel-identical. There you do NOT bind through `field_mappings`; you read the layout's own JSON and swap the query. This is the **theme-canonical `#parent` repeater form**, the form YT5 itself emits and renders. (Cold agents have hand-authored it twice successfully; it is documented here so you don't have to reverse-engineer it from a `page_get_layout` dump again.)
137
+
138
+ > **For dynamizing an EXISTING layout, prefer `yootheme_builder_page_dynamize`
139
+ > (one call).** It runs the copy-swap-write-publish below for you — you supply
140
+ > only the section, the source name, and a `leaf_map` of leaf→field names. See
141
+ > `apimapper_get_skill({ topic: "dynamize-existing-layout" })` for the worked
142
+ > example. The `#parent` anatomy below is **background + fallback**: read it to
143
+ > understand what the tool authors by construction, and use it for the
144
+ > hand-edit path when `page_dynamize` is unavailable or can't author a shape you
145
+ > need.
146
+
147
+ ### The `#parent` repeater anatomy
148
+
149
+ An iterating list in a theme layout has three layers:
150
+
151
+ 1. **The repeating element** (a `column`, `panel`, row item, …) carries the source query:
152
+ ```jsonc
153
+ "source": { "query": { "name": "customArticles", "arguments": { … } } }
154
+ ```
155
+ **To dynamize it, swap ONLY that query name** to your published flow's LIST source:
156
+ ```jsonc
157
+ "source": { "query": { "name": "apiMapperFlow<Id>List" } } // from binding_for_flow → source_name_list
158
+ ```
159
+ The swap *is* the dynamization — the rest of the designed markup is untouched.
160
+
161
+ 2. **The child `fragment`** = the clone body. It binds to the parent iteration via the `#parent` sentinel:
162
+ ```jsonc
163
+ "source": { "query": { "name": "#parent" } }
164
+ ```
165
+
166
+ 3. **Each leaf** (the bits that actually print a value) reads `#parent` plus a `props.content` pointing at one field, with formatting filters living **inside the binding**:
167
+ ```jsonc
168
+ "source": {
169
+ "query": { "name": "#parent" },
170
+ "props": { "content": { "name": "field.match_date", "filters": { "date": "D, d.m.y" } } }
171
+ }
172
+ ```
173
+ Field names use dot-paths (`field.match_date`, `club_crest.imagefile`). For a nested object, the leaf uses the subquery form: `"query": { "name": "#parent", "field": { "name": "field.match_home_team", "directives": [ … ] } }`.
174
+
175
+ ### `_condition` only bites with a `filters.condition` operator
176
+
177
+ A leaf or element shows/hides via `_condition` — but `_condition: { name: X }` **alone is wireless** (it renders unconditionally). The operator must ride in `filters.condition`:
178
+
179
+ - `"filters": { "condition": "!" }` → render only when the field is **EMPTY**.
180
+ - `"filters": { "condition": "!!" }` → render only when the field is **FILLED**.
181
+
182
+ ### Boolean fields filter unreliably → use empty/filled STRING semantics
183
+
184
+ Do **not** drive a `_condition` off a boolean flow field (`played`, `is_next`) — boolean binding is unreliable here. Instead, make the flow emit a **string that is empty or filled**, then condition on `!`/`!!`:
185
+
186
+ ```jsonc
187
+ // Transform projection (API Mapper side):
188
+ {
189
+ "result": "(played && score) || ''", // '' when not played → hide score block with '!'
190
+ "next": "(is_next && 'next') || ''" // '' unless next → show the red highlight row with '!!'
191
+ }
192
+ ```
193
+
194
+ So the per-row highlight (exactly one red "next match" row) is just: emit `next` as `''`/`'next'`, then `_condition` with `filters.condition: '!!'` on the highlight element.
195
+
196
+ ### The dynamize-an-existing-layout loop
197
+
198
+ ```
199
+ page_get_layout (read the section's JSON)
200
+ → find the repeating element's source.query.name
201
+ → swap it to apiMapperFlow<Id>List
202
+ → remap each leaf's props.content to your flow's field names (keep the filters)
203
+ → convert any boolean _condition to a '!'/'!!' string condition
204
+ → write the layout back (page create/update)
205
+ ```
206
+
207
+ **This loop is the ONLY path to a design-exact result. COPY the reference
208
+ subtree — NEVER rebuild the design element-by-element with `element_add`.**
209
+ A hand-rebuilt "equivalent" layout renders visibly different (measured on the
210
+ FC-Greenfield probe: 213px row height vs the reference's 90px, wrapped team
211
+ names, missing league icon, missing CTA column) and FAILS any visual judge.
212
+ The reference JSON carries dozens of tuned props (`title_grid_width`,
213
+ `image_align`, grid breakpoints, column `layout` strings) you will not guess.
214
+
215
+ **INSERT NOTHING into the copied subtree either.** Adding even ONE extra
216
+ column/slot (e.g. a new "league position" text next to the matchup) shifts
217
+ every sibling's computed width — measured on a probe: team-name cells
218
+ collapsed from 213px to 55–67px, UIkit fell back to `uk-grid-stack`, names
219
+ wrapped and crests overlapped the text. Extra data goes into EXISTING leaves:
220
+ compose it into a field your flow already feeds a bound leaf with (e.g.
221
+ `"AFC Richmond (3rd)"` as the team-name value, or the meta line) — never into
222
+ a new element. Copy = byte-identical structure; only leaf field NAMES change.
223
+
224
+ **KEEP every leaf you have no flow field for** (a static league/matchday icon,
225
+ decorative images): leave its original binding untouched or feed it a flow
226
+ constant. REMOVE a leaf only when its target cannot exist for the new data
227
+ (e.g. a "go to review" link whose target article doesn't exist for sheet
228
+ rows) — default KEEP, remove only with a reason. And NEVER invent asset paths:
229
+ if you did not read the path from the reference layout or an API response, do
230
+ not guess one (a guessed `…-league-logo.svg` 404s silently).
231
+
232
+ **"But the original binds RELATIONAL/nested fields and my flow is FLAT" is
233
+ NOT a reason to rebuild.** The element STRUCTURE and all its props transfer
234
+ verbatim regardless of the binding shape. Only the leaf `source.props.*.name`
235
+ values change: a nested original leaf like
236
+ `field.match_home_team.club_crest.imagefile` simply becomes your flat field
237
+ name (`home_crest`). Keep every other prop byte-identical. If the original
238
+ leaves bind via `#parent`, keep the `#parent` query form too — just swap the
239
+ field names inside `props`.
240
+
241
+ Practical copy mechanics: take the section subtree from `page_get_layout`,
242
+ perform the swaps above on the JSON, then create the page with
243
+ `yootheme_builder_pages_create({ layout })` (or write a full layout via the
244
+ pages update path). One write — no element-by-element surgery.
245
+
246
+ ### Leaves you have no flow field for: default KEEP, remove only with a reason
247
+
248
+ When you copy the reference subtree you will hit leaves that your flow does
249
+ **not** supply a value for — a static matchday/league icon, a divider image, a
250
+ decorative crest. **The default is KEEP, not delete.** Deleting a leaf changes
251
+ the visual composition the design depends on, so removal needs an explicit
252
+ justification on a per-leaf basis. The two correct ways to keep an unmapped
253
+ leaf:
254
+
255
+ - **Static asset (an image / icon that never varies):** leave its binding
256
+ **exactly as the reference had it** — a static `image` URL or a theme asset
257
+ reference renders the same icon for every cloned row. Do not touch it.
258
+ - **A value the flow can supply as a constant:** bind it to a **flow constant**
259
+ field (emit a fixed string from the Transform, e.g. `"league": 'Premier'`)
260
+ and point the leaf's `props.content` at that field. The icon/value is now
261
+ uniform across rows but flows through the same `#parent` plumbing.
262
+
263
+ **Only REMOVE a leaf when its TARGET cannot exist for the new data.** The
264
+ canonical example from the FC-Greenfield probe: the original row carried a
265
+ `GO TO MATCH REVIEW` link whose `href` resolved to a CMS *article* per match.
266
+ Sheet rows have **no backing article**, so that link target genuinely does not
267
+ exist — removing that one leaf is **CORRECT**. Contrast the league icon in the
268
+ same probe: it is a *static* asset with a target that still exists, so dropping
269
+ it was a **regression** (the R2 visual delta flagged "missing league icon").
270
+ Same probe, opposite verdicts — because one target vanished for sheet data and
271
+ the other did not.
272
+
273
+ Rule of thumb when judging a copy: walk every leaf and ask *"does this leaf's
274
+ target still exist for my data?"* — KEEP if yes (static asset or flow
275
+ constant), REMOVE only when the answer is a concrete no.
276
+
277
+ ### Authoring per-row visibility through `bind` — RESOLVED 2026-06-10 (F96 / F97); F95 still open
278
+
279
+ **F96 / F97 — RESOLVED 2026-06-10.** You CAN now author a *filtered*
280
+ `_condition` (e.g. "show this block only for played matches") directly through
281
+ `element_bind_source` — no separate pre-filtered flows, no hand-authored
282
+ `#parent`-JSON layout write. The fix landed in the yt-builder bind tool
283
+ (`builders.ts` field_mappings, F96/F82, 2026-06-10): a `field_mappings` entry
284
+ may now be an **object** carrying the operator, not just a bare field-name
285
+ string.
286
+
287
+ **The object-form `field_mappings` (the one-call recipe).** A mapping value is
288
+ EITHER the old string (`element_prop: "source_field"`) OR an object
289
+ `{ name, filters }` that rides a `filters.condition` operator straight onto the
290
+ leaf:
291
+
292
+ ```jsonc
293
+ yootheme_builder_element_bind_source({
294
+ template_id: "tpl_home",
295
+ element_path: "…/grid_item", // the *_item child, NOT the container
296
+ source_name: "apiMapperFlowAbcList",
297
+ bindingLevel: "item",
298
+ field_mappings: {
299
+ // plain string mapping — unconditional
300
+ title: "name",
301
+ content: "popup_body",
302
+ // OBJECT mapping — binds the field AND a filtered _condition in ONE call.
303
+ // '!!' → render only when `next_marker` is FILLED (the highlight row).
304
+ highlight: { name: "next_marker", filters: { condition: "!!" } }
305
+ // '!' → render only when the field is EMPTY.
306
+ },
307
+ etag: "W/\"…\""
308
+ })
309
+ ```
310
+
311
+ `handlers-bind.ts` forwards the object-form verbatim and the PHP side
312
+ normalises it, so the `filters.condition` lands as a real, wired `_condition`
313
+ on the leaf — exactly the operator the `#parent`-JSON layout write produces.
314
+ This is the one-call path: **bind the field and its visibility condition
315
+ together**; you no longer need a separate flow per visibility bucket just to get
316
+ per-row show/hide.
317
+
318
+ > Same `'!'` / `'!!'` string-semantics rule as above applies — drive the
319
+ > condition off a flow field that is **empty or filled** (emit `''` / `'next'`),
320
+ > not off a raw boolean (`played`, `is_next`), because boolean binding is
321
+ > unreliable here.
322
+
323
+ **F95 — still OPEN: large inline-layout `pages_create` drops its params.** When
324
+ the inline layout JSON gets large (~10 KB+), the create call's parameters are
325
+ dropped in transit (an empty page is created); a single-row create (~4 KB) goes
326
+ through fine. **Workaround:** write the layout in **smaller chunks** — one
327
+ row-block create that goes through, then add the remaining rows — rather than
328
+ one ~10 KB inline create.
329
+
330
+ ## F81 — Finding the design: it may be a NAMED SECTION inside another article
331
+
332
+ The page the customer calls "the X page" frequently is **not its own template** — it is a **named section embedded inside another article** (e.g. the "Matches" rows live as a named section in the Home article). If `pages_list` / a route lookup doesn't surface it, don't assume it's missing.
333
+
334
+ **Discovery tool:** `yootheme_builder_template_summary` lists a template's `named_sections`. Scan those to locate the design, then `page_get_layout` that template and work on the section's subtree. This turns a multi-scan hunt into one lookup.
335
+
336
+ ## F91 — Article-context cells render ~20% narrower than category templates
337
+
338
+ The SAME layout JSON gets **~20% less cell width** in an **article context** than in a **category template** (measured ≈200px vs ≈253px on the FC match rows). So enrichment bindings you add to a designed row — an extra crest image, a meta line, a second value in a panel — can **wrap** in the article context even though they fit in the category template the design was lifted from.
339
+
340
+ - After adding any enrichment binding (logo + meta on a panel, a second score line), **verify visually at the target viewport** — a text/DOM check will pass while the row visibly wraps.
341
+ - If it wraps, drop the enrichment or move it to a slot with more room; do not assume "fits in the source template ⇒ fits here".
342
+
343
+ ## Related topics
344
+
345
+ - `apimapper_get_skill({ topic: "yootheme" })` — publishing flows as sources; the derived-field rich-popup recipe.
346
+ - `apimapper_get_skill({ topic: "merge-two-sources-on-key" })` — when the source itself is a merge of two APIs.
347
+ - `apimapper_get_skill({ topic: "jmespath-pitfalls" })` — Transform projection traps + the date/number primitives used to synthesize derived fields.
348
+ - `apimapper_get_skill({ topic: "jmespath-cookbook" })` — copy-paste recipes, incl. the two-transform split for the depth limit you hit when composing the string-condition fields above.
@@ -2,6 +2,8 @@
2
2
 
3
3
  API Mapper sources show up in the YOOtheme Builder Source dropdown ONLY when a flow ends in a YOOtheme Output node and the flow is **published** (not just saved). This is the most common "source isn't appearing" cause.
4
4
 
5
+ > **REST tools only.** Build flows with the real `apimapper_*` REST tools below — `flow_setup_with_sources`, `flow_create`, `flow_update`, `flow_full_recompile_publish`, `yootheme_binding_for_flow`. This server has no UI-automation node-editing tools (the `add-node` / `configure-output` style tools belong to a separate UI MCP and are not callable here). To go from a published source into the actual Builder element, see `apimapper_get_skill({ topic: "yootheme-source-to-builder-handoff" })`.
6
+
5
7
  ## The publish pipeline
6
8
 
7
9
  ```
@@ -16,49 +18,82 @@ Source ─► [Filter] ─► [Transform] ─► [Merge] ─► Output
16
18
 
17
19
  Both output types can coexist on the same flow.
18
20
 
19
- ## Step-by-step
21
+ ## Step-by-step (REST)
20
22
 
21
- 1. **Build the flow** via `apimapper_flow_create` + `apimapper_app_add_node` calls, or one-shot via `apimapper_flow_setup_with_sources`.
23
+ 1. **Build the flow.** The one-shot path lays out source → (merge) → (filter) → output for you, including the YOOtheme output node:
22
24
 
23
- 2. **Add a YOOtheme Output node** (NOT Shortcode):
24
- ```
25
- apimapper_app_configure_output(
26
- flow_id="flow_abc",
27
- output_type="yootheme",
28
- source_name="Marketing Posts"
29
- )
25
+ ```jsonc
26
+ apimapper_flow_setup_with_sources({
27
+ name: "Marketing Posts",
28
+ sources: [{ connection: "conn_blog" }],
29
+ output: { type: "yootheme", name: "Marketing Posts" }
30
+ })
30
31
  ```
31
- The `source_name` is what appears in the YOOtheme Source dropdown. Keep it short and human-readable.
32
32
 
33
- 3. **Compile** to validate schema + graph:
34
- ```
35
- apimapper_flow_compile(flow_id="flow_abc")
36
- ```
37
- This catches:
38
- - Disconnected nodes
39
- - Type drift between Transform output and Output input
40
- - Missing required fields
41
- - Filter expressions referencing fields that don't exist on the upstream
33
+ The output `name` is what appears in the YOOtheme Source dropdown. Keep it short and human-readable. For full control over nodes/edges use `apimapper_flow_create`; to edit an existing graph use `apimapper_flow_update` (advanced — call via `apimapper_advanced({ tool: "apimapper_flow_update", arguments: {…} })`).
34
+
35
+ 2. **Publish (compile + register + invalidate cache) in one call:**
42
36
 
43
- 4. **Publish**:
37
+ ```jsonc
38
+ apimapper_flow_full_recompile_publish({ flow_id: "flow_abc" })
44
39
  ```
45
- apimapper_flow_full_recompile_publish(flow_id="flow_abc")
40
+
41
+ This compiles the graph (catching disconnected nodes, type drift, missing fields, and filter expressions referencing non-existent fields), writes the flow into the YOOtheme Source registry, and invalidates the YOOtheme schema cache. Prefer this over a bare compile — a plain `apimapper_flow_compile` is gateway-only and does not force a schema-cache flush.
42
+
43
+ 3. **Discover the exact source name + bindable fields:**
44
+
45
+ ```jsonc
46
+ apimapper_yootheme_binding_for_flow({ flow_id: "flow_abc" })
47
+ // → { source_name_singular, source_name_list, fields, ready_for_binding, ... }
46
48
  ```
47
- This writes the flow into the YOOtheme Source registry and invalidates the YOOtheme schema cache.
48
49
 
49
- 5. **Verify in YOOtheme Builder**:
50
- - Open any YOOtheme page or template.
51
- - Add a Grid / Slider / Dynamic element.
52
- - "Source" dropdown → look for **the name you set in step 2** ("Marketing Posts"), NOT a "API Mapper" entry.
50
+ 4. **Verify in the YOOtheme Builder:** open any page/template, add a Grid / Slider / Map element, and look for **the name you set in step 1** ("Marketing Posts") in the Source dropdown — NOT a "API Mapper" entry.
51
+
52
+ ## Binding the published source into a Builder element
53
+
54
+ Once published, the source is bound to a Builder element through the **yt-builder-mcp** server, not here. The full bridge (recompile → `binding_for_flow` → `yootheme_builder_element_bind_source` with `bindingLevel`, `field_mappings`, and the Multi-Items `*_item` rule) lives in its own topic:
55
+
56
+ ```jsonc
57
+ apimapper_get_skill({ topic: "yootheme-source-to-builder-handoff" })
58
+ ```
53
59
 
54
60
  ## Schema for YOOtheme dynamic content
55
61
 
56
- YOOtheme reads the published flow's output schema (introspected by `apimapper_flow_detect_schema`). Each field on the schema becomes a mappable property in the YOOtheme element editor (title, image, link, date, …).
62
+ YOOtheme reads the published flow's output schema. Each field on the schema becomes a mappable property in the YOOtheme element editor (title, image, link, date, …).
63
+
64
+ To control which fields YOOtheme sees, use a `Transform` node with an explicit JMESPath projection — e.g. `[*].{title: name, image: cover.url, link: permalink}`. The Visual Expression Builder in the UI produces equivalent expressions; both end up in the same compiled flow.
65
+
66
+ ### Rich popups / multi-facet displays via a DERIVED Transform field (F42)
67
+
68
+ A `map_item` popup (and several other item slots) exposes only a small fixed set of bind targets — `title`, `meta`, `content`, `image`. When the customer wants **more facets than there are slots** (e.g. price + beds + baths + rating + review-count all in one popup), do **not** look for extra slots — compose them into a single **derived field** in the Transform node, then bind that one field to `content`:
69
+
70
+ ```jsonc
71
+ // Transform projection — synthesize a composite field the schema will expose.
72
+ [*].{
73
+ title: name,
74
+ image: photo,
75
+ location: join(',', [to_string(lat), to_string(lng)]), // map_item.location wants ONE "lat,lng" field
76
+ popup_body: join(' · ', [
77
+ join('', ['$', to_string(price)]),
78
+ join(' ', [to_string(beds), 'bd']),
79
+ join(' ', [to_string(baths), 'ba']),
80
+ join('', ['★', to_string(rating)])
81
+ ])
82
+ }
83
+ ```
57
84
 
58
- To control which fields YOOtheme sees:
59
- - Use a `Transform` node with an explicit JMESPath/expression projection.
60
- - `[*].{title: name, image: cover.url, link: permalink}` is a typical canonical projection.
61
- - The Visual Expression Builder in the UI produces equivalent expressions; both end up in the same compiled flow.
85
+ After re-publishing, `popup_body` is a first-class field you bind to the popup's `content` slot. This is the canonical pattern for "5+ facets, 4 slots". (For the `'lat,lng'` `location` contract and the map binding itself, see the handoff topic.)
86
+
87
+ > Derived fields are invisible to `apimapper_yootheme_binding_for_flow` until you re-run `apimapper_flow_full_recompile_publish`. Change the projection recompile re-read bindings re-bind.
88
+
89
+ ## The map sequence (F46) — ETag-locked bind + publish, in order
90
+
91
+ A Map element is wired with the same Multi-Items discipline as Grid/Slider, plus the coordinate + popup specifics. The end-to-end sequence:
92
+
93
+ 1. `apimapper_flow_full_recompile_publish({ flow_id })` — publish the source (with the derived `location` + popup fields above).
94
+ 2. `apimapper_yootheme_binding_for_flow({ flow_id })` — get `source_name_list` + field names.
95
+ 3. In yt-builder-mcp, read the element's ETag, then bind on the **`map_item` child** (never the `map` container) by calling `yootheme_builder_element_bind_source` directly (a first-class tool in tools/list), mapping `location` to the single `'lat,lng'` field and the popup slots to your fields. The exact call is in the handoff topic.
96
+ 4. Publish the page and verify markers render. If `location` is unbound the Map renders structurally fine with **zero pins** — bind `location` or no markers appear.
62
97
 
63
98
  ## Where YOOtheme features live (NOT in the flow)
64
99
 
@@ -72,25 +107,21 @@ These belong in YOOtheme, not in API Mapper:
72
107
  | Pagination | YOOtheme element + URL handling |
73
108
  | Per-page query overrides | YOOtheme element bindings (`source`) |
74
109
 
75
- API Mapper's job is to deliver typed clean JSON. The render-time slicing / sorting / filtering lives in YOOtheme. Don't reimplement it in a Transform node — it just bloats the flow and creates two truth sources.
110
+ API Mapper's job is to deliver typed clean JSON. Render-time slicing / sorting / filtering lives in YOOtheme — don't reimplement it in a Transform node.
76
111
 
77
112
  ## Shortcode output (WordPress only)
78
113
 
79
- Same flow can ALSO emit a Shortcode output (`output_type="shortcode"`). Use this for:
80
- - Embedding in classic editor posts
81
- - Using outside YOOtheme (page builders, plugins)
82
- - Quick previews in admin
83
-
84
- Shortcode currently supports a subset of the YOOtheme render-time features. See `https://wootsup.com/docs/mcp/api-mapper/` (the shortcode subpage will land in a later release).
114
+ The same flow can ALSO emit a Shortcode output (`output.type = "shortcode"`). Use this for embedding in classic editor posts, using outside YOOtheme, or quick admin previews. Shortcode currently supports a subset of the YOOtheme render-time features.
85
115
 
86
116
  ## Common pitfalls
87
117
 
88
- 1. **Save vs Publish** — saving stores the flow in `wp_options` (or Joomla equivalent) but does NOT register it as a YOOtheme Source. Always publish.
89
- 2. **Looking for "API Mapper" in the dropdown** — wrong. Sources appear under the **flow's published source_name**.
90
- 3. **Stale YOOtheme schema cache** — after a major schema change (Transform projection updated, new field added), clear YOOtheme cache: `yootheme_clear_cache` (sibling MCP) or via WP-Admin YOOtheme Settings → Clear Cache.
91
- 4. **Republishing without recompile** schema drift won't propagate. Use `apimapper_flow_full_recompile_publish`, not `apimapper_flow_publish`.
92
- 5. **Dynamic content fields show as empty** — usually case sensitivity. YOOtheme expects field names lowercase; the Transform projection must produce lowercase keys. Field-Cards in the UI handle this automatically.
118
+ 1. **Save vs Publish** — saving stores the flow but does NOT register it as a YOOtheme Source. Always publish via `apimapper_flow_full_recompile_publish`.
119
+ 2. **Looking for "API Mapper" in the dropdown** — wrong. Sources appear under the **flow's published source name**.
120
+ 3. **Stale YOOtheme schema cache** — `apimapper_flow_full_recompile_publish` flushes it; a bare publish or compile may not. Always use the recompile-publish tool after a schema change.
121
+ 4. **Derived fields missing after a Transform edit** re-run `apimapper_flow_full_recompile_publish`, then re-read `apimapper_yootheme_binding_for_flow`. New fields are invisible to binding until recompile.
122
+ 5. **Dynamic content fields show as empty** — usually case sensitivity. YOOtheme expects lowercase field names; the Transform projection must produce lowercase keys.
123
+ 6. **Map with zero pins** — the `map_item.location` slot is unbound, or `location` is two separate lat/lng fields instead of one `'lat,lng'` field. Synthesize a single `location` field in the Transform.
93
124
 
94
125
  ## Multi-platform behaviour
95
126
 
96
- WordPress and Joomla produce identical YOOtheme schemas — the Source/Type registration goes through the same YOOtheme PHP API. But the underlying REST surface differs (see `skill://apimapper/joomla` for the Joomla envelope). Tools auto-detect the platform and route correctly.
127
+ WordPress and Joomla produce identical YOOtheme schemas — the Source/Type registration goes through the same YOOtheme PHP API. The underlying REST surface differs (see `apimapper_get_skill({ topic: "joomla" })`). Tools auto-detect the platform and route correctly.