@case-framework/survey-assistant 0.7.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 (130) hide show
  1. package/README.md +58 -3
  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-BJQ_1nMX.mjs → authoring-references-DbuOKerg.mjs} +12 -3
  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-1KNnNO1_.mjs → capabilities-DCMA71PP.mjs} +4 -19
  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 -411
  13. package/dist/{constants-Be91Gay9.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-BH05e-C2.mjs → controller-proxy-DEeFl9IA.mjs} +10 -20
  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-Bo85Oyj0.mjs → engine-nXoCWYpQ.mjs} +755 -590
  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-Tnqv-yKW.d.mts → index-Ciq7vHIs.d.mts} +80 -141
  34. package/dist/index-Ciq7vHIs.d.mts.map +1 -0
  35. package/dist/{index-nhrgbgO5.d.mts → index-D7wT6P3t.d.mts} +277 -60
  36. package/dist/index-D7wT6P3t.d.mts.map +1 -0
  37. package/dist/{index-l_EK4yK7.d.mts → index-WB5kfnz0.d.mts} +15 -71
  38. package/dist/index-WB5kfnz0.d.mts.map +1 -0
  39. package/dist/{index-x0qprHgK.d.mts → index-r5riv4md.d.mts} +33 -24
  40. package/dist/index-r5riv4md.d.mts.map +1 -0
  41. package/dist/{lifecycle-Bx0aq_4S.d.mts → lifecycle-Dm8u7QFh.d.mts} +34 -18
  42. package/dist/lifecycle-Dm8u7QFh.d.mts.map +1 -0
  43. package/dist/{memory-thread-repository-KWHQWFEW.mjs → memory-thread-repository-DAnOGIB3.mjs} +51 -27
  44. package/dist/memory-thread-repository-DAnOGIB3.mjs.map +1 -0
  45. package/dist/{protocol-BhzQbx81.mjs → protocol-CF4Bh_Ch.mjs} +118 -36
  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-B3ybjJ-X.mjs → react-C_GY-Fsc.mjs} +146 -39
  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 +100 -31
  56. package/dist/references/core-rules.md +6 -3
  57. package/dist/references/embedded-forms.md +34 -14
  58. package/dist/references/expressions.md +220 -58
  59. package/dist/references/fair-by-design.md +9 -9
  60. package/dist/references/item-types.md +305 -71
  61. package/dist/references/response-variables.md +38 -0
  62. package/dist/references/source-material-surveys.md +4 -3
  63. package/dist/references/survey-data-model.md +23 -8
  64. package/dist/{request-context-CG4SWijt.mjs → request-context-Dg4jSwN1.mjs} +4 -10
  65. package/dist/request-context-Dg4jSwN1.mjs.map +1 -0
  66. package/dist/{request-guardrails-TmgQtqp4.mjs → request-guardrails-C0ugBOp0.mjs} +6 -5
  67. package/dist/request-guardrails-C0ugBOp0.mjs.map +1 -0
  68. package/dist/server-agent.d.mts +37 -39
  69. package/dist/server-agent.d.mts.map +1 -1
  70. package/dist/server-agent.mjs +45 -20
  71. package/dist/server-agent.mjs.map +1 -1
  72. package/dist/server-runtime.d.mts +23 -42
  73. package/dist/server-runtime.d.mts.map +1 -1
  74. package/dist/server-runtime.mjs +12 -12
  75. package/dist/server-runtime.mjs.map +1 -1
  76. package/dist/server-tasks.d.mts +2 -2
  77. package/dist/server-tasks.mjs +2 -313
  78. package/dist/server-tools.d.mts +1 -1
  79. package/dist/server-tools.mjs +1 -1
  80. package/dist/server.d.mts +13 -36
  81. package/dist/server.d.mts.map +1 -1
  82. package/dist/server.mjs +5 -5
  83. package/dist/server.mjs.map +1 -1
  84. package/dist/storage-postgres.d.mts +9 -25
  85. package/dist/storage-postgres.d.mts.map +1 -1
  86. package/dist/storage-postgres.mjs +5 -4
  87. package/dist/storage-postgres.mjs.map +1 -1
  88. package/dist/{tasks-CYqrcaRO.d.mts → tasks-B9h3LPam.d.mts} +18 -34
  89. package/dist/tasks-B9h3LPam.d.mts.map +1 -0
  90. package/dist/tasks-BZEiwgxA.mjs +396 -0
  91. package/dist/tasks-BZEiwgxA.mjs.map +1 -0
  92. package/dist/thread-documents-DrHbaXqP.d.mts +119 -0
  93. package/dist/thread-documents-DrHbaXqP.d.mts.map +1 -0
  94. package/dist/{thread-handlers-DBAaU7BA.d.mts → thread-handlers-Bef0td9N.d.mts} +8 -40
  95. package/dist/thread-handlers-Bef0td9N.d.mts.map +1 -0
  96. package/dist/{tools-BvSovehT.mjs → tools-CEsv-WoO.mjs} +296 -105
  97. package/dist/tools-CEsv-WoO.mjs.map +1 -0
  98. package/dist/ui.css +1 -1
  99. package/dist/ui.d.mts +15 -54
  100. package/dist/ui.d.mts.map +1 -1
  101. package/dist/ui.mjs +172 -137
  102. package/dist/ui.mjs.map +1 -1
  103. package/docs/integration.md +11 -1
  104. package/package.json +40 -35
  105. package/dist/authoring-references-BJQ_1nMX.mjs.map +0 -1
  106. package/dist/capabilities-1KNnNO1_.mjs.map +0 -1
  107. package/dist/capabilities-DVTfogjv.d.mts.map +0 -1
  108. package/dist/capabilities-default.mjs.map +0 -1
  109. package/dist/constants-Be91Gay9.d.mts.map +0 -1
  110. package/dist/controller-proxy-BH05e-C2.mjs.map +0 -1
  111. package/dist/controller-proxy-Bd0ZHBPF.d.mts +0 -85
  112. package/dist/controller-proxy-Bd0ZHBPF.d.mts.map +0 -1
  113. package/dist/digest-SJmoYVaK.d.mts.map +0 -1
  114. package/dist/engine-Bo85Oyj0.mjs.map +0 -1
  115. package/dist/index-Tnqv-yKW.d.mts.map +0 -1
  116. package/dist/index-l_EK4yK7.d.mts.map +0 -1
  117. package/dist/index-nhrgbgO5.d.mts.map +0 -1
  118. package/dist/index-x0qprHgK.d.mts.map +0 -1
  119. package/dist/lifecycle-Bx0aq_4S.d.mts.map +0 -1
  120. package/dist/memory-thread-repository-KWHQWFEW.mjs.map +0 -1
  121. package/dist/protocol-BhzQbx81.mjs.map +0 -1
  122. package/dist/react-B3ybjJ-X.mjs.map +0 -1
  123. package/dist/request-context-CG4SWijt.mjs.map +0 -1
  124. package/dist/request-guardrails-TmgQtqp4.mjs.map +0 -1
  125. package/dist/server-tasks.mjs.map +0 -1
  126. package/dist/tasks-CYqrcaRO.d.mts.map +0 -1
  127. package/dist/thread-documents-rmd6nyX9.d.mts +0 -107
  128. package/dist/thread-documents-rmd6nyX9.d.mts.map +0 -1
  129. package/dist/thread-handlers-DBAaU7BA.d.mts.map +0 -1
  130. package/dist/tools-BvSovehT.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
- import { X as SURVEY_OPERATION_KINDS, bt as surveyOperationSchemasByKind, f as surveyChangeSetSchema, q as assistantItemTypeManifestSchema, yt as surveyOperationSchema } from "./protocol-BhzQbx81.mjs";
2
- import { a as getDeterministicCreateItemTypes, o as inspectAssistantItem, s as resolveAssistantCapabilityReference } from "./capabilities-1KNnNO1_.mjs";
3
- import { $ as sliceWithCursor, A as buildEmbeddedFormDetails, C as buildExpressionMutationBatch, F as clampLimit, G as itemMatchesQuery, H as getRawSurvey, I as findDirectChildrenByFullKey, J as mapOutlineById, K as itemSummarySchema, M as buildItemTranslationEntries, Q as resolveItemSummary, S as normalizeSurveyChangeOperations, T as expressionMutationSchema, U as getRawSurveyItems, V as getKeyIndexEntries, W as itemConfigDetailsSchema, X as normalizeText, Y as mapRawItemsById, at as summarizeRawItem, ct as analyzeSurveyAssistantExpressionReferences, d as collectExpressionMutationRecoveryTargets, et as summarizeFocusItemNode, f as collectMalformedSurveyOperationRecoveryTargets, it as summarizeOutlineNode, j as buildItemConfigDetails, k as buildConfigSearchText, m as auditSurvey, nt as summarizeKeyIndexEntries, ot as translationEntrySchema, p as collectSurveyOperationRecoveryTargets, q as mapKeyIndexById, st as unique, tt as summarizeItemById, u as collectExpressionMutationRecoveryRefinements, ut as getSurveyAssistantExpressionDiagnosticGuidance, v as createSurveyChangeSet } from "./engine-Bo85Oyj0.mjs";
4
- import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-CG4SWijt.mjs";
5
- import { a as surveyAuthoringReferences, i as surveyAuthoringReferenceTopics, r as getBuiltInItemTypeAuthoringReference, t as builtInSurveyAuthoringItemTypes } from "./authoring-references-BJQ_1nMX.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
7
  import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getContentPlainText, getValueAtJsonPointer, isBlockingSurveyExpressionReferenceDiagnostic } from "@case-framework/survey-core";
8
8
  import { randomUUID } from "node:crypto";
@@ -68,6 +68,16 @@ const formatItemCapabilityProfile = (manifest, kind) => {
68
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.");
69
69
  return lines.join("\n");
70
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");
71
81
  const formatSurveyOperationContractReference = (kind, options = {}) => {
72
82
  const { $schema: _schemaVersion, ...contract } = z.toJSONSchema(surveyOperationSchemasByKind[kind], {
73
83
  io: "input",
@@ -89,7 +99,8 @@ const formatSurveyOperationContractReference = (kind, options = {}) => {
89
99
  "```json",
90
100
  JSON.stringify(renderedContract, null, 2),
91
101
  "```",
92
- ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : []
102
+ ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : [],
103
+ ...options.itemConfigurationSchema && (kind === "patch-item-config" || kind === "create-item") ? [formatItemConfigurationContract(options.itemConfigurationSchema)] : []
93
104
  ].join("\n");
94
105
  };
95
106
  //#endregion
@@ -246,7 +257,8 @@ const surveyAuthoringReferenceTool = createTool({
246
257
  title: itemCapability ? `${inputData.operationKind} for ${itemCapability.displayName}` : `${inputData.operationKind} operation contract`,
247
258
  content: formatSurveyOperationContractReference(inputData.operationKind, {
248
259
  detail: contractDetail,
249
- itemCapability
260
+ itemCapability,
261
+ itemConfigurationSchema: inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.configurationSchema : void 0
250
262
  })
251
263
  };
252
264
  }
@@ -297,7 +309,7 @@ const surveyAuthoringReferenceTool = createTool({
297
309
  itemType: inputData.itemType,
298
310
  operationKind: inputData.operationKind,
299
311
  title: entry.title,
300
- content: entry.content
312
+ content: inputData.topic === "item-types" && capability?.configurationSchema ? `${entry.content}\n\n${formatItemConfigurationContract(capability.configurationSchema)}` : entry.content
301
313
  };
302
314
  },
303
315
  toModelOutput: (value) => toTextModelOutput(value, (output) => `# ${output.title}\n\n${output.content}`)
@@ -322,16 +334,17 @@ const OPERATIONS_DESCRIPTION = [
322
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.",
323
335
  "Any translations owned by an operation must be inside that same operation object. Never put translations beside the survey_change operations array.",
324
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.",
325
- "Canonical create-item shape: put parentItemId and optional index in target; put the required key and 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.",
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.",
326
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.",
327
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.",
328
340
  "Use remove-survey-locale with locale to destructively remove an existing survey language and all translations stored for it.",
329
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.",
330
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.",
331
- "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.",
332
- "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.",
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.",
333
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.`}`,
334
- "Use patch-item-config with itemId and plural patches whose JSON Pointer paths are relative to that item config root.",
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.",
335
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.",
336
349
  "When deleting referenced images and assets, include every reference-removal operation and remove-image-asset last in the same operations array.",
337
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.",
@@ -367,7 +380,6 @@ const describeOperationTarget = (operation) => {
367
380
  case "update-placed-image": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}#${operation.imageIndex}`;
368
381
  case "update-item-label":
369
382
  case "update-item-color":
370
- case "update-item-key":
371
383
  case "patch-item-config":
372
384
  case "remove-item":
373
385
  case "move-item": return `${operation.kind} ${operation.itemId}`;
@@ -379,11 +391,12 @@ const describeOperationTarget = (operation) => {
379
391
  case "move-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.fieldId}`;
380
392
  case "set-embedded-form":
381
393
  case "remove-embedded-form": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
382
- 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}`;
383
395
  case "update-choice-option-label":
384
396
  case "remove-choice-option": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
385
397
  case "reorder-choice-options": return `${operation.kind} ${operation.itemId}`;
386
398
  case "reorder-items": return `${operation.kind} ${operation.parentItemId}`;
399
+ case "update-response-settings":
387
400
  case "update-survey-metadata":
388
401
  case "update-survey-pagination": return operation.kind;
389
402
  case "update-survey-translation": return `${operation.kind} ${operation.locale}:${operation.surface}:${operation.contentKey}`;
@@ -392,7 +405,7 @@ const describeOperationTarget = (operation) => {
392
405
  case "set-image-asset":
393
406
  case "update-image-asset-metadata":
394
407
  case "remove-image-asset": return `${operation.kind} ${operation.assetId}`;
395
- 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}`;
396
409
  case "survey-json-patch": return `${operation.kind} ${operation.patches.map((patch) => patch.path).join(", ")}`;
397
410
  }
398
411
  };
@@ -410,11 +423,19 @@ const summarizeValidation = (event) => {
410
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.`);
411
424
  if (event.postChange) {
412
425
  const unavailableItemCount = event.postChange.touchedItemCount - event.postChange.availableTouchedItemCount;
413
- 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` : ""}.`);
414
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.`);
415
428
  for (const item of event.postChange.touchedItems) {
416
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("; ");
417
- 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
+ }
418
439
  }
419
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.");
420
441
  }
@@ -500,7 +521,7 @@ const inputSchema$8 = z.object({
500
521
  retryOfChangeIds: retryOfChangeIdsSchema,
501
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."),
502
523
  reason: z.string().max(500).optional().describe("Context action only: optional inspection reason."),
503
- itemIds: z.array(z.string().min(1).max(240)).max(12).optional().describe("Context action only: filter response slots and expression locations by item ids, keys, or fullKeys. Omit to inspect survey-wide references, including controlling questions outside the conditioned item."),
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."),
504
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."),
505
526
  limit: z.number().int().min(1).max(80).optional().describe("Context action only: rows per catalog page; defaults to 80."),
506
527
  includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item."),
@@ -542,12 +563,17 @@ const contextOutputSchema = z.object({
542
563
  totalExpressionLocations: z.number().int().nonnegative().optional(),
543
564
  unresolvedItemIds: z.array(z.string()).optional(),
544
565
  responseSlots: z.array(z.object({
545
- ref: z.string(),
546
- 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()
547
573
  })),
548
574
  expressionLocations: z.array(z.object({
549
575
  itemId: z.string(),
550
- key: z.string(),
576
+ editorName: z.string(),
551
577
  itemType: z.string(),
552
578
  rawIndex: z.number().int().nonnegative(),
553
579
  locations: z.array(z.string())
@@ -591,22 +617,32 @@ const summarizeDraftEvent = (event) => {
591
617
  };
592
618
  const buildContextOutput = (snapshot, input) => {
593
619
  const rawItems = getRawSurveyItems(snapshot);
620
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
594
621
  const resolvedItems = input.itemIds?.map((identifier) => ({
595
622
  identifier,
596
- itemId: snapshot.inspection.keyIndex.find((entry) => entry.itemId === identifier || entry.key === identifier || entry.fullKey === identifier)?.itemId
623
+ itemId: snapshot.inspection.itemIndex.find((entry) => entry.itemId === identifier || entry.editorName === identifier || entry.breadcrumb === identifier)?.itemId
597
624
  }));
598
625
  const itemFilter = resolvedItems?.length ? new Set(resolvedItems.flatMap((entry) => entry.itemId ? [entry.itemId] : [])) : void 0;
599
626
  const unresolvedItemIds = resolvedItems?.filter((entry) => !entry.itemId).map((entry) => entry.identifier) ?? [];
600
627
  const surveyResponseSlots = Object.entries(buildResponseSlotTypes(snapshot));
601
- const allResponseSlots = surveyResponseSlots.filter(([ref]) => !itemFilter || [...itemFilter].some((id) => ref.startsWith(`${id}...`))).map(([ref, valueType]) => ({
602
- ref,
603
- ...valueType ? { valueType } : {}
604
- }));
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
+ });
605
641
  const offset = Number(input.cursor ?? 0);
606
642
  const limit = input.limit ?? 80;
607
643
  const responseSlots = allResponseSlots.slice(offset, offset + limit);
608
644
  const allExpressionLocations = input.action === "context" && input.includeRawLocations === true ? rawItems.map((item, rawIndex) => {
609
- if (itemFilter && !itemFilter.has(item.id) && !itemFilter.has(item.key)) return;
645
+ if (itemFilter && !itemFilter.has(item.id)) return;
610
646
  const locations = [
611
647
  item.displayConditions?.root ? "displayConditions.root" : void 0,
612
648
  ...Object.keys(item.displayConditions?.components ?? {}).map((componentId) => `displayConditions.components.${componentId}`),
@@ -616,7 +652,7 @@ const buildContextOutput = (snapshot, input) => {
616
652
  ].filter((location) => Boolean(location));
617
653
  return {
618
654
  itemId: item.id,
619
- key: item.key,
655
+ editorName: names.get(item.id) ?? item.id,
620
656
  itemType: item.itemType,
621
657
  rawIndex,
622
658
  locations
@@ -633,17 +669,17 @@ const buildContextOutput = (snapshot, input) => {
633
669
  "Use prepare mutations for display conditions, disabled conditions, validations, prefills, and template values.",
634
670
  "Display, disabled, validation, and prefill.when expressions must return boolean.",
635
671
  "Use response refs exactly as listed and check their value types before choosing functions.",
636
- "An isDefined ref means only that the respondent answered; it never means a particular answer such as Yes, selected, eligible, or has children.",
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.",
637
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.",
638
- "Avoid accidental item display-condition response cycles, including self-reference or A-to-B-to-A chains. Shared analysis reports them as advisory because prefills can make them intentional; prefer an acyclic controlling source item unless the dependency is deliberate.",
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.",
639
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[].",
640
676
  "Response-slot order: set-like deterministic snapshot construction order; it has no survey or respondent-visible meaning.",
641
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).`,
642
- `Response slots (${responseSlots.length}/${allResponseSlots.length}, offset ${offset}): ${responseSlots.map((slot) => `${slot.ref}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
678
+ `Response slots (${responseSlots.length}/${allResponseSlots.length}, offset ${offset}): ${responseSlots.map((slot) => `${JSON.stringify(slot.ref)}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
643
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.",
644
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,
645
681
  "Functions: and, or, not, eq, gt, gte, lt, lte, in_range, list_contains, sum, min, max, str_eq, date_eq.",
646
- 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
647
683
  ].filter((line) => Boolean(line)).join("\n"),
648
684
  totalResponseSlots: allResponseSlots.length,
649
685
  surveyResponseSlotCount: surveyResponseSlots.length,
@@ -772,6 +808,69 @@ const parseInspectionOutput = (schema, value, backend) => {
772
808
  return parsed.data;
773
809
  };
774
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
775
874
  //#region src/server/tools/inspect-current-context.ts
776
875
  const contextScopeSchema = z.enum([
777
876
  "summary",
@@ -794,9 +893,9 @@ const translationSnippetSchema = z.object({
794
893
  const inputSchema$7 = z.object({
795
894
  scopes: z.array(contextScopeSchema).min(1).default(["summary"]).describe("Which current editor context sections to inspect."),
796
895
  itemId: z.string().optional().describe("Optional item id to inspect. Defaults to the selected/focused item."),
797
- query: z.string().max(180).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."),
798
897
  cursor: z.string().optional().describe("Optional numeric cursor returned by a previous outline or response slot request."),
799
- 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."),
800
899
  limit: z.number().int().min(1).max(50).default(12).describe("Maximum number of rows to return for list-like scopes.")
801
900
  });
802
901
  const outputSchema$6 = z.object({
@@ -810,6 +909,7 @@ const outputSchema$6 = z.object({
810
909
  sessionId: z.string().nullable(),
811
910
  view: z.string(),
812
911
  selectedItemId: z.string().optional(),
912
+ responseSlotId: z.string().optional(),
813
913
  focusItemId: z.string().optional(),
814
914
  selectedLocale: z.string(),
815
915
  recentItemIds: z.array(z.string()),
@@ -859,7 +959,7 @@ const outputSchema$6 = z.object({
859
959
  nextCursor: z.string().optional(),
860
960
  truncated: z.boolean(),
861
961
  ordering: z.string(),
862
- slots: z.array(z.string()),
962
+ slots: z.array(responseVariableSummarySchema),
863
963
  note: z.string().optional()
864
964
  }).optional(),
865
965
  uiContext: z.object({
@@ -913,13 +1013,14 @@ const flattenFocusDescendants = (node) => {
913
1013
  };
914
1014
  const getCandidateItems = (snapshot) => {
915
1015
  const outlineById = mapOutlineById(snapshot);
916
- return summarizeKeyIndexEntries(getKeyIndexEntries(snapshot), outlineById);
1016
+ return summarizeItemIndexEntries(getItemIndexEntries(snapshot), outlineById);
917
1017
  };
918
1018
  const buildSummary = (snapshot) => ({
919
1019
  rootItem: snapshot.inspection.outline.filter((item) => item.depth === 0).map(summarizeOutlineNode)[0],
920
1020
  sessionId: snapshot.version.sessionId,
921
1021
  view: snapshot.ui.view,
922
1022
  selectedItemId: snapshot.ui.selectedItemId,
1023
+ responseSlotId: snapshot.ui.responseSlotId,
923
1024
  focusItemId: snapshot.ui.focusItemId,
924
1025
  selectedLocale: snapshot.ui.selectedLocale,
925
1026
  recentItemIds: snapshot.ui.recentItemIds ?? [],
@@ -927,7 +1028,7 @@ const buildSummary = (snapshot) => ({
927
1028
  locales: snapshot.inspection.locales,
928
1029
  itemCount: snapshot.inspection.itemCount,
929
1030
  historyIndex: snapshot.version.historyIndex,
930
- outlineItemCount: snapshot.inspection.keyIndex.length,
1031
+ outlineItemCount: snapshot.inspection.itemIndex.length,
931
1032
  responseSlotCount: snapshot.inspection.responseSlots.length,
932
1033
  availableItemTypes: snapshot.capabilities.itemTypes.map((entry) => entry.itemType),
933
1034
  deterministicCreateItemTypes: getDeterministicCreateItemTypes(snapshot.capabilities),
@@ -935,7 +1036,7 @@ const buildSummary = (snapshot) => ({
935
1036
  });
936
1037
  const buildSelectedItem = (snapshot, input) => {
937
1038
  const outlineById = mapOutlineById(snapshot);
938
- const indexById = mapKeyIndexById(snapshot);
1039
+ const indexById = mapItemIndexById(snapshot);
939
1040
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
940
1041
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.selectedItemId ?? snapshot.ui.focusItemId;
941
1042
  if (!requestedItemId) return {
@@ -968,11 +1069,12 @@ const buildSelectedItem = (snapshot, input) => {
968
1069
  const buildOutline = (snapshot, input) => {
969
1070
  const query = normalizeText(input.query);
970
1071
  const outlineById = mapOutlineById(snapshot);
971
- const indexById = mapKeyIndexById(snapshot);
1072
+ const indexById = mapItemIndexById(snapshot);
972
1073
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
973
1074
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.focusItemId ?? snapshot.ui.selectedItemId;
974
1075
  const outlineMode = input.outlineMode ?? "full";
975
1076
  let strategy = "focused-snapshot-outline";
1077
+ let parentShuffled;
976
1078
  let items;
977
1079
  let note;
978
1080
  if (query) {
@@ -981,8 +1083,12 @@ const buildOutline = (snapshot, input) => {
981
1083
  } else if (outlineMode === "children" && requestedItemId) {
982
1084
  strategy = "direct-children";
983
1085
  const parent = resolvedInputItem ?? summarizeItemById(requestedItemId, indexById, outlineById);
984
- items = parent ? findDirectChildrenByFullKey(getKeyIndexEntries(snapshot), outlineById, parent.fullKey) : [];
985
- 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
+ }
986
1092
  } else if (outlineMode === "focused" && requestedItemId && snapshot.inspection.focus?.focusItemId === requestedItemId) {
987
1093
  const focus = snapshot.inspection.focus;
988
1094
  const descendantIds = snapshot.inspection.focusItem ? flattenFocusDescendants(snapshot.inspection.focusItem).map((item) => item.itemId) : [];
@@ -999,7 +1105,7 @@ const buildOutline = (snapshot, input) => {
999
1105
  items = item ? [item] : [];
1000
1106
  note = "The requested item is outside the selected focus tree in this snapshot.";
1001
1107
  } else {
1002
- strategy = "full-key-index";
1108
+ strategy = "full-item-index";
1003
1109
  items = getCandidateItems(snapshot);
1004
1110
  }
1005
1111
  const page = sliceWithCursor(items, input);
@@ -1012,15 +1118,21 @@ const buildOutline = (snapshot, input) => {
1012
1118
  returned: page.items.length,
1013
1119
  nextCursor: page.nextCursor,
1014
1120
  truncated: page.truncated,
1015
- 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],
1016
1128
  items: page.items,
1017
1129
  note
1018
1130
  };
1019
1131
  };
1020
1132
  const buildResponseSlots = (snapshot, input) => {
1021
- const responseSlots = snapshot.inspection.responseSlots;
1133
+ const responseSlots = snapshot.responseVariables;
1022
1134
  const query = normalizeText(input.query);
1023
- const filtered = query ? responseSlots.filter((slot) => slot.toLowerCase().includes(query)) : responseSlots;
1135
+ const filtered = query ? responseSlots.filter((slot) => responseVariableMatchesQuery(slot, query)) : responseSlots;
1024
1136
  const page = sliceWithCursor(filtered, input);
1025
1137
  return {
1026
1138
  available: true,
@@ -1030,7 +1142,7 @@ const buildResponseSlots = (snapshot, input) => {
1030
1142
  nextCursor: page.nextCursor,
1031
1143
  truncated: page.truncated,
1032
1144
  ordering: "Set-like response references in deterministic snapshot construction order; do not interpret their position as survey or respondent order.",
1033
- slots: page.items
1145
+ slots: page.items.map(summarizeResponseVariable)
1034
1146
  };
1035
1147
  };
1036
1148
  const inspectSurveyCurrentContext = (inputData, requestContext) => {
@@ -1087,7 +1199,9 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1087
1199
  //#endregion
1088
1200
  //#region src/server/tools/inspect-item-details.ts
1089
1201
  const inputSchema$6 = z.object({
1090
- 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."),
1091
1205
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict returned translations to. Defaults to all locales."),
1092
1206
  textLimit: z.number().int().min(20).max(2e3).default(400).describe("Maximum characters returned per translation snippet."),
1093
1207
  includeChildren: z.boolean().optional().describe("Deprecated alias for includeSurveyChildren. This means survey-tree children only, not choice options or form fields."),
@@ -1104,6 +1218,11 @@ const outputSchema$5 = z.object({
1104
1218
  surveyChildrenTruncated: z.boolean().optional(),
1105
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."),
1106
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(),
1107
1226
  capabilityDetails: z.object({
1108
1227
  summary: z.string(),
1109
1228
  details: z.unknown().optional()
@@ -1120,12 +1239,12 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1120
1239
  reason: "No live editor snapshot was provided with this assistant turn."
1121
1240
  };
1122
1241
  const outlineById = mapOutlineById(snapshot);
1123
- const indexById = mapKeyIndexById(snapshot);
1242
+ const indexById = mapItemIndexById(snapshot);
1124
1243
  const rawItemsById = mapRawItemsById(snapshot);
1125
1244
  const item = resolveItemSummary(input.itemId, indexById, outlineById);
1126
1245
  if (!item) return {
1127
1246
  ok: false,
1128
- reason: `No item matching "${input.itemId}" (by itemId, key, or fullKey) was found in the current context snapshot.`
1247
+ reason: `No item matching "${input.itemId}" (by exact itemId) was found in the current context snapshot.`
1129
1248
  };
1130
1249
  const rawItem = rawItemsById.get(item.itemId);
1131
1250
  const rawConfig = rawItem?.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
@@ -1139,12 +1258,23 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1139
1258
  }
1140
1259
  };
1141
1260
  if (input.includeSurveyChildren) {
1142
- const allChildren = findDirectChildrenByFullKey(getKeyIndexEntries(snapshot), outlineById, item.fullKey);
1261
+ const entries = getItemIndexEntries(snapshot);
1262
+ const allChildren = findDirectChildrenByParentId(entries, outlineById, item.itemId);
1143
1263
  const limit = clampLimit(input.childrenLimit);
1144
1264
  output.surveyChildren = allChildren.slice(0, limit);
1145
1265
  output.surveyChildrenTruncated = allChildren.length > limit;
1146
1266
  }
1147
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.";
1148
1278
  output.configDetails = rawItem ? buildItemConfigDetails(rawItem) : void 0;
1149
1279
  output.capabilityDetails = rawItem ? inspectAssistantItem({
1150
1280
  capabilities: snapshot.trustedCapabilities,
@@ -1155,7 +1285,8 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1155
1285
  const rawOptions = Array.isArray(rawChoiceConfig?.options) ? rawChoiceConfig.options : [];
1156
1286
  for (const option of output.configDetails.choiceOptions) {
1157
1287
  const rawOption = rawOptions.find((candidate) => candidate && typeof candidate === "object" && "id" in candidate && candidate.id === option.id);
1158
- const embeddedFormDetails = buildEmbeddedFormDetails(option, rawOption && typeof rawOption === "object" && "embeddedForm" in rawOption ? rawOption.embeddedForm : void 0);
1288
+ const embeddedForm = rawOption && typeof rawOption === "object" && "embeddedForm" in rawOption ? rawOption.embeddedForm : void 0;
1289
+ const embeddedFormDetails = buildEmbeddedFormDetails(option, embeddedForm);
1159
1290
  if (embeddedFormDetails?.formFieldGroups?.length) output.configDetails.formFieldGroups = [...output.configDetails.formFieldGroups ?? [], ...embeddedFormDetails.formFieldGroups];
1160
1291
  }
1161
1292
  }
@@ -1169,14 +1300,14 @@ const MAX_VALUE_CHARACTER_LIMIT = 6e4;
1169
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\".");
1170
1301
  const inputSchema$5 = z.object({
1171
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."),
1172
- 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."),
1173
1304
  textOffset: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
1174
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.")
1175
1306
  });
1176
1307
  const rawSurveyItemPathSchema = z.object({
1177
1308
  path: z.string(),
1178
1309
  id: z.string(),
1179
- key: z.string(),
1310
+ editorName: z.string(),
1180
1311
  itemType: z.string(),
1181
1312
  parentId: z.string().optional(),
1182
1313
  childCount: z.number().int().nonnegative().optional()
@@ -1237,6 +1368,7 @@ const buildOverview = (snapshot) => {
1237
1368
  const rawSurvey = getRawSurvey(snapshot);
1238
1369
  const rawItems = getRawSurveyItems(snapshot);
1239
1370
  const parentByChildId = buildParentByChildId(rawSurvey);
1371
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1240
1372
  return {
1241
1373
  topLevelKeys: Object.keys(rawSurvey).sort(),
1242
1374
  surveyItemCount: rawItems.length,
@@ -1244,7 +1376,7 @@ const buildOverview = (snapshot) => {
1244
1376
  itemPaths: rawItems.map((item, index) => ({
1245
1377
  path: `/surveyItems/${index}`,
1246
1378
  id: item.id,
1247
- key: item.key,
1379
+ editorName: names.get(item.id) ?? item.id,
1248
1380
  itemType: item.itemType,
1249
1381
  parentId: parentByChildId.get(item.id),
1250
1382
  childCount: getGroupChildren(item).length || void 0
@@ -1305,7 +1437,7 @@ const findRawItemByIdentifier = (snapshot, identifier) => {
1305
1437
  item: rawItems[directIndex],
1306
1438
  index: directIndex
1307
1439
  };
1308
- const resolved = resolveItemSummary(identifier, mapKeyIndexById(snapshot), mapOutlineById(snapshot));
1440
+ const resolved = resolveItemSummary(identifier, mapItemIndexById(snapshot), mapOutlineById(snapshot));
1309
1441
  if (!resolved) return;
1310
1442
  const resolvedIndex = rawItems.findIndex((item) => item.id === resolved.itemId);
1311
1443
  if (resolvedIndex < 0) return;
@@ -1329,11 +1461,12 @@ const buildItemValues = (snapshot, input) => {
1329
1461
  if (!resolved) return {
1330
1462
  identifier,
1331
1463
  found: false,
1332
- 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.`
1333
1465
  };
1334
1466
  const itemId = resolved.item.id;
1335
1467
  const rawItemOutput = toOutputValue(resolved.item, input.valueCharacterLimit);
1336
- const translationsOutput = toOutputValue(getTranslationsByLocaleForItem(rawSurvey, itemId), input.valueCharacterLimit);
1468
+ const translationsByLocale = getTranslationsByLocaleForItem(rawSurvey, itemId);
1469
+ const translationsOutput = toOutputValue(translationsByLocale, input.valueCharacterLimit);
1337
1470
  const translationsValue = "value" in translationsOutput && isRecord$1(translationsOutput.value) ? translationsOutput.value : void 0;
1338
1471
  return {
1339
1472
  identifier,
@@ -1420,7 +1553,7 @@ const outputSchema$3 = z.object({
1420
1553
  ordering: z.string().optional().describe("How results and their matches are ordered; this search output is not survey order."),
1421
1554
  results: z.array(searchResultSchema).optional()
1422
1555
  });
1423
- const STOP_WORDS = new Set([
1556
+ const STOP_WORDS = /* @__PURE__ */ new Set([
1424
1557
  "a",
1425
1558
  "about",
1426
1559
  "an",
@@ -1473,7 +1606,7 @@ const candidateItems = (snapshot) => {
1473
1606
  const rawItemsById = mapRawItemsById(snapshot);
1474
1607
  const seen = /* @__PURE__ */ new Set();
1475
1608
  const candidates = [];
1476
- for (const entry of summarizeKeyIndexEntries(snapshot.inspection.keyIndex, outlineById)) {
1609
+ for (const entry of summarizeItemIndexEntries(snapshot.inspection.itemIndex, outlineById)) {
1477
1610
  seen.add(entry.itemId);
1478
1611
  candidates.push({
1479
1612
  item: entry,
@@ -1497,8 +1630,8 @@ const searchItems = (snapshot, input) => {
1497
1630
  const structuralFields = [
1498
1631
  ["itemId", item.itemId],
1499
1632
  ["itemType", item.itemType],
1500
- ["key", item.key],
1501
- ["fullKey", item.fullKey],
1633
+ ["key", item.editorName],
1634
+ ["breadcrumb", item.breadcrumb],
1502
1635
  ["itemLabel", item.itemLabel],
1503
1636
  ["path", item.path?.join(" ")]
1504
1637
  ];
@@ -1540,7 +1673,7 @@ const searchItems = (snapshot, input) => {
1540
1673
  matches: matches.slice(0, MAX_MATCHES_PER_ITEM),
1541
1674
  score: matches.reduce((sum, match) => sum + match.score, 0)
1542
1675
  };
1543
- }).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));
1544
1677
  };
1545
1678
  const searchSurveyItems = (inputData, requestContext) => {
1546
1679
  const input = inputSchema$4.parse(inputData);
@@ -1559,7 +1692,7 @@ const searchSurveyItems = (inputData, requestContext) => {
1559
1692
  returned: page.items.length,
1560
1693
  nextCursor: page.nextCursor,
1561
1694
  truncated: page.truncated,
1562
- 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.",
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.",
1563
1696
  results: page.items
1564
1697
  }, "Survey-item search");
1565
1698
  };
@@ -1596,14 +1729,14 @@ const inputSchema$3 = z.object({
1596
1729
  "ui-context",
1597
1730
  "history"
1598
1731
  ])).min(1).max(6)).describe("For current-context: context sections to inspect. Empty means omitted."),
1599
- 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."),
1600
1733
  sourceLocale: optionalNonBlankString(40).describe("For localization: source locale to translate. Defaults to the selected editor locale."),
1601
1734
  targetLocale: optionalNonBlankString(40).describe("For localization: required target locale whose coverage should be inspected."),
1602
1735
  missingOnly: optionalInput(z.boolean()).describe("For localization: return only source surfaces that do not yet exist in the target locale."),
1603
- itemRef: optionalNonBlankString(240).describe("For current-context outline, item-details, or raw-json: item id, key, fullKey, visible label, or \"selected\"."),
1604
- cursor: optionalNonBlankString(240).describe("For current-context, search-items, or localization pagination: numeric cursor from a previous result."),
1605
- 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."),
1606
- 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.`),
1607
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."),
1608
1741
  includeChildren: optionalInput(z.boolean()).describe("For item-details: include direct survey-tree children."),
1609
1742
  includeItemConfig: optionalInput(z.boolean()).describe("For item-details: include choice/form/internal item config details."),
@@ -1654,6 +1787,7 @@ const truncateLocalizationText = (content) => {
1654
1787
  };
1655
1788
  const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1656
1789
  const survey = snapshot.survey;
1790
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1657
1791
  const sourceLocale = input.sourceLocale ?? snapshot.ui.selectedLocale;
1658
1792
  const sourceTranslations = survey.translations?.[sourceLocale];
1659
1793
  const targetTranslations = survey.translations?.[targetLocale];
@@ -1702,7 +1836,7 @@ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1702
1836
  }
1703
1837
  for (const item of survey.surveyItems) for (const [contentKey, sourceContent] of Object.entries(sourceTranslations.itemTranslations?.[item.id] ?? {})) addSurface({
1704
1838
  itemId: item.id,
1705
- itemKey: item.key,
1839
+ itemKey: names.get(item.id) ?? item.id,
1706
1840
  itemType: item.itemType,
1707
1841
  contentKey,
1708
1842
  sourceContent,
@@ -1885,7 +2019,9 @@ const surveyInspectTool = createTool({
1885
2019
  itemId,
1886
2020
  includeSurveyChildren: input.includeChildren ?? false,
1887
2021
  includeItemConfig: input.includeItemConfig ?? true,
1888
- childrenLimit: input.limit
2022
+ childrenLimit: input.limit,
2023
+ responseVariablesLimit: input.limit,
2024
+ cursor: input.cursor
1889
2025
  }, requestContext));
1890
2026
  if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
1891
2027
  const result = inspection.value;
@@ -1897,6 +2033,7 @@ const surveyInspectTool = createTool({
1897
2033
  };
1898
2034
  }
1899
2035
  if (input.action === "expression-diagnostics") {
2036
+ const visibilityIssues = getAssistantVisibilityIssues(createAssistantSurvey(snapshot.survey, snapshot.trustedCapabilities));
1900
2037
  const allDiagnostics = analyzeSurveyAssistantExpressionReferences(snapshot.survey, snapshot.trustedCapabilities);
1901
2038
  const itemId = input.itemRef ? resolveItemRef(input, snapshot) : void 0;
1902
2039
  const diagnostics = itemId ? allDiagnostics.filter((diagnostic) => diagnostic.ownerItemId === itemId) : allDiagnostics;
@@ -1911,7 +2048,9 @@ const surveyInspectTool = createTool({
1911
2048
  byCategory[diagnostic.category] = getSurveyAssistantExpressionDiagnosticGuidance(diagnostic);
1912
2049
  }
1913
2050
  const result = {
1914
- valid: blockingCount === 0,
2051
+ valid: blockingCount === 0 && visibilityIssues.length === 0,
2052
+ visibilityIssues,
2053
+ visibilityScope: "survey",
1915
2054
  count: diagnostics.length,
1916
2055
  blockingCount,
1917
2056
  advisoryCount,
@@ -1922,7 +2061,7 @@ const surveyInspectTool = createTool({
1922
2061
  return {
1923
2062
  ok: true,
1924
2063
  action: input.action,
1925
- modelSummary: 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)}`,
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)}`,
1926
2065
  result
1927
2066
  };
1928
2067
  }
@@ -1975,7 +2114,7 @@ const surveyInspectTool = createTool({
1975
2114
  inspectedPathCount: boundedPaths?.length ?? 0,
1976
2115
  ignoredPathCount,
1977
2116
  omittedPathCount,
1978
- ...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) } : {},
1979
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."
1980
2119
  } : void 0;
1981
2120
  const result = requestCoverage && isRecord(rawResult) ? {
@@ -1995,33 +2134,45 @@ const surveyInspectTool = createTool({
1995
2134
  //#endregion
1996
2135
  //#region src/server/tools/survey-attachment-tool.ts
1997
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
+ });
1998
2171
  const surveyAttachmentTool = createTool({
1999
2172
  id: TOOL_ID$2,
2000
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.",
2001
- inputSchema: z.object({
2002
- 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."),
2003
- documentRequests: z.array(z.object({
2004
- documentId: z.string().min(1),
2005
- pages: z.object({
2006
- start: z.number().int().min(1).max(1e5),
2007
- end: z.number().int().min(1).max(1e5)
2008
- }).optional().describe("One-based inclusive PDF page range to prioritize."),
2009
- focus: z.string().min(1).max(300).optional().describe("Short section, question range, or fact to prioritize in the original file.")
2010
- })).min(1).max(4).optional().describe("Stored attachments with an optional bounded reading focus.")
2011
- }),
2012
- outputSchema: z.object({
2013
- loaded: z.array(z.object({
2014
- id: z.string(),
2015
- filename: z.string(),
2016
- mediaType: z.string(),
2017
- pages: z.object({
2018
- start: z.number().int(),
2019
- end: z.number().int()
2020
- }).optional(),
2021
- focus: z.string().optional()
2022
- })),
2023
- warnings: z.array(z.string()).optional()
2024
- }),
2174
+ inputSchema: inputSchema$2,
2175
+ outputSchema: outputSchema$1,
2025
2176
  mcp: { annotations: {
2026
2177
  title: "Load chat attachment",
2027
2178
  readOnlyHint: true,
@@ -2030,8 +2181,8 @@ const surveyAttachmentTool = createTool({
2030
2181
  openWorldHint: false
2031
2182
  } },
2032
2183
  execute: async (inputData, context) => {
2033
- const access = (context?.requestContext)?.get("attachmentAccess");
2034
- if (!access) return { loaded: [] };
2184
+ const requestContext = context?.requestContext;
2185
+ const access = requestContext?.get("attachmentAccess");
2035
2186
  const warnings = [];
2036
2187
  const requests = /* @__PURE__ */ new Map();
2037
2188
  for (const documentId of inputData.documentIds ?? []) requests.set(documentId, {});
@@ -2047,11 +2198,50 @@ const surveyAttachmentTool = createTool({
2047
2198
  });
2048
2199
  }
2049
2200
  const requestedIds = [...requests.keys()];
2050
- const authorized = access.documents.filter((document) => requestedIds.includes(document.id));
2051
- const attachments = await access.load(authorized.map((document) => document.id));
2052
- 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());
2053
2243
  return {
2054
- loaded: authorized.map((document) => {
2244
+ loaded: authorized.filter((document) => selected.has(document.id)).map((document) => {
2055
2245
  const request = requests.get(document.id);
2056
2246
  const pages = document.mediaType === "application/pdf" ? request?.pages : void 0;
2057
2247
  if (request?.pages && !pages) warnings.push(`Ignored a PDF page focus for non-PDF attachment ${document.name}.`);
@@ -2063,6 +2253,7 @@ const surveyAttachmentTool = createTool({
2063
2253
  ...request?.focus ? { focus: request.focus } : {}
2064
2254
  };
2065
2255
  }),
2256
+ ...unavailable.length > 0 ? { unavailable } : {},
2066
2257
  ...warnings.length > 0 ? { warnings } : {}
2067
2258
  };
2068
2259
  },
@@ -2277,7 +2468,7 @@ const surveyAssetTool = createTool({
2277
2468
  const manifest = access?.documents.find((entry) => entry.id === input.documentId);
2278
2469
  if (!access || !manifest) throw new Error(`Attachment "${input.documentId}" is not available for this turn.`);
2279
2470
  if (!manifest.mediaType.startsWith("image/")) throw new Error(`Attachment "${manifest.name}" is not an image.`);
2280
- const [loaded] = await access.load([input.documentId]);
2471
+ const loaded = (await access.load([input.documentId])).find((document) => document.documentId === input.documentId);
2281
2472
  if (!loaded) throw new Error(`Attachment "${manifest.name}" could not be loaded.`);
2282
2473
  const base = input.assetId ?? slug(manifest.name);
2283
2474
  let assetId = base;
@@ -2378,4 +2569,4 @@ const surveyAssistantTools = Object.freeze({
2378
2569
  //#endregion
2379
2570
  export { surveyAuditTool as i, surveyAssistantTools as n, surveyAssetTool as r, SURVEY_ASSISTANT_TOOL_IDS as t };
2380
2571
 
2381
- //# sourceMappingURL=tools-BvSovehT.mjs.map
2572
+ //# sourceMappingURL=tools-CEsv-WoO.mjs.map