@wootsup/yt-builder-mcp 1.10.0 → 1.12.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 +12 -11
- package/bin/yt-builder-mcp.js +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
- package/dist/catalog/tool-catalog-markdown.js +13 -1
- package/dist/catalog/tool-catalog-markdown.js.map +1 -1
- package/dist/cli/doctor-command.d.ts.map +1 -1
- package/dist/cli/doctor-command.js +22 -2
- package/dist/cli/doctor-command.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/clients/index.d.ts +1 -1
- package/dist/clients/index.js +1 -1
- package/dist/diagnostics/ca-reexec.js +2 -2
- package/dist/diagnostics/diagnose-network-error.d.ts +1 -1
- package/dist/diagnostics/doctor.d.ts +34 -2
- package/dist/diagnostics/doctor.d.ts.map +1 -1
- package/dist/diagnostics/doctor.js +63 -2
- package/dist/diagnostics/doctor.js.map +1 -1
- package/dist/diagnostics/startup-self-probe.js +1 -1
- package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
- package/dist/gateway/advanced-read-tool.js +14 -2
- package/dist/gateway/advanced-read-tool.js.map +1 -1
- package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/discovery.js +12 -1
- package/dist/gateway/advanced-tool/discovery.js.map +1 -1
- package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/domains.js +5 -0
- package/dist/gateway/advanced-tool/domains.js.map +1 -1
- package/dist/gateway/advanced-tool/execute.d.ts +7 -1
- package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/execute.js +58 -4
- package/dist/gateway/advanced-tool/execute.js.map +1 -1
- package/dist/gateway/essentials.d.ts +4 -4
- package/dist/gateway/essentials.d.ts.map +1 -1
- package/dist/gateway/essentials.js +12 -7
- package/dist/gateway/essentials.js.map +1 -1
- package/dist/install-skill.js +1 -1
- package/dist/net/extra-headers.d.ts +1 -1
- package/dist/net/extra-headers.js +1 -1
- package/dist/net/internal-host.js +2 -2
- package/dist/net/proxy-dispatcher.js +1 -1
- package/dist/net/proxy-dispatcher.js.map +1 -1
- package/dist/net/site-basic-auth.d.ts +2 -2
- package/dist/net/site-basic-auth.js +2 -2
- package/dist/proxy/bridge.d.ts +2 -2
- package/dist/proxy/bridge.js +3 -3
- package/dist/proxy/bridge.js.map +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +3 -3
- package/dist/setup-cli.d.ts +2 -2
- package/dist/setup-cli.js +2 -2
- package/dist/setup-cli.js.map +1 -1
- package/dist/setup-npx-spec.d.ts +1 -1
- package/dist/setup-npx-spec.js +1 -1
- package/dist/setup-wizard-handshake.js +1 -1
- package/dist/setup-wizard-types.d.ts +1 -1
- package/dist/setup-wizard.js +3 -3
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sites/client-pool.d.ts +1 -1
- package/dist/sites/client-pool.js +1 -1
- package/dist/sites/tools/sites-list.d.ts.map +1 -1
- package/dist/sites/tools/sites-list.js +26 -4
- package/dist/sites/tools/sites-list.js.map +1 -1
- package/dist/sites/tools/sites-test.js +1 -1
- package/dist/sites/tools/sites-test.js.map +1 -1
- package/dist/sites/tools/use-site.js +2 -2
- package/dist/sites/tools/use-site.js.map +1 -1
- package/dist/tools/budgeted-table.d.ts +21 -0
- package/dist/tools/budgeted-table.d.ts.map +1 -0
- package/dist/tools/budgeted-table.js +137 -0
- package/dist/tools/budgeted-table.js.map +1 -0
- package/dist/tools/elements/builders.d.ts.map +1 -1
- package/dist/tools/elements/builders.js +102 -40
- package/dist/tools/elements/builders.js.map +1 -1
- package/dist/tools/elements/handlers-write.d.ts +21 -0
- package/dist/tools/elements/handlers-write.d.ts.map +1 -1
- package/dist/tools/elements/handlers-write.js +231 -11
- package/dist/tools/elements/handlers-write.js.map +1 -1
- package/dist/tools/elements/handlers.d.ts +20 -12
- package/dist/tools/elements/handlers.d.ts.map +1 -1
- package/dist/tools/elements/handlers.js +77 -26
- package/dist/tools/elements/handlers.js.map +1 -1
- package/dist/tools/elements/schema-validation.d.ts +147 -7
- package/dist/tools/elements/schema-validation.d.ts.map +1 -1
- package/dist/tools/elements/schema-validation.js +508 -28
- package/dist/tools/elements/schema-validation.js.map +1 -1
- package/dist/tools/format/elements-format.d.ts.map +1 -1
- package/dist/tools/format/elements-format.js +56 -12
- package/dist/tools/format/elements-format.js.map +1 -1
- package/dist/tools/format/health-format.d.ts +16 -0
- package/dist/tools/format/health-format.d.ts.map +1 -1
- package/dist/tools/format/health-format.js +9 -0
- package/dist/tools/format/health-format.js.map +1 -1
- package/dist/tools/format/inspection-format.d.ts +7 -0
- package/dist/tools/format/inspection-format.d.ts.map +1 -1
- package/dist/tools/format/inspection-format.js +18 -0
- package/dist/tools/format/inspection-format.js.map +1 -1
- package/dist/tools/format/pages-format.d.ts +1 -1
- package/dist/tools/format/pages-format.d.ts.map +1 -1
- package/dist/tools/format/pages-format.js +7 -0
- package/dist/tools/format/pages-format.js.map +1 -1
- package/dist/tools/format/sources-format.d.ts +12 -5
- package/dist/tools/format/sources-format.d.ts.map +1 -1
- package/dist/tools/format/sources-format.js +31 -6
- package/dist/tools/format/sources-format.js.map +1 -1
- package/dist/tools/health.d.ts.map +1 -1
- package/dist/tools/health.js +85 -11
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspection.d.ts +34 -0
- package/dist/tools/inspection.d.ts.map +1 -1
- package/dist/tools/inspection.js +666 -62
- package/dist/tools/inspection.js.map +1 -1
- package/dist/tools/layout-flatten.d.ts +7 -1
- package/dist/tools/layout-flatten.d.ts.map +1 -1
- package/dist/tools/layout-flatten.js +24 -13
- package/dist/tools/layout-flatten.js.map +1 -1
- package/dist/tools/library.d.ts +29 -2
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +247 -34
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/local-content/builders.js +14 -14
- package/dist/tools/local-content/builders.js.map +1 -1
- package/dist/tools/local-content/handlers.d.ts +3 -1
- package/dist/tools/local-content/handlers.d.ts.map +1 -1
- package/dist/tools/local-content/handlers.js +42 -9
- package/dist/tools/local-content/handlers.js.map +1 -1
- package/dist/tools/local-content/schemas.d.ts +9 -4
- package/dist/tools/local-content/schemas.d.ts.map +1 -1
- package/dist/tools/local-content/schemas.js +14 -6
- package/dist/tools/local-content/schemas.js.map +1 -1
- package/dist/tools/multi-items/builders.d.ts.map +1 -1
- package/dist/tools/multi-items/builders.js +11 -10
- package/dist/tools/multi-items/builders.js.map +1 -1
- package/dist/tools/navigation/builders.d.ts +18 -0
- package/dist/tools/navigation/builders.d.ts.map +1 -0
- package/dist/tools/navigation/builders.js +47 -0
- package/dist/tools/navigation/builders.js.map +1 -0
- package/dist/tools/navigation/handlers.d.ts +30 -0
- package/dist/tools/navigation/handlers.d.ts.map +1 -0
- package/dist/tools/navigation/handlers.js +41 -0
- package/dist/tools/navigation/handlers.js.map +1 -0
- package/dist/tools/navigation/index.d.ts +15 -0
- package/dist/tools/navigation/index.d.ts.map +1 -0
- package/dist/tools/navigation/index.js +14 -0
- package/dist/tools/navigation/index.js.map +1 -0
- package/dist/tools/navigation/schemas.d.ts +46 -0
- package/dist/tools/navigation/schemas.d.ts.map +1 -0
- package/dist/tools/navigation/schemas.js +41 -0
- package/dist/tools/navigation/schemas.js.map +1 -0
- package/dist/tools/pages/builders.d.ts.map +1 -1
- package/dist/tools/pages/builders.js +213 -81
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/pages/handlers-audit.d.ts +112 -3
- package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
- package/dist/tools/pages/handlers-audit.js +623 -7
- package/dist/tools/pages/handlers-audit.js.map +1 -1
- package/dist/tools/pages/handlers-read.d.ts +51 -8
- package/dist/tools/pages/handlers-read.d.ts.map +1 -1
- package/dist/tools/pages/handlers-read.js +348 -69
- package/dist/tools/pages/handlers-read.js.map +1 -1
- package/dist/tools/pages/handlers-write.d.ts +7 -3
- package/dist/tools/pages/handlers-write.d.ts.map +1 -1
- package/dist/tools/pages/handlers-write.js +69 -25
- package/dist/tools/pages/handlers-write.js.map +1 -1
- package/dist/tools/pages/header-transparency.d.ts +64 -0
- package/dist/tools/pages/header-transparency.d.ts.map +1 -0
- package/dist/tools/pages/header-transparency.js +119 -0
- package/dist/tools/pages/header-transparency.js.map +1 -0
- package/dist/tools/pages/schemas.d.ts +284 -13
- package/dist/tools/pages/schemas.d.ts.map +1 -1
- package/dist/tools/pages/schemas.js +434 -26
- package/dist/tools/pages/schemas.js.map +1 -1
- package/dist/tools/pages/target.d.ts +162 -0
- package/dist/tools/pages/target.d.ts.map +1 -0
- package/dist/tools/pages/target.js +239 -0
- package/dist/tools/pages/target.js.map +1 -0
- package/dist/tools/response-budget.d.ts +77 -0
- package/dist/tools/response-budget.d.ts.map +1 -0
- package/dist/tools/response-budget.js +134 -0
- package/dist/tools/response-budget.js.map +1 -0
- package/dist/tools/shared-schemas.d.ts +45 -0
- package/dist/tools/shared-schemas.d.ts.map +1 -1
- package/dist/tools/shared-schemas.js +50 -0
- package/dist/tools/shared-schemas.js.map +1 -1
- package/dist/tools/sources/builders.d.ts.map +1 -1
- package/dist/tools/sources/builders.js +55 -39
- package/dist/tools/sources/builders.js.map +1 -1
- package/dist/tools/sources/handlers-bind.d.ts +10 -11
- package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
- package/dist/tools/sources/handlers-bind.js +110 -9
- package/dist/tools/sources/handlers-bind.js.map +1 -1
- package/dist/tools/sources/handlers.d.ts +4 -2
- package/dist/tools/sources/handlers.d.ts.map +1 -1
- package/dist/tools/sources/handlers.js +11 -7
- package/dist/tools/sources/handlers.js.map +1 -1
- package/dist/tools/sparse-fields.d.ts +61 -8
- package/dist/tools/sparse-fields.d.ts.map +1 -1
- package/dist/tools/sparse-fields.js +124 -12
- package/dist/tools/sparse-fields.js.map +1 -1
- package/dist/tools/sublayout/builders.d.ts.map +1 -1
- package/dist/tools/sublayout/builders.js +1 -2
- package/dist/tools/sublayout/builders.js.map +1 -1
- package/dist/tools/tool-builder/results.d.ts +19 -6
- package/dist/tools/tool-builder/results.d.ts.map +1 -1
- package/dist/tools/tool-builder/results.js +133 -6
- package/dist/tools/tool-builder/results.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/skills/yt-builder-mcp/SKILL.md +1713 -72
package/dist/tools/inspection.js
CHANGED
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import { detailResult
|
|
11
|
+
import { detailResult } from '@getimo/mcp-toolkit';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { BIND_VIA_GATEWAY_EXAMPLE } from './bind-guidance.js';
|
|
14
14
|
import { TYPES_COMPACT_COLUMNS, TYPES_TABLE_COLUMNS, buildTypeSchemaDetail, flattenTypesPayload, mapTypeRow, } from './format/inspection-format.js';
|
|
15
|
-
import { SITE_ID_SCHEMA } from './shared-schemas.js';
|
|
15
|
+
import { INCLUDE_META_SCHEMA, SITE_ID_SCHEMA } from './shared-schemas.js';
|
|
16
16
|
import { resolveSiteOrError } from './pool-resolve-helper.js';
|
|
17
17
|
import { defineTool, errorResult, readOnly, structuredResult, withSiteMeta, } from './tool-builder.js';
|
|
18
|
-
import { DEFAULT_FIELDS_TYPES_LIST, FIELDS, projectFields, projectedFieldsEcho, projectionFeedback, projectionWarning, } from './sparse-fields.js';
|
|
18
|
+
import { DEFAULT_FIELDS_TYPES_LIST, FIELDS, FIELD_NAMES, FIELD_NAME_CONTAINS, MAX_CHARS, applyMaxChars, projectFields, projectedFieldsEcho, projectionFeedback, projectionWarning, selectDescriptorsByName, } from './sparse-fields.js';
|
|
19
19
|
import { ITEM_CHILDREN_OF_CONTAINER } from './multi-items/item-container-map.js';
|
|
20
|
+
import { budgetedTableResult } from "./budgeted-table.js";
|
|
21
|
+
import { RESPONSE_BUDGET } from "./response-budget.js";
|
|
20
22
|
// ─── outputSchemas (Wave G.2 §4) ─────────────────────────────────────
|
|
21
23
|
const TYPES_LIST_OUTPUT_SCHEMA = z.object({
|
|
22
24
|
items: z.array(z.record(z.string(), z.unknown())),
|
|
@@ -38,7 +40,173 @@ const TYPE_SCHEMA_OUTPUT_SCHEMA = z.object({
|
|
|
38
40
|
// field descriptors — each `name` is a valid prop key for element_add /
|
|
39
41
|
// _update_settings.
|
|
40
42
|
fields: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
43
|
+
// Task 7b (plan 2026-07-27-02): the fieldset groups' own sentences, stated
|
|
44
|
+
// ONCE each and joined on a field's `group`. YOOtheme parks the semantics of
|
|
45
|
+
// a whole class of fields there rather than on the field
|
|
46
|
+
// (grid.image_width/image_height carry the crop rule this way and declare
|
|
47
|
+
// nothing themselves), so a caller that reads only a descriptor's
|
|
48
|
+
// `description` misses the rule it needed. Narrows with the descriptor
|
|
49
|
+
// selection, so a `field_names` read carries only the groups it named.
|
|
50
|
+
//
|
|
51
|
+
// The RULE is stated here rather than in the 600-char tool description,
|
|
52
|
+
// which names only the key. Same carrier choice, and the same reason, as the
|
|
53
|
+
// `site_id` param description in `docs/mcp-response-budgets.md` §9.2: a
|
|
54
|
+
// schema `.describe()` rides in the wire `tools/list` payload (measured
|
|
55
|
+
// there at 19.2 % of it for `outputSchema`), so this states MORE while
|
|
56
|
+
// costing the tool-description budget nothing.
|
|
57
|
+
groups: z
|
|
58
|
+
.array(z.object({
|
|
59
|
+
name: z.string().describe('Matches a field descriptor\'s `group`.'),
|
|
60
|
+
description: z
|
|
61
|
+
.string()
|
|
62
|
+
.describe("The fieldset group's own sentence, as YOOtheme ships it."),
|
|
63
|
+
}))
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Fieldset groups that carry a rule of their own, stated once each. ' +
|
|
66
|
+
'YOOtheme parks the semantics of a whole class of fields on the ' +
|
|
67
|
+
'GROUP rather than on the field — grid.image_width/image_height ' +
|
|
68
|
+
'declare no description and the crop rule lives here — so a ' +
|
|
69
|
+
'descriptor with no `description` is not undocumented, you are ' +
|
|
70
|
+
'reading at the wrong level. Join on the field\'s `group`. ' +
|
|
71
|
+
'Narrows with field_names/name_contains.'),
|
|
72
|
+
// Task 7b Step 4 (plan 2026-07-27-02, inventory groups B, A, L). NOT narrowed
|
|
73
|
+
// by a field_names/name_contains selection: these describe the element TYPE,
|
|
74
|
+
// and the caller asking for two fields of a 154-field type is exactly the
|
|
75
|
+
// caller about to write those two fields.
|
|
76
|
+
//
|
|
77
|
+
// The `.describe()` texts below are deliberately terse. They carry ONLY what
|
|
78
|
+
// a caller cannot read off the arrays themselves — the CNF semantics, the
|
|
79
|
+
// deletion consequence, the `&&`/`parent.` grammar, and what each
|
|
80
|
+
// media_autoswap key means. Everything that IS in the data (which props, which
|
|
81
|
+
// flags, which elements invert the precedence) is stated per element in the
|
|
82
|
+
// RESPONSE, where it costs bytes once per call instead of once per session in
|
|
83
|
+
// every host's tools/list.
|
|
84
|
+
//
|
|
85
|
+
// MEASURED, 2026-07-28: a first draft of these describes added 1953 B to the
|
|
86
|
+
// wire tools/list (136422 B → 138375 B, `tests/perf/token-baseline.test.ts`)
|
|
87
|
+
// and made this tool the largest entry on the server. Spending ~2 KB of every
|
|
88
|
+
// session's discovery payload to narrate four keys defeats the point of
|
|
89
|
+
// shipping them as arrays, so the prose was cut to the non-derivable facts.
|
|
90
|
+
renders_only_if: z
|
|
91
|
+
.array(z.array(z.string()))
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('ALL groups need >=1 non-empty member or YOOtheme DELETES this element at ' +
|
|
94
|
+
'render (no error; an emptied parent then collapses too). Applies to ' +
|
|
95
|
+
'PRESENT-but-empty props; for ABSENT ones see placeholder_fallback. A ' +
|
|
96
|
+
"member may be `a&&b`; `parent.` prefixes the container's prop."),
|
|
97
|
+
placeholder_fallback: z
|
|
98
|
+
.object({ suppressed_by: z.array(z.string()), fills: z.array(z.string()) })
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Why renders_only_if often does NOT delete: leave every suppressed_by key ' +
|
|
101
|
+
"UNSET and YOOtheme merges its placeholder (prerender, front end too), so " +
|
|
102
|
+
'fills render as lorem ipsum/"Title"/a placeholder image on the LIVE page. ' +
|
|
103
|
+
'Write any suppressed_by key, even "", to suppress the merge and get deletion.'),
|
|
104
|
+
// 2026-07-28. The shipped text said only "it BLANKS (not CSS-hide)", which
|
|
105
|
+
// rules out ONE wrong reading and leaves the other: a caller that turns off
|
|
106
|
+
// a container's `show_title`, watches its titles disappear, and reads
|
|
107
|
+
// "BLANKS" concludes its write was destroyed — and reaches for the one
|
|
108
|
+
// repair that cannot work, re-writing props that were never lost.
|
|
109
|
+
//
|
|
110
|
+
// MEASURED, YOOtheme Pro 5.0.37 (a two-item `grid`, written with
|
|
111
|
+
// element_update_settings + page_publish, read back with page_get_layout and
|
|
112
|
+
// from the published page): `show_title:false` took rendered `.el-title` from
|
|
113
|
+
// 2 to 0 with neither title string anywhere in the section HTML, while the
|
|
114
|
+
// grid's stored children subtree stayed byte-identical (equal sha256, 699 B,
|
|
115
|
+
// `diff` exit 0) and the whole-layout diff was ONE added line — the flag, on
|
|
116
|
+
// the GRID node, nothing on any child. Flipping it back restored the baseline
|
|
117
|
+
// render byte-for-byte. Stripped to `link` alone the items left the output
|
|
118
|
+
// entirely (`.el-item` 2 -> 0, `.uk-grid` 2 -> 1) while the read-back still
|
|
119
|
+
// carried `link` and `link_text` in full. The full measurement, including the
|
|
120
|
+
// source-bound case, is in SKILL.md; the pin is in
|
|
121
|
+
// tests/tools/inspection-blanked-by-parent-describe.test.ts.
|
|
122
|
+
//
|
|
123
|
+
// The two halves are BOTH here, and deliberately not split with SKILL.md,
|
|
124
|
+
// because the audience the defect reached is the cold agent reading one tool
|
|
125
|
+
// response with no skill installed.
|
|
126
|
+
blanked_by_parent: z
|
|
127
|
+
.record(z.string(), z.array(z.string()))
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('Container show_* flag -> props of THIS element the RENDER blanks (not ' +
|
|
130
|
+
'CSS-hide). Blanks the OUTPUT only: your stored value survives and the ' +
|
|
131
|
+
'flag is reversible. Runs before renders_only_if, so clearing one can ' +
|
|
132
|
+
'drop the whole item — intersect the two to see which.'),
|
|
133
|
+
media_autoswap: z
|
|
134
|
+
.object({
|
|
135
|
+
precedence: z.array(z.string()),
|
|
136
|
+
sniffed: z.array(z.string()).optional(),
|
|
137
|
+
clears_on_conflict: z.array(z.string()).optional(),
|
|
138
|
+
})
|
|
139
|
+
.optional()
|
|
140
|
+
.describe('Image/video collision, per element — not uniform across YOOtheme. ' +
|
|
141
|
+
'precedence: first non-empty renders. sniffed: URL-sniffed, value MOVED ' +
|
|
142
|
+
'between image/video. clears_on_conflict: prop SET FALSE, not just skipped.'),
|
|
143
|
+
// Task 7b Steps 5-6 (plan 2026-07-27-02, inventory groups P/M/D/E/K/S/O/N/F).
|
|
144
|
+
// Two keys, one mechanism: an id list that costs ~14 B per reference, and the
|
|
145
|
+
// note texts stated ONCE per response. The alternative measured at 2.5-3.0 KB
|
|
146
|
+
// (the inventory's per-prop cost table) because `link_striptags` alone is
|
|
147
|
+
// referenced by 17 (type, prop) pairs.
|
|
148
|
+
//
|
|
149
|
+
// These two `.describe()` texts say only what a caller CANNOT read off one
|
|
150
|
+
// response: that `rules` is a POINTER (an opaque id is useless without knowing
|
|
151
|
+
// where it resolves), why the pointed-at rule is worth following, and the
|
|
152
|
+
// narrowing asymmetry (element-level ids always resolve, field-level ones only
|
|
153
|
+
// while their field is returned — invisible from a single response).
|
|
154
|
+
//
|
|
155
|
+
// MEASURED, 2026-07-28: a first draft of these two texts (495 chars) cost
|
|
156
|
+
// 704 B of the wire tools/list and made this tool the largest entry on the
|
|
157
|
+
// server. The full prose lives in SKILL.md's `rules` + `rule_notes` section
|
|
158
|
+
// instead, where it costs the wire nothing and is ledger-PINNED
|
|
159
|
+
// (skill-md-behavior-truth.test.ts) so it cannot silently vanish — the same
|
|
160
|
+
// carrier choice, and the same reason, as `placeholder`/`enum_labels` in the
|
|
161
|
+
// tool description above.
|
|
162
|
+
rules: z
|
|
163
|
+
.array(z.string())
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('Shared render-rule ids — resolve each in `rule_notes`. Each marks a case ' +
|
|
166
|
+
'where the write succeeds and the render differs; read before writing ' +
|
|
167
|
+
'that prop.'),
|
|
168
|
+
rule_notes: z
|
|
169
|
+
.record(z.string(), z.string())
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('id -> rule text, for the ids this response references. Element-level ids ' +
|
|
172
|
+
'always resolve; a field-level one only while its field is returned.'),
|
|
173
|
+
// 2026-07-28. Found by LIVE verification: both 4.5.33 sites (wp-dev,
|
|
174
|
+
// joomla-dev — their own yootheme_builder_health says so) were served
|
|
175
|
+
// `no_effect: "No effect in YOOtheme Pro 5.0.37 …"`. The claim was read in a
|
|
176
|
+
// 5.0.37 tree; nobody read theirs. Every version-scoped key in this response
|
|
177
|
+
// now names the build it was read on, and `status` says whether that is the
|
|
178
|
+
// build you are running.
|
|
179
|
+
//
|
|
180
|
+
// `keys` is the BOUND, and it is why the describe below can say "those
|
|
181
|
+
// ONLY". This response also carries `binding_contract`, whose `child_type`
|
|
182
|
+
// comes from ItemContainerMap — read on YT-Pro 4.5.33, a DIFFERENT build,
|
|
183
|
+
// with a per-build difference recorded in that file's own docblock. The
|
|
184
|
+
// first draft described this record as covering "the version-scoped keys
|
|
185
|
+
// here", which vouched for it. Listing the covered keys ends that by
|
|
186
|
+
// construction rather than by prose kept in sync with a growing surface.
|
|
187
|
+
//
|
|
188
|
+
// The rule rides in this `.describe()` rather than the 600-char tool
|
|
189
|
+
// description for the same reason `groups` does — a schema describe is on the
|
|
190
|
+
// wire tools/list payload and costs the tool-description budget nothing
|
|
191
|
+
// (`docs/mcp-response-budgets.md` §9.2).
|
|
192
|
+
claims_verified_on: z
|
|
193
|
+
.object({
|
|
194
|
+
build: z.string(),
|
|
195
|
+
here: z.string().nullable(),
|
|
196
|
+
status: z.enum(['match', 'differs', 'unknown']),
|
|
197
|
+
keys: z.array(z.string()),
|
|
198
|
+
})
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Build the keys named in `keys` were READ on vs this site — those ' +
|
|
201
|
+
'ONLY, never the whole response. "differs"/"unknown" = NOT ' +
|
|
202
|
+
're-checked here, so they are reports, not facts, and no_effect ' +
|
|
203
|
+
'ships as no_effect_unverified.'),
|
|
204
|
+
// The descriptors the TYPE declares — unchanged by a selection, so a caller
|
|
205
|
+
// always knows the size of what it did not ask for (Task 4 / P2).
|
|
41
206
|
field_count: z.number(),
|
|
207
|
+
// The descriptors THIS response carries. Equal to `field_count` when no
|
|
208
|
+
// `field_names`/`name_contains` selection was made.
|
|
209
|
+
returned_count: z.number(),
|
|
42
210
|
// Wave-6 R3 (2026-05-29): binding contract annotation. Surfaces the
|
|
43
211
|
// Multi-Items binding pattern for container types (grid/slider/switcher/
|
|
44
212
|
// list/accordion/gallery/panel/overlay), and identifies content-binding
|
|
@@ -270,6 +438,234 @@ function deriveBindingContractBase(elementType) {
|
|
|
270
438
|
* (tests/tools/inspection-minimal-layout-bare.test.ts).
|
|
271
439
|
*/
|
|
272
440
|
export const __test_deriveBindingContract = deriveBindingContract;
|
|
441
|
+
/**
|
|
442
|
+
* ELEMENT_WRITE-4 (part 1) — the node-level keys the YT schema serializer lists
|
|
443
|
+
* in `fields[]` even though they are SIBLINGS of `props` on the node, not props.
|
|
444
|
+
* Writing `props.name` via element_add is silently accepted (the key IS in
|
|
445
|
+
* fields[]) but never renders as the builder label. Annotating them steers the
|
|
446
|
+
* agent to set them OUTSIDE props (a first-class `name` arg is the cross-file
|
|
447
|
+
* follow-up owned by the element_add builder schema).
|
|
448
|
+
*/
|
|
449
|
+
const NODE_LEVEL_FIELD_KEYS = new Set(['name', 'status', 'source', 'id']);
|
|
450
|
+
/**
|
|
451
|
+
* Annotate node-level descriptors (`name`/`status`/`source`/`id`) with
|
|
452
|
+
* `group:'node-level'` + a value_hint so the agent knows they live OUTSIDE
|
|
453
|
+
* `props`. Non-destructive: returns a shallow-cloned array; other descriptors
|
|
454
|
+
* pass through untouched.
|
|
455
|
+
*
|
|
456
|
+
* `group:'node-level'` OVERRIDES whatever group the descriptor arrived with.
|
|
457
|
+
* That is a change from "only fill an ABSENT group", and it is load-bearing:
|
|
458
|
+
* Task 7b Step 1 (plan 2026-07-27-02) taught the PHP projection to read
|
|
459
|
+
* YOOtheme's fieldset TREE, and YOOtheme does list some of these keys inside a
|
|
460
|
+
* UI group. MEASURED over the real element.php configs: `column.source` and
|
|
461
|
+
* `column.id` now arrive as `{"group":"Advanced"}`, which under the old
|
|
462
|
+
* fill-if-absent rule SUPPRESSED the node-level marker on exactly the two keys
|
|
463
|
+
* whose mis-write is silent (`props.source` is ignored; the real `source` is a
|
|
464
|
+
* sibling of `props`). "This key is not a prop" outranks "this key sits in the
|
|
465
|
+
* Advanced panel" — the first prevents a silent no-op, the second is navigation.
|
|
466
|
+
* The `value_hint` carried the warning either way, but a consumer branching on
|
|
467
|
+
* `group` would have lost it.
|
|
468
|
+
*
|
|
469
|
+
* Safe against the two synthetic groups the PHP side assigns
|
|
470
|
+
* (`runtime-accepted` on button.content/link, `responsive-width` on
|
|
471
|
+
* column.width_*): neither list contains any of these four names, so no
|
|
472
|
+
* deliberate group is clobbered.
|
|
473
|
+
*/
|
|
474
|
+
function annotateNodeLevelFields(fields) {
|
|
475
|
+
return fields.map((field) => {
|
|
476
|
+
const name = field.name;
|
|
477
|
+
if (typeof name !== 'string' || !NODE_LEVEL_FIELD_KEYS.has(name)) {
|
|
478
|
+
return field;
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
...field,
|
|
482
|
+
group: 'node-level',
|
|
483
|
+
value_hint: typeof field.value_hint === 'string' && field.value_hint !== ''
|
|
484
|
+
? field.value_hint
|
|
485
|
+
: 'builder label / node metadata — lives OUTSIDE `props` (a SIBLING of ' +
|
|
486
|
+
'props on the node). Do NOT set it as props.' + name + ' via ' +
|
|
487
|
+
'element_update_settings; it is silently ignored there.',
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Narrow the root `groups` list to the groups the SELECTED descriptors name.
|
|
493
|
+
*
|
|
494
|
+
* Returns `undefined` when the backend sent no usable list, so a schema without
|
|
495
|
+
* groups (and a payload from an older backend) adds no key and the response
|
|
496
|
+
* stays byte-identical to before. The PHP transport's twin is
|
|
497
|
+
* `McpElementTypeGetSchemaHandler::narrowGroups()`.
|
|
498
|
+
*/
|
|
499
|
+
function narrowGroupsToSelection(raw, selected) {
|
|
500
|
+
if (!Array.isArray(raw))
|
|
501
|
+
return undefined;
|
|
502
|
+
const groups = [];
|
|
503
|
+
for (const entry of raw) {
|
|
504
|
+
if (entry === null || typeof entry !== 'object')
|
|
505
|
+
continue;
|
|
506
|
+
const { name, description } = entry;
|
|
507
|
+
if (typeof name === 'string' && name !== '' && typeof description === 'string') {
|
|
508
|
+
groups.push({ name, description });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (groups.length === 0)
|
|
512
|
+
return undefined;
|
|
513
|
+
if (selected === undefined)
|
|
514
|
+
return groups;
|
|
515
|
+
const named = new Set();
|
|
516
|
+
for (const field of selected) {
|
|
517
|
+
if (typeof field.group === 'string')
|
|
518
|
+
named.add(field.group);
|
|
519
|
+
}
|
|
520
|
+
return groups.filter((g) => named.has(g.name));
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Lift the three element-level render rules off the backend payload.
|
|
524
|
+
*
|
|
525
|
+
* Every branch is shape-VALIDATED rather than cast, so a malformed or older
|
|
526
|
+
* backend contributes no key and the response stays byte-identical to before —
|
|
527
|
+
* the same fail-quiet contract as `narrowGroupsToSelection`. A half-valid
|
|
528
|
+
* `media_autoswap` (no `precedence`) is dropped whole rather than forwarded
|
|
529
|
+
* partially: `precedence` is the fact, and the optional keys qualify it.
|
|
530
|
+
*/
|
|
531
|
+
function extractRenderRules(schema) {
|
|
532
|
+
const out = {};
|
|
533
|
+
const isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
|
|
534
|
+
const gate = schema.renders_only_if;
|
|
535
|
+
if (Array.isArray(gate) && gate.length > 0 && gate.every(isStringList)) {
|
|
536
|
+
out.renders_only_if = gate;
|
|
537
|
+
}
|
|
538
|
+
const blanked = schema.blanked_by_parent;
|
|
539
|
+
if (blanked !== null && typeof blanked === 'object' && !Array.isArray(blanked)) {
|
|
540
|
+
const map = {};
|
|
541
|
+
for (const [flag, props] of Object.entries(blanked)) {
|
|
542
|
+
if (isStringList(props))
|
|
543
|
+
map[flag] = props;
|
|
544
|
+
}
|
|
545
|
+
if (Object.keys(map).length > 0)
|
|
546
|
+
out.blanked_by_parent = map;
|
|
547
|
+
}
|
|
548
|
+
const media = schema.media_autoswap;
|
|
549
|
+
if (media !== null && typeof media === 'object' && !Array.isArray(media)) {
|
|
550
|
+
const { precedence, sniffed, clears_on_conflict } = media;
|
|
551
|
+
if (isStringList(precedence) && precedence.length > 0) {
|
|
552
|
+
out.media_autoswap = {
|
|
553
|
+
precedence,
|
|
554
|
+
...(isStringList(sniffed) ? { sniffed } : {}),
|
|
555
|
+
...(isStringList(clears_on_conflict) ? { clears_on_conflict } : {}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// An EMPTY `fills` is meaningful (the merge fires but adds nothing non-empty,
|
|
560
|
+
// so the gate still drops the node) and is forwarded. A missing or malformed
|
|
561
|
+
// `suppressed_by` is absence: it is the half that decides absent-vs-empty, and
|
|
562
|
+
// half a rule here turns that decision into a guess.
|
|
563
|
+
const fallback = schema.placeholder_fallback;
|
|
564
|
+
if (fallback !== null && typeof fallback === 'object' && !Array.isArray(fallback)) {
|
|
565
|
+
const { suppressed_by, fills } = fallback;
|
|
566
|
+
if (isStringList(suppressed_by) && suppressed_by.length > 0 && isStringList(fills)) {
|
|
567
|
+
out.placeholder_fallback = { suppressed_by, fills };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Which YOOtheme build the version-scoped keys in this response were READ on,
|
|
574
|
+
* versus what the site reports (2026-07-28).
|
|
575
|
+
*
|
|
576
|
+
* Found by live verification: `element_type_get_schema({element_type:'grid',
|
|
577
|
+
* field_names:['lightbox_image_orientation'], site_id:'wp-dev'})` returned
|
|
578
|
+
* `no_effect: "No effect in YOOtheme Pro 5.0.37 …"` on a site whose own
|
|
579
|
+
* `yootheme_builder_health` reports `"yootheme_version":"4.5.33"`. Same on
|
|
580
|
+
* `joomla-dev`. The PHP side now frames such a claim for the build it is
|
|
581
|
+
* actually talking to; this lifts the record onto the MCP surface so the
|
|
582
|
+
* framing survives the hop.
|
|
583
|
+
*
|
|
584
|
+
* Fail-quiet and shape-VALIDATED like {@link extractRenderRules}: a backend that
|
|
585
|
+
* predates the record (any deployed 1.11.0, which is every site today) sends no
|
|
586
|
+
* key, and the response stays byte-identical to before. `here` is `null` in the
|
|
587
|
+
* `unknown` stance — a real value, so it is validated as nullable rather than
|
|
588
|
+
* dropped, because "we could not resolve it" is exactly the fact worth shipping.
|
|
589
|
+
*
|
|
590
|
+
* ── `keys` IS REQUIRED, NOT OPTIONAL ───────────────────────────────────────
|
|
591
|
+
* `keys` is the record's BOUND: the response keys it vouches for. Without it the
|
|
592
|
+
* record is an unbounded statement about the whole response — and this function's
|
|
593
|
+
* own caller composes `binding_contract` into that response from
|
|
594
|
+
* `ItemContainerMap::MAP`, read on YT-Pro 4.5.33, a DIFFERENT build from the one
|
|
595
|
+
* the record names. A record that reached the wire unbounded would therefore
|
|
596
|
+
* assert exactly the thing it exists to prevent. No shipped plugin emits the
|
|
597
|
+
* unbounded shape (the record has never been released), so requiring the bound
|
|
598
|
+
* costs no compatibility and closes the hole by construction.
|
|
599
|
+
*/
|
|
600
|
+
function extractClaimProvenance(schema) {
|
|
601
|
+
const raw = schema.claims_verified_on;
|
|
602
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
603
|
+
return undefined;
|
|
604
|
+
const { build, here, status, keys } = raw;
|
|
605
|
+
if (typeof build !== 'string' || build === '')
|
|
606
|
+
return undefined;
|
|
607
|
+
if (status !== 'match' && status !== 'differs' && status !== 'unknown')
|
|
608
|
+
return undefined;
|
|
609
|
+
if (here !== null && typeof here !== 'string')
|
|
610
|
+
return undefined;
|
|
611
|
+
if (!Array.isArray(keys) || keys.length === 0)
|
|
612
|
+
return undefined;
|
|
613
|
+
if (!keys.every((k) => typeof k === 'string'))
|
|
614
|
+
return undefined;
|
|
615
|
+
return { build, here, status, keys };
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Task 7b Steps 5-6 (plan 2026-07-27-02, inventory groups P, M, D, E, K, S, O, N,
|
|
619
|
+
* F) — the SHARED render-rule notes: `rules` (a list of ids, on the element and on
|
|
620
|
+
* each field) resolved by `rule_notes` (id → the one shared sentence set).
|
|
621
|
+
*
|
|
622
|
+
* Distinct from {@link extractRenderRules} above, which forwards the four
|
|
623
|
+
* machine-readable Step-4 arrays (`renders_only_if` & co.). These are the nine
|
|
624
|
+
* groups an agent cannot deduce and that have no array form — prose, paid for
|
|
625
|
+
* ONCE per response instead of once per referencing prop.
|
|
626
|
+
*
|
|
627
|
+
* Narrowing is the point of this function. `rule_notes` must carry exactly the
|
|
628
|
+
* ids the RESPONSE still references, so a `field_names` read of two descriptors
|
|
629
|
+
* out of 154 does not ship the notes of the 152 it dropped — that would undo
|
|
630
|
+
* Task 4 for the narrow reads it exists to serve, which is the same failure
|
|
631
|
+
* {@link narrowGroupsToSelection} exists to prevent. The element's OWN `rules`
|
|
632
|
+
* always survive: they describe the container the caller is about to write into,
|
|
633
|
+
* not any one descriptor.
|
|
634
|
+
*
|
|
635
|
+
* Read off `selected` (the descriptors this response carries) rather than the
|
|
636
|
+
* sub-key-projected `fields`, for the same reason `groups` is: `fields:["type"]`
|
|
637
|
+
* strips the `rules` key itself, and narrowing on the projected list would then
|
|
638
|
+
* delete every note the caller can still see referenced nowhere.
|
|
639
|
+
*
|
|
640
|
+
* A referenced id with no note is dropped rather than forwarded as a dangling
|
|
641
|
+
* pointer, and a note nobody references is dropped rather than forwarded as
|
|
642
|
+
* dead weight.
|
|
643
|
+
*/
|
|
644
|
+
function extractSharedRuleNotes(schema, selected) {
|
|
645
|
+
const isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
|
|
646
|
+
const elementRules = isStringList(schema.rules) ? schema.rules : [];
|
|
647
|
+
const referenced = new Set(elementRules);
|
|
648
|
+
for (const field of selected ?? []) {
|
|
649
|
+
const refs = field.rules;
|
|
650
|
+
if (isStringList(refs)) {
|
|
651
|
+
for (const id of refs)
|
|
652
|
+
referenced.add(id);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const rawNotes = schema.rule_notes;
|
|
656
|
+
const notes = {};
|
|
657
|
+
if (rawNotes !== null && typeof rawNotes === 'object' && !Array.isArray(rawNotes)) {
|
|
658
|
+
for (const [id, text] of Object.entries(rawNotes)) {
|
|
659
|
+
if (typeof text === 'string' && text !== '' && referenced.has(id)) {
|
|
660
|
+
notes[id] = text;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
...(elementRules.length > 0 ? { rules: elementRules } : {}),
|
|
666
|
+
...(Object.keys(notes).length > 0 ? { rule_notes: notes } : {}),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
273
669
|
export function buildInspectionTools(pool) {
|
|
274
670
|
return [
|
|
275
671
|
defineTool({
|
|
@@ -282,15 +678,17 @@ export function buildInspectionTools(pool) {
|
|
|
282
678
|
// headline `content`) — call element_type_get_schema first.
|
|
283
679
|
'Keywords: "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. ' +
|
|
284
680
|
'Prop keys differ per element (grid_item `title` vs headline `content`) — ' +
|
|
285
|
-
'use element_type_get_schema before element_add/bind. ' +
|
|
286
|
-
'
|
|
681
|
+
'use element_type_get_schema before element_add/bind. Container rows carry ' +
|
|
682
|
+
'`requires_child_type` (grid→grid_item); `has_children`/`has_children_support` ' +
|
|
683
|
+
'are aliases (same value).',
|
|
287
684
|
inputSchema: {
|
|
288
685
|
site_id: SITE_ID_SCHEMA,
|
|
289
686
|
fields: FIELDS,
|
|
687
|
+
include_meta: INCLUDE_META_SCHEMA,
|
|
290
688
|
},
|
|
291
689
|
outputSchema: TYPES_LIST_OUTPUT_SCHEMA,
|
|
292
690
|
annotations: readOnly('List Element Types'),
|
|
293
|
-
handler: async ({ site_id, fields }) => {
|
|
691
|
+
handler: async ({ site_id, fields, include_meta }) => {
|
|
294
692
|
const r = await resolveSiteOrError(pool, site_id);
|
|
295
693
|
if (!r.ok)
|
|
296
694
|
return r.error;
|
|
@@ -299,18 +697,28 @@ export function buildInspectionTools(pool) {
|
|
|
299
697
|
const data = await siteClient.get('/element-types');
|
|
300
698
|
const flat = flattenTypesPayload(data);
|
|
301
699
|
const mapped = flat.map(mapTypeRow);
|
|
302
|
-
|
|
700
|
+
// ELEMENT_WRITE-26: surface the container→item pairing per row
|
|
701
|
+
// (grid→grid_item, button→button_item) so the Multi-Items child
|
|
702
|
+
// requirement is visible WITHOUT opening each type's schema.
|
|
703
|
+
// Present only on container rows; leaf/item rows stay unchanged.
|
|
704
|
+
const mappedRecords = mapped.map((row) => {
|
|
705
|
+
const child = CONTAINER_TO_ITEM_TYPE[row.name];
|
|
706
|
+
return child !== undefined
|
|
707
|
+
? { ...row, requires_child_type: child }
|
|
708
|
+
: { ...row };
|
|
709
|
+
});
|
|
303
710
|
const items = projectFields(mappedRecords, fields, DEFAULT_FIELDS_TYPES_LIST);
|
|
304
711
|
const echo = projectedFieldsEcho(fields, DEFAULT_FIELDS_TYPES_LIST);
|
|
305
712
|
const feedback = projectionFeedback(mappedRecords, fields);
|
|
306
713
|
// F23 (2026-06-09): shared loud-warning helper.
|
|
307
714
|
const unknownNote = projectionWarning(feedback);
|
|
308
|
-
const toolkitResult =
|
|
715
|
+
const toolkitResult = budgetedTableResult(mappedRecords, {
|
|
309
716
|
columns: [...TYPES_TABLE_COLUMNS],
|
|
310
717
|
compactColumns: [...TYPES_COMPACT_COLUMNS],
|
|
311
718
|
header: (count) => `${String(count)} element types${unknownNote}`,
|
|
312
719
|
footer: 'Use yootheme_builder_element_type_get_schema <name> for fields.',
|
|
313
|
-
|
|
720
|
+
includeMeta: include_meta,
|
|
721
|
+
}, RESPONSE_BUDGET.BULKY);
|
|
314
722
|
return withSiteMeta(structuredResult(toolkitResult, {
|
|
315
723
|
items,
|
|
316
724
|
total: items.length,
|
|
@@ -365,6 +773,19 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
365
773
|
.describe('DEPRECATED alias of `element_type` — use `element_type` ' +
|
|
366
774
|
'instead. Kept for 1.0.x backward-compatibility; will be ' +
|
|
367
775
|
'removed in a future major.'),
|
|
776
|
+
// ELEMENT_WRITE-17 / SOURCES_BINDING-23: this was the only read tool with
|
|
777
|
+
// NO way to bound its response (grid returns ~154 field descriptors in one
|
|
778
|
+
// blob). `fields[]` sparse-projects each field descriptor to the requested
|
|
779
|
+
// sub-keys; `max_chars` caps the text leg — mirroring the sibling reads.
|
|
780
|
+
fields: FIELDS,
|
|
781
|
+
// Plan 2026-07-27-02 Task 4 (P1, closes F8): SELECT which descriptors
|
|
782
|
+
// come back, instead of receiving all 154 with 88.6 % of them cut off
|
|
783
|
+
// (measured: `docs/mcp-response-size-baseline.md` §2). Orthogonal to
|
|
784
|
+
// `fields[]` — that one picks sub-keys OF a descriptor, these pick the
|
|
785
|
+
// descriptors themselves, and a selected one is returned COMPLETE.
|
|
786
|
+
field_names: FIELD_NAMES,
|
|
787
|
+
name_contains: FIELD_NAME_CONTAINS,
|
|
788
|
+
max_chars: MAX_CHARS,
|
|
368
789
|
};
|
|
369
790
|
const TYPE_SCHEMA_REFINED = z
|
|
370
791
|
.object(TYPE_SCHEMA_SHAPE)
|
|
@@ -375,12 +796,77 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
375
796
|
});
|
|
376
797
|
return defineTool({
|
|
377
798
|
name: 'yootheme_builder_element_type_get_schema',
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
799
|
+
// 544 chars — the per-tool budget is 600
|
|
800
|
+
// (`tests/gateway/description-length.test.ts`). Task 4 bought the room
|
|
801
|
+
// for the selector by tightening prose, not by dropping a fact; Task 6
|
|
802
|
+
// bought the room for `text?`/`description?` the same way. The five
|
|
803
|
+
// edits that paid for it are pure wording, no fact removed: "Returns
|
|
804
|
+
// each field as" → "Each field:", "prop keys are rejected" → "prop keys
|
|
805
|
+
// rejected", "hidden in the UI" → "hidden in UI", "for
|
|
806
|
+
// containers/items" → "(containers/items)", and "valid_prop_keys +
|
|
807
|
+
// did_you_mean" → "valid_prop_keys+did_you_mean" (the same `a+b` form
|
|
808
|
+
// "value_hint+enum" already uses two clauses later).
|
|
809
|
+
//
|
|
810
|
+
// Task 5b Step 3 (2026-07-28) — RESTORED "boolean". Task 4's tightening
|
|
811
|
+
// pass went one edit too far: `checkbox = boolean true/false` became
|
|
812
|
+
// `checkbox = true/false` (commit 139cb7d68). That deleted the word
|
|
813
|
+
// carrying the VALUE TYPE, and this clause was the only statement of a
|
|
814
|
+
// checkbox prop's type anywhere on the tool surface — an agent writing
|
|
815
|
+
// `element_update_settings({props:{image_expand:"1"}})` had nothing left
|
|
816
|
+
// to tell it the field wants a boolean rather than a truthy string.
|
|
817
|
+
// Naming the two values is not naming the type. Cost of the restore: 8
|
|
818
|
+
// chars, paid for many times over by dropping the repeated site_id
|
|
819
|
+
// sentence (57 chars here; the fact now rides in the `site_id` param
|
|
820
|
+
// description, which is where a host reads it while building the call).
|
|
821
|
+
// Pinned in tests/skills/skill-md-behavior-truth.test.ts (Task 5b block).
|
|
822
|
+
// Task 7b Steps 1+3 (plan 2026-07-27-02): the descriptor grew two
|
|
823
|
+
// DECISION-RELEVANT keys, so the illustrative shape names them.
|
|
824
|
+
// - `groups[]` — the enclosing fieldset group's own sentence, at the
|
|
825
|
+
// schema ROOT and joined on the field's `group`. YOOtheme parks the
|
|
826
|
+
// semantics of a whole class of fields there rather than on the field
|
|
827
|
+
// (grid.image_width/image_height carry the crop rule this way and
|
|
828
|
+
// declare nothing themselves), so a caller that reads only
|
|
829
|
+
// `description` misses the rule it needed. It is the one key of the
|
|
830
|
+
// set NOT self-describing from a field descriptor — a field naming
|
|
831
|
+
// `group:"Width/Height"` gives no reason to look at a root key — so
|
|
832
|
+
// this is where the budget is spent. The RULE rides in the
|
|
833
|
+
// `outputSchema` `.describe()`, which is on the wire and costs this
|
|
834
|
+
// budget nothing (`docs/mcp-response-budgets.md` §9.2).
|
|
835
|
+
// Was `group_description?` per field until the hoist measured 32
|
|
836
|
+
// occurrences of 5 distinct strings.
|
|
837
|
+
// - `no_effect` — the prop is declared and read by nothing. Naming it
|
|
838
|
+
// here is what stops an agent treating the descriptor as writable
|
|
839
|
+
// guidance; the VALUE carries the full read fact + the build it was
|
|
840
|
+
// read on, so no further prose is spent in this budget.
|
|
841
|
+
// The shape list is illustrative, not exhaustive (it has never named
|
|
842
|
+
// `default`/`enable`/`show`/`filter`/`constraint`). `placeholder` and
|
|
843
|
+
// `enum_labels` are therefore documented in SKILL.md rather than
|
|
844
|
+
// here — the budget is nearly spent, and both are self-describing once
|
|
845
|
+
// seen in a response, sitting next to the key they qualify
|
|
846
|
+
// (`enum_labels` beside `enum`).
|
|
847
|
+
// Space for the two additions was earned inside this same description,
|
|
848
|
+
// not by raising the budget: "Fetch a type's prop schema before" →
|
|
849
|
+
// "Prop schema; read before", "unknown prop keys" → "unknown keys"
|
|
850
|
+
// (the word `prop` is already in the opening clause), and "Read
|
|
851
|
+
// value_hint+enum first" → "value_hint+enum first". MEASURED: 544 →
|
|
852
|
+
// 595 chars, inside the 600-char tool-description budget.
|
|
853
|
+
// No fact left: every clause the pre-change description carried is still
|
|
854
|
+
// here, and the pinned phrases are VERBATIM — "checkbox = boolean
|
|
855
|
+
// true/false", "Select descriptors: field_names/name_contains",
|
|
856
|
+
// "field_count/returned_count", "Bound size with fields[]/max_chars".
|
|
857
|
+
// ("Bound size:" was tried and correctly REJECTED by the P4 max_chars
|
|
858
|
+
// pin in tests/skills/skill-md-behavior-truth.test.ts — the four chars
|
|
859
|
+
// were found elsewhere rather than by weakening the pin.)
|
|
860
|
+
description: 'Prop schema; read before element_add/element_update_settings: ' +
|
|
861
|
+
'unknown keys rejected (error lists valid_prop_keys+' +
|
|
862
|
+
'did_you_mean). Field: {name,type,label?,text?,description?,enum?,' +
|
|
863
|
+
'value_hint?,group?,no_effect?} + groups[] + ' +
|
|
864
|
+
'field_count/returned_count + binding_contract ' +
|
|
865
|
+
'(containers/items). value_hint+enum first: some defaults are SEO-fatal ' +
|
|
866
|
+
'(headline.title_element→h1); checkbox = boolean true/false; ' +
|
|
867
|
+
'no_effect = read by nothing, never write it; ' +
|
|
868
|
+
'group:"runtime-accepted" = honoured at render, hidden in UI. Select ' +
|
|
869
|
+
'descriptors: field_names/name_contains. Bound size with fields[]/max_chars.',
|
|
384
870
|
inputSchema: TYPE_SCHEMA_SHAPE,
|
|
385
871
|
// F-203 follow-up: the refined object is what the SDK actually
|
|
386
872
|
// validates. The raw shape above stays for handler-type inference.
|
|
@@ -424,64 +910,182 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
424
910
|
const label = typeof schema.label === 'string' ? schema.label : undefined;
|
|
425
911
|
const origin = typeof schema.origin === 'string' ? schema.origin : undefined;
|
|
426
912
|
// `fields` is a list of {name,type,label?} field descriptors.
|
|
427
|
-
const
|
|
913
|
+
const rawFields = Array.isArray(schema.fields)
|
|
428
914
|
? schema.fields
|
|
429
915
|
: undefined;
|
|
430
|
-
|
|
916
|
+
// ELEMENT_WRITE-4 (part 1): the node-level keys {name,status,
|
|
917
|
+
// source,id} are siblings of `props`, NOT props — but they render
|
|
918
|
+
// in the Fields table with no group, so an agent writes props.name
|
|
919
|
+
// via element_add and it is silently accepted (name is a fields[]
|
|
920
|
+
// entry). Annotate them so the agent knows they live OUTSIDE props.
|
|
921
|
+
const annotatedFields = rawFields !== undefined ? annotateNodeLevelFields(rawFields) : undefined;
|
|
922
|
+
// ELEMENT_WRITE-17 / SOURCES_BINDING-23: project each descriptor to
|
|
923
|
+
// the requested sub-keys when `fields[]` was passed (bounds the
|
|
924
|
+
// structured leg); `max_chars` bounds the text leg below.
|
|
925
|
+
const fieldsParam = Array.isArray(args.fields) &&
|
|
926
|
+
args.fields.every((f) => typeof f === 'string')
|
|
927
|
+
? args.fields
|
|
928
|
+
: undefined;
|
|
929
|
+
const maxChars = typeof args.max_chars === 'number' ? args.max_chars : undefined;
|
|
930
|
+
// Task 4 (P1): SELECT the descriptors first, THEN sub-key-project
|
|
931
|
+
// them. The order is load-bearing — `fields:["type"]` strips the
|
|
932
|
+
// very `name`/`label` the selector matches on, so projecting first
|
|
933
|
+
// would make the two arguments mutually exclusive instead of
|
|
934
|
+
// composable.
|
|
935
|
+
const fieldNamesParam = Array.isArray(args.field_names) &&
|
|
936
|
+
args.field_names.every((f) => typeof f === 'string')
|
|
937
|
+
? args.field_names
|
|
938
|
+
: undefined;
|
|
939
|
+
const nameContains = typeof args.name_contains === 'string' ? args.name_contains : undefined;
|
|
940
|
+
const selected = annotatedFields !== undefined
|
|
941
|
+
? selectDescriptorsByName(annotatedFields, fieldNamesParam, nameContains)
|
|
942
|
+
: undefined;
|
|
943
|
+
const fields = selected !== undefined
|
|
944
|
+
? projectFields(selected, fieldsParam, [])
|
|
945
|
+
: undefined;
|
|
946
|
+
// `field_count` keeps its meaning — the descriptors the TYPE has —
|
|
947
|
+
// so a call with no selector is unchanged. `returned_count` is what
|
|
948
|
+
// this response actually carries, so a selection is never silent
|
|
949
|
+
// (P2) and the caller can see what it did not ask for.
|
|
950
|
+
const fieldCount = annotatedFields !== undefined ? annotatedFields.length : 0;
|
|
951
|
+
const returnedCount = selected !== undefined ? selected.length : 0;
|
|
952
|
+
// Task 7b (plan 2026-07-27-02): each fieldset group's sentence is
|
|
953
|
+
// stated ONCE at the schema root and joined on the field's
|
|
954
|
+
// `group` (MEASURED: 32 occurrences of 5 distinct strings, 7334 B
|
|
955
|
+
// carrying 1204 B of payload). Narrow it with the selection —
|
|
956
|
+
// otherwise a two-descriptor read of a 154-field type ships every
|
|
957
|
+
// group sentence the type has, undoing Task 4 for exactly the
|
|
958
|
+
// narrow reads it exists to serve. Read off `selected`, i.e.
|
|
959
|
+
// BEFORE the `fields[]` sub-key whitelist: `fields:["type"]`
|
|
960
|
+
// strips the join key, and narrowing on the projected list would
|
|
961
|
+
// then delete every sentence. Same ordering rule, same reason, as
|
|
962
|
+
// selection-before-projection above.
|
|
963
|
+
const groups = narrowGroupsToSelection(schema.groups, selected);
|
|
964
|
+
// Task 7b Step 4: the element-level render rules. Read off
|
|
965
|
+
// `schema`, NOT narrowed by `selected` — see the pass-through
|
|
966
|
+
// comment at the structuredResult call below for why.
|
|
967
|
+
const renderRules = extractRenderRules(schema);
|
|
968
|
+
// Task 7b Steps 5-6: the shared rule notes, narrowed to the ids
|
|
969
|
+
// this response still references. See extractSharedRuleNotes().
|
|
970
|
+
const sharedRules = extractSharedRuleNotes(schema, selected);
|
|
971
|
+
// 2026-07-28: which build the version-scoped keys above were read
|
|
972
|
+
// on. Element-level like `renderRules`, and for the same reason —
|
|
973
|
+
// it qualifies claims the caller is about to act on, so a narrow
|
|
974
|
+
// field selection must not be able to drop it.
|
|
975
|
+
const claimProvenance = extractClaimProvenance(schema);
|
|
431
976
|
// Wave-6 R3 (2026-05-29): attach a binding contract so
|
|
432
977
|
// the agent doesn't have to reverse-engineer the
|
|
433
978
|
// Multi-Items pattern from layout + inspection probes.
|
|
434
979
|
const bindingContract = deriveBindingContract(name);
|
|
435
|
-
const toolkitResult = detailResult(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
:
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
980
|
+
const toolkitResult = detailResult({
|
|
981
|
+
...buildTypeSchemaDetail({
|
|
982
|
+
name,
|
|
983
|
+
label,
|
|
984
|
+
origin,
|
|
985
|
+
fields,
|
|
986
|
+
fieldCount,
|
|
987
|
+
returnedCount,
|
|
988
|
+
// Deliberately NOT handed to the detail renderer: the TS
|
|
989
|
+
// text leg is a NAME|TYPE|LABEL table and never carried
|
|
990
|
+
// `group_description` either, so the hoist moves the fact
|
|
991
|
+
// between root and field WITHOUT adding a new payload to
|
|
992
|
+
// the text leg. Its carrier is unchanged —
|
|
993
|
+
// `structuredContent` here, the JSON text leg on PHP.
|
|
994
|
+
...(bindingContract !== undefined
|
|
995
|
+
? {
|
|
996
|
+
bindingContract: {
|
|
997
|
+
kind: bindingContract.kind,
|
|
998
|
+
pattern: bindingContract.pattern,
|
|
999
|
+
...(bindingContract.child_type !== undefined
|
|
1000
|
+
? { child_type: bindingContract.child_type }
|
|
1001
|
+
: {}),
|
|
1002
|
+
...(bindingContract.binding_target_props !== undefined
|
|
1003
|
+
? {
|
|
1004
|
+
binding_target_props: bindingContract.binding_target_props,
|
|
1005
|
+
}
|
|
1006
|
+
: {}),
|
|
1007
|
+
...(bindingContract.canonical_text_prop !== undefined
|
|
1008
|
+
? {
|
|
1009
|
+
canonical_text_prop: bindingContract.canonical_text_prop,
|
|
1010
|
+
}
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(bindingContract.location_prop !== undefined
|
|
1013
|
+
? { location_prop: bindingContract.location_prop }
|
|
1014
|
+
: {}),
|
|
1015
|
+
...(bindingContract.set_via_tool !== undefined
|
|
1016
|
+
? { set_via_tool: bindingContract.set_via_tool }
|
|
1017
|
+
: {}),
|
|
1018
|
+
...(bindingContract.discover_source_tool !== undefined
|
|
1019
|
+
? {
|
|
1020
|
+
discover_source_tool: bindingContract.discover_source_tool,
|
|
1021
|
+
}
|
|
1022
|
+
: {}),
|
|
1023
|
+
...(bindingContract.do_not !== undefined
|
|
1024
|
+
? { do_not: bindingContract.do_not }
|
|
1025
|
+
: {}),
|
|
1026
|
+
...(bindingContract.minimal_layout !== undefined
|
|
1027
|
+
? { minimal_layout: bindingContract.minimal_layout }
|
|
1028
|
+
: {}),
|
|
1029
|
+
},
|
|
1030
|
+
}
|
|
1031
|
+
: {}),
|
|
1032
|
+
}),
|
|
1033
|
+
includeMeta: false,
|
|
1034
|
+
});
|
|
1035
|
+
return withSiteMeta(structuredResult(applyMaxChars(toolkitResult, maxChars), {
|
|
480
1036
|
name,
|
|
481
1037
|
...(label !== undefined ? { label } : {}),
|
|
482
1038
|
...(origin !== undefined ? { origin } : {}),
|
|
1039
|
+
...(groups !== undefined ? { groups } : {}),
|
|
1040
|
+
// Task 7b Step 4 (plan 2026-07-27-02, inventory groups B/A/L):
|
|
1041
|
+
// element-level render rules, forwarded whole. Deliberately
|
|
1042
|
+
// NOT narrowed by the descriptor selection the way `groups`
|
|
1043
|
+
// is — a group sentence belongs to the fields it annotates,
|
|
1044
|
+
// but "this element is deleted at render when these props are
|
|
1045
|
+
// empty" is true of the element regardless of which
|
|
1046
|
+
// descriptors were asked for, and suppressing it on a narrow
|
|
1047
|
+
// read would hide it from exactly the caller about to write.
|
|
1048
|
+
//
|
|
1049
|
+
// Same carrier choice as `groups`: structuredContent here,
|
|
1050
|
+
// the JSON text leg on the PHP transport. The TS text leg is
|
|
1051
|
+
// a NAME|TYPE|LABEL table and never carried element-level
|
|
1052
|
+
// keys, so nothing is added to it.
|
|
1053
|
+
...(renderRules.renders_only_if !== undefined
|
|
1054
|
+
? { renders_only_if: renderRules.renders_only_if }
|
|
1055
|
+
: {}),
|
|
1056
|
+
...(renderRules.blanked_by_parent !== undefined
|
|
1057
|
+
? { blanked_by_parent: renderRules.blanked_by_parent }
|
|
1058
|
+
: {}),
|
|
1059
|
+
...(renderRules.media_autoswap !== undefined
|
|
1060
|
+
? { media_autoswap: renderRules.media_autoswap }
|
|
1061
|
+
: {}),
|
|
1062
|
+
// Directly after the gate it qualifies: a caller that sees
|
|
1063
|
+
// `renders_only_if` alone concludes "absent props render
|
|
1064
|
+
// nothing", which is false on 27 of the 28 gated types.
|
|
1065
|
+
...(renderRules.placeholder_fallback !== undefined
|
|
1066
|
+
? { placeholder_fallback: renderRules.placeholder_fallback }
|
|
1067
|
+
: {}),
|
|
1068
|
+
// Task 7b Steps 5-6: the element's own rule ids, then the note
|
|
1069
|
+
// texts resolving every id the response still references
|
|
1070
|
+
// (element-level or on a returned field). Placed BEFORE
|
|
1071
|
+
// `fields` so a reader meets the vocabulary before the
|
|
1072
|
+
// descriptors that use it.
|
|
1073
|
+
...(sharedRules.rules !== undefined ? { rules: sharedRules.rules } : {}),
|
|
1074
|
+
...(sharedRules.rule_notes !== undefined
|
|
1075
|
+
? { rule_notes: sharedRules.rule_notes }
|
|
1076
|
+
: {}),
|
|
1077
|
+
// 2026-07-28: LAST of the element-level keys and still BEFORE
|
|
1078
|
+
// `fields`, so a reader meets the provenance of the claims
|
|
1079
|
+
// before the descriptors that carry them — the same ordering
|
|
1080
|
+
// argument as `rule_notes` above. A `differs`/`unknown` status
|
|
1081
|
+
// is what tells a caller that `no_effect_unverified` on a
|
|
1082
|
+
// descriptor below is a report, not a fact about their site.
|
|
1083
|
+
...(claimProvenance !== undefined
|
|
1084
|
+
? { claims_verified_on: claimProvenance }
|
|
1085
|
+
: {}),
|
|
483
1086
|
...(fields !== undefined ? { fields } : {}),
|
|
484
1087
|
field_count: fieldCount,
|
|
1088
|
+
returned_count: returnedCount,
|
|
485
1089
|
...(bindingContract !== undefined
|
|
486
1090
|
? { binding_contract: bindingContract }
|
|
487
1091
|
: {}),
|