@case-framework/survey-assistant 0.8.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 (114) hide show
  1. package/dist/{authoring-references-DbuOKerg.mjs → authoring-references-CmwLc_C8.mjs} +40 -32
  2. package/dist/authoring-references-CmwLc_C8.mjs.map +1 -0
  3. package/dist/capabilities-D55dq-fO.mjs +582 -0
  4. package/dist/capabilities-D55dq-fO.mjs.map +1 -0
  5. package/dist/capabilities-default.d.mts +4 -12
  6. package/dist/capabilities-default.d.mts.map +1 -1
  7. package/dist/capabilities-default.mjs +2 -2
  8. package/dist/capabilities-z95e1mti.d.mts +117 -0
  9. package/dist/capabilities-z95e1mti.d.mts.map +1 -0
  10. package/dist/{constants-B6HzpEsx.d.mts → constants-HL_klgJl.d.mts} +4 -4
  11. package/dist/{constants-B6HzpEsx.d.mts.map → constants-HL_klgJl.d.mts.map} +1 -1
  12. package/dist/{controller-proxy-DEeFl9IA.mjs → controller-proxy-BQ1YsVxJ.mjs} +4 -3
  13. package/dist/controller-proxy-BQ1YsVxJ.mjs.map +1 -0
  14. package/dist/{controller-proxy-D7uQfm5f.d.mts → controller-proxy-Cmzdr9tV.d.mts} +4 -4
  15. package/dist/{controller-proxy-D7uQfm5f.d.mts.map → controller-proxy-Cmzdr9tV.d.mts.map} +1 -1
  16. package/dist/default-D22AKTqE.mjs +233 -0
  17. package/dist/default-D22AKTqE.mjs.map +1 -0
  18. package/dist/{digest-C39WyAWG.d.mts → digest-FnTTDBq2.d.mts} +2 -2
  19. package/dist/digest-FnTTDBq2.d.mts.map +1 -0
  20. package/dist/digest.d.mts +1 -1
  21. package/dist/engine-D950TfvC.mjs +3098 -0
  22. package/dist/engine-D950TfvC.mjs.map +1 -0
  23. package/dist/engine.d.mts +4 -3
  24. package/dist/engine.mjs +3 -3
  25. package/dist/{index-WB5kfnz0.d.mts → index-B8TLBd1G.d.mts} +8 -6
  26. package/dist/index-B8TLBd1G.d.mts.map +1 -0
  27. package/dist/index-CaX3hZmr.d.mts +23813 -0
  28. package/dist/index-CaX3hZmr.d.mts.map +1 -0
  29. package/dist/{index-r5riv4md.d.mts → index-CeUlGkoU.d.mts} +27 -6
  30. package/dist/index-CeUlGkoU.d.mts.map +1 -0
  31. package/dist/{index-D7wT6P3t.d.mts → index-DYHdvSE8.d.mts} +83 -41
  32. package/dist/index-DYHdvSE8.d.mts.map +1 -0
  33. package/dist/lifecycle-DFzxVxpq.d.mts +54346 -0
  34. package/dist/lifecycle-DFzxVxpq.d.mts.map +1 -0
  35. package/dist/{memory-thread-repository-DAnOGIB3.mjs → memory-thread-repository-DmLadygY.mjs} +15 -9
  36. package/dist/{memory-thread-repository-DAnOGIB3.mjs.map → memory-thread-repository-DmLadygY.mjs.map} +1 -1
  37. package/dist/protocol-BQ7uIDYT.mjs +569 -0
  38. package/dist/protocol-BQ7uIDYT.mjs.map +1 -0
  39. package/dist/protocol.d.mts +4 -4
  40. package/dist/protocol.mjs +3 -2
  41. package/dist/{react-C_GY-Fsc.mjs → react-DybJ5pWw.mjs} +17 -11
  42. package/dist/react-DybJ5pWw.mjs.map +1 -0
  43. package/dist/react-integration.d.mts +1 -1
  44. package/dist/react-integration.mjs +1 -1
  45. package/dist/react.d.mts +2 -2
  46. package/dist/react.mjs +2 -2
  47. package/dist/references/assistant-operations.md +190 -215
  48. package/dist/references/core-rules.md +2 -2
  49. package/dist/references/element-types.md +189 -0
  50. package/dist/references/expressions.md +215 -238
  51. package/dist/references/follow-ups.md +151 -0
  52. package/dist/references/localization.md +26 -12
  53. package/dist/references/response-variables.md +3 -3
  54. package/dist/references/rich-text-content.md +22 -43
  55. package/dist/references/source-material-surveys.md +20 -25
  56. package/dist/references/survey-data-model.md +36 -117
  57. package/dist/{request-context-Dg4jSwN1.mjs → request-context-KVh8VjgH.mjs} +4 -4
  58. package/dist/request-context-KVh8VjgH.mjs.map +1 -0
  59. package/dist/server-agent.d.mts +28 -7
  60. package/dist/server-agent.d.mts.map +1 -1
  61. package/dist/server-agent.mjs +33 -25
  62. package/dist/server-agent.mjs.map +1 -1
  63. package/dist/server-runtime.d.mts +6 -5
  64. package/dist/server-runtime.d.mts.map +1 -1
  65. package/dist/server-runtime.mjs +3 -3
  66. package/dist/server-tasks.d.mts +1 -1
  67. package/dist/server-tasks.mjs +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 +7 -6
  71. package/dist/server.d.mts.map +1 -1
  72. package/dist/server.mjs +3 -3
  73. package/dist/{tasks-BZEiwgxA.mjs → tasks-Coh5N027.mjs} +16 -15
  74. package/dist/tasks-Coh5N027.mjs.map +1 -0
  75. package/dist/{tasks-B9h3LPam.d.mts → tasks-wb9M-bak.d.mts} +11 -11
  76. package/dist/{tasks-B9h3LPam.d.mts.map → tasks-wb9M-bak.d.mts.map} +1 -1
  77. package/dist/{thread-handlers-Bef0td9N.d.mts → thread-handlers-C3Iya92v.d.mts} +6 -5
  78. package/dist/thread-handlers-C3Iya92v.d.mts.map +1 -0
  79. package/dist/{tools-CEsv-WoO.mjs → tools-D0KBDolR.mjs} +435 -408
  80. package/dist/tools-D0KBDolR.mjs.map +1 -0
  81. package/dist/turn-survey-draft-B34lggSG.d.mts +185 -0
  82. package/dist/turn-survey-draft-B34lggSG.d.mts.map +1 -0
  83. package/dist/ui.d.mts +3 -3
  84. package/dist/ui.d.mts.map +1 -1
  85. package/dist/ui.mjs +35 -23
  86. package/dist/ui.mjs.map +1 -1
  87. package/package.json +7 -7
  88. package/dist/authoring-references-DbuOKerg.mjs.map +0 -1
  89. package/dist/capabilities-CmeeAjEc.d.mts +0 -192
  90. package/dist/capabilities-CmeeAjEc.d.mts.map +0 -1
  91. package/dist/capabilities-DCMA71PP.mjs +0 -58
  92. package/dist/capabilities-DCMA71PP.mjs.map +0 -1
  93. package/dist/controller-proxy-DEeFl9IA.mjs.map +0 -1
  94. package/dist/default-fhSon2RM.mjs +0 -760
  95. package/dist/default-fhSon2RM.mjs.map +0 -1
  96. package/dist/digest-C39WyAWG.d.mts.map +0 -1
  97. package/dist/engine-nXoCWYpQ.mjs +0 -6888
  98. package/dist/engine-nXoCWYpQ.mjs.map +0 -1
  99. package/dist/index-Ciq7vHIs.d.mts +0 -716
  100. package/dist/index-Ciq7vHIs.d.mts.map +0 -1
  101. package/dist/index-D7wT6P3t.d.mts.map +0 -1
  102. package/dist/index-WB5kfnz0.d.mts.map +0 -1
  103. package/dist/index-r5riv4md.d.mts.map +0 -1
  104. package/dist/lifecycle-Dm8u7QFh.d.mts +0 -545
  105. package/dist/lifecycle-Dm8u7QFh.d.mts.map +0 -1
  106. package/dist/protocol-CF4Bh_Ch.mjs +0 -1255
  107. package/dist/protocol-CF4Bh_Ch.mjs.map +0 -1
  108. package/dist/react-C_GY-Fsc.mjs.map +0 -1
  109. package/dist/references/embedded-forms.md +0 -146
  110. package/dist/references/item-types.md +0 -641
  111. package/dist/request-context-Dg4jSwN1.mjs.map +0 -1
  112. package/dist/tasks-BZEiwgxA.mjs.map +0 -1
  113. package/dist/thread-handlers-Bef0td9N.d.mts.map +0 -1
  114. package/dist/tools-CEsv-WoO.mjs.map +0 -1
@@ -1,11 +1,15 @@
1
- import { Q as SURVEY_OPERATION_KINDS, Y as assistantItemTypeManifestSchema, bt as surveyOperationSchema, f as surveyChangeSetSchema, xt as surveyOperationSchemasByKind } from "./protocol-CF4Bh_Ch.mjs";
2
- import { C as isRecord$3, D as expressionMutationSchema, G as getRawSurveyItems, J as itemSummarySchema, K as itemConfigDetailsSchema, L as clampLimit, M as buildEmbeddedFormDetails, N as buildItemConfigDetails, P as buildItemTranslationEntries, Q as normalizeText, R as findDirectChildrenByParentId, T as buildExpressionMutationBatch, U as getItemIndexEntries, W as getRawSurvey, X as mapOutlineById, Y as mapItemIndexById, Z as mapRawItemsById, _t as createAssistantSurvey, ct as translationEntrySchema, d as collectExpressionMutationRecoveryTargets, et as resolveItemSummary, f as collectMalformedSurveyOperationRecoveryTargets, ft as getSurveyAssistantExpressionDiagnosticGuidance, it as summarizeItemIndexEntries, j as buildConfigSearchText, lt as unique, m as auditSurvey, nt as summarizeFocusItemNode, ot as summarizeOutlineNode, p as collectSurveyOperationRecoveryTargets, q as itemMatchesQuery, rt as summarizeItemById, st as summarizeRawItem, tt as sliceWithCursor, u as collectExpressionMutationRecoveryRefinements, ut as analyzeSurveyAssistantExpressionReferences, v as createSurveyChangeSet, vt as getAssistantVisibilityIssues, w as normalizeSurveyChangeOperations } from "./engine-nXoCWYpQ.mjs";
3
- import { a as getDeterministicCreateItemTypes, o as inspectAssistantItem, s as resolveAssistantCapabilityReference } from "./capabilities-DCMA71PP.mjs";
4
- import { r as getSurveyAssistantEngineContext, s as requireSurveyAssistantTurnState } from "./request-context-Dg4jSwN1.mjs";
5
- import { a as surveyAuthoringReferences, i as surveyAuthoringReferenceTopics, r as getBuiltInItemTypeAuthoringReference, t as builtInSurveyAuthoringItemTypes } from "./authoring-references-DbuOKerg.mjs";
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,74 +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
  };
71
- const formatItemConfigurationContract = (schema) => [
72
- "## Validated item configuration",
73
- "This schema validates the resulting configuration after creation or an atomic patch. patch-item-config paths are relative to this object. Inspect existing configuration and preserve surviving identities; include related translations in the same proposal.",
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.",
74
96
  "```json",
75
97
  JSON.stringify(z.toJSONSchema(schema, {
76
98
  io: "input",
77
- unrepresentable: "any"
78
- }), null, 2),
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),
79
106
  "```"
80
107
  ].join("\n\n");
81
108
  const formatSurveyOperationContractReference = (kind, options = {}) => {
82
109
  const { $schema: _schemaVersion, ...contract } = z.toJSONSchema(surveyOperationSchemasByKind[kind], {
83
110
  io: "input",
84
- reused: "inline",
111
+ reused: "ref",
85
112
  unrepresentable: "any"
86
113
  });
87
114
  const detail = options.detail ?? "compact";
88
115
  const renderedContract = detail === "full" ? contract : compactJsonSchema(contract);
89
- const itemCapability = options.itemCapability;
116
+ const elementCapability = options.elementCapability;
90
117
  return [
91
118
  `# \`${kind}\` operation contract`,
92
119
  "",
@@ -94,13 +121,23 @@ const formatSurveyOperationContractReference = (kind, options = {}) => {
94
121
  "the canonical post-normalization field shape. Cross-field and snapshot-dependent rules are",
95
122
  "enforced when the proposal is validated. Follow focused authoring guidance when it recommends",
96
123
  "a supported model-facing shorthand; otherwise use this canonical shape.",
97
- detail === "compact" ? "This compact view preserves the operation structure while omitting deeply nested value schemas. Request detail \"full\" only for protocol debugging; load a focused rich-text, expression, form, or item-type reference for authoring guidance." : "This full view is intended for protocol debugging and may be large.",
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.",
98
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."] : [],
99
136
  "```json",
100
- JSON.stringify(renderedContract, null, 2),
137
+ JSON.stringify(renderedContract, null, detail === "full" ? 2 : void 0),
101
138
  "```",
102
- ...itemCapability ? ["", formatItemCapabilityProfile(itemCapability, kind)] : [],
103
- ...options.itemConfigurationSchema && (kind === "patch-item-config" || kind === "create-item") ? [formatItemConfigurationContract(options.itemConfigurationSchema)] : []
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)] : []
104
141
  ].join("\n");
105
142
  };
106
143
  //#endregion
@@ -178,7 +215,7 @@ const TOOL_ID$6 = "survey_authoring_reference";
178
215
  const topicSchema = z.string().min(1).max(120);
179
216
  const inputSchema$10 = z.object({
180
217
  topic: topicSchema.describe("The narrow authoring reference to load."),
181
- 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."),
182
219
  operationKind: z.string().min(1).max(120).optional().describe("For topic assistant-operations, return only this operation's generated canonical field contract."),
183
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."),
184
221
  reason: z.string().max(500).optional().describe("Short reason this reference is needed.")
@@ -186,7 +223,7 @@ const inputSchema$10 = z.object({
186
223
  const outputSchema$8 = z.object({
187
224
  ok: z.boolean(),
188
225
  topic: topicSchema,
189
- itemType: z.string().optional(),
226
+ elementType: z.string().optional(),
190
227
  operationKind: z.string().optional(),
191
228
  detail: z.enum(SURVEY_OPERATION_CONTRACT_DETAIL_LEVELS).optional(),
192
229
  title: z.string(),
@@ -198,7 +235,7 @@ const formatCanonicalNameRecovery = (input, candidates, noun) => {
198
235
  };
199
236
  const surveyAuthoringReferenceTool = createTool({
200
237
  id: TOOL_ID$6,
201
- description: "Load one narrow CASE authoring reference. With topic \"item-types\", pass itemType to load only that type. For a survey_change operation, use topic \"assistant-operations\" with operationKind and optionally itemType to compose the structural contract with that installed item capability. Compact detail is the authoring default; full detail is only for protocol debugging.",
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.",
202
239
  inputSchema: inputSchema$10,
203
240
  outputSchema: outputSchema$8,
204
241
  mcp: { annotations: {
@@ -211,12 +248,20 @@ const surveyAuthoringReferenceTool = createTool({
211
248
  execute: async (inputData, context) => {
212
249
  const contractDetail = inputData.detail ?? "compact";
213
250
  const engineContext = getSurveyAssistantEngineContext(context?.requestContext);
214
- const itemCapability = inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.manifest ?? engineContext?.capabilities.itemTypes.find((entry) => entry.itemType === inputData.itemType) : void 0;
215
- 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
+ };
216
261
  if (inputData.operationKind && inputData.topic !== "assistant-operations") return {
217
262
  ok: false,
218
263
  topic: inputData.topic,
219
- itemType: inputData.itemType,
264
+ elementType: inputData.elementType,
220
265
  operationKind: inputData.operationKind,
221
266
  title: "Operation contract unavailable",
222
267
  content: "operationKind is supported only with topic assistant-operations."
@@ -230,62 +275,65 @@ const surveyAuthoringReferenceTool = createTool({
230
275
  title: "Operation contract unavailable",
231
276
  content: `No built-in survey_change operation named "${inputData.operationKind}" is available.` + formatCanonicalNameRecovery(inputData.operationKind, SURVEY_OPERATION_KINDS, "operation")
232
277
  };
233
- if (inputData.itemType && !itemCapability && !getBuiltInItemTypeAuthoringReference(inputData.itemType)) return {
278
+ if (inputData.elementType && !elementCapability && !getBuiltInElementTypeAuthoringReference(inputData.elementType)) return {
234
279
  ok: false,
235
280
  topic: inputData.topic,
236
- itemType: inputData.itemType,
281
+ elementType: inputData.elementType,
237
282
  operationKind: inputData.operationKind,
238
283
  detail: contractDetail,
239
284
  title: "Item capability unavailable",
240
- 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")
241
286
  };
242
- if (itemCapability && !itemCapability.supportedOperations.includes(inputData.operationKind)) return {
287
+ if (elementCapability && !elementCapability.supportedOperations.includes(inputData.operationKind)) return {
243
288
  ok: false,
244
289
  topic: inputData.topic,
245
- itemType: inputData.itemType,
290
+ elementType: inputData.elementType,
246
291
  operationKind: inputData.operationKind,
247
292
  detail: contractDetail,
248
293
  title: "Operation unsupported for item type",
249
- 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}".`
250
295
  };
251
296
  return {
252
297
  ok: true,
253
298
  topic: inputData.topic,
254
- itemType: inputData.itemType,
299
+ elementType: inputData.elementType,
255
300
  operationKind: inputData.operationKind,
256
301
  detail: contractDetail,
257
- title: itemCapability ? `${inputData.operationKind} for ${itemCapability.displayName}` : `${inputData.operationKind} operation contract`,
302
+ title: elementCapability ? `${inputData.operationKind} for ${elementCapability.displayName}` : `${inputData.operationKind} operation contract`,
258
303
  content: formatSurveyOperationContractReference(inputData.operationKind, {
259
304
  detail: contractDetail,
260
- itemCapability,
261
- itemConfigurationSchema: inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType)?.configurationSchema : void 0
305
+ elementCapability,
306
+ elementSchema: inputData.elementType ? engineContext?.trustedCapabilities?.registry.get(inputData.elementType).schema : void 0
262
307
  })
263
308
  };
264
309
  }
265
310
  const builtInEntry = surveyAuthoringReferenceTopics.includes(inputData.topic) ? surveyAuthoringReferences[inputData.topic] : void 0;
266
- const capability = inputData.itemType ? engineContext?.trustedCapabilities?.byItemType.get(inputData.itemType) : void 0;
267
- const capabilityReference = inputData.itemType ? resolveAssistantCapabilityReference({
311
+ const capability = inputData.elementType ? engineContext?.trustedCapabilities?.byElementType.get(inputData.elementType) : void 0;
312
+ const capabilityReference = inputData.elementType ? resolveAssistantCapabilityReference({
268
313
  capabilities: engineContext?.trustedCapabilities,
269
- itemType: inputData.itemType,
314
+ elementType: inputData.elementType,
270
315
  topic: inputData.topic
271
316
  }) : void 0;
272
- const focusedBuiltInItemTypeReference = inputData.topic === "item-types" && inputData.itemType ? getBuiltInItemTypeAuthoringReference(inputData.itemType) : void 0;
273
- 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 {
274
319
  ok: false,
275
320
  topic: inputData.topic,
276
- itemType: inputData.itemType,
321
+ elementType: inputData.elementType,
277
322
  title: "Item capability unavailable",
278
- 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")
279
324
  };
280
325
  const capabilityEntry = capabilityReference ? {
281
326
  title: capabilityReference.title ?? `${capability?.manifest.displayName} authoring`,
282
327
  content: capabilityReference.markdown
283
328
  } : void 0;
284
- const focusedCapabilityProfile = inputData.topic === "item-types" && itemCapability ? {
285
- title: itemCapability.displayName,
286
- content: formatItemCapabilityProfile(itemCapability)
329
+ const focusedCapabilityProfile = inputData.topic === "element-types" && elementCapability ? {
330
+ title: elementCapability.displayName,
331
+ content: formatElementCapabilityProfile(elementCapability)
287
332
  } : void 0;
288
- 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;
289
337
  const entry = baseEntry && capabilityEntry ? {
290
338
  title: baseEntry.title,
291
339
  content: baseEntry.content === capabilityEntry.content ? baseEntry.content : [
@@ -298,18 +346,18 @@ const surveyAuthoringReferenceTool = createTool({
298
346
  if (!entry) return {
299
347
  ok: false,
300
348
  topic: inputData.topic,
301
- itemType: inputData.itemType,
349
+ elementType: inputData.elementType,
302
350
  operationKind: inputData.operationKind,
303
351
  title: "Reference unavailable",
304
- content: (inputData.itemType ? `No reference named "${inputData.topic}" is installed for item type "${inputData.itemType}".` : `No built-in reference named "${inputData.topic}" is available.`) + formatCanonicalNameRecovery(inputData.topic, [...surveyAuthoringReferenceTopics, ...itemCapability?.referenceTopics ?? []], "reference")
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")
305
353
  };
306
354
  return {
307
355
  ok: true,
308
356
  topic: inputData.topic,
309
- itemType: inputData.itemType,
357
+ elementType: inputData.elementType,
310
358
  operationKind: inputData.operationKind,
311
359
  title: entry.title,
312
- content: inputData.topic === "item-types" && capability?.configurationSchema ? `${entry.content}\n\n${formatItemConfigurationContract(capability.configurationSchema)}` : 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
313
361
  };
314
362
  },
315
363
  toModelOutput: (value) => toTextModelOutput(value, (output) => `# ${output.title}\n\n${output.content}`)
@@ -330,25 +378,18 @@ const summarizePendingRepairs = (event) => {
330
378
  //#region src/server/tools/survey-change-tool.ts
331
379
  const TOOL_ID$5 = "survey_change";
332
380
  const OPERATIONS_DESCRIPTION = [
333
- "Typed survey operation objects to validate and package as one undoable change. Every object uses discriminator \"kind\", never \"type\".",
334
- "For an unfamiliar canonical shape, load survey_authoring_reference with topic \"assistant-operations\" and operationKind; it returns only that operation schema, generated from the validator.",
335
- "Any translations owned by an operation must be inside that same operation object. Never put translations beside the survey_change operations array.",
336
- "Every translation payload uses exactly one representation: plainText, canonical content, or the model-facing richTextBlocks shorthand. Shorthand is accepted wherever a typed translation accepts structured content and is normalized to canonical content before validation. Use it only for surfaces that focused guidance identifies as rendering rich text.",
337
- "Canonical create-item shape: put parentItemId and optional zero-based index in target (omitting index appends; index: 0 inserts first); put the required itemType, plus optional id, config, and creation-time expressions, in item; put respondent-visible plainText or structured content in top-level translations. Put choice maxSelection and options inside item.config. For top-level creation, target the root group identified by the snapshot or inspection.",
338
- "Use update-item-translation with itemId, locale, contentKey, and plainText for ordinary respondent-visible text. For a complete formatted replacement, prefer the richTextBlocks shorthand from rich-text-content; canonical content remains available when needed. Item title, subtitle, topContent, bottomContent, cardFooter, info content, option labels, and field labels are translations, not item labels.",
339
- "Use add-survey-locale only when the user explicitly requests an empty language registration. An ordinary localization request requires real translation operations, which introduce the locale themselves.",
340
- "Use remove-survey-locale with locale to destructively remove an existing survey language and all translations stored for it.",
341
- "Each call adds a delta to this turn's validated draft. Later calls may target ids created by earlier successful calls in the same turn.",
342
- "Use operations: [] only with retryOfChangeIds when pendingRepairs.allowsAcknowledgment is true, after inspecting the validator-confirmed no-op. This does not discharge identifiable pending edits or create an editor commit by itself.",
343
- "For an existing choice item, use add-choice-option with itemId, optional locale/index, and option: { id?, code?, label }; label is a plain string and creates its translation.",
344
- "Every reorder replaces the complete inspected list: reorder-choice-options uses optionIds, reorder-form-field-groups uses groupIds, and reorder-items uses top-level parentItemId (not target.parentItemId) and itemIds for every direct child. Include every existing entry exactly once in desired order.",
345
- `Use update-item-color only with a supported swatch. ${`Allowed editor item-color swatches: ${SURVEY_EDITOR_ITEM_COLORS.join(", ")}. Use one of these exact values for update-item-color; do not wait for validation feedback to choose a swatch.`}`,
346
- "Use update-response-settings with naming and settings arrays for variable names, matrix coding components, option export codes and export formats. Load response-variables for the shared coding contract.",
347
- "Use patch-item-config with itemId and plural patches whose JSON Pointer paths are relative to that item config root. Include related translations inside the operation. Load the focused item capability reference for its configuration and content keys.",
348
- "Use update-placed-image only for an image already embedded in rich text in the live survey or validated turn draft. It can switch the placement between a local asset and an external URL and update its rendered ALT text, caption, width, height, maximum dimensions, and alignment. For a new item, prefer putting the canonical image block inside create-item.translations[].richTextBlocks. Do not use image asset metadata when the user asked to change one placed image.",
349
- "When deleting referenced images and assets, include every reference-removal operation and remove-image-asset last in the same operations array.",
350
- "A survey-json-patch proposal contains exactly one operation with kind survey-json-patch and one plural patches array containing every related raw edit. It must be the only operation in that survey_change call. Paths are rooted at the survey document, such as /surveyItems/<index>/config or /translations/<locale>; never start at /config or modify /$schema. Patch translation leaves rather than replacing existing locale or item-translation containers.",
351
- "Keep each delta bounded; this is a per-call constraint, not a reason to end the turn. Successful chunks accumulate automatically, so substantial requests should continue with later deltas while capacity remains."
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."
352
393
  ].join(" ");
353
394
  const inputSchema$9 = z.object({
354
395
  retryOfChangeIds: retryOfChangeIdsSchema,
@@ -375,39 +416,8 @@ const rejectedOperationParseEvent = ({ issueText, summary, turnId }) => ({
375
416
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
376
417
  });
377
418
  const describeOperationTarget = (operation) => {
378
- switch (operation.kind) {
379
- case "update-item-translation": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}`;
380
- case "update-placed-image": return `${operation.kind} ${operation.locale}:${operation.itemId}:${operation.contentKey}#${operation.imageIndex}`;
381
- case "update-item-label":
382
- case "update-item-color":
383
- case "patch-item-config":
384
- case "remove-item":
385
- case "move-item": return `${operation.kind} ${operation.itemId}`;
386
- case "upsert-form-field-group": return `${operation.kind} ${operation.target.itemId}:${operation.group.id}`;
387
- case "remove-form-field-group": return `${operation.kind} ${operation.target.itemId}:${operation.groupId}`;
388
- case "reorder-form-field-groups": return `${operation.kind} ${operation.target.itemId}`;
389
- case "upsert-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.field.id}`;
390
- case "remove-form-field":
391
- case "move-form-field": return `${operation.kind} ${operation.target.itemId}:${operation.fieldId}`;
392
- case "set-embedded-form":
393
- case "remove-embedded-form": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
394
- case "add-choice-option": return `${operation.kind} ${operation.itemId}:${operation.option.id ?? operation.option.code ?? operation.option.label}`;
395
- case "update-choice-option-label":
396
- case "remove-choice-option": return `${operation.kind} ${operation.itemId}:${operation.optionId}`;
397
- case "reorder-choice-options": return `${operation.kind} ${operation.itemId}`;
398
- case "reorder-items": return `${operation.kind} ${operation.parentItemId}`;
399
- case "update-response-settings":
400
- case "update-survey-metadata":
401
- case "update-survey-pagination": return operation.kind;
402
- case "update-survey-translation": return `${operation.kind} ${operation.locale}:${operation.surface}:${operation.contentKey}`;
403
- case "add-survey-locale":
404
- case "remove-survey-locale": return `${operation.kind} ${operation.locale}`;
405
- case "set-image-asset":
406
- case "update-image-asset-metadata":
407
- case "remove-image-asset": return `${operation.kind} ${operation.assetId}`;
408
- case "create-item": return `${operation.kind} ${operation.item.id ?? operation.item.metadata?.itemLabel ?? operation.item.id ?? operation.item.itemType}`;
409
- case "survey-json-patch": return `${operation.kind} ${operation.patches.map((patch) => patch.path).join(", ")}`;
410
- }
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}`;
411
421
  };
412
422
  const summarizeValidation = (event) => {
413
423
  const lines = [`${event.status === "validated" ? "Validated" : "Rejected"} survey change: ${event.summary}`];
@@ -426,7 +436,7 @@ const summarizeValidation = (event) => {
426
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` : ""}.`);
427
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.`);
428
438
  for (const item of event.postChange.touchedItems) {
429
- const translations = item.translations.map(({ contentKey, locale, textLength, textPreview, truncated }) => truncated ? `${locale}:${contentKey}=${JSON.stringify(`${textPreview}…`)} [truncated preview; ${textLength} characters total]` : `${locale}:${contentKey}=${JSON.stringify(textPreview)}`).join("; ");
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("; ");
430
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"}`);
431
441
  if (item.placement) {
432
442
  const p = item.placement;
@@ -448,7 +458,7 @@ const summarizeValidation = (event) => {
448
458
  const location = [
449
459
  finding.itemId,
450
460
  finding.locale,
451
- finding.contentKey
461
+ finding.localizationRefs ? JSON.stringify(finding.localizationRefs) : void 0
452
462
  ].filter(Boolean).join(":");
453
463
  lines.push(`- ${finding.severity} ${finding.code}${location ? ` (${location})` : ""}: ${finding.messagePreview}${finding.truncated ? `… [truncated preview; ${finding.messageLength} characters total]` : ""}`);
454
464
  }
@@ -459,7 +469,6 @@ const summarizeValidation = (event) => {
459
469
  lines.push("Issues:");
460
470
  for (const issue of event.validation.issues) lines.push(`- ${issue.code}: ${issue.message}`);
461
471
  const issueCodes = new Set(event.validation.issues.map((issue) => issue.code));
462
- 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.");
463
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.");
464
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.");
465
474
  }
@@ -526,7 +535,7 @@ const inputSchema$8 = z.object({
526
535
  limit: z.number().int().min(1).max(80).optional().describe("Context action only: rows per catalog page; defaults to 80."),
527
536
  includeRawLocations: z.boolean().optional().describe("Context action only: include existing expression locations by raw item."),
528
537
  summary: z.string().max(500).optional().describe("Prepare action only: required user-facing summary."),
529
- mutations: z.array(z.unknown()).min(1).max(MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL).optional().describe(`Prepare action only: an array of 1-${MAX_EXPRESSION_MUTATIONS_PER_PROPOSAL} canonical expression mutation objects from the loaded expressions reference. Example entry: {"kind":"set-item-display-condition","itemId":"<existing-item-id>","expression":{"type":"function","functionName":"eq","arguments":[...]}}. expression is CASE expression JSON, never an expression-language snippet.`)
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.`)
530
539
  }).strict().superRefine((value, ctx) => {
531
540
  if (value.action === "context") {
532
541
  if (value.summary?.trim()) ctx.addIssue({
@@ -556,11 +565,18 @@ const contextOutputSchema = z.object({
556
565
  ok: z.boolean(),
557
566
  action: z.literal("context"),
558
567
  modelSummary: z.string(),
559
- 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."),
560
569
  nextCursor: z.string().optional(),
561
570
  totalResponseSlots: z.number().int().nonnegative().optional(),
562
571
  surveyResponseSlotCount: z.number().int().nonnegative().optional(),
563
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(),
564
580
  unresolvedItemIds: z.array(z.string()).optional(),
565
581
  responseSlots: z.array(z.object({
566
582
  ref: z.object({
@@ -584,7 +600,6 @@ const createIssue = ({ severity = "error", ...input }) => ({
584
600
  severity,
585
601
  ...input
586
602
  });
587
- const buildResponseSlotTypes = (snapshot) => snapshot.responseSlotTypes;
588
603
  const rejectedEvent = ({ issues, summary, turnId, warnings = [] }) => ({
589
604
  protocolVersion: 1,
590
605
  id: randomUUID(),
@@ -620,36 +635,39 @@ const buildContextOutput = (snapshot, input) => {
620
635
  const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
621
636
  const resolvedItems = input.itemIds?.map((identifier) => ({
622
637
  identifier,
623
- itemId: snapshot.inspection.itemIndex.find((entry) => entry.itemId === identifier || entry.editorName === identifier || entry.breadcrumb === identifier)?.itemId
638
+ itemId: snapshot.inspection.itemIndex.find((entry) => entry.itemId === identifier)?.itemId
624
639
  }));
625
640
  const itemFilter = resolvedItems?.length ? new Set(resolvedItems.flatMap((entry) => entry.itemId ? [entry.itemId] : [])) : void 0;
626
641
  const unresolvedItemIds = resolvedItems?.filter((entry) => !entry.itemId).map((entry) => entry.identifier) ?? [];
627
- const surveyResponseSlots = Object.entries(buildResponseSlotTypes(snapshot));
628
- const allResponseSlots = surveyResponseSlots.filter(([ref]) => !itemFilter || snapshot.responseVariables.some((slot) => itemFilter.has(slot.itemId) && JSON.parse(ref)[0] === slot.slotId)).map(([key, valueType]) => {
629
- const [slotId, method] = JSON.parse(key);
630
- const slot = snapshot.responseVariables.find((slot) => slot.slotId === slotId);
631
- return {
632
- ref: {
633
- slotId,
634
- method
635
- },
636
- valueType,
637
- variableName: slot?.variableName,
638
- itemId: slot?.itemId
639
- };
640
- });
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"
664
+ }));
641
665
  const offset = Number(input.cursor ?? 0);
642
666
  const limit = input.limit ?? 80;
643
667
  const responseSlots = allResponseSlots.slice(offset, offset + limit);
644
668
  const allExpressionLocations = input.action === "context" && input.includeRawLocations === true ? rawItems.map((item, rawIndex) => {
645
669
  if (itemFilter && !itemFilter.has(item.id)) return;
646
- const locations = [
647
- item.displayConditions?.root ? "displayConditions.root" : void 0,
648
- ...Object.keys(item.displayConditions?.components ?? {}).map((componentId) => `displayConditions.components.${componentId}`),
649
- ...Object.keys(item.disabledConditions?.components ?? {}).map((componentId) => `disabledConditions.components.${componentId}`),
650
- ...Object.keys(item.validations ?? {}).map((validationKey) => `validations.${validationKey}`),
651
- ...(item.prefills ?? []).map((prefill) => `prefills.${prefill.id}`)
652
- ].filter((location) => Boolean(location));
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("/")}`);
653
671
  return {
654
672
  itemId: item.id,
655
673
  editorName: names.get(item.id) ?? item.id,
@@ -659,7 +677,7 @@ const buildContextOutput = (snapshot, input) => {
659
677
  };
660
678
  }).filter((entry) => Boolean(entry)) : void 0;
661
679
  const expressionLocations = allExpressionLocations?.slice(offset, offset + limit);
662
- const totalAvailable = Math.max(allResponseSlots.length, allExpressionLocations?.length ?? 0);
680
+ const totalAvailable = Math.max(allResponseSlots.length, allStateTargets.length, allExpressionLocations?.length ?? 0);
663
681
  const nextCursor = offset + limit < totalAvailable ? String(offset + limit) : void 0;
664
682
  return {
665
683
  ok: true,
@@ -667,31 +685,36 @@ const buildContextOutput = (snapshot, input) => {
667
685
  modelSummary: [
668
686
  "Expression capability loaded.",
669
687
  "Use prepare mutations for display conditions, disabled conditions, validations, prefills, and template values.",
670
- "Display, disabled, validation, and prefill.when expressions must return boolean.",
688
+ "Visibility, disabled and validation expressions must return boolean.",
671
689
  "Use response refs exactly as listed and check their value types before choosing functions.",
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.",
672
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.",
673
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.",
674
- "Visibility must be acyclic, including item, ancestor, component and computed-response dependencies. Prefills do not permit cycles. Use an independent controlling answer or serialized context value. Hidden answers are retained but inactive: get is undefined and isDefined is false. Undefined/nonboolean visibility hides its target; customExpression callbacks are not supported in visibility.",
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.",
675
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[].",
676
696
  "Response-slot order: set-like deterministic snapshot construction order; it has no survey or respondent-visible meaning.",
677
- itemFilter ? `Response-slot scope: filtered to itemIds=${JSON.stringify(input.itemIds)}; ${allResponseSlots.length} matching of ${surveyResponseSlots.length} survey-wide slots. References outside this filter are omitted, not absent. To find controlling questions elsewhere, call survey_expression action=context without itemIds or cursor.` : `Response-slot scope: survey-wide (${surveyResponseSlots.length} slots).`,
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))}`,
678
699
  `Response slots (${responseSlots.length}/${allResponseSlots.length}, offset ${offset}): ${responseSlots.map((slot) => `${JSON.stringify(slot.ref)}${slot.valueType ? `:${slot.valueType}` : ""}`).join(", ")}`,
679
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.",
680
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,
681
- "Functions: and, or, not, eq, gt, gte, lt, lte, in_range, list_contains, sum, min, max, str_eq, date_eq.",
702
+ "Functions: and, or, not, eq, gt, gte, lt, lte, in_range, list_contains, list_count, if, sum, min, max, str_eq, date_eq.",
682
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
683
704
  ].filter((line) => Boolean(line)).join("\n"),
684
705
  totalResponseSlots: allResponseSlots.length,
685
- surveyResponseSlotCount: surveyResponseSlots.length,
706
+ surveyResponseSlotCount: snapshot.responseVariables.length * 2,
686
707
  ...allExpressionLocations ? { totalExpressionLocations: allExpressionLocations.length } : {},
687
708
  ...nextCursor ? { nextCursor } : {},
688
709
  ...unresolvedItemIds.length ? { unresolvedItemIds } : {},
689
710
  listOrdering: {
690
711
  responseSlots: "Set-like deterministic snapshot construction order; position has no survey or respondent-visible meaning.",
691
712
  expressionLocations: "Stored surveyItems array order; locations within an item are grouped by expression surface, not ranked or respondent-visible order.",
692
- "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."
693
714
  },
694
715
  responseSlots,
716
+ stateTargets: allStateTargets.slice(offset, offset + limit),
717
+ totalStateTargets: allStateTargets.length,
695
718
  ...expressionLocations ? { expressionLocations } : {}
696
719
  };
697
720
  };
@@ -755,7 +778,7 @@ const surveyExpressionTool = createTool({
755
778
  }), collectExpressionMutationRecoveryTargets(input.mutations, snapshot));
756
779
  const prepareInput = prepareParse.data;
757
780
  const addressedTargets = collectExpressionMutationRecoveryTargets(prepareInput.mutations, snapshot);
758
- const { issues, patches } = buildExpressionMutationBatch(prepareInput.mutations, snapshot);
781
+ const { issues, patches } = buildExpressionMutationBatch(prepareInput.mutations, snapshot.model);
759
782
  const errors = issues.filter((issue) => issue.severity === "error");
760
783
  const warnings = issues.filter((issue) => issue.severity === "warning");
761
784
  if (errors.length > 0) return reject(rejectedEvent({
@@ -801,6 +824,104 @@ const surveyExpressionTool = createTool({
801
824
  toModelOutput: (value) => toTextModelOutput(value, (output) => "action" in output && output.action === "context" ? output.modelSummary : summarizeDraftEvent(output))
802
825
  });
803
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
804
925
  //#region src/server/tools/parse-inspection-output.ts
805
926
  const parseInspectionOutput = (schema, value, backend) => {
806
927
  const parsed = schema.safeParse(value);
@@ -815,7 +936,7 @@ const responseVariableSummarySchema = z.object({
815
936
  itemId: z.string(),
816
937
  variableName: z.string(),
817
938
  valueType: z.string(),
818
- primitiveId: z.string().optional(),
939
+ elementId: z.string().optional(),
819
940
  rowId: z.string().optional(),
820
941
  columnId: z.string().optional(),
821
942
  namingKind: z.enum(["variable", "matrix"]).optional(),
@@ -824,8 +945,8 @@ const responseVariableSummarySchema = z.object({
824
945
  sourceSlotIds: z.array(z.string())
825
946
  }).optional(),
826
947
  computedSourcesTruncated: z.boolean(),
827
- referenceValueKeys: z.record(z.string(), z.string()).optional(),
828
- referenceValueScores: z.record(z.string(), z.number()).optional(),
948
+ referenceValueKeys: stringRecord(z.string(), z.string()).optional(),
949
+ referenceValueScores: stringRecord(z.string(), z.number()).optional(),
829
950
  optionIds: z.array(z.string()),
830
951
  optionsTotal: z.number(),
831
952
  optionsTruncated: z.boolean()
@@ -844,7 +965,7 @@ const summarizeResponseVariable = (slot) => {
844
965
  itemId: slot.itemId,
845
966
  variableName: slot.variableName,
846
967
  valueType: slot.valueType,
847
- primitiveId: slot.primitiveId,
968
+ elementId: slot.elementId,
848
969
  rowId: slot.rowId,
849
970
  columnId: slot.columnId,
850
971
  namingKind: slot.naming?.kind,
@@ -864,7 +985,7 @@ const responseVariableMatchesQuery = (slot, query) => [
864
985
  slot.slotId,
865
986
  slot.itemId,
866
987
  slot.variableName,
867
- slot.primitiveId,
988
+ slot.elementId,
868
989
  slot.rowId,
869
990
  slot.columnId,
870
991
  ...responseOptionIds(slot),
@@ -887,7 +1008,8 @@ const outlineModeSchema$1 = z.enum([
887
1008
  ]);
888
1009
  const translationSnippetSchema = z.object({
889
1010
  locale: z.string(),
890
- contentKey: z.string(),
1011
+ ownerId: z.string(),
1012
+ role: z.string(),
891
1013
  text: z.string()
892
1014
  });
893
1015
  const inputSchema$7 = z.object({
@@ -904,7 +1026,7 @@ const outputSchema$6 = z.object({
904
1026
  revision: z.number().int().nonnegative().optional(),
905
1027
  contentHash: z.string().optional(),
906
1028
  scopes: z.array(contextScopeSchema).optional(),
907
- 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."),
908
1030
  summary: z.object({
909
1031
  sessionId: z.string().nullable(),
910
1032
  view: z.string(),
@@ -920,9 +1042,9 @@ const outputSchema$6 = z.object({
920
1042
  outlineItemCount: z.number().int().nonnegative(),
921
1043
  responseSlotCount: z.number().int().nonnegative(),
922
1044
  rootItem: itemSummarySchema.optional(),
923
- availableItemTypes: z.array(z.string()),
924
- deterministicCreateItemTypes: z.array(z.string()),
925
- itemTypeDetails: z.array(assistantItemTypeManifestSchema)
1045
+ availableElementTypes: z.array(z.string()),
1046
+ deterministicCreateElementTypes: z.array(z.string()),
1047
+ elementTypeDetails: z.array(assistantElementTypeManifestSchema)
926
1048
  }).optional(),
927
1049
  selectedItem: z.object({
928
1050
  available: z.boolean(),
@@ -971,7 +1093,13 @@ const outputSchema$6 = z.object({
971
1093
  componentId: z.string().optional(),
972
1094
  label: z.string().optional(),
973
1095
  details: z.string().optional()
974
- }))
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()
975
1103
  }).optional(),
976
1104
  history: z.object({
977
1105
  currentIndex: z.number().int(),
@@ -1030,9 +1158,9 @@ const buildSummary = (snapshot) => ({
1030
1158
  historyIndex: snapshot.version.historyIndex,
1031
1159
  outlineItemCount: snapshot.inspection.itemIndex.length,
1032
1160
  responseSlotCount: snapshot.inspection.responseSlots.length,
1033
- availableItemTypes: snapshot.capabilities.itemTypes.map((entry) => entry.itemType),
1034
- deterministicCreateItemTypes: getDeterministicCreateItemTypes(snapshot.capabilities),
1035
- itemTypeDetails: snapshot.capabilities.itemTypes
1161
+ availableElementTypes: snapshot.capabilities.elementTypes.map((entry) => entry.elementType),
1162
+ deterministicCreateElementTypes: getDeterministicCreateElementTypes(snapshot.capabilities),
1163
+ elementTypeDetails: snapshot.capabilities.elementTypes
1036
1164
  });
1037
1165
  const buildSelectedItem = (snapshot, input) => {
1038
1166
  const outlineById = mapOutlineById(snapshot);
@@ -1087,7 +1215,7 @@ const buildOutline = (snapshot, input) => {
1087
1215
  if (!parent) note = "The requested item could not be resolved by exact item ID.";
1088
1216
  else {
1089
1217
  const rawParent = snapshot.survey.surveyItems.find((item) => item.id === parent.itemId);
1090
- if (rawParent?.itemType === "group") parentShuffled = isRecord$3(rawParent.config) && rawParent.config.shuffleItems === true;
1218
+ if (rawParent?.itemType === "group") parentShuffled = rawParent.config.shuffleItems === true;
1091
1219
  }
1092
1220
  } else if (outlineMode === "focused" && requestedItemId && snapshot.inspection.focus?.focusItemId === requestedItemId) {
1093
1221
  const focus = snapshot.inspection.focus;
@@ -1161,9 +1289,9 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1161
1289
  if (scopes.includes("summary")) Object.assign(listOrdering, {
1162
1290
  "summary.recentItemIds": "Most-recent-first UI history; not survey structure.",
1163
1291
  "summary.locales": "Selected locale first, then remaining source locale order; locale position has no survey meaning.",
1164
- "summary.availableItemTypes": "Capability registry order; treat as a set of available types.",
1165
- "summary.deterministicCreateItemTypes": "Capability registry order; treat as a set of available types.",
1166
- "summary.itemTypeDetails": "Capability registry order; treat as a set of available type definitions."
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."
1167
1295
  });
1168
1296
  if (scopes.includes("selected-item")) Object.assign(listOrdering, {
1169
1297
  "selectedItem.pathItems": "Root-to-selected survey-tree path.",
@@ -1191,7 +1319,8 @@ const inspectSurveyCurrentContext = (inputData, requestContext) => {
1191
1319
  if (scopes.includes("ui-context")) output.uiContext = {
1192
1320
  activeSurface: snapshot.ui.activeSurface,
1193
1321
  openPanels: snapshot.ui.openPanels ?? [],
1194
- highlights: snapshot.ui.highlights ?? []
1322
+ highlights: snapshot.ui.highlights ?? [],
1323
+ ...snapshot.ui.task ? { task: snapshot.ui.task } : {}
1195
1324
  };
1196
1325
  if (scopes.includes("history") && snapshot.ui.history) output.history = snapshot.ui.history;
1197
1326
  return parseInspectionOutput(outputSchema$6, output, "Current-context inspection");
@@ -1204,9 +1333,8 @@ const inputSchema$6 = z.object({
1204
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."),
1205
1334
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict returned translations to. Defaults to all locales."),
1206
1335
  textLimit: z.number().int().min(20).max(2e3).default(400).describe("Maximum characters returned per translation snippet."),
1207
- includeChildren: z.boolean().optional().describe("Deprecated alias for includeSurveyChildren. This means survey-tree children only, not choice options or form fields."),
1208
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."),
1209
- 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."),
1210
1338
  childrenLimit: z.number().int().min(1).max(50).default(20).describe("Maximum number of direct survey-tree children to return.")
1211
1339
  });
1212
1340
  const outputSchema$5 = z.object({
@@ -1216,23 +1344,22 @@ const outputSchema$5 = z.object({
1216
1344
  translations: z.array(translationEntrySchema).optional(),
1217
1345
  surveyChildren: z.array(itemSummarySchema).optional(),
1218
1346
  surveyChildrenTruncated: z.boolean().optional(),
1219
- listOrdering: z.record(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1347
+ listOrdering: stringRecord(z.string(), z.string()).optional().describe("Meaning of each returned list. A list is positional only when its entry says so."),
1220
1348
  configDetails: itemConfigDetailsSchema.optional(),
1221
1349
  responseVariables: z.array(responseVariableSummarySchema).optional(),
1222
1350
  responseVariablesTotal: z.number().optional(),
1223
1351
  responseVariablesTruncated: z.boolean().optional(),
1224
1352
  responseVariablesNextCursor: z.string().optional(),
1225
1353
  responseVariablesNote: z.string().optional(),
1226
- capabilityDetails: z.object({
1354
+ capabilityDetails: z.array(z.object({
1355
+ elementId: z.string(),
1356
+ elementType: z.string(),
1227
1357
  summary: z.string(),
1228
1358
  details: z.unknown().optional()
1229
- }).optional()
1359
+ })).optional()
1230
1360
  });
1231
1361
  const inspectSurveyItemDetails = (inputData, requestContext) => {
1232
- const input = inputSchema$6.parse({
1233
- ...inputData,
1234
- includeSurveyChildren: inputData.includeSurveyChildren ?? inputData.includeChildren
1235
- });
1362
+ const input = inputSchema$6.parse(inputData);
1236
1363
  const snapshot = getSurveyAssistantEngineContext(requestContext);
1237
1364
  if (!snapshot) return {
1238
1365
  ok: false,
@@ -1247,14 +1374,13 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1247
1374
  reason: `No item matching "${input.itemId}" (by exact itemId) was found in the current context snapshot.`
1248
1375
  };
1249
1376
  const rawItem = rawItemsById.get(item.itemId);
1250
- const rawConfig = rawItem?.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1251
1377
  const output = {
1252
1378
  ok: true,
1253
1379
  item,
1254
1380
  translations: buildItemTranslationEntries(snapshot, item.itemId, input),
1255
1381
  listOrdering: {
1256
- translations: "Grouped by locale and then source translation-key order; this list is not positional survey content.",
1257
- surveyChildren: rawConfig?.shuffleItems === true ? "Stored group-child order; respondents see shuffled children, so this is not a stable respondent-visible order." : "Stored group-child order; this is the respondent-visible order when the parent is a group."
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."
1258
1384
  }
1259
1385
  };
1260
1386
  if (input.includeSurveyChildren) {
@@ -1274,22 +1400,19 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1274
1400
  output.responseVariablesTotal = variables.length;
1275
1401
  output.responseVariablesTruncated = page.truncated;
1276
1402
  output.responseVariablesNextCursor = page.nextCursor;
1277
- output.responseVariablesNote = "Use responseVariablesNextCursor as cursor with the same itemRef to continue. Option and computed-source summaries include up to 50 entries; inspect exact raw-json config paths for complete domains and export settings. Restart pagination after mutations.";
1278
- output.configDetails = rawItem ? buildItemConfigDetails(rawItem) : void 0;
1279
- output.capabilityDetails = rawItem ? inspectAssistantItem({
1280
- capabilities: snapshot.trustedCapabilities,
1281
- item: rawItem
1282
- }) : void 0;
1283
- if (rawItem?.itemType === "choiceItem" && output.configDetails?.choiceOptions) {
1284
- const rawChoiceConfig = rawItem.config && typeof rawItem.config === "object" && !Array.isArray(rawItem.config) ? rawItem.config : void 0;
1285
- const rawOptions = Array.isArray(rawChoiceConfig?.options) ? rawChoiceConfig.options : [];
1286
- for (const option of output.configDetails.choiceOptions) {
1287
- const rawOption = rawOptions.find((candidate) => candidate && typeof candidate === "object" && "id" in candidate && candidate.id === option.id);
1288
- const embeddedForm = rawOption && typeof rawOption === "object" && "embeddedForm" in rawOption ? rawOption.embeddedForm : void 0;
1289
- const embeddedFormDetails = buildEmbeddedFormDetails(option, embeddedForm);
1290
- if (embeddedFormDetails?.formFieldGroups?.length) output.configDetails.formFieldGroups = [...output.configDetails.formFieldGroups ?? [], ...embeddedFormDetails.formFieldGroups];
1291
- }
1292
- }
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
+ });
1293
1416
  }
1294
1417
  return parseInspectionOutput(outputSchema$5, output, "Item-details inspection");
1295
1418
  };
@@ -1297,7 +1420,7 @@ const inspectSurveyItemDetails = (inputData, requestContext) => {
1297
1420
  //#region src/server/tools/inspect-raw-survey.ts
1298
1421
  const DEFAULT_VALUE_CHARACTER_LIMIT = 12e3;
1299
1422
  const MAX_VALUE_CHARACTER_LIMIT = 6e4;
1300
- const jsonPointerPathSchema = z.string().max(500).describe("JSON Pointer path into the survey document. Examples: \"/surveyItems/0/config/items\", \"/metadata\", \"/translations/en/itemTranslations/<itemId>/title\".");
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\".");
1301
1424
  const inputSchema$5 = z.object({
1302
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."),
1303
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."),
@@ -1317,7 +1440,7 @@ const outputSchema$4 = z.object({
1317
1440
  reason: z.string().optional(),
1318
1441
  revision: z.number().int().nonnegative().optional(),
1319
1442
  contentHash: z.string().optional(),
1320
- 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."),
1321
1444
  overview: z.object({
1322
1445
  topLevelKeys: z.array(z.string()),
1323
1446
  surveyItemCount: z.number().int().nonnegative(),
@@ -1347,7 +1470,7 @@ const outputSchema$4 = z.object({
1347
1470
  rawItemPreview: z.string().optional(),
1348
1471
  rawItemTruncated: z.boolean().optional(),
1349
1472
  translationPaths: z.array(z.string()).optional(),
1350
- translationsByLocale: z.record(z.string(), z.unknown()).optional(),
1473
+ translationsByLocale: stringRecord(z.string(), z.unknown()).optional(),
1351
1474
  translationsPreview: z.string().optional(),
1352
1475
  translationsTruncated: z.boolean().optional(),
1353
1476
  error: z.string().optional()
@@ -1446,16 +1569,19 @@ const findRawItemByIdentifier = (snapshot, identifier) => {
1446
1569
  index: resolvedIndex
1447
1570
  };
1448
1571
  };
1449
- const getTranslationsByLocaleForItem = (rawSurvey, itemId) => {
1572
+ const getTranslationsByLocaleForItem = (snapshot, itemId) => {
1450
1573
  const entries = {};
1451
- for (const [locale, localeTranslations] of Object.entries(rawSurvey.translations ?? {})) {
1452
- const itemTranslations = localeTranslations?.itemTranslations?.[itemId];
1453
- 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);
1454
1581
  }
1455
1582
  return entries;
1456
1583
  };
1457
1584
  const buildItemValues = (snapshot, input) => {
1458
- const rawSurvey = getRawSurvey(snapshot);
1459
1585
  return input.itemIdentifiers.map((identifier) => {
1460
1586
  const resolved = findRawItemByIdentifier(snapshot, identifier);
1461
1587
  if (!resolved) return {
@@ -1465,7 +1591,7 @@ const buildItemValues = (snapshot, input) => {
1465
1591
  };
1466
1592
  const itemId = resolved.item.id;
1467
1593
  const rawItemOutput = toOutputValue(resolved.item, input.valueCharacterLimit);
1468
- const translationsByLocale = getTranslationsByLocaleForItem(rawSurvey, itemId);
1594
+ const translationsByLocale = getTranslationsByLocaleForItem(snapshot, itemId);
1469
1595
  const translationsOutput = toOutputValue(translationsByLocale, input.valueCharacterLimit);
1470
1596
  const translationsValue = "value" in translationsOutput && isRecord$1(translationsOutput.value) ? translationsOutput.value : void 0;
1471
1597
  return {
@@ -1476,7 +1602,7 @@ const buildItemValues = (snapshot, input) => {
1476
1602
  rawItem: "value" in rawItemOutput ? rawItemOutput.value : void 0,
1477
1603
  rawItemPreview: "preview" in rawItemOutput ? rawItemOutput.preview : void 0,
1478
1604
  rawItemTruncated: rawItemOutput.truncated,
1479
- 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)}`)),
1480
1606
  translationsByLocale: translationsValue,
1481
1607
  translationsPreview: "preview" in translationsOutput ? translationsOutput.preview : void 0,
1482
1608
  translationsTruncated: translationsOutput.truncated
@@ -1526,7 +1652,8 @@ const searchMatchSchema = z.object({
1526
1652
  ]),
1527
1653
  field: z.string().optional(),
1528
1654
  locale: z.string().optional(),
1529
- contentKey: z.string().optional(),
1655
+ ownerId: z.string().optional(),
1656
+ role: z.string().optional(),
1530
1657
  text: z.string(),
1531
1658
  score: z.number()
1532
1659
  });
@@ -1537,7 +1664,8 @@ const searchResultSchema = z.object({
1537
1664
  });
1538
1665
  const inputSchema$4 = z.object({
1539
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\"."),
1540
- 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."),
1541
1669
  locales: z.array(z.string()).optional().describe("Optional locale codes to restrict translation-content matches to."),
1542
1670
  cursor: z.string().optional().describe("Optional numeric cursor returned by a previous search."),
1543
1671
  limit: z.number().int().min(1).max(50).default(12).describe("Maximum number of matching items to return.")
@@ -1596,7 +1724,7 @@ const scoreText = (value, query, terms, baseScore) => {
1596
1724
  const addMatch = (matches, match) => {
1597
1725
  const text = match.text?.trim();
1598
1726
  if (!text) return;
1599
- 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({
1600
1728
  ...match,
1601
1729
  text
1602
1730
  });
@@ -1625,11 +1753,19 @@ const candidateItems = (snapshot) => {
1625
1753
  const searchItems = (snapshot, input) => {
1626
1754
  const terms = queryTerms(input.query);
1627
1755
  const itemTypeFilter = input.itemTypes?.length ? new Set(input.itemTypes) : void 0;
1628
- 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 }) => {
1629
1764
  const matches = [];
1630
1765
  const structuralFields = [
1631
1766
  ["itemId", item.itemId],
1632
1767
  ["itemType", item.itemType],
1768
+ ["elementTypes", [...typesByItem.get(item.itemId) ?? []].join(" ")],
1633
1769
  ["key", item.editorName],
1634
1770
  ["breadcrumb", item.breadcrumb],
1635
1771
  ["itemLabel", item.itemLabel],
@@ -1649,16 +1785,17 @@ const searchItems = (snapshot, input) => {
1649
1785
  locales: input.locales,
1650
1786
  textLimit: DEFAULT_TEXT_LIMIT
1651
1787
  })) {
1652
- 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);
1653
1789
  if (score !== null) addMatch(matches, {
1654
1790
  source: "translation",
1655
1791
  locale: translation.locale,
1656
- contentKey: translation.contentKey,
1792
+ ownerId: translation.ownerId,
1793
+ role: translation.role,
1657
1794
  text: translation.text,
1658
1795
  score
1659
1796
  });
1660
1797
  }
1661
- if (rawItem) for (const text of buildConfigSearchText(rawItem)) {
1798
+ if (rawItem) for (const text of buildConfigSearchText(rawItem, snapshot.model)) {
1662
1799
  const score = scoreText(text, input.query, terms, 60);
1663
1800
  if (score !== null) addMatch(matches, {
1664
1801
  source: "config",
@@ -1730,11 +1867,27 @@ const inputSchema$3 = z.object({
1730
1867
  "history"
1731
1868
  ])).min(1).max(6)).describe("For current-context: context sections to inspect. Empty means omitted."),
1732
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."),
1733
- sourceLocale: optionalNonBlankString(40).describe("For localization: source locale to translate. Defaults to the selected editor locale."),
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."),
1734
1878
  targetLocale: optionalNonBlankString(40).describe("For localization: required target locale whose coverage should be inspected."),
1735
- missingOnly: optionalInput(z.boolean()).describe("For localization: return only source surfaces that do not yet exist in the target locale."),
1736
- itemRef: optionalNonBlankString(240).describe("For current-context outline, item-details, or raw-json: item id, key, breadcrumb, visible label, or \"selected\"."),
1737
- cursor: optionalNonBlankString(240).describe("For current-context, item-details response variables, search-items, or localization pagination: numeric cursor from a previous result."),
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."),
1738
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."),
1739
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.`),
1740
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."),
@@ -1775,149 +1928,27 @@ const inspectionBackendFailure = (action, reason) => ({
1775
1928
  modelSummary: reason
1776
1929
  });
1777
1930
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1778
- const MAX_LOCALIZATION_TEXT_CHARS = 2e3;
1779
- const SURVEY_TRANSLATION_SURFACES = [
1780
- "surveyCardContent",
1781
- "navigationContent",
1782
- "validationMessages"
1783
- ];
1784
- const truncateLocalizationText = (content) => {
1785
- const text = getContentPlainText(content).trim();
1786
- return text.length > MAX_LOCALIZATION_TEXT_CHARS ? `${text.slice(0, MAX_LOCALIZATION_TEXT_CHARS)}...` : text;
1787
- };
1788
- const listLocalizationSurfaces = (input, snapshot, targetLocale) => {
1789
- const survey = snapshot.survey;
1790
- const names = new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item.editorName]));
1791
- const sourceLocale = input.sourceLocale ?? snapshot.ui.selectedLocale;
1792
- const sourceTranslations = survey.translations?.[sourceLocale];
1793
- const targetTranslations = survey.translations?.[targetLocale];
1794
- if (!sourceTranslations) return {
1795
- ok: false,
1796
- reason: `Source locale "${sourceLocale}" is not present in the current survey draft.`
1797
- };
1798
- const surfaces = [];
1799
- const addSurface = ({ contentKey, itemId, itemKey, itemType, sourceContent, surface, targetContent }) => {
1800
- const sourceText = truncateLocalizationText(sourceContent);
1801
- if (!sourceText) return;
1802
- const sourcePath = `/translations/${encodeJsonPointerSegment(sourceLocale)}/${itemId ? `itemTranslations/${encodeJsonPointerSegment(itemId)}` : surface}/${encodeJsonPointerSegment(contentKey)}`;
1803
- surfaces.push({
1804
- scope: itemId ? "item" : "survey",
1805
- ...surface ? { surface } : {},
1806
- ...itemId ? {
1807
- itemId,
1808
- itemKey,
1809
- itemType
1810
- } : {},
1811
- contentKey,
1812
- sourceType: sourceContent.type,
1813
- sourceText,
1814
- sourceTextLength: getContentPlainText(sourceContent).trim().length,
1815
- sourceTruncated: getContentPlainText(sourceContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS,
1816
- sourcePath,
1817
- ...sourceContent.type !== "richText" ? { sourceTextPath: `${sourcePath}/content` } : {},
1818
- ...sourceContent.type === "richText" ? { sourceContent } : {},
1819
- targetPresent: Boolean(targetContent && truncateLocalizationText(targetContent)),
1820
- ...targetContent ? {
1821
- targetType: targetContent.type,
1822
- targetText: truncateLocalizationText(targetContent),
1823
- targetTextLength: getContentPlainText(targetContent).trim().length,
1824
- targetTruncated: getContentPlainText(targetContent).trim().length > MAX_LOCALIZATION_TEXT_CHARS
1825
- } : {}
1826
- });
1827
- };
1828
- for (const surface of SURVEY_TRANSLATION_SURFACES) {
1829
- const targetSurface = targetTranslations?.[surface];
1830
- for (const [contentKey, sourceContent] of Object.entries(sourceTranslations[surface] ?? {})) addSurface({
1831
- surface,
1832
- contentKey,
1833
- sourceContent,
1834
- targetContent: targetSurface?.[contentKey]
1835
- });
1836
- }
1837
- for (const item of survey.surveyItems) for (const [contentKey, sourceContent] of Object.entries(sourceTranslations.itemTranslations?.[item.id] ?? {})) addSurface({
1838
- itemId: item.id,
1839
- itemKey: names.get(item.id) ?? item.id,
1840
- itemType: item.itemType,
1841
- contentKey,
1842
- sourceContent,
1843
- targetContent: targetTranslations?.itemTranslations?.[item.id]?.[contentKey]
1844
- });
1845
- const missingCount = surfaces.filter((surface) => !surface.targetPresent).length;
1846
- const filteredSurfaces = input.missingOnly ? surfaces.filter((surface) => !surface.targetPresent) : surfaces;
1847
- const parsedCursor = Number.parseInt(input.cursor ?? "0", 10);
1848
- const offset = Number.isFinite(parsedCursor) && parsedCursor >= 0 ? parsedCursor : 0;
1849
- const limit = input.limit ?? 20;
1850
- const page = filteredSurfaces.slice(offset, offset + limit);
1851
- const nextOffset = offset + page.length;
1852
- return {
1853
- ok: true,
1854
- result: {
1855
- sourceLocale,
1856
- targetLocale,
1857
- missingOnly: input.missingOnly ?? false,
1858
- contentCoverage: "sourceText and targetText are previews. For sourceTruncated plain text, inspect raw-json paths: [sourceTextPath] and follow nextTextOffset with textOffset until complete before translating. Rich-text sourceContent is the complete canonical value. Never translate only a truncated preview or treat targetPresent as proof of translation completeness.",
1859
- totalSourceSurfaces: surfaces.length,
1860
- totalAvailable: filteredSurfaces.length,
1861
- missingCount,
1862
- returned: page.length,
1863
- ...nextOffset < filteredSurfaces.length ? { nextCursor: String(nextOffset) } : {},
1864
- 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." },
1865
- surfaces: page
1866
- }
1867
- };
1868
- };
1869
1931
  const resolveItemRef = (input, snapshot) => {
1870
1932
  if (input.itemRef && input.itemRef !== "selected") return input.itemRef;
1871
1933
  return snapshot?.ui.selectedItemId;
1872
1934
  };
1873
- const listAssetUsageSummaries = (survey) => {
1874
- const usagesByAssetId = /* @__PURE__ */ new Map();
1875
- const addContent = (content, usage) => {
1876
- for (const assetUsage of getAssetUsagesFromContent(content)) {
1877
- const usages = usagesByAssetId.get(assetUsage.assetId) ?? [];
1878
- usages.push({
1879
- ...usage,
1880
- blockIndex: assetUsage.blockIndex
1881
- });
1882
- usagesByAssetId.set(assetUsage.assetId, usages);
1883
- }
1884
- };
1885
- for (const [locale, localeTranslations] of Object.entries(survey.translations ?? {})) {
1886
- for (const scope of [
1887
- "surveyCardContent",
1888
- "navigationContent",
1889
- "validationMessages"
1890
- ]) for (const [contentKey, content] of Object.entries(localeTranslations[scope] ?? {})) addContent(content, {
1891
- locale,
1892
- scope,
1893
- contentKey
1894
- });
1895
- for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations ?? {})) for (const [contentKey, content] of Object.entries(itemTranslations ?? {})) addContent(content, {
1896
- locale,
1897
- scope: "itemTranslation",
1898
- itemId,
1899
- contentKey
1900
- });
1901
- }
1902
- return usagesByAssetId;
1903
- };
1904
1935
  const summarizeAssetSource = (asset) => ({
1905
1936
  type: asset.source.type,
1906
1937
  mediaType: asset.source.mediaType
1907
1938
  });
1908
1939
  const listAssets = (input, snapshot) => {
1909
1940
  const survey = snapshot.survey;
1910
- const usagesByAssetId = listAssetUsageSummaries(survey);
1941
+ const usages = snapshot.model.getAssetUsages();
1911
1942
  const query = input.query?.trim().toLowerCase();
1912
1943
  const allAssets = Object.entries(survey.assets ?? {}).map(([assetId, asset]) => {
1913
- const usages = usagesByAssetId.get(assetId) ?? [];
1944
+ const assetUsages = usages.filter((usage) => usage.assetId === assetId);
1914
1945
  return {
1915
1946
  assetId,
1916
1947
  kind: asset.kind,
1917
1948
  source: summarizeAssetSource(asset),
1918
1949
  ...asset.metadata ? { metadata: asset.metadata } : {},
1919
- usageCount: usages.length,
1920
- usages
1950
+ usageCount: assetUsages.length,
1951
+ usages: assetUsages
1921
1952
  };
1922
1953
  }).filter((asset) => {
1923
1954
  if (!query) return true;
@@ -1947,7 +1978,7 @@ const listAssets = (input, snapshot) => {
1947
1978
  };
1948
1979
  const surveyInspectTool = createTool({
1949
1980
  id: TOOL_ID$3,
1950
- description: "Inspect the current request-scoped survey draft through one compact interface: current context, item search, item details, paged source/target localization surfaces, image assets with explicit asset IDs and usages, expression-reference diagnostics, or focused raw JSON. For expression diagnostics, valid means there are no blocking diagnostics; advisories remain counted. At turn start the draft equals the live snapshot; successful same-turn edits appear immediately.",
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.",
1951
1982
  inputSchema: inputSchema$3,
1952
1983
  outputSchema: outputSchema$2,
1953
1984
  mcp: { annotations: {
@@ -1996,7 +2027,10 @@ const surveyInspectTool = createTool({
1996
2027
  const inspection = runInspectionBackend(() => searchSurveyItems({
1997
2028
  query,
1998
2029
  limit: input.limit,
1999
- cursor: input.cursor
2030
+ cursor: input.cursor,
2031
+ itemTypes: input.itemTypes,
2032
+ elementTypes: input.elementTypes,
2033
+ locales: input.locales
2000
2034
  }, requestContext));
2001
2035
  if (!inspection.ok) return inspectionBackendFailure(input.action, inspection.reason);
2002
2036
  const result = inspection.value;
@@ -2037,15 +2071,16 @@ const surveyInspectTool = createTool({
2037
2071
  const allDiagnostics = analyzeSurveyAssistantExpressionReferences(snapshot.survey, snapshot.trustedCapabilities);
2038
2072
  const itemId = input.itemRef ? resolveItemRef(input, snapshot) : void 0;
2039
2073
  const diagnostics = itemId ? allDiagnostics.filter((diagnostic) => diagnostic.ownerItemId === itemId) : allDiagnostics;
2040
- const orderedDiagnostics = [...diagnostics].sort((left, right) => Number(isBlockingSurveyExpressionReferenceDiagnostic(right)) - Number(isBlockingSurveyExpressionReferenceDiagnostic(left)));
2041
- const blockingCount = diagnostics.filter(isBlockingSurveyExpressionReferenceDiagnostic).length;
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;
2042
2076
  const advisoryCount = diagnostics.length - blockingCount;
2043
2077
  const limit = input.limit ?? 50;
2044
2078
  const limitedDiagnostics = orderedDiagnostics.slice(0, limit);
2045
- const guidanceByDispositionAndCategory = {};
2079
+ const guidanceByCode = {};
2046
2080
  for (const diagnostic of orderedDiagnostics) {
2047
- const byCategory = guidanceByDispositionAndCategory[diagnostic.disposition] ??= {};
2048
- 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);
2049
2084
  }
2050
2085
  const result = {
2051
2086
  valid: blockingCount === 0 && visibilityIssues.length === 0,
@@ -2055,7 +2090,7 @@ const surveyInspectTool = createTool({
2055
2090
  blockingCount,
2056
2091
  advisoryCount,
2057
2092
  diagnostics: limitedDiagnostics,
2058
- guidanceByDispositionAndCategory,
2093
+ guidanceByCode,
2059
2094
  truncated: diagnostics.length > limit
2060
2095
  };
2061
2096
  return {
@@ -2072,7 +2107,10 @@ const surveyInspectTool = createTool({
2072
2107
  reason: "Localization inspection requires a targetLocale.",
2073
2108
  modelSummary: "Localization inspection requires a targetLocale."
2074
2109
  };
2075
- const localization = listLocalizationSurfaces(input, snapshot, input.targetLocale);
2110
+ const localization = inspectLocalization({
2111
+ ...input,
2112
+ targetLocale: input.targetLocale
2113
+ }, snapshot);
2076
2114
  if (!localization.ok) return {
2077
2115
  ok: false,
2078
2116
  action: input.action,
@@ -2129,7 +2167,10 @@ const surveyInspectTool = createTool({
2129
2167
  result
2130
2168
  };
2131
2169
  },
2132
- 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)
2133
2174
  });
2134
2175
  //#endregion
2135
2176
  //#region src/server/tools/survey-attachment-tool.ts
@@ -2326,14 +2367,14 @@ const formatAuditForModel = (output) => {
2326
2367
  const location = [
2327
2368
  finding.itemId,
2328
2369
  finding.locale,
2329
- finding.contentKey
2370
+ finding.localizationRefs ? JSON.stringify(finding.localizationRefs) : finding.path?.join("/")
2330
2371
  ].filter(Boolean).join(":");
2331
2372
  return `- [${finding.severity}] ${finding.code}${location ? ` (${location})` : ""}: ${previewAuditMessage(finding.message)}${finding.suggestion ? ` Guidance: ${previewAuditMessage(finding.suggestion)}` : ""}`;
2332
2373
  }).join("\n")}` : void 0,
2333
2374
  report.qualityReviewPrinciples.length > 0 ? `Qualitative review rubric:\n${report.qualityReviewPrinciples.map((principle) => `- ${principle.id}: ${principle.prompt}`).join("\n")}` : void 0,
2334
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) => {
2335
2376
  const content = entry.content ? `; current content=${JSON.stringify(previewAuditMessage(entry.content))}` : "; current content unavailable";
2336
- 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}`;
2337
2378
  }).join("\n")}\n${report.keyReview.note}` : `Key/content review evidence: none returned (${report.keyReview.entryCount} total entries).`,
2338
2379
  report.note,
2339
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."
@@ -2384,21 +2425,7 @@ const inputSchema = z.object({
2384
2425
  const slug = (value) => value.toLowerCase().replace(/\.[^.]+$/, "").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100) || "image";
2385
2426
  const OMITTED_EMBEDDED_IMAGE_DATA = "[embedded image data omitted from transcript]";
2386
2427
  const getAssetReferenceLabels = (snapshot, assetId) => {
2387
- const translations = (snapshot?.survey)?.translations;
2388
- if (!translations) return [];
2389
- const references = [];
2390
- const inspectContent = (content, label) => {
2391
- if (getAssetUsagesFromContent(content).some((usage) => usage.assetId === assetId)) references.push(label);
2392
- };
2393
- for (const [locale, localeTranslations] of Object.entries(translations)) {
2394
- for (const surface of [
2395
- "surveyCardContent",
2396
- "navigationContent",
2397
- "validationMessages"
2398
- ]) for (const [contentKey, content] of Object.entries(localeTranslations[surface] ?? {})) inspectContent(content, `${locale}:${surface}:${contentKey}`);
2399
- for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations ?? {})) for (const [contentKey, content] of Object.entries(itemTranslations ?? {})) inspectContent(content, `${locale}:item:${itemId}:${contentKey}`);
2400
- }
2401
- 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}`}`) ?? [];
2402
2429
  };
2403
2430
  const getToolOutput = (value) => value && typeof value === "object" && "output" in value && value.output !== void 0 ? value.output : value;
2404
2431
  const describeAssetOperation = (operation) => {
@@ -2418,8 +2445,8 @@ const summarizeAssetChange = (event) => {
2418
2445
  lines.push(`Imported: ${event.operations.filter((operation) => operation.kind === "set-image-asset").map(describeAssetOperation).join(", ")}`);
2419
2446
  lines.push(`Asset id to reference: ${importedAssetIds.join(", ")}`);
2420
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.");
2421
- lines.push(`New info-item image source: { "type": "asset", "assetId": "${importedAssetIds[0]}" }. Embed the canonical image block inside the create-item translation richTextBlocks array.`);
2422
- lines.push(`New info-item translation example: { "locale": "<selected-locale>", "contentKey": "content", "richTextBlocks": [{ "type": "image", "source": { "type": "asset", "assetId": "${importedAssetIds[0]}" }, "alt": "<meaningful description>" }] }.`);
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>" }] }.`);
2423
2450
  }
2424
2451
  }
2425
2452
  if (event.validation.issues.length > 0) lines.push(`Issues:\n${event.validation.issues.map((issue) => `- ${issue.code}: ${issue.message}`).join("\n")}`);
@@ -2474,7 +2501,7 @@ const surveyAssetTool = createTool({
2474
2501
  let assetId = base;
2475
2502
  let suffix = 2;
2476
2503
  const stagedAssetIds = new Set(turnDraft.draftOperations.flatMap((draftOperation) => draftOperation.kind === "set-image-asset" ? [draftOperation.assetId] : []));
2477
- while (snapshot?.survey.assets?.[assetId] || stagedAssetIds.has(assetId)) assetId = `${base}-${suffix++}`;
2504
+ while (ownValue(snapshot?.survey.assets, assetId) || stagedAssetIds.has(assetId)) assetId = `${base}-${suffix++}`;
2478
2505
  operation = {
2479
2506
  kind: "set-image-asset",
2480
2507
  assetId,
@@ -2525,7 +2552,7 @@ const surveyAssetTool = createTool({
2525
2552
  validation: {
2526
2553
  ...event.validation,
2527
2554
  valid: false,
2528
- issues: [...event.validation.issues, {
2555
+ issues: [...event.validation.issues.filter((issue) => issue.code !== "image-asset-still-referenced"), {
2529
2556
  severity: "error",
2530
2557
  code: "image-asset-still-referenced",
2531
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.`,
@@ -2569,4 +2596,4 @@ const surveyAssistantTools = Object.freeze({
2569
2596
  //#endregion
2570
2597
  export { surveyAuditTool as i, surveyAssistantTools as n, surveyAssetTool as r, SURVEY_ASSISTANT_TOOL_IDS as t };
2571
2598
 
2572
- //# sourceMappingURL=tools-CEsv-WoO.mjs.map
2599
+ //# sourceMappingURL=tools-D0KBDolR.mjs.map