@case-framework/survey-assistant 0.2.2 → 0.3.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 (107) hide show
  1. package/dist/{authoring-references-DYuOAdVx.mjs → authoring-references-yyYC4Ydi.mjs} +9 -3
  2. package/dist/authoring-references-yyYC4Ydi.mjs.map +1 -0
  3. package/dist/{capabilities-DlC1l6qy.d.mts → capabilities-CnsVuJ1k.d.mts} +12 -4
  4. package/dist/capabilities-CnsVuJ1k.d.mts.map +1 -0
  5. package/dist/capabilities-default.d.mts +1 -1
  6. package/dist/capabilities-default.d.mts.map +1 -1
  7. package/dist/capabilities-default.mjs +38 -4
  8. package/dist/capabilities-default.mjs.map +1 -1
  9. package/dist/{constants-D-5m_KGB.d.mts → constants-DrGZXtQf.d.mts} +12 -7
  10. package/dist/constants-DrGZXtQf.d.mts.map +1 -0
  11. package/dist/{controller-proxy-0WJZUZYx.mjs → controller-proxy-CpkQRezb.mjs} +3 -2
  12. package/dist/controller-proxy-CpkQRezb.mjs.map +1 -0
  13. package/dist/{controller-proxy-Hkuub9dt.d.mts → controller-proxy-DmBdzqKs.d.mts} +5 -2
  14. package/dist/controller-proxy-DmBdzqKs.d.mts.map +1 -0
  15. package/dist/{digest-BvXpeWcR.d.mts → digest-Dyjch_sC.d.mts} +2 -2
  16. package/dist/{digest-BvXpeWcR.d.mts.map → digest-Dyjch_sC.d.mts.map} +1 -1
  17. package/dist/digest.d.mts +1 -1
  18. package/dist/{engine-0AJrv0hI.mjs → engine-CWTuN3o2.mjs} +866 -663
  19. package/dist/engine-CWTuN3o2.mjs.map +1 -0
  20. package/dist/engine.d.mts +3 -3
  21. package/dist/engine.mjs +3 -3
  22. package/dist/fair-guidance-CEdJYccK.mjs +21 -0
  23. package/dist/fair-guidance-CEdJYccK.mjs.map +1 -0
  24. package/dist/form-authoring-DCd9XfXf.mjs +155 -0
  25. package/dist/form-authoring-DCd9XfXf.mjs.map +1 -0
  26. package/dist/{index-JXH-t2EJ.d.mts → index-Bo4dHAo8.d.mts} +68 -6
  27. package/dist/index-Bo4dHAo8.d.mts.map +1 -0
  28. package/dist/{index-MEOL9_Gr.d.mts → index-D4OitUg4.d.mts} +5 -430
  29. package/dist/index-D4OitUg4.d.mts.map +1 -0
  30. package/dist/{index-BcyfBeDj.d.mts → index-DZk20e3k.d.mts} +44 -22
  31. package/dist/index-DZk20e3k.d.mts.map +1 -0
  32. package/dist/{index-CneD8ibB.d.mts → index-Dxgg0-D_.d.mts} +12 -3
  33. package/dist/index-Dxgg0-D_.d.mts.map +1 -0
  34. package/dist/lifecycle-DWv5CiSu.d.mts +523 -0
  35. package/dist/lifecycle-DWv5CiSu.d.mts.map +1 -0
  36. package/dist/{memory-thread-repository-1ocPOda8.mjs → memory-thread-repository-acbz6HZJ.mjs} +181 -26
  37. package/dist/memory-thread-repository-acbz6HZJ.mjs.map +1 -0
  38. package/dist/{protocol-DpCYmCjr.mjs → protocol-DATvkr1S.mjs} +125 -6
  39. package/dist/protocol-DATvkr1S.mjs.map +1 -0
  40. package/dist/protocol.d.mts +4 -3
  41. package/dist/protocol.mjs +2 -2
  42. package/dist/{react-DXK5zWNo.mjs → react-Br5YE8H-.mjs} +132 -12
  43. package/dist/react-Br5YE8H-.mjs.map +1 -0
  44. package/dist/react-integration.d.mts +1 -1
  45. package/dist/react-integration.mjs +1 -1
  46. package/dist/react.d.mts +2 -2
  47. package/dist/react.mjs +2 -2
  48. package/dist/references/assistant-operations.md +13 -18
  49. package/dist/references/embedded-forms.md +41 -2
  50. package/dist/references/expressions.md +17 -9
  51. package/dist/references/fair-by-design.md +80 -0
  52. package/dist/references/item-types.md +19 -3
  53. package/dist/references/localization.md +7 -4
  54. package/dist/references/source-material-surveys.md +6 -4
  55. package/dist/{request-context-BBzU3VdG.mjs → request-context-BEjnaaFr.mjs} +10 -5
  56. package/dist/request-context-BEjnaaFr.mjs.map +1 -0
  57. package/dist/server-agent.d.mts +44 -22
  58. package/dist/server-agent.d.mts.map +1 -1
  59. package/dist/server-agent.mjs +71 -14
  60. package/dist/server-agent.mjs.map +1 -1
  61. package/dist/server-runtime.d.mts +22 -11
  62. package/dist/server-runtime.d.mts.map +1 -1
  63. package/dist/server-runtime.mjs +47 -19
  64. package/dist/server-runtime.mjs.map +1 -1
  65. package/dist/server-tasks.d.mts +1 -1
  66. package/dist/server-tasks.mjs +6 -4
  67. package/dist/server-tasks.mjs.map +1 -1
  68. package/dist/server-tools.d.mts +1 -1
  69. package/dist/server-tools.mjs +1 -1
  70. package/dist/server.d.mts +16 -7
  71. package/dist/server.d.mts.map +1 -1
  72. package/dist/server.mjs +4 -4
  73. package/dist/storage-postgres.d.mts +1 -1
  74. package/dist/{tasks-DcfQNJwO.d.mts → tasks-DQgL449A.d.mts} +8 -4
  75. package/dist/tasks-DQgL449A.d.mts.map +1 -0
  76. package/dist/{thread-documents-BlxNjc7S.d.mts → thread-documents-Dvo6XS7o.d.mts} +1 -1
  77. package/dist/{thread-documents-BlxNjc7S.d.mts.map → thread-documents-Dvo6XS7o.d.mts.map} +1 -1
  78. package/dist/{thread-handlers-uddXwQjp.d.mts → thread-handlers-CeYl45xG.d.mts} +9 -4
  79. package/dist/thread-handlers-CeYl45xG.d.mts.map +1 -0
  80. package/dist/{tools-wgLen05n.mjs → tools-Dg9HZrYU.mjs} +404 -78
  81. package/dist/tools-Dg9HZrYU.mjs.map +1 -0
  82. package/dist/ui.css +2 -2
  83. package/dist/ui.d.mts +94 -10
  84. package/dist/ui.d.mts.map +1 -1
  85. package/dist/ui.mjs +343 -112
  86. package/dist/ui.mjs.map +1 -1
  87. package/docs/integration.md +74 -6
  88. package/package.json +25 -19
  89. package/dist/authoring-references-DYuOAdVx.mjs.map +0 -1
  90. package/dist/capabilities-DKbNCnBf.mjs +0 -66
  91. package/dist/capabilities-DKbNCnBf.mjs.map +0 -1
  92. package/dist/capabilities-DlC1l6qy.d.mts.map +0 -1
  93. package/dist/constants-D-5m_KGB.d.mts.map +0 -1
  94. package/dist/controller-proxy-0WJZUZYx.mjs.map +0 -1
  95. package/dist/controller-proxy-Hkuub9dt.d.mts.map +0 -1
  96. package/dist/engine-0AJrv0hI.mjs.map +0 -1
  97. package/dist/index-BcyfBeDj.d.mts.map +0 -1
  98. package/dist/index-CneD8ibB.d.mts.map +0 -1
  99. package/dist/index-JXH-t2EJ.d.mts.map +0 -1
  100. package/dist/index-MEOL9_Gr.d.mts.map +0 -1
  101. package/dist/memory-thread-repository-1ocPOda8.mjs.map +0 -1
  102. package/dist/protocol-DpCYmCjr.mjs.map +0 -1
  103. package/dist/react-DXK5zWNo.mjs.map +0 -1
  104. package/dist/request-context-BBzU3VdG.mjs.map +0 -1
  105. package/dist/tasks-DcfQNJwO.d.mts.map +0 -1
  106. package/dist/thread-handlers-uddXwQjp.d.mts.map +0 -1
  107. package/dist/tools-wgLen05n.mjs.map +0 -1
@@ -1,16 +1,16 @@
1
- import { G as assistantItemTypeManifestSchema, J as SURVEY_OPERATION_KINDS, _t as surveyOperationSchema, u as surveyChangeSetSchema, vt as surveyOperationSchemasByKind } from "./protocol-DpCYmCjr.mjs";
2
- import { a as getDeterministicCreateItemTypes, o as inspectAssistantItem, s as resolveAssistantCapabilityReference } from "./capabilities-DKbNCnBf.mjs";
3
- import { A as getRawSurvey, B as resolveItemSummary, C as clampLimit, F as mapKeyIndexById, H as summarizeFocusItemNode, I as mapOutlineById, J as translationEntrySchema, K as summarizeOutlineNode, L as mapRawItemsById, M as itemConfigDetailsSchema, N as itemMatchesQuery, P as itemSummarySchema, R as normalizeText, U as summarizeItemById, V as sliceWithCursor, W as summarizeKeyIndexEntries, Y as unique, _ as buildConfigSearchText, b as buildItemTranslationEntries, d as normalizeSurveyChangeOperations, f as buildExpressionMutationPatches, j as getRawSurveyItems, k as getKeyIndexEntries, l as createSurveyChangeSet, p as expressionMutationSchema, q as summarizeRawItem, t as auditSurvey, v as buildEmbeddedFormDetails, w as findDirectChildrenByFullKey, y as buildItemConfigDetails } from "./engine-0AJrv0hI.mjs";
4
- import { o as requireSurveyAssistantTurnState, r as getSurveyAssistantEngineContext } from "./request-context-BBzU3VdG.mjs";
5
- import { n as surveyAuthoringReferenceTopics, r as surveyAuthoringReferences, t as getBuiltInItemTypeAuthoringReference } from "./authoring-references-DYuOAdVx.mjs";
1
+ import { X as SURVEY_OPERATION_KINDS, bt as surveyOperationSchemasByKind, f as surveyChangeSetSchema, q as assistantItemTypeManifestSchema, yt as surveyOperationSchema } from "./protocol-DATvkr1S.mjs";
2
+ import { d as getDeterministicCreateItemTypes, f as inspectAssistantItem, p as resolveAssistantCapabilityReference } from "./form-authoring-DCd9XfXf.mjs";
3
+ import { A as getRawSurvey, B as resolveItemSummary, C as clampLimit, F as mapKeyIndexById, H as summarizeFocusItemNode, I as mapOutlineById, J as translationEntrySchema, K as summarizeOutlineNode, L as mapRawItemsById, M as itemConfigDetailsSchema, N as itemMatchesQuery, P as itemSummarySchema, R as normalizeText, U as summarizeItemById, V as sliceWithCursor, W as summarizeKeyIndexEntries, Y as unique, _ as buildConfigSearchText, b as buildItemTranslationEntries, d as normalizeSurveyChangeOperations, f as buildExpressionMutationPatches, i as auditSurvey, j as getRawSurveyItems, k as getKeyIndexEntries, l as createSurveyChangeSet, p as expressionMutationSchema, q as summarizeRawItem, v as buildEmbeddedFormDetails, w as findDirectChildrenByFullKey, y as buildItemConfigDetails } from "./engine-CWTuN3o2.mjs";
4
+ import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-BEjnaaFr.mjs";
5
+ import { i as surveyAuthoringReferences, n as getBuiltInItemTypeAuthoringReference, r as surveyAuthoringReferenceTopics, t as builtInSurveyAuthoringItemTypes } from "./authoring-references-yyYC4Ydi.mjs";
6
6
  import { z } from "zod";
7
- import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getValueAtJsonPointer } from "@case-framework/survey-core";
7
+ import { JsonPatchError, SURVEY_EDITOR_ITEM_COLORS, encodeJsonPointerSegment, getAssetUsagesFromContent, getContentPlainText, getValueAtJsonPointer } from "@case-framework/survey-core";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createTool, isValidationError } from "@mastra/core/tools";
10
10
  //#region src/server/operation-contract-reference.ts
11
11
  const isSurveyOperationKind = (value) => SURVEY_OPERATION_KINDS.includes(value);
12
12
  const SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS = ["compact", "full"];
13
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
14
  const summarizeLargeSchema = (schema, propertyName) => {
15
15
  const description = typeof schema.description === "string" ? schema.description : propertyName ? `Canonical value for "${propertyName}".` : "Canonical value.";
16
16
  const type = typeof schema.type === "string" ? schema.type : void 0;
@@ -21,7 +21,7 @@ const summarizeLargeSchema = (schema, propertyName) => {
21
21
  };
22
22
  const compactJsonSchema = (value, propertyName, depth = 0) => {
23
23
  if (Array.isArray(value)) return value.map((entry) => compactJsonSchema(entry, propertyName, depth + 1));
24
- if (!isRecord$1(value)) return value;
24
+ if (!isRecord$2(value)) return value;
25
25
  if (typeof value.$ref === "string") return { description: "Recursive structured value omitted from this compact reference; request full detail only for protocol debugging." };
26
26
  if (depth >= 3 && JSON.stringify(value).length > 12e3) return summarizeLargeSchema(value, propertyName);
27
27
  const compact = {};
@@ -39,7 +39,7 @@ const compactJsonSchema = (value, propertyName, depth = 0) => {
39
39
  "required",
40
40
  "additionalProperties"
41
41
  ]) if (value[key] !== void 0) compact[key] = value[key];
42
- if (isRecord$1(value.properties)) compact.properties = Object.fromEntries(Object.entries(value.properties).map(([key, schema]) => [key, compactJsonSchema(schema, key, depth + 1)]));
42
+ if (isRecord$2(value.properties)) compact.properties = Object.fromEntries(Object.entries(value.properties).map(([key, schema]) => [key, compactJsonSchema(schema, key, depth + 1)]));
43
43
  if (value.items !== void 0) compact.items = compactJsonSchema(value.items, propertyName, depth + 1);
44
44
  for (const unionKey of ["oneOf", "anyOf"]) if (Array.isArray(value[unionKey])) compact[unionKey] = value[unionKey].map((entry) => compactJsonSchema(entry, propertyName, depth + 1));
45
45
  return compact;
@@ -54,6 +54,7 @@ const formatItemCapabilityProfile = (manifest, kind) => {
54
54
  `${manifest.displayName}: ${manifest.description ?? "No additional description."}`,
55
55
  `Creation mode: ${manifest.create}.`,
56
56
  manifest.authoring?.configSummary ? `Configuration: ${manifest.authoring.configSummary}` : void 0,
57
+ 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,
57
58
  fixedSurfaces.length > 0 ? `Fixed content surfaces: ${fixedSurfaces.map((surface) => surface.key).join(", ")}.` : "Fixed content surfaces: none.",
58
59
  patternedSurfaces.length > 0 ? `Dynamic content surfaces: ${patternedSurfaces.map((surface) => surface.key).join(", ")}.` : void 0,
59
60
  kind === "create-item" && requiredSurfaces.length > 0 ? `Required on creation: ${requiredSurfaces.map((surface) => surface.key).join(", ")}.` : void 0,
@@ -87,6 +88,55 @@ const formatSurveyOperationContractReference = (kind, options = {}) => {
87
88
  ].join("\n");
88
89
  };
89
90
  //#endregion
91
+ //#region src/server/canonical-name-suggestions.ts
92
+ const CANONICAL_ACTION_TOKENS = {
93
+ add: "create",
94
+ append: "create",
95
+ create: "create",
96
+ delete: "remove",
97
+ edit: "update",
98
+ insert: "create",
99
+ modify: "update",
100
+ patch: "update",
101
+ relocate: "move",
102
+ remove: "remove",
103
+ reorder: "reorder",
104
+ replace: "update",
105
+ set: "update",
106
+ sort: "reorder",
107
+ update: "update"
108
+ };
109
+ const tokenizeCanonicalName = (value) => value.normalize("NFKD").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map((token) => CANONICAL_ACTION_TOKENS[token] ?? token);
110
+ const editDistance = (left, right) => {
111
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
112
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
113
+ const current = [leftIndex];
114
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
115
+ previous.splice(0, previous.length, ...current);
116
+ }
117
+ return previous[right.length] ?? 0;
118
+ };
119
+ const scoreCanonicalName = (input, candidate) => {
120
+ const inputTokens = tokenizeCanonicalName(input);
121
+ const candidateTokens = tokenizeCanonicalName(candidate);
122
+ const normalizedInput = inputTokens.join("-");
123
+ const normalizedCandidate = candidateTokens.join("-");
124
+ if (!normalizedInput || !normalizedCandidate) return 0;
125
+ if (normalizedInput === normalizedCandidate) return 4;
126
+ const maximumLength = Math.max(normalizedInput.length, normalizedCandidate.length);
127
+ const similarity = 1 - editDistance(normalizedInput, normalizedCandidate) / maximumLength;
128
+ const candidateTokenSet = new Set(candidateTokens);
129
+ const tokenCoverage = inputTokens.filter((token) => candidateTokenSet.has(token)).length / Math.max(inputTokens.length, candidateTokens.length);
130
+ const sameFinalToken = inputTokens.at(-1) === candidateTokens.at(-1) ? .5 : 0;
131
+ const sameFirstToken = inputTokens[0] === candidateTokens[0] ? .2 : 0;
132
+ return similarity + tokenCoverage + sameFinalToken + sameFirstToken;
133
+ };
134
+ /** Suggests registry-derived canonical names without silently accepting an alias. */
135
+ const suggestCanonicalNames = (input, candidates, limit = 3) => [...new Set(candidates)].filter((candidate) => candidate !== input).map((candidate) => ({
136
+ candidate,
137
+ score: scoreCanonicalName(input, candidate)
138
+ })).filter((entry) => entry.score >= .9).sort((left, right) => right.score - left.score || left.candidate.localeCompare(right.candidate)).slice(0, Math.max(0, limit)).map((entry) => entry.candidate);
139
+ //#endregion
90
140
  //#region src/server/tools/tool-model-output.ts
91
141
  const FALLBACK_MODEL_TEXT = "The tool returned no model-readable output. Review the tool arguments and retry with valid values.";
92
142
  const unwrapToolOutput$1 = (value) => value && typeof value === "object" && "output" in value && value.output !== void 0 ? value.output : value;
@@ -110,25 +160,31 @@ const toTextModelOutput = (value, summarize) => {
110
160
  //#region src/server/tools/survey-authoring-reference-tool.ts
111
161
  const TOOL_ID$10 = "survey_authoring_reference";
112
162
  const topicSchema = z.string().min(1).max(120);
163
+ const inputSchema$10 = z.object({
164
+ topic: topicSchema.describe("The narrow authoring reference to load."),
165
+ 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."),
166
+ operationKind: z.string().min(1).max(120).optional().describe("For topic assistant-operations, return only this operation's generated canonical field contract."),
167
+ 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."),
168
+ reason: z.string().max(500).optional().describe("Short reason this reference is needed.")
169
+ });
170
+ const outputSchema$8 = z.object({
171
+ ok: z.boolean(),
172
+ topic: topicSchema,
173
+ itemType: z.string().optional(),
174
+ operationKind: z.string().optional(),
175
+ detail: z.enum(SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS).optional(),
176
+ title: z.string(),
177
+ content: z.string()
178
+ });
179
+ const formatCanonicalNameRecovery = (input, candidates, noun) => {
180
+ const suggestions = suggestCanonicalNames(input, candidates);
181
+ return suggestions.length > 0 ? ` Closest canonical ${noun}${suggestions.length === 1 ? "" : "s"}: ${suggestions.map((value) => `"${value}"`).join(", ")}. Retry with the intended canonical name.` : " Retry without the focused name to load the available catalog.";
182
+ };
113
183
  const surveyAuthoringReferenceTool = createTool({
114
184
  id: TOOL_ID$10,
115
185
  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.",
116
- inputSchema: z.object({
117
- topic: topicSchema.describe("The narrow authoring reference to load."),
118
- 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."),
119
- operationKind: z.string().min(1).max(120).optional().describe("For topic assistant-operations, return only this operation's generated canonical field contract."),
120
- 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."),
121
- reason: z.string().max(500).optional().describe("Short reason this reference is needed.")
122
- }),
123
- outputSchema: z.object({
124
- ok: z.boolean(),
125
- topic: topicSchema,
126
- itemType: z.string().optional(),
127
- operationKind: z.string().optional(),
128
- detail: z.enum(SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS).optional(),
129
- title: z.string(),
130
- content: z.string()
131
- }),
186
+ inputSchema: inputSchema$10,
187
+ outputSchema: outputSchema$8,
132
188
  mcp: { annotations: {
133
189
  title: "Read authoring reference",
134
190
  readOnlyHint: true,
@@ -140,6 +196,7 @@ const surveyAuthoringReferenceTool = createTool({
140
196
  const contractDetail = inputData.detail ?? "compact";
141
197
  const engineContext = getSurveyAssistantEngineContext(context?.requestContext);
142
198
  const itemCapability = inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.manifest ?? engineContext?.capabilities.itemTypes.find((entry) => entry.itemType === inputData.itemType) : void 0;
199
+ const availableItemTypes = [...builtInSurveyAuthoringItemTypes, ...engineContext?.capabilities.itemTypes.map((entry) => entry.itemType) ?? []];
143
200
  if (inputData.operationKind && inputData.topic !== "assistant-operations") return {
144
201
  ok: false,
145
202
  topic: inputData.topic,
@@ -155,16 +212,16 @@ const surveyAuthoringReferenceTool = createTool({
155
212
  operationKind: inputData.operationKind,
156
213
  detail: contractDetail,
157
214
  title: "Operation contract unavailable",
158
- content: `No built-in survey_change operation named "${inputData.operationKind}" is available.`
215
+ content: `No built-in survey_change operation named "${inputData.operationKind}" is available.` + formatCanonicalNameRecovery(inputData.operationKind, SURVEY_OPERATION_KINDS, "operation")
159
216
  };
160
- if (inputData.itemType && !itemCapability) return {
217
+ if (inputData.itemType && !itemCapability && !getBuiltInItemTypeAuthoringReference(inputData.itemType)) return {
161
218
  ok: false,
162
219
  topic: inputData.topic,
163
220
  itemType: inputData.itemType,
164
221
  operationKind: inputData.operationKind,
165
222
  detail: contractDetail,
166
223
  title: "Item capability unavailable",
167
- content: `No installed survey item capability named "${inputData.itemType}" is available.`
224
+ content: `No installed survey item capability named "${inputData.itemType}" is available.` + formatCanonicalNameRecovery(inputData.itemType, availableItemTypes, "item type")
168
225
  };
169
226
  if (itemCapability && !itemCapability.supportedOperations.includes(inputData.operationKind)) return {
170
227
  ok: false,
@@ -196,6 +253,13 @@ const surveyAuthoringReferenceTool = createTool({
196
253
  topic: inputData.topic
197
254
  }) : void 0;
198
255
  const focusedBuiltInItemTypeReference = inputData.topic === "item-types" && inputData.itemType ? getBuiltInItemTypeAuthoringReference(inputData.itemType) : void 0;
256
+ if (inputData.itemType && !focusedBuiltInItemTypeReference && !capability && !capabilityReference) return {
257
+ ok: false,
258
+ topic: inputData.topic,
259
+ itemType: inputData.itemType,
260
+ title: "Item capability unavailable",
261
+ content: `No installed survey item capability named "${inputData.itemType}" is available.` + formatCanonicalNameRecovery(inputData.itemType, availableItemTypes, "item type")
262
+ };
199
263
  const capabilityEntry = capabilityReference ? {
200
264
  title: capabilityReference.title ?? `${capability?.manifest.displayName} authoring`,
201
265
  content: capabilityReference.markdown
@@ -207,7 +271,7 @@ const surveyAuthoringReferenceTool = createTool({
207
271
  itemType: inputData.itemType,
208
272
  operationKind: inputData.operationKind,
209
273
  title: "Reference unavailable",
210
- content: inputData.itemType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.itemType}".` : `No built-in reference named "${inputData.topic}" is available.`
274
+ content: (inputData.itemType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.itemType}".` : `No built-in reference named "${inputData.topic}" is available.`) + formatCanonicalNameRecovery(inputData.topic, surveyAuthoringReferenceTopics, "reference")
211
275
  };
212
276
  return {
213
277
  ok: true,
@@ -229,7 +293,8 @@ const OPERATIONS_DESCRIPTION = [
229
293
  "Any translations owned by an operation must be inside that same operation object. Never put translations beside the survey_change operations array.",
230
294
  "Create items with { kind: \"create-item\", target: { parentItemId, index? }, item: { id?, key, itemType, config }, translations? }. Put respondent content in top-level translations and item settings in item.config.",
231
295
  "Use update-item-translation with itemId, locale, contentKey, and plainText for ordinary text or content for a complete structured value. Item titles and option/field labels are translations, not item labels.",
232
- "Use remove-survey-locale with locale to remove an existing survey language; the final remaining locale cannot be removed.",
296
+ "Use add-survey-locale only when the user explicitly requests an empty language registration. An ordinary localization request requires real translation operations, which introduce the locale themselves.",
297
+ "Use remove-survey-locale with locale to destructively remove an existing survey language and all translations stored for it.",
233
298
  "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.",
234
299
  "For an existing choice item, use add-choice-option with itemId, optional locale/index, and option: { id?, key?, label }; label is a plain string and creates its translation.",
235
300
  "Every reorder replaces the complete inspected list: reorder-choice-options uses optionIds, reorder-form-field-groups uses groupIds, and reorder-items uses itemIds for every direct child. Include every existing entry exactly once in desired order.",
@@ -238,7 +303,7 @@ const OPERATIONS_DESCRIPTION = [
238
303
  "Use update-placed-image only for an image placement that already exists in the live survey or validated turn draft.",
239
304
  "When deleting referenced images and assets, include every reference-removal operation and remove-image-asset last in the same operations array.",
240
305
  "Raw fallback is one { kind: \"survey-json-patch\", patches } operation containing all related survey-rooted edits; it must be the only operation in the call.",
241
- "Keep each delta bounded; successful chunks accumulate automatically in the turn draft."
306
+ "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."
242
307
  ].join(" ");
243
308
  const inputSchema$9 = z.object({
244
309
  summary: z.string().min(1).max(500).describe("Short user-facing summary of the intended survey change."),
@@ -289,6 +354,7 @@ const describeOperationTarget = (operation) => {
289
354
  case "update-survey-metadata":
290
355
  case "update-survey-pagination": return operation.kind;
291
356
  case "update-survey-translation": return `${operation.kind} ${operation.locale}:${operation.surface}:${operation.contentKey}`;
357
+ case "add-survey-locale":
292
358
  case "remove-survey-locale": return `${operation.kind} ${operation.locale}`;
293
359
  case "set-image-asset":
294
360
  case "update-image-asset-metadata":
@@ -300,20 +366,48 @@ const describeOperationTarget = (operation) => {
300
366
  const summarizeValidation = (event) => {
301
367
  const lines = [`${event.status === "validated" ? "Validated" : "Rejected"} survey change: ${event.summary}`];
302
368
  if (event.status === "rejected") {
303
- lines.push("Draft unchanged. Fix this same rejected target in another call; the previously validated draft is retained. An unrelated valid edit does not repair this rejection.");
369
+ lines.push("TURN APPLY STATE: BLOCKED. Draft unchanged. This rejection does not finalize the editor or end the active turn. Fix this same rejected target in another survey_change call; the previously validated request-local draft is retained only for that repair. An unrelated valid edit does not repair this rejection.", "If this response ends while the turn is blocked, no prepared change from this turn, including every previously validated change, will reach the live editor. Do not tell the user that request-local draft content exists in the editor; either repair the rejected target now or state that none of this turn's changes were applied.");
304
370
  if (event.operations.length > 0) {
305
371
  const checklist = event.operations.slice(0, 24).map(describeOperationTarget);
306
372
  lines.push(`Unapplied target sample (${checklist.length} of ${event.operations.length}):\n${checklist.map((target) => `- ${target}`).join("\n")}`);
307
373
  if (event.operations.length > checklist.length) lines.push(`The sample omits ${event.operations.length - checklist.length} additional operation target(s). The original tool input is authoritative; preserve its complete requested scope when retrying.`);
308
374
  }
309
375
  }
310
- if (event.status === "validated") 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.`);
376
+ if (event.status === "validated") {
377
+ lines.push(`Draft updated (${event.operations.length} total operation${event.operations.length === 1 ? "" : "s"}). Later tools can inspect and edit the resulting draft state. The live editor is unchanged until successful turn completion.`);
378
+ if (event.postChange) {
379
+ const unavailableItemCount = event.postChange.touchedItemCount - event.postChange.availableTouchedItemCount;
380
+ lines.push(`Canonical post-change preview (first occurrence in operation order; projected fields only): ${event.postChange.itemCount} survey item(s); locales ${event.postChange.locales.length}/${event.postChange.localeCount} shown; targets ${event.postChange.touchedTargets.length}/${event.postChange.targetCount} shown; available touched-item snapshots ${event.postChange.touchedItems.length}/${event.postChange.availableTouchedItemCount} shown${unavailableItemCount > 0 ? `; ${unavailableItemCount} touched item target(s) are absent after the change` : ""}.`);
381
+ lines.push(`Target preview: ${event.postChange.touchedTargets.join(", ") || "none"}. Absence from this bounded projection does not prove that a field or target is absent from the canonical draft.`);
382
+ for (const item of event.postChange.touchedItems) {
383
+ const translations = item.translations.map(({ contentKey, locale, textLength, textPreview, truncated }) => truncated ? `${locale}:${contentKey}=${JSON.stringify(`${textPreview}…`)} [truncated preview; ${textLength} characters total]` : `${locale}:${contentKey}=${JSON.stringify(textPreview)}`).join("; ");
384
+ lines.push(`- ${item.fullKey} (${item.itemType}, id ${item.itemId}); response slots ${item.responseSlots.length}/${item.responseSlotCount}: ${item.responseSlots.join(", ") || "none"}; translations ${item.translations.length}/${item.translationCount}: ${translations || "none"}`);
385
+ }
386
+ if (event.postChange.truncated) lines.push("The post-change preview is truncated. The validated draft still contains the complete change; inspect any relevant omitted target or field before claiming its exact post-change value.");
387
+ }
388
+ }
389
+ if (event.verification) {
390
+ lines.push(`Verification required before claiming completion (${event.verification.reasons.join(", ")}). Inspect the exact affected targets in the current draft; validation alone does not prove source fidelity or every omitted preview value.`);
391
+ if (event.verification.newAuditFindingCount > 0) {
392
+ lines.push(`New deterministic audit findings: ${event.verification.auditFindings.length}/${event.verification.newAuditFindingCount} shown.`);
393
+ for (const finding of event.verification.auditFindings) {
394
+ const location = [
395
+ finding.itemId,
396
+ finding.locale,
397
+ finding.contentKey
398
+ ].filter(Boolean).join(":");
399
+ lines.push(`- ${finding.severity} ${finding.code}${location ? ` (${location})` : ""}: ${finding.messagePreview}${finding.truncated ? `… [truncated preview; ${finding.messageLength} characters total]` : ""}`);
400
+ }
401
+ if (event.verification.truncated) lines.push("Verification diagnostics are bounded. Use survey_audit or focused survey_inspect calls for the complete relevant state.");
402
+ }
403
+ }
311
404
  if (event.validation.issues.length > 0) {
312
405
  lines.push("Issues:");
313
406
  for (const issue of event.validation.issues) lines.push(`- ${issue.code}: ${issue.message}`);
314
407
  const issueCodes = new Set(event.validation.issues.map((issue) => issue.code));
315
- if (issueCodes.has("missing-create-content") || issueCodes.has("missing-field-groups-wrapper") || issueCodes.has("missing-choice-options") || issueCodes.has("missing-choice-option-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.");
408
+ 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.");
316
409
  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.");
410
+ 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.");
317
411
  }
318
412
  if (event.validation.warnings.length > 0) {
319
413
  lines.push("Warnings:");
@@ -366,12 +460,12 @@ const prepareActionSchema = z.object({
366
460
  mutations: z.array(expressionMutationSchema).min(1).max(40)
367
461
  });
368
462
  const inputSchema$8 = z.object({
369
- action: z.enum(["context", "prepare"]).describe("Use context to inspect expression refs; use prepare to create expression edits."),
463
+ action: z.enum(["context", "prepare"]).describe("Choose exactly one envelope. Inspect with { \"action\": \"context\", \"itemIds\"?: [...], \"includeRawLocations\"?: true, \"reason\"?: \"...\" }; omit summary and mutations. Create edits with { \"action\": \"prepare\", \"summary\": \"...\", \"mutations\": [...] }; omit reason, itemIds, and includeRawLocations."),
370
464
  reason: z.string().max(500).optional(),
371
- itemIds: z.array(z.string().min(1).max(240)).max(12).optional().describe("For context: optional item ids/keys to focus existing expression locations."),
372
- includeRawLocations: z.boolean().optional().describe("For context: include existing expression locations by raw item."),
373
- summary: z.string().min(1).max(500).optional().describe("For prepare: user-facing summary."),
374
- mutations: z.array(z.record(z.string(), z.unknown())).min(1).max(40).optional().describe("For prepare: expression mutation objects using kebab-case kind names. Item visibility shape: { kind: \"set-item-display-condition\", itemId: \"<existing-item-id>\", expression: { type: \"function\", functionName: \"eq\", arguments: [...] } }. expression is always CASE expression JSON, never a string or expression-language snippet. Other supported kinds set/clear component display/disabled conditions, validation expressions, prefills, and template values.")
465
+ itemIds: z.array(z.string().min(1).max(240)).max(12).optional().describe("Context action only: optional item ids/keys to focus existing expression locations. Omit for prepare."),
466
+ includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item. Omit for prepare."),
467
+ summary: z.string().min(1).max(500).optional().describe("Prepare action only: required user-facing summary. Omit for context."),
468
+ mutations: z.array(z.record(z.string(), z.unknown())).min(1).max(40).optional().describe("Prepare action only: required expression mutation objects using kebab-case kind names; omit this field for context. Item visibility shape: { kind: \"set-item-display-condition\", itemId: \"<existing-item-id>\", expression: { type: \"function\", functionName: \"eq\", arguments: [...] } }. expression is always CASE expression JSON, never a string or expression-language snippet. Other supported kinds set/clear component display/disabled conditions, validation expressions, prefills, and template values.")
375
469
  }).strict().superRefine((value, ctx) => {
376
470
  if (value.action !== "prepare") return;
377
471
  if (!value.summary) ctx.addIssue({
@@ -442,7 +536,7 @@ const rejectedEvent = ({ issues, summary, turnId, warnings = [] }) => ({
442
536
  const summarizeDraftEvent = (event) => {
443
537
  const lines = [`${event.status === "validated" ? "Validated" : "Rejected"} expression change: ${event.summary}`];
444
538
  if (event.status === "validated") lines.push(`Draft updated (${event.operations.length} total operation${event.operations.length === 1 ? "" : "s"}). The expression is immediately visible to later tools in this turn.`);
445
- if (event.status === "rejected") lines.push("Draft unchanged. Fix this same expression target; the previously validated draft is retained. An unrelated valid edit does not repair this rejection.");
539
+ if (event.status === "rejected") lines.push("TURN APPLY STATE: BLOCKED. Draft unchanged. Fix this same expression target; the previously validated request-local draft is retained only for that repair. An unrelated valid edit does not repair this rejection.", "If this response ends while the turn is blocked, no prepared change from this turn, including every previously validated change, will reach the live editor. Do not tell the user that request-local draft content exists in the editor; either repair the rejected target now or state that none of this turn's changes were applied.");
446
540
  if (event.validation.issues.length > 0) {
447
541
  lines.push("Issues:");
448
542
  for (const issue of event.validation.issues) lines.push(`- ${issue.code}: ${issue.message}`);
@@ -1009,7 +1103,7 @@ const summarizeConfigDetails = (configDetails) => {
1009
1103
  for (const option of configDetails.choiceOptions ?? []) {
1010
1104
  lines.push(`- ${option.position}. option ${option.key ?? option.id} (id=${option.id}, labelContentKey=${option.labelContentKey})`);
1011
1105
  if (option.embeddedFormItemId || option.embeddedFormFieldGroupCount) {
1012
- lines.push(` embedded form: itemId=${option.embeddedFormItemId ?? "inline"}, fieldGroups=${option.embeddedFormFieldGroupCount ?? 0}`);
1106
+ lines.push(` embedded form: itemId=${option.embeddedFormItemId ?? "inline"}, fieldGroups=${option.embeddedFormFieldGroupCount ?? 0}, authoringMode=${option.embeddedFormAuthoringMode ?? "custom"}, simpleModeValid=${option.embeddedFormSimpleModeValid ?? true}`);
1013
1107
  if (option.embeddedFormTranslationPrefix || option.embeddedFormResponseSlotPrefix) lines.push(` embedded form keys: translationPrefix=${option.embeddedFormTranslationPrefix ?? "n/a"}, responseSlotPrefix=${option.embeddedFormResponseSlotPrefix ?? "n/a"}`);
1014
1108
  }
1015
1109
  }
@@ -1172,9 +1266,9 @@ const outputSchema$4 = z.object({
1172
1266
  error: z.string().optional()
1173
1267
  })).optional()
1174
1268
  });
1175
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1269
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1176
1270
  const getGroupChildren = (item) => {
1177
- if (!isRecord(item) || !isRecord(item.config) || !Array.isArray(item.config.items)) return [];
1271
+ if (!isRecord$1(item) || !isRecord$1(item.config) || !Array.isArray(item.config.items)) return [];
1178
1272
  return item.config.items.filter((entry) => typeof entry === "string");
1179
1273
  };
1180
1274
  const buildParentByChildId = (rawSurvey) => {
@@ -1267,7 +1361,7 @@ const buildItemValues = (snapshot, input) => {
1267
1361
  const itemId = resolved.item.id;
1268
1362
  const rawItemOutput = toOutputValue(resolved.item, input.valueCharacterLimit);
1269
1363
  const translationsOutput = toOutputValue(getTranslationsByLocaleForItem(rawSurvey, itemId), input.valueCharacterLimit);
1270
- const translationsValue = "value" in translationsOutput && isRecord(translationsOutput.value) ? translationsOutput.value : void 0;
1364
+ const translationsValue = "value" in translationsOutput && isRecord$1(translationsOutput.value) ? translationsOutput.value : void 0;
1271
1365
  return {
1272
1366
  identifier,
1273
1367
  found: true,
@@ -1585,6 +1679,7 @@ const inspectActionSchema = z.enum([
1585
1679
  "current-context",
1586
1680
  "search-items",
1587
1681
  "item-details",
1682
+ "localization",
1588
1683
  "assets",
1589
1684
  "raw-json"
1590
1685
  ]);
@@ -1593,6 +1688,8 @@ const outlineModeSchema = z.enum([
1593
1688
  "focused",
1594
1689
  "children"
1595
1690
  ]);
1691
+ const MAX_RAW_JSON_PATHS_PER_RESULT = 20;
1692
+ const MAX_RAW_JSON_PATHS_PER_REQUEST = 100;
1596
1693
  const optionalInput = (schema) => z.preprocess((value) => value === null ? void 0 : value, schema.optional());
1597
1694
  const optionalArrayInput = (schema) => z.preprocess((value) => value === null || Array.isArray(value) && value.length === 0 ? void 0 : value, schema.optional());
1598
1695
  const optionalNonBlankString = (maxLength) => z.preprocess((value) => value === null || typeof value === "string" && value.trim().length === 0 ? void 0 : value, z.string().min(1).max(maxLength).optional());
@@ -1608,10 +1705,13 @@ const inputSchema$3 = z.object({
1608
1705
  "history"
1609
1706
  ])).min(1).max(6)).describe("For current-context: context sections to inspect. Empty means omitted."),
1610
1707
  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."),
1708
+ sourceLocale: optionalNonBlankString(40).describe("For localization: source locale to translate. Defaults to the selected editor locale."),
1709
+ targetLocale: optionalNonBlankString(40).describe("For localization: required target locale whose coverage should be inspected."),
1710
+ missingOnly: optionalInput(z.boolean()).describe("For localization: return only source surfaces that do not yet exist in the target locale."),
1611
1711
  itemRef: optionalNonBlankString(240).describe("For current-context outline, item-details, or raw-json: item id, key, fullKey, visible label, or \"selected\"."),
1612
- cursor: optionalNonBlankString(240).describe("For current-context/search-items pagination: numeric cursor from a previous result."),
1712
+ cursor: optionalNonBlankString(240).describe("For current-context, search-items, or localization pagination: numeric cursor from a previous result."),
1613
1713
  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."),
1614
- paths: optionalArrayInput(z.array(z.string().min(1).max(500)).min(1).max(20)).describe("For raw-json: exact JSON Pointer paths to inspect. Empty means omitted."),
1714
+ 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.`),
1615
1715
  includeChildren: optionalInput(z.boolean()).describe("For item-details: include direct survey-tree children."),
1616
1716
  includeItemConfig: optionalInput(z.boolean()).describe("For item-details: include choice/form/internal item config details."),
1617
1717
  limit: optionalInput(z.number().int().min(1).max(50)).describe("Maximum list rows to return.")
@@ -1636,6 +1736,89 @@ const stringifyForModel = (value) => {
1636
1736
  if (typeof value === "string") return value;
1637
1737
  return JSON.stringify(value, null, 2);
1638
1738
  };
1739
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1740
+ const MAX_LOCALIZATION_TEXT_CHARS = 2e3;
1741
+ const SURVEY_TRANSLATION_SURFACES = [
1742
+ "surveyCardContent",
1743
+ "navigationContent",
1744
+ "validationMessages"
1745
+ ];
1746
+ const truncateLocalizationText = (content) => {
1747
+ const text = getContentPlainText(content).trim();
1748
+ return text.length > MAX_LOCALIZATION_TEXT_CHARS ? `${text.slice(0, MAX_LOCALIZATION_TEXT_CHARS)}...` : text;
1749
+ };
1750
+ const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1751
+ const survey = snapshot.survey;
1752
+ const sourceLocale = input.sourceLocale ?? snapshot.ui.selectedLocale;
1753
+ const sourceTranslations = survey.translations?.[sourceLocale];
1754
+ const targetTranslations = survey.translations?.[targetLocale];
1755
+ if (!sourceTranslations) return {
1756
+ ok: false,
1757
+ reason: `Source locale "${sourceLocale}" is not present in the current survey draft.`
1758
+ };
1759
+ const surfaces = [];
1760
+ const addSurface = ({ contentKey, itemId, itemKey, itemType, sourceContent, surface, targetContent }) => {
1761
+ const sourceText = truncateLocalizationText(sourceContent);
1762
+ if (!sourceText) return;
1763
+ surfaces.push({
1764
+ scope: itemId ? "item" : "survey",
1765
+ ...surface ? { surface } : {},
1766
+ ...itemId ? {
1767
+ itemId,
1768
+ itemKey,
1769
+ itemType
1770
+ } : {},
1771
+ contentKey,
1772
+ sourceType: sourceContent.type,
1773
+ sourceText,
1774
+ ...sourceContent.type === "richText" ? { sourceContent } : {},
1775
+ targetPresent: Boolean(targetContent && truncateLocalizationText(targetContent)),
1776
+ ...targetContent ? {
1777
+ targetType: targetContent.type,
1778
+ targetText: truncateLocalizationText(targetContent)
1779
+ } : {}
1780
+ });
1781
+ };
1782
+ for (const surface of SURVEY_TRANSLATION_SURFACES) {
1783
+ const targetSurface = targetTranslations?.[surface];
1784
+ for (const [contentKey, sourceContent] of Object.entries(sourceTranslations[surface] ?? {})) addSurface({
1785
+ surface,
1786
+ contentKey,
1787
+ sourceContent,
1788
+ targetContent: targetSurface?.[contentKey]
1789
+ });
1790
+ }
1791
+ for (const item of survey.surveyItems) for (const [contentKey, sourceContent] of Object.entries(sourceTranslations.itemTranslations?.[item.id] ?? {})) addSurface({
1792
+ itemId: item.id,
1793
+ itemKey: item.key,
1794
+ itemType: item.itemType,
1795
+ contentKey,
1796
+ sourceContent,
1797
+ targetContent: targetTranslations?.itemTranslations?.[item.id]?.[contentKey]
1798
+ });
1799
+ const missingCount = surfaces.filter((surface) => !surface.targetPresent).length;
1800
+ const filteredSurfaces = input.missingOnly ? surfaces.filter((surface) => !surface.targetPresent) : surfaces;
1801
+ const parsedCursor = Number.parseInt(input.cursor ?? "0", 10);
1802
+ const offset = Number.isFinite(parsedCursor) && parsedCursor >= 0 ? parsedCursor : 0;
1803
+ const limit = input.limit ?? 20;
1804
+ const page = filteredSurfaces.slice(offset, offset + limit);
1805
+ const nextOffset = offset + page.length;
1806
+ return {
1807
+ ok: true,
1808
+ result: {
1809
+ sourceLocale,
1810
+ targetLocale,
1811
+ missingOnly: input.missingOnly ?? false,
1812
+ totalSourceSurfaces: surfaces.length,
1813
+ totalAvailable: filteredSurfaces.length,
1814
+ missingCount,
1815
+ returned: page.length,
1816
+ ...nextOffset < filteredSurfaces.length ? { nextCursor: String(nextOffset) } : {},
1817
+ 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." },
1818
+ surfaces: page
1819
+ }
1820
+ };
1821
+ };
1639
1822
  const resolveItemRef = (input, snapshot) => {
1640
1823
  if (input.itemRef && input.itemRef !== "selected") return input.itemRef;
1641
1824
  return snapshot?.ui.selectedItemId;
@@ -1717,7 +1900,7 @@ const listAssets = (input, snapshot) => {
1717
1900
  };
1718
1901
  const surveyInspectTool = createTool({
1719
1902
  id: TOOL_ID$3,
1720
- description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, image assets with explicit asset IDs and usages, or focused raw JSON. At turn start the draft equals the live snapshot; successful same-turn edits appear immediately.",
1903
+ description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, paged source/target localization surfaces, image assets with explicit asset IDs and usages, or focused raw JSON. At turn start the draft equals the live snapshot; successful same-turn edits appear immediately.",
1721
1904
  inputSchema: inputSchema$3,
1722
1905
  outputSchema: outputSchema$2,
1723
1906
  mcp: { annotations: {
@@ -1792,6 +1975,27 @@ const surveyInspectTool = createTool({
1792
1975
  result
1793
1976
  };
1794
1977
  }
1978
+ if (input.action === "localization") {
1979
+ if (!input.targetLocale) return {
1980
+ ok: false,
1981
+ action: input.action,
1982
+ reason: "Localization inspection requires a targetLocale.",
1983
+ modelSummary: "Localization inspection requires a targetLocale."
1984
+ };
1985
+ const localization = listLocalizationSurfaces(input, snapshot, input.targetLocale);
1986
+ if (!localization.ok) return {
1987
+ ok: false,
1988
+ action: input.action,
1989
+ reason: localization.reason,
1990
+ modelSummary: localization.reason
1991
+ };
1992
+ return {
1993
+ ok: true,
1994
+ action: input.action,
1995
+ modelSummary: stringifyForModel(localization.result),
1996
+ result: localization.result
1997
+ };
1998
+ }
1795
1999
  if (input.action === "assets") {
1796
2000
  const result = listAssets(input, snapshot);
1797
2001
  return {
@@ -1801,16 +2005,34 @@ const surveyInspectTool = createTool({
1801
2005
  result
1802
2006
  };
1803
2007
  }
1804
- const itemId = resolveItemRef(input, snapshot);
2008
+ const itemId = input.itemRef ? resolveItemRef(input, snapshot) : void 0;
1805
2009
  const itemIdentifiers = itemId ? [itemId] : void 0;
1806
- const result = unwrapToolOutput(await runTool(surveyGetRawSurveyTool, {
1807
- paths: input.paths,
2010
+ const requestedPaths = itemIdentifiers ? void 0 : input.paths;
2011
+ const boundedPaths = requestedPaths?.slice(0, MAX_RAW_JSON_PATHS_PER_RESULT);
2012
+ const rawResult = unwrapToolOutput(await runTool(surveyGetRawSurveyTool, {
2013
+ paths: boundedPaths,
1808
2014
  itemIdentifiers
1809
2015
  }, context));
2016
+ const ignoredPathCount = itemIdentifiers ? input.paths?.length ?? 0 : 0;
2017
+ const omittedPathCount = Math.max(0, (requestedPaths?.length ?? 0) - (boundedPaths?.length ?? 0));
2018
+ const requestCoverage = ignoredPathCount > 0 || omittedPathCount > 0 ? {
2019
+ precedence: itemIdentifiers ? "itemRef was resolved and inspected; explicit paths were ignored." : "Explicit paths were inspected in request order.",
2020
+ requestedPathCount: input.paths?.length ?? 0,
2021
+ inspectedPathCount: boundedPaths?.length ?? 0,
2022
+ ignoredPathCount,
2023
+ omittedPathCount,
2024
+ ...omittedPathCount > 0 ? { nextPaths: requestedPaths?.slice(MAX_RAW_JSON_PATHS_PER_RESULT, MAX_RAW_JSON_PATHS_PER_RESULT * 2) } : {},
2025
+ 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."
2026
+ } : void 0;
2027
+ const result = requestCoverage && isRecord(rawResult) ? {
2028
+ ...rawResult,
2029
+ requestCoverage
2030
+ } : rawResult;
2031
+ const modelSummary = [requestCoverage ? `Raw JSON request normalized instead of rejected: ${JSON.stringify(requestCoverage)}` : void 0, stringifyForModel(result)].filter((value) => Boolean(value)).join("\n");
1810
2032
  return {
1811
2033
  ok: true,
1812
2034
  action: input.action,
1813
- modelSummary: stringifyForModel(result),
2035
+ modelSummary,
1814
2036
  result
1815
2037
  };
1816
2038
  },
@@ -1821,13 +2043,31 @@ const surveyInspectTool = createTool({
1821
2043
  const TOOL_ID$2 = "survey_attachment";
1822
2044
  const surveyAttachmentTool = createTool({
1823
2045
  id: TOOL_ID$2,
1824
- description: "Attach only the stored PDFs or images needed for the current task to the next native model request. After this succeeds, read the original files directly and continue the task.",
1825
- inputSchema: z.object({ documentIds: z.array(z.string().min(1)).min(1).max(4).describe("IDs of only the stored attachments needed for the current request.") }),
1826
- outputSchema: z.object({ loaded: z.array(z.object({
1827
- id: z.string(),
1828
- filename: z.string(),
1829
- mediaType: z.string()
1830
- })) }),
2046
+ 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.",
2047
+ inputSchema: z.object({
2048
+ 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."),
2049
+ documentRequests: z.array(z.object({
2050
+ documentId: z.string().min(1),
2051
+ pages: z.object({
2052
+ start: z.number().int().min(1).max(1e5),
2053
+ end: z.number().int().min(1).max(1e5)
2054
+ }).optional().describe("One-based inclusive PDF page range to prioritize."),
2055
+ focus: z.string().min(1).max(300).optional().describe("Short section, question range, or fact to prioritize in the original file.")
2056
+ })).min(1).max(4).optional().describe("Stored attachments with an optional bounded reading focus.")
2057
+ }),
2058
+ outputSchema: z.object({
2059
+ loaded: z.array(z.object({
2060
+ id: z.string(),
2061
+ filename: z.string(),
2062
+ mediaType: z.string(),
2063
+ pages: z.object({
2064
+ start: z.number().int(),
2065
+ end: z.number().int()
2066
+ }).optional(),
2067
+ focus: z.string().optional()
2068
+ })),
2069
+ warnings: z.array(z.string()).optional()
2070
+ }),
1831
2071
  mcp: { annotations: {
1832
2072
  title: "Load chat attachment",
1833
2073
  readOnlyHint: true,
@@ -1838,35 +2078,121 @@ const surveyAttachmentTool = createTool({
1838
2078
  execute: async (inputData, context) => {
1839
2079
  const access = (context?.requestContext)?.get("attachmentAccess");
1840
2080
  if (!access) return { loaded: [] };
1841
- const requestedIds = [...new Set(inputData.documentIds)];
2081
+ const warnings = [];
2082
+ const requests = /* @__PURE__ */ new Map();
2083
+ for (const documentId of inputData.documentIds ?? []) requests.set(documentId, {});
2084
+ for (const request of inputData.documentRequests ?? []) {
2085
+ if (request.pages && request.pages.end < request.pages.start) {
2086
+ warnings.push(`Ignored invalid page range ${request.pages.start}-${request.pages.end} for ${request.documentId}.`);
2087
+ requests.set(request.documentId, { ...request.focus ? { focus: request.focus } : {} });
2088
+ continue;
2089
+ }
2090
+ requests.set(request.documentId, {
2091
+ ...request.pages ? { pages: request.pages } : {},
2092
+ ...request.focus ? { focus: request.focus } : {}
2093
+ });
2094
+ }
2095
+ const requestedIds = [...requests.keys()];
1842
2096
  const authorized = access.documents.filter((document) => requestedIds.includes(document.id));
1843
2097
  const attachments = await access.load(authorized.map((document) => document.id));
1844
2098
  access.selectedAttachments.splice(0, access.selectedAttachments.length, ...attachments);
1845
- return { loaded: authorized.map((document) => ({
1846
- id: document.id,
1847
- filename: document.name,
1848
- mediaType: document.mediaType
1849
- })) };
2099
+ return {
2100
+ loaded: authorized.map((document) => {
2101
+ const request = requests.get(document.id);
2102
+ const pages = document.mediaType === "application/pdf" ? request?.pages : void 0;
2103
+ if (request?.pages && !pages) warnings.push(`Ignored a PDF page focus for non-PDF attachment ${document.name}.`);
2104
+ return {
2105
+ id: document.id,
2106
+ filename: document.name,
2107
+ mediaType: document.mediaType,
2108
+ ...pages ? { pages } : {},
2109
+ ...request?.focus ? { focus: request.focus } : {}
2110
+ };
2111
+ }),
2112
+ ...warnings.length > 0 ? { warnings } : {}
2113
+ };
1850
2114
  },
1851
- toModelOutput: (value) => toTextModelOutput(value, () => "The selected original attachments are included in this model input. Read them directly now and continue the user's task. Do not claim that their content is unavailable or ask the user to paste extracted text.")
2115
+ toModelOutput: (value) => toTextModelOutput(value, (output) => {
2116
+ if (output.loaded.length === 0) return [
2117
+ "No authorized stored attachment was loaded. Do not infer source content from conversation memory.",
2118
+ ...(output.warnings ?? []).map((warning) => `Warning: ${warning}`),
2119
+ "Inspect the available attachment ids and retry with only the source documents needed for this task."
2120
+ ].join("\n");
2121
+ const focuses = output.loaded.map((document) => {
2122
+ const details = [document.pages ? `prioritize one-based pages ${document.pages.start}-${document.pages.end}` : void 0, document.focus ? `focus: ${document.focus}` : void 0].filter(Boolean);
2123
+ return details.length > 0 ? `- ${document.filename}: ${details.join("; ")}` : void 0;
2124
+ }).filter((line) => Boolean(line));
2125
+ return [
2126
+ "The selected complete original attachments are included in this model input. Read them directly now and continue the user's task. Conversation memory is not a lossless source.",
2127
+ focuses.length > 0 ? `Requested reading focus (the complete original remains authoritative):\n${focuses.join("\n")}` : void 0,
2128
+ ...(output.warnings ?? []).map((warning) => `Warning: ${warning}`),
2129
+ "Do not claim that successfully loaded content is unavailable or ask the user to paste extracted text."
2130
+ ].filter((line) => Boolean(line)).join("\n");
2131
+ })
1852
2132
  });
1853
2133
  //#endregion
1854
2134
  //#region src/server/tools/survey-audit-tool.ts
1855
2135
  const TOOL_ID$1 = "survey_audit";
2136
+ const inputSchema$1 = z.object({
2137
+ sourceLocale: z.string().min(1).optional().describe("Locale treated as the source when checking translation completeness."),
2138
+ targetLocales: z.array(z.string().min(1)).max(20).optional(),
2139
+ maxFindings: z.number().int().min(1).max(300).default(120).optional(),
2140
+ reason: z.string().max(500).optional()
2141
+ });
2142
+ const outputSchema = z.object({
2143
+ ok: z.boolean(),
2144
+ reason: z.string().optional(),
2145
+ report: z.unknown().optional()
2146
+ });
2147
+ const MAX_AUDIT_MODEL_GROUPS = 16;
2148
+ const MAX_AUDIT_MODEL_EXAMPLES = 12;
2149
+ const MAX_AUDIT_MESSAGE_CHARACTERS = 300;
2150
+ const previewAuditMessage = (message) => {
2151
+ const characters = [...message];
2152
+ if (characters.length <= MAX_AUDIT_MESSAGE_CHARACTERS) return message;
2153
+ return `${characters.slice(0, MAX_AUDIT_MESSAGE_CHARACTERS).join("")}… [${characters.length} characters total]`;
2154
+ };
2155
+ const formatAuditForModel = (output) => {
2156
+ if (!output.ok || !output.report || typeof output.report !== "object") return output.reason ?? "The deterministic survey audit could not be completed.";
2157
+ const report = output.report;
2158
+ const groups = /* @__PURE__ */ new Map();
2159
+ for (const finding of report.findings) {
2160
+ const key = `${finding.severity}\u0000${finding.category}\u0000${finding.code}`;
2161
+ const group = groups.get(key) ?? {
2162
+ category: finding.category,
2163
+ code: finding.code,
2164
+ count: 0,
2165
+ severity: finding.severity
2166
+ };
2167
+ group.count += 1;
2168
+ groups.set(key, group);
2169
+ }
2170
+ const groupedFindings = [...groups.values()].sort((left, right) => right.count - left.count || left.severity.localeCompare(right.severity) || left.code.localeCompare(right.code));
2171
+ const shownGroups = groupedFindings.slice(0, MAX_AUDIT_MODEL_GROUPS);
2172
+ const examples = report.findings.slice(0, MAX_AUDIT_MODEL_EXAMPLES);
2173
+ return [
2174
+ `Deterministic survey audit: ${report.findingCount} finding${report.findingCount === 1 ? "" : "s"} (errors=${report.summary.error}, warnings=${report.summary.warning}, info=${report.summary.info}).`,
2175
+ `Finding coverage returned by the audit: ${report.findings.length}/${report.findingCount}${report.truncated ? " (bounded)" : ""}.`,
2176
+ `Metrics: ${JSON.stringify(report.metrics)}.`,
2177
+ shownGroups.length > 0 ? `Finding groups (${shownGroups.length}/${groupedFindings.length} shown):\n${shownGroups.map((group) => `- ${group.severity}/${group.category}/${group.code}: ${group.count} returned finding${group.count === 1 ? "" : "s"}`).join("\n")}` : "Finding groups: none.",
2178
+ examples.length > 0 ? `Ordered examples (${examples.length}/${report.findings.length} returned findings shown):\n${examples.map((finding) => {
2179
+ const location = [
2180
+ finding.itemId,
2181
+ finding.locale,
2182
+ finding.contentKey
2183
+ ].filter(Boolean).join(":");
2184
+ return `- ${finding.code}${location ? ` (${location})` : ""}: ${previewAuditMessage(finding.message)}`;
2185
+ }).join("\n")}` : void 0,
2186
+ report.qualityReviewPrinciples.length > 0 ? `Qualitative review rubric:\n${report.qualityReviewPrinciples.map((principle) => `- ${principle.id}: ${principle.prompt}`).join("\n")}` : void 0,
2187
+ report.note,
2188
+ "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."
2189
+ ].filter((line) => Boolean(line)).join("\n\n");
2190
+ };
1856
2191
  const surveyAuditTool = createTool({
1857
2192
  id: TOOL_ID$1,
1858
2193
  description: "Audit the current survey for deterministic structural, content, localization, response, and asset issues, and return a research-informed questionnaire-quality review rubric for qualitative critique. This is read-only and is not psychometric validation.",
1859
- inputSchema: z.object({
1860
- sourceLocale: z.string().min(1).optional().describe("Locale treated as the source when checking translation completeness."),
1861
- targetLocales: z.array(z.string().min(1)).max(20).optional(),
1862
- maxFindings: z.number().int().min(1).max(300).default(120).optional(),
1863
- reason: z.string().max(500).optional()
1864
- }),
1865
- outputSchema: z.object({
1866
- ok: z.boolean(),
1867
- reason: z.string().optional(),
1868
- report: z.unknown().optional()
1869
- }),
2194
+ inputSchema: inputSchema$1,
2195
+ outputSchema,
1870
2196
  mcp: { annotations: {
1871
2197
  title: "Audit survey",
1872
2198
  readOnlyHint: true,
@@ -1885,7 +2211,7 @@ const surveyAuditTool = createTool({
1885
2211
  report: auditSurvey(snapshot, input)
1886
2212
  };
1887
2213
  },
1888
- toModelOutput: (value) => toTextModelOutput(value, (output) => JSON.stringify(output, null, 2))
2214
+ toModelOutput: (value) => toTextModelOutput(value, formatAuditForModel)
1889
2215
  });
1890
2216
  //#endregion
1891
2217
  //#region src/server/tools/survey-asset-tool.ts
@@ -2089,4 +2415,4 @@ const surveyAssistantTools = Object.freeze({
2089
2415
  //#endregion
2090
2416
  export { surveyAuditTool as i, surveyAssistantTools as n, surveyAssetTool as r, SURVEY_ASSISTANT_TOOL_IDS as t };
2091
2417
 
2092
- //# sourceMappingURL=tools-wgLen05n.mjs.map
2418
+ //# sourceMappingURL=tools-Dg9HZrYU.mjs.map