@wootsup/yt-builder-mcp 1.11.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 +10 -10
- 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.js +1 -1
- 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/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/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 +4 -2
- 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 +48 -24
- package/dist/tools/elements/builders.js.map +1 -1
- package/dist/tools/elements/handlers-write.d.ts +19 -0
- package/dist/tools/elements/handlers-write.d.ts.map +1 -1
- package/dist/tools/elements/handlers-write.js +100 -3
- package/dist/tools/elements/handlers-write.js.map +1 -1
- package/dist/tools/elements/handlers.d.ts +13 -8
- package/dist/tools/elements/handlers.d.ts.map +1 -1
- package/dist/tools/elements/handlers.js +28 -18
- package/dist/tools/elements/handlers.js.map +1 -1
- package/dist/tools/elements/schema-validation.d.ts +129 -7
- package/dist/tools/elements/schema-validation.d.ts.map +1 -1
- package/dist/tools/elements/schema-validation.js +459 -23
- 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/health.d.ts.map +1 -1
- package/dist/tools/health.js +67 -6
- 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 +600 -65
- package/dist/tools/inspection.js.map +1 -1
- package/dist/tools/library.d.ts +6 -2
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +36 -21
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/local-content/builders.js +4 -4
- 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 +8 -4
- package/dist/tools/local-content/handlers.js.map +1 -1
- package/dist/tools/multi-items/builders.d.ts.map +1 -1
- package/dist/tools/multi-items/builders.js +2 -4
- 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 +148 -49
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/pages/handlers-audit.d.ts +14 -3
- package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
- package/dist/tools/pages/handlers-audit.js +11 -1
- package/dist/tools/pages/handlers-audit.js.map +1 -1
- package/dist/tools/pages/handlers-read.d.ts +43 -5
- package/dist/tools/pages/handlers-read.d.ts.map +1 -1
- package/dist/tools/pages/handlers-read.js +176 -44
- package/dist/tools/pages/handlers-read.js.map +1 -1
- package/dist/tools/pages/handlers-write.d.ts +2 -2
- package/dist/tools/pages/handlers-write.d.ts.map +1 -1
- package/dist/tools/pages/handlers-write.js +3 -2
- 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 +95 -9
- package/dist/tools/pages/schemas.d.ts.map +1 -1
- package/dist/tools/pages/schemas.js +201 -19
- package/dist/tools/pages/schemas.js.map +1 -1
- 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 +23 -0
- package/dist/tools/shared-schemas.d.ts.map +1 -1
- package/dist/tools/shared-schemas.js +28 -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 +13 -15
- package/dist/tools/sources/builders.js.map +1 -1
- package/dist/tools/sources/handlers-bind.d.ts +3 -3
- package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
- package/dist/tools/sources/handlers-bind.js +4 -2
- 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 +49 -8
- package/dist/tools/sparse-fields.d.ts.map +1 -1
- package/dist/tools/sparse-fields.js +90 -10
- 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 +19 -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 +1202 -83
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, MAX_CHARS, applyMaxChars, 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
|
|
@@ -283,8 +451,25 @@ const NODE_LEVEL_FIELD_KEYS = new Set(['name', 'status', 'source', 'id']);
|
|
|
283
451
|
* Annotate node-level descriptors (`name`/`status`/`source`/`id`) with
|
|
284
452
|
* `group:'node-level'` + a value_hint so the agent knows they live OUTSIDE
|
|
285
453
|
* `props`. Non-destructive: returns a shallow-cloned array; other descriptors
|
|
286
|
-
* pass through untouched
|
|
287
|
-
*
|
|
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.
|
|
288
473
|
*/
|
|
289
474
|
function annotateNodeLevelFields(fields) {
|
|
290
475
|
return fields.map((field) => {
|
|
@@ -294,7 +479,7 @@ function annotateNodeLevelFields(fields) {
|
|
|
294
479
|
}
|
|
295
480
|
return {
|
|
296
481
|
...field,
|
|
297
|
-
|
|
482
|
+
group: 'node-level',
|
|
298
483
|
value_hint: typeof field.value_hint === 'string' && field.value_hint !== ''
|
|
299
484
|
? field.value_hint
|
|
300
485
|
: 'builder label / node metadata — lives OUTSIDE `props` (a SIBLING of ' +
|
|
@@ -303,6 +488,184 @@ function annotateNodeLevelFields(fields) {
|
|
|
303
488
|
};
|
|
304
489
|
});
|
|
305
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
|
+
}
|
|
306
669
|
export function buildInspectionTools(pool) {
|
|
307
670
|
return [
|
|
308
671
|
defineTool({
|
|
@@ -317,15 +680,15 @@ export function buildInspectionTools(pool) {
|
|
|
317
680
|
'Prop keys differ per element (grid_item `title` vs headline `content`) — ' +
|
|
318
681
|
'use element_type_get_schema before element_add/bind. Container rows carry ' +
|
|
319
682
|
'`requires_child_type` (grid→grid_item); `has_children`/`has_children_support` ' +
|
|
320
|
-
'are aliases (same value).
|
|
321
|
-
'Operates on the default site unless site_id is provided.',
|
|
683
|
+
'are aliases (same value).',
|
|
322
684
|
inputSchema: {
|
|
323
685
|
site_id: SITE_ID_SCHEMA,
|
|
324
686
|
fields: FIELDS,
|
|
687
|
+
include_meta: INCLUDE_META_SCHEMA,
|
|
325
688
|
},
|
|
326
689
|
outputSchema: TYPES_LIST_OUTPUT_SCHEMA,
|
|
327
690
|
annotations: readOnly('List Element Types'),
|
|
328
|
-
handler: async ({ site_id, fields }) => {
|
|
691
|
+
handler: async ({ site_id, fields, include_meta }) => {
|
|
329
692
|
const r = await resolveSiteOrError(pool, site_id);
|
|
330
693
|
if (!r.ok)
|
|
331
694
|
return r.error;
|
|
@@ -349,12 +712,13 @@ export function buildInspectionTools(pool) {
|
|
|
349
712
|
const feedback = projectionFeedback(mappedRecords, fields);
|
|
350
713
|
// F23 (2026-06-09): shared loud-warning helper.
|
|
351
714
|
const unknownNote = projectionWarning(feedback);
|
|
352
|
-
const toolkitResult =
|
|
715
|
+
const toolkitResult = budgetedTableResult(mappedRecords, {
|
|
353
716
|
columns: [...TYPES_TABLE_COLUMNS],
|
|
354
717
|
compactColumns: [...TYPES_COMPACT_COLUMNS],
|
|
355
718
|
header: (count) => `${String(count)} element types${unknownNote}`,
|
|
356
719
|
footer: 'Use yootheme_builder_element_type_get_schema <name> for fields.',
|
|
357
|
-
|
|
720
|
+
includeMeta: include_meta,
|
|
721
|
+
}, RESPONSE_BUDGET.BULKY);
|
|
358
722
|
return withSiteMeta(structuredResult(toolkitResult, {
|
|
359
723
|
items,
|
|
360
724
|
total: items.length,
|
|
@@ -414,6 +778,13 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
414
778
|
// blob). `fields[]` sparse-projects each field descriptor to the requested
|
|
415
779
|
// sub-keys; `max_chars` caps the text leg — mirroring the sibling reads.
|
|
416
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,
|
|
417
788
|
max_chars: MAX_CHARS,
|
|
418
789
|
};
|
|
419
790
|
const TYPE_SCHEMA_REFINED = z
|
|
@@ -425,14 +796,77 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
425
796
|
});
|
|
426
797
|
return defineTool({
|
|
427
798
|
name: 'yootheme_builder_element_type_get_schema',
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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.',
|
|
436
870
|
inputSchema: TYPE_SCHEMA_SHAPE,
|
|
437
871
|
// F-203 follow-up: the refined object is what the SDK actually
|
|
438
872
|
// validates. The raw shape above stays for handler-type inference.
|
|
@@ -493,64 +927,165 @@ function buildElementTypeGetSchemaTool(pool) {
|
|
|
493
927
|
? args.fields
|
|
494
928
|
: undefined;
|
|
495
929
|
const maxChars = typeof args.max_chars === 'number' ? args.max_chars : undefined;
|
|
496
|
-
|
|
497
|
-
|
|
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, [])
|
|
498
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.
|
|
499
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);
|
|
500
976
|
// Wave-6 R3 (2026-05-29): attach a binding contract so
|
|
501
977
|
// the agent doesn't have to reverse-engineer the
|
|
502
978
|
// Multi-Items pattern from layout + inspection probes.
|
|
503
979
|
const bindingContract = deriveBindingContract(name);
|
|
504
|
-
const toolkitResult = detailResult(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
});
|
|
548
1035
|
return withSiteMeta(structuredResult(applyMaxChars(toolkitResult, maxChars), {
|
|
549
1036
|
name,
|
|
550
1037
|
...(label !== undefined ? { label } : {}),
|
|
551
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
|
+
: {}),
|
|
552
1086
|
...(fields !== undefined ? { fields } : {}),
|
|
553
1087
|
field_count: fieldCount,
|
|
1088
|
+
returned_count: returnedCount,
|
|
554
1089
|
...(bindingContract !== undefined
|
|
555
1090
|
? { binding_contract: bindingContract }
|
|
556
1091
|
: {}),
|