@case-framework/survey-assistant 0.6.0 → 0.8.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 (135) hide show
  1. package/README.md +67 -7
  2. package/dist/{attachments-B0hg3vpT.mjs → attachments-BuNni5vB.mjs} +1 -1
  3. package/dist/{attachments-B0hg3vpT.mjs.map → attachments-BuNni5vB.mjs.map} +1 -1
  4. package/dist/{authoring-references-CW38pYzK.mjs → authoring-references-DbuOKerg.mjs} +13 -4
  5. package/dist/authoring-references-DbuOKerg.mjs.map +1 -0
  6. package/dist/{capabilities-DVTfogjv.d.mts → capabilities-CmeeAjEc.d.mts} +3 -4
  7. package/dist/capabilities-CmeeAjEc.d.mts.map +1 -0
  8. package/dist/capabilities-DCMA71PP.mjs +58 -0
  9. package/dist/capabilities-DCMA71PP.mjs.map +1 -0
  10. package/dist/capabilities-default.d.mts +13 -12
  11. package/dist/capabilities-default.d.mts.map +1 -1
  12. package/dist/capabilities-default.mjs +2 -414
  13. package/dist/{constants-BMGTgfjv.d.mts → constants-B6HzpEsx.d.mts} +7 -28
  14. package/dist/constants-B6HzpEsx.d.mts.map +1 -0
  15. package/dist/{constants-0UHGFcRJ.mjs → constants-Ct-vv9cu.mjs} +1 -1
  16. package/dist/{constants-0UHGFcRJ.mjs.map → constants-Ct-vv9cu.mjs.map} +1 -1
  17. package/dist/controller-proxy-D7uQfm5f.d.mts +65 -0
  18. package/dist/controller-proxy-D7uQfm5f.d.mts.map +1 -0
  19. package/dist/{controller-proxy-DMTqer6T.mjs → controller-proxy-DEeFl9IA.mjs} +29 -26
  20. package/dist/controller-proxy-DEeFl9IA.mjs.map +1 -0
  21. package/dist/default-fhSon2RM.mjs +760 -0
  22. package/dist/default-fhSon2RM.mjs.map +1 -0
  23. package/dist/{digest-SJmoYVaK.d.mts → digest-C39WyAWG.d.mts} +2 -3
  24. package/dist/digest-C39WyAWG.d.mts.map +1 -0
  25. package/dist/digest.d.mts +1 -1
  26. package/dist/digest.mjs.map +1 -1
  27. package/dist/{engine-CQwOAcwK.mjs → engine-nXoCWYpQ.mjs} +2821 -2018
  28. package/dist/engine-nXoCWYpQ.mjs.map +1 -0
  29. package/dist/engine.d.mts +3 -3
  30. package/dist/engine.mjs +3 -3
  31. package/dist/{fair-guidance-CRYCRxQm.mjs → fair-guidance-BmO4PswW.mjs} +1 -1
  32. package/dist/{fair-guidance-CRYCRxQm.mjs.map → fair-guidance-BmO4PswW.mjs.map} +1 -1
  33. package/dist/{index-D6nqkz0O.d.mts → index-Ciq7vHIs.d.mts} +121 -197
  34. package/dist/index-Ciq7vHIs.d.mts.map +1 -0
  35. package/dist/{index-DvTqML-0.d.mts → index-D7wT6P3t.d.mts} +294 -63
  36. package/dist/index-D7wT6P3t.d.mts.map +1 -0
  37. package/dist/index-WB5kfnz0.d.mts +348 -0
  38. package/dist/index-WB5kfnz0.d.mts.map +1 -0
  39. package/dist/{index-B0lxsAzx.d.mts → index-r5riv4md.d.mts} +50 -29
  40. package/dist/index-r5riv4md.d.mts.map +1 -0
  41. package/dist/{lifecycle-BmGAIUlv.d.mts → lifecycle-Dm8u7QFh.d.mts} +40 -18
  42. package/dist/lifecycle-Dm8u7QFh.d.mts.map +1 -0
  43. package/dist/{memory-thread-repository-CjFGVNZh.mjs → memory-thread-repository-DAnOGIB3.mjs} +89 -34
  44. package/dist/memory-thread-repository-DAnOGIB3.mjs.map +1 -0
  45. package/dist/{protocol-DJUP4gc0.mjs → protocol-CF4Bh_Ch.mjs} +182 -50
  46. package/dist/protocol-CF4Bh_Ch.mjs.map +1 -0
  47. package/dist/protocol.d.mts +4 -4
  48. package/dist/protocol.mjs +3 -3
  49. package/dist/react-C_GY-Fsc.mjs +1727 -0
  50. package/dist/react-C_GY-Fsc.mjs.map +1 -0
  51. package/dist/react-integration.d.mts +2 -2
  52. package/dist/react-integration.mjs +2 -2
  53. package/dist/react.d.mts +3 -3
  54. package/dist/react.mjs +3 -3
  55. package/dist/references/assistant-operations.md +143 -68
  56. package/dist/references/core-rules.md +6 -3
  57. package/dist/references/embedded-forms.md +34 -14
  58. package/dist/references/expressions.md +238 -48
  59. package/dist/references/fair-by-design.md +9 -9
  60. package/dist/references/item-types.md +306 -72
  61. package/dist/references/localization.md +5 -4
  62. package/dist/references/response-variables.md +38 -0
  63. package/dist/references/rich-text-content.md +60 -27
  64. package/dist/references/source-material-surveys.md +13 -5
  65. package/dist/references/survey-data-model.md +32 -9
  66. package/dist/{request-context-BjTHNcDd.mjs → request-context-Dg4jSwN1.mjs} +4 -10
  67. package/dist/request-context-Dg4jSwN1.mjs.map +1 -0
  68. package/dist/{request-guardrails-TmgQtqp4.mjs → request-guardrails-C0ugBOp0.mjs} +6 -5
  69. package/dist/request-guardrails-C0ugBOp0.mjs.map +1 -0
  70. package/dist/server-agent.d.mts +53 -44
  71. package/dist/server-agent.d.mts.map +1 -1
  72. package/dist/server-agent.mjs +48 -27
  73. package/dist/server-agent.mjs.map +1 -1
  74. package/dist/server-runtime.d.mts +36 -49
  75. package/dist/server-runtime.d.mts.map +1 -1
  76. package/dist/server-runtime.mjs +17 -12
  77. package/dist/server-runtime.mjs.map +1 -1
  78. package/dist/server-tasks.d.mts +2 -2
  79. package/dist/server-tasks.mjs +2 -313
  80. package/dist/server-tools.d.mts +1 -1
  81. package/dist/server-tools.mjs +1 -1
  82. package/dist/server.d.mts +13 -36
  83. package/dist/server.d.mts.map +1 -1
  84. package/dist/server.mjs +5 -5
  85. package/dist/server.mjs.map +1 -1
  86. package/dist/storage-postgres.d.mts +9 -25
  87. package/dist/storage-postgres.d.mts.map +1 -1
  88. package/dist/storage-postgres.mjs +5 -4
  89. package/dist/storage-postgres.mjs.map +1 -1
  90. package/dist/{tasks-DxzD25Zc.d.mts → tasks-B9h3LPam.d.mts} +19 -35
  91. package/dist/tasks-B9h3LPam.d.mts.map +1 -0
  92. package/dist/tasks-BZEiwgxA.mjs +396 -0
  93. package/dist/tasks-BZEiwgxA.mjs.map +1 -0
  94. package/dist/thread-documents-DrHbaXqP.d.mts +119 -0
  95. package/dist/thread-documents-DrHbaXqP.d.mts.map +1 -0
  96. package/dist/{thread-handlers-AbQbt4sX.d.mts → thread-handlers-Bef0td9N.d.mts} +10 -40
  97. package/dist/thread-handlers-Bef0td9N.d.mts.map +1 -0
  98. package/dist/{tools-CFh35R4d.mjs → tools-CEsv-WoO.mjs} +705 -578
  99. package/dist/tools-CEsv-WoO.mjs.map +1 -0
  100. package/dist/ui.css +1 -1
  101. package/dist/ui.d.mts +17 -55
  102. package/dist/ui.d.mts.map +1 -1
  103. package/dist/ui.mjs +759 -171
  104. package/dist/ui.mjs.map +1 -1
  105. package/docs/integration.md +51 -1
  106. package/package.json +49 -42
  107. package/dist/authoring-references-CW38pYzK.mjs.map +0 -1
  108. package/dist/capabilities-DVTfogjv.d.mts.map +0 -1
  109. package/dist/capabilities-default.mjs.map +0 -1
  110. package/dist/constants-BMGTgfjv.d.mts.map +0 -1
  111. package/dist/controller-proxy-DMTqer6T.mjs.map +0 -1
  112. package/dist/controller-proxy-mRvez2qw.d.mts +0 -85
  113. package/dist/controller-proxy-mRvez2qw.d.mts.map +0 -1
  114. package/dist/digest-SJmoYVaK.d.mts.map +0 -1
  115. package/dist/engine-CQwOAcwK.mjs.map +0 -1
  116. package/dist/form-authoring-kQDgC5oz.mjs +0 -155
  117. package/dist/form-authoring-kQDgC5oz.mjs.map +0 -1
  118. package/dist/index-B0lxsAzx.d.mts.map +0 -1
  119. package/dist/index-BS-Xjk1h.d.mts +0 -224
  120. package/dist/index-BS-Xjk1h.d.mts.map +0 -1
  121. package/dist/index-D6nqkz0O.d.mts.map +0 -1
  122. package/dist/index-DvTqML-0.d.mts.map +0 -1
  123. package/dist/lifecycle-BmGAIUlv.d.mts.map +0 -1
  124. package/dist/memory-thread-repository-CjFGVNZh.mjs.map +0 -1
  125. package/dist/protocol-DJUP4gc0.mjs.map +0 -1
  126. package/dist/react-Dqlmasrs.mjs +0 -768
  127. package/dist/react-Dqlmasrs.mjs.map +0 -1
  128. package/dist/request-context-BjTHNcDd.mjs.map +0 -1
  129. package/dist/request-guardrails-TmgQtqp4.mjs.map +0 -1
  130. package/dist/server-tasks.mjs.map +0 -1
  131. package/dist/tasks-DxzD25Zc.d.mts.map +0 -1
  132. package/dist/thread-documents-rmd6nyX9.d.mts +0 -107
  133. package/dist/thread-documents-rmd6nyX9.d.mts.map +0 -1
  134. package/dist/thread-handlers-AbQbt4sX.d.mts.map +0 -1
  135. package/dist/tools-CFh35R4d.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
- import { X as SURVEY_OPERATION_KINDS, bt as surveyOperationSchemasByKind, f as surveyChangeSetSchema, q as assistantItemTypeManifestSchema, yt as surveyOperationSchema } from "./protocol-DJUP4gc0.mjs";
2
- import { d as getDeterministicCreateItemTypes, f as inspectAssistantItem, p as resolveAssistantCapabilityReference } from "./form-authoring-kQDgC5oz.mjs";
3
- import { A as getKeyIndexEntries, F as itemSummarySchema, G as summarizeKeyIndexEntries, H as sliceWithCursor, I as mapKeyIndexById, J as summarizeRawItem, L as mapOutlineById, M as getRawSurveyItems, N as itemConfigDetailsSchema, P as itemMatchesQuery, R as mapRawItemsById, T as findDirectChildrenByFullKey, U as summarizeFocusItemNode, V as resolveItemSummary, W as summarizeItemById, X as unique, Y as translationEntrySchema, Z as analyzeSurveyAssistantExpressionReferences, b as buildItemConfigDetails, f as normalizeSurveyChangeOperations, i as auditSurvey, j as getRawSurvey, l as createSurveyChangeSet, m as expressionMutationSchema, p as buildExpressionMutationPatches, q as summarizeOutlineNode, v as buildConfigSearchText, w as clampLimit, x as buildItemTranslationEntries, y as buildEmbeddedFormDetails, z as normalizeText } from "./engine-CQwOAcwK.mjs";
4
- import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-BjTHNcDd.mjs";
5
- import { a as surveyAuthoringReferences, i as surveyAuthoringReferenceTopics, r as getBuiltInItemTypeAuthoringReference, t as builtInSurveyAuthoringItemTypes } from "./authoring-references-CW38pYzK.mjs";
1
+ import { Q as SURVEY_OPERATION_KINDS, Y as assistantItemTypeManifestSchema, bt as surveyOperationSchema, f as surveyChangeSetSchema, xt as surveyOperationSchemasByKind } from "./protocol-CF4Bh_Ch.mjs";
2
+ import { C as isRecord$3, D as expressionMutationSchema, G as getRawSurveyItems, J as itemSummarySchema, K as itemConfigDetailsSchema, L as clampLimit, M as buildEmbeddedFormDetails, N as buildItemConfigDetails, P as buildItemTranslationEntries, Q as normalizeText, R as findDirectChildrenByParentId, T as buildExpressionMutationBatch, U as getItemIndexEntries, W as getRawSurvey, X as mapOutlineById, Y as mapItemIndexById, Z as mapRawItemsById, _t as createAssistantSurvey, ct as translationEntrySchema, d as collectExpressionMutationRecoveryTargets, et as resolveItemSummary, f as collectMalformedSurveyOperationRecoveryTargets, ft as getSurveyAssistantExpressionDiagnosticGuidance, it as summarizeItemIndexEntries, j as buildConfigSearchText, lt as unique, m as auditSurvey, nt as summarizeFocusItemNode, ot as summarizeOutlineNode, p as collectSurveyOperationRecoveryTargets, q as itemMatchesQuery, rt as summarizeItemById, st as summarizeRawItem, tt as sliceWithCursor, u as collectExpressionMutationRecoveryRefinements, ut as analyzeSurveyAssistantExpressionReferences, v as createSurveyChangeSet, vt as getAssistantVisibilityIssues, w as normalizeSurveyChangeOperations } from "./engine-nXoCWYpQ.mjs";
3
+ import { a as getDeterministicCreateItemTypes, o as inspectAssistantItem, s as resolveAssistantCapabilityReference } from "./capabilities-DCMA71PP.mjs";
4
+ import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-Dg4jSwN1.mjs";
5
+ import { a as surveyAuthoringReferences, i as surveyAuthoringReferenceTopics, r as getBuiltInItemTypeAuthoringReference, t as builtInSurveyAuthoringItemTypes } from "./authoring-references-DbuOKerg.mjs";
6
6
  import { z } from "zod";
7
- import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getContentPlainText, getValueAtJsonPointer } from "@case-framework/survey-core";
7
+ import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getContentPlainText, getValueAtJsonPointer, isBlockingSurveyExpressionReferenceDiagnostic } from "@case-framework/survey-core";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createTool, isValidationError } from "@mastra/core/tools";
10
10
  //#region src/server/operation-contract-reference.ts
@@ -53,16 +53,31 @@ const formatItemCapabilityProfile = (manifest, kind) => {
53
53
  "",
54
54
  `${manifest.displayName}: ${manifest.description ?? "No additional description."}`,
55
55
  `Creation mode: ${manifest.create}.`,
56
+ `Supported operations: ${manifest.supportedOperations.join(", ")}.`,
57
+ manifest.authoring?.compactHint,
56
58
  manifest.authoring?.configSummary ? `Configuration: ${manifest.authoring.configSummary}` : void 0,
57
59
  manifest.creationPresets?.length ? `Available creation preset ids: ${manifest.creationPresets.map((preset) => `\`${preset.id}\``).join(", ")}. Use the server-trusted configuration guidance above for their semantics.` : void 0,
58
- fixedSurfaces.length > 0 ? `Fixed content surfaces: ${fixedSurfaces.map((surface) => surface.key).join(", ")}.` : "Fixed content surfaces: none.",
59
- patternedSurfaces.length > 0 ? `Dynamic content surfaces: ${patternedSurfaces.map((surface) => surface.key).join(", ")}.` : void 0,
60
- kind === "create-item" && requiredSurfaces.length > 0 ? `Required on creation: ${requiredSurfaces.map((surface) => surface.key).join(", ")}.` : void 0,
61
- manifest.referenceTopics?.length ? `Load a dedicated reference only when needed: ${manifest.referenceTopics.join(", ")}.` : void 0
60
+ fixedSurfaces.length > 0 ? `Fixed content surfaces: ${fixedSurfaces.map((surface) => `${surface.key} (format=${surface.format})`).join(", ")}.` : "Fixed content surfaces: none.",
61
+ patternedSurfaces.length > 0 ? `Dynamic content surfaces: ${patternedSurfaces.map((surface) => `${surface.key} (format=${surface.format})`).join(", ")}.` : void 0,
62
+ (!kind || kind === "create-item") && requiredSurfaces.length > 0 ? `Required on creation: ${requiredSurfaces.map((surface) => surface.key).join(", ")}.` : void 0,
63
+ manifest.referenceTopics?.length ? `Load dedicated guidance with survey_authoring_reference using these exact arguments: ${manifest.referenceTopics.map((topic) => JSON.stringify({
64
+ topic,
65
+ itemType: manifest.itemType
66
+ })).join("; ")}.` : void 0
62
67
  ].filter((line) => Boolean(line));
63
68
  if (kind === "create-item") lines.push("", "Creation scope: include the initial structure, configuration, and visible translations that are already known. A later successful mutation call in the same turn may target the created id; the server adds that delta to the validated turn draft.");
64
69
  return lines.join("\n");
65
70
  };
71
+ const formatItemConfigurationContract = (schema) => [
72
+ "## Validated item configuration",
73
+ "This schema validates the resulting configuration after creation or an atomic patch. patch-item-config paths are relative to this object. Inspect existing configuration and preserve surviving identities; include related translations in the same proposal.",
74
+ "```json",
75
+ JSON.stringify(z.toJSONSchema(schema, {
76
+ io: "input",
77
+ unrepresentable: "any"
78
+ }), null, 2),
79
+ "```"
80
+ ].join("\n\n");
66
81
  const formatSurveyOperationContractReference = (kind, options = {}) => {
67
82
  const { $schema: _schemaVersion, ...contract } = z.toJSONSchema(surveyOperationSchemasByKind[kind], {
68
83
  io: "input",
@@ -76,15 +91,16 @@ const formatSurveyOperationContractReference = (kind, options = {}) => {
76
91
  `# \`${kind}\` operation contract`,
77
92
  "",
78
93
  "This JSON Schema is generated from the structural validator used by `survey_change`; it is",
79
- "the canonical field shape. Cross-field and snapshot-dependent rules are enforced when the",
80
- "proposal is validated. The tool may normalize limited schema-near shorthand, but prefer this",
81
- "canonical form.",
94
+ "the canonical post-normalization field shape. Cross-field and snapshot-dependent rules are",
95
+ "enforced when the proposal is validated. Follow focused authoring guidance when it recommends",
96
+ "a supported model-facing shorthand; otherwise use this canonical shape.",
82
97
  detail === "compact" ? "This compact view preserves the operation structure while omitting deeply nested value schemas. Request detail \"full\" only for protocol debugging; load a focused rich-text, expression, form, or item-type reference for authoring guidance." : "This full view is intended for protocol debugging and may be large.",
83
98
  "",
84
99
  "```json",
85
100
  JSON.stringify(renderedContract, null, 2),
86
101
  "```",
87
- ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : []
102
+ ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : [],
103
+ ...options.itemConfigurationSchema && (kind === "patch-item-config" || kind === "create-item") ? [formatItemConfigurationContract(options.itemConfigurationSchema)] : []
88
104
  ].join("\n");
89
105
  };
90
106
  //#endregion
@@ -139,7 +155,7 @@ const suggestCanonicalNames = (input, candidates, limit = 3) => [...new Set(cand
139
155
  //#endregion
140
156
  //#region src/server/tools/tool-model-output.ts
141
157
  const FALLBACK_MODEL_TEXT = "The tool returned no model-readable output. Review the tool arguments and retry with valid values.";
142
- const unwrapToolOutput$1 = (value) => value && typeof value === "object" && "output" in value && value.output !== void 0 ? value.output : value;
158
+ const unwrapToolOutput = (value) => value && typeof value === "object" && "output" in value && value.output !== void 0 ? value.output : value;
143
159
  const createTextModelOutput = (text) => ({
144
160
  type: "content",
145
161
  value: [{
@@ -148,7 +164,7 @@ const createTextModelOutput = (text) => ({
148
164
  }]
149
165
  });
150
166
  const toTextModelOutput = (value, summarize) => {
151
- const output = unwrapToolOutput$1(value);
167
+ const output = unwrapToolOutput(value);
152
168
  if (isValidationError(output)) return createTextModelOutput(output.message);
153
169
  try {
154
170
  return createTextModelOutput(summarize(output));
@@ -158,7 +174,7 @@ const toTextModelOutput = (value, summarize) => {
158
174
  };
159
175
  //#endregion
160
176
  //#region src/server/tools/survey-authoring-reference-tool.ts
161
- const TOOL_ID$10 = "survey_authoring_reference";
177
+ const TOOL_ID$6 = "survey_authoring_reference";
162
178
  const topicSchema = z.string().min(1).max(120);
163
179
  const inputSchema$10 = z.object({
164
180
  topic: topicSchema.describe("The narrow authoring reference to load."),
@@ -181,7 +197,7 @@ const formatCanonicalNameRecovery = (input, candidates, noun) => {
181
197
  return suggestions.length > 0 ? ` Closest canonical ${noun}${suggestions.length === 1 ? "" : "s"}: ${suggestions.map((value) => `"${value}"`).join(", ")}. Retry with the intended canonical name.` : " Retry without the focused name to load the available catalog.";
182
198
  };
183
199
  const surveyAuthoringReferenceTool = createTool({
184
- id: TOOL_ID$10,
200
+ id: TOOL_ID$6,
185
201
  description: "Load one narrow CASE authoring reference. With topic \"item-types\", pass itemType to load only that type. For a survey_change operation, use topic \"assistant-operations\" with operationKind and optionally itemType to compose the structural contract with that installed item capability. Compact detail is the authoring default; full detail is only for protocol debugging.",
186
202
  inputSchema: inputSchema$10,
187
203
  outputSchema: outputSchema$8,
@@ -241,7 +257,8 @@ const surveyAuthoringReferenceTool = createTool({
241
257
  title: itemCapability ? `${inputData.operationKind} for ${itemCapability.displayName}` : `${inputData.operationKind} operation contract`,
242
258
  content: formatSurveyOperationContractReference(inputData.operationKind, {
243
259
  detail: contractDetail,
244
- itemCapability
260
+ itemCapability,
261
+ itemConfigurationSchema: inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.configurationSchema : void 0
245
262
  })
246
263
  };
247
264
  }
@@ -264,14 +281,27 @@ const surveyAuthoringReferenceTool = createTool({
264
281
  title: capabilityReference.title ?? `${capability?.manifest.displayName} authoring`,
265
282
  content: capabilityReference.markdown
266
283
  } : void 0;
267
- const entry = focusedBuiltInItemTypeReference ?? capabilityEntry ?? builtInEntry;
284
+ const focusedCapabilityProfile = inputData.topic === "item-types" && itemCapability ? {
285
+ title: itemCapability.displayName,
286
+ content: formatItemCapabilityProfile(itemCapability)
287
+ } : void 0;
288
+ const baseEntry = inputData.topic === "item-types" && inputData.itemType ? focusedBuiltInItemTypeReference ?? focusedCapabilityProfile : builtInEntry;
289
+ const entry = baseEntry && capabilityEntry ? {
290
+ title: baseEntry.title,
291
+ content: baseEntry.content === capabilityEntry.content ? baseEntry.content : [
292
+ baseEntry.content,
293
+ "## Item-specific constraints",
294
+ "Apply the following capability constraints when using the shared grammar above.",
295
+ capabilityEntry.content
296
+ ].join("\n\n")
297
+ } : baseEntry ?? capabilityEntry;
268
298
  if (!entry) return {
269
299
  ok: false,
270
300
  topic: inputData.topic,
271
301
  itemType: inputData.itemType,
272
302
  operationKind: inputData.operationKind,
273
303
  title: "Reference unavailable",
274
- content: (inputData.itemType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.itemType}".` : `No built-in reference named "${inputData.topic}" is available.`) + formatCanonicalNameRecovery(inputData.topic, surveyAuthoringReferenceTopics, "reference")
304
+ content: (inputData.itemType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.itemType}".` : `No built-in reference named "${inputData.topic}" is available.`) + formatCanonicalNameRecovery(inputData.topic, [...surveyAuthoringReferenceTopics, ...itemCapability?.referenceTopics ?? []], "reference")
275
305
  };
276
306
  return {
277
307
  ok: true,
@@ -279,35 +309,51 @@ const surveyAuthoringReferenceTool = createTool({
279
309
  itemType: inputData.itemType,
280
310
  operationKind: inputData.operationKind,
281
311
  title: entry.title,
282
- content: entry.content
312
+ content: inputData.topic === "item-types" && capability?.configurationSchema ? `${entry.content}\n\n${formatItemConfigurationContract(capability.configurationSchema)}` : entry.content
283
313
  };
284
314
  },
285
315
  toModelOutput: (value) => toTextModelOutput(value, (output) => `# ${output.title}\n\n${output.content}`)
286
316
  });
287
317
  //#endregion
318
+ //#region src/server/tools/mutation-recovery.ts
319
+ const retryOfChangeIdsSchema = z.array(z.string().min(1)).max(20).optional().describe("Rejected ids for required retries or intentional removal of pending targets. Retry the complete corrected scope with non-empty mutations. Only allowsAcknowledgment permits inspected no-ops via survey_change operations: []. Linkage never clears identifiable pending edits.");
320
+ const summarizePendingRepairs = (event) => {
321
+ if (!event.pendingRepairs?.length) return [];
322
+ const requiredIds = event.pendingRepairs.filter((repair) => repair.requiresExplicitRetry).map((repair) => repair.changeId);
323
+ return [
324
+ ...requiredIds.length > 0 ? [`Required retry linkage (copy exactly): ${JSON.stringify({ retryOfChangeIds: requiredIds })}. This addresses linkage only; preserve the requested content and structure while correcting validation errors.`] : [],
325
+ "TURN APPLY STATE: BLOCKED. These rejected proposal obligations remain unapplied. Repair every target; successful partial retries accumulate but do not clear the remaining obligations.",
326
+ ...event.pendingRepairs.map((repair) => `Rejected change ${repair.changeId}: ${JSON.stringify(repair.targets)}.${repair.requiresExplicitRetry ? ` This repair requires explicit linkage. Include every id in the aggregate Required retry linkage above with the complete corrected scope. ${repair.allowsAcknowledgment ? "Validator-confirmed no-op obligations may be acknowledged after inspection with survey_change operations: []; other obligations remain pending." : "A non-empty corrected retry is required; an empty acknowledgment cannot resolve this rejection."} Identifiable targets still need repair. No further mutations are staged until required links are supplied.` : " Retry these same operations and surfaces with corrected values. If intentionally removing a pending target, include its rejected change id in retryOfChangeIds."}`)
327
+ ];
328
+ };
329
+ //#endregion
288
330
  //#region src/server/tools/survey-change-tool.ts
289
- const TOOL_ID$9 = "survey_change";
331
+ const TOOL_ID$5 = "survey_change";
290
332
  const OPERATIONS_DESCRIPTION = [
291
333
  "Typed survey operation objects to validate and package as one undoable change. Every object uses discriminator \"kind\", never \"type\".",
292
334
  "For an unfamiliar canonical shape, load survey_authoring_reference with topic \"assistant-operations\" and operationKind; it returns only that operation schema, generated from the validator.",
293
335
  "Any translations owned by an operation must be inside that same operation object. Never put translations beside the survey_change operations array.",
294
- "Create items with { kind: \"create-item\", target: { parentItemId, index? }, item: { id?, key, itemType, config }, translations? }. Put respondent content in top-level translations and item settings in item.config.",
295
- "Use update-item-translation with itemId, locale, contentKey, and plainText for ordinary text or content for a complete structured value. Item titles and option/field labels are translations, not item labels.",
336
+ "Every translation payload uses exactly one representation: plainText, canonical content, or the model-facing richTextBlocks shorthand. Shorthand is accepted wherever a typed translation accepts structured content and is normalized to canonical content before validation. Use it only for surfaces that focused guidance identifies as rendering rich text.",
337
+ "Canonical create-item shape: put parentItemId and optional zero-based index in target (omitting index appends; index: 0 inserts first); put the required itemType, plus optional id, config, and creation-time expressions, in item; put respondent-visible plainText or structured content in top-level translations. Put choice maxSelection and options inside item.config. For top-level creation, target the root group identified by the snapshot or inspection.",
338
+ "Use update-item-translation with itemId, locale, contentKey, and plainText for ordinary respondent-visible text. For a complete formatted replacement, prefer the richTextBlocks shorthand from rich-text-content; canonical content remains available when needed. Item title, subtitle, topContent, bottomContent, cardFooter, info content, option labels, and field labels are translations, not item labels.",
296
339
  "Use add-survey-locale only when the user explicitly requests an empty language registration. An ordinary localization request requires real translation operations, which introduce the locale themselves.",
297
340
  "Use remove-survey-locale with locale to destructively remove an existing survey language and all translations stored for it.",
298
341
  "Each call adds a delta to this turn's validated draft. Later calls may target ids created by earlier successful calls in the same turn.",
299
- "For an existing choice item, use add-choice-option with itemId, optional locale/index, and option: { id?, key?, label }; label is a plain string and creates its translation.",
300
- "Every reorder replaces the complete inspected list: reorder-choice-options uses optionIds, reorder-form-field-groups uses groupIds, and reorder-items uses itemIds for every direct child. Include every existing entry exactly once in desired order.",
342
+ "Use operations: [] only with retryOfChangeIds when pendingRepairs.allowsAcknowledgment is true, after inspecting the validator-confirmed no-op. This does not discharge identifiable pending edits or create an editor commit by itself.",
343
+ "For an existing choice item, use add-choice-option with itemId, optional locale/index, and option: { id?, code?, label }; label is a plain string and creates its translation.",
344
+ "Every reorder replaces the complete inspected list: reorder-choice-options uses optionIds, reorder-form-field-groups uses groupIds, and reorder-items uses top-level parentItemId (not target.parentItemId) and itemIds for every direct child. Include every existing entry exactly once in desired order.",
301
345
  `Use update-item-color only with a supported swatch. ${`Allowed editor item-color swatches: ${SURVEY_EDITOR_ITEM_COLORS.join(", ")}. Use one of these exact values for update-item-color; do not wait for validation feedback to choose a swatch.`}`,
302
- "Use patch-item-config with itemId and plural patches whose JSON Pointer paths are relative to that item config root.",
303
- "Use update-placed-image only for an image placement that already exists in the live survey or validated turn draft.",
346
+ "Use update-response-settings with naming and settings arrays for variable names, matrix coding components, option export codes and export formats. Load response-variables for the shared coding contract.",
347
+ "Use patch-item-config with itemId and plural patches whose JSON Pointer paths are relative to that item config root. Include related translations inside the operation. Load the focused item capability reference for its configuration and content keys.",
348
+ "Use update-placed-image only for an image already embedded in rich text in the live survey or validated turn draft. It can switch the placement between a local asset and an external URL and update its rendered ALT text, caption, width, height, maximum dimensions, and alignment. For a new item, prefer putting the canonical image block inside create-item.translations[].richTextBlocks. Do not use image asset metadata when the user asked to change one placed image.",
304
349
  "When deleting referenced images and assets, include every reference-removal operation and remove-image-asset last in the same operations array.",
305
- "Raw fallback is one { kind: \"survey-json-patch\", patches } operation containing all related survey-rooted edits; it must be the only operation in the call.",
350
+ "A survey-json-patch proposal contains exactly one operation with kind survey-json-patch and one plural patches array containing every related raw edit. It must be the only operation in that survey_change call. Paths are rooted at the survey document, such as /surveyItems/<index>/config or /translations/<locale>; never start at /config or modify /$schema. Patch translation leaves rather than replacing existing locale or item-translation containers.",
306
351
  "Keep each delta bounded; this is a per-call constraint, not a reason to end the turn. Successful chunks accumulate automatically, so substantial requests should continue with later deltas while capacity remains."
307
352
  ].join(" ");
308
353
  const inputSchema$9 = z.object({
354
+ retryOfChangeIds: retryOfChangeIdsSchema,
309
355
  summary: z.string().min(1).max(500).describe("Short user-facing summary of the intended survey change."),
310
- operations: z.array(z.unknown()).min(1).max(200).describe(OPERATIONS_DESCRIPTION)
356
+ operations: z.array(z.unknown()).min(0).max(200).describe(OPERATIONS_DESCRIPTION)
311
357
  }).strict();
312
358
  const rejectedOperationParseEvent = ({ issueText, summary, turnId }) => ({
313
359
  protocolVersion: 1,
@@ -334,7 +380,6 @@ const describeOperationTarget = (operation) => {
334
380
  case "update-placed-image": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}#${operation.imageIndex}`;
335
381
  case "update-item-label":
336
382
  case "update-item-color":
337
- case "update-item-key":
338
383
  case "patch-item-config":
339
384
  case "remove-item":
340
385
  case "move-item": return `${operation.kind} ${operation.itemId}`;
@@ -346,11 +391,12 @@ const describeOperationTarget = (operation) => {
346
391
  case "move-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.fieldId}`;
347
392
  case "set-embedded-form":
348
393
  case "remove-embedded-form": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
349
- case "add-choice-option": return `${operation.kind} ${operation.itemId}:${operation.option.id ?? operation.option.key ?? operation.option.label}`;
394
+ case "add-choice-option": return `${operation.kind} ${operation.itemId}:${operation.option.id ?? operation.option.code ?? operation.option.label}`;
350
395
  case "update-choice-option-label":
351
396
  case "remove-choice-option": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
352
397
  case "reorder-choice-options": return `${operation.kind} ${operation.itemId}`;
353
398
  case "reorder-items": return `${operation.kind} ${operation.parentItemId}`;
399
+ case "update-response-settings":
354
400
  case "update-survey-metadata":
355
401
  case "update-survey-pagination": return operation.kind;
356
402
  case "update-survey-translation": return `${operation.kind} ${operation.locale}:${operation.surface}:${operation.contentKey}`;
@@ -359,7 +405,7 @@ const describeOperationTarget = (operation) => {
359
405
  case "set-image-asset":
360
406
  case "update-image-asset-metadata":
361
407
  case "remove-image-asset": return `${operation.kind} ${operation.assetId}`;
362
- case "create-item": return `${operation.kind} ${operation.item.id ?? operation.item.key ?? operation.item.itemType}`;
408
+ case "create-item": return `${operation.kind} ${operation.item.id ?? operation.item.metadata?.itemLabel ?? operation.item.id ?? operation.item.itemType}`;
363
409
  case "survey-json-patch": return `${operation.kind} ${operation.patches.map((patch) => patch.path).join(", ")}`;
364
410
  }
365
411
  };
@@ -377,11 +423,19 @@ const summarizeValidation = (event) => {
377
423
  lines.push(`Draft updated (${event.operations.length} total operation${event.operations.length === 1 ? "" : "s"}). Later tools can inspect and edit the resulting draft state. The live editor is unchanged until successful turn completion.`);
378
424
  if (event.postChange) {
379
425
  const unavailableItemCount = event.postChange.touchedItemCount - event.postChange.availableTouchedItemCount;
380
- lines.push(`Canonical post-change preview (first occurrence in operation order; projected fields only): ${event.postChange.itemCount} survey item(s); locales ${event.postChange.locales.length}/${event.postChange.localeCount} shown; targets ${event.postChange.touchedTargets.length}/${event.postChange.targetCount} shown; available touched-item snapshots ${event.postChange.touchedItems.length}/${event.postChange.availableTouchedItemCount} shown${unavailableItemCount > 0 ? `; ${unavailableItemCount} touched item target(s) are absent after the change` : ""}.`);
426
+ lines.push(`Canonical post-change preview (mutation targets first, then affected parents; projected fields only): ${event.postChange.itemCount} survey item(s); locales ${event.postChange.locales.length}/${event.postChange.localeCount} shown; targets ${event.postChange.touchedTargets.length}/${event.postChange.targetCount} shown; available touched-item snapshots ${event.postChange.touchedItems.length}/${event.postChange.availableTouchedItemCount} shown${unavailableItemCount > 0 ? `; ${unavailableItemCount} touched item target(s) are absent after the change` : ""}.`);
381
427
  lines.push(`Target preview: ${event.postChange.touchedTargets.join(", ") || "none"}. Absence from this bounded projection does not prove that a field or target is absent from the canonical draft.`);
382
428
  for (const item of event.postChange.touchedItems) {
383
429
  const translations = item.translations.map(({ contentKey, locale, textLength, textPreview, truncated }) => truncated ? `${locale}:${contentKey}=${JSON.stringify(`${textPreview}…`)} [truncated preview; ${textLength} characters total]` : `${locale}:${contentKey}=${JSON.stringify(textPreview)}`).join("; ");
384
- lines.push(`- ${item.fullKey} (${item.itemType}, id ${item.itemId}); response slots ${item.responseSlots.length}/${item.responseSlotCount}: ${item.responseSlots.join(", ") || "none"}; translations ${item.translations.length}/${item.translationCount}: ${translations || "none"}`);
430
+ lines.push(`- ${item.breadcrumb} (${item.itemType}, id ${item.itemId}); response slots ${item.responseSlots.length}/${item.responseSlotCount}: ${item.responseSlots.join(", ") || "none"}; translations ${item.translations.length}/${item.translationCount}: ${translations || "none"}`);
431
+ if (item.placement) {
432
+ const p = item.placement;
433
+ lines.push(` Stored placement: parent ${p.parentItemId}; index ${p.index} (zero-based), ${p.index + 1}/${p.siblingCount}; previous ${p.previousItemId ?? "none"}; next ${p.nextItemId ?? "none"}.${p.shuffled ? " Parent shuffles children; respondent order may differ." : ""}`);
434
+ }
435
+ if (item.childOrder) {
436
+ const order = item.childOrder;
437
+ lines.push(` Stored child order (${order.itemIds.length}/${order.total}${order.truncated ? "; truncated" : ""}): ${JSON.stringify(order.itemIds)}.${order.shuffled ? " Group shuffles children; respondent order may differ." : ""}`);
438
+ }
385
439
  }
386
440
  if (event.postChange.truncated) lines.push("The post-change preview is truncated. The validated draft still contains the complete change; inspect any relevant omitted target or field before claiming its exact post-change value.");
387
441
  }
@@ -413,10 +467,11 @@ const summarizeValidation = (event) => {
413
467
  lines.push("Warnings:");
414
468
  for (const warning of event.validation.warnings) lines.push(`- ${warning.code}: ${warning.message}`);
415
469
  }
470
+ lines.push(...summarizePendingRepairs(event));
416
471
  return lines.join("\n");
417
472
  };
418
473
  const surveyChangeTool = createTool({
419
- id: TOOL_ID$9,
474
+ id: TOOL_ID$5,
420
475
  description: "Add one bounded typed-operation delta to the request-scoped survey draft. Successful calls are immediately visible to every later tool in this turn. Rejected calls leave the draft unchanged; retry the failed target. The live editor changes once after successful completion. Every operation uses \"kind\".",
421
476
  inputSchema: inputSchema$9,
422
477
  outputSchema: surveyChangeSetSchema,
@@ -431,13 +486,20 @@ const surveyChangeTool = createTool({
431
486
  const { turnDraft, turnId } = requireSurveyAssistantTurnState(context?.requestContext);
432
487
  const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
433
488
  const defaultLocale = snapshot?.ui.selectedLocale ?? snapshot?.inspection.locales[0] ?? "en";
434
- const operationsParse = z.array(surveyOperationSchema).safeParse(normalizeSurveyChangeOperations(inputData.operations, defaultLocale));
489
+ const normalized = normalizeSurveyChangeOperations(inputData.operations, defaultLocale);
490
+ if (normalized.issues.length > 0) return turnDraft.reject(rejectedOperationParseEvent({
491
+ summary: inputData.summary,
492
+ turnId,
493
+ issueText: normalized.issues.slice(0, 8).map((issue) => `${issue.path}: ${issue.message}`).join("; ")
494
+ }), collectMalformedSurveyOperationRecoveryTargets(normalized.operations, snapshot, normalized.issues));
495
+ const operationsParse = z.array(surveyOperationSchema).safeParse(normalized.operations);
435
496
  if (!operationsParse.success) return turnDraft.reject(rejectedOperationParseEvent({
436
497
  summary: inputData.summary,
437
498
  turnId,
438
499
  issueText: operationsParse.error.issues.slice(0, 8).map((issue) => `${issue.path.join(".") || "operation"}: ${issue.message}`).join("; ")
439
- }));
500
+ }), collectMalformedSurveyOperationRecoveryTargets(normalized.operations, snapshot, normalized.issues));
440
501
  return turnDraft.stage({
502
+ retryOfChangeIds: inputData.retryOfChangeIds,
441
503
  operations: operationsParse.data,
442
504
  snapshot,
443
505
  summary: inputData.summary
@@ -447,28 +509,39 @@ const surveyChangeTool = createTool({
447
509
  });
448
510
  //#endregion
449
511
  //#region src/server/tools/survey-expression-tool.ts
450
- const TOOL_ID$8 = "survey_expression";
451
- const contextActionSchema = z.object({
452
- action: z.literal("context"),
453
- reason: z.string().max(500).optional(),
454
- itemIds: z.array(z.string().min(1).max(240)).max(12).optional(),
455
- includeRawLocations: z.boolean().optional()
456
- });
512
+ const TOOL_ID$4 = "survey_expression";
513
+ const MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL = 40;
514
+ const expressionMutationListSchema = z.array(expressionMutationSchema).min(1).max(MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL);
457
515
  const prepareActionSchema = z.object({
458
516
  action: z.literal("prepare"),
459
517
  summary: z.string().min(1).max(500),
460
- mutations: z.array(expressionMutationSchema).min(1).max(40)
518
+ mutations: expressionMutationListSchema
461
519
  });
462
520
  const inputSchema$8 = z.object({
463
- action: z.enum(["context", "prepare"]).describe("Choose exactly one envelope. Inspect with { \"action\": \"context\", \"itemIds\"?: [...], \"includeRawLocations\"?: true, \"reason\"?: \"...\" }; omit summary and mutations. Create edits with { \"action\": \"prepare\", \"summary\": \"...\", \"mutations\": [...] }; omit reason, itemIds, and includeRawLocations."),
464
- reason: z.string().max(500).optional(),
465
- itemIds: z.array(z.string().min(1).max(240)).max(12).optional().describe("Context action only: optional item ids/keys to focus existing expression locations. Omit for prepare."),
466
- includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item. Omit for prepare."),
467
- summary: z.string().min(1).max(500).optional().describe("Prepare action only: required user-facing summary. Omit for context."),
468
- mutations: z.array(z.record(z.string(), z.unknown())).min(1).max(40).optional().describe("Prepare action only: required expression mutation objects using kebab-case kind names; omit this field for context. Item visibility shape: { kind: \"set-item-display-condition\", itemId: \"<existing-item-id>\", expression: { type: \"function\", functionName: \"eq\", arguments: [...] } }. expression is always CASE expression JSON, never a string or expression-language snippet. Other supported kinds set/clear component display/disabled conditions, validation expressions, prefills, and template values.")
521
+ retryOfChangeIds: retryOfChangeIdsSchema,
522
+ action: z.enum(["context", "prepare"]).describe("Choose exactly one envelope. Inspect with action=\"context\" and optional reason, itemIds, or includeRawLocations. Create edits with action=\"prepare\", summary, and mutations."),
523
+ reason: z.string().max(500).optional().describe("Context action only: optional inspection reason."),
524
+ itemIds: z.array(z.string().min(1).max(240)).max(12).optional().describe("Context action only: filter response slots and expression locations by exact item IDs. Omit to inspect survey-wide references, including controlling questions outside the conditioned item."),
525
+ cursor: z.string().regex(/^\d+$/).max(12).optional().describe("Context action only: nextCursor from the preceding page, with the same itemIds and includeRawLocations. Restart after a draft mutation."),
526
+ limit: z.number().int().min(1).max(80).optional().describe("Context action only: rows per catalog page; defaults to 80."),
527
+ includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item."),
528
+ summary: z.string().max(500).optional().describe("Prepare action only: required user-facing summary."),
529
+ mutations: z.array(z.unknown()).min(1).max(MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL).optional().describe(`Prepare action only: an array of 1-${MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL} canonical expression mutation objects from the loaded expressions reference. Example entry: {"kind":"set-item-display-condition","itemId":"<existing-item-id>","expression":{"type":"function","functionName":"eq","arguments":[...]}}. expression is CASE expression JSON, never an expression-language snippet.`)
469
530
  }).strict().superRefine((value, ctx) => {
470
- if (value.action !== "prepare") return;
471
- if (!value.summary) ctx.addIssue({
531
+ if (value.action === "context") {
532
+ if (value.summary?.trim()) ctx.addIssue({
533
+ code: z.ZodIssueCode.custom,
534
+ path: ["summary"],
535
+ message: "Omit summary for context; use action=prepare to stage mutations."
536
+ });
537
+ if (value.mutations !== void 0) ctx.addIssue({
538
+ code: z.ZodIssueCode.custom,
539
+ path: ["mutations"],
540
+ message: "Omit mutations for context; use action=prepare to stage mutations."
541
+ });
542
+ return;
543
+ }
544
+ if (!value.summary?.trim()) ctx.addIssue({
472
545
  code: z.ZodIssueCode.custom,
473
546
  path: ["summary"],
474
547
  message: "Required for prepare."
@@ -476,7 +549,7 @@ const inputSchema$8 = z.object({
476
549
  if (!value.mutations?.length) ctx.addIssue({
477
550
  code: z.ZodIssueCode.custom,
478
551
  path: ["mutations"],
479
- message: "At least one mutation is required for prepare."
552
+ message: "A non-empty mutation array is required for prepare."
480
553
  });
481
554
  });
482
555
  const contextOutputSchema = z.object({
@@ -484,13 +557,23 @@ const contextOutputSchema = z.object({
484
557
  action: z.literal("context"),
485
558
  modelSummary: z.string(),
486
559
  listOrdering: z.record(z.string(), z.string()).describe("Meaning of each returned list. Expression references are not positional."),
560
+ nextCursor: z.string().optional(),
561
+ totalResponseSlots: z.number().int().nonnegative().optional(),
562
+ surveyResponseSlotCount: z.number().int().nonnegative().optional(),
563
+ totalExpressionLocations: z.number().int().nonnegative().optional(),
564
+ unresolvedItemIds: z.array(z.string()).optional(),
487
565
  responseSlots: z.array(z.object({
488
- ref: z.string(),
489
- valueType: z.string().optional()
566
+ ref: z.object({
567
+ slotId: z.string(),
568
+ method: z.enum(["get", "isDefined"])
569
+ }),
570
+ valueType: z.string().optional(),
571
+ variableName: z.string().optional(),
572
+ itemId: z.string().optional()
490
573
  })),
491
574
  expressionLocations: z.array(z.object({
492
575
  itemId: z.string(),
493
- key: z.string(),
576
+ editorName: z.string(),
494
577
  itemType: z.string(),
495
578
  rawIndex: z.number().int().nonnegative(),
496
579
  locations: z.array(z.string())
@@ -501,22 +584,6 @@ const createIssue = ({ severity = "error", ...input }) => ({
501
584
  severity,
502
585
  ...input
503
586
  });
504
- const expressionMutationTarget = (mutation) => {
505
- switch (mutation.kind) {
506
- case "set-item-display-condition":
507
- case "clear-item-display-condition": return `expression:item:${mutation.itemId}:display`;
508
- case "set-component-display-condition":
509
- case "clear-component-display-condition": return `expression:item:${mutation.itemId}:component:${mutation.componentId}:display`;
510
- case "set-component-disabled-condition":
511
- case "clear-component-disabled-condition": return `expression:item:${mutation.itemId}:component:${mutation.componentId}:disabled`;
512
- case "set-validation-expression":
513
- case "clear-validation-expression": return `expression:item:${mutation.itemId}:validation:${mutation.validationKey}`;
514
- case "set-prefill": return `expression:item:${mutation.itemId}:prefill:${mutation.prefill.id}`;
515
- case "clear-prefill": return `expression:item:${mutation.itemId}:prefill:${mutation.prefillId}`;
516
- case "set-template-value":
517
- case "clear-template-value": return `expression:template:${mutation.templateKey}`;
518
- }
519
- };
520
587
  const buildResponseSlotTypes = (snapshot) => snapshot.responseSlotTypes;
521
588
  const rejectedEvent = ({ issues, summary, turnId, warnings = [] }) => ({
522
589
  protocolVersion: 1,
@@ -545,18 +612,37 @@ const summarizeDraftEvent = (event) => {
545
612
  lines.push("Warnings:");
546
613
  for (const warning of event.validation.warnings) lines.push(`- ${warning.code}: ${warning.message}`);
547
614
  }
615
+ lines.push(...summarizePendingRepairs(event));
548
616
  return lines.join("\n");
549
617
  };
550
618
  const buildContextOutput = (snapshot, input) => {
551
619
  const rawItems = getRawSurveyItems(snapshot);
552
- const responseSlotTypes = buildResponseSlotTypes(snapshot);
553
- const responseSlots = Object.entries(responseSlotTypes).map(([ref, valueType]) => ({
554
- ref,
555
- ...valueType ? { valueType } : {}
620
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
621
+ const resolvedItems = input.itemIds?.map((identifier) => ({
622
+ identifier,
623
+ itemId: snapshot.inspection.itemIndex.find((entry) => entry.itemId === identifier || entry.editorName === identifier || entry.breadcrumb === identifier)?.itemId
556
624
  }));
557
- const itemFilter = input.action === "context" && input.itemIds?.length ? new Set(input.itemIds) : void 0;
558
- const expressionLocations = input.action === "context" && input.includeRawLocations === true ? rawItems.map((item, rawIndex) => {
559
- if (itemFilter && !itemFilter.has(item.id) && !itemFilter.has(item.key)) return;
625
+ const itemFilter = resolvedItems?.length ? new Set(resolvedItems.flatMap((entry) => entry.itemId ? [entry.itemId] : [])) : void 0;
626
+ const unresolvedItemIds = resolvedItems?.filter((entry) => !entry.itemId).map((entry) => entry.identifier) ?? [];
627
+ const surveyResponseSlots = Object.entries(buildResponseSlotTypes(snapshot));
628
+ const allResponseSlots = surveyResponseSlots.filter(([ref]) => !itemFilter || snapshot.responseVariables.some((slot) => itemFilter.has(slot.itemId) && JSON.parse(ref)[0] === slot.slotId)).map(([key, valueType]) => {
629
+ const [slotId, method] = JSON.parse(key);
630
+ const slot = snapshot.responseVariables.find((slot) => slot.slotId === slotId);
631
+ return {
632
+ ref: {
633
+ slotId,
634
+ method
635
+ },
636
+ valueType,
637
+ variableName: slot?.variableName,
638
+ itemId: slot?.itemId
639
+ };
640
+ });
641
+ const offset = Number(input.cursor ?? 0);
642
+ const limit = input.limit ?? 80;
643
+ const responseSlots = allResponseSlots.slice(offset, offset + limit);
644
+ const allExpressionLocations = input.action === "context" && input.includeRawLocations === true ? rawItems.map((item, rawIndex) => {
645
+ if (itemFilter && !itemFilter.has(item.id)) return;
560
646
  const locations = [
561
647
  item.displayConditions?.root ? "displayConditions.root" : void 0,
562
648
  ...Object.keys(item.displayConditions?.components ?? {}).map((componentId) => `displayConditions.components.${componentId}`),
@@ -566,12 +652,15 @@ const buildContextOutput = (snapshot, input) => {
566
652
  ].filter((location) => Boolean(location));
567
653
  return {
568
654
  itemId: item.id,
569
- key: item.key,
655
+ editorName: names.get(item.id) ?? item.id,
570
656
  itemType: item.itemType,
571
657
  rawIndex,
572
658
  locations
573
659
  };
574
660
  }).filter((entry) => Boolean(entry)) : void 0;
661
+ const expressionLocations = allExpressionLocations?.slice(offset, offset + limit);
662
+ const totalAvailable = Math.max(allResponseSlots.length, allExpressionLocations?.length ?? 0);
663
+ const nextCursor = offset + limit < totalAvailable ? String(offset + limit) : void 0;
575
664
  return {
576
665
  ok: true,
577
666
  action: "context",
@@ -580,13 +669,23 @@ const buildContextOutput = (snapshot, input) => {
580
669
  "Use prepare mutations for display conditions, disabled conditions, validations, prefills, and template values.",
581
670
  "Display, disabled, validation, and prefill.when expressions must return boolean.",
582
671
  "Use response refs exactly as listed and check their value types before choosing functions.",
672
+ "An isDefined ref means only that an active answer exists; it never means a particular answer such as Yes, selected, eligible, or has children.",
673
+ "When the condition names or implies a choice, inspect the source choice item for the exact option id, then compare its get ref with that reference using eq or list_contains.",
674
+ "Visibility must be acyclic, including item, ancestor, component and computed-response dependencies. Prefills do not permit cycles. Use an independent controlling answer or serialized context value. Hidden answers are retained but inactive: get is undefined and isDefined is false. Undefined/nonboolean visibility hides its target; customExpression callbacks are not supported in visibility.",
583
675
  "For choice options: use eq(response reference, reference const) when the slot type is reference; use list_contains(response reference[], reference const) only when the slot type is reference[].",
584
676
  "Response-slot order: set-like deterministic snapshot construction order; it has no survey or respondent-visible meaning.",
585
- `Response slots (${responseSlots.length}): ${responseSlots.slice(0, 80).map((slot) => `${slot.ref}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
586
- responseSlots.length > 80 ? "Response slot list truncated in summary; inspect context result for more." : void 0,
677
+ itemFilter ? `Response-slot scope: filtered to itemIds=${JSON.stringify(input.itemIds)}; ${allResponseSlots.length} matching of ${surveyResponseSlots.length} survey-wide slots. References outside this filter are omitted, not absent. To find controlling questions elsewhere, call survey_expression action=context without itemIds or cursor.` : `Response-slot scope: survey-wide (${surveyResponseSlots.length} slots).`,
678
+ `Response slots (${responseSlots.length}/${allResponseSlots.length}, offset ${offset}): ${responseSlots.map((slot) => `${JSON.stringify(slot.ref)}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
679
+ nextCursor ? `More expression context is available. Call survey_expression action=context with cursor="${nextCursor}" and the same itemIds, includeRawLocations, and limit. Restart without a cursor after a draft mutation.` : "Expression context pagination complete for this scope.",
680
+ unresolvedItemIds.length > 0 ? `Unresolved item identifiers: ${JSON.stringify(unresolvedItemIds)}. Inspect current context or search-items to resolve them; no references for these targets are implied.` : void 0,
587
681
  "Functions: and, or, not, eq, gt, gte, lt, lte, in_range, list_contains, sum, min, max, str_eq, date_eq.",
588
- expressionLocations ? `Existing expression locations: ${expressionLocations.filter((entry) => entry.locations.length > 0).map((entry) => `${entry.key}(${entry.itemId}) -> ${entry.locations.join("|")}`).join("; ") || "none"}` : void 0
682
+ expressionLocations ? `Existing expression locations: ${expressionLocations.filter((entry) => entry.locations.length > 0).map((entry) => `${entry.editorName}(${entry.itemId}) -> ${entry.locations.join("|")}`).join("; ") || "none"}` : void 0
589
683
  ].filter((line) => Boolean(line)).join("\n"),
684
+ totalResponseSlots: allResponseSlots.length,
685
+ surveyResponseSlotCount: surveyResponseSlots.length,
686
+ ...allExpressionLocations ? { totalExpressionLocations: allExpressionLocations.length } : {},
687
+ ...nextCursor ? { nextCursor } : {},
688
+ ...unresolvedItemIds.length ? { unresolvedItemIds } : {},
590
689
  listOrdering: {
591
690
  responseSlots: "Set-like deterministic snapshot construction order; position has no survey or respondent-visible meaning.",
592
691
  expressionLocations: "Stored surveyItems array order; locations within an item are grouped by expression surface, not ranked or respondent-visible order.",
@@ -597,7 +696,7 @@ const buildContextOutput = (snapshot, input) => {
597
696
  };
598
697
  };
599
698
  const surveyExpressionTool = createTool({
600
- id: TOOL_ID$8,
699
+ id: TOOL_ID$4,
601
700
  description: "Inspect response refs or add general expression edits to the request-scoped survey draft. Successful mutation calls are immediately visible to later tools; rejected calls leave the draft unchanged. Covers display and disabled conditions, validations, prefills, and template values.",
602
701
  inputSchema: inputSchema$8,
603
702
  outputSchema: outputSchema$7,
@@ -630,13 +729,22 @@ const surveyExpressionTool = createTool({
630
729
  })]
631
730
  }));
632
731
  }
633
- if (input.action === "context") {
634
- const contextParse = contextActionSchema.safeParse(inputData);
635
- return buildContextOutput(snapshot, contextParse.success ? contextParse.data : input);
636
- }
732
+ if (input.action === "context") return buildContextOutput(snapshot, input);
637
733
  const { turnDraft, turnId } = requireSurveyAssistantTurnState(requestContext);
638
- const reject = (event, addressedTargets = []) => turnDraft.reject(event, addressedTargets);
639
- const prepareParse = prepareActionSchema.safeParse(inputData);
734
+ const reject = (event, addressedTargets = [], refinements = []) => turnDraft.reject(event, addressedTargets, refinements);
735
+ if (!Array.isArray(input.mutations)) return reject(rejectedEvent({
736
+ summary: input.summary ?? "Expression change",
737
+ turnId,
738
+ issues: [createIssue({
739
+ code: "invalid-expression-mutation-shape",
740
+ message: "mutations is required for prepare and must be a canonical mutation array."
741
+ })]
742
+ }));
743
+ const prepareParse = prepareActionSchema.safeParse({
744
+ action: input.action,
745
+ summary: input.summary,
746
+ mutations: input.mutations
747
+ });
640
748
  if (!prepareParse.success) return reject(rejectedEvent({
641
749
  summary: input.summary ?? "Expression change",
642
750
  turnId,
@@ -644,22 +752,10 @@ const surveyExpressionTool = createTool({
644
752
  code: "invalid-expression-mutation-shape",
645
753
  message: prepareParse.error.issues.slice(0, 8).map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ")
646
754
  })]
647
- }));
755
+ }), collectExpressionMutationRecoveryTargets(input.mutations, snapshot));
648
756
  const prepareInput = prepareParse.data;
649
- const addressedTargets = [...new Set(prepareInput.mutations.map(expressionMutationTarget))];
650
- const issues = [];
651
- const patches = [];
652
- const responseSlotTypes = buildResponseSlotTypes(snapshot);
653
- prepareInput.mutations.forEach((mutation, operationIndex) => {
654
- buildExpressionMutationPatches({
655
- mutation,
656
- operationIndex,
657
- issues,
658
- patches,
659
- responseSlotTypes,
660
- snapshot
661
- });
662
- });
757
+ const addressedTargets = collectExpressionMutationRecoveryTargets(prepareInput.mutations, snapshot);
758
+ const { issues, patches } = buildExpressionMutationBatch(prepareInput.mutations, snapshot);
663
759
  const errors = issues.filter((issue) => issue.severity === "error");
664
760
  const warnings = issues.filter((issue) => issue.severity === "warning");
665
761
  if (errors.length > 0) return reject(rejectedEvent({
@@ -667,21 +763,26 @@ const surveyExpressionTool = createTool({
667
763
  turnId,
668
764
  issues: errors,
669
765
  warnings
670
- }), addressedTargets);
671
- if (patches.length === 0) return reject(rejectedEvent({
672
- summary: prepareInput.summary,
673
- turnId,
674
- issues: [createIssue({
766
+ }), collectExpressionMutationRecoveryTargets(prepareInput.mutations, snapshot, errors), collectExpressionMutationRecoveryRefinements(prepareInput.mutations, snapshot, errors));
767
+ if (patches.length === 0) {
768
+ const noPatchIssues = prepareInput.mutations.map((_, operationIndex) => createIssue({
675
769
  code: "no-expression-patches",
676
- message: "No expression patches were produced. Check whether the requested locations already match the target state."
677
- })],
678
- warnings
679
- }), addressedTargets);
770
+ operationIndex,
771
+ message: "No expression patches were produced. Inspect whether the requested locations already match the target state; acknowledge already-satisfied work through a linked survey_change with operations: []."
772
+ }));
773
+ return reject(rejectedEvent({
774
+ summary: prepareInput.summary,
775
+ turnId,
776
+ issues: noPatchIssues,
777
+ warnings
778
+ }), collectExpressionMutationRecoveryTargets(prepareInput.mutations, snapshot, noPatchIssues), collectExpressionMutationRecoveryRefinements(prepareInput.mutations, snapshot, noPatchIssues));
779
+ }
680
780
  const operation = {
681
781
  kind: "survey-json-patch",
682
782
  patches
683
783
  };
684
784
  const event = turnDraft.stage({
785
+ retryOfChangeIds: input.retryOfChangeIds,
685
786
  addressedTargets,
686
787
  allowExpressionJsonPatches: true,
687
788
  handledMutationRoutes: ["expression"],
@@ -700,8 +801,77 @@ const surveyExpressionTool = createTool({
700
801
  toModelOutput: (value) => toTextModelOutput(value, (output) => "action" in output && output.action === "context" ? output.modelSummary : summarizeDraftEvent(output))
701
802
  });
702
803
  //#endregion
703
- //#region src/server/tools/survey-get-current-context-tool.ts
704
- const TOOL_ID$7 = "survey_get_current_context";
804
+ //#region src/server/tools/parse-inspection-output.ts
805
+ const parseInspectionOutput = (schema, value, backend) => {
806
+ const parsed = schema.safeParse(value);
807
+ if (!parsed.success) throw new Error(`${backend} produced invalid inspection output.`, { cause: parsed.error });
808
+ return parsed.data;
809
+ };
810
+ //#endregion
811
+ //#region src/server/tools/response-variable-summary.ts
812
+ const MAX_COMPONENTS = 50;
813
+ const responseVariableSummarySchema = z.object({
814
+ slotId: z.string(),
815
+ itemId: z.string(),
816
+ variableName: z.string(),
817
+ valueType: z.string(),
818
+ primitiveId: z.string().optional(),
819
+ rowId: z.string().optional(),
820
+ columnId: z.string().optional(),
821
+ namingKind: z.enum(["variable", "matrix"]).optional(),
822
+ computed: z.object({
823
+ operation: z.string(),
824
+ sourceSlotIds: z.array(z.string())
825
+ }).optional(),
826
+ computedSourcesTruncated: z.boolean(),
827
+ referenceValueKeys: z.record(z.string(), z.string()).optional(),
828
+ referenceValueScores: z.record(z.string(), z.number()).optional(),
829
+ optionIds: z.array(z.string()),
830
+ optionsTotal: z.number(),
831
+ optionsTruncated: z.boolean()
832
+ });
833
+ const responseOptionIds = (slot) => [.../* @__PURE__ */ new Set([
834
+ ...Object.keys(slot.referenceValueKeys ?? {}),
835
+ ...Object.keys(slot.referenceValueScores ?? {}),
836
+ ...(slot.allowedValues ?? []).flatMap((value) => value.type === "reference" ? [value.value] : value.type === "reference[]" ? value.value : [])
837
+ ])];
838
+ /** Coding discovery excludes config paths and duplicated translations for every survey locale. */
839
+ const summarizeResponseVariable = (slot) => {
840
+ const optionIds = responseOptionIds(slot);
841
+ const take = (values) => values && Object.fromEntries(optionIds.slice(0, MAX_COMPONENTS).filter((id) => Object.hasOwn(values, id)).map((id) => [id, values[id]]));
842
+ return {
843
+ slotId: slot.slotId,
844
+ itemId: slot.itemId,
845
+ variableName: slot.variableName,
846
+ valueType: slot.valueType,
847
+ primitiveId: slot.primitiveId,
848
+ rowId: slot.rowId,
849
+ columnId: slot.columnId,
850
+ namingKind: slot.naming?.kind,
851
+ computed: slot.computed && {
852
+ ...slot.computed,
853
+ sourceSlotIds: slot.computed.sourceSlotIds.slice(0, MAX_COMPONENTS)
854
+ },
855
+ computedSourcesTruncated: (slot.computed?.sourceSlotIds.length ?? 0) > MAX_COMPONENTS,
856
+ referenceValueKeys: take(slot.referenceValueKeys),
857
+ referenceValueScores: take(slot.referenceValueScores),
858
+ optionIds: optionIds.slice(0, MAX_COMPONENTS),
859
+ optionsTotal: optionIds.length,
860
+ optionsTruncated: optionIds.length > MAX_COMPONENTS
861
+ };
862
+ };
863
+ const responseVariableMatchesQuery = (slot, query) => [
864
+ slot.slotId,
865
+ slot.itemId,
866
+ slot.variableName,
867
+ slot.primitiveId,
868
+ slot.rowId,
869
+ slot.columnId,
870
+ ...responseOptionIds(slot),
871
+ ...Object.values(slot.referenceValueKeys ?? {})
872
+ ].some((value) => value?.toLowerCase().includes(query));
873
+ //#endregion
874
+ //#region src/server/tools/inspect-current-context.ts
705
875
  const contextScopeSchema = z.enum([
706
876
  "summary",
707
877
  "selected-item",
@@ -723,9 +893,9 @@ const translationSnippetSchema = z.object({
723
893
  const inputSchema$7 = z.object({
724
894
  scopes: z.array(contextScopeSchema).min(1).default(["summary"]).describe("Which current editor context sections to inspect."),
725
895
  itemId: z.string().optional().describe("Optional item id to inspect. Defaults to the selected/focused item."),
726
- query: z.string().max(160).optional().describe("Optional text used to find matching item ids, keys, labels, or response slots."),
896
+ query: z.string().max(180).optional().describe("Optional text used to find matching item IDs, editor names, labels, or response variables."),
727
897
  cursor: z.string().optional().describe("Optional numeric cursor returned by a previous outline or response slot request."),
728
- outlineMode: outlineModeSchema$1.default("full").optional().describe("For outline scope: \"full\" pages the complete survey key index, \"focused\" returns the selected/focused neighborhood, and \"children\" returns direct children of itemId."),
898
+ outlineMode: outlineModeSchema$1.default("full").optional().describe("For outline scope: \"full\" pages the complete survey item index, \"focused\" returns the selected/focused neighborhood, and \"children\" returns direct children of itemId."),
729
899
  limit: z.number().int().min(1).max(50).default(12).describe("Maximum number of rows to return for list-like scopes.")
730
900
  });
731
901
  const outputSchema$6 = z.object({
@@ -739,6 +909,7 @@ const outputSchema$6 = z.object({
739
909
  sessionId: z.string().nullable(),
740
910
  view: z.string(),
741
911
  selectedItemId: z.string().optional(),
912
+ responseSlotId: z.string().optional(),
742
913
  focusItemId: z.string().optional(),
743
914
  selectedLocale: z.string(),
744
915
  recentItemIds: z.array(z.string()),
@@ -788,7 +959,7 @@ const outputSchema$6 = z.object({
788
959
  nextCursor: z.string().optional(),
789
960
  truncated: z.boolean(),
790
961
  ordering: z.string(),
791
- slots: z.array(z.string()),
962
+ slots: z.array(responseVariableSummarySchema),
792
963
  note: z.string().optional()
793
964
  }).optional(),
794
965
  uiContext: z.object({
@@ -842,13 +1013,14 @@ const flattenFocusDescendants = (node) => {
842
1013
  };
843
1014
  const getCandidateItems = (snapshot) => {
844
1015
  const outlineById = mapOutlineById(snapshot);
845
- return summarizeKeyIndexEntries(getKeyIndexEntries(snapshot), outlineById);
1016
+ return summarizeItemIndexEntries(getItemIndexEntries(snapshot), outlineById);
846
1017
  };
847
1018
  const buildSummary = (snapshot) => ({
848
1019
  rootItem: snapshot.inspection.outline.filter((item) => item.depth === 0).map(summarizeOutlineNode)[0],
849
1020
  sessionId: snapshot.version.sessionId,
850
1021
  view: snapshot.ui.view,
851
1022
  selectedItemId: snapshot.ui.selectedItemId,
1023
+ responseSlotId: snapshot.ui.responseSlotId,
852
1024
  focusItemId: snapshot.ui.focusItemId,
853
1025
  selectedLocale: snapshot.ui.selectedLocale,
854
1026
  recentItemIds: snapshot.ui.recentItemIds ?? [],
@@ -856,7 +1028,7 @@ const buildSummary = (snapshot) => ({
856
1028
  locales: snapshot.inspection.locales,
857
1029
  itemCount: snapshot.inspection.itemCount,
858
1030
  historyIndex: snapshot.version.historyIndex,
859
- outlineItemCount: snapshot.inspection.keyIndex.length,
1031
+ outlineItemCount: snapshot.inspection.itemIndex.length,
860
1032
  responseSlotCount: snapshot.inspection.responseSlots.length,
861
1033
  availableItemTypes: snapshot.capabilities.itemTypes.map((entry) => entry.itemType),
862
1034
  deterministicCreateItemTypes: getDeterministicCreateItemTypes(snapshot.capabilities),
@@ -864,7 +1036,7 @@ const buildSummary = (snapshot) => ({
864
1036
  });
865
1037
  const buildSelectedItem = (snapshot, input) => {
866
1038
  const outlineById = mapOutlineById(snapshot);
867
- const indexById = mapKeyIndexById(snapshot);
1039
+ const indexById = mapItemIndexById(snapshot);
868
1040
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
869
1041
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.selectedItemId ?? snapshot.ui.focusItemId;
870
1042
  if (!requestedItemId) return {
@@ -897,11 +1069,12 @@ const buildSelectedItem = (snapshot, input) => {
897
1069
  const buildOutline = (snapshot, input) => {
898
1070
  const query = normalizeText(input.query);
899
1071
  const outlineById = mapOutlineById(snapshot);
900
- const indexById = mapKeyIndexById(snapshot);
1072
+ const indexById = mapItemIndexById(snapshot);
901
1073
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
902
1074
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.focusItemId ?? snapshot.ui.selectedItemId;
903
1075
  const outlineMode = input.outlineMode ?? "full";
904
1076
  let strategy = "focused-snapshot-outline";
1077
+ let parentShuffled;
905
1078
  let items;
906
1079
  let note;
907
1080
  if (query) {
@@ -910,8 +1083,12 @@ const buildOutline = (snapshot, input) => {
910
1083
  } else if (outlineMode === "children" && requestedItemId) {
911
1084
  strategy = "direct-children";
912
1085
  const parent = resolvedInputItem ?? summarizeItemById(requestedItemId, indexById, outlineById);
913
- items = parent ? findDirectChildrenByFullKey(getKeyIndexEntries(snapshot), outlineById, parent.fullKey) : [];
914
- if (!parent) note = "The requested item could not be resolved by item id, key, or fullKey.";
1086
+ items = parent ? findDirectChildrenByParentId(getItemIndexEntries(snapshot), outlineById, parent.itemId) : [];
1087
+ if (!parent) note = "The requested item could not be resolved by exact item ID.";
1088
+ else {
1089
+ const rawParent = snapshot.survey.surveyItems.find((item) => item.id === parent.itemId);
1090
+ if (rawParent?.itemType === "group") parentShuffled = isRecord$3(rawParent.config) && rawParent.config.shuffleItems === true;
1091
+ }
915
1092
  } else if (outlineMode === "focused" && requestedItemId && snapshot.inspection.focus?.focusItemId === requestedItemId) {
916
1093
  const focus = snapshot.inspection.focus;
917
1094
  const descendantIds = snapshot.inspection.focusItem ? flattenFocusDescendants(snapshot.inspection.focusItem).map((item) => item.itemId) : [];
@@ -928,7 +1105,7 @@ const buildOutline = (snapshot, input) => {
928
1105
  items = item ? [item] : [];
929
1106
  note = "The requested item is outside the selected focus tree in this snapshot.";
930
1107
  } else {
931
- strategy = "full-key-index";
1108
+ strategy = "full-item-index";
932
1109
  items = getCandidateItems(snapshot);
933
1110
  }
934
1111
  const page = sliceWithCursor(items, input);
@@ -941,15 +1118,21 @@ const buildOutline = (snapshot, input) => {
941
1118
  returned: page.items.length,
942
1119
  nextCursor: page.nextCursor,
943
1120
  truncated: page.truncated,
944
- ordering: strategy === "focused-snapshot-outline" ? "Survey-tree preorder: configured group-child order is preserved; unparented items are appended by item id." : strategy === "direct-children" ? "Stored group-child order; this is not a stable respondent-visible order if that group shuffles children." : strategy === "query-match" ? "Survey-tree preorder after filtering by the query; this is not relevance-ranked." : "Relationship grouping for the focused item, not a single positional survey order.",
1121
+ ordering: {
1122
+ "full-item-index": "Survey-tree preorder: configured group-child order is preserved; unparented items are appended by item id. Shuffled groups may render a different respondent order.",
1123
+ "direct-children": parentShuffled === void 0 ? "No resolved parent group; this result does not establish sibling order." : parentShuffled ? "Stored group-child order; this group shuffles children, so respondent order may differ." : "Stored group-child order; this group does not shuffle children.",
1124
+ "query-match": "Survey-tree preorder after filtering by the query; this is not relevance-ranked.",
1125
+ "item-match": "Single matched item; this does not establish sibling order.",
1126
+ "focused-snapshot-outline": "Relationship grouping for the focused item, not a single positional survey order."
1127
+ }[strategy],
945
1128
  items: page.items,
946
1129
  note
947
1130
  };
948
1131
  };
949
1132
  const buildResponseSlots = (snapshot, input) => {
950
- const responseSlots = snapshot.inspection.responseSlots;
1133
+ const responseSlots = snapshot.responseVariables;
951
1134
  const query = normalizeText(input.query);
952
- const filtered = query ? responseSlots.filter((slot) => slot.toLowerCase().includes(query)) : responseSlots;
1135
+ const filtered = query ? responseSlots.filter((slot) => responseVariableMatchesQuery(slot, query)) : responseSlots;
953
1136
  const page = sliceWithCursor(filtered, input);
954
1137
  return {
955
1138
  available: true,
@@ -959,131 +1142,72 @@ const buildResponseSlots = (snapshot, input) => {
959
1142
  nextCursor: page.nextCursor,
960
1143
  truncated: page.truncated,
961
1144
  ordering: "Set-like response references in deterministic snapshot construction order; do not interpret their position as survey or respondent order.",
962
- slots: page.items
1145
+ slots: page.items.map(summarizeResponseVariable)
963
1146
  };
964
1147
  };
965
- const summarizeForModel$3 = (output) => {
966
- if (!output.ok) return output.reason ?? "Current editor context is unavailable.";
967
- const lines = [`Snapshot revision ${output.revision}, content hash ${output.contentHash}.`];
968
- if (output.summary) {
969
- const selected = output.summary.selectedItemId ?? "none";
970
- const surveyKey = output.summary.surveyKey ?? "not set";
971
- lines.push(`Summary: view=${output.summary.view}; selectedItemId=${selected}; locale=${output.summary.selectedLocale}; surveyKey=${surveyKey}; itemCount=${output.summary.itemCount} (excluding the structural root group).`);
972
- if (output.summary.rootItem) lines.push(`Root group for top-level create-item target.parentItemId: ${output.summary.rootItem.fullKey} (id=${output.summary.rootItem.itemId}, key=${output.summary.rootItem.key}).`);
973
- lines.push(`Outline access: ${output.summary.outlineItemCount} item(s), including the structural root group, are pageable.`);
974
- const creatableDetails = output.summary.itemTypeDetails.filter((entry) => entry.create === "deterministic").map((entry) => {
975
- const primarySurface = entry.contentSurfaces.find((surface) => surface.role === "primary");
976
- const fixedSurfaces = entry.contentSurfaces.filter((surface) => !surface.pattern).map((surface) => surface.key);
977
- const patterns = entry.contentSurfaces.filter((surface) => surface.pattern).map((surface) => surface.key);
978
- const contentKeys = [
979
- primarySurface ? `primary=${primarySurface.key}` : void 0,
980
- fixedSurfaces.length ? `visible=${fixedSurfaces.join("|")}` : void 0,
981
- patterns.length ? `dynamic=${patterns.join("|")}` : void 0,
982
- entry.authoring?.compactHint
983
- ].filter((value) => Boolean(value)).join("; ");
984
- return contentKeys ? `${entry.itemType} (${contentKeys})` : entry.itemType;
985
- });
986
- if (creatableDetails.length > 0) lines.push(`Creatable item types: ${creatableDetails.join(", ")}.`);
987
- }
988
- if (output.selectedItem) if (output.selectedItem.available && output.selectedItem.item) {
989
- lines.push(`Selected item: ${output.selectedItem.item.fullKey} (${output.selectedItem.item.itemType})${output.selectedItem.item.itemLabel ? ` - ${output.selectedItem.item.itemLabel}` : ""}.`);
990
- if (output.selectedItem.translationSnippets?.length) lines.push(`Translation snippets: ${output.selectedItem.translationSnippets.map((entry) => `${entry.locale}.${entry.contentKey}: ${entry.text}`).join("; ")}`);
991
- } else lines.push(`Selected item unavailable: ${output.selectedItem.reason ?? "unknown reason"}.`);
992
- if (output.outline) {
993
- lines.push(`Outline (${output.outline.strategy}${output.outline.mode ? `, mode=${output.outline.mode}` : ""}): ${output.outline.returned}/${output.outline.totalAvailable} rows returned.`);
994
- for (const item of output.outline.items) lines.push(`- ${item.fullKey} (${item.itemType})${item.itemLabel ? ` - ${item.itemLabel}` : ""}`);
995
- lines.push(`Outline order: ${output.outline.ordering}`);
996
- if (output.outline.nextCursor) lines.push(`More outline rows are available with cursor ${output.outline.nextCursor}.`);
997
- }
998
- if (output.responseSlots) {
999
- lines.push(output.responseSlots.available ? `Response slots: ${output.responseSlots.slots.join(", ") || "none"}` : `Response slots unavailable: ${output.responseSlots.note ?? "not indexed"}.`);
1000
- if (output.responseSlots.available) lines.push(`Response-slot order: ${output.responseSlots.ordering}`);
1001
- }
1002
- if (output.uiContext) lines.push(`UI context: activeSurface=${output.uiContext.activeSurface ?? "not provided"}; openPanels=${output.uiContext.openPanels.join(", ") || "none"}; highlights=${output.uiContext.highlights.map((entry) => entry.label ?? entry.itemId ?? entry.componentId ?? entry.kind).join(", ") || "none"}.`);
1003
- if (output.history) lines.push(`Recent editor history (${output.history.currentIndex + 1}/${output.history.totalEntries}): ${output.history.recent.map((entry) => `${entry.index}:${entry.label}${entry.isCurrent ? " [current]" : ""}`).join("; ") || "none"}.`);
1004
- return lines.join("\n");
1148
+ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1149
+ const input = inputSchema$7.parse({
1150
+ ...inputData,
1151
+ outlineMode: inputData.outlineMode ?? "full"
1152
+ });
1153
+ const snapshot = getSurveyAssistantEngineContext(requestContext);
1154
+ if (!snapshot) return {
1155
+ ok: false,
1156
+ reason: "No live editor snapshot was provided with this assistant turn.",
1157
+ scopes: input.scopes
1158
+ };
1159
+ const scopes = unique(input.scopes);
1160
+ const listOrdering = {};
1161
+ if (scopes.includes("summary")) Object.assign(listOrdering, {
1162
+ "summary.recentItemIds": "Most-recent-first UI history; not survey structure.",
1163
+ "summary.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1164
+ "summary.availableItemTypes": "Capability registry order; treat as a set of available types.",
1165
+ "summary.deterministicCreateItemTypes": "Capability registry order; treat as a set of available types.",
1166
+ "summary.itemTypeDetails": "Capability registry order; treat as a set of available type definitions."
1167
+ });
1168
+ if (scopes.includes("selected-item")) Object.assign(listOrdering, {
1169
+ "selectedItem.pathItems": "Root-to-selected survey-tree path.",
1170
+ "selectedItem.siblingItems": "Source survey item storage order; not necessarily respondent-visible order.",
1171
+ "selectedItem.childItems": "Stored group-child order when the focused item is a group.",
1172
+ "selectedItem.translationSnippets": "Grouped by locale and then source translation-key order; not positional survey content.",
1173
+ "selectedItem.descendantItems": "Depth-first survey-tree traversal from the selected item."
1174
+ });
1175
+ if (scopes.includes("ui-context")) Object.assign(listOrdering, {
1176
+ "uiContext.openPanels": "UI-provided set-like list; position has no meaning.",
1177
+ "uiContext.highlights": "UI-provided set-like list; position has no meaning."
1178
+ });
1179
+ if (scopes.includes("history")) listOrdering["history.recent"] = "Chronological editor history, oldest to newest within this returned window.";
1180
+ const output = {
1181
+ ok: true,
1182
+ revision: snapshot.version.revision,
1183
+ contentHash: snapshot.version.contentHash,
1184
+ scopes,
1185
+ ...Object.keys(listOrdering).length > 0 ? { listOrdering } : {}
1186
+ };
1187
+ if (scopes.includes("summary")) output.summary = buildSummary(snapshot);
1188
+ if (scopes.includes("selected-item")) output.selectedItem = buildSelectedItem(snapshot, input);
1189
+ if (scopes.includes("outline")) output.outline = buildOutline(snapshot, input);
1190
+ if (scopes.includes("response-slots")) output.responseSlots = buildResponseSlots(snapshot, input);
1191
+ if (scopes.includes("ui-context")) output.uiContext = {
1192
+ activeSurface: snapshot.ui.activeSurface,
1193
+ openPanels: snapshot.ui.openPanels ?? [],
1194
+ highlights: snapshot.ui.highlights ?? []
1195
+ };
1196
+ if (scopes.includes("history") && snapshot.ui.history) output.history = snapshot.ui.history;
1197
+ return parseInspectionOutput(outputSchema$6, output, "Current-context inspection");
1005
1198
  };
1006
- const surveyGetCurrentContextTool = createTool({
1007
- id: TOOL_ID$7,
1008
- description: "Read the current Survey Editor context from the latest request snapshot. Use for questions about this screen, selected item, survey outline, item keys, labels, locales, or response slots. The outline scope can page the complete survey key index, so use cursor/limit instead of guessing when a focused outline is truncated.",
1009
- inputSchema: inputSchema$7,
1010
- outputSchema: outputSchema$6,
1011
- mcp: { annotations: {
1012
- title: "Get current survey context",
1013
- readOnlyHint: true,
1014
- destructiveHint: false,
1015
- idempotentHint: true,
1016
- openWorldHint: false
1017
- } },
1018
- execute: async (inputData, context) => {
1019
- const input = {
1020
- ...inputData,
1021
- scopes: inputData.scopes ?? ["summary"],
1022
- limit: inputData.limit ?? 12,
1023
- outlineMode: inputData.outlineMode ?? "full"
1024
- };
1025
- const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
1026
- if (!snapshot) return {
1027
- ok: false,
1028
- reason: "No live editor snapshot was provided with this assistant turn.",
1029
- scopes: input.scopes
1030
- };
1031
- const scopes = unique(input.scopes);
1032
- const listOrdering = {};
1033
- if (scopes.includes("summary")) Object.assign(listOrdering, {
1034
- "summary.recentItemIds": "Most-recent-first UI history; not survey structure.",
1035
- "summary.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1036
- "summary.availableItemTypes": "Capability registry order; treat as a set of available types.",
1037
- "summary.deterministicCreateItemTypes": "Capability registry order; treat as a set of available types.",
1038
- "summary.itemTypeDetails": "Capability registry order; treat as a set of available type definitions."
1039
- });
1040
- if (scopes.includes("selected-item")) Object.assign(listOrdering, {
1041
- "selectedItem.pathItems": "Root-to-selected survey-tree path.",
1042
- "selectedItem.siblingItems": "Source survey item storage order; not necessarily respondent-visible order.",
1043
- "selectedItem.childItems": "Stored group-child order when the focused item is a group.",
1044
- "selectedItem.translationSnippets": "Grouped by locale and then source translation-key order; not positional survey content.",
1045
- "selectedItem.descendantItems": "Depth-first survey-tree traversal from the selected item."
1046
- });
1047
- if (scopes.includes("ui-context")) Object.assign(listOrdering, {
1048
- "uiContext.openPanels": "UI-provided set-like list; position has no meaning.",
1049
- "uiContext.highlights": "UI-provided set-like list; position has no meaning."
1050
- });
1051
- if (scopes.includes("history")) listOrdering["history.recent"] = "Chronological editor history, oldest to newest within this returned window.";
1052
- const output = {
1053
- ok: true,
1054
- revision: snapshot.version.revision,
1055
- contentHash: snapshot.version.contentHash,
1056
- scopes,
1057
- ...Object.keys(listOrdering).length > 0 ? { listOrdering } : {}
1058
- };
1059
- if (scopes.includes("summary")) output.summary = buildSummary(snapshot);
1060
- if (scopes.includes("selected-item")) output.selectedItem = buildSelectedItem(snapshot, input);
1061
- if (scopes.includes("outline")) output.outline = buildOutline(snapshot, input);
1062
- if (scopes.includes("response-slots")) output.responseSlots = buildResponseSlots(snapshot, input);
1063
- if (scopes.includes("ui-context")) output.uiContext = {
1064
- activeSurface: snapshot.ui.activeSurface,
1065
- openPanels: snapshot.ui.openPanels ?? [],
1066
- highlights: snapshot.ui.highlights ?? []
1067
- };
1068
- if (scopes.includes("history") && snapshot.ui.history) output.history = snapshot.ui.history;
1069
- return output;
1070
- },
1071
- toModelOutput: (value) => toTextModelOutput(value, summarizeForModel$3)
1072
- });
1073
1199
  //#endregion
1074
- //#region src/server/tools/survey-get-item-details-tool.ts
1075
- const TOOL_ID$6 = "survey_get_item_details";
1076
- const DEFAULT_TEXT_LIMIT$1 = 400;
1077
- const MAX_TEXT_LIMIT = 2e3;
1078
- const DEFAULT_CHILDREN_LIMIT = 20;
1200
+ //#region src/server/tools/inspect-item-details.ts
1079
1201
  const inputSchema$6 = z.object({
1080
- itemId: z.string().describe("Identifier of the item to inspect: its itemId, key (e.g. \"info_1\"), or fullKey (e.g. \"survey.info_1\"). Discover candidates first via survey_inspect search or current context."),
1202
+ cursor: z.string().optional(),
1203
+ responseVariablesLimit: z.number().int().min(1).max(50).default(20),
1204
+ itemId: z.string().describe("Exact persistent itemId. Discover candidates by editor name, breadcrumb or content using survey_inspect search; duplicate names are allowed, so select the intended ID."),
1081
1205
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict returned translations to. Defaults to all locales."),
1082
- textLimit: z.number().int().min(20).max(MAX_TEXT_LIMIT).default(DEFAULT_TEXT_LIMIT$1).describe("Maximum characters returned per translation snippet."),
1206
+ textLimit: z.number().int().min(20).max(2e3).default(400).describe("Maximum characters returned per translation snippet."),
1083
1207
  includeChildren: z.boolean().optional().describe("Deprecated alias for includeSurveyChildren. This means survey-tree children only, not choice options or form fields."),
1084
1208
  includeSurveyChildren: z.boolean().default(false).describe("Whether to include a shallow list of this item's direct survey-tree children, such as group members."),
1085
1209
  includeItemConfig: z.boolean().default(true).describe("Whether to include item-internal config details such as choice options and form field groups."),
1086
- childrenLimit: z.number().int().min(1).max(50).default(DEFAULT_CHILDREN_LIMIT).describe("Maximum number of direct survey-tree children to return.")
1210
+ childrenLimit: z.number().int().min(1).max(50).default(20).describe("Maximum number of direct survey-tree children to return.")
1087
1211
  });
1088
1212
  const outputSchema$5 = z.object({
1089
1213
  ok: z.boolean(),
@@ -1094,140 +1218,96 @@ const outputSchema$5 = z.object({
1094
1218
  surveyChildrenTruncated: z.boolean().optional(),
1095
1219
  listOrdering: z.record(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1096
1220
  configDetails: itemConfigDetailsSchema.optional(),
1221
+ responseVariables: z.array(responseVariableSummarySchema).optional(),
1222
+ responseVariablesTotal: z.number().optional(),
1223
+ responseVariablesTruncated: z.boolean().optional(),
1224
+ responseVariablesNextCursor: z.string().optional(),
1225
+ responseVariablesNote: z.string().optional(),
1097
1226
  capabilityDetails: z.object({
1098
1227
  summary: z.string(),
1099
1228
  details: z.unknown().optional()
1100
1229
  }).optional()
1101
1230
  });
1102
- const summarizeConfigDetails = (configDetails) => {
1103
- const lines = [`Config details (${configDetails.kind}): ${configDetails.summary.join("; ")}`];
1104
- if (configDetails.mainResponseSlotId) lines.push(`Main response slot: ${configDetails.mainResponseSlotId}`);
1105
- for (const option of configDetails.choiceOptions ?? []) {
1106
- lines.push(`- ${option.position}. option ${option.key ?? option.id} (id=${option.id}, labelContentKey=${option.labelContentKey})`);
1107
- if (option.embeddedFormItemId || option.embeddedFormFieldGroupCount) {
1108
- lines.push(` embedded form: itemId=${option.embeddedFormItemId ?? "inline"}, fieldGroups=${option.embeddedFormFieldGroupCount ?? 0}, authoringMode=${option.embeddedFormAuthoringMode ?? "custom"}, simpleModeValid=${option.embeddedFormSimpleModeValid ?? true}`);
1109
- if (option.embeddedFormTranslationPrefix || option.embeddedFormResponseSlotPrefix) lines.push(` embedded form keys: translationPrefix=${option.embeddedFormTranslationPrefix ?? "n/a"}, responseSlotPrefix=${option.embeddedFormResponseSlotPrefix ?? "n/a"}`);
1231
+ const inspectSurveyItemDetails = (inputData, requestContext) => {
1232
+ const input = inputSchema$6.parse({
1233
+ ...inputData,
1234
+ includeSurveyChildren: inputData.includeSurveyChildren ?? inputData.includeChildren
1235
+ });
1236
+ const snapshot = getSurveyAssistantEngineContext(requestContext);
1237
+ if (!snapshot) return {
1238
+ ok: false,
1239
+ reason: "No live editor snapshot was provided with this assistant turn."
1240
+ };
1241
+ const outlineById = mapOutlineById(snapshot);
1242
+ const indexById = mapItemIndexById(snapshot);
1243
+ const rawItemsById = mapRawItemsById(snapshot);
1244
+ const item = resolveItemSummary(input.itemId, indexById, outlineById);
1245
+ if (!item) return {
1246
+ ok: false,
1247
+ reason: `No item matching "${input.itemId}" (by exact itemId) was found in the current context snapshot.`
1248
+ };
1249
+ const rawItem = rawItemsById.get(item.itemId);
1250
+ const rawConfig = rawItem?.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1251
+ const output = {
1252
+ ok: true,
1253
+ item,
1254
+ translations: buildItemTranslationEntries(snapshot, item.itemId, input),
1255
+ listOrdering: {
1256
+ translations: "Grouped by locale and then source translation-key order; this list is not positional survey content.",
1257
+ surveyChildren: rawConfig?.shuffleItems === true ? "Stored group-child order; respondents see shuffled children, so this is not a stable respondent-visible order." : "Stored group-child order; this is the respondent-visible order when the parent is a group."
1110
1258
  }
1259
+ };
1260
+ if (input.includeSurveyChildren) {
1261
+ const entries = getItemIndexEntries(snapshot);
1262
+ const allChildren = findDirectChildrenByParentId(entries, outlineById, item.itemId);
1263
+ const limit = clampLimit(input.childrenLimit);
1264
+ output.surveyChildren = allChildren.slice(0, limit);
1265
+ output.surveyChildrenTruncated = allChildren.length > limit;
1111
1266
  }
1112
- for (const group of configDetails.formFieldGroups ?? []) {
1113
- lines.push(`- ${group.position}. field group ${group.id}: ${group.fieldCount} field(s), titleContentKey=${group.titleContentKey}, layout=${group.layout.base}/${group.layout.xs}/${group.layout.sm}/${group.layout.md}/${group.layout.xl}`);
1114
- if (group.hasDisplayCondition !== void 0) lines.push(` hasDisplayCondition=${group.hasDisplayCondition ? "true" : "false"}`);
1115
- for (const field of group.fields) {
1116
- lines.push(` - ${field.position}. field ${field.key} (${field.type}, id=${field.id}, responseSlot=${field.responseSlotId}, labelContentKey=${field.labelContentKey})`);
1117
- lines.push(` extra content keys: description=${field.descriptionContentKey}, error=${field.errorContentKey}${field.placeholderContentKey ? `, placeholder=${field.placeholderContentKey}` : ""}${field.unitContentKey ? `, unit=${field.unitContentKey}` : ""}${field.checkedLabelContentKey ? `, checkedLabel=${field.checkedLabelContentKey}` : ""}${field.uncheckedLabelContentKey ? `, uncheckedLabel=${field.uncheckedLabelContentKey}` : ""}`);
1118
- if (field.validationTypes?.length) lines.push(` validations: ${field.validationTypes.join(", ")}`);
1119
- if (field.validationMessageContentKeys?.length) lines.push(` validation message keys: ${field.validationMessageContentKeys.join(", ")}`);
1120
- if (field.selectOptions?.length) lines.push(` select options: ${field.selectOptions.map((option) => `${option.position}. ${option.value ?? option.id} (${option.labelContentKey})`).join(", ")}`);
1267
+ if (input.includeItemConfig) {
1268
+ const variables = snapshot.responseVariables.filter((slot) => slot.itemId === item.itemId);
1269
+ const page = sliceWithCursor(variables, {
1270
+ cursor: input.cursor,
1271
+ limit: input.responseVariablesLimit
1272
+ });
1273
+ output.responseVariables = page.items.map(summarizeResponseVariable);
1274
+ output.responseVariablesTotal = variables.length;
1275
+ output.responseVariablesTruncated = page.truncated;
1276
+ output.responseVariablesNextCursor = page.nextCursor;
1277
+ output.responseVariablesNote = "Use responseVariablesNextCursor as cursor with the same itemRef to continue. Option and computed-source summaries include up to 50 entries; inspect exact raw-json config paths for complete domains and export settings. Restart pagination after mutations.";
1278
+ output.configDetails = rawItem ? buildItemConfigDetails(rawItem) : void 0;
1279
+ output.capabilityDetails = rawItem ? inspectAssistantItem({
1280
+ capabilities: snapshot.trustedCapabilities,
1281
+ item: rawItem
1282
+ }) : void 0;
1283
+ if (rawItem?.itemType === "choiceItem" && output.configDetails?.choiceOptions) {
1284
+ const rawChoiceConfig = rawItem.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1285
+ const rawOptions = Array.isArray(rawChoiceConfig?.options) ? rawChoiceConfig.options : [];
1286
+ for (const option of output.configDetails.choiceOptions) {
1287
+ const rawOption = rawOptions.find((candidate) => candidate && typeof candidate === "object" && "id" in candidate && candidate.id === option.id);
1288
+ const embeddedForm = rawOption && typeof rawOption === "object" && "embeddedForm" in rawOption ? rawOption.embeddedForm : void 0;
1289
+ const embeddedFormDetails = buildEmbeddedFormDetails(option, embeddedForm);
1290
+ if (embeddedFormDetails?.formFieldGroups?.length) output.configDetails.formFieldGroups = [...output.configDetails.formFieldGroups ?? [], ...embeddedFormDetails.formFieldGroups];
1291
+ }
1121
1292
  }
1122
1293
  }
1123
- return lines;
1124
- };
1125
- const summarizeForModel$2 = (output) => {
1126
- if (!output.ok) return output.reason ?? "Item details are unavailable.";
1127
- const lines = [];
1128
- if (output.item) lines.push(`Item: ${output.item.fullKey} (${output.item.itemType})${output.item.itemLabel ? ` - ${output.item.itemLabel}` : ""}.`);
1129
- if (output.translations?.length) lines.push(`Translations: ${output.translations.map((entry) => `${entry.locale}.${entry.contentKey}: ${entry.text}`).join("; ")}`);
1130
- else lines.push("Translations: none found for this item.");
1131
- if (output.surveyChildren) {
1132
- lines.push(`Survey-tree children (${output.surveyChildren.length}):`);
1133
- for (const child of output.surveyChildren) lines.push(`- ${child.fullKey} (${child.itemType})${child.itemLabel ? ` - ${child.itemLabel}` : ""}`);
1134
- if (output.surveyChildrenTruncated) lines.push("More children exist than were returned; narrow down or raise childrenLimit.");
1135
- }
1136
- if (output.configDetails) {
1137
- if (output.configDetails.listOrdering) for (const [list, meaning] of Object.entries(output.configDetails.listOrdering)) lines.push(`List order (${list}): ${meaning}`);
1138
- lines.push(...summarizeConfigDetails(output.configDetails));
1139
- }
1140
- if (output.capabilityDetails) {
1141
- lines.push(`Item capability: ${output.capabilityDetails.summary}`);
1142
- if (output.capabilityDetails.details !== void 0) lines.push(`Capability details: ${JSON.stringify(output.capabilityDetails.details)}`);
1143
- }
1144
- return lines.join("\n");
1294
+ return parseInspectionOutput(outputSchema$5, output, "Item-details inspection");
1145
1295
  };
1146
- const surveyGetItemDetailsTool = createTool({
1147
- id: TOOL_ID$6,
1148
- description: "Fetch full details for a SPECIFIC survey item, whether or not it is currently selected. Returns item translations plus item-internal config details such as choice options and form fields. Use survey_search_items first when you only know visible text.",
1149
- inputSchema: inputSchema$6,
1150
- outputSchema: outputSchema$5,
1151
- mcp: { annotations: {
1152
- title: "Get survey item details",
1153
- readOnlyHint: true,
1154
- destructiveHint: false,
1155
- idempotentHint: true,
1156
- openWorldHint: false
1157
- } },
1158
- execute: async (inputData, context) => {
1159
- const input = {
1160
- ...inputData,
1161
- textLimit: inputData.textLimit ?? DEFAULT_TEXT_LIMIT$1,
1162
- includeSurveyChildren: inputData.includeSurveyChildren ?? inputData.includeChildren ?? false,
1163
- includeItemConfig: inputData.includeItemConfig ?? true,
1164
- childrenLimit: inputData.childrenLimit ?? DEFAULT_CHILDREN_LIMIT
1165
- };
1166
- const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
1167
- if (!snapshot) return {
1168
- ok: false,
1169
- reason: "No live editor snapshot was provided with this assistant turn."
1170
- };
1171
- const outlineById = mapOutlineById(snapshot);
1172
- const indexById = mapKeyIndexById(snapshot);
1173
- const rawItemsById = mapRawItemsById(snapshot);
1174
- const item = resolveItemSummary(input.itemId, indexById, outlineById);
1175
- if (!item) return {
1176
- ok: false,
1177
- reason: `No item matching "${input.itemId}" (by itemId, key, or fullKey) was found in the current context snapshot.`
1178
- };
1179
- const rawItem = rawItemsById.get(item.itemId);
1180
- const rawConfig = rawItem?.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1181
- const output = {
1182
- ok: true,
1183
- item,
1184
- translations: buildItemTranslationEntries(snapshot, item.itemId, input),
1185
- listOrdering: {
1186
- translations: "Grouped by locale and then source translation-key order; this list is not positional survey content.",
1187
- surveyChildren: rawConfig?.shuffleItems === true ? "Stored group-child order; respondents see shuffled children, so this is not a stable respondent-visible order." : "Stored group-child order; this is the respondent-visible order when the parent is a group."
1188
- }
1189
- };
1190
- if (input.includeSurveyChildren) {
1191
- const allChildren = findDirectChildrenByFullKey(getKeyIndexEntries(snapshot), outlineById, item.fullKey);
1192
- const limit = clampLimit(input.childrenLimit);
1193
- output.surveyChildren = allChildren.slice(0, limit);
1194
- output.surveyChildrenTruncated = allChildren.length > limit;
1195
- }
1196
- if (input.includeItemConfig) {
1197
- output.configDetails = rawItem ? buildItemConfigDetails(rawItem) : void 0;
1198
- output.capabilityDetails = rawItem ? inspectAssistantItem({
1199
- capabilities: snapshot.trustedCapabilities,
1200
- item: rawItem
1201
- }) : void 0;
1202
- if (rawItem?.itemType === "choiceItem" && output.configDetails?.choiceOptions) {
1203
- const rawChoiceConfig = rawItem.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1204
- const rawOptions = Array.isArray(rawChoiceConfig?.options) ? rawChoiceConfig.options : [];
1205
- for (const option of output.configDetails.choiceOptions) {
1206
- const rawOption = rawOptions.find((candidate) => candidate && typeof candidate === "object" && "id" in candidate && candidate.id === option.id);
1207
- const embeddedFormDetails = buildEmbeddedFormDetails(option, rawOption && typeof rawOption === "object" && "embeddedForm" in rawOption ? rawOption.embeddedForm : void 0);
1208
- if (embeddedFormDetails?.formFieldGroups?.length) output.configDetails.formFieldGroups = [...output.configDetails.formFieldGroups ?? [], ...embeddedFormDetails.formFieldGroups];
1209
- }
1210
- }
1211
- }
1212
- return output;
1213
- },
1214
- toModelOutput: (value) => toTextModelOutput(value, summarizeForModel$2)
1215
- });
1216
1296
  //#endregion
1217
- //#region src/server/tools/survey-get-raw-survey-tool.ts
1218
- const TOOL_ID$5 = "survey_get_raw_survey";
1297
+ //#region src/server/tools/inspect-raw-survey.ts
1219
1298
  const DEFAULT_VALUE_CHARACTER_LIMIT = 12e3;
1220
1299
  const MAX_VALUE_CHARACTER_LIMIT = 6e4;
1221
1300
  const jsonPointerPathSchema = z.string().max(500).describe("JSON Pointer path into the survey document. Examples: \"/surveyItems/0/config/items\", \"/metadata\", \"/translations/en/itemTranslations/<itemId>/title\".");
1222
1301
  const inputSchema$5 = z.object({
1223
1302
  paths: z.array(jsonPointerPathSchema).min(1).max(20).optional().describe("Optional exact JSON Pointer paths to inspect. Omit paths and itemIdentifiers for a top-level raw survey overview."),
1224
- itemIdentifiers: z.array(z.string().min(1).max(240)).min(1).max(20).optional().describe("Optional item ids, keys, or fullKeys to inspect. Returns exact /surveyItems/<index> paths plus item translation paths for each item."),
1303
+ itemIdentifiers: z.array(z.string().min(1).max(240)).min(1).max(20).optional().describe("Optional exact item IDs to inspect. Returns exact /surveyItems/<index> paths plus item translation paths for each item."),
1304
+ textOffset: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
1225
1305
  valueCharacterLimit: z.number().int().min(1e3).max(MAX_VALUE_CHARACTER_LIMIT).default(DEFAULT_VALUE_CHARACTER_LIMIT).describe("Maximum serialized characters returned per requested value.")
1226
1306
  });
1227
1307
  const rawSurveyItemPathSchema = z.object({
1228
1308
  path: z.string(),
1229
1309
  id: z.string(),
1230
- key: z.string(),
1310
+ editorName: z.string(),
1231
1311
  itemType: z.string(),
1232
1312
  parentId: z.string().optional(),
1233
1313
  childCount: z.number().int().nonnegative().optional()
@@ -1250,6 +1330,11 @@ const outputSchema$4 = z.object({
1250
1330
  found: z.boolean(),
1251
1331
  value: z.unknown().optional(),
1252
1332
  preview: z.string().optional(),
1333
+ text: z.string().optional(),
1334
+ textOffset: z.number().int().nonnegative().optional(),
1335
+ textLength: z.number().int().nonnegative().optional(),
1336
+ nextTextOffset: z.number().int().nonnegative().optional(),
1337
+ recovery: z.string().optional(),
1253
1338
  truncated: z.boolean().optional(),
1254
1339
  error: z.string().optional()
1255
1340
  })).optional(),
@@ -1283,6 +1368,7 @@ const buildOverview = (snapshot) => {
1283
1368
  const rawSurvey = getRawSurvey(snapshot);
1284
1369
  const rawItems = getRawSurveyItems(snapshot);
1285
1370
  const parentByChildId = buildParentByChildId(rawSurvey);
1371
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1286
1372
  return {
1287
1373
  topLevelKeys: Object.keys(rawSurvey).sort(),
1288
1374
  surveyItemCount: rawItems.length,
@@ -1290,7 +1376,7 @@ const buildOverview = (snapshot) => {
1290
1376
  itemPaths: rawItems.map((item, index) => ({
1291
1377
  path: `/surveyItems/${index}`,
1292
1378
  id: item.id,
1293
- key: item.key,
1379
+ editorName: names.get(item.id) ?? item.id,
1294
1380
  itemType: item.itemType,
1295
1381
  parentId: parentByChildId.get(item.id),
1296
1382
  childCount: getGroupChildren(item).length || void 0
@@ -1313,10 +1399,27 @@ const buildValues = (snapshot, input) => {
1313
1399
  const rawSurvey = getRawSurvey(snapshot);
1314
1400
  return input.paths.map((path) => {
1315
1401
  try {
1402
+ const value = getValueAtJsonPointer(rawSurvey, path);
1403
+ if (typeof value === "string" && (input.textOffset !== void 0 || JSON.stringify(value).length > input.valueCharacterLimit)) {
1404
+ const characters = [...value];
1405
+ const textOffset = input.textOffset ?? 0;
1406
+ const text = characters.slice(textOffset, textOffset + input.valueCharacterLimit).join("");
1407
+ const nextTextOffset = textOffset + [...text].length;
1408
+ return {
1409
+ path,
1410
+ found: true,
1411
+ text,
1412
+ textOffset,
1413
+ textLength: characters.length,
1414
+ truncated: textOffset > 0 || nextTextOffset < characters.length,
1415
+ ...nextTextOffset < characters.length ? { nextTextOffset } : {},
1416
+ recovery: "This is a string segment, not the complete value. Concatenate segments in offset order. Continue with survey_inspect action=raw-json, this same path, and textOffset=nextTextOffset until nextTextOffset is absent. Restart after a draft mutation."
1417
+ };
1418
+ }
1316
1419
  return {
1317
1420
  path,
1318
1421
  found: true,
1319
- ...toOutputValue(getValueAtJsonPointer(rawSurvey, path), input.valueCharacterLimit)
1422
+ ...toOutputValue(value, input.valueCharacterLimit)
1320
1423
  };
1321
1424
  } catch (error) {
1322
1425
  return {
@@ -1334,7 +1437,7 @@ const findRawItemByIdentifier = (snapshot, identifier) => {
1334
1437
  item: rawItems[directIndex],
1335
1438
  index: directIndex
1336
1439
  };
1337
- const resolved = resolveItemSummary(identifier, mapKeyIndexById(snapshot), mapOutlineById(snapshot));
1440
+ const resolved = resolveItemSummary(identifier, mapItemIndexById(snapshot), mapOutlineById(snapshot));
1338
1441
  if (!resolved) return;
1339
1442
  const resolvedIndex = rawItems.findIndex((item) => item.id === resolved.itemId);
1340
1443
  if (resolvedIndex < 0) return;
@@ -1358,11 +1461,12 @@ const buildItemValues = (snapshot, input) => {
1358
1461
  if (!resolved) return {
1359
1462
  identifier,
1360
1463
  found: false,
1361
- error: `No item matching "${identifier}" was found by item id, key, or fullKey.`
1464
+ error: `No item matching "${identifier}" was found by exact item ID.`
1362
1465
  };
1363
1466
  const itemId = resolved.item.id;
1364
1467
  const rawItemOutput = toOutputValue(resolved.item, input.valueCharacterLimit);
1365
- const translationsOutput = toOutputValue(getTranslationsByLocaleForItem(rawSurvey, itemId), input.valueCharacterLimit);
1468
+ const translationsByLocale = getTranslationsByLocaleForItem(rawSurvey, itemId);
1469
+ const translationsOutput = toOutputValue(translationsByLocale, input.valueCharacterLimit);
1366
1470
  const translationsValue = "value" in translationsOutput && isRecord$1(translationsOutput.value) ? translationsOutput.value : void 0;
1367
1471
  return {
1368
1472
  identifier,
@@ -1379,88 +1483,38 @@ const buildItemValues = (snapshot, input) => {
1379
1483
  };
1380
1484
  });
1381
1485
  };
1382
- const summarizeForModel$1 = (output) => {
1383
- if (!output.ok) return output.reason ?? "Raw survey context is unavailable.";
1384
- const lines = [];
1385
- if (output.overview) {
1386
- lines.push(`Raw survey overview: ${output.overview.surveyItemCount} item(s), top-level keys ${output.overview.topLevelKeys.join(", ")}.`);
1387
- lines.push(`Locales: ${output.overview.locales.join(", ") || "none"}.`);
1388
- lines.push("Survey item JSON paths:");
1389
- for (const item of output.overview.itemPaths.slice(0, 50)) lines.push(`- ${item.path}: ${item.key} (${item.itemType}, id=${item.id})${item.parentId ? ` parent=${item.parentId}` : ""}`);
1390
- if (output.overview.itemPaths.length > 50) lines.push("More item paths exist; request a specific path for exact raw JSON.");
1391
- }
1392
- if (output.listOrdering) for (const [list, meaning] of Object.entries(output.listOrdering)) lines.push(`List order (${list}): ${meaning}`);
1393
- for (const item of output.items ?? []) {
1394
- if (!item.found) {
1395
- lines.push(`${item.identifier}: not found (${item.error ?? "unknown error"})`);
1396
- continue;
1397
- }
1398
- lines.push(`${item.identifier}: itemId=${item.itemId}; raw path ${item.itemPath}.`);
1399
- if (item.translationPaths?.length) lines.push(`Translation paths: ${item.translationPaths.join(", ")}.`);
1400
- if (item.rawItemTruncated) lines.push(`Raw item truncated preview:\n${item.rawItemPreview ?? ""}`);
1401
- else lines.push(`Raw item: ${JSON.stringify(item.rawItem)}`);
1402
- if (item.translationsTruncated) lines.push(`Translations truncated preview:\n${item.translationsPreview ?? ""}`);
1403
- else lines.push(`Translations by locale: ${JSON.stringify(item.translationsByLocale ?? {})}`);
1404
- }
1405
- for (const entry of output.values ?? []) {
1406
- if (!entry.found) {
1407
- lines.push(`${entry.path}: not found (${entry.error ?? "unknown error"})`);
1408
- continue;
1409
- }
1410
- if (entry.truncated) lines.push(`${entry.path}: truncated JSON preview:\n${entry.preview ?? ""}`);
1411
- else lines.push(`${entry.path}: ${JSON.stringify(entry.value)}`);
1412
- }
1413
- return lines.join("\n");
1486
+ const inspectRawSurvey = (inputData, requestContext) => {
1487
+ const input = inputSchema$5.parse(inputData);
1488
+ const snapshot = getSurveyAssistantEngineContext(requestContext);
1489
+ if (!snapshot) return {
1490
+ ok: false,
1491
+ reason: "No live editor snapshot was provided with this assistant turn."
1492
+ };
1493
+ return parseInspectionOutput(outputSchema$4, {
1494
+ ok: true,
1495
+ revision: snapshot.version.revision,
1496
+ contentHash: snapshot.version.contentHash,
1497
+ listOrdering: {
1498
+ "overview.topLevelKeys": "Alphabetical key order; not survey content order.",
1499
+ "overview.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1500
+ "overview.itemPaths": "Stored surveyItems array order. The numeric segment in each JSON Pointer path is its zero-based stored position; this differs from group-child display order.",
1501
+ values: "Same order as the requested JSON Pointer paths.",
1502
+ items: "Same order as the requested itemIdentifiers."
1503
+ },
1504
+ overview: input.paths || input.itemIdentifiers ? void 0 : buildOverview(snapshot),
1505
+ items: input.itemIdentifiers ? buildItemValues(snapshot, {
1506
+ itemIdentifiers: input.itemIdentifiers,
1507
+ valueCharacterLimit: input.valueCharacterLimit ?? DEFAULT_VALUE_CHARACTER_LIMIT
1508
+ }) : void 0,
1509
+ values: input.paths ? buildValues(snapshot, {
1510
+ paths: input.paths,
1511
+ textOffset: input.textOffset,
1512
+ valueCharacterLimit: input.valueCharacterLimit ?? DEFAULT_VALUE_CHARACTER_LIMIT
1513
+ }) : void 0
1514
+ }, "Raw-survey inspection");
1414
1515
  };
1415
- const surveyGetRawSurveyTool = createTool({
1416
- id: TOOL_ID$5,
1417
- description: "Inspect exact raw survey JSON by item id/key/fullKey or JSON Pointer path. Use this before survey-json-patch edits to inspect item config, survey-level metadata, maxItemsPerPage, templateValues, translations/locales, or group ordering arrays.",
1418
- inputSchema: inputSchema$5,
1419
- outputSchema: outputSchema$4,
1420
- mcp: { annotations: {
1421
- title: "Get raw survey JSON",
1422
- readOnlyHint: true,
1423
- destructiveHint: false,
1424
- idempotentHint: true,
1425
- openWorldHint: false
1426
- } },
1427
- execute: async (inputData, context) => {
1428
- const input = {
1429
- ...inputData,
1430
- valueCharacterLimit: inputData.valueCharacterLimit ?? DEFAULT_VALUE_CHARACTER_LIMIT
1431
- };
1432
- const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
1433
- if (!snapshot) return {
1434
- ok: false,
1435
- reason: "No live editor snapshot was provided with this assistant turn."
1436
- };
1437
- return {
1438
- ok: true,
1439
- revision: snapshot.version.revision,
1440
- contentHash: snapshot.version.contentHash,
1441
- listOrdering: {
1442
- "overview.topLevelKeys": "Alphabetical key order; not survey content order.",
1443
- "overview.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1444
- "overview.itemPaths": "Stored surveyItems array order. The numeric segment in each JSON Pointer path is its zero-based stored position; this differs from group-child display order.",
1445
- values: "Same order as the requested JSON Pointer paths.",
1446
- items: "Same order as the requested itemIdentifiers."
1447
- },
1448
- overview: input.paths || input.itemIdentifiers ? void 0 : buildOverview(snapshot),
1449
- items: input.itemIdentifiers ? buildItemValues(snapshot, {
1450
- itemIdentifiers: input.itemIdentifiers,
1451
- valueCharacterLimit: input.valueCharacterLimit ?? DEFAULT_VALUE_CHARACTER_LIMIT
1452
- }) : void 0,
1453
- values: input.paths ? buildValues(snapshot, {
1454
- paths: input.paths,
1455
- valueCharacterLimit: input.valueCharacterLimit ?? DEFAULT_VALUE_CHARACTER_LIMIT
1456
- }) : void 0
1457
- };
1458
- },
1459
- toModelOutput: (value) => toTextModelOutput(value, summarizeForModel$1)
1460
- });
1461
1516
  //#endregion
1462
- //#region src/server/tools/survey-search-items-tool.ts
1463
- const TOOL_ID$4 = "survey_search_items";
1517
+ //#region src/server/tools/search-survey-items.ts
1464
1518
  const MAX_QUERY_LENGTH = 180;
1465
1519
  const MAX_MATCHES_PER_ITEM = 6;
1466
1520
  const DEFAULT_TEXT_LIMIT = 220;
@@ -1499,7 +1553,7 @@ const outputSchema$3 = z.object({
1499
1553
  ordering: z.string().optional().describe("How results and their matches are ordered; this search output is not survey order."),
1500
1554
  results: z.array(searchResultSchema).optional()
1501
1555
  });
1502
- const STOP_WORDS = new Set([
1556
+ const STOP_WORDS = /* @__PURE__ */ new Set([
1503
1557
  "a",
1504
1558
  "about",
1505
1559
  "an",
@@ -1552,7 +1606,7 @@ const candidateItems = (snapshot) => {
1552
1606
  const rawItemsById = mapRawItemsById(snapshot);
1553
1607
  const seen = /* @__PURE__ */ new Set();
1554
1608
  const candidates = [];
1555
- for (const entry of summarizeKeyIndexEntries(snapshot.inspection.keyIndex, outlineById)) {
1609
+ for (const entry of summarizeItemIndexEntries(snapshot.inspection.itemIndex, outlineById)) {
1556
1610
  seen.add(entry.itemId);
1557
1611
  candidates.push({
1558
1612
  item: entry,
@@ -1576,8 +1630,8 @@ const searchItems = (snapshot, input) => {
1576
1630
  const structuralFields = [
1577
1631
  ["itemId", item.itemId],
1578
1632
  ["itemType", item.itemType],
1579
- ["key", item.key],
1580
- ["fullKey", item.fullKey],
1633
+ ["key", item.editorName],
1634
+ ["breadcrumb", item.breadcrumb],
1581
1635
  ["itemLabel", item.itemLabel],
1582
1636
  ["path", item.path?.join(" ")]
1583
1637
  ];
@@ -1619,61 +1673,29 @@ const searchItems = (snapshot, input) => {
1619
1673
  matches: matches.slice(0, MAX_MATCHES_PER_ITEM),
1620
1674
  score: matches.reduce((sum, match) => sum + match.score, 0)
1621
1675
  };
1622
- }).filter((result) => Boolean(result)).sort((a, b) => b.score - a.score || a.item.fullKey.localeCompare(b.item.fullKey));
1676
+ }).filter((result) => Boolean(result)).sort((a, b) => b.score - a.score || a.item.breadcrumb.localeCompare(b.item.breadcrumb));
1623
1677
  };
1624
- const summarizeForModel = (output) => {
1625
- if (!output.ok) return output.reason ?? "Survey item search is unavailable.";
1626
- const lines = [`Search "${output.query ?? ""}": ${output.returned ?? 0}/${output.totalAvailable ?? 0} matching item(s) returned.`];
1627
- for (const result of output.results ?? []) {
1628
- lines.push(`- ${result.item.fullKey} (${result.item.itemType}, itemId=${result.item.itemId}, score=${result.score})`);
1629
- for (const match of result.matches) {
1630
- const source = match.source === "translation" ? `${match.locale}.${match.contentKey}` : match.field ?? match.source;
1631
- lines.push(` match ${match.source}:${source}: ${match.text}`);
1632
- }
1633
- }
1634
- if (output.nextCursor) lines.push(`More search results are available with cursor ${output.nextCursor}.`);
1635
- if (output.ordering) lines.push(`Search-result order: ${output.ordering}`);
1636
- lines.push("Use survey_inspect with action \"item-details\" and itemRef to inspect a result.");
1637
- return lines.join("\n");
1678
+ const searchSurveyItems = (inputData, requestContext) => {
1679
+ const input = inputSchema$4.parse(inputData);
1680
+ const snapshot = getSurveyAssistantEngineContext(requestContext);
1681
+ if (!snapshot) return {
1682
+ ok: false,
1683
+ reason: "No live editor snapshot was provided with this assistant turn.",
1684
+ query: input.query
1685
+ };
1686
+ const results = searchItems(snapshot, input);
1687
+ const page = sliceWithCursor(results, input);
1688
+ return parseInspectionOutput(outputSchema$3, {
1689
+ ok: true,
1690
+ query: input.query,
1691
+ totalAvailable: results.length,
1692
+ returned: page.items.length,
1693
+ nextCursor: page.nextCursor,
1694
+ truncated: page.truncated,
1695
+ ordering: "Results are ranked by descending text-match score, then structural breadcrumb. Matches within each result are ranked by descending score. Neither order is survey or respondent-visible order.",
1696
+ results: page.items
1697
+ }, "Survey-item search");
1638
1698
  };
1639
- const surveySearchItemsTool = createTool({
1640
- id: TOOL_ID$4,
1641
- description: "Search all survey items by visible translated text, item keys, labels, and config identifiers. Use this when the user refers to an unselected item by title, label, option text, field label, or another phrase rather than an item id.",
1642
- inputSchema: inputSchema$4,
1643
- outputSchema: outputSchema$3,
1644
- mcp: { annotations: {
1645
- title: "Search survey items",
1646
- readOnlyHint: true,
1647
- destructiveHint: false,
1648
- idempotentHint: true,
1649
- openWorldHint: false
1650
- } },
1651
- execute: async (inputData, context) => {
1652
- const input = {
1653
- ...inputData,
1654
- limit: inputData.limit ?? 12
1655
- };
1656
- const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
1657
- if (!snapshot) return {
1658
- ok: false,
1659
- reason: "No live editor snapshot was provided with this assistant turn.",
1660
- query: input.query
1661
- };
1662
- const results = searchItems(snapshot, input);
1663
- const page = sliceWithCursor(results, input);
1664
- return {
1665
- ok: true,
1666
- query: input.query,
1667
- totalAvailable: results.length,
1668
- returned: page.items.length,
1669
- nextCursor: page.nextCursor,
1670
- truncated: page.truncated,
1671
- ordering: "Results are ranked by descending text-match score, then item full key. Matches within each result are ranked by descending score. Neither order is survey or respondent-visible order.",
1672
- results: page.items
1673
- };
1674
- },
1675
- toModelOutput: (value) => toTextModelOutput(value, summarizeForModel)
1676
- });
1677
1699
  //#endregion
1678
1700
  //#region src/server/tools/survey-inspect-tool.ts
1679
1701
  const TOOL_ID$3 = "survey_inspect";
@@ -1707,14 +1729,15 @@ const inputSchema$3 = z.object({
1707
1729
  "ui-context",
1708
1730
  "history"
1709
1731
  ])).min(1).max(6)).describe("For current-context: context sections to inspect. Empty means omitted."),
1710
- query: optionalNonBlankString(180).describe("For search-items: text to search across survey content. For assets: optional asset id, filename, name, ALT text, or media type filter. For current-context outline: optional key/fullKey/label filter."),
1732
+ query: optionalNonBlankString(180).describe("For search-items: text to search across survey content. For assets: optional asset id, filename, name, ALT text, or media type filter. For current-context outline: optional editor-name/breadcrumb/label filter."),
1711
1733
  sourceLocale: optionalNonBlankString(40).describe("For localization: source locale to translate. Defaults to the selected editor locale."),
1712
1734
  targetLocale: optionalNonBlankString(40).describe("For localization: required target locale whose coverage should be inspected."),
1713
1735
  missingOnly: optionalInput(z.boolean()).describe("For localization: return only source surfaces that do not yet exist in the target locale."),
1714
- itemRef: optionalNonBlankString(240).describe("For current-context outline, item-details, or raw-json: item id, key, fullKey, visible label, or \"selected\"."),
1715
- cursor: optionalNonBlankString(240).describe("For current-context, search-items, or localization pagination: numeric cursor from a previous result."),
1716
- outlineMode: optionalInput(outlineModeSchema).describe("For current-context outline: \"full\" pages all survey items, \"focused\" returns the selected/focused neighborhood, and \"children\" returns direct children of itemRef."),
1717
- paths: optionalArrayInput(z.array(z.string().min(1).max(500)).min(1).max(MAX_RAW_JSON_PATHS_PER_REQUEST)).describe(`For raw-json: exact JSON Pointer paths to inspect in request order. itemRef takes precedence when both are supplied. Up to ${MAX_RAW_JSON_PATHS_PER_RESULT} paths are returned per call; larger valid requests receive explicit coverage and retry guidance instead of failing input validation. Empty means omitted.`),
1736
+ itemRef: optionalNonBlankString(240).describe("For current-context outline, item-details, or raw-json: item id, key, breadcrumb, visible label, or \"selected\"."),
1737
+ cursor: optionalNonBlankString(240).describe("For current-context, item-details response variables, search-items, or localization pagination: numeric cursor from a previous result."),
1738
+ outlineMode: optionalInput(outlineModeSchema).describe("For current-context outline: \"full\" pages all survey items, \"focused\" returns the selected/focused neighborhood, and \"children\" returns direct children of itemRef in stored sibling order. To verify placement or prepare a reorder, use \"children\" with the parent itemRef and page until complete; shuffled groups may render a different respondent order."),
1739
+ paths: optionalArrayInput(z.array(z.string().max(500)).min(1).max(MAX_RAW_JSON_PATHS_PER_REQUEST)).describe(`For raw-json: exact JSON Pointer paths to inspect in request order. itemRef takes precedence when both are supplied. Up to ${MAX_RAW_JSON_PATHS_PER_RESULT} paths are returned per call; larger valid requests receive explicit coverage and retry guidance instead of failing input validation. An empty paths array means omitted; a path of "" selects the document root (JSON Pointer), while "/" selects an empty property name.`),
1740
+ textOffset: optionalInput(z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)).describe("For raw-json exact string paths: nextTextOffset from a partial result. Continue with the same single path until nextTextOffset is absent; restart after mutations."),
1718
1741
  includeChildren: optionalInput(z.boolean()).describe("For item-details: include direct survey-tree children."),
1719
1742
  includeItemConfig: optionalInput(z.boolean()).describe("For item-details: include choice/form/internal item config details."),
1720
1743
  limit: optionalInput(z.number().int().min(1).max(50)).describe("Maximum list rows to return.")
@@ -1726,19 +1749,31 @@ const outputSchema$2 = z.object({
1726
1749
  modelSummary: z.string(),
1727
1750
  result: z.unknown().optional()
1728
1751
  });
1729
- const runTool = async (tool, input, context) => {
1730
- const execute = tool.execute;
1731
- if (!execute) throw new Error("Survey inspection backend is not executable.");
1732
- return execute(input, context);
1733
- };
1734
- const unwrapToolOutput = (value) => {
1735
- if (value && typeof value === "object" && "output" in value && value.output !== void 0) return value.output;
1736
- return value;
1737
- };
1738
1752
  const stringifyForModel = (value) => {
1739
1753
  if (typeof value === "string") return value;
1740
1754
  return JSON.stringify(value, null, 2);
1741
1755
  };
1756
+ const runInspectionBackend = (inspect) => {
1757
+ try {
1758
+ return {
1759
+ ok: true,
1760
+ value: inspect()
1761
+ };
1762
+ } catch (error) {
1763
+ if (!(error instanceof z.ZodError)) throw error;
1764
+ const firstIssue = error.issues[0];
1765
+ return {
1766
+ ok: false,
1767
+ reason: `Survey inspection validation failed${firstIssue?.path?.length ? ` at ${firstIssue.path.join(".").slice(0, 120)}` : ""}: ${(firstIssue?.message ?? "Invalid internal inspection data.").slice(0, 400)}`
1768
+ };
1769
+ }
1770
+ };
1771
+ const inspectionBackendFailure = (action, reason) => ({
1772
+ ok: false,
1773
+ action,
1774
+ reason,
1775
+ modelSummary: reason
1776
+ });
1742
1777
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1743
1778
  const MAX_LOCALIZATION_TEXT_CHARS = 2e3;
1744
1779
  const SURVEY_TRANSLATION_SURFACES = [
@@ -1752,6 +1787,7 @@ const truncateLocalizationText = (content) => {
1752
1787
  };
1753
1788
  const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1754
1789
  const survey = snapshot.survey;
1790
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1755
1791
  const sourceLocale = input.sourceLocale ?? snapshot.ui.selectedLocale;
1756
1792
  const sourceTranslations = survey.translations?.[sourceLocale];
1757
1793
  const targetTranslations = survey.translations?.[targetLocale];
@@ -1763,6 +1799,7 @@ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1763
1799
  const addSurface = ({ contentKey, itemId, itemKey, itemType, sourceContent, surface, targetContent }) => {
1764
1800
  const sourceText = truncateLocalizationText(sourceContent);
1765
1801
  if (!sourceText) return;
1802
+ const sourcePath = `/translations/${encodeJsonPointerSegment(sourceLocale)}/${itemId ? `itemTranslations/${encodeJsonPointerSegment(itemId)}` : surface}/${encodeJsonPointerSegment(contentKey)}`;
1766
1803
  surfaces.push({
1767
1804
  scope: itemId ? "item" : "survey",
1768
1805
  ...surface ? { surface } : {},
@@ -1774,11 +1811,17 @@ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1774
1811
  contentKey,
1775
1812
  sourceType: sourceContent.type,
1776
1813
  sourceText,
1814
+ sourceTextLength: getContentPlainText(sourceContent).trim().length,
1815
+ sourceTruncated: getContentPlainText(sourceContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS,
1816
+ sourcePath,
1817
+ ...sourceContent.type !== "richText" ? { sourceTextPath: `${sourcePath}/content` } : {},
1777
1818
  ...sourceContent.type === "richText" ? { sourceContent } : {},
1778
1819
  targetPresent: Boolean(targetContent && truncateLocalizationText(targetContent)),
1779
1820
  ...targetContent ? {
1780
1821
  targetType: targetContent.type,
1781
- targetText: truncateLocalizationText(targetContent)
1822
+ targetText: truncateLocalizationText(targetContent),
1823
+ targetTextLength: getContentPlainText(targetContent).trim().length,
1824
+ targetTruncated: getContentPlainText(targetContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS
1782
1825
  } : {}
1783
1826
  });
1784
1827
  };
@@ -1793,7 +1836,7 @@ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1793
1836
  }
1794
1837
  for (const item of survey.surveyItems) for (const [contentKey, sourceContent] of Object.entries(sourceTranslations.itemTranslations?.[item.id] ?? {})) addSurface({
1795
1838
  itemId: item.id,
1796
- itemKey: item.key,
1839
+ itemKey: names.get(item.id) ?? item.id,
1797
1840
  itemType: item.itemType,
1798
1841
  contentKey,
1799
1842
  sourceContent,
@@ -1812,6 +1855,7 @@ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1812
1855
  sourceLocale,
1813
1856
  targetLocale,
1814
1857
  missingOnly: input.missingOnly ?? false,
1858
+ contentCoverage: "sourceText and targetText are previews. For sourceTruncated plain text, inspect raw-json paths: [sourceTextPath] and follow nextTextOffset with textOffset until complete before translating. Rich-text sourceContent is the complete canonical value. Never translate only a truncated preview or treat targetPresent as proof of translation completeness.",
1815
1859
  totalSourceSurfaces: surfaces.length,
1816
1860
  totalAvailable: filteredSurfaces.length,
1817
1861
  missingCount,
@@ -1903,7 +1947,7 @@ const listAssets = (input, snapshot) => {
1903
1947
  };
1904
1948
  const surveyInspectTool = createTool({
1905
1949
  id: TOOL_ID$3,
1906
- description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, paged source/target localization surfaces, image assets with explicit asset IDs and usages, or focused raw JSON. At turn start the draft equals the live snapshot; successful same-turn edits appear immediately.",
1950
+ description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, paged source/target localization surfaces, image assets with explicit asset IDs and usages, expression-reference diagnostics, or focused raw JSON. For expression diagnostics, valid means there are no blocking diagnostics; advisories remain counted. At turn start the draft equals the live snapshot; successful same-turn edits appear immediately.",
1907
1951
  inputSchema: inputSchema$3,
1908
1952
  outputSchema: outputSchema$2,
1909
1953
  mcp: { annotations: {
@@ -1915,7 +1959,8 @@ const surveyInspectTool = createTool({
1915
1959
  } },
1916
1960
  execute: async (inputData, context) => {
1917
1961
  const input = inputData;
1918
- const snapshot = getSurveyAssistantEngineContext(context?.requestContext);
1962
+ const requestContext = context?.requestContext;
1963
+ const snapshot = getSurveyAssistantEngineContext(requestContext);
1919
1964
  if (!snapshot) return {
1920
1965
  ok: false,
1921
1966
  action: input.action,
@@ -1923,14 +1968,16 @@ const surveyInspectTool = createTool({
1923
1968
  modelSummary: "No survey draft is available for this turn."
1924
1969
  };
1925
1970
  if (input.action === "current-context") {
1926
- const result = unwrapToolOutput(await runTool(surveyGetCurrentContextTool, {
1971
+ const inspection = runInspectionBackend(() => inspectSurveyCurrentContext({
1927
1972
  scopes: input.scopes ?? ["summary", "selected-item"],
1928
1973
  limit: input.limit,
1929
1974
  cursor: input.cursor,
1930
1975
  itemId: resolveItemRef(input, snapshot),
1931
1976
  query: input.query,
1932
1977
  outlineMode: input.outlineMode
1933
- }, context));
1978
+ }, requestContext));
1979
+ if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
1980
+ const result = inspection.value;
1934
1981
  return {
1935
1982
  ok: true,
1936
1983
  action: input.action,
@@ -1945,11 +1992,14 @@ const surveyInspectTool = createTool({
1945
1992
  reason: "Search requires a query.",
1946
1993
  modelSummary: "Search requires a query."
1947
1994
  };
1948
- const result = unwrapToolOutput(await runTool(surveySearchItemsTool, {
1949
- query: input.query,
1995
+ const query = input.query;
1996
+ const inspection = runInspectionBackend(() => searchSurveyItems({
1997
+ query,
1950
1998
  limit: input.limit,
1951
1999
  cursor: input.cursor
1952
- }, context));
2000
+ }, requestContext));
2001
+ if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
2002
+ const result = inspection.value;
1953
2003
  return {
1954
2004
  ok: true,
1955
2005
  action: input.action,
@@ -1965,12 +2015,16 @@ const surveyInspectTool = createTool({
1965
2015
  reason: "No item reference was provided and no item is selected.",
1966
2016
  modelSummary: "No item reference was provided and no item is selected."
1967
2017
  };
1968
- const result = unwrapToolOutput(await runTool(surveyGetItemDetailsTool, {
2018
+ const inspection = runInspectionBackend(() => inspectSurveyItemDetails({
1969
2019
  itemId,
1970
2020
  includeSurveyChildren: input.includeChildren ?? false,
1971
2021
  includeItemConfig: input.includeItemConfig ?? true,
1972
- childrenLimit: input.limit
1973
- }, context));
2022
+ childrenLimit: input.limit,
2023
+ responseVariablesLimit: input.limit,
2024
+ cursor: input.cursor
2025
+ }, requestContext));
2026
+ if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
2027
+ const result = inspection.value;
1974
2028
  return {
1975
2029
  ok: true,
1976
2030
  action: input.action,
@@ -1979,20 +2033,35 @@ const surveyInspectTool = createTool({
1979
2033
  };
1980
2034
  }
1981
2035
  if (input.action === "expression-diagnostics") {
2036
+ const visibilityIssues = getAssistantVisibilityIssues(createAssistantSurvey(snapshot.survey, snapshot.trustedCapabilities));
1982
2037
  const allDiagnostics = analyzeSurveyAssistantExpressionReferences(snapshot.survey, snapshot.trustedCapabilities);
1983
2038
  const itemId = input.itemRef ? resolveItemRef(input, snapshot) : void 0;
1984
2039
  const diagnostics = itemId ? allDiagnostics.filter((diagnostic) => diagnostic.ownerItemId === itemId) : allDiagnostics;
2040
+ const orderedDiagnostics = [...diagnostics].sort((left, right) => Number(isBlockingSurveyExpressionReferenceDiagnostic(right)) - Number(isBlockingSurveyExpressionReferenceDiagnostic(left)));
2041
+ const blockingCount = diagnostics.filter(isBlockingSurveyExpressionReferenceDiagnostic).length;
2042
+ const advisoryCount = diagnostics.length - blockingCount;
1985
2043
  const limit = input.limit ?? 50;
2044
+ const limitedDiagnostics = orderedDiagnostics.slice(0, limit);
2045
+ const guidanceByDispositionAndCategory = {};
2046
+ for (const diagnostic of orderedDiagnostics) {
2047
+ const byCategory = guidanceByDispositionAndCategory[diagnostic.disposition] ??= {};
2048
+ byCategory[diagnostic.category] = getSurveyAssistantExpressionDiagnosticGuidance(diagnostic);
2049
+ }
1986
2050
  const result = {
1987
- valid: diagnostics.length === 0,
2051
+ valid: blockingCount === 0 && visibilityIssues.length === 0,
2052
+ visibilityIssues,
2053
+ visibilityScope: "survey",
1988
2054
  count: diagnostics.length,
1989
- diagnostics: diagnostics.slice(0, limit),
2055
+ blockingCount,
2056
+ advisoryCount,
2057
+ diagnostics: limitedDiagnostics,
2058
+ guidanceByDispositionAndCategory,
1990
2059
  truncated: diagnostics.length > limit
1991
2060
  };
1992
2061
  return {
1993
2062
  ok: true,
1994
2063
  action: input.action,
1995
- modelSummary: diagnostics.length === 0 ? "No broken expression references were found in the current survey draft." : `${diagnostics.length} broken expression reference${diagnostics.length === 1 ? " was" : "s were"} found. Repair every listed location, then run expression-diagnostics again before claiming completion.\n${stringifyForModel(result)}`,
2064
+ modelSummary: visibilityIssues.length > 0 ? `Survey-wide visibility validation failed. Repair cyclic or callback-based visibility using independent controlling answers or serialized context values. These checks include ancestor, component and computed dependencies outside any item filter.\n${stringifyForModel(result)}` : diagnostics.length === 0 ? "No expression reference diagnostics were found in the current survey draft." : blockingCount === 0 ? `${advisoryCount} advisory expression diagnostic${advisoryCount === 1 ? " was" : "s were"} found. Review each listed diagnostic according to its category-specific guidance.\n${stringifyForModel(result)}` : `${blockingCount} blocking expression reference diagnostic${blockingCount === 1 ? " was" : "s were"} found${advisoryCount > 0 ? ` alongside ${advisoryCount} advisory expression diagnostic${advisoryCount === 1 ? "" : "s"}` : ""}. Repair every blocking location, preserve advisories unless their category-specific guidance or the user requires a change, then run expression-diagnostics again before claiming the blocking repair is complete.\n${stringifyForModel(result)}`,
1996
2065
  result
1997
2066
  };
1998
2067
  }
@@ -2030,10 +2099,13 @@ const surveyInspectTool = createTool({
2030
2099
  const itemIdentifiers = itemId ? [itemId] : void 0;
2031
2100
  const requestedPaths = itemIdentifiers ? void 0 : input.paths;
2032
2101
  const boundedPaths = requestedPaths?.slice(0, MAX_RAW_JSON_PATHS_PER_RESULT);
2033
- const rawResult = unwrapToolOutput(await runTool(surveyGetRawSurveyTool, {
2102
+ const rawInspection = runInspectionBackend(() => inspectRawSurvey({
2034
2103
  paths: boundedPaths,
2035
- itemIdentifiers
2036
- }, context));
2104
+ itemIdentifiers,
2105
+ textOffset: input.textOffset
2106
+ }, requestContext));
2107
+ if (!rawInspection.ok) return inspectionBackendFailure(input.action, rawInspection.reason);
2108
+ const rawResult = rawInspection.value;
2037
2109
  const ignoredPathCount = itemIdentifiers ? input.paths?.length ?? 0 : 0;
2038
2110
  const omittedPathCount = Math.max(0, (requestedPaths?.length ?? 0) - (boundedPaths?.length ?? 0));
2039
2111
  const requestCoverage = ignoredPathCount > 0 || omittedPathCount > 0 ? {
@@ -2042,7 +2114,7 @@ const surveyInspectTool = createTool({
2042
2114
  inspectedPathCount: boundedPaths?.length ?? 0,
2043
2115
  ignoredPathCount,
2044
2116
  omittedPathCount,
2045
- ...omittedPathCount > 0 ? { nextPaths: requestedPaths?.slice(MAX_RAW_JSON_PATHS_PER_RESULT, MAX_RAW_JSON_PATHS_PER_RESULT * 2) } : {},
2117
+ ...omittedPathCount > 0 ? { nextPaths: requestedPaths?.slice(MAX_RAW_JSON_PATHS_PER_RESULT, 40) } : {},
2046
2118
  recovery: itemIdentifiers ? "Use a separate raw-json call without itemRef if the explicit paths are still needed." : "Retry the returned nextPaths in a separate raw-json call if they are still needed."
2047
2119
  } : void 0;
2048
2120
  const result = requestCoverage && isRecord(rawResult) ? {
@@ -2062,33 +2134,45 @@ const surveyInspectTool = createTool({
2062
2134
  //#endregion
2063
2135
  //#region src/server/tools/survey-attachment-tool.ts
2064
2136
  const TOOL_ID$2 = "survey_attachment";
2137
+ const inputSchema$2 = z.object({
2138
+ documentIds: z.array(z.string().min(1)).min(1).max(4).optional().describe("Stored attachment IDs to load completely when no page/section focus is needed."),
2139
+ documentRequests: z.array(z.object({
2140
+ documentId: z.string().min(1),
2141
+ pages: z.object({
2142
+ start: z.number().int().min(1).max(1e5),
2143
+ end: z.number().int().min(1).max(1e5)
2144
+ }).optional().describe("One-based inclusive PDF page range to prioritize."),
2145
+ focus: z.string().min(1).max(300).optional().describe("Short section, question range, or fact to prioritize in the original file.")
2146
+ })).min(1).max(4).optional().describe("Stored attachments with an optional bounded reading focus.")
2147
+ });
2148
+ const outputSchema$1 = z.object({
2149
+ loaded: z.array(z.object({
2150
+ id: z.string(),
2151
+ filename: z.string(),
2152
+ mediaType: z.string(),
2153
+ pages: z.object({
2154
+ start: z.number().int(),
2155
+ end: z.number().int()
2156
+ }).optional(),
2157
+ focus: z.string().optional()
2158
+ })),
2159
+ unavailable: z.array(z.object({
2160
+ id: z.string(),
2161
+ filename: z.string().optional(),
2162
+ mediaType: z.string().optional(),
2163
+ reason: z.enum([
2164
+ "not-listed",
2165
+ "content-unavailable",
2166
+ "attachment-limit"
2167
+ ])
2168
+ })).optional(),
2169
+ warnings: z.array(z.string()).optional()
2170
+ });
2065
2171
  const surveyAttachmentTool = createTool({
2066
2172
  id: TOOL_ID$2,
2067
2173
  description: "Attach only the stored PDFs or images needed for the current task to the next native model request. For source continuation, use this before conversational recall and provide a page or section focus when known. The original file remains authoritative.",
2068
- inputSchema: z.object({
2069
- documentIds: z.array(z.string().min(1)).min(1).max(4).optional().describe("Stored attachment IDs to load completely when no page/section focus is needed."),
2070
- documentRequests: z.array(z.object({
2071
- documentId: z.string().min(1),
2072
- pages: z.object({
2073
- start: z.number().int().min(1).max(1e5),
2074
- end: z.number().int().min(1).max(1e5)
2075
- }).optional().describe("One-based inclusive PDF page range to prioritize."),
2076
- focus: z.string().min(1).max(300).optional().describe("Short section, question range, or fact to prioritize in the original file.")
2077
- })).min(1).max(4).optional().describe("Stored attachments with an optional bounded reading focus.")
2078
- }),
2079
- outputSchema: z.object({
2080
- loaded: z.array(z.object({
2081
- id: z.string(),
2082
- filename: z.string(),
2083
- mediaType: z.string(),
2084
- pages: z.object({
2085
- start: z.number().int(),
2086
- end: z.number().int()
2087
- }).optional(),
2088
- focus: z.string().optional()
2089
- })),
2090
- warnings: z.array(z.string()).optional()
2091
- }),
2174
+ inputSchema: inputSchema$2,
2175
+ outputSchema: outputSchema$1,
2092
2176
  mcp: { annotations: {
2093
2177
  title: "Load chat attachment",
2094
2178
  readOnlyHint: true,
@@ -2097,8 +2181,8 @@ const surveyAttachmentTool = createTool({
2097
2181
  openWorldHint: false
2098
2182
  } },
2099
2183
  execute: async (inputData, context) => {
2100
- const access = (context?.requestContext)?.get("attachmentAccess");
2101
- if (!access) return { loaded: [] };
2184
+ const requestContext = context?.requestContext;
2185
+ const access = requestContext?.get("attachmentAccess");
2102
2186
  const warnings = [];
2103
2187
  const requests = /* @__PURE__ */ new Map();
2104
2188
  for (const documentId of inputData.documentIds ?? []) requests.set(documentId, {});
@@ -2114,11 +2198,50 @@ const surveyAttachmentTool = createTool({
2114
2198
  });
2115
2199
  }
2116
2200
  const requestedIds = [...requests.keys()];
2117
- const authorized = access.documents.filter((document) => requestedIds.includes(document.id));
2118
- const attachments = await access.load(authorized.map((document) => document.id));
2119
- access.selectedAttachments.splice(0, access.selectedAttachments.length, ...attachments);
2201
+ const authorized = access?.documents.filter((document) => requests.has(document.id)) ?? [];
2202
+ const unavailable = requestedIds.filter((id) => !authorized.some((document) => document.id === id)).map((id) => ({
2203
+ id,
2204
+ reason: "not-listed"
2205
+ }));
2206
+ for (const { id } of unavailable) warnings.push(`Stored attachment ${id} is not in the available manifest.`);
2207
+ if (!access) return {
2208
+ loaded: [],
2209
+ unavailable,
2210
+ warnings
2211
+ };
2212
+ const attachments = await access.load(authorized.filter((document) => !access.selectedAttachments.some((attachment) => attachment.documentId === document.id)).map((document) => document.id));
2213
+ const selected = new Map(access.selectedAttachments.map((attachment) => [attachment.documentId, attachment]));
2214
+ const nativePayloads = new Map([...requestContext?.get("modelAttachments") ?? [], ...selected.values()].map((attachment) => [`${attachment.mediaType}\u0000${attachment.contentHash}`, Buffer.byteLength(attachment.data, "base64")]));
2215
+ let totalBytes = [...nativePayloads.values()].reduce((sum, bytes) => sum + bytes, 0);
2216
+ for (const document of authorized) {
2217
+ if (selected.has(document.id)) continue;
2218
+ const attachment = attachments.find((candidate) => candidate.documentId === document.id);
2219
+ let reason = "content-unavailable";
2220
+ if (attachment) {
2221
+ const key = `${attachment.mediaType}\u0000${attachment.contentHash}`;
2222
+ const bytes = Buffer.byteLength(attachment.data, "base64");
2223
+ const alreadyIncluded = nativePayloads.has(key);
2224
+ if (bytes <= access.limits.maxAttachmentBytes && (alreadyIncluded || nativePayloads.size < access.limits.maxAttachments && totalBytes + bytes <= access.limits.maxTotalAttachmentBytes)) {
2225
+ selected.set(document.id, attachment);
2226
+ if (!alreadyIncluded) {
2227
+ nativePayloads.set(key, bytes);
2228
+ totalBytes += bytes;
2229
+ }
2230
+ continue;
2231
+ }
2232
+ reason = "attachment-limit";
2233
+ }
2234
+ unavailable.push({
2235
+ id: document.id,
2236
+ filename: document.name,
2237
+ mediaType: document.mediaType,
2238
+ reason
2239
+ });
2240
+ warnings.push(reason === "attachment-limit" ? `Stored attachment ${document.name} (${document.id}) exceeds this turn's attachment limit and was not loaded. Use a new turn with fewer or smaller sources.` : `Stored attachment ${document.name} (${document.id}) is unavailable. No content was loaded for it. Retry loading it or ask the user to upload it again.`);
2241
+ }
2242
+ access.selectedAttachments.splice(0, access.selectedAttachments.length, ...selected.values());
2120
2243
  return {
2121
- loaded: authorized.map((document) => {
2244
+ loaded: authorized.filter((document) => selected.has(document.id)).map((document) => {
2122
2245
  const request = requests.get(document.id);
2123
2246
  const pages = document.mediaType === "application/pdf" ? request?.pages : void 0;
2124
2247
  if (request?.pages && !pages) warnings.push(`Ignored a PDF page focus for non-PDF attachment ${document.name}.`);
@@ -2130,6 +2253,7 @@ const surveyAttachmentTool = createTool({
2130
2253
  ...request?.focus ? { focus: request.focus } : {}
2131
2254
  };
2132
2255
  }),
2256
+ ...unavailable.length > 0 ? { unavailable } : {},
2133
2257
  ...warnings.length > 0 ? { warnings } : {}
2134
2258
  };
2135
2259
  },
@@ -2204,7 +2328,7 @@ const formatAuditForModel = (output) => {
2204
2328
  finding.locale,
2205
2329
  finding.contentKey
2206
2330
  ].filter(Boolean).join(":");
2207
- return `- ${finding.code}${location ? ` (${location})` : ""}: ${previewAuditMessage(finding.message)}`;
2331
+ return `- [${finding.severity}] ${finding.code}${location ? ` (${location})` : ""}: ${previewAuditMessage(finding.message)}${finding.suggestion ? ` Guidance: ${previewAuditMessage(finding.suggestion)}` : ""}`;
2208
2332
  }).join("\n")}` : void 0,
2209
2333
  report.qualityReviewPrinciples.length > 0 ? `Qualitative review rubric:\n${report.qualityReviewPrinciples.map((principle) => `- ${principle.id}: ${principle.prompt}`).join("\n")}` : void 0,
2210
2334
  report.keyReview.entries.length > 0 ? `Key/content review evidence (${report.keyReview.offset + 1}-${report.keyReview.offset + report.keyReview.returnedEntryCount} of ${report.keyReview.entryCount}, source locale ${report.keyReview.sourceLocale ?? "unavailable"}${report.keyReview.truncated ? "; more pages available" : ""}):\n${report.keyReview.entries.map((entry) => {
@@ -2244,6 +2368,7 @@ const surveyAuditTool = createTool({
2244
2368
  //#region src/server/tools/survey-asset-tool.ts
2245
2369
  const TOOL_ID = "survey_asset";
2246
2370
  const inputSchema = z.object({
2371
+ retryOfChangeIds: retryOfChangeIdsSchema,
2247
2372
  action: z.enum([
2248
2373
  "import-attachment",
2249
2374
  "update-metadata",
@@ -2293,12 +2418,13 @@ const summarizeAssetChange = (event) => {
2293
2418
  lines.push(`Imported: ${event.operations.filter((operation) => operation.kind === "set-image-asset").map(describeAssetOperation).join(", ")}`);
2294
2419
  lines.push(`Asset id to reference: ${importedAssetIds.join(", ")}`);
2295
2420
  lines.push("The asset is now available to later mutation calls in this turn. To use it in a new item, reference it from that item's create-item translation. If a later delta is rejected, fix only that delta; this import remains in the draft.");
2296
- lines.push(`New info-item image source: { "type": "asset", "assetId": "${importedAssetIds[0]}" }. Embed it in a richText image block inside the create-item translation content.`);
2297
- lines.push(`New info-item translation example: { "locale": "<selected-locale>", "contentKey": "content", "content": { "type": "richText", "version": 1, "doc": { "type": "doc", "blocks": [{ "type": "image", "source": { "type": "asset", "assetId": "${importedAssetIds[0]}" }, "alt": "<meaningful description>" }] } } }.`);
2421
+ lines.push(`New info-item image source: { "type": "asset", "assetId": "${importedAssetIds[0]}" }. Embed the canonical image block inside the create-item translation richTextBlocks array.`);
2422
+ lines.push(`New info-item translation example: { "locale": "<selected-locale>", "contentKey": "content", "richTextBlocks": [{ "type": "image", "source": { "type": "asset", "assetId": "${importedAssetIds[0]}" }, "alt": "<meaningful description>" }] }.`);
2298
2423
  }
2299
2424
  }
2300
2425
  if (event.validation.issues.length > 0) lines.push(`Issues:\n${event.validation.issues.map((issue) => `- ${issue.code}: ${issue.message}`).join("\n")}`);
2301
2426
  if (event.validation.warnings.length > 0) lines.push(`Warnings:\n${event.validation.warnings.map((warning) => `- ${warning.code}: ${warning.message}`).join("\n")}`);
2427
+ lines.push(...summarizePendingRepairs(event));
2302
2428
  return lines.join("\n");
2303
2429
  };
2304
2430
  const redactEmbeddedImageOperation = (operation) => operation.kind === "set-image-asset" ? {
@@ -2342,7 +2468,7 @@ const surveyAssetTool = createTool({
2342
2468
  const manifest = access?.documents.find((entry) => entry.id === input.documentId);
2343
2469
  if (!access || !manifest) throw new Error(`Attachment "${input.documentId}" is not available for this turn.`);
2344
2470
  if (!manifest.mediaType.startsWith("image/")) throw new Error(`Attachment "${manifest.name}" is not an image.`);
2345
- const [loaded] = await access.load([input.documentId]);
2471
+ const loaded = (await access.load([input.documentId])).find((document) => document.documentId === input.documentId);
2346
2472
  if (!loaded) throw new Error(`Attachment "${manifest.name}" could not be loaded.`);
2347
2473
  const base = input.assetId ?? slug(manifest.name);
2348
2474
  let assetId = base;
@@ -2408,9 +2534,10 @@ const surveyAssetTool = createTool({
2408
2534
  normalizedOperations: []
2409
2535
  }
2410
2536
  });
2411
- if (event.status === "rejected") return turnDraft.reject(event, [`asset:${operation.assetId}`]);
2537
+ if (event.status === "rejected") return turnDraft.reject(event, collectSurveyOperationRecoveryTargets([operation], snapshot));
2412
2538
  }
2413
2539
  return turnDraft.stage({
2540
+ retryOfChangeIds: input.retryOfChangeIds,
2414
2541
  operations: [operation],
2415
2542
  snapshot,
2416
2543
  summary: input.summary
@@ -2423,18 +2550,18 @@ const surveyAssetTool = createTool({
2423
2550
  //#region src/server/tools/index.ts
2424
2551
  const SURVEY_ASSISTANT_TOOL_IDS = [
2425
2552
  TOOL_ID$3,
2426
- TOOL_ID$9,
2427
- TOOL_ID$8,
2428
- TOOL_ID$10,
2553
+ TOOL_ID$5,
2554
+ TOOL_ID$4,
2555
+ TOOL_ID$6,
2429
2556
  TOOL_ID$2,
2430
2557
  TOOL_ID$1,
2431
2558
  TOOL_ID
2432
2559
  ];
2433
2560
  const surveyAssistantTools = Object.freeze({
2434
2561
  [TOOL_ID$3]: surveyInspectTool,
2435
- [TOOL_ID$9]: surveyChangeTool,
2436
- [TOOL_ID$10]: surveyAuthoringReferenceTool,
2437
- [TOOL_ID$8]: surveyExpressionTool,
2562
+ [TOOL_ID$5]: surveyChangeTool,
2563
+ [TOOL_ID$6]: surveyAuthoringReferenceTool,
2564
+ [TOOL_ID$4]: surveyExpressionTool,
2438
2565
  [TOOL_ID$2]: surveyAttachmentTool,
2439
2566
  [TOOL_ID$1]: surveyAuditTool,
2440
2567
  [TOOL_ID]: surveyAssetTool
@@ -2442,4 +2569,4 @@ const surveyAssistantTools = Object.freeze({
2442
2569
  //#endregion
2443
2570
  export { surveyAuditTool as i, surveyAssistantTools as n, surveyAssetTool as r, SURVEY_ASSISTANT_TOOL_IDS as t };
2444
2571
 
2445
- //# sourceMappingURL=tools-CFh35R4d.mjs.map
2572
+ //# sourceMappingURL=tools-CEsv-WoO.mjs.map