@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.
Files changed (215) hide show
  1. package/README.md +12 -11
  2. package/bin/yt-builder-mcp.js +1 -1
  3. package/dist/auth.d.ts +1 -1
  4. package/dist/auth.js +1 -1
  5. package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
  6. package/dist/catalog/tool-catalog-markdown.js +13 -1
  7. package/dist/catalog/tool-catalog-markdown.js.map +1 -1
  8. package/dist/cli/doctor-command.d.ts.map +1 -1
  9. package/dist/cli/doctor-command.js +22 -2
  10. package/dist/cli/doctor-command.js.map +1 -1
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.js +1 -1
  13. package/dist/client.js.map +1 -1
  14. package/dist/clients/index.d.ts +1 -1
  15. package/dist/clients/index.js +1 -1
  16. package/dist/diagnostics/ca-reexec.js +2 -2
  17. package/dist/diagnostics/diagnose-network-error.d.ts +1 -1
  18. package/dist/diagnostics/doctor.d.ts +34 -2
  19. package/dist/diagnostics/doctor.d.ts.map +1 -1
  20. package/dist/diagnostics/doctor.js +63 -2
  21. package/dist/diagnostics/doctor.js.map +1 -1
  22. package/dist/diagnostics/startup-self-probe.js +1 -1
  23. package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
  24. package/dist/gateway/advanced-read-tool.js +14 -2
  25. package/dist/gateway/advanced-read-tool.js.map +1 -1
  26. package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
  27. package/dist/gateway/advanced-tool/discovery.js +12 -1
  28. package/dist/gateway/advanced-tool/discovery.js.map +1 -1
  29. package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
  30. package/dist/gateway/advanced-tool/domains.js +5 -0
  31. package/dist/gateway/advanced-tool/domains.js.map +1 -1
  32. package/dist/gateway/advanced-tool/execute.d.ts +7 -1
  33. package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
  34. package/dist/gateway/advanced-tool/execute.js +58 -4
  35. package/dist/gateway/advanced-tool/execute.js.map +1 -1
  36. package/dist/gateway/essentials.d.ts +4 -4
  37. package/dist/gateway/essentials.d.ts.map +1 -1
  38. package/dist/gateway/essentials.js +12 -7
  39. package/dist/gateway/essentials.js.map +1 -1
  40. package/dist/install-skill.js +1 -1
  41. package/dist/net/extra-headers.d.ts +1 -1
  42. package/dist/net/extra-headers.js +1 -1
  43. package/dist/net/internal-host.js +2 -2
  44. package/dist/net/proxy-dispatcher.js +1 -1
  45. package/dist/net/proxy-dispatcher.js.map +1 -1
  46. package/dist/net/site-basic-auth.d.ts +2 -2
  47. package/dist/net/site-basic-auth.js +2 -2
  48. package/dist/proxy/bridge.d.ts +2 -2
  49. package/dist/proxy/bridge.js +3 -3
  50. package/dist/proxy/bridge.js.map +1 -1
  51. package/dist/server.d.ts +3 -3
  52. package/dist/server.js +3 -3
  53. package/dist/setup-cli.d.ts +2 -2
  54. package/dist/setup-cli.js +2 -2
  55. package/dist/setup-cli.js.map +1 -1
  56. package/dist/setup-npx-spec.d.ts +1 -1
  57. package/dist/setup-npx-spec.js +1 -1
  58. package/dist/setup-wizard-handshake.js +1 -1
  59. package/dist/setup-wizard-types.d.ts +1 -1
  60. package/dist/setup-wizard.js +3 -3
  61. package/dist/setup-wizard.js.map +1 -1
  62. package/dist/sites/client-pool.d.ts +1 -1
  63. package/dist/sites/client-pool.js +1 -1
  64. package/dist/sites/tools/sites-list.d.ts.map +1 -1
  65. package/dist/sites/tools/sites-list.js +26 -4
  66. package/dist/sites/tools/sites-list.js.map +1 -1
  67. package/dist/sites/tools/sites-test.js +1 -1
  68. package/dist/sites/tools/sites-test.js.map +1 -1
  69. package/dist/sites/tools/use-site.js +2 -2
  70. package/dist/sites/tools/use-site.js.map +1 -1
  71. package/dist/tools/budgeted-table.d.ts +21 -0
  72. package/dist/tools/budgeted-table.d.ts.map +1 -0
  73. package/dist/tools/budgeted-table.js +137 -0
  74. package/dist/tools/budgeted-table.js.map +1 -0
  75. package/dist/tools/elements/builders.d.ts.map +1 -1
  76. package/dist/tools/elements/builders.js +102 -40
  77. package/dist/tools/elements/builders.js.map +1 -1
  78. package/dist/tools/elements/handlers-write.d.ts +21 -0
  79. package/dist/tools/elements/handlers-write.d.ts.map +1 -1
  80. package/dist/tools/elements/handlers-write.js +231 -11
  81. package/dist/tools/elements/handlers-write.js.map +1 -1
  82. package/dist/tools/elements/handlers.d.ts +20 -12
  83. package/dist/tools/elements/handlers.d.ts.map +1 -1
  84. package/dist/tools/elements/handlers.js +77 -26
  85. package/dist/tools/elements/handlers.js.map +1 -1
  86. package/dist/tools/elements/schema-validation.d.ts +147 -7
  87. package/dist/tools/elements/schema-validation.d.ts.map +1 -1
  88. package/dist/tools/elements/schema-validation.js +508 -28
  89. package/dist/tools/elements/schema-validation.js.map +1 -1
  90. package/dist/tools/format/elements-format.d.ts.map +1 -1
  91. package/dist/tools/format/elements-format.js +56 -12
  92. package/dist/tools/format/elements-format.js.map +1 -1
  93. package/dist/tools/format/health-format.d.ts +16 -0
  94. package/dist/tools/format/health-format.d.ts.map +1 -1
  95. package/dist/tools/format/health-format.js +9 -0
  96. package/dist/tools/format/health-format.js.map +1 -1
  97. package/dist/tools/format/inspection-format.d.ts +7 -0
  98. package/dist/tools/format/inspection-format.d.ts.map +1 -1
  99. package/dist/tools/format/inspection-format.js +18 -0
  100. package/dist/tools/format/inspection-format.js.map +1 -1
  101. package/dist/tools/format/pages-format.d.ts +1 -1
  102. package/dist/tools/format/pages-format.d.ts.map +1 -1
  103. package/dist/tools/format/pages-format.js +7 -0
  104. package/dist/tools/format/pages-format.js.map +1 -1
  105. package/dist/tools/format/sources-format.d.ts +12 -5
  106. package/dist/tools/format/sources-format.d.ts.map +1 -1
  107. package/dist/tools/format/sources-format.js +31 -6
  108. package/dist/tools/format/sources-format.js.map +1 -1
  109. package/dist/tools/health.d.ts.map +1 -1
  110. package/dist/tools/health.js +85 -11
  111. package/dist/tools/health.js.map +1 -1
  112. package/dist/tools/index.d.ts.map +1 -1
  113. package/dist/tools/index.js +7 -0
  114. package/dist/tools/index.js.map +1 -1
  115. package/dist/tools/inspection.d.ts +34 -0
  116. package/dist/tools/inspection.d.ts.map +1 -1
  117. package/dist/tools/inspection.js +666 -62
  118. package/dist/tools/inspection.js.map +1 -1
  119. package/dist/tools/layout-flatten.d.ts +7 -1
  120. package/dist/tools/layout-flatten.d.ts.map +1 -1
  121. package/dist/tools/layout-flatten.js +24 -13
  122. package/dist/tools/layout-flatten.js.map +1 -1
  123. package/dist/tools/library.d.ts +29 -2
  124. package/dist/tools/library.d.ts.map +1 -1
  125. package/dist/tools/library.js +247 -34
  126. package/dist/tools/library.js.map +1 -1
  127. package/dist/tools/local-content/builders.js +14 -14
  128. package/dist/tools/local-content/builders.js.map +1 -1
  129. package/dist/tools/local-content/handlers.d.ts +3 -1
  130. package/dist/tools/local-content/handlers.d.ts.map +1 -1
  131. package/dist/tools/local-content/handlers.js +42 -9
  132. package/dist/tools/local-content/handlers.js.map +1 -1
  133. package/dist/tools/local-content/schemas.d.ts +9 -4
  134. package/dist/tools/local-content/schemas.d.ts.map +1 -1
  135. package/dist/tools/local-content/schemas.js +14 -6
  136. package/dist/tools/local-content/schemas.js.map +1 -1
  137. package/dist/tools/multi-items/builders.d.ts.map +1 -1
  138. package/dist/tools/multi-items/builders.js +11 -10
  139. package/dist/tools/multi-items/builders.js.map +1 -1
  140. package/dist/tools/navigation/builders.d.ts +18 -0
  141. package/dist/tools/navigation/builders.d.ts.map +1 -0
  142. package/dist/tools/navigation/builders.js +47 -0
  143. package/dist/tools/navigation/builders.js.map +1 -0
  144. package/dist/tools/navigation/handlers.d.ts +30 -0
  145. package/dist/tools/navigation/handlers.d.ts.map +1 -0
  146. package/dist/tools/navigation/handlers.js +41 -0
  147. package/dist/tools/navigation/handlers.js.map +1 -0
  148. package/dist/tools/navigation/index.d.ts +15 -0
  149. package/dist/tools/navigation/index.d.ts.map +1 -0
  150. package/dist/tools/navigation/index.js +14 -0
  151. package/dist/tools/navigation/index.js.map +1 -0
  152. package/dist/tools/navigation/schemas.d.ts +46 -0
  153. package/dist/tools/navigation/schemas.d.ts.map +1 -0
  154. package/dist/tools/navigation/schemas.js +41 -0
  155. package/dist/tools/navigation/schemas.js.map +1 -0
  156. package/dist/tools/pages/builders.d.ts.map +1 -1
  157. package/dist/tools/pages/builders.js +213 -81
  158. package/dist/tools/pages/builders.js.map +1 -1
  159. package/dist/tools/pages/handlers-audit.d.ts +112 -3
  160. package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
  161. package/dist/tools/pages/handlers-audit.js +623 -7
  162. package/dist/tools/pages/handlers-audit.js.map +1 -1
  163. package/dist/tools/pages/handlers-read.d.ts +51 -8
  164. package/dist/tools/pages/handlers-read.d.ts.map +1 -1
  165. package/dist/tools/pages/handlers-read.js +348 -69
  166. package/dist/tools/pages/handlers-read.js.map +1 -1
  167. package/dist/tools/pages/handlers-write.d.ts +7 -3
  168. package/dist/tools/pages/handlers-write.d.ts.map +1 -1
  169. package/dist/tools/pages/handlers-write.js +69 -25
  170. package/dist/tools/pages/handlers-write.js.map +1 -1
  171. package/dist/tools/pages/header-transparency.d.ts +64 -0
  172. package/dist/tools/pages/header-transparency.d.ts.map +1 -0
  173. package/dist/tools/pages/header-transparency.js +119 -0
  174. package/dist/tools/pages/header-transparency.js.map +1 -0
  175. package/dist/tools/pages/schemas.d.ts +284 -13
  176. package/dist/tools/pages/schemas.d.ts.map +1 -1
  177. package/dist/tools/pages/schemas.js +434 -26
  178. package/dist/tools/pages/schemas.js.map +1 -1
  179. package/dist/tools/pages/target.d.ts +162 -0
  180. package/dist/tools/pages/target.d.ts.map +1 -0
  181. package/dist/tools/pages/target.js +239 -0
  182. package/dist/tools/pages/target.js.map +1 -0
  183. package/dist/tools/response-budget.d.ts +77 -0
  184. package/dist/tools/response-budget.d.ts.map +1 -0
  185. package/dist/tools/response-budget.js +134 -0
  186. package/dist/tools/response-budget.js.map +1 -0
  187. package/dist/tools/shared-schemas.d.ts +45 -0
  188. package/dist/tools/shared-schemas.d.ts.map +1 -1
  189. package/dist/tools/shared-schemas.js +50 -0
  190. package/dist/tools/shared-schemas.js.map +1 -1
  191. package/dist/tools/sources/builders.d.ts.map +1 -1
  192. package/dist/tools/sources/builders.js +55 -39
  193. package/dist/tools/sources/builders.js.map +1 -1
  194. package/dist/tools/sources/handlers-bind.d.ts +10 -11
  195. package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
  196. package/dist/tools/sources/handlers-bind.js +110 -9
  197. package/dist/tools/sources/handlers-bind.js.map +1 -1
  198. package/dist/tools/sources/handlers.d.ts +4 -2
  199. package/dist/tools/sources/handlers.d.ts.map +1 -1
  200. package/dist/tools/sources/handlers.js +11 -7
  201. package/dist/tools/sources/handlers.js.map +1 -1
  202. package/dist/tools/sparse-fields.d.ts +61 -8
  203. package/dist/tools/sparse-fields.d.ts.map +1 -1
  204. package/dist/tools/sparse-fields.js +124 -12
  205. package/dist/tools/sparse-fields.js.map +1 -1
  206. package/dist/tools/sublayout/builders.d.ts.map +1 -1
  207. package/dist/tools/sublayout/builders.js +1 -2
  208. package/dist/tools/sublayout/builders.js.map +1 -1
  209. package/dist/tools/tool-builder/results.d.ts +19 -6
  210. package/dist/tools/tool-builder/results.d.ts.map +1 -1
  211. package/dist/tools/tool-builder/results.js +133 -6
  212. package/dist/tools/tool-builder/results.js.map +1 -1
  213. package/manifest.json +1 -1
  214. package/package.json +2 -2
  215. package/skills/yt-builder-mcp/SKILL.md +1713 -72
@@ -7,6 +7,7 @@
7
7
  * @license MIT
8
8
  */
9
9
  import { z } from 'zod';
10
+ import { INCLUDE_META_SCHEMA } from '../shared-schemas.js';
10
11
  import { MAX_CHARS } from '../sparse-fields.js';
11
12
  export const PAGES_LIST_OUTPUT_SCHEMA = z.object({
12
13
  // F-Frontend-URL (2026-05-25): each row carries `frontend_url`,
@@ -35,7 +36,7 @@ export const PAGES_LIST_OUTPUT_SCHEMA = z.object({
35
36
  // field projection + available_fields discovery surface it.
36
37
  items: z.array(z.record(z.string(), z.unknown())),
37
38
  total: z.number(),
38
- // B2 (Glöckle 2026-06-25): pagination. `total` is the (filtered) count;
39
+ // B2 (customer 2026-06-25): pagination. `total` is the (filtered) count;
39
40
  // `returned` is how many rows this page carries; `next_cursor` (present
40
41
  // only when more remain) is passed back as `cursor` for the next page.
41
42
  returned: z.number().optional(),
@@ -61,7 +62,7 @@ export const SCHEMA_OUTPUT_SCHEMA = z.object({
61
62
  // row for back-compat but is optional so callers reading the schema
62
63
  // know `rel_path` is the canonical projection column.
63
64
  //
64
- // G-B (Glöckle 2026-06-26): every row-field is now `.optional()` +
65
+ // G-B (customer 2026-06-26): every row-field is now `.optional()` +
65
66
  // `.passthrough()` so a `fields[]` projection (which drops the unrequested
66
67
  // keys) still validates — exactly the relaxation `element_list`'s output
67
68
  // schema uses. The default (no fields) full row still carries all keys.
@@ -96,16 +97,36 @@ export const ETAG_OUTPUT_SCHEMA = z.object({
96
97
  // the RESULT-LEVEL by `withSiteMeta`, NEVER into `structuredContent`
97
98
  // (see results.ts W12-R2 commentary) — this schema therefore validates
98
99
  // only the inner template-summary shape.
100
+ //
101
+ // T-C5 (mega-push plan 2026-07-24-02, customer P1.5): `template_summary` is now
102
+ // BIMODAL — a single-`template_id` call returns these fields at the TOP LEVEL
103
+ // (unchanged wire shape, back-compat preserved); a `template_ids` BATCH call
104
+ // instead returns `{summaries: {<id>: <that SAME per-template shape>},
105
+ // not_found: [...]}` and omits every top-level field below. The 7 fields that
106
+ // used to be REQUIRED (template_id/counts_by_type/bound_count/max_depth/total/
107
+ // named_sections/etag) are now `.optional()` too, SPECIFICALLY so one strict
108
+ // Zod object can validate BOTH response variants without a `z.union()` — the
109
+ // codebase's established alternative for a bimodal MCP response is dropping
110
+ // `outputSchema` entirely (see page_get_layout / page_dynamize in
111
+ // builders.ts), which would lose validation for the (overwhelmingly common)
112
+ // single-template case too. Declaring every possible field of BOTH shapes as
113
+ // optional on one object keeps `additionalProperties:false` catching
114
+ // genuinely unexpected keys while tolerating "this variant's fields are
115
+ // absent" — the SAME relaxation `resolved_target`/`FINDING` already use
116
+ // elsewhere in this file for the identical reason (different response
117
+ // variants omit different keys).
99
118
  export const TEMPLATE_SUMMARY_OUTPUT_SCHEMA = z.object({
100
- template_id: z.string(),
101
- counts_by_type: z.record(z.string(), z.number().int().nonnegative()),
102
- bound_count: z.number().int().nonnegative(),
103
- max_depth: z.number().int().nonnegative(),
104
- total: z.number().int().nonnegative(),
105
- named_sections: z.array(z.object({
119
+ template_id: z.string().optional(),
120
+ counts_by_type: z.record(z.string(), z.number().int().nonnegative()).optional(),
121
+ bound_count: z.number().int().nonnegative().optional(),
122
+ max_depth: z.number().int().nonnegative().optional(),
123
+ total: z.number().int().nonnegative().optional(),
124
+ named_sections: z
125
+ .array(z.object({
106
126
  path: z.string(),
107
127
  name: z.string(),
108
- })),
128
+ }))
129
+ .optional(),
109
130
  // pages_audit impl plan Task 7 (2026-07-19): additive enrichment —
110
131
  // heading-level census (h1-h6 + div — `div` counts `description_list`
111
132
  // elements, the one type whose title_element defaults to div),
@@ -114,9 +135,54 @@ export const TEMPLATE_SUMMARY_OUTPUT_SCHEMA = z.object({
114
135
  // a NEW MCP client talking to an OLD (pre-1.10.0) host plugin — which
115
136
  // doesn't emit these fields yet — still validates.
116
137
  heading_levels: z.record(z.string(), z.number().int().nonnegative()).optional(),
138
+ // T-A3 (mega-push plan 2026-07-24-02): a static, always-present sibling
139
+ // stating which sources feed heading_levels above — html/text/code
140
+ // raw-HTML elements count (T-A1 widened the shared LayoutAuditExtractor
141
+ // pass to read them), module/widget output does NOT (no content field to
142
+ // parse). `.optional()` for the same pre-1.10.0-host back-compat reason
143
+ // as heading_levels itself.
144
+ heading_levels_note: z.string().optional(),
117
145
  images_missing_alt: z.number().int().nonnegative().optional(),
118
146
  disabled_elements: z.number().int().nonnegative().optional(),
119
- etag: z.string(),
147
+ // F191 (2026-06-12) source-transparency flags the PHP summary() has emitted
148
+ // since June but this schema never declared, so a strict SDK parse silently
149
+ // stripped them. `already_dynamized` = the layout already binds an API Mapper
150
+ // source (demo/clone vs pristine theme); `is_theme_template` = a real YOOtheme
151
+ // theme template vs a per-page/per-article layout. `.optional()` for the same
152
+ // pre-1.10.0-host back-compat reason as the fields above.
153
+ already_dynamized: z.boolean().optional(),
154
+ is_theme_template: z.boolean().optional(),
155
+ etag: z.string().optional(),
156
+ // Task 6 (Wave 1 audit, F4, 2026-07-24): one entry per content-bearing
157
+ // node (structural wrappers section/row/column/fragment/layout excluded).
158
+ // Disabled/render-dead nodes ARE included, unlike disabled_elements above:
159
+ // this is the field that says WHICH node and WHY, not just a count.
160
+ // T-E2 (mega-push plan 2026-07-24-02, Thomas-Entscheid C): OPT-IN now —
161
+ // absent unless the caller passed `include_nodes:true` (previously
162
+ // unconditional). `.optional()` covers BOTH that opt-out-by-default case
163
+ // AND the pre-1.10.0-host back-compat reason the three fields above share.
164
+ content_nodes: z
165
+ .array(z.object({
166
+ path: z.string(),
167
+ type: z.string(),
168
+ status: z.boolean(),
169
+ effective_visible: z.boolean(),
170
+ hidden_on_mobile: z.boolean(),
171
+ headings: z.array(z.string()),
172
+ alts: z.array(z.string()),
173
+ }))
174
+ .optional(),
175
+ // T-C5: page 2+ of content_nodes when `include_nodes:true` + `limit` were
176
+ // supplied. Present ONLY while more content_nodes rows remain (mirrors
177
+ // pages_list / element_list — never an always-present nullable field).
178
+ next_cursor: z.string().optional(),
179
+ // T-C5 (customer P1.5) batch shape — present ONLY when the caller passed
180
+ // `template_ids` instead of `template_id`. One entry per id that resolved
181
+ // (SAME per-template shape as the single-mode top-level fields above,
182
+ // left as an open record here rather than nesting the whole shape a
183
+ // second time); `not_found` lists ids that did not.
184
+ summaries: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
185
+ not_found: z.array(z.string()).optional(),
120
186
  });
121
187
  // ─── C1 — pages_create + pages_delete (v1.2) ──────────────────────────
122
188
  //
@@ -350,9 +416,11 @@ export const PAGE_DYNAMIZE_INPUT = {
350
416
  .string()
351
417
  .min(1)
352
418
  .optional()
353
- .describe('SINGLE-block path: the published flow LIST source the one iterating column ' +
354
- 'query is rewritten to (e.g. "apiMapperFlowAbcList"). Provide THIS or ' +
355
- '`source_map` exactly one. Use yootheme_builder_sources_list to discover.'),
419
+ .describe('SINGLE-block path: the LIST source the one iterating column query is rewritten ' +
420
+ 'to. Accepts ANY list source a published API Mapper flow ' +
421
+ '(apiMapperFlow…List, e.g. "apiMapperFlowAbcList") OR a native source/relation ' +
422
+ '(e.g. "posts.customPosts", "articles"). Provide THIS or `source_map` — exactly ' +
423
+ 'one. Use yootheme_builder_sources_list to discover.'),
356
424
  // F201 (multi-block): a section with SEVERAL source blocks (Played / Next /
357
425
  // Later) dynamizes in ONE call. Each entry keys the EXISTING iterating
358
426
  // column query (`match`) to its flow list source (`source_name`) + its own
@@ -437,6 +505,14 @@ export const PAGE_DYNAMIZE_INPUT = {
437
505
  .optional()
438
506
  .describe('Optimistic-lock ETag from yootheme_builder_get_etag. Optional; when provided, ' +
439
507
  '412 on conflict. Recommended for collaborative edits.'),
508
+ // SOURCES_BINDING-27: preflight the dynamize without creating a page.
509
+ dry_run: z
510
+ .boolean()
511
+ .optional()
512
+ .describe('Preflight ONLY. When true, compute the dynamize and return {summary, ' +
513
+ 'would_change, dry_run:true} WITHOUT creating a page — nothing persists ' +
514
+ '(etag == new_etag). Inspect summary.queries_swapped / unmatched_map_keys ' +
515
+ 'before the real call.'),
440
516
  };
441
517
  /**
442
518
  * Wire shape for `yootheme_builder_pages_delete` on success.
@@ -464,13 +540,97 @@ export const PAGES_DELETE_OUTPUT_SCHEMA = z
464
540
  })
465
541
  .refine((v) => typeof v.page_id === 'number' || typeof v.article_id === 'number', { message: 'response must contain either page_id (WP) or article_id (Joomla)' });
466
542
  // ─── G-A (Track-C URL→layout) — pages_resolve ───────────────────────
543
+ // ─── A-6 (single-page audit plan, 2026-07-22-02) — semantic `target` ──────
544
+ //
545
+ // The shared addressing object BOTH `pages_resolve` and `pages_audit` accept.
546
+ // It is deliberately PERMISSIVE (every member optional, no `.refine`): the
547
+ // exactly-one-of contract is enforced in the HANDLERS as a typed
548
+ // `too_many_targets` result. F205 (`gateway/advanced-read-tool.ts`): an
549
+ // SDK-level zod rejection surfaces as a raw -32602 and bypasses every piece of
550
+ // typed steering the tool provides.
551
+ //
552
+ // The VOCABULARY lives in these field `.describe()` strings on purpose — the
553
+ // tool descriptions are pinned at MAX_DESCRIPTION_LENGTH=600 by
554
+ // `tests/gateway/description-length.test.ts` and pages_resolve already sits at
555
+ // 577, so there is no room there. Field describes carry no length pin.
556
+ /**
557
+ * The five addressing keys, in the order they are documented + put on the
558
+ * wire. SSOT for both the `matched_by` echo enum and the runtime iteration in
559
+ * `./target.ts`.
560
+ */
561
+ export const TARGET_KEYS = ['template_id', 'post_id', 'url', 'special', 'title'];
562
+ /**
563
+ * The semantic-special vocabulary, byte-identical to PHP `SpecialTargetMap`'s
564
+ * key set. `date` is IN the vocabulary but unsupported on both platforms — it
565
+ * resolves to a clean `special_not_supported` error rather than "unknown
566
+ * special", which is why it is listed here.
567
+ */
568
+ export const SPECIAL_TARGET_TOKENS = [
569
+ 'home',
570
+ 'front-page',
571
+ 'blog',
572
+ 'search',
573
+ '404',
574
+ 'category',
575
+ 'tag',
576
+ 'author',
577
+ 'archive',
578
+ 'date',
579
+ ];
580
+ export const PAGES_TARGET = z
581
+ .object({
582
+ template_id: z
583
+ .string()
584
+ .min(1)
585
+ .optional()
586
+ .describe('An exact template id (e.g. "page:4816", "article:12", "helpArchiveTpl").'),
587
+ post_id: z
588
+ .union([z.number().int().positive(), z.string().min(1)])
589
+ .optional()
590
+ .describe('The numeric WordPress post / Joomla article id (e.g. 4816).'),
591
+ url: z
592
+ .string()
593
+ .min(1)
594
+ .optional()
595
+ .describe('A public frontend URL (e.g. "https://site.com/schedule-demo/").'),
596
+ special: z
597
+ .enum(SPECIAL_TARGET_TOKENS)
598
+ .optional()
599
+ .describe('A semantic page role: "home" / "front-page" / "blog" (the configured ' +
600
+ 'front page or posts page), "search", "404", "category", "tag", ' +
601
+ '"author" (Joomla: the Contact template), "archive" (the post ' +
602
+ 'archive). "date" is recognised but not supported on either platform ' +
603
+ 'yet and returns special_not_supported. DISCOVERY_AUDIT-17: a special ' +
604
+ 'resolves ONLY when the site has a theme template assigned to that role, ' +
605
+ 'so search/tag/author/archive availability is SITE-SPECIFIC — a role with ' +
606
+ 'no template returns special_not_resolvable (e.g. "archive" resolves on ' +
607
+ 'one site but not another).'),
608
+ title: z
609
+ .string()
610
+ .min(1)
611
+ .optional()
612
+ .describe('Case-insensitive substring of the page NAME/label (e.g. "SunWest"). ' +
613
+ 'Zero matches → title_no_match; two or more → title_ambiguous with a ' +
614
+ 'candidate list to choose from.'),
615
+ })
616
+ .describe('Address exactly ONE page. Supply exactly one of template_id / post_id / url / ' +
617
+ 'special / title — supplying two returns a typed too_many_targets error, ' +
618
+ 'supplying none runs the unscoped default.');
467
619
  export const PAGES_RESOLVE_INPUT = {
620
+ // A-6: OPTIONAL since a target-only resolve must be expressible. The
621
+ // exactly-one-of(url, target) contract lives in `handlePagesResolve`
622
+ // (typed too_many_targets / missing_url), not in the schema.
468
623
  url: z
469
624
  .string()
470
625
  .min(1)
626
+ .optional()
471
627
  .describe('A public frontend URL of the page/article (e.g. ' +
472
628
  '"https://site.com/schedule-demo/"). Resolved to the per-page ' +
473
- 'template_id that renders it — paste a URL, get a layout-addressable id.'),
629
+ 'template_id that renders it — paste a URL, get a layout-addressable id. ' +
630
+ 'Omit when addressing the page via `target` instead.'),
631
+ target: PAGES_TARGET.optional().describe('Resolve a page WITHOUT knowing its URL — by template_id, post_id, url, a ' +
632
+ 'semantic special ("home", "blog", "404", "search", "category", "tag", ' +
633
+ '"author", "archive") or a title substring. Mutually exclusive with `url`.'),
474
634
  };
475
635
  export const PAGES_RESOLVE_OUTPUT_SCHEMA = z.object({
476
636
  // The read-tool-addressable id every read tool accepts as `template_id`
@@ -484,17 +644,27 @@ export const PAGES_RESOLVE_OUTPUT_SCHEMA = z.object({
484
644
  // Content type — `get_post_type`/archive post-type/taxonomy on WP,
485
645
  // "article"/"category" on Joomla.
486
646
  type: z.string(),
647
+ // The page's human display name. Load-bearing for the fuzzy addresses: a
648
+ // `special`/`title` resolve is only checkable if the caller can see WHICH
649
+ // page it landed on without a second round-trip. `.optional()` because a
650
+ // host predating the label fill omits it and this is a STRICT object.
651
+ label: z.string().optional(),
487
652
  // Canonical permalink (null when the platform cannot re-derive one).
488
653
  frontend_url: z.string().nullable(),
489
654
  // G-A2: the matched YT type/condition for a theme template
490
- // (e.g. "archive-gloeckle_help", "taxonomy-category_faq"), else null for a
655
+ // (e.g. "archive-example_help", "taxonomy-category_faq"), else null for a
491
656
  // per-page layout.
492
657
  context: z.string().nullable(),
493
658
  // G-A2: which kind was resolved — a per-page layout vs. the governing theme
494
659
  // template — so the agent knows it reached the right surface.
495
660
  resolved_via: z.enum(['per_page', 'theme_template']),
661
+ // A-6: WHICH branch of the `target` object matched, echoed so the caller can
662
+ // tell the user how a fuzzy address (a title substring, a semantic special)
663
+ // was interpreted. `.optional()` is load-bearing: a `url` resolve and every
664
+ // host predating A-5 omit it, and this is a STRICT object.
665
+ matched_by: z.enum(TARGET_KEYS).optional(),
496
666
  });
497
- // ─── F5 + N5 (Glöckle HANDOFF-4/5) — render_preview ──────────────────
667
+ // ─── F5 + N5 (customer HANDOFF-4/5) — render_preview ──────────────────
498
668
  //
499
669
  // The input shape (`template_id` OR `url`, exactly one — mirroring
500
670
  // page_get_layout's addressing) is assembled in `builders.ts` from the shared
@@ -516,7 +686,7 @@ export const PAGES_RESOLVE_OUTPUT_SCHEMA = z.object({
516
686
  * - cache_purge_hint: the constant operator steer (purge Rocket/CDN after save).
517
687
  * - template_id: echoed back (the resolved id the render ran on).
518
688
  * - matched: OPTIONAL — present ONLY when the caller passed `expect`; did the
519
- * rendered HTML contain that substring (Glöckle HANDOFF-6 #4).
689
+ * rendered HTML contain that substring (customer HANDOFF-6 #4).
520
690
  * - snippet: OPTIONAL — present ONLY when the caller passed `expect`; a
521
691
  * ~200-char window around the first match (or null when not
522
692
  * matched / the render threw). NULLABLE so the no-match case
@@ -529,13 +699,64 @@ export const RENDER_PREVIEW_OUTPUT_SCHEMA = z.object({
529
699
  error: z.string().optional(),
530
700
  cache_purge_hint: z.string(),
531
701
  template_id: z.string(),
532
- // F5 + N5 / Glöckle HANDOFF-6 #4 — the optional `expect` substring check.
702
+ // F5 + N5 / customer HANDOFF-6 #4 — the optional `expect` substring check.
533
703
  // Both keys are emitted ONLY when the caller passed `expect`; declared here
534
704
  // (strict ZodObject → additionalProperties:false) so a strict host does not
535
705
  // -32602-reject the `expect` variant. `snippet` is nullable (null on a miss).
536
706
  matched: z.boolean().optional(),
537
707
  snippet: z.string().nullable().optional(),
538
708
  });
709
+ // ─── T-A5 (heading_outline tool, mega-push plan 2026-07-24-02) ─────────────
710
+ //
711
+ // A real, doc-order H1-H6 outline of ONE template, each heading attributed to
712
+ // its SOURCE element (`element_type`/`path`), plus the alt-status of every
713
+ // image (including images living inside `html`/`text`/`code` raw markup).
714
+ // Read-only STRUCTURE inspection — no findings/severity (that is
715
+ // `pages_audit`'s job). `template_id`-only addressing (no `post_id`/`url`
716
+ // sugar): resolve via `yootheme_builder_pages_resolve` first, like the other
717
+ // per-page tools that need a bare template_id.
718
+ //
719
+ // The INPUT shape is assembled directly in `builders.ts` (template_id + a
720
+ // local `mode` enum), mirroring render_preview/template_summary — only the
721
+ // STRICT output schema lives here.
722
+ /**
723
+ * Wire shape for `yootheme_builder_heading_outline` on success.
724
+ *
725
+ * STRICT (additionalProperties:false default ZodObject) — every field the
726
+ * handler puts in structuredContent MUST be declared here, or a strict MCP
727
+ * host -32602-rejects the SUCCESS.
728
+ *
729
+ * - outline: every H1-H6 heading, in TRUE document order, each carrying its
730
+ * TEXT and the source element that carries it (`element_type`/`path`).
731
+ * - images: every image this pass sees (populated OR empty alt) — `alt` is
732
+ * null for a missing/empty alt, the raw string otherwise; `is_generic_alt`
733
+ * flags an exact match against the same generic-phrase list pages_audit's
734
+ * `generic-alt-text` check uses ("logo", "image", "photo", ...).
735
+ * - h1_count / has_level_skip / images_missing_alt: derived flags —
736
+ * has_level_skip mirrors pages_audit's `heading-skip` check EXACTLY (a
737
+ * level more than one above the highest seen so far, including a
738
+ * document that OPENS below h1).
739
+ */
740
+ export const HEADING_OUTLINE_OUTPUT_SCHEMA = z.object({
741
+ template_id: z.string(),
742
+ mode: z.enum(['builder', 'rendered']),
743
+ etag: z.string(),
744
+ outline: z.array(z.object({
745
+ level: z.string(),
746
+ text: z.string(),
747
+ element_type: z.string(),
748
+ path: z.string(),
749
+ })),
750
+ images: z.array(z.object({
751
+ alt: z.string().nullable(),
752
+ is_generic_alt: z.boolean(),
753
+ element_type: z.string(),
754
+ path: z.string(),
755
+ })),
756
+ h1_count: z.number().int().nonnegative(),
757
+ has_level_skip: z.boolean(),
758
+ images_missing_alt: z.number().int().nonnegative(),
759
+ });
539
760
  // ─── Task 6 (pages_audit design, 2026-07-19) — shared FINDING/QUICK_CHECK ──
540
761
  //
541
762
  // Defined ONCE here — referenced by `pages_audit`'s own output schema AND
@@ -543,21 +764,94 @@ export const RENDER_PREVIEW_OUTPUT_SCHEMA = z.object({
543
764
  // the finding shape has exactly one source of truth across all three
544
765
  // surfaces (design §5-§7). Mirrors the PHP wire shape verbatim
545
766
  // (`PageAuditService::audit()` / `AuditCheck`'s partial-finding contract).
767
+ /**
768
+ * The human-locatable address of ONE audit hit (B-3/B-5) — mirrors the PHP
769
+ * `FindingLocator::toArray()` wire shape. `path` stays the machine write
770
+ * address; `breadcrumb`/`element_type`/`element_name` are what a HUMAN needs
771
+ * to find the element in the customizer. Per the PHP null-key policy the
772
+ * optional members are OMITTED rather than sent as null.
773
+ *
774
+ * FAIL-SOFT BY CONSTRUCTION. `pages[]` rows used to ride through
775
+ * `.catchall(z.unknown())` untyped; now that `locators` is a typed array, a
776
+ * mismatch is no longer stripped — the MCP SDK THROWS `McpError`
777
+ * (InvalidParams) on an outputSchema violation, which fails the WHOLE audit
778
+ * call. So the optional members are `.nullish()` (an explicit `null` from a
779
+ * host that does not honour the omit-nulls policy must not be fatal) and the
780
+ * object carries a `.catchall`, so a field a future host version adds can
781
+ * never hard-fail an otherwise perfectly good audit.
782
+ *
783
+ * `repeat` is the one deliberately STRICT member: PHP emits it clamped to
784
+ * `>= 1` (`repeat: 0` is meaningless — a hit that renders zero times is not a
785
+ * hit), so `min(1)` states the real contract rather than tolerating a value
786
+ * that could only mean a producer bug.
787
+ */
788
+ export const FINDING_LOCATOR = z
789
+ .object({
790
+ path: z.string(),
791
+ breadcrumb: z.string(),
792
+ element_type: z.string(),
793
+ element_name: z.string().nullish(),
794
+ heading_source: z.string().nullish(),
795
+ content_snippet: z.string().nullish(),
796
+ repeat: z.number().int().min(1),
797
+ prop: z.string().nullish(),
798
+ name: z.string().nullish(),
799
+ })
800
+ .catchall(z.unknown());
546
801
  /**
547
802
  * One audit finding. `pages` carries per-page mini-evidence whose KEYS vary
548
803
  * by check code (`first_headline_path`, `alt_paths`, `cluster_id`,
549
804
  * `iterated`, `disabled_elements`, `elements_content`, ...) — `.catchall`
550
805
  * keeps every row's base `{id, label}` typed while tolerating the
551
806
  * check-specific extras without a per-code union.
807
+ *
808
+ * The ADDITIVE B-3/B-5 keys (`locators`, `hits_truncated`,
809
+ * `locators_stripped`, `builder_url`) are declared EXPLICITLY for
810
+ * self-documentation — the `.catchall` already tolerated them, but a named
811
+ * member states the contract. All stay `.optional()`: a host plugin predating
812
+ * B-3 emits none of them.
813
+ *
814
+ * `frontend_url` (T-B1, 2026-07-24) rides alongside `builder_url` on EVERY
815
+ * page entry now, not just the nine element-located "hit" checks — a
816
+ * theme/archive template with no public route legitimately omits it (never
817
+ * emitted as `null`), same convention as `builder_url`.
818
+ *
819
+ * `hits_truncated` and `locators_stripped` mean DIFFERENT things and are never
820
+ * interchangeable: the former says the upstream `hit_cap` dropped hits (so the
821
+ * raw `paths`/`alt_paths`/`skips`/`dead_anchors` arrays are incomplete), the
822
+ * latter says the transport's shrink-to-fit shed locator PROSE while leaving
823
+ * those arrays complete.
824
+ *
825
+ * FORWARD-COMPAT `.catchall`, for the same reason `FINDING_LOCATOR` carries one:
826
+ * the handler forwards each finding VERBATIM, and a strict object generates
827
+ * `additionalProperties:false`, so ONE additive field on the PHP side (an
828
+ * explicit hit total, a new evidence key) would make a strict host -32602-reject
829
+ * an otherwise perfect audit. Every declared member is still validated; only
830
+ * UNDECLARED ones ride through untyped.
552
831
  */
553
832
  export const FINDING = z.object({
554
833
  code: z.string(),
555
- category: z.enum(['seo', 'structure', 'content', 'links']),
834
+ category: z.enum(['seo', 'structure', 'content', 'links', 'design']),
556
835
  severity: z.enum(['critical', 'warning', 'info']),
557
836
  title: z.string(),
558
837
  count: z.number().int().nonnegative(),
559
838
  pages: z.array(z
560
- .object({ id: z.string(), label: z.string() })
839
+ .object({
840
+ id: z.string(),
841
+ label: z.string(),
842
+ locators: z.array(FINDING_LOCATOR).optional(),
843
+ // NAME-2: `count` is ambiguous ACROSS levels on this wire — it
844
+ // means PAGES on the finding and HITS on a page entry. The host
845
+ // therefore emits an unambiguous `hits` alias next to a page
846
+ // entry's `count` whenever that count really is a hit tally.
847
+ // Declared (not merely tolerated via `.catchall`) so the field
848
+ // is part of the contract and `hitCountOf` may prefer it.
849
+ hits: z.number().int().nonnegative().optional(),
850
+ hits_truncated: z.boolean().optional(),
851
+ locators_stripped: z.boolean().optional(),
852
+ builder_url: z.string().nullable().optional(),
853
+ frontend_url: z.string().nullable().optional(),
854
+ })
561
855
  .catchall(z.unknown())),
562
856
  pages_truncated: z.boolean(),
563
857
  // Machine-executable remediation (design §5): absent for findings with
@@ -569,7 +863,7 @@ export const FINDING = z.object({
569
863
  note: z.string(),
570
864
  })
571
865
  .optional(),
572
- });
866
+ }).catchall(z.unknown());
573
867
  /**
574
868
  * The advisory block `page_publish` (default on) / `page_save`
575
869
  * (opt-in via `quick_check:true`) attach (Task 8). Design §7: `{clean:true}`
@@ -586,6 +880,35 @@ export const QUICK_CHECK = z.object({
586
880
  * spread in separately by `builders.ts` (mirrors PAGES_RESOLVE_INPUT).
587
881
  */
588
882
  export const PAGES_AUDIT_INPUT = {
883
+ // T-A2 (mega-push plan 2026-07-24-02): opt-in rendered-DOM audit. Default
884
+ // omitted = "builder", byte-identical to the pre-T-A2 sweep (reads the
885
+ // saved layout JSON tree; no `mode`/`render_scan`/`render_errors` keys on
886
+ // the response).
887
+ mode: z
888
+ .enum(['builder', 'rendered'])
889
+ .optional()
890
+ .describe('Audit source. "builder" (default) reads the saved layout tree: fast, but blind to ' +
891
+ 'module/widget output and anything a plugin injects only at render time. ' +
892
+ '"rendered" renders each page first (via the SAME engine render_preview uses) and ' +
893
+ 'audits the ACTUAL DOM: a curated subset of checks (headings/alt/anchor-text/' +
894
+ 'dead-anchor/design; NOT thin-content/duplicate-layout/empty-page, which need the ' +
895
+ 'builder tree). This is the MOST valuable mode for the "design" category, since a ' +
896
+ 'plugin form (Formidable/ACF) only exists at render time. Findings carry no builder ' +
897
+ 'element path in this mode (empty breadcrumb/' +
898
+ 'element_type by design, there is no builder node to point at); locate the hit by ' +
899
+ 'its content_snippet or the page\'s builder_url/frontend_url instead. A SITE-wide ' +
900
+ 'rendered sweep is capped (rendering is expensive); the response\'s render_scan ' +
901
+ '{attempted, rendered, capped} always says so honestly, and render_errors lists any ' +
902
+ 'page whose render failed (excluded from findings, never reported as a false clean).'),
903
+ // A-6: scope the whole sweep to ONE page. Omit for the site-wide default.
904
+ // Site-wide checks that compare pages against EACH OTHER (duplicate-layout,
905
+ // title-collision) cannot run on a single page and self-filter out; the
906
+ // response says so in its `note`.
907
+ target: PAGES_TARGET.optional().describe('Audit ONE page instead of the whole site — by template_id, post_id, url, a ' +
908
+ 'semantic special ("home", "blog", "404", "search", "category", "tag", ' +
909
+ '"author", "archive") or a title substring. The response then carries ' +
910
+ 'scope:"single" + resolved_target (which page was actually audited) and ' +
911
+ 'per-hit locators. Cannot be combined with `types`.'),
589
912
  types: z
590
913
  .array(z.string().min(1))
591
914
  .optional()
@@ -593,12 +916,28 @@ export const PAGES_AUDIT_INPUT = {
593
916
  'same `type` values yootheme_builder_pages_list rows carry). Default: every ' +
594
917
  'scanned template/page is included.'),
595
918
  checks: z
596
- .array(z.enum(['seo', 'structure', 'content', 'links']))
919
+ .array(z.enum(['seo', 'structure', 'content', 'links', 'design']))
597
920
  .optional()
598
- .describe('Run only these check categories (default: all four). "seo" = title-collision, ' +
921
+ .describe('Run only these check categories (default: all five). "seo" = title-collision, ' +
599
922
  'non-pretty-url, stale-content. "structure" = empty-page, missing-h1, ' +
600
- 'multiple-h1, no-headings, heading-skip. "content" = images-missing-alt, ' +
601
- 'thin-content, duplicate-layout. "links" = dead-anchor.'),
923
+ 'multiple-h1, no-headings, heading-skip, heading-hidden-on-mobile (hidden at ' +
924
+ 'the mobile-first UIkit breakpoint; Google indexes mobile-first, so this is ' +
925
+ 'effectively invisible for ranking), long-heading. "content" = ' +
926
+ 'images-missing-alt, generic-alt-text, thin-content, duplicate-layout. ' +
927
+ '"links" = dead-anchor, anchor-text-quality. "design" = design-missing-uikit-class, ' +
928
+ 'design-foreign-class, design-inline-style: markup breaking out of the UIkit ' +
929
+ 'design system (e.g. a plugin form submit button with no uk-button); most ' +
930
+ 'valuable in mode:"rendered", where a plugin\'s own output actually exists — ' +
931
+ 'plus header-contrast-risk (warning): the first section pulls the navigation ' +
932
+ 'over itself (header_transparent) in a colour the theme\'s own --uk-inverse ' +
933
+ 'declaration contradicts, with no image or video hiding the background that ' +
934
+ 'declaration describes; and header-contrast-unverifiable (info): the same ' +
935
+ 'transparent header over a hero image/video, whatever the colour — the medium ' +
936
+ 'hides that background, so no colour is verifiable from the props, none is ' +
937
+ 'recommended, and readability is only verifiable by looking at the rendered ' +
938
+ 'page. Both are builder-mode only (section props do not exist ' +
939
+ 'in a rendered document). Headings and image alt are also read from raw HTML ' +
940
+ '(html/text element content), not just builder props.'),
602
941
  thin_content_threshold: z
603
942
  .number()
604
943
  .int()
@@ -613,7 +952,17 @@ export const PAGES_AUDIT_INPUT = {
613
952
  .optional()
614
953
  .describe('Months since a page\'s `modified_at` after which it is flagged `stale-content`. ' +
615
954
  'Pages with an empty modified_at are never flagged. Default: 24.'),
616
- max_chars: MAX_CHARS,
955
+ // PERF-4 / PROD-2: on THIS tool `max_chars` bounds BOTH legs — the shared
956
+ // describe's "structuredContent stays complete" is true everywhere else,
957
+ // but an audit's structured leg was measured at 349 KB (~89k tokens) for a
958
+ // site sweep, so it carries its own default ceiling
959
+ // ({@see STRUCTURED_BUDGET_CHARS}) that this argument raises or lowers.
960
+ max_chars: MAX_CHARS.describe('Optional cap (chars) on this result. Bounds BOTH the text leg and the ' +
961
+ 'structuredContent payload (default: 48000 chars for structuredContent). ' +
962
+ 'When the structured leg has to shrink, locator detail is shed before ' +
963
+ 'evidence pages and the response carries structured_truncated:true. Raise ' +
964
+ 'it for a complete pathological page, lower it on a tight context budget.'),
965
+ include_meta: INCLUDE_META_SCHEMA,
617
966
  };
618
967
  /**
619
968
  * Wire shape for `yootheme_builder_pages_audit` (design §5). `scanned.by_type`
@@ -623,6 +972,34 @@ export const PAGES_AUDIT_INPUT = {
623
972
  */
624
973
  export const PAGES_AUDIT_OUTPUT_SCHEMA = z.object({
625
974
  etag: z.string(),
975
+ // B-5 — the three single-target keys. `.optional()` is LOAD-BEARING, not
976
+ // decoration: this is a strict object, and both a site sweep and every
977
+ // host predating single-target mode omit all three, so a non-optional
978
+ // member would make every such (perfectly valid) response fail validation.
979
+ scope: z.string().optional(),
980
+ note: z.string().optional(),
981
+ // Every member of `PageQuery::descriptorFromRow()` /
982
+ // `augmentResolverDescriptor()` must be declared here — the audit handler
983
+ // forwards `resolved_target` VERBATIM, and this object generates
984
+ // `additionalProperties:false`, so an undeclared key makes a strict host
985
+ // -32602-reject a SUCCESSFUL call (W12-R2). All eight are `.optional()`
986
+ // because a host predating single-target mode sends none of them.
987
+ resolved_target: z
988
+ .object({
989
+ template_id: z.string().optional(),
990
+ post_id: z.union([z.number(), z.string()]).nullable().optional(),
991
+ type: z.string().optional(),
992
+ label: z.string().optional(),
993
+ frontend_url: z.string().nullable().optional(),
994
+ // The matched YT type/condition for a theme template
995
+ // (e.g. "taxonomy-category_faq"); null for a per-page layout.
996
+ context: z.string().nullable().optional(),
997
+ // `per_page` | `theme_template` — kept a plain string rather than an
998
+ // enum so a newer host's additional kind is forwarded, not rejected.
999
+ resolved_via: z.string().optional(),
1000
+ matched_by: z.string().optional(),
1001
+ })
1002
+ .optional(),
626
1003
  scanned: z.object({
627
1004
  templates: z.number().int().nonnegative(),
628
1005
  by_type: z.record(z.string(), z.number().int().nonnegative()),
@@ -634,5 +1011,36 @@ export const PAGES_AUDIT_OUTPUT_SCHEMA = z.object({
634
1011
  }),
635
1012
  findings: z.array(FINDING),
636
1013
  disclaimer: z.string(),
1014
+ // PERF-4 / PROD-2 — set by the TS handler (never by a host) when the shed
1015
+ // ladder had to run to bring `structuredContent` under its char budget.
1016
+ // Its PRESENCE is the whole signal: a complete payload omits the key
1017
+ // entirely, so a reader can never mistake a shrunk audit for a full one.
1018
+ // Which entries lost what is flagged in place — `locators_stripped` on the
1019
+ // page rows whose locator prose went, `pages_truncated` on the findings
1020
+ // whose evidence pages were capped or dropped.
1021
+ structured_truncated: z.boolean().optional(),
1022
+ // T-A2 — present ONLY when `mode:"rendered"` was requested (absent on the
1023
+ // default builder sweep, matching `scope`'s own "absent means the old
1024
+ // behaviour" convention). `render_scan` is ALWAYS present alongside it,
1025
+ // even when the cap never engaged — Design-Nut 4's "never silent" rule:
1026
+ // a caller can always see attempted vs. actually-rendered.
1027
+ mode: z.enum(['rendered']).optional(),
1028
+ render_scan: z
1029
+ .object({
1030
+ attempted: z.number().int().nonnegative(),
1031
+ rendered: z.number().int().nonnegative(),
1032
+ capped: z.boolean(),
1033
+ })
1034
+ .optional(),
1035
+ // Present only when at least one template's render threw — excluded from
1036
+ // findings entirely (a render failure must never masquerade as a content
1037
+ // finding), surfaced here instead so nothing is silently dropped.
1038
+ render_errors: z
1039
+ .array(z.object({
1040
+ id: z.string(),
1041
+ label: z.string(),
1042
+ error: z.string(),
1043
+ }))
1044
+ .optional(),
637
1045
  });
638
1046
  //# sourceMappingURL=schemas.js.map