@case-framework/survey-assistant 0.7.0 → 0.9.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 (142) 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-CmwLc_C8.mjs} +49 -32
  5. package/dist/authoring-references-CmwLc_C8.mjs.map +1 -0
  6. package/dist/capabilities-D55dq-fO.mjs +582 -0
  7. package/dist/capabilities-D55dq-fO.mjs.map +1 -0
  8. package/dist/capabilities-default.d.mts +5 -12
  9. package/dist/capabilities-default.d.mts.map +1 -1
  10. package/dist/capabilities-default.mjs +2 -411
  11. package/dist/capabilities-z95e1mti.d.mts +117 -0
  12. package/dist/capabilities-z95e1mti.d.mts.map +1 -0
  13. package/dist/{constants-0UHGFcRJ.mjs → constants-Ct-vv9cu.mjs} +1 -1
  14. package/dist/{constants-0UHGFcRJ.mjs.map → constants-Ct-vv9cu.mjs.map} +1 -1
  15. package/dist/{constants-Be91Gay9.d.mts → constants-HL_klgJl.d.mts} +7 -28
  16. package/dist/constants-HL_klgJl.d.mts.map +1 -0
  17. package/dist/{controller-proxy-BH05e-C2.mjs → controller-proxy-BQ1YsVxJ.mjs} +12 -21
  18. package/dist/controller-proxy-BQ1YsVxJ.mjs.map +1 -0
  19. package/dist/controller-proxy-Cmzdr9tV.d.mts +65 -0
  20. package/dist/controller-proxy-Cmzdr9tV.d.mts.map +1 -0
  21. package/dist/default-D22AKTqE.mjs +233 -0
  22. package/dist/default-D22AKTqE.mjs.map +1 -0
  23. package/dist/{digest-SJmoYVaK.d.mts → digest-FnTTDBq2.d.mts} +2 -3
  24. package/dist/digest-FnTTDBq2.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-D950TfvC.mjs +3098 -0
  28. package/dist/engine-D950TfvC.mjs.map +1 -0
  29. package/dist/engine.d.mts +4 -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-l_EK4yK7.d.mts → index-B8TLBd1G.d.mts} +18 -72
  34. package/dist/index-B8TLBd1G.d.mts.map +1 -0
  35. package/dist/index-CaX3hZmr.d.mts +23813 -0
  36. package/dist/index-CaX3hZmr.d.mts.map +1 -0
  37. package/dist/{index-x0qprHgK.d.mts → index-CeUlGkoU.d.mts} +58 -28
  38. package/dist/index-CeUlGkoU.d.mts.map +1 -0
  39. package/dist/{index-nhrgbgO5.d.mts → index-DYHdvSE8.d.mts} +347 -88
  40. package/dist/index-DYHdvSE8.d.mts.map +1 -0
  41. package/dist/lifecycle-DFzxVxpq.d.mts +54346 -0
  42. package/dist/lifecycle-DFzxVxpq.d.mts.map +1 -0
  43. package/dist/{memory-thread-repository-KWHQWFEW.mjs → memory-thread-repository-DmLadygY.mjs} +60 -30
  44. package/dist/memory-thread-repository-DmLadygY.mjs.map +1 -0
  45. package/dist/protocol-BQ7uIDYT.mjs +569 -0
  46. package/dist/protocol-BQ7uIDYT.mjs.map +1 -0
  47. package/dist/protocol.d.mts +4 -4
  48. package/dist/protocol.mjs +4 -3
  49. package/dist/{react-B3ybjJ-X.mjs → react-DybJ5pWw.mjs} +159 -46
  50. package/dist/react-DybJ5pWw.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 +224 -180
  56. package/dist/references/core-rules.md +6 -3
  57. package/dist/references/element-types.md +189 -0
  58. package/dist/references/expressions.md +307 -168
  59. package/dist/references/fair-by-design.md +9 -9
  60. package/dist/references/follow-ups.md +151 -0
  61. package/dist/references/localization.md +26 -12
  62. package/dist/references/response-variables.md +38 -0
  63. package/dist/references/rich-text-content.md +22 -43
  64. package/dist/references/source-material-surveys.md +20 -24
  65. package/dist/references/survey-data-model.md +36 -102
  66. package/dist/{request-context-CG4SWijt.mjs → request-context-KVh8VjgH.mjs} +4 -10
  67. package/dist/request-context-KVh8VjgH.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 +62 -43
  71. package/dist/server-agent.d.mts.map +1 -1
  72. package/dist/server-agent.mjs +73 -40
  73. package/dist/server-agent.mjs.map +1 -1
  74. package/dist/server-runtime.d.mts +24 -42
  75. package/dist/server-runtime.d.mts.map +1 -1
  76. package/dist/server-runtime.mjs +12 -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 +14 -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-Coh5N027.mjs +397 -0
  91. package/dist/tasks-Coh5N027.mjs.map +1 -0
  92. package/dist/{tasks-CYqrcaRO.d.mts → tasks-wb9M-bak.d.mts} +25 -41
  93. package/dist/tasks-wb9M-bak.d.mts.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-DBAaU7BA.d.mts → thread-handlers-C3Iya92v.d.mts} +9 -40
  97. package/dist/thread-handlers-C3Iya92v.d.mts.map +1 -0
  98. package/dist/{tools-BvSovehT.mjs → tools-D0KBDolR.mjs} +677 -459
  99. package/dist/tools-D0KBDolR.mjs.map +1 -0
  100. package/dist/turn-survey-draft-B34lggSG.d.mts +185 -0
  101. package/dist/turn-survey-draft-B34lggSG.d.mts.map +1 -0
  102. package/dist/ui.css +1 -1
  103. package/dist/ui.d.mts +15 -54
  104. package/dist/ui.d.mts.map +1 -1
  105. package/dist/ui.mjs +203 -156
  106. package/dist/ui.mjs.map +1 -1
  107. package/docs/integration.md +11 -1
  108. package/package.json +41 -36
  109. package/dist/authoring-references-BJQ_1nMX.mjs.map +0 -1
  110. package/dist/capabilities-1KNnNO1_.mjs +0 -73
  111. package/dist/capabilities-1KNnNO1_.mjs.map +0 -1
  112. package/dist/capabilities-DVTfogjv.d.mts +0 -193
  113. package/dist/capabilities-DVTfogjv.d.mts.map +0 -1
  114. package/dist/capabilities-default.mjs.map +0 -1
  115. package/dist/constants-Be91Gay9.d.mts.map +0 -1
  116. package/dist/controller-proxy-BH05e-C2.mjs.map +0 -1
  117. package/dist/controller-proxy-Bd0ZHBPF.d.mts +0 -85
  118. package/dist/controller-proxy-Bd0ZHBPF.d.mts.map +0 -1
  119. package/dist/digest-SJmoYVaK.d.mts.map +0 -1
  120. package/dist/engine-Bo85Oyj0.mjs +0 -6723
  121. package/dist/engine-Bo85Oyj0.mjs.map +0 -1
  122. package/dist/index-Tnqv-yKW.d.mts +0 -777
  123. package/dist/index-Tnqv-yKW.d.mts.map +0 -1
  124. package/dist/index-l_EK4yK7.d.mts.map +0 -1
  125. package/dist/index-nhrgbgO5.d.mts.map +0 -1
  126. package/dist/index-x0qprHgK.d.mts.map +0 -1
  127. package/dist/lifecycle-Bx0aq_4S.d.mts +0 -529
  128. package/dist/lifecycle-Bx0aq_4S.d.mts.map +0 -1
  129. package/dist/memory-thread-repository-KWHQWFEW.mjs.map +0 -1
  130. package/dist/protocol-BhzQbx81.mjs +0 -1173
  131. package/dist/protocol-BhzQbx81.mjs.map +0 -1
  132. package/dist/react-B3ybjJ-X.mjs.map +0 -1
  133. package/dist/references/embedded-forms.md +0 -126
  134. package/dist/references/item-types.md +0 -407
  135. package/dist/request-context-CG4SWijt.mjs.map +0 -1
  136. package/dist/request-guardrails-TmgQtqp4.mjs.map +0 -1
  137. package/dist/server-tasks.mjs.map +0 -1
  138. package/dist/tasks-CYqrcaRO.d.mts.map +0 -1
  139. package/dist/thread-documents-rmd6nyX9.d.mts +0 -107
  140. package/dist/thread-documents-rmd6nyX9.d.mts.map +0 -1
  141. package/dist/thread-handlers-DBAaU7BA.d.mts.map +0 -1
  142. package/dist/tools-BvSovehT.mjs.map +0 -1
@@ -1,11 +1,15 @@
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 { f as surveyChangeSetSchema } from "./protocol-BQ7uIDYT.mjs";
2
+ import { E as surveyOperationSchemasByKind, T as surveyOperationSchema, a as SURVEY_OPERATION_KINDS, r as assistantElementTypeManifestSchema } from "./capabilities-D55dq-fO.mjs";
3
+ import { stableStringify } from "./digest.mjs";
4
+ import { A as itemSummarySchema, B as summarizeItemIndexEntries, D as getRawSurveyItems, E as getRawSurvey, G as unique, H as summarizeOutlineNode, I as resolveItemSummary, K as buildExpressionMutationBatch, L as sliceWithCursor, M as mapOutlineById, N as mapRawItemsById, O as itemConfigDetailsSchema, P as normalizeText, R as summarizeFocusItemNode, T as getItemIndexEntries, U as summarizeRawItem, W as translationEntrySchema, X as getSurveyAssistantExpressionDiagnosticGuidance, Y as analyzeSurveyAssistantExpressionReferences, a as collectExpressionMutationRecoveryTargets, b as buildItemTranslationEntries, c as auditSurvey, f as createSurveyChangeSet, h as normalizeSurveyChangeOperations, i as collectExpressionMutationRecoveryRefinements, j as mapItemIndexById, k as itemMatchesQuery, nt as createAssistantSurvey, o as collectMalformedSurveyOperationRecoveryTargets, q as expressionMutationSchema, rt as getAssistantVisibilityIssues, s as collectSurveyOperationRecoveryTargets, v as buildConfigSearchText, w as findDirectChildrenByParentId, x as clampLimit, y as buildItemConfigDetails, z as summarizeItemById } from "./engine-D950TfvC.mjs";
5
+ import { c as getDeterministicCreateElementTypes, l as inspectAssistantElement, u as resolveAssistantCapabilityReference } from "./default-D22AKTqE.mjs";
6
+ import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-KVh8VjgH.mjs";
7
+ import { a as surveyAuthoringReferences, i as surveyAuthoringReferenceTopics, r as getBuiltInElementTypeAuthoringReference, t as builtInSurveyAuthoringElementTypes } from "./authoring-references-CmwLc_C8.mjs";
6
8
  import { z } from "zod";
7
- import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getContentPlainText, getValueAtJsonPointer, isBlockingSurveyExpressionReferenceDiagnostic } from "@case-framework/survey-core";
8
- import { randomUUID } from "node:crypto";
9
+ import { stringRecord } from "@case-framework/survey-core/schema";
10
+ import { SURVEY_EDITOR_ITEM_COLORS } from "@case-framework/survey-core/editor/colors";
11
+ import { JsonPatchError, SURVEY_LOCALIZATION_SURFACES as SURVEY_LOCALIZATION_SURFACES$1, encodeJsonPointerSegment, getContentPlainText, getLocalizationEntryKey, getValueAtJsonPointer, hasLocalizationText, listLocalizationEntries, ownValue, schemaAnnotation, setOwnValue } from "@case-framework/survey-core";
12
+ import { createHash, randomUUID } from "node:crypto";
9
13
  import { createTool, isValidationError } from "@mastra/core/tools";
10
14
  //#region src/server/operation-contract-reference.ts
11
15
  const isSurveyOperationKind = (value) => SURVEY_OPERATION_KINDS.includes(value);
@@ -19,64 +23,97 @@ const summarizeLargeSchema = (schema, propertyName) => {
19
23
  description: `${description} The deeply nested schema is omitted from this compact reference; load the dedicated authoring reference when this structured value is needed.`
20
24
  };
21
25
  };
22
- const compactJsonSchema = (value, propertyName, depth = 0) => {
23
- if (Array.isArray(value)) return value.map((entry) => compactJsonSchema(entry, propertyName, depth + 1));
24
- if (!isRecord$2(value)) return value;
25
- if (typeof value.$ref === "string") return { description: "Recursive structured value omitted from this compact reference; request full detail only for protocol debugging." };
26
- if (depth >= 3 && JSON.stringify(value).length > 12e3) return summarizeLargeSchema(value, propertyName);
27
- const compact = {};
28
- for (const key of [
29
- "type",
30
- "const",
31
- "enum",
32
- "description",
33
- "minimum",
34
- "maximum",
35
- "minLength",
36
- "maxLength",
37
- "minItems",
38
- "maxItems",
39
- "required",
40
- "additionalProperties"
41
- ]) if (value[key] !== void 0) compact[key] = value[key];
42
- if (isRecord$2(value.properties)) compact.properties = Object.fromEntries(Object.entries(value.properties).map(([key, schema]) => [key, compactJsonSchema(schema, key, depth + 1)]));
43
- if (value.items !== void 0) compact.items = compactJsonSchema(value.items, propertyName, depth + 1);
44
- for (const unionKey of ["oneOf", "anyOf"]) if (Array.isArray(value[unionKey])) compact[unionKey] = value[unionKey].map((entry) => compactJsonSchema(entry, propertyName, depth + 1));
45
- return compact;
26
+ /** Compact explanatory material without deleting the grammar behind recursive references. */
27
+ const compactJsonSchema = (root) => {
28
+ const targets = /* @__PURE__ */ new Set();
29
+ const collect = (value) => {
30
+ if (!value || typeof value !== "object") return;
31
+ if ("$ref" in value && typeof value.$ref === "string" && value.$ref.startsWith("#")) targets.add(decodeURIComponent(value.$ref.slice(1)));
32
+ Object.values(value).forEach(collect);
33
+ };
34
+ collect(root);
35
+ const segment = (key) => String(key).replace(/~/g, "~0").replace(/\//g, "~1");
36
+ const compact = (value, pointer = "", propertyName, depth = 0) => {
37
+ if (targets.has(pointer)) return value;
38
+ if (Array.isArray(value)) return value.map((entry, index) => compact(entry, `${pointer}/${index}`, propertyName, depth + 1));
39
+ if (!isRecord$2(value)) return value;
40
+ if (typeof value.$ref === "string") return value;
41
+ if (![...targets].some((target) => target.startsWith(`${pointer}/`)) && depth >= 3 && JSON.stringify(value).length > 12e3) return summarizeLargeSchema(value, propertyName);
42
+ const result = {};
43
+ for (const key of [
44
+ "type",
45
+ "const",
46
+ "enum",
47
+ "description",
48
+ "minimum",
49
+ "maximum",
50
+ "exclusiveMinimum",
51
+ "exclusiveMaximum",
52
+ "multipleOf",
53
+ "pattern",
54
+ "format",
55
+ "minLength",
56
+ "maxLength",
57
+ "minItems",
58
+ "maxItems",
59
+ "uniqueItems",
60
+ "minProperties",
61
+ "maxProperties",
62
+ "required",
63
+ "additionalProperties"
64
+ ]) if (value[key] !== void 0) result[key] = value[key];
65
+ if (isRecord$2(value.properties)) result.properties = Object.fromEntries(Object.entries(value.properties).map(([key, schema]) => [key, compact(schema, `${pointer}/properties/${segment(key)}`, key, depth + 1)]));
66
+ if (value.items !== void 0) result.items = compact(value.items, `${pointer}/items`, propertyName, depth + 1);
67
+ for (const key of [
68
+ "oneOf",
69
+ "anyOf",
70
+ "allOf"
71
+ ]) if (Array.isArray(value[key])) result[key] = value[key].map((entry, index) => compact(entry, `${pointer}/${key}/${index}`, propertyName, depth + 1));
72
+ for (const key of ["$defs", "definitions"]) if (value[key] !== void 0) result[key] = value[key];
73
+ return result;
74
+ };
75
+ return compact(root);
46
76
  };
47
- const formatItemCapabilityProfile = (manifest, kind) => {
48
- const fixedSurfaces = manifest.contentSurfaces.filter((surface) => !surface.pattern);
49
- const patternedSurfaces = manifest.contentSurfaces.filter((surface) => surface.pattern);
50
- const requiredSurfaces = fixedSurfaces.filter((surface) => surface.requiredOnCreate);
51
- const lines = [
52
- `## Item capability: \`${manifest.itemType}\``,
53
- "",
54
- `${manifest.displayName}: ${manifest.description ?? "No additional description."}`,
55
- `Creation mode: ${manifest.create}.`,
56
- `Supported operations: ${manifest.supportedOperations.join(", ")}.`,
77
+ const formatElementCapabilityProfile = (manifest, kind) => {
78
+ return [
79
+ `## Element capability: ${manifest.elementType}`,
80
+ `${manifest.displayName}: ${manifest.description ?? ""}`,
81
+ `Creation mode: ${manifest.create}. Supported operations: ${manifest.supportedOperations.join(", ")}.`,
57
82
  manifest.authoring?.compactHint,
58
- manifest.authoring?.configSummary ? `Configuration: ${manifest.authoring.configSummary}` : void 0,
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,
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({
83
+ manifest.authoring?.configSummary,
84
+ manifest.creationPresets?.length ? `Creation presets: ${manifest.creationPresets.map((preset) => preset.id).join(", ")}.` : void 0,
85
+ manifest.referenceTopics?.length ? `Additional reference topics: ${manifest.referenceTopics.join(", ")}. Load a topic with survey_authoring_reference: ${manifest.referenceTopics.map((topic) => JSON.stringify({
64
86
  topic,
65
- itemType: manifest.itemType
66
- })).join("; ")}.` : void 0
67
- ].filter((line) => Boolean(line));
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
- return lines.join("\n");
87
+ elementType: manifest.elementType
88
+ })).join("; ")}.` : void 0,
89
+ "Configuration, identity, expressions and owned components come from the trusted registry's canonical schema. Inspect item details for actual owners, roles, slots and paths. Keep existing IDs during edits."
90
+ ].filter((line) => Boolean(line)).join("\n\n");
70
91
  };
92
+ const formatElementContract = (schema, detail = "compact") => [
93
+ "## Canonical element schema",
94
+ "This is the trusted element schema. patch-element-config paths are relative to its config property. The envelope also carries placement, response settings, prefills and conditions. Inspect owned roles for translated text; preserve surviving IDs.",
95
+ "Objects with x-survey-content-roles own localized content under their id. Use those exact role names and inline/block policies in ownerId/role translation entries; do not put translated wording in configuration fields.",
96
+ "```json",
97
+ JSON.stringify(z.toJSONSchema(schema, {
98
+ io: "input",
99
+ reused: "ref",
100
+ unrepresentable: "any",
101
+ override: ({ zodSchema, jsonSchema }) => {
102
+ const annotation = schemaAnnotation(zodSchema);
103
+ if (annotation?.kind === "owner") jsonSchema["x-survey-content-roles"] = annotation.roles;
104
+ }
105
+ }), null, detail === "full" ? 2 : void 0),
106
+ "```"
107
+ ].join("\n\n");
71
108
  const formatSurveyOperationContractReference = (kind, options = {}) => {
72
109
  const { $schema: _schemaVersion, ...contract } = z.toJSONSchema(surveyOperationSchemasByKind[kind], {
73
110
  io: "input",
74
- reused: "inline",
111
+ reused: "ref",
75
112
  unrepresentable: "any"
76
113
  });
77
114
  const detail = options.detail ?? "compact";
78
115
  const renderedContract = detail === "full" ? contract : compactJsonSchema(contract);
79
- const itemCapability = options.itemCapability;
116
+ const elementCapability = options.elementCapability;
80
117
  return [
81
118
  `# \`${kind}\` operation contract`,
82
119
  "",
@@ -84,12 +121,23 @@ const formatSurveyOperationContractReference = (kind, options = {}) => {
84
121
  "the canonical post-normalization field shape. Cross-field and snapshot-dependent rules are",
85
122
  "enforced when the proposal is validated. Follow focused authoring guidance when it recommends",
86
123
  "a supported model-facing shorthand; otherwise use this canonical shape.",
87
- 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.",
124
+ 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, follow-up, or element-type reference for authoring guidance." : "This full view is intended for protocol debugging and may be large.",
88
125
  "",
126
+ ...[
127
+ "create-item",
128
+ "insert-element",
129
+ "insert-blocks",
130
+ "patch-element-config",
131
+ "patch-item-config",
132
+ "update-content",
133
+ "update-survey-translation"
134
+ ].includes(kind) ? ["Localized content accepts exactly one of plainText, canonical content, or richTextBlocks shorthand. Load rich-text-content for the full block grammar. Translation entries use locale, ownerId and role; survey surfaces use their declared contentKey."] : [],
135
+ ...kind === "survey-json-patch" ? ["Use this fallback only for exact inspected paths when no typed operation fits. It must be the only operation in a delta. Preserve existing localized values and schema identity; use survey_expression for expressions."] : [],
89
136
  "```json",
90
- JSON.stringify(renderedContract, null, 2),
137
+ JSON.stringify(renderedContract, null, detail === "full" ? 2 : void 0),
91
138
  "```",
92
- ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : []
139
+ ...elementCapability ? ["", formatElementCapabilityProfile(elementCapability, kind)] : [],
140
+ ...options.elementSchema && (kind === "patch-element-config" || kind === "insert-element" || kind === "insert-preset" || kind === "create-item") ? [formatElementContract(options.elementSchema, detail)] : []
93
141
  ].join("\n");
94
142
  };
95
143
  //#endregion
@@ -167,7 +215,7 @@ const TOOL_ID$6 = "survey_authoring_reference";
167
215
  const topicSchema = z.string().min(1).max(120);
168
216
  const inputSchema$10 = z.object({
169
217
  topic: topicSchema.describe("The narrow authoring reference to load."),
170
- itemType: z.string().min(1).max(120).optional().describe("Installed item type to focus. With topic \"item-types\", returns only that item type section instead of the combined reference."),
218
+ elementType: z.string().min(1).max(120).optional().describe("Installed element type to focus. With topic \"element-types\", returns only that element type section instead of the combined reference."),
171
219
  operationKind: z.string().min(1).max(120).optional().describe("For topic assistant-operations, return only this operation's generated canonical field contract."),
172
220
  detail: z.enum(SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS).optional().describe("Contract detail for assistant-operations. Use \"compact\" for authoring and \"full\" only for protocol debugging."),
173
221
  reason: z.string().max(500).optional().describe("Short reason this reference is needed.")
@@ -175,7 +223,7 @@ const inputSchema$10 = z.object({
175
223
  const outputSchema$8 = z.object({
176
224
  ok: z.boolean(),
177
225
  topic: topicSchema,
178
- itemType: z.string().optional(),
226
+ elementType: z.string().optional(),
179
227
  operationKind: z.string().optional(),
180
228
  detail: z.enum(SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS).optional(),
181
229
  title: z.string(),
@@ -187,7 +235,7 @@ const formatCanonicalNameRecovery = (input, candidates, noun) => {
187
235
  };
188
236
  const surveyAuthoringReferenceTool = createTool({
189
237
  id: TOOL_ID$6,
190
- 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.",
238
+ description: "Load one narrow CASE authoring reference. With topic \"element-types\", pass elementType to load only that type. For a survey_change operation, use topic \"assistant-operations\" with operationKind and optionally elementType to compose the structural contract with that installed element capability. Compact detail is the authoring default; full detail is only for protocol debugging.",
191
239
  inputSchema: inputSchema$10,
192
240
  outputSchema: outputSchema$8,
193
241
  mcp: { annotations: {
@@ -200,12 +248,20 @@ const surveyAuthoringReferenceTool = createTool({
200
248
  execute: async (inputData, context) => {
201
249
  const contractDetail = inputData.detail ?? "compact";
202
250
  const engineContext = getSurveyAssistantEngineContext(context?.requestContext);
203
- const itemCapability = inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.manifest ?? engineContext?.capabilities.itemTypes.find((entry) => entry.itemType === inputData.itemType) : void 0;
204
- const availableItemTypes = [...builtInSurveyAuthoringItemTypes, ...engineContext?.capabilities.itemTypes.map((entry) => entry.itemType) ?? []];
251
+ const elementCapability = inputData.elementType ? engineContext?.trustedCapabilities?.byElementType.get(inputData.elementType)?.manifest ?? engineContext?.capabilities.elementTypes.find((entry) => entry.elementType === inputData.elementType) : void 0;
252
+ const availableElementTypes = engineContext ? engineContext.capabilities.elementTypes.map((entry) => entry.elementType) : builtInSurveyAuthoringElementTypes;
253
+ if (inputData.elementType && engineContext && !elementCapability) return {
254
+ ok: false,
255
+ topic: inputData.topic,
256
+ elementType: inputData.elementType,
257
+ operationKind: inputData.operationKind,
258
+ title: "Element capability unavailable",
259
+ content: `No installed survey element capability named "${inputData.elementType}" is available.` + formatCanonicalNameRecovery(inputData.elementType, availableElementTypes, "element type")
260
+ };
205
261
  if (inputData.operationKind && inputData.topic !== "assistant-operations") return {
206
262
  ok: false,
207
263
  topic: inputData.topic,
208
- itemType: inputData.itemType,
264
+ elementType: inputData.elementType,
209
265
  operationKind: inputData.operationKind,
210
266
  title: "Operation contract unavailable",
211
267
  content: "operationKind is supported only with topic assistant-operations."
@@ -219,61 +275,65 @@ const surveyAuthoringReferenceTool = createTool({
219
275
  title: "Operation contract unavailable",
220
276
  content: `No built-in survey_change operation named "${inputData.operationKind}" is available.` + formatCanonicalNameRecovery(inputData.operationKind, SURVEY_OPERATION_KINDS, "operation")
221
277
  };
222
- if (inputData.itemType && !itemCapability && !getBuiltInItemTypeAuthoringReference(inputData.itemType)) return {
278
+ if (inputData.elementType && !elementCapability && !getBuiltInElementTypeAuthoringReference(inputData.elementType)) return {
223
279
  ok: false,
224
280
  topic: inputData.topic,
225
- itemType: inputData.itemType,
281
+ elementType: inputData.elementType,
226
282
  operationKind: inputData.operationKind,
227
283
  detail: contractDetail,
228
284
  title: "Item capability unavailable",
229
- content: `No installed survey item capability named "${inputData.itemType}" is available.` + formatCanonicalNameRecovery(inputData.itemType, availableItemTypes, "item type")
285
+ content: `No installed survey item capability named "${inputData.elementType}" is available.` + formatCanonicalNameRecovery(inputData.elementType, availableElementTypes, "item type")
230
286
  };
231
- if (itemCapability && !itemCapability.supportedOperations.includes(inputData.operationKind)) return {
287
+ if (elementCapability && !elementCapability.supportedOperations.includes(inputData.operationKind)) return {
232
288
  ok: false,
233
289
  topic: inputData.topic,
234
- itemType: inputData.itemType,
290
+ elementType: inputData.elementType,
235
291
  operationKind: inputData.operationKind,
236
292
  detail: contractDetail,
237
293
  title: "Operation unsupported for item type",
238
- content: `Item type "${itemCapability.itemType}" does not support survey_change operation "${inputData.operationKind}".`
294
+ content: `Item type "${elementCapability.elementType}" does not support survey_change operation "${inputData.operationKind}".`
239
295
  };
240
296
  return {
241
297
  ok: true,
242
298
  topic: inputData.topic,
243
- itemType: inputData.itemType,
299
+ elementType: inputData.elementType,
244
300
  operationKind: inputData.operationKind,
245
301
  detail: contractDetail,
246
- title: itemCapability ? `${inputData.operationKind} for ${itemCapability.displayName}` : `${inputData.operationKind} operation contract`,
302
+ title: elementCapability ? `${inputData.operationKind} for ${elementCapability.displayName}` : `${inputData.operationKind} operation contract`,
247
303
  content: formatSurveyOperationContractReference(inputData.operationKind, {
248
304
  detail: contractDetail,
249
- itemCapability
305
+ elementCapability,
306
+ elementSchema: inputData.elementType ? engineContext?.trustedCapabilities?.registry.get(inputData.elementType).schema : void 0
250
307
  })
251
308
  };
252
309
  }
253
310
  const builtInEntry = surveyAuthoringReferenceTopics.includes(inputData.topic) ? surveyAuthoringReferences[inputData.topic] : void 0;
254
- const capability = inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType) : void 0;
255
- const capabilityReference = inputData.itemType ? resolveAssistantCapabilityReference({
311
+ const capability = inputData.elementType ? engineContext?.trustedCapabilities?.byElementType.get(inputData.elementType) : void 0;
312
+ const capabilityReference = inputData.elementType ? resolveAssistantCapabilityReference({
256
313
  capabilities: engineContext?.trustedCapabilities,
257
- itemType: inputData.itemType,
314
+ elementType: inputData.elementType,
258
315
  topic: inputData.topic
259
316
  }) : void 0;
260
- const focusedBuiltInItemTypeReference = inputData.topic === "item-types" && inputData.itemType ? getBuiltInItemTypeAuthoringReference(inputData.itemType) : void 0;
261
- if (inputData.itemType && !focusedBuiltInItemTypeReference && !capability && !capabilityReference) return {
317
+ const focusedBuiltInElementTypeReference = inputData.topic === "element-types" && inputData.elementType ? getBuiltInElementTypeAuthoringReference(inputData.elementType) : void 0;
318
+ if (inputData.elementType && !focusedBuiltInElementTypeReference && !capability && !capabilityReference) return {
262
319
  ok: false,
263
320
  topic: inputData.topic,
264
- itemType: inputData.itemType,
321
+ elementType: inputData.elementType,
265
322
  title: "Item capability unavailable",
266
- content: `No installed survey item capability named "${inputData.itemType}" is available.` + formatCanonicalNameRecovery(inputData.itemType, availableItemTypes, "item type")
323
+ content: `No installed survey item capability named "${inputData.elementType}" is available.` + formatCanonicalNameRecovery(inputData.elementType, availableElementTypes, "item type")
267
324
  };
268
325
  const capabilityEntry = capabilityReference ? {
269
326
  title: capabilityReference.title ?? `${capability?.manifest.displayName} authoring`,
270
327
  content: capabilityReference.markdown
271
328
  } : void 0;
272
- const focusedCapabilityProfile = inputData.topic === "item-types" && itemCapability ? {
273
- title: itemCapability.displayName,
274
- content: formatItemCapabilityProfile(itemCapability)
329
+ const focusedCapabilityProfile = inputData.topic === "element-types" && elementCapability ? {
330
+ title: elementCapability.displayName,
331
+ content: formatElementCapabilityProfile(elementCapability)
275
332
  } : void 0;
276
- const baseEntry = inputData.topic === "item-types" && inputData.itemType ? focusedBuiltInItemTypeReference ?? focusedCapabilityProfile : builtInEntry;
333
+ const baseEntry = inputData.topic === "element-types" && inputData.elementType ? focusedBuiltInElementTypeReference && focusedCapabilityProfile ? {
334
+ ...focusedBuiltInElementTypeReference,
335
+ content: [focusedBuiltInElementTypeReference.content, focusedCapabilityProfile.content].join("\n\n")
336
+ } : focusedBuiltInElementTypeReference ?? focusedCapabilityProfile : builtInEntry;
277
337
  const entry = baseEntry && capabilityEntry ? {
278
338
  title: baseEntry.title,
279
339
  content: baseEntry.content === capabilityEntry.content ? baseEntry.content : [
@@ -286,18 +346,18 @@ const surveyAuthoringReferenceTool = createTool({
286
346
  if (!entry) return {
287
347
  ok: false,
288
348
  topic: inputData.topic,
289
- itemType: inputData.itemType,
349
+ elementType: inputData.elementType,
290
350
  operationKind: inputData.operationKind,
291
351
  title: "Reference unavailable",
292
- 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")
352
+ content: (inputData.elementType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.elementType}".` : `No built-in reference named "${inputData.topic}" is available.`) + formatCanonicalNameRecovery(inputData.topic, [...surveyAuthoringReferenceTopics, ...elementCapability?.referenceTopics ?? []], "reference")
293
353
  };
294
354
  return {
295
355
  ok: true,
296
356
  topic: inputData.topic,
297
- itemType: inputData.itemType,
357
+ elementType: inputData.elementType,
298
358
  operationKind: inputData.operationKind,
299
359
  title: entry.title,
300
- content: entry.content
360
+ content: inputData.topic === "element-types" && inputData.elementType && engineContext?.trustedCapabilities ? `${entry.content}\n\n${formatElementContract(engineContext.trustedCapabilities.registry.get(inputData.elementType).schema)}` : entry.content
301
361
  };
302
362
  },
303
363
  toModelOutput: (value) => toTextModelOutput(value, (output) => `# ${output.title}\n\n${output.content}`)
@@ -318,24 +378,18 @@ const summarizePendingRepairs = (event) => {
318
378
  //#region src/server/tools/survey-change-tool.ts
319
379
  const TOOL_ID$5 = "survey_change";
320
380
  const OPERATIONS_DESCRIPTION = [
321
- "Typed survey operation objects to validate and package as one undoable change. Every object uses discriminator \"kind\", never \"type\".",
322
- "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
- "Any translations owned by an operation must be inside that same operation object. Never put translations beside the survey_change operations array.",
324
- "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.",
326
- "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
- "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
- "Use remove-survey-locale with locale to destructively remove an existing survey language and all translations stored for it.",
329
- "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
- "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.",
333
- `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.",
335
- "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
- "When deleting referenced images and assets, include every reference-removal operation and remove-image-asset last in the same operations array.",
337
- "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.",
338
- "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."
381
+ "Typed operations use \"kind\". Load survey_authoring_reference topic=assistant-operations with operationKind for the exact generated schema.",
382
+ "Use insert-preset with elementType, presetId, locale and target {parentItemId,index?} for a familiar standalone question; target {layoutId,index?} adds it to an existing layout. Presets materialize fresh IDs once, before staging. Inspect the result before later edits.",
383
+ "For authored composition use create-item with a canonical content/group/page-break item, insert-element into a layout or insert-blocks into a body. Include ownerId/role/locale translations in the same operation. Sections are full-width vertical blocks, with one section level and one follow-up level enforced independently.",
384
+ "Use update-content with ref {scope:\"content\",ownerId,role} or a survey ref, locale and exactly one of plainText, content or richTextBlocks shorthand. content:null clears a value. Text belongs to declared owners, never flattened item keys. Keep all locales intact when moving content.",
385
+ "Use patch-element-config for choice options, matrix rows/cells and element settings. Paths are relative to the inspected config. Use move-element/move-block for moves and explicit ruleTransfer when required. Use survey_expression for expressions; never route them through generic configuration patches.",
386
+ "Use update-response-settings for variable names, matrix naming, option codes and exports. Never rename a single computed matrix slot.",
387
+ "Every reorder supplies all current children exactly once. Use move-item for flow moves and reorder-items for flow sibling order.",
388
+ `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.`,
389
+ "Image placement edits use update-placed-image with the same owner/survey ref. For imported bytes use survey_asset; remove placements before removing an asset.",
390
+ "Register an empty locale only if explicitly requested. Translation writes can introduce a locale; remove-survey-locale deletes its translations.",
391
+ "A raw survey-json-patch must be the only operation in a delta. Inspect its exact pointers first, preserve existing translations, and never change schema identity. All nested elements still require trusted authoring grants.",
392
+ "Successful deltas accumulate in the same turn draft; rejected deltas leave it unchanged and require repair of the same target. The live editor changes once, through its normal undo path, after successful completion. An empty operations array is only for a linked validator-confirmed no-op acknowledgment."
339
393
  ].join(" ");
340
394
  const inputSchema$9 = z.object({
341
395
  retryOfChangeIds: retryOfChangeIdsSchema,
@@ -362,39 +416,8 @@ const rejectedOperationParseEvent = ({ issueText, summary, turnId }) => ({
362
416
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
363
417
  });
364
418
  const describeOperationTarget = (operation) => {
365
- switch (operation.kind) {
366
- case "update-item-translation": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}`;
367
- case "update-placed-image": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}#${operation.imageIndex}`;
368
- case "update-item-label":
369
- case "update-item-color":
370
- case "update-item-key":
371
- case "patch-item-config":
372
- case "remove-item":
373
- case "move-item": return `${operation.kind} ${operation.itemId}`;
374
- case "upsert-form-field-group": return `${operation.kind} ${operation.target.itemId}:${operation.group.id}`;
375
- case "remove-form-field-group": return `${operation.kind} ${operation.target.itemId}:${operation.groupId}`;
376
- case "reorder-form-field-groups": return `${operation.kind} ${operation.target.itemId}`;
377
- case "upsert-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.field.id}`;
378
- case "remove-form-field":
379
- case "move-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.fieldId}`;
380
- case "set-embedded-form":
381
- 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}`;
383
- case "update-choice-option-label":
384
- case "remove-choice-option": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
385
- case "reorder-choice-options": return `${operation.kind} ${operation.itemId}`;
386
- case "reorder-items": return `${operation.kind} ${operation.parentItemId}`;
387
- case "update-survey-metadata":
388
- case "update-survey-pagination": return operation.kind;
389
- case "update-survey-translation": return `${operation.kind} ${operation.locale}:${operation.surface}:${operation.contentKey}`;
390
- case "add-survey-locale":
391
- case "remove-survey-locale": return `${operation.kind} ${operation.locale}`;
392
- case "set-image-asset":
393
- case "update-image-asset-metadata":
394
- 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}`;
396
- case "survey-json-patch": return `${operation.kind} ${operation.patches.map((patch) => patch.path).join(", ")}`;
397
- }
419
+ const target = "itemId" in operation ? operation.itemId : "elementId" in operation ? operation.elementId : "ownerId" in operation ? operation.ownerId : "ref" in operation ? JSON.stringify(operation.ref) : "assetId" in operation ? operation.assetId : "item" in operation ? operation.item.id : "target" in operation ? JSON.stringify(operation.target) : "bodyOwnerId" in operation ? operation.bodyOwnerId : "layoutId" in operation ? operation.layoutId : "parentItemId" in operation ? operation.parentItemId : "locale" in operation ? operation.locale : "survey";
420
+ return `${operation.kind} ${target}`;
398
421
  };
399
422
  const summarizeValidation = (event) => {
400
423
  const lines = [`${event.status === "validated" ? "Validated" : "Rejected"} survey change: ${event.summary}`];
@@ -410,11 +433,19 @@ const summarizeValidation = (event) => {
410
433
  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
434
  if (event.postChange) {
412
435
  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` : ""}.`);
436
+ 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
437
  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
438
  for (const item of event.postChange.touchedItems) {
416
- 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"}`);
439
+ const translations = item.translations.map(({ ownerId, role, locale, textLength, textPreview, truncated }) => truncated ? `${locale}:${ownerId}/${role}=${JSON.stringify(`${textPreview}…`)} [truncated preview; ${textLength} characters total]` : `${locale}:${ownerId}/${role}=${JSON.stringify(textPreview)}`).join("; ");
440
+ 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"}`);
441
+ if (item.placement) {
442
+ const p = item.placement;
443
+ 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." : ""}`);
444
+ }
445
+ if (item.childOrder) {
446
+ const order = item.childOrder;
447
+ 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." : ""}`);
448
+ }
418
449
  }
419
450
  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
451
  }
@@ -427,7 +458,7 @@ const summarizeValidation = (event) => {
427
458
  const location = [
428
459
  finding.itemId,
429
460
  finding.locale,
430
- finding.contentKey
461
+ finding.localizationRefs ? JSON.stringify(finding.localizationRefs) : void 0
431
462
  ].filter(Boolean).join(":");
432
463
  lines.push(`- ${finding.severity} ${finding.code}${location ? ` (${location})` : ""}: ${finding.messagePreview}${finding.truncated ? `… [truncated preview; ${finding.messageLength} characters total]` : ""}`);
433
464
  }
@@ -438,7 +469,6 @@ const summarizeValidation = (event) => {
438
469
  lines.push("Issues:");
439
470
  for (const issue of event.validation.issues) lines.push(`- ${issue.code}: ${issue.message}`);
440
471
  const issueCodes = new Set(event.validation.issues.map((issue) => issue.code));
441
- if (issueCodes.has("missing-create-content") || issueCodes.has("missing-field-groups-wrapper") || issueCodes.has("missing-choice-options") || issueCodes.has("missing-choice-option-labels") || issueCodes.has("missing-form-field-content") || issueCodes.has("missing-form-select-option-labels") || issueCodes.has("missing-form-field-labels") || issueCodes.has("missing-embedded-form-field-labels")) lines.push("Recovery hint: load the assistant-operations reference, then retry only the rejected delta. Put visible text in top-level translations[] beside item, config inside item.config, choice options in item.config.options, and form fields inside item.config.fieldGroups[].fields. Every choice option id needs a matching top-level translation whose contentKey is options.<optionId>.label.");
442
472
  if (issueCodes.has("too-many-operations")) lines.push("Recovery hint: keep the same inspected survey context and split this rejected delta into smaller survey_change calls. Each successful call extends the same turn draft.");
443
473
  if (issueCodes.has("item-already-in-target-parent")) lines.push("Recovery hint: remove unchanged same-parent move-item operations. To change sibling order within one parent, inspect its complete current children and use reorder-items instead.");
444
474
  }
@@ -500,12 +530,12 @@ const inputSchema$8 = z.object({
500
530
  retryOfChangeIds: retryOfChangeIdsSchema,
501
531
  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
532
  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."),
533
+ 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
534
  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
535
  limit: z.number().int().min(1).max(80).optional().describe("Context action only: rows per catalog page; defaults to 80."),
506
536
  includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item."),
507
537
  summary: z.string().max(500).optional().describe("Prepare action only: required user-facing summary."),
508
- 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.`)
538
+ 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-condition","ownerId":"<existing-owner-id>","property":"visibility","expression":{"type":"function","functionName":"eq","arguments":[...]}}. expression is CASE expression JSON, never an expression-language snippet.`)
509
539
  }).strict().superRefine((value, ctx) => {
510
540
  if (value.action === "context") {
511
541
  if (value.summary?.trim()) ctx.addIssue({
@@ -535,19 +565,31 @@ const contextOutputSchema = z.object({
535
565
  ok: z.boolean(),
536
566
  action: z.literal("context"),
537
567
  modelSummary: z.string(),
538
- listOrdering: z.record(z.string(), z.string()).describe("Meaning of each returned list. Expression references are not positional."),
568
+ listOrdering: stringRecord(z.string(), z.string()).describe("Meaning of each returned list. Expression references are not positional."),
539
569
  nextCursor: z.string().optional(),
540
570
  totalResponseSlots: z.number().int().nonnegative().optional(),
541
571
  surveyResponseSlotCount: z.number().int().nonnegative().optional(),
542
572
  totalExpressionLocations: z.number().int().nonnegative().optional(),
573
+ totalStateTargets: z.number().int().nonnegative().optional(),
574
+ stateTargets: z.array(z.object({
575
+ targetRef: z.object({ ownerId: z.string() }),
576
+ state: z.literal("disabled"),
577
+ itemId: z.string(),
578
+ kind: z.string()
579
+ })).optional(),
543
580
  unresolvedItemIds: z.array(z.string()).optional(),
544
581
  responseSlots: z.array(z.object({
545
- ref: z.string(),
546
- valueType: z.string().optional()
582
+ ref: z.object({
583
+ slotId: z.string(),
584
+ method: z.enum(["get", "isDefined"])
585
+ }),
586
+ valueType: z.string().optional(),
587
+ variableName: z.string().optional(),
588
+ itemId: z.string().optional()
547
589
  })),
548
590
  expressionLocations: z.array(z.object({
549
591
  itemId: z.string(),
550
- key: z.string(),
592
+ editorName: z.string(),
551
593
  itemType: z.string(),
552
594
  rawIndex: z.number().int().nonnegative(),
553
595
  locations: z.array(z.string())
@@ -558,7 +600,6 @@ const createIssue = ({ severity = "error", ...input }) => ({
558
600
  severity,
559
601
  ...input
560
602
  });
561
- const buildResponseSlotTypes = (snapshot) => snapshot.responseSlotTypes;
562
603
  const rejectedEvent = ({ issues, summary, turnId, warnings = [] }) => ({
563
604
  protocolVersion: 1,
564
605
  id: randomUUID(),
@@ -591,39 +632,52 @@ const summarizeDraftEvent = (event) => {
591
632
  };
592
633
  const buildContextOutput = (snapshot, input) => {
593
634
  const rawItems = getRawSurveyItems(snapshot);
635
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
594
636
  const resolvedItems = input.itemIds?.map((identifier) => ({
595
637
  identifier,
596
- itemId: snapshot.inspection.keyIndex.find((entry) => entry.itemId === identifier || entry.key === identifier || entry.fullKey === identifier)?.itemId
638
+ itemId: snapshot.inspection.itemIndex.find((entry) => entry.itemId === identifier)?.itemId
597
639
  }));
598
640
  const itemFilter = resolvedItems?.length ? new Set(resolvedItems.flatMap((entry) => entry.itemId ? [entry.itemId] : [])) : void 0;
599
641
  const unresolvedItemIds = resolvedItems?.filter((entry) => !entry.itemId).map((entry) => entry.identifier) ?? [];
600
- 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 } : {}
642
+ const allResponseSlots = snapshot.responseVariables.filter((slot) => !itemFilter || itemFilter.has(slot.itemId)).flatMap((slot) => [{
643
+ ref: {
644
+ slotId: slot.slotId,
645
+ method: "get"
646
+ },
647
+ valueType: slot.valueType,
648
+ variableName: slot.variableName,
649
+ itemId: slot.itemId
650
+ }, {
651
+ ref: {
652
+ slotId: slot.slotId,
653
+ method: "isDefined"
654
+ },
655
+ valueType: "boolean",
656
+ variableName: slot.variableName,
657
+ itemId: slot.itemId
658
+ }]);
659
+ const allStateTargets = [...snapshot.model.owners.values()].filter((owner) => !itemFilter || itemFilter.has(owner.itemId)).map((owner) => ({
660
+ targetRef: { ownerId: owner.id },
661
+ itemId: owner.itemId,
662
+ kind: owner.kind,
663
+ state: "disabled"
604
664
  }));
605
665
  const offset = Number(input.cursor ?? 0);
606
666
  const limit = input.limit ?? 80;
607
667
  const responseSlots = allResponseSlots.slice(offset, offset + limit);
608
668
  const allExpressionLocations = input.action === "context" && input.includeRawLocations === true ? rawItems.map((item, rawIndex) => {
609
- if (itemFilter && !itemFilter.has(item.id) && !itemFilter.has(item.key)) return;
610
- const locations = [
611
- item.displayConditions?.root ? "displayConditions.root" : void 0,
612
- ...Object.keys(item.displayConditions?.components ?? {}).map((componentId) => `displayConditions.components.${componentId}`),
613
- ...Object.keys(item.disabledConditions?.components ?? {}).map((componentId) => `disabledConditions.components.${componentId}`),
614
- ...Object.keys(item.validations ?? {}).map((validationKey) => `validations.${validationKey}`),
615
- ...(item.prefills ?? []).map((prefill) => `prefills.${prefill.id}`)
616
- ].filter((location) => Boolean(location));
669
+ if (itemFilter && !itemFilter.has(item.id)) return;
670
+ const locations = snapshot.model.expressions.filter((entry) => entry.path[0] === "surveyItems" && entry.path[1] === rawIndex).map((entry) => `/${entry.path.map((part) => encodeJsonPointerSegment(String(part))).join("/")}`);
617
671
  return {
618
672
  itemId: item.id,
619
- key: item.key,
673
+ editorName: names.get(item.id) ?? item.id,
620
674
  itemType: item.itemType,
621
675
  rawIndex,
622
676
  locations
623
677
  };
624
678
  }).filter((entry) => Boolean(entry)) : void 0;
625
679
  const expressionLocations = allExpressionLocations?.slice(offset, offset + limit);
626
- const totalAvailable = Math.max(allResponseSlots.length, allExpressionLocations?.length ?? 0);
680
+ const totalAvailable = Math.max(allResponseSlots.length, allStateTargets.length, allExpressionLocations?.length ?? 0);
627
681
  const nextCursor = offset + limit < totalAvailable ? String(offset + limit) : void 0;
628
682
  return {
629
683
  ok: true,
@@ -631,31 +685,36 @@ const buildContextOutput = (snapshot, input) => {
631
685
  modelSummary: [
632
686
  "Expression capability loaded.",
633
687
  "Use prepare mutations for display conditions, disabled conditions, validations, prefills, and template values.",
634
- "Display, disabled, validation, and prefill.when expressions must return boolean.",
688
+ "Visibility, disabled and validation expressions must return boolean.",
635
689
  "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.",
690
+ "State nodes use {type:'ownerState',state:'disabled',targetRef:{ownerId}} (isDisabled in the editor). They include inherited disabling but do not mean hidden, missing or unchangeable due to selection limits. State references participate in the same dependency graph and cycle checks as answers.",
691
+ "Disabled answers remain active and are submitted. For selected-and-enabled behavior combine selection with not(isDisabled(option)); a disabled ancestor also makes that condition false. Use ordinary selection for read-only prefilled answers that must still drive branching.",
692
+ "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
693
  "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.",
694
+ "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. An absent visibility condition shows its target; an evaluated missing/nonboolean condition hides it. All expressions use the canonical JSON grammar.",
639
695
  "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
696
  "Response-slot order: set-like deterministic snapshot construction order; it has no survey or respondent-visible meaning.",
641
- 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(", ")}`,
697
+ itemFilter ? `Response-slot scope: filtered to itemIds=${JSON.stringify(input.itemIds)}; ${allResponseSlots.length} matching of ${snapshot.responseVariables.length * 2} 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 (${snapshot.responseVariables.length * 2} slots).`,
698
+ `State targets (${allStateTargets.length}): ${JSON.stringify(allStateTargets.slice(offset, offset + limit))}`,
699
+ `Response slots (${responseSlots.length}/${allResponseSlots.length}, offset ${offset}): ${responseSlots.map((slot) => `${JSON.stringify(slot.ref)}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
643
700
  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
701
  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
- "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
702
+ "Functions: and, or, not, eq, gt, gte, lt, lte, in_range, list_contains, list_count, if, sum, min, max, str_eq, date_eq.",
703
+ 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
704
  ].filter((line) => Boolean(line)).join("\n"),
648
705
  totalResponseSlots: allResponseSlots.length,
649
- surveyResponseSlotCount: surveyResponseSlots.length,
706
+ surveyResponseSlotCount: snapshot.responseVariables.length * 2,
650
707
  ...allExpressionLocations ? { totalExpressionLocations: allExpressionLocations.length } : {},
651
708
  ...nextCursor ? { nextCursor } : {},
652
709
  ...unresolvedItemIds.length ? { unresolvedItemIds } : {},
653
710
  listOrdering: {
654
711
  responseSlots: "Set-like deterministic snapshot construction order; position has no survey or respondent-visible meaning.",
655
712
  expressionLocations: "Stored surveyItems array order; locations within an item are grouped by expression surface, not ranked or respondent-visible order.",
656
- "expressionLocations[].locations": "Grouped by expression surface (root, components, validations, prefills); position has no semantic meaning."
713
+ "expressionLocations[].locations": "Canonical schema traversal order; paths locate conditions, rules, prefills and element expressions."
657
714
  },
658
715
  responseSlots,
716
+ stateTargets: allStateTargets.slice(offset, offset + limit),
717
+ totalStateTargets: allStateTargets.length,
659
718
  ...expressionLocations ? { expressionLocations } : {}
660
719
  };
661
720
  };
@@ -719,7 +778,7 @@ const surveyExpressionTool = createTool({
719
778
  }), collectExpressionMutationRecoveryTargets(input.mutations, snapshot));
720
779
  const prepareInput = prepareParse.data;
721
780
  const addressedTargets = collectExpressionMutationRecoveryTargets(prepareInput.mutations, snapshot);
722
- const { issues, patches } = buildExpressionMutationBatch(prepareInput.mutations, snapshot);
781
+ const { issues, patches } = buildExpressionMutationBatch(prepareInput.mutations, snapshot.model);
723
782
  const errors = issues.filter((issue) => issue.severity === "error");
724
783
  const warnings = issues.filter((issue) => issue.severity === "warning");
725
784
  if (errors.length > 0) return reject(rejectedEvent({
@@ -765,6 +824,104 @@ const surveyExpressionTool = createTool({
765
824
  toModelOutput: (value) => toTextModelOutput(value, (output) => "action" in output && output.action === "context" ? output.modelSummary : summarizeDraftEvent(output))
766
825
  });
767
826
  //#endregion
827
+ //#region src/server/tools/localization-inspection.ts
828
+ function describeContent(content, locale, ref) {
829
+ const text = getContentPlainText(content);
830
+ const path = `/translations/${encodeJsonPointerSegment(locale)}/${ref.scope === "content" ? `content/${encodeJsonPointerSegment(ref.ownerId)}` : ref.surface}/${encodeJsonPointerSegment(ref.scope === "content" ? ref.role : ref.contentKey)}`;
831
+ return {
832
+ locale,
833
+ type: content.type,
834
+ text: text.slice(0, 2e3),
835
+ textLength: text.length,
836
+ truncated: text.length > 2e3,
837
+ path,
838
+ ...content.type === "richText" ? { content } : { textPath: `${path}/content` }
839
+ };
840
+ }
841
+ function inspectLocalization(input, snapshot) {
842
+ const survey = snapshot.survey;
843
+ const fail = (reason) => ({
844
+ ok: false,
845
+ reason
846
+ });
847
+ if (input.sourceLocale === input.targetLocale) return fail("Source and target locales must differ.");
848
+ if (input.sourceLocale && !ownValue(survey.translations, input.sourceLocale)) return fail(`Source locale "${input.sourceLocale}" is not present in the current survey draft.`);
849
+ const itemId = input.itemRef === "selected" ? snapshot.ui.selectedItemId : input.itemRef;
850
+ if (input.itemRef && (!itemId || !survey.surveyItems.some((item) => item.id === itemId))) return fail("Unknown localization item. Use an existing canonical item id.");
851
+ const targetTranslations = ownValue(survey.translations, input.targetLocale);
852
+ let entries = listLocalizationEntries(snapshot.model, {
853
+ sourceLocale: input.sourceLocale,
854
+ ...itemId ? { itemIds: [itemId] } : {}
855
+ }).filter((entry) => Object.entries(entry.contents).some(([locale, content]) => locale !== input.targetLocale && hasLocalizationText(content)));
856
+ const unavailableRefs = [];
857
+ if (input.localizationRefs) {
858
+ const wanted = new Set(input.localizationRefs.map(getLocalizationEntryKey));
859
+ const available = new Set(entries.map((entry) => entry.key));
860
+ for (const ref of input.localizationRefs) if (!available.has(getLocalizationEntryKey(ref))) unavailableRefs.push(ref);
861
+ entries = entries.filter((entry) => wanted.has(entry.key));
862
+ }
863
+ const referenceValues = (entry) => Object.fromEntries(Object.entries(entry.contents).filter(([locale]) => locale !== input.targetLocale));
864
+ const fingerprint = createHash("sha256").update(stableStringify({
865
+ sourceLocale: input.sourceLocale,
866
+ targetLocale: input.targetLocale,
867
+ itemId,
868
+ refs: input.localizationRefs?.map(getLocalizationEntryKey).sort(),
869
+ missingOnly: Boolean(input.missingOnly),
870
+ entries: entries.map((entry) => [entry.key, referenceValues(entry)])
871
+ })).digest("hex");
872
+ let position = 0;
873
+ if (input.cursor) {
874
+ const parsed = /^(\d+)\.([a-f0-9]{64})$/.exec(input.cursor);
875
+ if (!parsed || parsed[2] !== fingerprint || !Number.isSafeInteger(Number(parsed[1])) || Number(parsed[1]) > entries.length) return fail("Localization cursor is stale or invalid. Reference content, order, or scope changed. Restart without a cursor; for review, start a fresh pass and report earlier corrections separately.");
876
+ position = Number(parsed[1]);
877
+ }
878
+ const missingCount = entries.filter((entry) => !hasLocalizationText(ownValue(entry.contents, input.targetLocale))).length;
879
+ const matches = (entry) => !input.missingOnly || !hasLocalizationText(ownValue(entry.contents, input.targetLocale));
880
+ const page = [];
881
+ while (position < entries.length && page.length < (input.limit ?? 20)) {
882
+ const entry = entries[position++];
883
+ if (matches(entry)) page.push(entry);
884
+ }
885
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
886
+ return {
887
+ ok: true,
888
+ result: {
889
+ ...input.sourceLocale ? { sourceLocale: input.sourceLocale } : {},
890
+ targetLocale: input.targetLocale,
891
+ scope: itemId ? {
892
+ itemId,
893
+ includesDescendants: true
894
+ } : { survey: true },
895
+ ...input.localizationRefs ? {
896
+ localizationRefs: input.localizationRefs,
897
+ unavailableRefs,
898
+ unavailableReason: "These requested fields no longer exist in scope or lack reference wording in another language. Process the available fields, report unavailable refs separately, and never widen the scope."
899
+ } : {},
900
+ targetLocalePresent: Boolean(targetTranslations),
901
+ missingOnly: Boolean(input.missingOnly),
902
+ totalTexts: entries.length,
903
+ missingCount,
904
+ translatedCount: entries.length - missingCount,
905
+ totalAvailable: input.missingOnly ? missingCount : entries.length,
906
+ returned: page.length,
907
+ ...entries.slice(position).some(matches) ? { nextCursor: `${position}.${fingerprint}` } : {},
908
+ contentCoverage: "references contains all available non-target translations, not a single authoritative source. text is a preview. Read truncated plain/md values via raw-json paths: [textPath] and follow nextTextOffset using textOffset until complete. Rich-text content is complete. Read complete references and target before reviewing. targetPresent means presence, not correctness. Ask about material disagreements between references. sourceLocale, when supplied, identifies the explicitly requested source.",
909
+ listOrdering: { surfaces: "Survey texts first, then outline order. Cursor positions are in the complete scoped list before missingOnly. Target-text edits preserve cursors; changes to reference content, query, or entry order require a fresh pass." },
910
+ surfaces: page.map((entry) => ({
911
+ ...entry.ref,
912
+ entryKey: entry.key,
913
+ ...entry.ref.scope === "content" ? {
914
+ itemId: snapshot.model.owners.get(entry.ref.ownerId)?.itemId,
915
+ itemKey: names.get(snapshot.model.owners.get(entry.ref.ownerId)?.itemId ?? "")
916
+ } : {},
917
+ references: Object.entries(referenceValues(entry)).filter(([, content]) => hasLocalizationText(content)).map(([locale, content]) => describeContent(content, locale, entry.ref)),
918
+ targetPresent: hasLocalizationText(ownValue(entry.contents, input.targetLocale)),
919
+ ...ownValue(entry.contents, input.targetLocale) ? { target: describeContent(ownValue(entry.contents, input.targetLocale), input.targetLocale, entry.ref) } : {}
920
+ }))
921
+ }
922
+ };
923
+ }
924
+ //#endregion
768
925
  //#region src/server/tools/parse-inspection-output.ts
769
926
  const parseInspectionOutput = (schema, value, backend) => {
770
927
  const parsed = schema.safeParse(value);
@@ -772,6 +929,69 @@ const parseInspectionOutput = (schema, value, backend) => {
772
929
  return parsed.data;
773
930
  };
774
931
  //#endregion
932
+ //#region src/server/tools/response-variable-summary.ts
933
+ const MAX_COMPONENTS = 50;
934
+ const responseVariableSummarySchema = z.object({
935
+ slotId: z.string(),
936
+ itemId: z.string(),
937
+ variableName: z.string(),
938
+ valueType: z.string(),
939
+ elementId: z.string().optional(),
940
+ rowId: z.string().optional(),
941
+ columnId: z.string().optional(),
942
+ namingKind: z.enum(["variable", "matrix"]).optional(),
943
+ computed: z.object({
944
+ operation: z.string(),
945
+ sourceSlotIds: z.array(z.string())
946
+ }).optional(),
947
+ computedSourcesTruncated: z.boolean(),
948
+ referenceValueKeys: stringRecord(z.string(), z.string()).optional(),
949
+ referenceValueScores: stringRecord(z.string(), z.number()).optional(),
950
+ optionIds: z.array(z.string()),
951
+ optionsTotal: z.number(),
952
+ optionsTruncated: z.boolean()
953
+ });
954
+ const responseOptionIds = (slot) => [.../* @__PURE__ */ new Set([
955
+ ...Object.keys(slot.referenceValueKeys ?? {}),
956
+ ...Object.keys(slot.referenceValueScores ?? {}),
957
+ ...(slot.allowedValues ?? []).flatMap((value) => value.type === "reference" ? [value.value] : value.type === "reference[]" ? value.value : [])
958
+ ])];
959
+ /** Coding discovery excludes config paths and duplicated translations for every survey locale. */
960
+ const summarizeResponseVariable = (slot) => {
961
+ const optionIds = responseOptionIds(slot);
962
+ const take = (values) => values && Object.fromEntries(optionIds.slice(0, MAX_COMPONENTS).filter((id) => Object.hasOwn(values, id)).map((id) => [id, values[id]]));
963
+ return {
964
+ slotId: slot.slotId,
965
+ itemId: slot.itemId,
966
+ variableName: slot.variableName,
967
+ valueType: slot.valueType,
968
+ elementId: slot.elementId,
969
+ rowId: slot.rowId,
970
+ columnId: slot.columnId,
971
+ namingKind: slot.naming?.kind,
972
+ computed: slot.computed && {
973
+ ...slot.computed,
974
+ sourceSlotIds: slot.computed.sourceSlotIds.slice(0, MAX_COMPONENTS)
975
+ },
976
+ computedSourcesTruncated: (slot.computed?.sourceSlotIds.length ?? 0) > MAX_COMPONENTS,
977
+ referenceValueKeys: take(slot.referenceValueKeys),
978
+ referenceValueScores: take(slot.referenceValueScores),
979
+ optionIds: optionIds.slice(0, MAX_COMPONENTS),
980
+ optionsTotal: optionIds.length,
981
+ optionsTruncated: optionIds.length > MAX_COMPONENTS
982
+ };
983
+ };
984
+ const responseVariableMatchesQuery = (slot, query) => [
985
+ slot.slotId,
986
+ slot.itemId,
987
+ slot.variableName,
988
+ slot.elementId,
989
+ slot.rowId,
990
+ slot.columnId,
991
+ ...responseOptionIds(slot),
992
+ ...Object.values(slot.referenceValueKeys ?? {})
993
+ ].some((value) => value?.toLowerCase().includes(query));
994
+ //#endregion
775
995
  //#region src/server/tools/inspect-current-context.ts
776
996
  const contextScopeSchema = z.enum([
777
997
  "summary",
@@ -788,15 +1008,16 @@ const outlineModeSchema$1 = z.enum([
788
1008
  ]);
789
1009
  const translationSnippetSchema = z.object({
790
1010
  locale: z.string(),
791
- contentKey: z.string(),
1011
+ ownerId: z.string(),
1012
+ role: z.string(),
792
1013
  text: z.string()
793
1014
  });
794
1015
  const inputSchema$7 = z.object({
795
1016
  scopes: z.array(contextScopeSchema).min(1).default(["summary"]).describe("Which current editor context sections to inspect."),
796
1017
  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."),
1018
+ query: z.string().max(180).optional().describe("Optional text used to find matching item IDs, editor names, labels, or response variables."),
798
1019
  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."),
1020
+ 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
1021
  limit: z.number().int().min(1).max(50).default(12).describe("Maximum number of rows to return for list-like scopes.")
801
1022
  });
802
1023
  const outputSchema$6 = z.object({
@@ -805,11 +1026,12 @@ const outputSchema$6 = z.object({
805
1026
  revision: z.number().int().nonnegative().optional(),
806
1027
  contentHash: z.string().optional(),
807
1028
  scopes: z.array(contextScopeSchema).optional(),
808
- listOrdering: z.record(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1029
+ listOrdering: stringRecord(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
809
1030
  summary: z.object({
810
1031
  sessionId: z.string().nullable(),
811
1032
  view: z.string(),
812
1033
  selectedItemId: z.string().optional(),
1034
+ responseSlotId: z.string().optional(),
813
1035
  focusItemId: z.string().optional(),
814
1036
  selectedLocale: z.string(),
815
1037
  recentItemIds: z.array(z.string()),
@@ -820,9 +1042,9 @@ const outputSchema$6 = z.object({
820
1042
  outlineItemCount: z.number().int().nonnegative(),
821
1043
  responseSlotCount: z.number().int().nonnegative(),
822
1044
  rootItem: itemSummarySchema.optional(),
823
- availableItemTypes: z.array(z.string()),
824
- deterministicCreateItemTypes: z.array(z.string()),
825
- itemTypeDetails: z.array(assistantItemTypeManifestSchema)
1045
+ availableElementTypes: z.array(z.string()),
1046
+ deterministicCreateElementTypes: z.array(z.string()),
1047
+ elementTypeDetails: z.array(assistantElementTypeManifestSchema)
826
1048
  }).optional(),
827
1049
  selectedItem: z.object({
828
1050
  available: z.boolean(),
@@ -859,7 +1081,7 @@ const outputSchema$6 = z.object({
859
1081
  nextCursor: z.string().optional(),
860
1082
  truncated: z.boolean(),
861
1083
  ordering: z.string(),
862
- slots: z.array(z.string()),
1084
+ slots: z.array(responseVariableSummarySchema),
863
1085
  note: z.string().optional()
864
1086
  }).optional(),
865
1087
  uiContext: z.object({
@@ -871,7 +1093,13 @@ const outputSchema$6 = z.object({
871
1093
  componentId: z.string().optional(),
872
1094
  label: z.string().optional(),
873
1095
  details: z.string().optional()
874
- }))
1096
+ })),
1097
+ task: z.object({
1098
+ kind: z.string(),
1099
+ title: z.string().optional(),
1100
+ instructions: z.string().optional(),
1101
+ data: z.unknown().optional()
1102
+ }).optional()
875
1103
  }).optional(),
876
1104
  history: z.object({
877
1105
  currentIndex: z.number().int(),
@@ -913,13 +1141,14 @@ const flattenFocusDescendants = (node) => {
913
1141
  };
914
1142
  const getCandidateItems = (snapshot) => {
915
1143
  const outlineById = mapOutlineById(snapshot);
916
- return summarizeKeyIndexEntries(getKeyIndexEntries(snapshot), outlineById);
1144
+ return summarizeItemIndexEntries(getItemIndexEntries(snapshot), outlineById);
917
1145
  };
918
1146
  const buildSummary = (snapshot) => ({
919
1147
  rootItem: snapshot.inspection.outline.filter((item) => item.depth === 0).map(summarizeOutlineNode)[0],
920
1148
  sessionId: snapshot.version.sessionId,
921
1149
  view: snapshot.ui.view,
922
1150
  selectedItemId: snapshot.ui.selectedItemId,
1151
+ responseSlotId: snapshot.ui.responseSlotId,
923
1152
  focusItemId: snapshot.ui.focusItemId,
924
1153
  selectedLocale: snapshot.ui.selectedLocale,
925
1154
  recentItemIds: snapshot.ui.recentItemIds ?? [],
@@ -927,15 +1156,15 @@ const buildSummary = (snapshot) => ({
927
1156
  locales: snapshot.inspection.locales,
928
1157
  itemCount: snapshot.inspection.itemCount,
929
1158
  historyIndex: snapshot.version.historyIndex,
930
- outlineItemCount: snapshot.inspection.keyIndex.length,
1159
+ outlineItemCount: snapshot.inspection.itemIndex.length,
931
1160
  responseSlotCount: snapshot.inspection.responseSlots.length,
932
- availableItemTypes: snapshot.capabilities.itemTypes.map((entry) => entry.itemType),
933
- deterministicCreateItemTypes: getDeterministicCreateItemTypes(snapshot.capabilities),
934
- itemTypeDetails: snapshot.capabilities.itemTypes
1161
+ availableElementTypes: snapshot.capabilities.elementTypes.map((entry) => entry.elementType),
1162
+ deterministicCreateElementTypes: getDeterministicCreateElementTypes(snapshot.capabilities),
1163
+ elementTypeDetails: snapshot.capabilities.elementTypes
935
1164
  });
936
1165
  const buildSelectedItem = (snapshot, input) => {
937
1166
  const outlineById = mapOutlineById(snapshot);
938
- const indexById = mapKeyIndexById(snapshot);
1167
+ const indexById = mapItemIndexById(snapshot);
939
1168
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
940
1169
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.selectedItemId ?? snapshot.ui.focusItemId;
941
1170
  if (!requestedItemId) return {
@@ -968,11 +1197,12 @@ const buildSelectedItem = (snapshot, input) => {
968
1197
  const buildOutline = (snapshot, input) => {
969
1198
  const query = normalizeText(input.query);
970
1199
  const outlineById = mapOutlineById(snapshot);
971
- const indexById = mapKeyIndexById(snapshot);
1200
+ const indexById = mapItemIndexById(snapshot);
972
1201
  const resolvedInputItem = input.itemId ? resolveItemSummary(input.itemId, indexById, outlineById) : void 0;
973
1202
  const requestedItemId = resolvedInputItem?.itemId ?? input.itemId ?? snapshot.ui.focusItemId ?? snapshot.ui.selectedItemId;
974
1203
  const outlineMode = input.outlineMode ?? "full";
975
1204
  let strategy = "focused-snapshot-outline";
1205
+ let parentShuffled;
976
1206
  let items;
977
1207
  let note;
978
1208
  if (query) {
@@ -981,8 +1211,12 @@ const buildOutline = (snapshot, input) => {
981
1211
  } else if (outlineMode === "children" && requestedItemId) {
982
1212
  strategy = "direct-children";
983
1213
  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.";
1214
+ items = parent ? findDirectChildrenByParentId(getItemIndexEntries(snapshot), outlineById, parent.itemId) : [];
1215
+ if (!parent) note = "The requested item could not be resolved by exact item ID.";
1216
+ else {
1217
+ const rawParent = snapshot.survey.surveyItems.find((item) => item.id === parent.itemId);
1218
+ if (rawParent?.itemType === "group") parentShuffled = rawParent.config.shuffleItems === true;
1219
+ }
986
1220
  } else if (outlineMode === "focused" && requestedItemId && snapshot.inspection.focus?.focusItemId === requestedItemId) {
987
1221
  const focus = snapshot.inspection.focus;
988
1222
  const descendantIds = snapshot.inspection.focusItem ? flattenFocusDescendants(snapshot.inspection.focusItem).map((item) => item.itemId) : [];
@@ -999,7 +1233,7 @@ const buildOutline = (snapshot, input) => {
999
1233
  items = item ? [item] : [];
1000
1234
  note = "The requested item is outside the selected focus tree in this snapshot.";
1001
1235
  } else {
1002
- strategy = "full-key-index";
1236
+ strategy = "full-item-index";
1003
1237
  items = getCandidateItems(snapshot);
1004
1238
  }
1005
1239
  const page = sliceWithCursor(items, input);
@@ -1012,15 +1246,21 @@ const buildOutline = (snapshot, input) => {
1012
1246
  returned: page.items.length,
1013
1247
  nextCursor: page.nextCursor,
1014
1248
  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.",
1249
+ ordering: {
1250
+ "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.",
1251
+ "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.",
1252
+ "query-match": "Survey-tree preorder after filtering by the query; this is not relevance-ranked.",
1253
+ "item-match": "Single matched item; this does not establish sibling order.",
1254
+ "focused-snapshot-outline": "Relationship grouping for the focused item, not a single positional survey order."
1255
+ }[strategy],
1016
1256
  items: page.items,
1017
1257
  note
1018
1258
  };
1019
1259
  };
1020
1260
  const buildResponseSlots = (snapshot, input) => {
1021
- const responseSlots = snapshot.inspection.responseSlots;
1261
+ const responseSlots = snapshot.responseVariables;
1022
1262
  const query = normalizeText(input.query);
1023
- const filtered = query ? responseSlots.filter((slot) => slot.toLowerCase().includes(query)) : responseSlots;
1263
+ const filtered = query ? responseSlots.filter((slot) => responseVariableMatchesQuery(slot, query)) : responseSlots;
1024
1264
  const page = sliceWithCursor(filtered, input);
1025
1265
  return {
1026
1266
  available: true,
@@ -1030,7 +1270,7 @@ const buildResponseSlots = (snapshot, input) => {
1030
1270
  nextCursor: page.nextCursor,
1031
1271
  truncated: page.truncated,
1032
1272
  ordering: "Set-like response references in deterministic snapshot construction order; do not interpret their position as survey or respondent order.",
1033
- slots: page.items
1273
+ slots: page.items.map(summarizeResponseVariable)
1034
1274
  };
1035
1275
  };
1036
1276
  const inspectSurveyCurrentContext = (inputData, requestContext) => {
@@ -1049,9 +1289,9 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1049
1289
  if (scopes.includes("summary")) Object.assign(listOrdering, {
1050
1290
  "summary.recentItemIds": "Most-recent-first UI history; not survey structure.",
1051
1291
  "summary.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1052
- "summary.availableItemTypes": "Capability registry order; treat as a set of available types.",
1053
- "summary.deterministicCreateItemTypes": "Capability registry order; treat as a set of available types.",
1054
- "summary.itemTypeDetails": "Capability registry order; treat as a set of available type definitions."
1292
+ "summary.availableElementTypes": "Capability registry order; treat as a set of available types.",
1293
+ "summary.deterministicCreateElementTypes": "Capability registry order; treat as a set of available types.",
1294
+ "summary.elementTypeDetails": "Capability registry order; treat as a set of available type definitions."
1055
1295
  });
1056
1296
  if (scopes.includes("selected-item")) Object.assign(listOrdering, {
1057
1297
  "selectedItem.pathItems": "Root-to-selected survey-tree path.",
@@ -1079,7 +1319,8 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1079
1319
  if (scopes.includes("ui-context")) output.uiContext = {
1080
1320
  activeSurface: snapshot.ui.activeSurface,
1081
1321
  openPanels: snapshot.ui.openPanels ?? [],
1082
- highlights: snapshot.ui.highlights ?? []
1322
+ highlights: snapshot.ui.highlights ?? [],
1323
+ ...snapshot.ui.task ? { task: snapshot.ui.task } : {}
1083
1324
  };
1084
1325
  if (scopes.includes("history") && snapshot.ui.history) output.history = snapshot.ui.history;
1085
1326
  return parseInspectionOutput(outputSchema$6, output, "Current-context inspection");
@@ -1087,12 +1328,13 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1087
1328
  //#endregion
1088
1329
  //#region src/server/tools/inspect-item-details.ts
1089
1330
  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."),
1331
+ cursor: z.string().optional(),
1332
+ responseVariablesLimit: z.number().int().min(1).max(50).default(20),
1333
+ 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
1334
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict returned translations to. Defaults to all locales."),
1092
1335
  textLimit: z.number().int().min(20).max(2e3).default(400).describe("Maximum characters returned per translation snippet."),
1093
- includeChildren: z.boolean().optional().describe("Deprecated alias for includeSurveyChildren. This means survey-tree children only, not choice options or form fields."),
1094
1336
  includeSurveyChildren: z.boolean().default(false).describe("Whether to include a shallow list of this item's direct survey-tree children, such as group members."),
1095
- includeItemConfig: z.boolean().default(true).describe("Whether to include item-internal config details such as choice options and form field groups."),
1337
+ includeItemConfig: z.boolean().default(true).describe("Whether to include item-internal config details including elements, owned components and follow-up bodies."),
1096
1338
  childrenLimit: z.number().int().min(1).max(50).default(20).describe("Maximum number of direct survey-tree children to return.")
1097
1339
  });
1098
1340
  const outputSchema$5 = z.object({
@@ -1102,63 +1344,75 @@ const outputSchema$5 = z.object({
1102
1344
  translations: z.array(translationEntrySchema).optional(),
1103
1345
  surveyChildren: z.array(itemSummarySchema).optional(),
1104
1346
  surveyChildrenTruncated: z.boolean().optional(),
1105
- listOrdering: z.record(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1347
+ listOrdering: stringRecord(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1106
1348
  configDetails: itemConfigDetailsSchema.optional(),
1107
- capabilityDetails: z.object({
1349
+ responseVariables: z.array(responseVariableSummarySchema).optional(),
1350
+ responseVariablesTotal: z.number().optional(),
1351
+ responseVariablesTruncated: z.boolean().optional(),
1352
+ responseVariablesNextCursor: z.string().optional(),
1353
+ responseVariablesNote: z.string().optional(),
1354
+ capabilityDetails: z.array(z.object({
1355
+ elementId: z.string(),
1356
+ elementType: z.string(),
1108
1357
  summary: z.string(),
1109
1358
  details: z.unknown().optional()
1110
- }).optional()
1359
+ })).optional()
1111
1360
  });
1112
1361
  const inspectSurveyItemDetails = (inputData, requestContext) => {
1113
- const input = inputSchema$6.parse({
1114
- ...inputData,
1115
- includeSurveyChildren: inputData.includeSurveyChildren ?? inputData.includeChildren
1116
- });
1362
+ const input = inputSchema$6.parse(inputData);
1117
1363
  const snapshot = getSurveyAssistantEngineContext(requestContext);
1118
1364
  if (!snapshot) return {
1119
1365
  ok: false,
1120
1366
  reason: "No live editor snapshot was provided with this assistant turn."
1121
1367
  };
1122
1368
  const outlineById = mapOutlineById(snapshot);
1123
- const indexById = mapKeyIndexById(snapshot);
1369
+ const indexById = mapItemIndexById(snapshot);
1124
1370
  const rawItemsById = mapRawItemsById(snapshot);
1125
1371
  const item = resolveItemSummary(input.itemId, indexById, outlineById);
1126
1372
  if (!item) return {
1127
1373
  ok: false,
1128
- reason: `No item matching "${input.itemId}" (by itemId, key, or fullKey) was found in the current context snapshot.`
1374
+ reason: `No item matching "${input.itemId}" (by exact itemId) was found in the current context snapshot.`
1129
1375
  };
1130
1376
  const rawItem = rawItemsById.get(item.itemId);
1131
- const rawConfig = rawItem?.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1132
1377
  const output = {
1133
1378
  ok: true,
1134
1379
  item,
1135
1380
  translations: buildItemTranslationEntries(snapshot, item.itemId, input),
1136
1381
  listOrdering: {
1137
- translations: "Grouped by locale and then source translation-key order; this list is not positional survey content.",
1138
- 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."
1382
+ translations: "Grouped by locale and then declared content-owner and role order; this list is not positional survey content.",
1383
+ surveyChildren: rawItem?.itemType === "group" && rawItem.config.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."
1139
1384
  }
1140
1385
  };
1141
1386
  if (input.includeSurveyChildren) {
1142
- const allChildren = findDirectChildrenByFullKey(getKeyIndexEntries(snapshot), outlineById, item.fullKey);
1387
+ const entries = getItemIndexEntries(snapshot);
1388
+ const allChildren = findDirectChildrenByParentId(entries, outlineById, item.itemId);
1143
1389
  const limit = clampLimit(input.childrenLimit);
1144
1390
  output.surveyChildren = allChildren.slice(0, limit);
1145
1391
  output.surveyChildrenTruncated = allChildren.length > limit;
1146
1392
  }
1147
1393
  if (input.includeItemConfig) {
1148
- output.configDetails = rawItem ? buildItemConfigDetails(rawItem) : void 0;
1149
- output.capabilityDetails = rawItem ? inspectAssistantItem({
1150
- capabilities: snapshot.trustedCapabilities,
1151
- item: rawItem
1152
- }) : void 0;
1153
- if (rawItem?.itemType === "choiceItem" && output.configDetails?.choiceOptions) {
1154
- const rawChoiceConfig = rawItem.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1155
- const rawOptions = Array.isArray(rawChoiceConfig?.options) ? rawChoiceConfig.options : [];
1156
- for (const option of output.configDetails.choiceOptions) {
1157
- 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);
1159
- if (embeddedFormDetails?.formFieldGroups?.length) output.configDetails.formFieldGroups = [...output.configDetails.formFieldGroups ?? [], ...embeddedFormDetails.formFieldGroups];
1160
- }
1161
- }
1394
+ const variables = snapshot.responseVariables.filter((slot) => slot.itemId === item.itemId);
1395
+ const page = sliceWithCursor(variables, {
1396
+ cursor: input.cursor,
1397
+ limit: input.responseVariablesLimit
1398
+ });
1399
+ output.responseVariables = page.items.map(summarizeResponseVariable);
1400
+ output.responseVariablesTotal = variables.length;
1401
+ output.responseVariablesTruncated = page.truncated;
1402
+ output.responseVariablesNextCursor = page.nextCursor;
1403
+ output.responseVariablesNote = "Use responseVariablesNextCursor as cursor with the same itemId 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.";
1404
+ output.configDetails = rawItem ? buildItemConfigDetails(rawItem, snapshot.model) : void 0;
1405
+ output.capabilityDetails = [...snapshot.model.elements.values()].filter((element) => element.itemId === item.itemId).flatMap(({ node }) => {
1406
+ const details = inspectAssistantElement({
1407
+ capabilities: snapshot.trustedCapabilities,
1408
+ element: node
1409
+ });
1410
+ return details ? [{
1411
+ elementId: node.id,
1412
+ elementType: node.elementType,
1413
+ ...details
1414
+ }] : [];
1415
+ });
1162
1416
  }
1163
1417
  return parseInspectionOutput(outputSchema$5, output, "Item-details inspection");
1164
1418
  };
@@ -1166,17 +1420,17 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1166
1420
  //#region src/server/tools/inspect-raw-survey.ts
1167
1421
  const DEFAULT_VALUE_CHARACTER_LIMIT = 12e3;
1168
1422
  const MAX_VALUE_CHARACTER_LIMIT = 6e4;
1169
- 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\".");
1423
+ const jsonPointerPathSchema = z.string().max(500).describe("JSON Pointer path into the survey document. Examples: \"/surveyItems/0/config/items\", \"/metadata\", \"/translations/en/content/<ownerId>/title\".");
1170
1424
  const inputSchema$5 = z.object({
1171
1425
  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."),
1426
+ 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
1427
  textOffset: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
1174
1428
  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
1429
  });
1176
1430
  const rawSurveyItemPathSchema = z.object({
1177
1431
  path: z.string(),
1178
1432
  id: z.string(),
1179
- key: z.string(),
1433
+ editorName: z.string(),
1180
1434
  itemType: z.string(),
1181
1435
  parentId: z.string().optional(),
1182
1436
  childCount: z.number().int().nonnegative().optional()
@@ -1186,7 +1440,7 @@ const outputSchema$4 = z.object({
1186
1440
  reason: z.string().optional(),
1187
1441
  revision: z.number().int().nonnegative().optional(),
1188
1442
  contentHash: z.string().optional(),
1189
- listOrdering: z.record(z.string(), z.string()).optional().describe("Meaning of each returned list. Raw JSON arrays retain their stored order."),
1443
+ listOrdering: stringRecord(z.string(), z.string()).optional().describe("Meaning of each returned list. Raw JSON arrays retain their stored order."),
1190
1444
  overview: z.object({
1191
1445
  topLevelKeys: z.array(z.string()),
1192
1446
  surveyItemCount: z.number().int().nonnegative(),
@@ -1216,7 +1470,7 @@ const outputSchema$4 = z.object({
1216
1470
  rawItemPreview: z.string().optional(),
1217
1471
  rawItemTruncated: z.boolean().optional(),
1218
1472
  translationPaths: z.array(z.string()).optional(),
1219
- translationsByLocale: z.record(z.string(), z.unknown()).optional(),
1473
+ translationsByLocale: stringRecord(z.string(), z.unknown()).optional(),
1220
1474
  translationsPreview: z.string().optional(),
1221
1475
  translationsTruncated: z.boolean().optional(),
1222
1476
  error: z.string().optional()
@@ -1237,6 +1491,7 @@ const buildOverview = (snapshot) => {
1237
1491
  const rawSurvey = getRawSurvey(snapshot);
1238
1492
  const rawItems = getRawSurveyItems(snapshot);
1239
1493
  const parentByChildId = buildParentByChildId(rawSurvey);
1494
+ const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1240
1495
  return {
1241
1496
  topLevelKeys: Object.keys(rawSurvey).sort(),
1242
1497
  surveyItemCount: rawItems.length,
@@ -1244,7 +1499,7 @@ const buildOverview = (snapshot) => {
1244
1499
  itemPaths: rawItems.map((item, index) => ({
1245
1500
  path: `/surveyItems/${index}`,
1246
1501
  id: item.id,
1247
- key: item.key,
1502
+ editorName: names.get(item.id) ?? item.id,
1248
1503
  itemType: item.itemType,
1249
1504
  parentId: parentByChildId.get(item.id),
1250
1505
  childCount: getGroupChildren(item).length || void 0
@@ -1305,7 +1560,7 @@ const findRawItemByIdentifier = (snapshot, identifier) => {
1305
1560
  item: rawItems[directIndex],
1306
1561
  index: directIndex
1307
1562
  };
1308
- const resolved = resolveItemSummary(identifier, mapKeyIndexById(snapshot), mapOutlineById(snapshot));
1563
+ const resolved = resolveItemSummary(identifier, mapItemIndexById(snapshot), mapOutlineById(snapshot));
1309
1564
  if (!resolved) return;
1310
1565
  const resolvedIndex = rawItems.findIndex((item) => item.id === resolved.itemId);
1311
1566
  if (resolvedIndex < 0) return;
@@ -1314,26 +1569,30 @@ const findRawItemByIdentifier = (snapshot, identifier) => {
1314
1569
  index: resolvedIndex
1315
1570
  };
1316
1571
  };
1317
- const getTranslationsByLocaleForItem = (rawSurvey, itemId) => {
1572
+ const getTranslationsByLocaleForItem = (snapshot, itemId) => {
1318
1573
  const entries = {};
1319
- for (const [locale, localeTranslations] of Object.entries(rawSurvey.translations ?? {})) {
1320
- const itemTranslations = localeTranslations?.itemTranslations?.[itemId];
1321
- if (itemTranslations) entries[locale] = itemTranslations;
1574
+ const ownerIds = [...snapshot.model.owners.values()].filter((owner) => owner.itemId === itemId).map((owner) => owner.id);
1575
+ for (const [locale, translations] of Object.entries(snapshot.survey.translations ?? {})) {
1576
+ const content = Object.fromEntries(ownerIds.flatMap((ownerId) => {
1577
+ const content = ownValue(translations.content, ownerId);
1578
+ return content ? [[ownerId, content]] : [];
1579
+ }));
1580
+ if (Object.keys(content).length) setOwnValue(entries, locale, content);
1322
1581
  }
1323
1582
  return entries;
1324
1583
  };
1325
1584
  const buildItemValues = (snapshot, input) => {
1326
- const rawSurvey = getRawSurvey(snapshot);
1327
1585
  return input.itemIdentifiers.map((identifier) => {
1328
1586
  const resolved = findRawItemByIdentifier(snapshot, identifier);
1329
1587
  if (!resolved) return {
1330
1588
  identifier,
1331
1589
  found: false,
1332
- error: `No item matching "${identifier}" was found by item id, key, or fullKey.`
1590
+ error: `No item matching "${identifier}" was found by exact item ID.`
1333
1591
  };
1334
1592
  const itemId = resolved.item.id;
1335
1593
  const rawItemOutput = toOutputValue(resolved.item, input.valueCharacterLimit);
1336
- const translationsOutput = toOutputValue(getTranslationsByLocaleForItem(rawSurvey, itemId), input.valueCharacterLimit);
1594
+ const translationsByLocale = getTranslationsByLocaleForItem(snapshot, itemId);
1595
+ const translationsOutput = toOutputValue(translationsByLocale, input.valueCharacterLimit);
1337
1596
  const translationsValue = "value" in translationsOutput && isRecord$1(translationsOutput.value) ? translationsOutput.value : void 0;
1338
1597
  return {
1339
1598
  identifier,
@@ -1343,7 +1602,7 @@ const buildItemValues = (snapshot, input) => {
1343
1602
  rawItem: "value" in rawItemOutput ? rawItemOutput.value : void 0,
1344
1603
  rawItemPreview: "preview" in rawItemOutput ? rawItemOutput.preview : void 0,
1345
1604
  rawItemTruncated: rawItemOutput.truncated,
1346
- translationPaths: snapshot.inspection.locales.map((locale) => `/translations/${encodeJsonPointerSegment(locale)}/itemTranslations/${encodeJsonPointerSegment(itemId)}`),
1605
+ translationPaths: snapshot.inspection.locales.flatMap((locale) => [...snapshot.model.owners.values()].filter((owner) => owner.itemId === itemId).map((owner) => `/translations/${encodeJsonPointerSegment(locale)}/content/${encodeJsonPointerSegment(owner.id)}`)),
1347
1606
  translationsByLocale: translationsValue,
1348
1607
  translationsPreview: "preview" in translationsOutput ? translationsOutput.preview : void 0,
1349
1608
  translationsTruncated: translationsOutput.truncated
@@ -1393,7 +1652,8 @@ const searchMatchSchema = z.object({
1393
1652
  ]),
1394
1653
  field: z.string().optional(),
1395
1654
  locale: z.string().optional(),
1396
- contentKey: z.string().optional(),
1655
+ ownerId: z.string().optional(),
1656
+ role: z.string().optional(),
1397
1657
  text: z.string(),
1398
1658
  score: z.number()
1399
1659
  });
@@ -1404,7 +1664,8 @@ const searchResultSchema = z.object({
1404
1664
  });
1405
1665
  const inputSchema$4 = z.object({
1406
1666
  query: z.string().min(1).max(MAX_QUERY_LENGTH).describe("Text to search for across all survey items, including visible translated content such as titles, option labels, and form field labels. Examples: \"age\", \"consent\", \"blood pressure\"."),
1407
- itemTypes: z.array(z.string()).optional().describe("Optional item types to restrict results to, such as choiceItem or formItem."),
1667
+ itemTypes: z.array(z.string()).optional().describe("Optional item types to restrict results to, content, group or page-break."),
1668
+ elementTypes: z.array(z.string()).optional().describe("Optional registered element types. Return items containing at least one matching element, including option follow-ups."),
1408
1669
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict translation-content matches to."),
1409
1670
  cursor: z.string().optional().describe("Optional numeric cursor returned by a previous search."),
1410
1671
  limit: z.number().int().min(1).max(50).default(12).describe("Maximum number of matching items to return.")
@@ -1420,7 +1681,7 @@ const outputSchema$3 = z.object({
1420
1681
  ordering: z.string().optional().describe("How results and their matches are ordered; this search output is not survey order."),
1421
1682
  results: z.array(searchResultSchema).optional()
1422
1683
  });
1423
- const STOP_WORDS = new Set([
1684
+ const STOP_WORDS = /* @__PURE__ */ new Set([
1424
1685
  "a",
1425
1686
  "about",
1426
1687
  "an",
@@ -1463,7 +1724,7 @@ const scoreText = (value, query, terms, baseScore) => {
1463
1724
  const addMatch = (matches, match) => {
1464
1725
  const text = match.text?.trim();
1465
1726
  if (!text) return;
1466
- if (!matches.some((existing) => existing.source === match.source && existing.field === match.field && existing.locale === match.locale && existing.contentKey === match.contentKey && existing.text === text)) matches.push({
1727
+ if (!matches.some((existing) => existing.source === match.source && existing.field === match.field && existing.locale === match.locale && existing.ownerId === match.ownerId && existing.role === match.role && existing.text === text)) matches.push({
1467
1728
  ...match,
1468
1729
  text
1469
1730
  });
@@ -1473,7 +1734,7 @@ const candidateItems = (snapshot) => {
1473
1734
  const rawItemsById = mapRawItemsById(snapshot);
1474
1735
  const seen = /* @__PURE__ */ new Set();
1475
1736
  const candidates = [];
1476
- for (const entry of summarizeKeyIndexEntries(snapshot.inspection.keyIndex, outlineById)) {
1737
+ for (const entry of summarizeItemIndexEntries(snapshot.inspection.itemIndex, outlineById)) {
1477
1738
  seen.add(entry.itemId);
1478
1739
  candidates.push({
1479
1740
  item: entry,
@@ -1492,13 +1753,21 @@ const candidateItems = (snapshot) => {
1492
1753
  const searchItems = (snapshot, input) => {
1493
1754
  const terms = queryTerms(input.query);
1494
1755
  const itemTypeFilter = input.itemTypes?.length ? new Set(input.itemTypes) : void 0;
1495
- return candidateItems(snapshot).filter(({ item }) => !itemTypeFilter || itemTypeFilter.has(item.itemType)).map(({ item, rawItem }) => {
1756
+ const elementTypeFilter = input.elementTypes?.length ? new Set(input.elementTypes) : void 0;
1757
+ const typesByItem = /* @__PURE__ */ new Map();
1758
+ for (const element of snapshot.model.elements.values()) {
1759
+ const types = typesByItem.get(element.itemId) ?? /* @__PURE__ */ new Set();
1760
+ types.add(element.node.elementType);
1761
+ typesByItem.set(element.itemId, types);
1762
+ }
1763
+ return candidateItems(snapshot).filter(({ item }) => !elementTypeFilter || [...typesByItem.get(item.itemId) ?? []].some((type) => elementTypeFilter.has(type))).filter(({ item }) => !itemTypeFilter || itemTypeFilter.has(item.itemType)).map(({ item, rawItem }) => {
1496
1764
  const matches = [];
1497
1765
  const structuralFields = [
1498
1766
  ["itemId", item.itemId],
1499
1767
  ["itemType", item.itemType],
1500
- ["key", item.key],
1501
- ["fullKey", item.fullKey],
1768
+ ["elementTypes", [...typesByItem.get(item.itemId) ?? []].join(" ")],
1769
+ ["key", item.editorName],
1770
+ ["breadcrumb", item.breadcrumb],
1502
1771
  ["itemLabel", item.itemLabel],
1503
1772
  ["path", item.path?.join(" ")]
1504
1773
  ];
@@ -1516,16 +1785,17 @@ const searchItems = (snapshot, input) => {
1516
1785
  locales: input.locales,
1517
1786
  textLimit: DEFAULT_TEXT_LIMIT
1518
1787
  })) {
1519
- const score = scoreText(`${translation.contentKey} ${translation.text}`, input.query, terms, 100);
1788
+ const score = scoreText(`${translation.ownerId} ${translation.role} ${translation.text}`, input.query, terms, 100);
1520
1789
  if (score !== null) addMatch(matches, {
1521
1790
  source: "translation",
1522
1791
  locale: translation.locale,
1523
- contentKey: translation.contentKey,
1792
+ ownerId: translation.ownerId,
1793
+ role: translation.role,
1524
1794
  text: translation.text,
1525
1795
  score
1526
1796
  });
1527
1797
  }
1528
- if (rawItem) for (const text of buildConfigSearchText(rawItem)) {
1798
+ if (rawItem) for (const text of buildConfigSearchText(rawItem, snapshot.model)) {
1529
1799
  const score = scoreText(text, input.query, terms, 60);
1530
1800
  if (score !== null) addMatch(matches, {
1531
1801
  source: "config",
@@ -1540,7 +1810,7 @@ const searchItems = (snapshot, input) => {
1540
1810
  matches: matches.slice(0, MAX_MATCHES_PER_ITEM),
1541
1811
  score: matches.reduce((sum, match) => sum + match.score, 0)
1542
1812
  };
1543
- }).filter((result) => Boolean(result)).sort((a, b) => b.score - a.score || a.item.fullKey.localeCompare(b.item.fullKey));
1813
+ }).filter((result) => Boolean(result)).sort((a, b) => b.score - a.score || a.item.breadcrumb.localeCompare(b.item.breadcrumb));
1544
1814
  };
1545
1815
  const searchSurveyItems = (inputData, requestContext) => {
1546
1816
  const input = inputSchema$4.parse(inputData);
@@ -1559,7 +1829,7 @@ const searchSurveyItems = (inputData, requestContext) => {
1559
1829
  returned: page.items.length,
1560
1830
  nextCursor: page.nextCursor,
1561
1831
  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.",
1832
+ 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
1833
  results: page.items
1564
1834
  }, "Survey-item search");
1565
1835
  };
@@ -1596,14 +1866,30 @@ const inputSchema$3 = z.object({
1596
1866
  "ui-context",
1597
1867
  "history"
1598
1868
  ])).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."),
1600
- sourceLocale: optionalNonBlankString(40).describe("For localization: source locale to translate. Defaults to the selected editor locale."),
1869
+ 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."),
1870
+ itemTypes: optionalArrayInput(z.array(z.enum([
1871
+ "content",
1872
+ "group",
1873
+ "page-break"
1874
+ ]))).describe("For search-items: optional flow item types."),
1875
+ elementTypes: optionalArrayInput(z.array(z.string().min(1).max(120))).describe("For search-items: optional registered element types contained in matching items."),
1876
+ locales: optionalArrayInput(z.array(z.string().min(1).max(40))).describe("For search-items: optional languages for translation matches."),
1877
+ sourceLocale: optionalNonBlankString(40).describe("For localization: optional explicitly requested source locale. Omit to use all available language versions as references; the selected UI locale does not establish a source."),
1601
1878
  targetLocale: optionalNonBlankString(40).describe("For localization: required target locale whose coverage should be inspected."),
1602
- 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.`),
1879
+ localizationRefs: optionalArrayInput(z.array(z.discriminatedUnion("scope", [z.object({
1880
+ scope: z.literal("content"),
1881
+ ownerId: z.string().min(1).max(240),
1882
+ role: z.string().min(1).max(500)
1883
+ }), z.object({
1884
+ scope: z.literal("survey"),
1885
+ surface: z.enum(SURVEY_LOCALIZATION_SURFACES$1),
1886
+ contentKey: z.string().min(1).max(500)
1887
+ })])).min(1).max(200)).describe("For localization: optional exact text field references selected by the author. Process larger selections in groups of at most 200 references."),
1888
+ missingOnly: optionalInput(z.boolean()).describe("For localization: return only texts missing in the target locale and present in at least one reference language."),
1889
+ itemRef: optionalNonBlankString(240).describe("For localization: an existing canonical item id or \"selected\"; scopes to its subtree, excluding survey texts. For current-context outline, item-details, or raw-json: item reference."),
1890
+ cursor: optionalNonBlankString(240).describe("Use the exact cursor returned by the previous result. Localization uses position.fingerprint, stable across target-text edits. Other actions use numeric offsets."),
1891
+ 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."),
1892
+ 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
1893
  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
1894
  includeChildren: optionalInput(z.boolean()).describe("For item-details: include direct survey-tree children."),
1609
1895
  includeItemConfig: optionalInput(z.boolean()).describe("For item-details: include choice/form/internal item config details."),
@@ -1642,148 +1928,27 @@ const inspectionBackendFailure = (action, reason) => ({
1642
1928
  modelSummary: reason
1643
1929
  });
1644
1930
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1645
- const MAX_LOCALIZATION_TEXT_CHARS = 2e3;
1646
- const SURVEY_TRANSLATION_SURFACES = [
1647
- "surveyCardContent",
1648
- "navigationContent",
1649
- "validationMessages"
1650
- ];
1651
- const truncateLocalizationText = (content) => {
1652
- const text = getContentPlainText(content).trim();
1653
- return text.length > MAX_LOCALIZATION_TEXT_CHARS ? `${text.slice(0, MAX_LOCALIZATION_TEXT_CHARS)}...` : text;
1654
- };
1655
- const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1656
- const survey = snapshot.survey;
1657
- const sourceLocale = input.sourceLocale ?? snapshot.ui.selectedLocale;
1658
- const sourceTranslations = survey.translations?.[sourceLocale];
1659
- const targetTranslations = survey.translations?.[targetLocale];
1660
- if (!sourceTranslations) return {
1661
- ok: false,
1662
- reason: `Source locale "${sourceLocale}" is not present in the current survey draft.`
1663
- };
1664
- const surfaces = [];
1665
- const addSurface = ({ contentKey, itemId, itemKey, itemType, sourceContent, surface, targetContent }) => {
1666
- const sourceText = truncateLocalizationText(sourceContent);
1667
- if (!sourceText) return;
1668
- const sourcePath = `/translations/${encodeJsonPointerSegment(sourceLocale)}/${itemId ? `itemTranslations/${encodeJsonPointerSegment(itemId)}` : surface}/${encodeJsonPointerSegment(contentKey)}`;
1669
- surfaces.push({
1670
- scope: itemId ? "item" : "survey",
1671
- ...surface ? { surface } : {},
1672
- ...itemId ? {
1673
- itemId,
1674
- itemKey,
1675
- itemType
1676
- } : {},
1677
- contentKey,
1678
- sourceType: sourceContent.type,
1679
- sourceText,
1680
- sourceTextLength: getContentPlainText(sourceContent).trim().length,
1681
- sourceTruncated: getContentPlainText(sourceContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS,
1682
- sourcePath,
1683
- ...sourceContent.type !== "richText" ? { sourceTextPath: `${sourcePath}/content` } : {},
1684
- ...sourceContent.type === "richText" ? { sourceContent } : {},
1685
- targetPresent: Boolean(targetContent && truncateLocalizationText(targetContent)),
1686
- ...targetContent ? {
1687
- targetType: targetContent.type,
1688
- targetText: truncateLocalizationText(targetContent),
1689
- targetTextLength: getContentPlainText(targetContent).trim().length,
1690
- targetTruncated: getContentPlainText(targetContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS
1691
- } : {}
1692
- });
1693
- };
1694
- for (const surface of SURVEY_TRANSLATION_SURFACES) {
1695
- const targetSurface = targetTranslations?.[surface];
1696
- for (const [contentKey, sourceContent] of Object.entries(sourceTranslations[surface] ?? {})) addSurface({
1697
- surface,
1698
- contentKey,
1699
- sourceContent,
1700
- targetContent: targetSurface?.[contentKey]
1701
- });
1702
- }
1703
- for (const item of survey.surveyItems) for (const [contentKey, sourceContent] of Object.entries(sourceTranslations.itemTranslations?.[item.id] ?? {})) addSurface({
1704
- itemId: item.id,
1705
- itemKey: item.key,
1706
- itemType: item.itemType,
1707
- contentKey,
1708
- sourceContent,
1709
- targetContent: targetTranslations?.itemTranslations?.[item.id]?.[contentKey]
1710
- });
1711
- const missingCount = surfaces.filter((surface) => !surface.targetPresent).length;
1712
- const filteredSurfaces = input.missingOnly ? surfaces.filter((surface) => !surface.targetPresent) : surfaces;
1713
- const parsedCursor = Number.parseInt(input.cursor ?? "0", 10);
1714
- const offset = Number.isFinite(parsedCursor) && parsedCursor >= 0 ? parsedCursor : 0;
1715
- const limit = input.limit ?? 20;
1716
- const page = filteredSurfaces.slice(offset, offset + limit);
1717
- const nextOffset = offset + page.length;
1718
- return {
1719
- ok: true,
1720
- result: {
1721
- sourceLocale,
1722
- targetLocale,
1723
- missingOnly: input.missingOnly ?? false,
1724
- 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.",
1725
- totalSourceSurfaces: surfaces.length,
1726
- totalAvailable: filteredSurfaces.length,
1727
- missingCount,
1728
- returned: page.length,
1729
- ...nextOffset < filteredSurfaces.length ? { nextCursor: String(nextOffset) } : {},
1730
- listOrdering: { surfaces: "Survey-level translation surfaces first, then item translations in stored survey-item and source content-key order. Cursor pagination preserves this order only while the draft is unchanged; restart without a cursor after mutations." },
1731
- surfaces: page
1732
- }
1733
- };
1734
- };
1735
1931
  const resolveItemRef = (input, snapshot) => {
1736
1932
  if (input.itemRef && input.itemRef !== "selected") return input.itemRef;
1737
1933
  return snapshot?.ui.selectedItemId;
1738
1934
  };
1739
- const listAssetUsageSummaries = (survey) => {
1740
- const usagesByAssetId = /* @__PURE__ */ new Map();
1741
- const addContent = (content, usage) => {
1742
- for (const assetUsage of getAssetUsagesFromContent(content)) {
1743
- const usages = usagesByAssetId.get(assetUsage.assetId) ?? [];
1744
- usages.push({
1745
- ...usage,
1746
- blockIndex: assetUsage.blockIndex
1747
- });
1748
- usagesByAssetId.set(assetUsage.assetId, usages);
1749
- }
1750
- };
1751
- for (const [locale, localeTranslations] of Object.entries(survey.translations ?? {})) {
1752
- for (const scope of [
1753
- "surveyCardContent",
1754
- "navigationContent",
1755
- "validationMessages"
1756
- ]) for (const [contentKey, content] of Object.entries(localeTranslations[scope] ?? {})) addContent(content, {
1757
- locale,
1758
- scope,
1759
- contentKey
1760
- });
1761
- for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations ?? {})) for (const [contentKey, content] of Object.entries(itemTranslations ?? {})) addContent(content, {
1762
- locale,
1763
- scope: "itemTranslation",
1764
- itemId,
1765
- contentKey
1766
- });
1767
- }
1768
- return usagesByAssetId;
1769
- };
1770
1935
  const summarizeAssetSource = (asset) => ({
1771
1936
  type: asset.source.type,
1772
1937
  mediaType: asset.source.mediaType
1773
1938
  });
1774
1939
  const listAssets = (input, snapshot) => {
1775
1940
  const survey = snapshot.survey;
1776
- const usagesByAssetId = listAssetUsageSummaries(survey);
1941
+ const usages = snapshot.model.getAssetUsages();
1777
1942
  const query = input.query?.trim().toLowerCase();
1778
1943
  const allAssets = Object.entries(survey.assets ?? {}).map(([assetId, asset]) => {
1779
- const usages = usagesByAssetId.get(assetId) ?? [];
1944
+ const assetUsages = usages.filter((usage) => usage.assetId === assetId);
1780
1945
  return {
1781
1946
  assetId,
1782
1947
  kind: asset.kind,
1783
1948
  source: summarizeAssetSource(asset),
1784
1949
  ...asset.metadata ? { metadata: asset.metadata } : {},
1785
- usageCount: usages.length,
1786
- usages
1950
+ usageCount: assetUsages.length,
1951
+ usages: assetUsages
1787
1952
  };
1788
1953
  }).filter((asset) => {
1789
1954
  if (!query) return true;
@@ -1813,7 +1978,7 @@ const listAssets = (input, snapshot) => {
1813
1978
  };
1814
1979
  const surveyInspectTool = createTool({
1815
1980
  id: TOOL_ID$3,
1816
- 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.",
1981
+ description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, paged multilingual 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.",
1817
1982
  inputSchema: inputSchema$3,
1818
1983
  outputSchema: outputSchema$2,
1819
1984
  mcp: { annotations: {
@@ -1862,7 +2027,10 @@ const surveyInspectTool = createTool({
1862
2027
  const inspection = runInspectionBackend(() => searchSurveyItems({
1863
2028
  query,
1864
2029
  limit: input.limit,
1865
- cursor: input.cursor
2030
+ cursor: input.cursor,
2031
+ itemTypes: input.itemTypes,
2032
+ elementTypes: input.elementTypes,
2033
+ locales: input.locales
1866
2034
  }, requestContext));
1867
2035
  if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
1868
2036
  const result = inspection.value;
@@ -1885,7 +2053,9 @@ const surveyInspectTool = createTool({
1885
2053
  itemId,
1886
2054
  includeSurveyChildren: input.includeChildren ?? false,
1887
2055
  includeItemConfig: input.includeItemConfig ?? true,
1888
- childrenLimit: input.limit
2056
+ childrenLimit: input.limit,
2057
+ responseVariablesLimit: input.limit,
2058
+ cursor: input.cursor
1889
2059
  }, requestContext));
1890
2060
  if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
1891
2061
  const result = inspection.value;
@@ -1897,32 +2067,36 @@ const surveyInspectTool = createTool({
1897
2067
  };
1898
2068
  }
1899
2069
  if (input.action === "expression-diagnostics") {
2070
+ const visibilityIssues = getAssistantVisibilityIssues(createAssistantSurvey(snapshot.survey, snapshot.trustedCapabilities));
1900
2071
  const allDiagnostics = analyzeSurveyAssistantExpressionReferences(snapshot.survey, snapshot.trustedCapabilities);
1901
2072
  const itemId = input.itemRef ? resolveItemRef(input, snapshot) : void 0;
1902
2073
  const diagnostics = itemId ? allDiagnostics.filter((diagnostic) => diagnostic.ownerItemId === itemId) : allDiagnostics;
1903
- const orderedDiagnostics = [...diagnostics].sort((left, right) => Number(isBlockingSurveyExpressionReferenceDiagnostic(right)) - Number(isBlockingSurveyExpressionReferenceDiagnostic(left)));
1904
- const blockingCount = diagnostics.filter(isBlockingSurveyExpressionReferenceDiagnostic).length;
2074
+ const orderedDiagnostics = [...diagnostics].sort((left, right) => Number(right.severity === "error") - Number(left.severity === "error"));
2075
+ const blockingCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1905
2076
  const advisoryCount = diagnostics.length - blockingCount;
1906
2077
  const limit = input.limit ?? 50;
1907
2078
  const limitedDiagnostics = orderedDiagnostics.slice(0, limit);
1908
- const guidanceByDispositionAndCategory = {};
2079
+ const guidanceByCode = {};
1909
2080
  for (const diagnostic of orderedDiagnostics) {
1910
- const byCategory = guidanceByDispositionAndCategory[diagnostic.disposition] ??= {};
1911
- byCategory[diagnostic.category] = getSurveyAssistantExpressionDiagnosticGuidance(diagnostic);
2081
+ const guidance = getSurveyAssistantExpressionDiagnosticGuidance(diagnostic);
2082
+ const existing = ownValue(guidanceByCode, diagnostic.code);
2083
+ setOwnValue(guidanceByCode, diagnostic.code, existing && !existing.includes(guidance) ? `${existing} ${guidance}` : guidance);
1912
2084
  }
1913
2085
  const result = {
1914
- valid: blockingCount === 0,
2086
+ valid: blockingCount === 0 && visibilityIssues.length === 0,
2087
+ visibilityIssues,
2088
+ visibilityScope: "survey",
1915
2089
  count: diagnostics.length,
1916
2090
  blockingCount,
1917
2091
  advisoryCount,
1918
2092
  diagnostics: limitedDiagnostics,
1919
- guidanceByDispositionAndCategory,
2093
+ guidanceByCode,
1920
2094
  truncated: diagnostics.length > limit
1921
2095
  };
1922
2096
  return {
1923
2097
  ok: true,
1924
2098
  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)}`,
2099
+ 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
2100
  result
1927
2101
  };
1928
2102
  }
@@ -1933,7 +2107,10 @@ const surveyInspectTool = createTool({
1933
2107
  reason: "Localization inspection requires a targetLocale.",
1934
2108
  modelSummary: "Localization inspection requires a targetLocale."
1935
2109
  };
1936
- const localization = listLocalizationSurfaces(input, snapshot, input.targetLocale);
2110
+ const localization = inspectLocalization({
2111
+ ...input,
2112
+ targetLocale: input.targetLocale
2113
+ }, snapshot);
1937
2114
  if (!localization.ok) return {
1938
2115
  ok: false,
1939
2116
  action: input.action,
@@ -1975,7 +2152,7 @@ const surveyInspectTool = createTool({
1975
2152
  inspectedPathCount: boundedPaths?.length ?? 0,
1976
2153
  ignoredPathCount,
1977
2154
  omittedPathCount,
1978
- ...omittedPathCount > 0 ? { nextPaths: requestedPaths?.slice(MAX_RAW_JSON_PATHS_PER_RESULT, MAX_RAW_JSON_PATHS_PER_RESULT * 2) } : {},
2155
+ ...omittedPathCount > 0 ? { nextPaths: requestedPaths?.slice(MAX_RAW_JSON_PATHS_PER_RESULT, 40) } : {},
1979
2156
  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
2157
  } : void 0;
1981
2158
  const result = requestCoverage && isRecord(rawResult) ? {
@@ -1990,38 +2167,53 @@ const surveyInspectTool = createTool({
1990
2167
  result
1991
2168
  };
1992
2169
  },
1993
- toModelOutput: (value) => toTextModelOutput(value, (output) => output.modelSummary)
2170
+ toModelOutput: (value) => toTextModelOutput(value, (output) => output.ok && output.action === "item-details" && isRecord(output.result) && output.result.ok === true ? JSON.stringify({
2171
+ capabilityDetails: output.result.capabilityDetails,
2172
+ ...output.result
2173
+ }) : output.modelSummary)
1994
2174
  });
1995
2175
  //#endregion
1996
2176
  //#region src/server/tools/survey-attachment-tool.ts
1997
2177
  const TOOL_ID$2 = "survey_attachment";
2178
+ const inputSchema$2 = z.object({
2179
+ 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."),
2180
+ documentRequests: z.array(z.object({
2181
+ documentId: z.string().min(1),
2182
+ pages: z.object({
2183
+ start: z.number().int().min(1).max(1e5),
2184
+ end: z.number().int().min(1).max(1e5)
2185
+ }).optional().describe("One-based inclusive PDF page range to prioritize."),
2186
+ focus: z.string().min(1).max(300).optional().describe("Short section, question range, or fact to prioritize in the original file.")
2187
+ })).min(1).max(4).optional().describe("Stored attachments with an optional bounded reading focus.")
2188
+ });
2189
+ const outputSchema$1 = z.object({
2190
+ loaded: z.array(z.object({
2191
+ id: z.string(),
2192
+ filename: z.string(),
2193
+ mediaType: z.string(),
2194
+ pages: z.object({
2195
+ start: z.number().int(),
2196
+ end: z.number().int()
2197
+ }).optional(),
2198
+ focus: z.string().optional()
2199
+ })),
2200
+ unavailable: z.array(z.object({
2201
+ id: z.string(),
2202
+ filename: z.string().optional(),
2203
+ mediaType: z.string().optional(),
2204
+ reason: z.enum([
2205
+ "not-listed",
2206
+ "content-unavailable",
2207
+ "attachment-limit"
2208
+ ])
2209
+ })).optional(),
2210
+ warnings: z.array(z.string()).optional()
2211
+ });
1998
2212
  const surveyAttachmentTool = createTool({
1999
2213
  id: TOOL_ID$2,
2000
2214
  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
- }),
2215
+ inputSchema: inputSchema$2,
2216
+ outputSchema: outputSchema$1,
2025
2217
  mcp: { annotations: {
2026
2218
  title: "Load chat attachment",
2027
2219
  readOnlyHint: true,
@@ -2030,8 +2222,8 @@ const surveyAttachmentTool = createTool({
2030
2222
  openWorldHint: false
2031
2223
  } },
2032
2224
  execute: async (inputData, context) => {
2033
- const access = (context?.requestContext)?.get("attachmentAccess");
2034
- if (!access) return { loaded: [] };
2225
+ const requestContext = context?.requestContext;
2226
+ const access = requestContext?.get("attachmentAccess");
2035
2227
  const warnings = [];
2036
2228
  const requests = /* @__PURE__ */ new Map();
2037
2229
  for (const documentId of inputData.documentIds ?? []) requests.set(documentId, {});
@@ -2047,11 +2239,50 @@ const surveyAttachmentTool = createTool({
2047
2239
  });
2048
2240
  }
2049
2241
  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);
2242
+ const authorized = access?.documents.filter((document) => requests.has(document.id)) ?? [];
2243
+ const unavailable = requestedIds.filter((id) => !authorized.some((document) => document.id === id)).map((id) => ({
2244
+ id,
2245
+ reason: "not-listed"
2246
+ }));
2247
+ for (const { id } of unavailable) warnings.push(`Stored attachment ${id} is not in the available manifest.`);
2248
+ if (!access) return {
2249
+ loaded: [],
2250
+ unavailable,
2251
+ warnings
2252
+ };
2253
+ const attachments = await access.load(authorized.filter((document) => !access.selectedAttachments.some((attachment) => attachment.documentId === document.id)).map((document) => document.id));
2254
+ const selected = new Map(access.selectedAttachments.map((attachment) => [attachment.documentId, attachment]));
2255
+ const nativePayloads = new Map([...requestContext?.get("modelAttachments") ?? [], ...selected.values()].map((attachment) => [`${attachment.mediaType}\u0000${attachment.contentHash}`, Buffer.byteLength(attachment.data, "base64")]));
2256
+ let totalBytes = [...nativePayloads.values()].reduce((sum, bytes) => sum + bytes, 0);
2257
+ for (const document of authorized) {
2258
+ if (selected.has(document.id)) continue;
2259
+ const attachment = attachments.find((candidate) => candidate.documentId === document.id);
2260
+ let reason = "content-unavailable";
2261
+ if (attachment) {
2262
+ const key = `${attachment.mediaType}\u0000${attachment.contentHash}`;
2263
+ const bytes = Buffer.byteLength(attachment.data, "base64");
2264
+ const alreadyIncluded = nativePayloads.has(key);
2265
+ if (bytes <= access.limits.maxAttachmentBytes && (alreadyIncluded || nativePayloads.size < access.limits.maxAttachments && totalBytes + bytes <= access.limits.maxTotalAttachmentBytes)) {
2266
+ selected.set(document.id, attachment);
2267
+ if (!alreadyIncluded) {
2268
+ nativePayloads.set(key, bytes);
2269
+ totalBytes += bytes;
2270
+ }
2271
+ continue;
2272
+ }
2273
+ reason = "attachment-limit";
2274
+ }
2275
+ unavailable.push({
2276
+ id: document.id,
2277
+ filename: document.name,
2278
+ mediaType: document.mediaType,
2279
+ reason
2280
+ });
2281
+ 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.`);
2282
+ }
2283
+ access.selectedAttachments.splice(0, access.selectedAttachments.length, ...selected.values());
2053
2284
  return {
2054
- loaded: authorized.map((document) => {
2285
+ loaded: authorized.filter((document) => selected.has(document.id)).map((document) => {
2055
2286
  const request = requests.get(document.id);
2056
2287
  const pages = document.mediaType === "application/pdf" ? request?.pages : void 0;
2057
2288
  if (request?.pages && !pages) warnings.push(`Ignored a PDF page focus for non-PDF attachment ${document.name}.`);
@@ -2063,6 +2294,7 @@ const surveyAttachmentTool = createTool({
2063
2294
  ...request?.focus ? { focus: request.focus } : {}
2064
2295
  };
2065
2296
  }),
2297
+ ...unavailable.length > 0 ? { unavailable } : {},
2066
2298
  ...warnings.length > 0 ? { warnings } : {}
2067
2299
  };
2068
2300
  },
@@ -2135,14 +2367,14 @@ const formatAuditForModel = (output) => {
2135
2367
  const location = [
2136
2368
  finding.itemId,
2137
2369
  finding.locale,
2138
- finding.contentKey
2370
+ finding.localizationRefs ? JSON.stringify(finding.localizationRefs) : finding.path?.join("/")
2139
2371
  ].filter(Boolean).join(":");
2140
2372
  return `- [${finding.severity}] ${finding.code}${location ? ` (${location})` : ""}: ${previewAuditMessage(finding.message)}${finding.suggestion ? ` Guidance: ${previewAuditMessage(finding.suggestion)}` : ""}`;
2141
2373
  }).join("\n")}` : void 0,
2142
2374
  report.qualityReviewPrinciples.length > 0 ? `Qualitative review rubric:\n${report.qualityReviewPrinciples.map((principle) => `- ${principle.id}: ${principle.prompt}`).join("\n")}` : void 0,
2143
2375
  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) => {
2144
2376
  const content = entry.content ? `; current content=${JSON.stringify(previewAuditMessage(entry.content))}` : "; current content unavailable";
2145
- return `- ${entry.kind} item=${entry.itemId} itemKey=${JSON.stringify(entry.itemKey)} key=${JSON.stringify(entry.key)} (${entry.keySource})${entry.contentKey ? ` contentKey=${entry.contentKey}` : ""}${content}`;
2377
+ return `- ${entry.kind} item=${entry.itemId} itemKey=${JSON.stringify(entry.itemKey)} key=${JSON.stringify(entry.key)} (${entry.keySource})${entry.localizationRefs?.length ? ` contentRefs=${JSON.stringify(entry.localizationRefs)}` : ""}${content}`;
2146
2378
  }).join("\n")}\n${report.keyReview.note}` : `Key/content review evidence: none returned (${report.keyReview.entryCount} total entries).`,
2147
2379
  report.note,
2148
2380
  "This audit checks the current survey deterministically. It does not compare the draft with an attached source document, so a clean audit does not establish source fidelity or completeness."
@@ -2193,21 +2425,7 @@ const inputSchema = z.object({
2193
2425
  const slug = (value) => value.toLowerCase().replace(/\.[^.]+$/, "").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100) || "image";
2194
2426
  const OMITTED_EMBEDDED_IMAGE_DATA = "[embedded image data omitted from transcript]";
2195
2427
  const getAssetReferenceLabels = (snapshot, assetId) => {
2196
- const translations = (snapshot?.survey)?.translations;
2197
- if (!translations) return [];
2198
- const references = [];
2199
- const inspectContent = (content, label) => {
2200
- if (getAssetUsagesFromContent(content).some((usage) => usage.assetId === assetId)) references.push(label);
2201
- };
2202
- for (const [locale, localeTranslations] of Object.entries(translations)) {
2203
- for (const surface of [
2204
- "surveyCardContent",
2205
- "navigationContent",
2206
- "validationMessages"
2207
- ]) for (const [contentKey, content] of Object.entries(localeTranslations[surface] ?? {})) inspectContent(content, `${locale}:${surface}:${contentKey}`);
2208
- for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations ?? {})) for (const [contentKey, content] of Object.entries(itemTranslations ?? {})) inspectContent(content, `${locale}:item:${itemId}:${contentKey}`);
2209
- }
2210
- return references;
2428
+ return snapshot?.model.getAssetUsages(assetId).map((usage) => `${usage.locale}:${usage.ref.scope === "content" ? `${usage.ref.ownerId}:${usage.ref.role}` : `${usage.ref.surface}:${usage.ref.contentKey}`}`) ?? [];
2211
2429
  };
2212
2430
  const getToolOutput = (value) => value && typeof value === "object" && "output" in value && value.output !== void 0 ? value.output : value;
2213
2431
  const describeAssetOperation = (operation) => {
@@ -2227,8 +2445,8 @@ const summarizeAssetChange = (event) => {
2227
2445
  lines.push(`Imported: ${event.operations.filter((operation) => operation.kind === "set-image-asset").map(describeAssetOperation).join(", ")}`);
2228
2446
  lines.push(`Asset id to reference: ${importedAssetIds.join(", ")}`);
2229
2447
  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.");
2230
- lines.push(`New info-item image source: { "type": "asset", "assetId": "${importedAssetIds[0]}" }. Embed the canonical image block inside the create-item translation richTextBlocks array.`);
2231
- lines.push(`New info-item translation example: { "locale": "<selected-locale>", "contentKey": "content", "richTextBlocks": [{ "type": "image", "source": { "type": "asset", "assetId": "${importedAssetIds[0]}" }, "alt": "<meaningful description>" }] }.`);
2448
+ lines.push(`Information element image source: { "type": "asset", "assetId": "${importedAssetIds[0]}" }. Embed the canonical image block inside the create-item translation richTextBlocks array.`);
2449
+ lines.push(`Information element translation example: { "locale": "<selected-locale>", "ownerId": "<information-element-id>", "role": "body", "richTextBlocks": [{ "type": "image", "source": { "type": "asset", "assetId": "${importedAssetIds[0]}" }, "alt": "<meaningful description>" }] }.`);
2232
2450
  }
2233
2451
  }
2234
2452
  if (event.validation.issues.length > 0) lines.push(`Issues:\n${event.validation.issues.map((issue) => `- ${issue.code}: ${issue.message}`).join("\n")}`);
@@ -2277,13 +2495,13 @@ const surveyAssetTool = createTool({
2277
2495
  const manifest = access?.documents.find((entry) => entry.id === input.documentId);
2278
2496
  if (!access || !manifest) throw new Error(`Attachment "${input.documentId}" is not available for this turn.`);
2279
2497
  if (!manifest.mediaType.startsWith("image/")) throw new Error(`Attachment "${manifest.name}" is not an image.`);
2280
- const [loaded] = await access.load([input.documentId]);
2498
+ const loaded = (await access.load([input.documentId])).find((document) => document.documentId === input.documentId);
2281
2499
  if (!loaded) throw new Error(`Attachment "${manifest.name}" could not be loaded.`);
2282
2500
  const base = input.assetId ?? slug(manifest.name);
2283
2501
  let assetId = base;
2284
2502
  let suffix = 2;
2285
2503
  const stagedAssetIds = new Set(turnDraft.draftOperations.flatMap((draftOperation) => draftOperation.kind === "set-image-asset" ? [draftOperation.assetId] : []));
2286
- while (snapshot?.survey.assets?.[assetId] || stagedAssetIds.has(assetId)) assetId = `${base}-${suffix++}`;
2504
+ while (ownValue(snapshot?.survey.assets, assetId) || stagedAssetIds.has(assetId)) assetId = `${base}-${suffix++}`;
2287
2505
  operation = {
2288
2506
  kind: "set-image-asset",
2289
2507
  assetId,
@@ -2334,7 +2552,7 @@ const surveyAssetTool = createTool({
2334
2552
  validation: {
2335
2553
  ...event.validation,
2336
2554
  valid: false,
2337
- issues: [...event.validation.issues, {
2555
+ issues: [...event.validation.issues.filter((issue) => issue.code !== "image-asset-still-referenced"), {
2338
2556
  severity: "error",
2339
2557
  code: "image-asset-still-referenced",
2340
2558
  message: `Image asset "${operation.assetId}" is still referenced by ${referenceLabels.slice(0, 3).join(", ")}. Prepare one survey_change delta containing every reference-removal operation first and remove-image-asset last.`,
@@ -2378,4 +2596,4 @@ const surveyAssistantTools = Object.freeze({
2378
2596
  //#endregion
2379
2597
  export { surveyAuditTool as i, surveyAssistantTools as n, surveyAssetTool as r, SURVEY_ASSISTANT_TOOL_IDS as t };
2380
2598
 
2381
- //# sourceMappingURL=tools-BvSovehT.mjs.map
2599
+ //# sourceMappingURL=tools-D0KBDolR.mjs.map