@case-framework/survey-assistant 0.2.1 → 0.2.3
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.
- package/dist/{authoring-references-DYuOAdVx.mjs → authoring-references-ChDTVnms.mjs} +8 -2
- package/dist/authoring-references-ChDTVnms.mjs.map +1 -0
- package/dist/{capabilities-DlC1l6qy.d.mts → capabilities-CnsVuJ1k.d.mts} +12 -4
- package/dist/capabilities-CnsVuJ1k.d.mts.map +1 -0
- package/dist/capabilities-default.d.mts +1 -1
- package/dist/capabilities-default.d.mts.map +1 -1
- package/dist/capabilities-default.mjs +38 -4
- package/dist/capabilities-default.mjs.map +1 -1
- package/dist/{constants-CgV4cTg5.d.mts → constants-BK1-g6pR.d.mts} +12 -7
- package/dist/constants-BK1-g6pR.d.mts.map +1 -0
- package/dist/{controller-proxy-BzpEOygv.d.mts → controller-proxy-CIDRd8Wu.d.mts} +2 -2
- package/dist/{controller-proxy-BzpEOygv.d.mts.map → controller-proxy-CIDRd8Wu.d.mts.map} +1 -1
- package/dist/{controller-proxy-0WJZUZYx.mjs → controller-proxy-DY36XFV3.mjs} +2 -2
- package/dist/{controller-proxy-0WJZUZYx.mjs.map → controller-proxy-DY36XFV3.mjs.map} +1 -1
- package/dist/{digest-BvXpeWcR.d.mts → digest-Dyjch_sC.d.mts} +2 -2
- package/dist/{digest-BvXpeWcR.d.mts.map → digest-Dyjch_sC.d.mts.map} +1 -1
- package/dist/digest.d.mts +1 -1
- package/dist/{engine-0AJrv0hI.mjs → engine-CWTuN3o2.mjs} +866 -663
- package/dist/engine-CWTuN3o2.mjs.map +1 -0
- package/dist/engine.d.mts +3 -3
- package/dist/engine.mjs +3 -3
- package/dist/fair-guidance-CEdJYccK.mjs +21 -0
- package/dist/fair-guidance-CEdJYccK.mjs.map +1 -0
- package/dist/form-authoring-DCd9XfXf.mjs +155 -0
- package/dist/form-authoring-DCd9XfXf.mjs.map +1 -0
- package/dist/{index-BUF-sE8j.d.mts → index-Bp5W_oPa.d.mts} +27 -7
- package/dist/index-Bp5W_oPa.d.mts.map +1 -0
- package/dist/{index-BdXb6jER.d.mts → index-CPOQgkTN.d.mts} +67 -6
- package/dist/index-CPOQgkTN.d.mts.map +1 -0
- package/dist/{index-BiTT71Xp.d.mts → index-EDL1V168.d.mts} +101 -7
- package/dist/{index-BiTT71Xp.d.mts.map → index-EDL1V168.d.mts.map} +1 -1
- package/dist/{index-BNdvGEaq.d.mts → index-SPHf3OoJ.d.mts} +9 -3
- package/dist/index-SPHf3OoJ.d.mts.map +1 -0
- package/dist/{memory-thread-repository-1ocPOda8.mjs → memory-thread-repository-acbz6HZJ.mjs} +181 -26
- package/dist/memory-thread-repository-acbz6HZJ.mjs.map +1 -0
- package/dist/{protocol-DpCYmCjr.mjs → protocol-DATvkr1S.mjs} +125 -6
- package/dist/protocol-DATvkr1S.mjs.map +1 -0
- package/dist/protocol.d.mts +3 -3
- package/dist/protocol.mjs +2 -2
- package/dist/{react-DXK5zWNo.mjs → react-3kSE6WtS.mjs} +127 -9
- package/dist/react-3kSE6WtS.mjs.map +1 -0
- package/dist/react-integration.d.mts +1 -1
- package/dist/react-integration.mjs +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.mjs +2 -2
- package/dist/references/assistant-operations.md +13 -18
- package/dist/references/embedded-forms.md +41 -2
- package/dist/references/expressions.md +17 -9
- package/dist/references/fair-by-design.md +80 -0
- package/dist/references/item-types.md +19 -3
- package/dist/references/localization.md +7 -4
- package/dist/references/source-material-surveys.md +6 -4
- package/dist/{request-context-BBzU3VdG.mjs → request-context-BEjnaaFr.mjs} +10 -5
- package/dist/request-context-BEjnaaFr.mjs.map +1 -0
- package/dist/server-agent.d.mts +28 -8
- package/dist/server-agent.d.mts.map +1 -1
- package/dist/server-agent.mjs +70 -13
- package/dist/server-agent.mjs.map +1 -1
- package/dist/server-runtime.d.mts +22 -11
- package/dist/server-runtime.d.mts.map +1 -1
- package/dist/server-runtime.mjs +47 -19
- package/dist/server-runtime.mjs.map +1 -1
- package/dist/server-tasks.d.mts +1 -1
- package/dist/server-tasks.mjs +4 -3
- package/dist/server-tasks.mjs.map +1 -1
- package/dist/server-tools.d.mts +1 -1
- package/dist/server-tools.mjs +1 -1
- package/dist/server.d.mts +16 -7
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +4 -4
- package/dist/storage-postgres.d.mts +1 -1
- package/dist/{tasks-BuLnWrR4.d.mts → tasks-DIZgr4nD.d.mts} +8 -4
- package/dist/tasks-DIZgr4nD.d.mts.map +1 -0
- package/dist/{thread-documents-BlxNjc7S.d.mts → thread-documents-C3kf0Vds.d.mts} +1 -1
- package/dist/{thread-documents-BlxNjc7S.d.mts.map → thread-documents-C3kf0Vds.d.mts.map} +1 -1
- package/dist/{thread-handlers-6L8sAreR.d.mts → thread-handlers-DZ1WsorO.d.mts} +9 -4
- package/dist/thread-handlers-DZ1WsorO.d.mts.map +1 -0
- package/dist/{tools-wgLen05n.mjs → tools-GDd5_GUh.mjs} +403 -77
- package/dist/tools-GDd5_GUh.mjs.map +1 -0
- package/dist/ui.css +1 -1
- package/dist/ui.d.mts +3 -3
- package/dist/ui.d.mts.map +1 -1
- package/dist/ui.mjs +152 -36
- package/dist/ui.mjs.map +1 -1
- package/docs/integration.md +73 -5
- package/package.json +4 -4
- package/dist/authoring-references-DYuOAdVx.mjs.map +0 -1
- package/dist/capabilities-DKbNCnBf.mjs +0 -66
- package/dist/capabilities-DKbNCnBf.mjs.map +0 -1
- package/dist/capabilities-DlC1l6qy.d.mts.map +0 -1
- package/dist/constants-CgV4cTg5.d.mts.map +0 -1
- package/dist/engine-0AJrv0hI.mjs.map +0 -1
- package/dist/index-BNdvGEaq.d.mts.map +0 -1
- package/dist/index-BUF-sE8j.d.mts.map +0 -1
- package/dist/index-BdXb6jER.d.mts.map +0 -1
- package/dist/memory-thread-repository-1ocPOda8.mjs.map +0 -1
- package/dist/protocol-DpCYmCjr.mjs.map +0 -1
- package/dist/react-DXK5zWNo.mjs.map +0 -1
- package/dist/request-context-BBzU3VdG.mjs.map +0 -1
- package/dist/tasks-BuLnWrR4.d.mts.map +0 -1
- package/dist/thread-handlers-6L8sAreR.d.mts.map +0 -1
- package/dist/tools-wgLen05n.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { I as rawSurveyDocumentSchema, f as surveyChangeSetSchema, l as surveyAssistantTurnFinalSchema } from "./protocol-DATvkr1S.mjs";
|
|
2
|
+
import { a as normalizeFormAuthoringConfig, c as assertSurveyAssistantCapabilities, i as isValidSimpleFormConfig, o as transitionFormAuthoringMode, r as isCompatibleSimpleFieldUpsert, t as getFormAuthoringMode } from "./form-authoring-DCd9XfXf.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ContentType, Expression, ExpressionType, FunctionExpressionNames, JsonPatchError, ReservedSurveyItemTypes, SurveyItemTranslations, SurveyTranslations, ValueReferenceMethod, ValueType, applyJsonPatch, generateId, getContentPlainText, isContentEmpty, parseJsonPointer, structuredCloneMethod } from "@case-framework/survey-core";
|
|
5
5
|
//#region src/engine/context.ts
|
|
@@ -483,6 +483,8 @@ const choiceOptionDetailsSchema = z.object({
|
|
|
483
483
|
hasDisplayCondition: z.boolean().optional(),
|
|
484
484
|
embeddedFormItemId: z.string().optional(),
|
|
485
485
|
embeddedFormFieldGroupCount: z.number().int().nonnegative().optional(),
|
|
486
|
+
embeddedFormAuthoringMode: z.enum(["simple", "custom"]).optional(),
|
|
487
|
+
embeddedFormSimpleModeValid: z.boolean().optional(),
|
|
486
488
|
embeddedFormTranslationPrefix: z.string().optional(),
|
|
487
489
|
embeddedFormResponseSlotPrefix: z.string().optional()
|
|
488
490
|
});
|
|
@@ -535,6 +537,8 @@ const itemConfigDetailsSchema = z.object({
|
|
|
535
537
|
"generic"
|
|
536
538
|
]),
|
|
537
539
|
summary: z.array(z.string()),
|
|
540
|
+
authoringMode: z.enum(["simple", "custom"]).optional(),
|
|
541
|
+
simpleModeValid: z.boolean().optional(),
|
|
538
542
|
mainResponseSlotId: z.string().optional(),
|
|
539
543
|
choiceOptions: z.array(choiceOptionDetailsSchema).optional(),
|
|
540
544
|
formFieldGroups: z.array(formFieldGroupDetailsSchema).optional(),
|
|
@@ -677,7 +681,8 @@ const buildChoiceConfigDetails = (rawItem) => {
|
|
|
677
681
|
if (!isRecord$5(option)) return;
|
|
678
682
|
const id = asString(option.id);
|
|
679
683
|
if (!id) return;
|
|
680
|
-
const
|
|
684
|
+
const embeddedForm = isRecord$5(option.embeddedForm) ? option.embeddedForm : void 0;
|
|
685
|
+
const embeddedFormFieldGroups = asArray(embeddedForm?.fieldGroups);
|
|
681
686
|
const details = {
|
|
682
687
|
position: index + 1,
|
|
683
688
|
id,
|
|
@@ -691,6 +696,8 @@ const buildChoiceConfigDetails = (rawItem) => {
|
|
|
691
696
|
if (embeddedFormItemId) details.embeddedFormItemId = embeddedFormItemId;
|
|
692
697
|
if (embeddedFormFieldGroups.length > 0) {
|
|
693
698
|
details.embeddedFormFieldGroupCount = embeddedFormFieldGroups.length;
|
|
699
|
+
details.embeddedFormAuthoringMode = getFormAuthoringMode(embeddedForm);
|
|
700
|
+
details.embeddedFormSimpleModeValid = isValidSimpleFormConfig(embeddedForm);
|
|
694
701
|
details.embeddedFormTranslationPrefix = getChoiceEmbeddedFormTranslationPrefix(id);
|
|
695
702
|
details.embeddedFormResponseSlotPrefix = getChoiceEmbeddedFormResponseSlotPrefix(id);
|
|
696
703
|
}
|
|
@@ -754,7 +761,8 @@ const buildFormFieldDetails = (field, position, translationNamespace, responseSl
|
|
|
754
761
|
};
|
|
755
762
|
const buildFormConfigDetails = (rawItem, configOverride, translationNamespace, responseSlotPrefix) => {
|
|
756
763
|
const configCandidate = configOverride ?? rawItem.config;
|
|
757
|
-
const
|
|
764
|
+
const config = isRecord$5(configCandidate) ? configCandidate : void 0;
|
|
765
|
+
const rawGroups = asArray(config?.fieldGroups);
|
|
758
766
|
if (rawItem.itemType !== "formItem" && rawGroups.length === 0) return;
|
|
759
767
|
const formFieldGroups = rawGroups.map((group, groupIndex) => {
|
|
760
768
|
if (!isRecord$5(group)) return;
|
|
@@ -780,9 +788,13 @@ const buildFormConfigDetails = (rawItem, configOverride, translationNamespace, r
|
|
|
780
788
|
};
|
|
781
789
|
}).filter((group) => Boolean(group));
|
|
782
790
|
const fieldCount = formFieldGroups.reduce((sum, group) => sum + group.fieldCount, 0);
|
|
791
|
+
const authoringMode = getFormAuthoringMode(config);
|
|
792
|
+
const simpleModeValid = isValidSimpleFormConfig(config);
|
|
783
793
|
return {
|
|
784
794
|
kind: "form",
|
|
785
|
-
summary: [`form item config: ${formFieldGroups.length} group(s), ${fieldCount} field(s)`],
|
|
795
|
+
summary: [`form item config: authoringMode=${authoringMode}, ${formFieldGroups.length} group(s), ${fieldCount} field(s)${simpleModeValid ? "" : "; malformed simple mode should be treated as custom"}`],
|
|
796
|
+
authoringMode,
|
|
797
|
+
simpleModeValid,
|
|
786
798
|
formFieldGroups,
|
|
787
799
|
listOrdering: {
|
|
788
800
|
formFieldGroups: "Stored configured order; positions are one-based.",
|
|
@@ -1942,6 +1954,12 @@ const MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT = 1e3;
|
|
|
1942
1954
|
const CHOICE_ITEM_TYPE = "choiceItem";
|
|
1943
1955
|
const FORM_ITEM_TYPE = "formItem";
|
|
1944
1956
|
const CHOICE_OPTION_LABEL_CONTENT_KEY_PATTERN = /^options\.([^.]+)\.label$/;
|
|
1957
|
+
const MAX_VALIDATION_DIAGNOSTIC_VALUES = 12;
|
|
1958
|
+
const formatBoundedDiagnosticValues = (values, limit = MAX_VALIDATION_DIAGNOSTIC_VALUES) => {
|
|
1959
|
+
const shown = values.slice(0, limit);
|
|
1960
|
+
const suffix = shown.length < values.length ? `, … (${shown.length}/${values.length} shown in operation order)` : "";
|
|
1961
|
+
return `${shown.join(", ")}${suffix}`;
|
|
1962
|
+
};
|
|
1945
1963
|
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1946
1964
|
const getTrimmedString = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1947
1965
|
const getFirstStringProperty = (record, keys) => {
|
|
@@ -2182,7 +2200,7 @@ const validateNewRawPatchLocalizedContent = ({ operation, originalSurvey, patche
|
|
|
2182
2200
|
if (missing.length === 0) return [];
|
|
2183
2201
|
return [createIssue({
|
|
2184
2202
|
code: "missing-raw-patch-localized-content",
|
|
2185
|
-
message: `The raw patch introduces respondent-visible content without its required translation. Add non-empty Content values in the same patches array for: ${missing.map(({ itemId, contentKey }) => `/translations/<locale>/itemTranslations/${itemId}/${contentKey}`)
|
|
2203
|
+
message: `The raw patch introduces respondent-visible content without its required translation. Add non-empty Content values in the same patches array for: ${formatBoundedDiagnosticValues(missing.map(({ itemId, contentKey }) => `/translations/<locale>/itemTranslations/${itemId}/${contentKey}`))}. Content keys use config ids exactly and are case-sensitive; for embedded fields, embeddedForms.<optionId> uses the choice option's id, not its key or embeddedForm id.`,
|
|
2186
2204
|
operationIndex: 0,
|
|
2187
2205
|
operationId: getOperationId(operation),
|
|
2188
2206
|
itemId: missing[0]?.itemId
|
|
@@ -2587,6 +2605,7 @@ const isSurveyLevelOperation = (operation) => [
|
|
|
2587
2605
|
"update-survey-metadata",
|
|
2588
2606
|
"update-survey-pagination",
|
|
2589
2607
|
"update-survey-translation",
|
|
2608
|
+
"add-survey-locale",
|
|
2590
2609
|
"remove-survey-locale",
|
|
2591
2610
|
"set-image-asset",
|
|
2592
2611
|
"update-image-asset-metadata",
|
|
@@ -2605,6 +2624,7 @@ const getOperationTarget = (operation) => {
|
|
|
2605
2624
|
case "update-survey-metadata":
|
|
2606
2625
|
case "update-survey-pagination":
|
|
2607
2626
|
case "update-survey-translation":
|
|
2627
|
+
case "add-survey-locale":
|
|
2608
2628
|
case "remove-survey-locale":
|
|
2609
2629
|
case "set-image-asset":
|
|
2610
2630
|
case "update-image-asset-metadata":
|
|
@@ -2714,17 +2734,26 @@ const getFormFieldGroupsForTarget = (rawItem, optionId) => {
|
|
|
2714
2734
|
const option = getChoiceOptionsFromRawItem(rawItem).find((candidate) => candidate.id === optionId);
|
|
2715
2735
|
return option ? getFormFieldGroupsFromConfig(option.embeddedForm) : void 0;
|
|
2716
2736
|
};
|
|
2717
|
-
const
|
|
2737
|
+
const getFormConfigForTarget = (rawItem, optionId) => {
|
|
2738
|
+
if (!optionId) return rawItem.itemType === FORM_ITEM_TYPE && isRecord$2(rawItem.config) ? rawItem.config : void 0;
|
|
2739
|
+
if (rawItem.itemType !== CHOICE_ITEM_TYPE) return void 0;
|
|
2740
|
+
const option = getChoiceOptionsFromRawItem(rawItem).find((candidate) => candidate.id === optionId);
|
|
2741
|
+
return option && isRecord$2(option.embeddedForm) ? option.embeddedForm : void 0;
|
|
2742
|
+
};
|
|
2743
|
+
const withFormFieldGroupsForTarget = (rawItem, fieldGroups, optionId, preserveSimpleMode = false) => {
|
|
2718
2744
|
if (!optionId) {
|
|
2719
2745
|
if (rawItem.itemType !== FORM_ITEM_TYPE) return void 0;
|
|
2720
2746
|
const config = isRecord$2(rawItem.config) ? rawItem.config : {};
|
|
2721
2747
|
return {
|
|
2722
2748
|
...rawItem,
|
|
2723
|
-
config: {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2749
|
+
config: transitionFormAuthoringMode({
|
|
2750
|
+
config: {
|
|
2751
|
+
id: rawItem.id,
|
|
2752
|
+
...config
|
|
2753
|
+
},
|
|
2754
|
+
fieldGroups,
|
|
2755
|
+
preserveSimpleMode
|
|
2756
|
+
})
|
|
2728
2757
|
};
|
|
2729
2758
|
}
|
|
2730
2759
|
if (rawItem.itemType !== CHOICE_ITEM_TYPE) return void 0;
|
|
@@ -2735,11 +2764,14 @@ const withFormFieldGroupsForTarget = (rawItem, fieldGroups, optionId) => {
|
|
|
2735
2764
|
const embeddedForm = isRecord$2(option.embeddedForm) ? option.embeddedForm : {};
|
|
2736
2765
|
options[optionIndex] = {
|
|
2737
2766
|
...option,
|
|
2738
|
-
embeddedForm: {
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2767
|
+
embeddedForm: transitionFormAuthoringMode({
|
|
2768
|
+
config: {
|
|
2769
|
+
id: optionId,
|
|
2770
|
+
...embeddedForm
|
|
2771
|
+
},
|
|
2772
|
+
fieldGroups,
|
|
2773
|
+
preserveSimpleMode
|
|
2774
|
+
})
|
|
2743
2775
|
};
|
|
2744
2776
|
return withChoiceOptionsOnRawItem(rawItem, options);
|
|
2745
2777
|
};
|
|
@@ -2751,6 +2783,10 @@ const normalizeFormOperationTranslations = (operation, optionId) => ({
|
|
|
2751
2783
|
contentKey: normalizeFormTranslationKey(translation.contentKey.trim(), optionId)
|
|
2752
2784
|
}))
|
|
2753
2785
|
});
|
|
2786
|
+
const normalizeEmbeddedFormAuthoringMode = (operation) => ({
|
|
2787
|
+
...operation,
|
|
2788
|
+
form: normalizeFormAuthoringConfig(operation.form) ?? operation.form
|
|
2789
|
+
});
|
|
2754
2790
|
const getItemTranslationText = ({ contentKey, itemId, locale, snapshot }) => {
|
|
2755
2791
|
const content = new SurveyTranslations(snapshot.survey.translations).getItemTranslations(itemId)?.getContent(locale, contentKey);
|
|
2756
2792
|
return content ? getContentPlainText(content).trim() : void 0;
|
|
@@ -2920,7 +2956,7 @@ const getChoiceCreateContentIssue = ({ operation, operationId, operationIndex, i
|
|
|
2920
2956
|
if (missingLabelOptionIds.length === 0) return;
|
|
2921
2957
|
return createIssue({
|
|
2922
2958
|
code: "missing-choice-option-labels",
|
|
2923
|
-
message: `New choice item options need visible labels. Missing labels for option id(s): ${missingLabelOptionIds
|
|
2959
|
+
message: `New choice item options need visible labels. Missing labels for option id(s): ${formatBoundedDiagnosticValues(missingLabelOptionIds)}.`,
|
|
2924
2960
|
operationIndex,
|
|
2925
2961
|
operationId,
|
|
2926
2962
|
itemId
|
|
@@ -3001,7 +3037,7 @@ const getFormOperationContentIssue = ({ fieldGroups, itemId, operationId, operat
|
|
|
3001
3037
|
if (missingContentKeys.length === 0) return;
|
|
3002
3038
|
return createIssue({
|
|
3003
3039
|
code: "missing-form-field-content",
|
|
3004
|
-
message: "Introduced form fields and select options need visible labels. Add non-empty translations for: " + missingContentKeys
|
|
3040
|
+
message: "Introduced form fields and select options need visible labels. Add non-empty translations for: " + formatBoundedDiagnosticValues(missingContentKeys) + ".",
|
|
3005
3041
|
operationIndex,
|
|
3006
3042
|
operationId,
|
|
3007
3043
|
itemId
|
|
@@ -3068,6 +3104,7 @@ const getMissingFieldGroupsWrapperHint = (record) => {
|
|
|
3068
3104
|
const hasNonEmptyCreateTranslation = (translations, contentKey) => (translations ?? []).some((translation) => translation.contentKey === contentKey && getTranslationPlainText(translation).length > 0);
|
|
3069
3105
|
const getFormCreateFieldLabelIssue = ({ operation, operationId, operationIndex, itemId }) => {
|
|
3070
3106
|
if (operation.item.itemType !== FORM_ITEM_TYPE || !isRecord$2(operation.item.config)) return;
|
|
3107
|
+
if (operation.item.config.authoringMode === "custom" && Array.isArray(operation.item.config.fieldGroups) && operation.item.config.fieldGroups.length === 1 && isRecord$2(operation.item.config.fieldGroups[0]) && Array.isArray(operation.item.config.fieldGroups[0].fields) && operation.item.config.fieldGroups[0].fields.length === 0) return;
|
|
3071
3108
|
const wrapperHint = getMissingFieldGroupsWrapperHint(operation.item.config);
|
|
3072
3109
|
if (wrapperHint) return createIssue({
|
|
3073
3110
|
code: "missing-field-groups-wrapper",
|
|
@@ -3084,7 +3121,7 @@ const getFormCreateFieldLabelIssue = ({ operation, operationId, operationIndex,
|
|
|
3084
3121
|
});
|
|
3085
3122
|
if (missingSelectOptionLabelContentKeys.length > 0) return createIssue({
|
|
3086
3123
|
code: "missing-form-select-option-labels",
|
|
3087
|
-
message: `New form item select options need visible labels. Add translations for: ` + missingSelectOptionLabelContentKeys
|
|
3124
|
+
message: `New form item select options need visible labels. Add translations for: ` + formatBoundedDiagnosticValues(missingSelectOptionLabelContentKeys) + ".",
|
|
3088
3125
|
operationIndex,
|
|
3089
3126
|
operationId,
|
|
3090
3127
|
itemId
|
|
@@ -3092,7 +3129,7 @@ const getFormCreateFieldLabelIssue = ({ operation, operationId, operationIndex,
|
|
|
3092
3129
|
if (missingLabelFieldIds.length === 0) return;
|
|
3093
3130
|
return createIssue({
|
|
3094
3131
|
code: "missing-form-field-labels",
|
|
3095
|
-
message: `New form item fields need visible labels. Add a translation with contentKey "fields.<fieldId>.label" for field id(s): ${missingLabelFieldIds
|
|
3132
|
+
message: `New form item fields need visible labels. Add a translation with contentKey "fields.<fieldId>.label" for field id(s): ${formatBoundedDiagnosticValues(missingLabelFieldIds)}.`,
|
|
3096
3133
|
operationIndex,
|
|
3097
3134
|
operationId,
|
|
3098
3135
|
itemId
|
|
@@ -3128,7 +3165,7 @@ const getChoiceEmbeddedFormFieldLabelIssue = ({ operation, operationId, operatio
|
|
|
3128
3165
|
if (missingLabelContentKeys.length === 0) return;
|
|
3129
3166
|
return createIssue({
|
|
3130
3167
|
code: "missing-embedded-form-field-labels",
|
|
3131
|
-
message: `New embedded form fields and select options need visible labels, otherwise respondents only see raw keys. Add translations for: ${missingLabelContentKeys
|
|
3168
|
+
message: `New embedded form fields and select options need visible labels, otherwise respondents only see raw keys. Add translations for: ${formatBoundedDiagnosticValues(missingLabelContentKeys)}.`,
|
|
3132
3169
|
operationIndex,
|
|
3133
3170
|
operationId,
|
|
3134
3171
|
itemId
|
|
@@ -3330,6 +3367,7 @@ const normalizeOperation = (operation, resolvedItemId) => {
|
|
|
3330
3367
|
locale: operation.locale.trim(),
|
|
3331
3368
|
contentKey: operation.contentKey.trim()
|
|
3332
3369
|
};
|
|
3370
|
+
case "add-survey-locale":
|
|
3333
3371
|
case "remove-survey-locale": return {
|
|
3334
3372
|
...operation,
|
|
3335
3373
|
locale: operation.locale.trim()
|
|
@@ -3411,9 +3449,11 @@ const validateSurveyOperations = ({ maxOperations = 200, operations, snapshot: s
|
|
|
3411
3449
|
const createdKeysByParentId = /* @__PURE__ */ new Map();
|
|
3412
3450
|
const plannedKeys = /* @__PURE__ */ new Map();
|
|
3413
3451
|
const deletedItemIds = /* @__PURE__ */ new Set();
|
|
3452
|
+
const plannedLocales = new Set(snapshot.inspection.locales);
|
|
3414
3453
|
const introducedLocaleWarnings = /* @__PURE__ */ new Set();
|
|
3415
3454
|
const addIntroducedLocaleWarning = ({ itemId, locale, operationId, operationIndex }) => {
|
|
3416
|
-
if (
|
|
3455
|
+
if (plannedLocales.has(locale)) return;
|
|
3456
|
+
plannedLocales.add(locale);
|
|
3417
3457
|
const warningKey = `${operationIndex}:${itemId ?? ""}:${locale}`;
|
|
3418
3458
|
if (introducedLocaleWarnings.has(warningKey)) return;
|
|
3419
3459
|
introducedLocaleWarnings.add(warningKey);
|
|
@@ -3680,25 +3720,45 @@ const validateSurveyOperations = ({ maxOperations = 200, operations, snapshot: s
|
|
|
3680
3720
|
return;
|
|
3681
3721
|
}
|
|
3682
3722
|
break;
|
|
3683
|
-
case "
|
|
3684
|
-
if (
|
|
3723
|
+
case "add-survey-locale":
|
|
3724
|
+
if (normalized.locale.length === 0) {
|
|
3685
3725
|
issues.push(createIssue({
|
|
3686
|
-
code: "survey-locale
|
|
3687
|
-
message:
|
|
3726
|
+
code: "invalid-survey-locale",
|
|
3727
|
+
message: "Survey locale cannot be blank.",
|
|
3728
|
+
operationIndex,
|
|
3729
|
+
operationId
|
|
3730
|
+
}));
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (plannedLocales.has(normalized.locale)) {
|
|
3734
|
+
issues.push(createIssue({
|
|
3735
|
+
code: "survey-locale-already-exists",
|
|
3736
|
+
message: `Locale "${normalized.locale}" is already present in the survey.`,
|
|
3688
3737
|
operationIndex,
|
|
3689
3738
|
operationId
|
|
3690
3739
|
}));
|
|
3691
3740
|
return;
|
|
3692
3741
|
}
|
|
3693
|
-
|
|
3742
|
+
plannedLocales.add(normalized.locale);
|
|
3743
|
+
warnings.push(createIssue({
|
|
3744
|
+
severity: "warning",
|
|
3745
|
+
code: "empty-locale-registration",
|
|
3746
|
+
message: `This operation only registers an empty "${normalized.locale}" locale. It does not complete a request to localize or translate existing respondent-facing content; continue with real translation operations unless the user explicitly requested an empty registration.`,
|
|
3747
|
+
operationIndex,
|
|
3748
|
+
operationId
|
|
3749
|
+
}));
|
|
3750
|
+
break;
|
|
3751
|
+
case "remove-survey-locale":
|
|
3752
|
+
if (!plannedLocales.has(normalized.locale)) {
|
|
3694
3753
|
issues.push(createIssue({
|
|
3695
|
-
code: "
|
|
3696
|
-
message: "
|
|
3754
|
+
code: "survey-locale-not-found",
|
|
3755
|
+
message: `Locale "${normalized.locale}" is not present in the survey.`,
|
|
3697
3756
|
operationIndex,
|
|
3698
3757
|
operationId
|
|
3699
3758
|
}));
|
|
3700
3759
|
return;
|
|
3701
3760
|
}
|
|
3761
|
+
plannedLocales.delete(normalized.locale);
|
|
3702
3762
|
break;
|
|
3703
3763
|
case "update-image-asset-metadata":
|
|
3704
3764
|
case "remove-image-asset":
|
|
@@ -3749,7 +3809,7 @@ const validateSurveyOperations = ({ maxOperations = 200, operations, snapshot: s
|
|
|
3749
3809
|
const normalized = rawNormalized.kind === "update-item-translation" || rawNormalized.kind === "update-placed-image" ? {
|
|
3750
3810
|
...rawNormalized,
|
|
3751
3811
|
contentKey: normalizeContentKeyForItemType(rawNormalized.contentKey, itemTypeManifestByType.get(rawItem.itemType))
|
|
3752
|
-
} : rawNormalized.kind === "upsert-form-field-group" || rawNormalized.kind === "upsert-form-field" ? normalizeFormOperationTranslations(rawNormalized, rawNormalized.target.optionId) : rawNormalized.kind === "set-embedded-form" ? normalizeFormOperationTranslations(rawNormalized, rawNormalized.optionId) : rawNormalized;
|
|
3812
|
+
} : rawNormalized.kind === "upsert-form-field-group" || rawNormalized.kind === "upsert-form-field" ? normalizeFormOperationTranslations(rawNormalized, rawNormalized.target.optionId) : rawNormalized.kind === "set-embedded-form" ? normalizeEmbeddedFormAuthoringMode(normalizeFormOperationTranslations(rawNormalized, rawNormalized.optionId)) : rawNormalized;
|
|
3753
3813
|
if (!itemTypeManifestByType.get(rawItem.itemType)?.supportedOperations.includes(normalized.kind)) {
|
|
3754
3814
|
issues.push(createIssue({
|
|
3755
3815
|
code: "operation-not-supported-for-item-type",
|
|
@@ -4076,7 +4136,12 @@ const validateSurveyOperations = ({ maxOperations = 200, operations, snapshot: s
|
|
|
4076
4136
|
...existingField,
|
|
4077
4137
|
...normalized.field
|
|
4078
4138
|
});
|
|
4079
|
-
const nextItem = withFormFieldGroupsForTarget(rawItem, nextGroups, normalized.target.optionId
|
|
4139
|
+
const nextItem = withFormFieldGroupsForTarget(rawItem, nextGroups, normalized.target.optionId, isCompatibleSimpleFieldUpsert({
|
|
4140
|
+
config: getFormConfigForTarget(rawItem, normalized.target.optionId),
|
|
4141
|
+
groupId: normalized.groupId,
|
|
4142
|
+
field: normalized.field,
|
|
4143
|
+
index: normalized.index
|
|
4144
|
+
}));
|
|
4080
4145
|
if (nextItem) rawItemsById.set(item.itemId, nextItem);
|
|
4081
4146
|
for (const translation of normalized.translations ?? []) addIntroducedLocaleWarning({
|
|
4082
4147
|
locale: translation.locale,
|
|
@@ -4453,6 +4518,16 @@ const validateSurveyOperations = ({ maxOperations = 200, operations, snapshot: s
|
|
|
4453
4518
|
break;
|
|
4454
4519
|
}
|
|
4455
4520
|
const previousParentId = parentByChildId.get(item.itemId);
|
|
4521
|
+
if (previousParentId === parent.itemId) {
|
|
4522
|
+
issues.push(createIssue({
|
|
4523
|
+
code: "item-already-in-target-parent",
|
|
4524
|
+
message: "The item is already in the target parent. Omit an unchanged placement or use reorder-items with the parent's complete child order.",
|
|
4525
|
+
operationIndex,
|
|
4526
|
+
operationId,
|
|
4527
|
+
itemId: item.itemId
|
|
4528
|
+
}));
|
|
4529
|
+
break;
|
|
4530
|
+
}
|
|
4456
4531
|
if (previousParentId) {
|
|
4457
4532
|
const previousParent = rawItemsById.get(previousParentId);
|
|
4458
4533
|
if (previousParent) rawItemsById.set(previousParentId, {
|
|
@@ -4858,7 +4933,12 @@ const applyValidatedSurveyOperationsToDraft = ({ operations, survey: inputSurvey
|
|
|
4858
4933
|
survey
|
|
4859
4934
|
});
|
|
4860
4935
|
}
|
|
4861
|
-
replaceItem(withFormFieldGroupsForTarget(item, groups, operation.target.optionId
|
|
4936
|
+
replaceItem(withFormFieldGroupsForTarget(item, groups, operation.target.optionId, operation.kind === "upsert-form-field" && isCompatibleSimpleFieldUpsert({
|
|
4937
|
+
config: getFormConfigForTarget(item, operation.target.optionId),
|
|
4938
|
+
groupId: operation.groupId,
|
|
4939
|
+
field: operation.field,
|
|
4940
|
+
index: operation.index
|
|
4941
|
+
})));
|
|
4862
4942
|
break;
|
|
4863
4943
|
}
|
|
4864
4944
|
case "remove-item": {
|
|
@@ -4935,6 +5015,12 @@ const applyValidatedSurveyOperationsToDraft = ({ operations, survey: inputSurvey
|
|
|
4935
5015
|
};
|
|
4936
5016
|
break;
|
|
4937
5017
|
}
|
|
5018
|
+
case "add-survey-locale":
|
|
5019
|
+
survey.translations = {
|
|
5020
|
+
...survey.translations ?? {},
|
|
5021
|
+
[operation.locale]: {}
|
|
5022
|
+
};
|
|
5023
|
+
break;
|
|
4938
5024
|
case "remove-survey-locale": {
|
|
4939
5025
|
const translations = { ...survey.translations ?? {} };
|
|
4940
5026
|
delete translations[operation.locale];
|
|
@@ -4990,677 +5076,794 @@ const createSurveyChangeSet = ({ maxOperations, operations, snapshot, summary, t
|
|
|
4990
5076
|
};
|
|
4991
5077
|
};
|
|
4992
5078
|
//#endregion
|
|
4993
|
-
//#region src/engine/
|
|
4994
|
-
const
|
|
4995
|
-
const
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5079
|
+
//#region src/engine/audit.ts
|
|
5080
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5081
|
+
const records = (value) => Array.isArray(value) ? value.filter(isRecord$1) : [];
|
|
5082
|
+
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
5083
|
+
const QUALITY_PRINCIPLES = [
|
|
5084
|
+
{
|
|
5085
|
+
id: "purpose-alignment",
|
|
5086
|
+
title: "Purpose and construct alignment",
|
|
5087
|
+
prompt: "Does every question serve a stated objective, and does it measure the intended construct?"
|
|
5088
|
+
},
|
|
5089
|
+
{
|
|
5090
|
+
id: "cognitive-burden",
|
|
5091
|
+
title: "Respondent burden",
|
|
5092
|
+
prompt: "Is the survey as short and cognitively simple as its purpose allows?"
|
|
5093
|
+
},
|
|
5094
|
+
{
|
|
5095
|
+
id: "question-wording",
|
|
5096
|
+
title: "Clear, neutral wording",
|
|
5097
|
+
prompt: "Are questions specific, neutral, answerable, and limited to one idea without assumptions or double negatives?"
|
|
5098
|
+
},
|
|
5099
|
+
{
|
|
5100
|
+
id: "recall",
|
|
5101
|
+
title: "Recall and reference periods",
|
|
5102
|
+
prompt: "Are recall periods explicit, realistic, and consistent with what respondents can remember?"
|
|
5103
|
+
},
|
|
5104
|
+
{
|
|
5105
|
+
id: "response-options",
|
|
5106
|
+
title: "Response option quality",
|
|
5107
|
+
prompt: "Are options mutually exclusive, collectively sufficient, balanced, ordered, and consistently scaled?"
|
|
5108
|
+
},
|
|
5109
|
+
{
|
|
5110
|
+
id: "flow",
|
|
5111
|
+
title: "Flow and completeness",
|
|
5112
|
+
prompt: "Does the order move logically from easy/general to specific, with needed instructions and no important objective omitted?"
|
|
5113
|
+
},
|
|
5114
|
+
{
|
|
5115
|
+
id: "routing",
|
|
5116
|
+
title: "Requiredness and routing",
|
|
5117
|
+
prompt: "Do required fields, branching, validation, and not-applicable paths avoid forcing inaccurate answers?"
|
|
5118
|
+
},
|
|
5119
|
+
{
|
|
5120
|
+
id: "sensitivity",
|
|
5121
|
+
title: "Sensitivity and ethics",
|
|
5122
|
+
prompt: "Are sensitive questions justified, respectfully phrased, appropriately optional, and paired with privacy context where needed?"
|
|
5123
|
+
},
|
|
5124
|
+
{
|
|
5125
|
+
id: "accessibility",
|
|
5126
|
+
title: "Accessibility and inclusion",
|
|
5127
|
+
prompt: "Is language plain and inclusive, and are content, controls, and images understandable without avoidable barriers?"
|
|
5128
|
+
},
|
|
5129
|
+
{
|
|
5130
|
+
id: "localization",
|
|
5131
|
+
title: "Cross-language equivalence",
|
|
5132
|
+
prompt: "Do translations preserve meaning and cultural relevance rather than merely matching words?"
|
|
5133
|
+
},
|
|
5134
|
+
{
|
|
5135
|
+
id: "testing",
|
|
5136
|
+
title: "Testing and validation",
|
|
5137
|
+
prompt: "Has the instrument been cognitively tested, piloted, and checked for reliability or validity where the intended use requires it?"
|
|
5027
5138
|
}
|
|
5139
|
+
];
|
|
5140
|
+
const translationText = (context, locale, itemId, contentKey) => {
|
|
5141
|
+
const localeTranslations = isRecord$1(context.survey.translations?.[locale]) ? context.survey.translations?.[locale] : void 0;
|
|
5142
|
+
const itemTranslations = isRecord$1(localeTranslations?.itemTranslations) ? localeTranslations.itemTranslations : void 0;
|
|
5143
|
+
const content = (isRecord$1(itemTranslations?.[itemId]) ? itemTranslations[itemId] : void 0)?.[contentKey];
|
|
5144
|
+
return content ? getContentPlainText(content).trim() : "";
|
|
5028
5145
|
};
|
|
5029
|
-
const
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
if (this.#context) throw new Error("The survey turn draft was initialized more than once.");
|
|
5051
|
-
this.#context = context;
|
|
5052
|
-
}
|
|
5053
|
-
stage({ addressedTargets = [], operations, snapshot, summary }) {
|
|
5054
|
-
this.#assertOpen();
|
|
5055
|
-
if (!this.#context && snapshot && "document" in snapshot && "trustedCapabilities" in snapshot) this.#context = snapshot;
|
|
5056
|
-
const currentContext = this.#context ?? snapshot;
|
|
5057
|
-
const targets = new Set([...collectOperationTargets(operations), ...addressedTargets]);
|
|
5058
|
-
const delta = createSurveyChangeSet({
|
|
5059
|
-
operations,
|
|
5060
|
-
snapshot: currentContext,
|
|
5061
|
-
summary,
|
|
5062
|
-
turnId: this.turnId
|
|
5146
|
+
const getGroups = (item, optionId) => {
|
|
5147
|
+
const config = isRecord$1(item.config) ? item.config : {};
|
|
5148
|
+
if (!optionId) return records(config.fieldGroups);
|
|
5149
|
+
const option = records(config.options).find((entry) => entry.id === optionId);
|
|
5150
|
+
return records(isRecord$1(option?.embeddedForm) ? option.embeddedForm.fieldGroups : void 0);
|
|
5151
|
+
};
|
|
5152
|
+
const auditSurvey = (context, options = {}) => {
|
|
5153
|
+
const findings = [];
|
|
5154
|
+
const add = (finding) => findings.push(finding);
|
|
5155
|
+
const items = context.survey.surveyItems;
|
|
5156
|
+
const itemsById = /* @__PURE__ */ new Map();
|
|
5157
|
+
const parentById = /* @__PURE__ */ new Map();
|
|
5158
|
+
const itemTypes = {};
|
|
5159
|
+
for (const item of items) {
|
|
5160
|
+
itemTypes[item.itemType] = (itemTypes[item.itemType] ?? 0) + 1;
|
|
5161
|
+
if (itemsById.has(item.id)) add({
|
|
5162
|
+
code: "duplicate-item-id",
|
|
5163
|
+
severity: "error",
|
|
5164
|
+
category: "structure",
|
|
5165
|
+
itemId: item.id,
|
|
5166
|
+
message: `Item id "${item.id}" is duplicated.`
|
|
5063
5167
|
});
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
operations: delta.operations,
|
|
5076
|
-
survey: this.#context.survey
|
|
5168
|
+
itemsById.set(item.id, item);
|
|
5169
|
+
}
|
|
5170
|
+
for (const parent of items) {
|
|
5171
|
+
const seenChildren = /* @__PURE__ */ new Set();
|
|
5172
|
+
for (const childId of strings(isRecord$1(parent.config) ? parent.config.items : void 0)) {
|
|
5173
|
+
if (seenChildren.has(childId)) add({
|
|
5174
|
+
code: "duplicate-child-reference",
|
|
5175
|
+
severity: "error",
|
|
5176
|
+
category: "structure",
|
|
5177
|
+
itemId: parent.id,
|
|
5178
|
+
message: `Child "${childId}" is listed more than once.`
|
|
5077
5179
|
});
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5180
|
+
seenChildren.add(childId);
|
|
5181
|
+
if (!itemsById.has(childId)) add({
|
|
5182
|
+
code: "missing-child",
|
|
5183
|
+
severity: "error",
|
|
5184
|
+
category: "structure",
|
|
5185
|
+
itemId: parent.id,
|
|
5186
|
+
message: `Referenced child "${childId}" does not exist.`
|
|
5187
|
+
});
|
|
5188
|
+
else if (parentById.has(childId)) add({
|
|
5189
|
+
code: "multiple-parents",
|
|
5190
|
+
severity: "error",
|
|
5191
|
+
category: "structure",
|
|
5192
|
+
itemId: childId,
|
|
5193
|
+
message: `Item is referenced by multiple parents.`
|
|
5194
|
+
});
|
|
5195
|
+
else parentById.set(childId, parent.id);
|
|
5082
5196
|
}
|
|
5083
|
-
const registered = surveyChangeSetSchema.parse({
|
|
5084
|
-
...delta,
|
|
5085
|
-
operations: cumulativeOperations,
|
|
5086
|
-
validation: {
|
|
5087
|
-
valid: true,
|
|
5088
|
-
issues: [],
|
|
5089
|
-
warnings: delta.validation.warnings,
|
|
5090
|
-
normalizedOperations: cumulativeOperations
|
|
5091
|
-
},
|
|
5092
|
-
...this.#latestChangeSetId ? { supersedesChangeIds: [this.#latestChangeSetId] } : {}
|
|
5093
|
-
});
|
|
5094
|
-
this.#candidates.set(registered.id, registered);
|
|
5095
|
-
this.#draftOperations = registered.operations;
|
|
5096
|
-
this.#latestChangeSetId = registered.id;
|
|
5097
|
-
for (const [changeSetId, unresolvedTargets] of this.#unresolvedCandidates) if (unresolvedTargets.size === 0 || [...unresolvedTargets].some((target) => targets.has(target))) this.#unresolvedCandidates.delete(changeSetId);
|
|
5098
|
-
return registered;
|
|
5099
5197
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
protocolVersion: 1,
|
|
5122
|
-
turnId: this.turnId,
|
|
5123
|
-
status,
|
|
5124
|
-
executableChangeSetIds: canAuthorize ? [this.#latestChangeSetId] : [],
|
|
5125
|
-
...blockedChangeSetIds.length > 0 ? { blockedChangeSetIds } : {},
|
|
5126
|
-
createdAt
|
|
5127
|
-
});
|
|
5128
|
-
return this.#final;
|
|
5129
|
-
}
|
|
5130
|
-
getCandidate(changeSetId) {
|
|
5131
|
-
return this.#candidates.get(changeSetId);
|
|
5132
|
-
}
|
|
5133
|
-
get draftOperations() {
|
|
5134
|
-
return this.#draftOperations;
|
|
5135
|
-
}
|
|
5136
|
-
get context() {
|
|
5137
|
-
return this.#context;
|
|
5138
|
-
}
|
|
5139
|
-
get final() {
|
|
5140
|
-
return this.#final;
|
|
5141
|
-
}
|
|
5142
|
-
#assertOpen() {
|
|
5143
|
-
if (this.#final) throw new Error("Cannot register a survey change after the turn was finalized.");
|
|
5144
|
-
}
|
|
5145
|
-
};
|
|
5146
|
-
//#endregion
|
|
5147
|
-
//#region src/engine/task-context.ts
|
|
5148
|
-
const MAX_RELATED_ITEMS = 48;
|
|
5149
|
-
const MAX_SIBLING_ITEMS = 80;
|
|
5150
|
-
const MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM = 24;
|
|
5151
|
-
const MAX_VISIBLE_TEXT_CHARS = 600;
|
|
5152
|
-
const MAX_NAMING_EXAMPLES = 40;
|
|
5153
|
-
const MAX_TRANSLATION_EXAMPLES = 60;
|
|
5154
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5155
|
-
const getItemLabel = (item) => {
|
|
5156
|
-
if (!isRecord$1(item.metadata)) return void 0;
|
|
5157
|
-
const label = item.metadata.itemLabel;
|
|
5158
|
-
return typeof label === "string" && label.trim().length > 0 ? label.trim() : void 0;
|
|
5159
|
-
};
|
|
5160
|
-
const getCenteredNeighbors = (values, focusIndex, limit) => {
|
|
5161
|
-
if (focusIndex < 0 || limit < 1) return [];
|
|
5162
|
-
const windowSize = Math.min(values.length, limit + 1);
|
|
5163
|
-
const idealStart = focusIndex - Math.floor(limit / 2);
|
|
5164
|
-
const start = Math.min(Math.max(0, idealStart), values.length - windowSize);
|
|
5165
|
-
return values.slice(start, start + windowSize).filter((_, index) => start + index !== focusIndex);
|
|
5166
|
-
};
|
|
5167
|
-
const buildSurveyAssistantTaskContext = (snapshot, options) => {
|
|
5168
|
-
const { inspection } = createSurveyAssistantEngineContext(snapshot, void 0, { focusItemId: options.focusItemId });
|
|
5169
|
-
const itemsById = new Map(snapshot.survey.surveyItems.map((item) => [item.id, item]));
|
|
5170
|
-
const parentById = new Map(inspection.outline.flatMap((item) => item.parentId ? [[item.itemId, item.parentId]] : []));
|
|
5171
|
-
const childIdsByParent = /* @__PURE__ */ new Map();
|
|
5172
|
-
for (const item of inspection.outline) {
|
|
5173
|
-
if (!item.parentId) continue;
|
|
5174
|
-
const childIds = childIdsByParent.get(item.parentId) ?? [];
|
|
5175
|
-
childIds.push(item.itemId);
|
|
5176
|
-
childIdsByParent.set(item.parentId, childIds);
|
|
5177
|
-
}
|
|
5178
|
-
const fullKeyById = new Map(inspection.keyIndex.map((item) => [item.itemId, item.fullKey]));
|
|
5179
|
-
const summarizeItem = (item) => {
|
|
5180
|
-
const allVisibleText = [];
|
|
5181
|
-
let visibleTextValueTruncated = false;
|
|
5182
|
-
for (const [locale, translations] of Object.entries(snapshot.survey.translations ?? {})) {
|
|
5183
|
-
const itemTranslations = translations.itemTranslations?.[item.id];
|
|
5184
|
-
if (!itemTranslations) continue;
|
|
5185
|
-
for (const [contentKey, content] of Object.entries(itemTranslations)) {
|
|
5186
|
-
const text = getContentPlainText(content).trim();
|
|
5187
|
-
if (text) {
|
|
5188
|
-
if (text.length > MAX_VISIBLE_TEXT_CHARS) visibleTextValueTruncated = true;
|
|
5189
|
-
allVisibleText.push({
|
|
5190
|
-
locale,
|
|
5191
|
-
contentKey,
|
|
5192
|
-
text: text.length > MAX_VISIBLE_TEXT_CHARS ? `${text.slice(0, MAX_VISIBLE_TEXT_CHARS)}...` : text
|
|
5193
|
-
});
|
|
5194
|
-
}
|
|
5195
|
-
}
|
|
5196
|
-
}
|
|
5197
|
-
return {
|
|
5198
|
-
id: item.id,
|
|
5199
|
-
key: item.key,
|
|
5200
|
-
fullKey: fullKeyById.get(item.id) ?? item.key,
|
|
5201
|
-
itemType: item.itemType,
|
|
5202
|
-
itemLabel: getItemLabel(item),
|
|
5203
|
-
visibleText: allVisibleText.slice(0, MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM),
|
|
5204
|
-
visibleTextTruncated: allVisibleText.length > MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM || visibleTextValueTruncated
|
|
5205
|
-
};
|
|
5206
|
-
};
|
|
5207
|
-
const rootItemId = inspection.outline.find((item) => item.depth === 0)?.itemId;
|
|
5208
|
-
const rootItem = rootItemId ? itemsById.get(rootItemId) : void 0;
|
|
5209
|
-
const result = {
|
|
5210
|
-
document: {
|
|
5211
|
-
revision: snapshot.version.revision,
|
|
5212
|
-
contentHash: snapshot.version.contentHash,
|
|
5213
|
-
capabilityDigest: snapshot.version.capabilityDigest
|
|
5214
|
-
},
|
|
5215
|
-
survey: {
|
|
5216
|
-
key: rootItem?.key,
|
|
5217
|
-
locales: inspection.locales,
|
|
5218
|
-
itemCount: inspection.itemCount,
|
|
5219
|
-
selectedLocale: snapshot.ui.selectedLocale
|
|
5220
|
-
}
|
|
5221
|
-
};
|
|
5222
|
-
const focusItem = options.focusItemId ? itemsById.get(options.focusItemId) : void 0;
|
|
5223
|
-
if (focusItem) {
|
|
5224
|
-
const parentId = parentById.get(focusItem.id);
|
|
5225
|
-
const parentItem = parentId ? itemsById.get(parentId) : void 0;
|
|
5226
|
-
const ancestorItems = [];
|
|
5227
|
-
const seenAncestorIds = /* @__PURE__ */ new Set();
|
|
5228
|
-
let ancestorId = parentId;
|
|
5229
|
-
while (ancestorId && !seenAncestorIds.has(ancestorId)) {
|
|
5230
|
-
seenAncestorIds.add(ancestorId);
|
|
5231
|
-
const ancestor = itemsById.get(ancestorId);
|
|
5232
|
-
if (!ancestor) break;
|
|
5233
|
-
ancestorItems.unshift(ancestor);
|
|
5234
|
-
ancestorId = parentById.get(ancestorId);
|
|
5235
|
-
}
|
|
5236
|
-
const siblingIds = parentItem ? childIdsByParent.get(parentItem.id) ?? [] : [];
|
|
5237
|
-
const siblingItemCount = Math.max(0, siblingIds.length - 1);
|
|
5238
|
-
const siblingItems = getCenteredNeighbors(siblingIds, siblingIds.indexOf(focusItem.id), MAX_SIBLING_ITEMS).map((itemId) => itemsById.get(itemId)).filter((item) => Boolean(item));
|
|
5239
|
-
const allRelatedItems = [];
|
|
5240
|
-
const seenRelatedIds = /* @__PURE__ */ new Set();
|
|
5241
|
-
const pending = [...childIdsByParent.get(focusItem.id) ?? []];
|
|
5242
|
-
while (pending.length > 0) {
|
|
5243
|
-
const itemId = pending.shift();
|
|
5244
|
-
const item = itemId ? itemsById.get(itemId) : void 0;
|
|
5245
|
-
if (!item || seenRelatedIds.has(item.id)) continue;
|
|
5246
|
-
seenRelatedIds.add(item.id);
|
|
5247
|
-
allRelatedItems.push(item);
|
|
5248
|
-
pending.push(...childIdsByParent.get(item.id) ?? []);
|
|
5198
|
+
const roots = items.filter((item) => !parentById.has(item.id));
|
|
5199
|
+
if (roots.length !== 1) add({
|
|
5200
|
+
code: "root-count",
|
|
5201
|
+
severity: "error",
|
|
5202
|
+
category: "structure",
|
|
5203
|
+
message: `Expected one survey root but found ${roots.length}.`
|
|
5204
|
+
});
|
|
5205
|
+
for (const parent of items) {
|
|
5206
|
+
const siblingKeys = /* @__PURE__ */ new Map();
|
|
5207
|
+
for (const childId of strings(isRecord$1(parent.config) ? parent.config.items : void 0)) {
|
|
5208
|
+
const child = itemsById.get(childId);
|
|
5209
|
+
if (!child) continue;
|
|
5210
|
+
const existing = siblingKeys.get(child.key);
|
|
5211
|
+
if (existing) add({
|
|
5212
|
+
code: "duplicate-sibling-key",
|
|
5213
|
+
severity: "error",
|
|
5214
|
+
category: "structure",
|
|
5215
|
+
itemId: child.id,
|
|
5216
|
+
message: `Key "${child.key}" duplicates sibling "${existing}".`
|
|
5217
|
+
});
|
|
5218
|
+
siblingKeys.set(child.key, child.id);
|
|
5249
5219
|
}
|
|
5250
|
-
const relatedItems = allRelatedItems.slice(0, MAX_RELATED_ITEMS);
|
|
5251
|
-
result.focus = {
|
|
5252
|
-
item: summarizeItem(focusItem),
|
|
5253
|
-
parent: parentItem ? summarizeItem(parentItem) : void 0,
|
|
5254
|
-
ancestorPath: ancestorItems.map(summarizeItem),
|
|
5255
|
-
siblingKeys: siblingItems.map((item) => item.key),
|
|
5256
|
-
siblingItems: siblingItems.map(summarizeItem),
|
|
5257
|
-
siblingItemCount,
|
|
5258
|
-
siblingItemsTruncated: siblingItemCount > siblingItems.length,
|
|
5259
|
-
relatedItems: relatedItems.map(summarizeItem),
|
|
5260
|
-
relatedItemCount: allRelatedItems.length,
|
|
5261
|
-
relatedItemsTruncated: allRelatedItems.length > relatedItems.length
|
|
5262
|
-
};
|
|
5263
5220
|
}
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
const
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
const rank = {
|
|
5276
|
-
sibling: 0,
|
|
5277
|
-
"same-type": 1,
|
|
5278
|
-
survey: 2
|
|
5279
|
-
};
|
|
5280
|
-
return rank[left.relationship] - rank[right.relationship] || left.siblingDistance - right.siblingDistance || left.index - right.index;
|
|
5281
|
-
});
|
|
5282
|
-
const allKeyExamples = rankedItems.map(({ item, relationship }) => ({
|
|
5283
|
-
itemId: item.id,
|
|
5284
|
-
itemType: item.itemType,
|
|
5285
|
-
key: item.key,
|
|
5286
|
-
relationship
|
|
5287
|
-
}));
|
|
5288
|
-
const allLabelExamples = rankedItems.flatMap(({ item, relationship }) => {
|
|
5289
|
-
const label = getItemLabel(item);
|
|
5290
|
-
return label ? [{
|
|
5221
|
+
const locales = context.inspection.locales;
|
|
5222
|
+
const sourceLocale = options.sourceLocale ?? context.ui.selectedLocale ?? locales[0];
|
|
5223
|
+
const targetLocales = options.targetLocales ?? locales.filter((locale) => locale !== sourceLocale);
|
|
5224
|
+
const manifests = new Map(context.capabilities.itemTypes.map((entry) => [entry.itemType, entry]));
|
|
5225
|
+
for (const item of items) {
|
|
5226
|
+
const manifest = manifests.get(item.itemType);
|
|
5227
|
+
if (!manifest) {
|
|
5228
|
+
add({
|
|
5229
|
+
code: "unsupported-item-type",
|
|
5230
|
+
severity: "warning",
|
|
5231
|
+
category: "structure",
|
|
5291
5232
|
itemId: item.id,
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5233
|
+
message: `Item type "${item.itemType}" is not advertised by the active editor.`
|
|
5234
|
+
});
|
|
5235
|
+
continue;
|
|
5236
|
+
}
|
|
5237
|
+
for (const surface of manifest.contentSurfaces.filter((entry) => entry.requiredOnCreate && !entry.pattern)) if (!translationText(context, sourceLocale, item.id, surface.key)) add({
|
|
5238
|
+
code: "missing-primary-content",
|
|
5239
|
+
severity: "warning",
|
|
5240
|
+
category: "content",
|
|
5241
|
+
itemId: item.id,
|
|
5242
|
+
locale: sourceLocale,
|
|
5243
|
+
contentKey: surface.key,
|
|
5244
|
+
message: `Required respondent content "${surface.key}" is empty in ${sourceLocale}.`
|
|
5297
5245
|
});
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
const
|
|
5312
|
-
|
|
5313
|
-
const
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
const sourceText = values[sourceLocale];
|
|
5327
|
-
if (!sourceText || Object.keys(values).length < 2) continue;
|
|
5328
|
-
const normalizedSource = sourceText.toLocaleLowerCase();
|
|
5329
|
-
const score = [...sourceTokens].reduce((total, token) => total + (normalizedSource.includes(token) ? 1 : 0), 0);
|
|
5330
|
-
allExamples.push({
|
|
5246
|
+
const sourceContent = isRecord$1(context.survey.translations?.[sourceLocale]?.itemTranslations?.[item.id]) ? context.survey.translations?.[sourceLocale]?.itemTranslations?.[item.id] : {};
|
|
5247
|
+
for (const [contentKey, content] of Object.entries(sourceContent ?? {})) {
|
|
5248
|
+
if (!getContentPlainText(content).trim()) continue;
|
|
5249
|
+
for (const locale of targetLocales) if (!translationText(context, locale, item.id, contentKey)) add({
|
|
5250
|
+
code: "missing-translation",
|
|
5251
|
+
severity: "warning",
|
|
5252
|
+
category: "localization",
|
|
5253
|
+
itemId: item.id,
|
|
5254
|
+
locale,
|
|
5255
|
+
contentKey,
|
|
5256
|
+
message: `Content "${contentKey}" has source text but no ${locale} translation.`
|
|
5257
|
+
});
|
|
5258
|
+
}
|
|
5259
|
+
const config = isRecord$1(item.config) ? item.config : {};
|
|
5260
|
+
if (item.itemType === "choiceItem") {
|
|
5261
|
+
const optionsList = records(config.options);
|
|
5262
|
+
if (optionsList.length === 0) add({
|
|
5263
|
+
code: "choice-without-options",
|
|
5264
|
+
severity: "error",
|
|
5265
|
+
category: "content",
|
|
5266
|
+
itemId: item.id,
|
|
5267
|
+
message: "Choice item has no options."
|
|
5268
|
+
});
|
|
5269
|
+
for (const option of optionsList) {
|
|
5270
|
+
if (typeof option.id === "string" && !translationText(context, sourceLocale, item.id, `options.${option.id}.label`)) add({
|
|
5271
|
+
code: "missing-option-label",
|
|
5272
|
+
severity: "warning",
|
|
5273
|
+
category: "content",
|
|
5331
5274
|
itemId: item.id,
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
order: itemIndex
|
|
5275
|
+
locale: sourceLocale,
|
|
5276
|
+
contentKey: `options.${option.id}.label`,
|
|
5277
|
+
message: `Choice option "${option.id}" has no visible label.`
|
|
5336
5278
|
});
|
|
5279
|
+
if (typeof option.id === "string" && option.embeddedForm) {
|
|
5280
|
+
for (const group of getGroups(item, option.id)) for (const field of records(group.fields)) if (typeof field.id === "string" && !translationText(context, sourceLocale, item.id, `embeddedForms.${option.id}.fields.${field.id}.label`)) add({
|
|
5281
|
+
code: "missing-field-label",
|
|
5282
|
+
severity: "warning",
|
|
5283
|
+
category: "content",
|
|
5284
|
+
itemId: item.id,
|
|
5285
|
+
locale: sourceLocale,
|
|
5286
|
+
contentKey: `embeddedForms.${option.id}.fields.${field.id}.label`,
|
|
5287
|
+
message: `Embedded field "${field.id}" has no visible label.`
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5337
5290
|
}
|
|
5291
|
+
}
|
|
5292
|
+
if (item.itemType === "formItem") {
|
|
5293
|
+
const groups = getGroups(item);
|
|
5294
|
+
if (groups.length === 0) add({
|
|
5295
|
+
code: "form-without-groups",
|
|
5296
|
+
severity: "error",
|
|
5297
|
+
category: "content",
|
|
5298
|
+
itemId: item.id,
|
|
5299
|
+
message: "Form item has no field groups."
|
|
5300
|
+
});
|
|
5301
|
+
for (const group of groups) for (const field of records(group.fields)) if (typeof field.id === "string" && !translationText(context, sourceLocale, item.id, `fields.${field.id}.label`)) add({
|
|
5302
|
+
code: "missing-field-label",
|
|
5303
|
+
severity: "warning",
|
|
5304
|
+
category: "content",
|
|
5305
|
+
itemId: item.id,
|
|
5306
|
+
locale: sourceLocale,
|
|
5307
|
+
contentKey: `fields.${field.id}.label`,
|
|
5308
|
+
message: `Form field "${field.id}" has no visible label.`
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
const assets = context.survey.assets ?? {};
|
|
5313
|
+
for (const [assetId, asset] of Object.entries(assets)) {
|
|
5314
|
+
const metadata = isRecord$1(asset.metadata) ? asset.metadata : {};
|
|
5315
|
+
if (asset.kind === "image" && typeof metadata.alt !== "string") add({
|
|
5316
|
+
code: "missing-image-alt",
|
|
5317
|
+
severity: "warning",
|
|
5318
|
+
category: "assets",
|
|
5319
|
+
message: `Image asset "${assetId}" has no alternative text.`,
|
|
5320
|
+
suggestion: "Add concise alt text, or an empty alt value when the image is purely decorative."
|
|
5338
5321
|
});
|
|
5339
|
-
allExamples.sort((left, right) => right.score - left.score || left.order - right.order);
|
|
5340
|
-
result.translation = {
|
|
5341
|
-
examples: allExamples.slice(0, MAX_TRANSLATION_EXAMPLES).map(({ itemId, contentKey, values }) => ({
|
|
5342
|
-
itemId,
|
|
5343
|
-
contentKey,
|
|
5344
|
-
values
|
|
5345
|
-
})),
|
|
5346
|
-
exampleCount: allExamples.length,
|
|
5347
|
-
examplesTruncated: allExamples.length > MAX_TRANSLATION_EXAMPLES
|
|
5348
|
-
};
|
|
5349
5322
|
}
|
|
5350
|
-
|
|
5323
|
+
const limited = findings.slice(0, options.maxFindings ?? 200);
|
|
5324
|
+
return {
|
|
5325
|
+
summary: findings.reduce((acc, entry) => {
|
|
5326
|
+
acc[entry.severity] += 1;
|
|
5327
|
+
acc.total += 1;
|
|
5328
|
+
return acc;
|
|
5329
|
+
}, {
|
|
5330
|
+
error: 0,
|
|
5331
|
+
warning: 0,
|
|
5332
|
+
info: 0,
|
|
5333
|
+
total: 0
|
|
5334
|
+
}),
|
|
5335
|
+
findingCount: findings.length,
|
|
5336
|
+
truncated: limited.length < findings.length,
|
|
5337
|
+
metrics: {
|
|
5338
|
+
itemCount: context.inspection.itemCount,
|
|
5339
|
+
localeCount: locales.length,
|
|
5340
|
+
responseSlotCount: context.inspection.responseSlots.length,
|
|
5341
|
+
assetCount: Object.keys(assets).length,
|
|
5342
|
+
itemTypes
|
|
5343
|
+
},
|
|
5344
|
+
findings: limited,
|
|
5345
|
+
qualityReviewPrinciples: QUALITY_PRINCIPLES,
|
|
5346
|
+
note: "Technical findings are deterministic. Quality principles are a structured review rubric; apply them with human/model judgment and domain context, and do not present them as psychometric validation."
|
|
5347
|
+
};
|
|
5351
5348
|
};
|
|
5352
5349
|
//#endregion
|
|
5353
|
-
//#region src/engine/
|
|
5354
|
-
const
|
|
5355
|
-
const
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5350
|
+
//#region src/engine/turn-survey-draft.ts
|
|
5351
|
+
const createId = () => globalThis.crypto.randomUUID();
|
|
5352
|
+
const operationTargets = (operation) => {
|
|
5353
|
+
switch (operation.kind) {
|
|
5354
|
+
case "create-item": return [`item:${operation.item.id}`];
|
|
5355
|
+
case "update-item-key":
|
|
5356
|
+
case "update-item-label":
|
|
5357
|
+
case "update-item-color":
|
|
5358
|
+
case "update-item-translation":
|
|
5359
|
+
case "update-placed-image":
|
|
5360
|
+
case "patch-item-config":
|
|
5361
|
+
case "add-choice-option":
|
|
5362
|
+
case "update-choice-option-label":
|
|
5363
|
+
case "remove-choice-option":
|
|
5364
|
+
case "reorder-choice-options":
|
|
5365
|
+
case "set-embedded-form":
|
|
5366
|
+
case "remove-embedded-form":
|
|
5367
|
+
case "remove-item":
|
|
5368
|
+
case "move-item": return [`item:${operation.itemId}`];
|
|
5369
|
+
case "upsert-form-field-group":
|
|
5370
|
+
case "remove-form-field-group":
|
|
5371
|
+
case "reorder-form-field-groups":
|
|
5372
|
+
case "upsert-form-field":
|
|
5373
|
+
case "remove-form-field":
|
|
5374
|
+
case "move-form-field": return [`item:${operation.target.itemId}`];
|
|
5375
|
+
case "reorder-items": return [`item:${operation.parentItemId}`];
|
|
5376
|
+
case "set-image-asset":
|
|
5377
|
+
case "update-image-asset-metadata":
|
|
5378
|
+
case "remove-image-asset": return [`asset:${operation.assetId}`];
|
|
5379
|
+
case "update-survey-metadata":
|
|
5380
|
+
case "update-survey-pagination":
|
|
5381
|
+
case "update-survey-translation":
|
|
5382
|
+
case "add-survey-locale":
|
|
5383
|
+
case "remove-survey-locale": return ["survey"];
|
|
5384
|
+
case "survey-json-patch": return operation.patches.map((patch) => `path:${patch.path}`);
|
|
5366
5385
|
}
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5386
|
+
};
|
|
5387
|
+
const collectOperationTargets = (operations) => new Set(operations.flatMap(operationTargets));
|
|
5388
|
+
const MAX_POST_CHANGE_TARGETS = 16;
|
|
5389
|
+
const MAX_POST_CHANGE_ITEMS = 6;
|
|
5390
|
+
const MAX_POST_CHANGE_TRANSLATIONS = 12;
|
|
5391
|
+
const MAX_POST_CHANGE_RESPONSE_SLOTS = 12;
|
|
5392
|
+
const MAX_POST_CHANGE_TEXT_CHARS = 100;
|
|
5393
|
+
const MAX_VERIFICATION_AUDIT_FINDINGS = 8;
|
|
5394
|
+
const MAX_VERIFICATION_MESSAGE_CHARS = 240;
|
|
5395
|
+
const SUBSTANTIAL_CHANGE_ITEM_COUNT = 6;
|
|
5396
|
+
const SUBSTANTIAL_CHANGE_OPERATION_COUNT = 12;
|
|
5397
|
+
const createPostChangeProjection = (context, targets) => {
|
|
5398
|
+
const allTargets = [...targets];
|
|
5399
|
+
const requestedItemIds = allTargets.filter((target) => target.startsWith("item:")).map((target) => target.slice(5));
|
|
5400
|
+
const itemEntries = requestedItemIds.map((itemId) => context.inspection.keyIndex.find((entry) => entry.itemId === itemId)).filter((entry) => entry !== void 0);
|
|
5401
|
+
const surveyTranslations = new SurveyTranslations(context.survey.translations);
|
|
5402
|
+
let truncated = allTargets.length > MAX_POST_CHANGE_TARGETS || itemEntries.length > MAX_POST_CHANGE_ITEMS;
|
|
5403
|
+
const touchedItems = itemEntries.slice(0, MAX_POST_CHANGE_ITEMS).map((entry) => {
|
|
5404
|
+
const itemTranslations = surveyTranslations.getItemTranslations(entry.itemId);
|
|
5405
|
+
const allTranslations = (itemTranslations?.locales ?? []).flatMap((locale) => Object.entries(itemTranslations?.getAllForLocale(locale) ?? {}).map(([contentKey, content]) => {
|
|
5406
|
+
const textCharacters = [...getContentPlainText(content)];
|
|
5407
|
+
return {
|
|
5408
|
+
locale,
|
|
5409
|
+
contentKey,
|
|
5410
|
+
textPreview: textCharacters.slice(0, MAX_POST_CHANGE_TEXT_CHARS).join(""),
|
|
5411
|
+
textLength: textCharacters.length,
|
|
5412
|
+
truncated: textCharacters.length > MAX_POST_CHANGE_TEXT_CHARS
|
|
5413
|
+
};
|
|
5414
|
+
}));
|
|
5415
|
+
const translations = allTranslations.slice(0, MAX_POST_CHANGE_TRANSLATIONS);
|
|
5416
|
+
const allResponseSlots = Object.keys(context.responseSlotTypes).filter((slot) => slot.startsWith(`${entry.itemId}...`));
|
|
5417
|
+
const responseSlots = allResponseSlots.slice(0, MAX_POST_CHANGE_RESPONSE_SLOTS);
|
|
5418
|
+
if (allTranslations.length > translations.length || allResponseSlots.length > responseSlots.length || translations.some((translation) => translation.truncated)) truncated = true;
|
|
5419
|
+
return {
|
|
5420
|
+
itemId: entry.itemId,
|
|
5421
|
+
itemType: entry.itemType,
|
|
5422
|
+
key: entry.key,
|
|
5423
|
+
fullKey: entry.fullKey,
|
|
5424
|
+
...entry.itemLabel ? { itemLabel: entry.itemLabel } : {},
|
|
5425
|
+
translationCount: allTranslations.length,
|
|
5426
|
+
translations,
|
|
5427
|
+
responseSlotCount: allResponseSlots.length,
|
|
5428
|
+
responseSlots
|
|
5429
|
+
};
|
|
5430
|
+
});
|
|
5431
|
+
return {
|
|
5432
|
+
version: context.version,
|
|
5433
|
+
itemCount: context.inspection.itemCount,
|
|
5434
|
+
localeCount: context.inspection.locales.length,
|
|
5435
|
+
locales: context.inspection.locales.slice(0, 32),
|
|
5436
|
+
targetCount: allTargets.length,
|
|
5437
|
+
touchedTargets: allTargets.slice(0, MAX_POST_CHANGE_TARGETS),
|
|
5438
|
+
touchedItemCount: requestedItemIds.length,
|
|
5439
|
+
availableTouchedItemCount: itemEntries.length,
|
|
5440
|
+
touchedItems,
|
|
5441
|
+
truncated: truncated || context.inspection.locales.length > 32
|
|
5442
|
+
};
|
|
5443
|
+
};
|
|
5444
|
+
const auditFindingKey = (finding) => [
|
|
5445
|
+
finding.code,
|
|
5446
|
+
finding.category,
|
|
5447
|
+
finding.itemId ?? "",
|
|
5448
|
+
finding.locale ?? "",
|
|
5449
|
+
finding.contentKey ?? ""
|
|
5450
|
+
].join("\0");
|
|
5451
|
+
const isComplexOperation = (operation) => {
|
|
5452
|
+
if (operation.kind === "survey-json-patch" || operation.kind === "patch-item-config") return true;
|
|
5453
|
+
if (operation.kind !== "create-item") return false;
|
|
5454
|
+
return Boolean(operation.item.displayConditions || operation.item.disabledConditions || operation.item.validations || operation.item.prefills);
|
|
5455
|
+
};
|
|
5456
|
+
const createChangeVerification = ({ after, before, operations, postChange }) => {
|
|
5457
|
+
const beforeFindingKeys = new Set(auditSurvey(before, { maxFindings: 1e4 }).findings.map(auditFindingKey));
|
|
5458
|
+
const newAuditFindings = auditSurvey(after, { maxFindings: 1e4 }).findings.filter((finding) => !beforeFindingKeys.has(auditFindingKey(finding)));
|
|
5459
|
+
const reasons = [];
|
|
5460
|
+
if (operations.length > SUBSTANTIAL_CHANGE_OPERATION_COUNT || postChange.touchedItemCount > SUBSTANTIAL_CHANGE_ITEM_COUNT) reasons.push("substantial-change");
|
|
5461
|
+
if (operations.some(isComplexOperation)) reasons.push("complex-change");
|
|
5462
|
+
if (postChange.truncated) reasons.push("bounded-preview");
|
|
5463
|
+
if (newAuditFindings.length > 0) reasons.push("new-audit-findings");
|
|
5464
|
+
if (reasons.length === 0) return void 0;
|
|
5465
|
+
let truncated = newAuditFindings.length > MAX_VERIFICATION_AUDIT_FINDINGS;
|
|
5466
|
+
const auditFindings = newAuditFindings.slice(0, MAX_VERIFICATION_AUDIT_FINDINGS).map((finding) => {
|
|
5467
|
+
const messageCharacters = [...finding.message];
|
|
5468
|
+
const messagePreview = messageCharacters.slice(0, MAX_VERIFICATION_MESSAGE_CHARS).join("");
|
|
5469
|
+
const messageTruncated = messageCharacters.length > MAX_VERIFICATION_MESSAGE_CHARS;
|
|
5470
|
+
if (messageTruncated) truncated = true;
|
|
5471
|
+
return {
|
|
5472
|
+
code: finding.code,
|
|
5473
|
+
severity: finding.severity,
|
|
5474
|
+
category: finding.category,
|
|
5475
|
+
...finding.itemId ? { itemId: finding.itemId } : {},
|
|
5476
|
+
...finding.locale ? { locale: finding.locale } : {},
|
|
5477
|
+
...finding.contentKey ? { contentKey: finding.contentKey } : {},
|
|
5478
|
+
messagePreview,
|
|
5479
|
+
messageLength: messageCharacters.length,
|
|
5480
|
+
truncated: messageTruncated
|
|
5481
|
+
};
|
|
5482
|
+
});
|
|
5483
|
+
return {
|
|
5484
|
+
required: true,
|
|
5485
|
+
reasons,
|
|
5486
|
+
newAuditFindingCount: newAuditFindings.length,
|
|
5487
|
+
auditFindings,
|
|
5488
|
+
truncated
|
|
5489
|
+
};
|
|
5490
|
+
};
|
|
5491
|
+
/**
|
|
5492
|
+
* Request-scoped transactional draft for one assistant response.
|
|
5493
|
+
*
|
|
5494
|
+
* Mutation tools submit deltas. A successful delta replaces the previous
|
|
5495
|
+
* candidate with a newly validated, cumulative change set. A rejected delta
|
|
5496
|
+
* leaves the accepted draft intact and blocks authorization until a later
|
|
5497
|
+
* delta validates. Only the latest cumulative candidate can be authorized.
|
|
5498
|
+
*/
|
|
5499
|
+
var TurnSurveyDraft = class {
|
|
5500
|
+
turnId;
|
|
5501
|
+
#candidates = /* @__PURE__ */ new Map();
|
|
5502
|
+
#draftOperations = [];
|
|
5503
|
+
#context;
|
|
5504
|
+
#latestChangeSetId;
|
|
5505
|
+
#unresolvedCandidates = /* @__PURE__ */ new Map();
|
|
5506
|
+
#final = null;
|
|
5507
|
+
constructor(turnId = createId()) {
|
|
5508
|
+
this.turnId = turnId;
|
|
5509
|
+
}
|
|
5510
|
+
initialize(context) {
|
|
5511
|
+
if (this.#context) throw new Error("The survey turn draft was initialized more than once.");
|
|
5512
|
+
this.#context = context;
|
|
5513
|
+
}
|
|
5514
|
+
stage({ addressedTargets = [], operations, snapshot, summary }) {
|
|
5515
|
+
this.#assertOpen();
|
|
5516
|
+
if (!this.#context && snapshot && "document" in snapshot && "trustedCapabilities" in snapshot) this.#context = snapshot;
|
|
5517
|
+
const currentContext = this.#context ?? snapshot;
|
|
5518
|
+
const targets = new Set([...collectOperationTargets(operations), ...addressedTargets]);
|
|
5519
|
+
const delta = createSurveyChangeSet({
|
|
5520
|
+
operations,
|
|
5521
|
+
snapshot: currentContext,
|
|
5522
|
+
summary,
|
|
5523
|
+
turnId: this.turnId
|
|
5524
|
+
});
|
|
5525
|
+
if (delta.status !== "validated" || !delta.validation.valid) return this.reject(delta, targets);
|
|
5526
|
+
const cumulativeOperations = [...this.#draftOperations, ...delta.operations];
|
|
5527
|
+
if (cumulativeOperations.length > 1e3) return this.reject(createSurveyChangeSet({
|
|
5528
|
+
maxOperations: MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT,
|
|
5529
|
+
operations: cumulativeOperations,
|
|
5530
|
+
snapshot: currentContext,
|
|
5531
|
+
summary,
|
|
5532
|
+
turnId: this.turnId
|
|
5533
|
+
}), targets);
|
|
5534
|
+
const beforeContext = this.#context;
|
|
5535
|
+
if (this.#context) {
|
|
5536
|
+
const survey = applyValidatedSurveyOperationsToDraft({
|
|
5537
|
+
operations: delta.operations,
|
|
5538
|
+
survey: this.#context.survey
|
|
5539
|
+
});
|
|
5540
|
+
this.#context = createSurveyAssistantEngineContext({
|
|
5541
|
+
...this.#context.document,
|
|
5542
|
+
survey
|
|
5543
|
+
}, this.#context.trustedCapabilities);
|
|
5373
5544
|
}
|
|
5374
|
-
this
|
|
5375
|
-
this
|
|
5376
|
-
|
|
5545
|
+
const postChange = this.#context ? createPostChangeProjection(this.#context, targets) : void 0;
|
|
5546
|
+
const verification = beforeContext && this.#context && postChange ? createChangeVerification({
|
|
5547
|
+
after: this.#context,
|
|
5548
|
+
before: beforeContext,
|
|
5549
|
+
operations: delta.operations,
|
|
5550
|
+
postChange
|
|
5551
|
+
}) : void 0;
|
|
5552
|
+
const registered = surveyChangeSetSchema.parse({
|
|
5553
|
+
...delta,
|
|
5554
|
+
operations: cumulativeOperations,
|
|
5555
|
+
...postChange ? { postChange } : {},
|
|
5556
|
+
...verification ? { verification } : {},
|
|
5557
|
+
validation: {
|
|
5558
|
+
valid: true,
|
|
5559
|
+
issues: [],
|
|
5560
|
+
warnings: delta.validation.warnings,
|
|
5561
|
+
normalizedOperations: cumulativeOperations
|
|
5562
|
+
},
|
|
5563
|
+
...this.#latestChangeSetId ? { supersedesChangeIds: [this.#latestChangeSetId] } : {}
|
|
5564
|
+
});
|
|
5565
|
+
this.#candidates.set(registered.id, registered);
|
|
5566
|
+
this.#draftOperations = registered.operations;
|
|
5567
|
+
this.#latestChangeSetId = registered.id;
|
|
5568
|
+
for (const [changeSetId, unresolvedTargets] of this.#unresolvedCandidates) if (unresolvedTargets.size === 0 || [...unresolvedTargets].some((target) => targets.has(target))) this.#unresolvedCandidates.delete(changeSetId);
|
|
5569
|
+
return registered;
|
|
5570
|
+
}
|
|
5571
|
+
reject(candidate, addressedTargets = []) {
|
|
5572
|
+
this.#assertOpen();
|
|
5573
|
+
if (this.#candidates.has(candidate.id)) throw new Error(`Survey change "${candidate.id}" was registered more than once.`);
|
|
5574
|
+
const registered = surveyChangeSetSchema.parse({
|
|
5575
|
+
...candidate,
|
|
5576
|
+
turnId: this.turnId,
|
|
5577
|
+
status: "rejected",
|
|
5578
|
+
validation: {
|
|
5579
|
+
...candidate.validation,
|
|
5580
|
+
valid: false
|
|
5581
|
+
}
|
|
5582
|
+
});
|
|
5583
|
+
this.#candidates.set(registered.id, registered);
|
|
5584
|
+
this.#unresolvedCandidates.set(registered.id, new Set(addressedTargets));
|
|
5585
|
+
return registered;
|
|
5377
5586
|
}
|
|
5378
|
-
|
|
5379
|
-
this
|
|
5380
|
-
this.
|
|
5381
|
-
|
|
5382
|
-
|
|
5587
|
+
finalize(status, createdAt = (/* @__PURE__ */ new Date()).toISOString(), details = {}) {
|
|
5588
|
+
if (this.#final) return this.#final;
|
|
5589
|
+
const blockedChangeSetIds = [...this.#unresolvedCandidates.keys()];
|
|
5590
|
+
const canAuthorize = status === "completed" && blockedChangeSetIds.length === 0 && this.#latestChangeSetId !== void 0;
|
|
5591
|
+
this.#final = surveyAssistantTurnFinalSchema.parse({
|
|
5592
|
+
protocolVersion: 1,
|
|
5593
|
+
turnId: this.turnId,
|
|
5594
|
+
status,
|
|
5595
|
+
executableChangeSetIds: canAuthorize ? [this.#latestChangeSetId] : [],
|
|
5596
|
+
...blockedChangeSetIds.length > 0 ? { blockedChangeSetIds } : {},
|
|
5597
|
+
...details,
|
|
5598
|
+
createdAt
|
|
5383
5599
|
});
|
|
5384
|
-
|
|
5385
|
-
const oldestKey = this.entries.keys().next().value;
|
|
5386
|
-
if (oldestKey === void 0) break;
|
|
5387
|
-
this.entries.delete(oldestKey);
|
|
5388
|
-
}
|
|
5600
|
+
return this.#final;
|
|
5389
5601
|
}
|
|
5390
|
-
|
|
5391
|
-
this.
|
|
5602
|
+
getCandidate(changeSetId) {
|
|
5603
|
+
return this.#candidates.get(changeSetId);
|
|
5604
|
+
}
|
|
5605
|
+
get draftOperations() {
|
|
5606
|
+
return this.#draftOperations;
|
|
5607
|
+
}
|
|
5608
|
+
get context() {
|
|
5609
|
+
return this.#context;
|
|
5610
|
+
}
|
|
5611
|
+
get final() {
|
|
5612
|
+
return this.#final;
|
|
5613
|
+
}
|
|
5614
|
+
#assertOpen() {
|
|
5615
|
+
if (this.#final) throw new Error("Cannot register a survey change after the turn was finalized.");
|
|
5392
5616
|
}
|
|
5393
5617
|
};
|
|
5394
5618
|
//#endregion
|
|
5395
|
-
//#region src/engine/
|
|
5619
|
+
//#region src/engine/task-context.ts
|
|
5620
|
+
const MAX_RELATED_ITEMS = 48;
|
|
5621
|
+
const MAX_SIBLING_ITEMS = 80;
|
|
5622
|
+
const MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM = 24;
|
|
5623
|
+
const MAX_VISIBLE_TEXT_CHARS = 600;
|
|
5624
|
+
const MAX_NAMING_EXAMPLES = 40;
|
|
5625
|
+
const MAX_TRANSLATION_EXAMPLES = 60;
|
|
5396
5626
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5397
|
-
const
|
|
5398
|
-
|
|
5399
|
-
const
|
|
5400
|
-
|
|
5401
|
-
id: "purpose-alignment",
|
|
5402
|
-
title: "Purpose and construct alignment",
|
|
5403
|
-
prompt: "Does every question serve a stated objective, and does it measure the intended construct?"
|
|
5404
|
-
},
|
|
5405
|
-
{
|
|
5406
|
-
id: "cognitive-burden",
|
|
5407
|
-
title: "Respondent burden",
|
|
5408
|
-
prompt: "Is the survey as short and cognitively simple as its purpose allows?"
|
|
5409
|
-
},
|
|
5410
|
-
{
|
|
5411
|
-
id: "question-wording",
|
|
5412
|
-
title: "Clear, neutral wording",
|
|
5413
|
-
prompt: "Are questions specific, neutral, answerable, and limited to one idea without assumptions or double negatives?"
|
|
5414
|
-
},
|
|
5415
|
-
{
|
|
5416
|
-
id: "recall",
|
|
5417
|
-
title: "Recall and reference periods",
|
|
5418
|
-
prompt: "Are recall periods explicit, realistic, and consistent with what respondents can remember?"
|
|
5419
|
-
},
|
|
5420
|
-
{
|
|
5421
|
-
id: "response-options",
|
|
5422
|
-
title: "Response option quality",
|
|
5423
|
-
prompt: "Are options mutually exclusive, collectively sufficient, balanced, ordered, and consistently scaled?"
|
|
5424
|
-
},
|
|
5425
|
-
{
|
|
5426
|
-
id: "flow",
|
|
5427
|
-
title: "Flow and completeness",
|
|
5428
|
-
prompt: "Does the order move logically from easy/general to specific, with needed instructions and no important objective omitted?"
|
|
5429
|
-
},
|
|
5430
|
-
{
|
|
5431
|
-
id: "routing",
|
|
5432
|
-
title: "Requiredness and routing",
|
|
5433
|
-
prompt: "Do required fields, branching, validation, and not-applicable paths avoid forcing inaccurate answers?"
|
|
5434
|
-
},
|
|
5435
|
-
{
|
|
5436
|
-
id: "sensitivity",
|
|
5437
|
-
title: "Sensitivity and ethics",
|
|
5438
|
-
prompt: "Are sensitive questions justified, respectfully phrased, appropriately optional, and paired with privacy context where needed?"
|
|
5439
|
-
},
|
|
5440
|
-
{
|
|
5441
|
-
id: "accessibility",
|
|
5442
|
-
title: "Accessibility and inclusion",
|
|
5443
|
-
prompt: "Is language plain and inclusive, and are content, controls, and images understandable without avoidable barriers?"
|
|
5444
|
-
},
|
|
5445
|
-
{
|
|
5446
|
-
id: "localization",
|
|
5447
|
-
title: "Cross-language equivalence",
|
|
5448
|
-
prompt: "Do translations preserve meaning and cultural relevance rather than merely matching words?"
|
|
5449
|
-
},
|
|
5450
|
-
{
|
|
5451
|
-
id: "testing",
|
|
5452
|
-
title: "Testing and validation",
|
|
5453
|
-
prompt: "Has the instrument been cognitively tested, piloted, and checked for reliability or validity where the intended use requires it?"
|
|
5454
|
-
}
|
|
5455
|
-
];
|
|
5456
|
-
const translationText = (context, locale, itemId, contentKey) => {
|
|
5457
|
-
const localeTranslations = isRecord(context.survey.translations?.[locale]) ? context.survey.translations?.[locale] : void 0;
|
|
5458
|
-
const itemTranslations = isRecord(localeTranslations?.itemTranslations) ? localeTranslations.itemTranslations : void 0;
|
|
5459
|
-
const content = (isRecord(itemTranslations?.[itemId]) ? itemTranslations[itemId] : void 0)?.[contentKey];
|
|
5460
|
-
return content ? getContentPlainText(content).trim() : "";
|
|
5627
|
+
const getItemLabel = (item) => {
|
|
5628
|
+
if (!isRecord(item.metadata)) return void 0;
|
|
5629
|
+
const label = item.metadata.itemLabel;
|
|
5630
|
+
return typeof label === "string" && label.trim().length > 0 ? label.trim() : void 0;
|
|
5461
5631
|
};
|
|
5462
|
-
const
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
const
|
|
5466
|
-
|
|
5632
|
+
const getCenteredNeighbors = (values, focusIndex, limit) => {
|
|
5633
|
+
if (focusIndex < 0 || limit < 1) return [];
|
|
5634
|
+
const windowSize = Math.min(values.length, limit + 1);
|
|
5635
|
+
const idealStart = focusIndex - Math.floor(limit / 2);
|
|
5636
|
+
const start = Math.min(Math.max(0, idealStart), values.length - windowSize);
|
|
5637
|
+
return values.slice(start, start + windowSize).filter((_, index) => start + index !== focusIndex);
|
|
5467
5638
|
};
|
|
5468
|
-
const
|
|
5469
|
-
const
|
|
5470
|
-
const
|
|
5471
|
-
const
|
|
5472
|
-
const
|
|
5473
|
-
const
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
code: "duplicate-item-id",
|
|
5479
|
-
severity: "error",
|
|
5480
|
-
category: "structure",
|
|
5481
|
-
itemId: item.id,
|
|
5482
|
-
message: `Item id "${item.id}" is duplicated.`
|
|
5483
|
-
});
|
|
5484
|
-
itemsById.set(item.id, item);
|
|
5639
|
+
const buildSurveyAssistantTaskContext = (snapshot, options) => {
|
|
5640
|
+
const { inspection } = createSurveyAssistantEngineContext(snapshot, void 0, { focusItemId: options.focusItemId });
|
|
5641
|
+
const itemsById = new Map(snapshot.survey.surveyItems.map((item) => [item.id, item]));
|
|
5642
|
+
const parentById = new Map(inspection.outline.flatMap((item) => item.parentId ? [[item.itemId, item.parentId]] : []));
|
|
5643
|
+
const childIdsByParent = /* @__PURE__ */ new Map();
|
|
5644
|
+
for (const item of inspection.outline) {
|
|
5645
|
+
if (!item.parentId) continue;
|
|
5646
|
+
const childIds = childIdsByParent.get(item.parentId) ?? [];
|
|
5647
|
+
childIds.push(item.itemId);
|
|
5648
|
+
childIdsByParent.set(item.parentId, childIds);
|
|
5485
5649
|
}
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
}
|
|
5504
|
-
else if (parentById.has(childId)) add({
|
|
5505
|
-
code: "multiple-parents",
|
|
5506
|
-
severity: "error",
|
|
5507
|
-
category: "structure",
|
|
5508
|
-
itemId: childId,
|
|
5509
|
-
message: `Item is referenced by multiple parents.`
|
|
5510
|
-
});
|
|
5511
|
-
else parentById.set(childId, parent.id);
|
|
5650
|
+
const fullKeyById = new Map(inspection.keyIndex.map((item) => [item.itemId, item.fullKey]));
|
|
5651
|
+
const summarizeItem = (item) => {
|
|
5652
|
+
const allVisibleText = [];
|
|
5653
|
+
let visibleTextValueTruncated = false;
|
|
5654
|
+
for (const [locale, translations] of Object.entries(snapshot.survey.translations ?? {})) {
|
|
5655
|
+
const itemTranslations = translations.itemTranslations?.[item.id];
|
|
5656
|
+
if (!itemTranslations) continue;
|
|
5657
|
+
for (const [contentKey, content] of Object.entries(itemTranslations)) {
|
|
5658
|
+
const text = getContentPlainText(content).trim();
|
|
5659
|
+
if (text) {
|
|
5660
|
+
if (text.length > MAX_VISIBLE_TEXT_CHARS) visibleTextValueTruncated = true;
|
|
5661
|
+
allVisibleText.push({
|
|
5662
|
+
locale,
|
|
5663
|
+
contentKey,
|
|
5664
|
+
text: text.length > MAX_VISIBLE_TEXT_CHARS ? `${text.slice(0, MAX_VISIBLE_TEXT_CHARS)}...` : text
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5512
5668
|
}
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5669
|
+
return {
|
|
5670
|
+
id: item.id,
|
|
5671
|
+
key: item.key,
|
|
5672
|
+
fullKey: fullKeyById.get(item.id) ?? item.key,
|
|
5673
|
+
itemType: item.itemType,
|
|
5674
|
+
itemLabel: getItemLabel(item),
|
|
5675
|
+
visibleText: allVisibleText.slice(0, MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM),
|
|
5676
|
+
visibleTextTruncated: allVisibleText.length > MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM || visibleTextValueTruncated
|
|
5677
|
+
};
|
|
5678
|
+
};
|
|
5679
|
+
const rootItemId = inspection.outline.find((item) => item.depth === 0)?.itemId;
|
|
5680
|
+
const rootItem = rootItemId ? itemsById.get(rootItemId) : void 0;
|
|
5681
|
+
const result = {
|
|
5682
|
+
document: {
|
|
5683
|
+
revision: snapshot.version.revision,
|
|
5684
|
+
contentHash: snapshot.version.contentHash,
|
|
5685
|
+
capabilityDigest: snapshot.version.capabilityDigest
|
|
5686
|
+
},
|
|
5687
|
+
survey: {
|
|
5688
|
+
key: rootItem?.key,
|
|
5689
|
+
locales: inspection.locales,
|
|
5690
|
+
itemCount: inspection.itemCount,
|
|
5691
|
+
selectedLocale: snapshot.ui.selectedLocale
|
|
5692
|
+
}
|
|
5693
|
+
};
|
|
5694
|
+
const focusItem = options.focusItemId ? itemsById.get(options.focusItemId) : void 0;
|
|
5695
|
+
if (focusItem) {
|
|
5696
|
+
const parentId = parentById.get(focusItem.id);
|
|
5697
|
+
const parentItem = parentId ? itemsById.get(parentId) : void 0;
|
|
5698
|
+
const ancestorItems = [];
|
|
5699
|
+
const seenAncestorIds = /* @__PURE__ */ new Set();
|
|
5700
|
+
let ancestorId = parentId;
|
|
5701
|
+
while (ancestorId && !seenAncestorIds.has(ancestorId)) {
|
|
5702
|
+
seenAncestorIds.add(ancestorId);
|
|
5703
|
+
const ancestor = itemsById.get(ancestorId);
|
|
5704
|
+
if (!ancestor) break;
|
|
5705
|
+
ancestorItems.unshift(ancestor);
|
|
5706
|
+
ancestorId = parentById.get(ancestorId);
|
|
5707
|
+
}
|
|
5708
|
+
const siblingIds = parentItem ? childIdsByParent.get(parentItem.id) ?? [] : [];
|
|
5709
|
+
const siblingItemCount = Math.max(0, siblingIds.length - 1);
|
|
5710
|
+
const siblingItems = getCenteredNeighbors(siblingIds, siblingIds.indexOf(focusItem.id), MAX_SIBLING_ITEMS).map((itemId) => itemsById.get(itemId)).filter((item) => Boolean(item));
|
|
5711
|
+
const allRelatedItems = [];
|
|
5712
|
+
const seenRelatedIds = /* @__PURE__ */ new Set();
|
|
5713
|
+
const pending = [...childIdsByParent.get(focusItem.id) ?? []];
|
|
5714
|
+
while (pending.length > 0) {
|
|
5715
|
+
const itemId = pending.shift();
|
|
5716
|
+
const item = itemId ? itemsById.get(itemId) : void 0;
|
|
5717
|
+
if (!item || seenRelatedIds.has(item.id)) continue;
|
|
5718
|
+
seenRelatedIds.add(item.id);
|
|
5719
|
+
allRelatedItems.push(item);
|
|
5720
|
+
pending.push(...childIdsByParent.get(item.id) ?? []);
|
|
5535
5721
|
}
|
|
5722
|
+
const relatedItems = allRelatedItems.slice(0, MAX_RELATED_ITEMS);
|
|
5723
|
+
result.focus = {
|
|
5724
|
+
item: summarizeItem(focusItem),
|
|
5725
|
+
parent: parentItem ? summarizeItem(parentItem) : void 0,
|
|
5726
|
+
ancestorPath: ancestorItems.map(summarizeItem),
|
|
5727
|
+
siblingKeys: siblingItems.map((item) => item.key),
|
|
5728
|
+
siblingItems: siblingItems.map(summarizeItem),
|
|
5729
|
+
siblingItemCount,
|
|
5730
|
+
siblingItemsTruncated: siblingItemCount > siblingItems.length,
|
|
5731
|
+
relatedItems: relatedItems.map(summarizeItem),
|
|
5732
|
+
relatedItemCount: allRelatedItems.length,
|
|
5733
|
+
relatedItemsTruncated: allRelatedItems.length > relatedItems.length
|
|
5734
|
+
};
|
|
5536
5735
|
}
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
const
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
code: "missing-primary-content",
|
|
5555
|
-
severity: "warning",
|
|
5556
|
-
category: "content",
|
|
5557
|
-
itemId: item.id,
|
|
5558
|
-
locale: sourceLocale,
|
|
5559
|
-
contentKey: surface.key,
|
|
5560
|
-
message: `Required respondent content "${surface.key}" is empty in ${sourceLocale}.`
|
|
5736
|
+
if ((options.purpose === "suggest-item-key" || options.purpose === "suggest-item-label") && focusItem) {
|
|
5737
|
+
const parentId = parentById.get(focusItem.id);
|
|
5738
|
+
const siblingIds = parentId ? childIdsByParent.get(parentId) ?? [] : [];
|
|
5739
|
+
const siblingPositionById = new Map(siblingIds.map((itemId, index) => [itemId, index]));
|
|
5740
|
+
const focusSiblingPosition = siblingPositionById.get(focusItem.id);
|
|
5741
|
+
const rankedItems = snapshot.survey.surveyItems.filter((item) => item.id !== focusItem.id).map((item, index) => ({
|
|
5742
|
+
item,
|
|
5743
|
+
index,
|
|
5744
|
+
siblingDistance: focusSiblingPosition !== void 0 && siblingPositionById.has(item.id) ? Math.abs(siblingPositionById.get(item.id) - focusSiblingPosition) : Number.POSITIVE_INFINITY,
|
|
5745
|
+
relationship: parentById.get(item.id) === parentId ? "sibling" : item.itemType === focusItem.itemType ? "same-type" : "survey"
|
|
5746
|
+
})).sort((left, right) => {
|
|
5747
|
+
const rank = {
|
|
5748
|
+
sibling: 0,
|
|
5749
|
+
"same-type": 1,
|
|
5750
|
+
survey: 2
|
|
5751
|
+
};
|
|
5752
|
+
return rank[left.relationship] - rank[right.relationship] || left.siblingDistance - right.siblingDistance || left.index - right.index;
|
|
5561
5753
|
});
|
|
5562
|
-
const
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
contentKey,
|
|
5572
|
-
message: `Content "${contentKey}" has source text but no ${locale} translation.`
|
|
5573
|
-
});
|
|
5574
|
-
}
|
|
5575
|
-
const config = isRecord(item.config) ? item.config : {};
|
|
5576
|
-
if (item.itemType === "choiceItem") {
|
|
5577
|
-
const optionsList = records(config.options);
|
|
5578
|
-
if (optionsList.length === 0) add({
|
|
5579
|
-
code: "choice-without-options",
|
|
5580
|
-
severity: "error",
|
|
5581
|
-
category: "content",
|
|
5754
|
+
const allKeyExamples = rankedItems.map(({ item, relationship }) => ({
|
|
5755
|
+
itemId: item.id,
|
|
5756
|
+
itemType: item.itemType,
|
|
5757
|
+
key: item.key,
|
|
5758
|
+
relationship
|
|
5759
|
+
}));
|
|
5760
|
+
const allLabelExamples = rankedItems.flatMap(({ item, relationship }) => {
|
|
5761
|
+
const label = getItemLabel(item);
|
|
5762
|
+
return label ? [{
|
|
5582
5763
|
itemId: item.id,
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5764
|
+
itemType: item.itemType,
|
|
5765
|
+
key: item.key,
|
|
5766
|
+
label,
|
|
5767
|
+
relationship
|
|
5768
|
+
}] : [];
|
|
5769
|
+
});
|
|
5770
|
+
result.naming = {
|
|
5771
|
+
keyExamples: allKeyExamples.slice(0, MAX_NAMING_EXAMPLES),
|
|
5772
|
+
keyExampleCount: allKeyExamples.length,
|
|
5773
|
+
keyExamplesTruncated: allKeyExamples.length > MAX_NAMING_EXAMPLES,
|
|
5774
|
+
labelExamples: allLabelExamples.slice(0, MAX_NAMING_EXAMPLES),
|
|
5775
|
+
labelExampleCount: allLabelExamples.length,
|
|
5776
|
+
labelExamplesTruncated: allLabelExamples.length > MAX_NAMING_EXAMPLES
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5779
|
+
const sourceLocale = options.sourceLocale;
|
|
5780
|
+
if (options.purpose === "translate-content" && sourceLocale) {
|
|
5781
|
+
const relevantLocales = [sourceLocale, ...options.targetLocales ?? []];
|
|
5782
|
+
const sourceTokens = new Set((options.sourceTexts ?? []).flatMap((text) => text.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u)).filter((token) => token.length >= 3));
|
|
5783
|
+
const allExamples = [];
|
|
5784
|
+
snapshot.survey.surveyItems.forEach((item, itemIndex) => {
|
|
5785
|
+
const contentKeys = /* @__PURE__ */ new Set();
|
|
5786
|
+
for (const locale of relevantLocales) {
|
|
5787
|
+
const itemTranslations = snapshot.survey.translations?.[locale]?.itemTranslations?.[item.id];
|
|
5788
|
+
for (const contentKey of Object.keys(itemTranslations ?? {})) contentKeys.add(contentKey);
|
|
5789
|
+
}
|
|
5790
|
+
for (const contentKey of contentKeys) {
|
|
5791
|
+
const values = {};
|
|
5792
|
+
for (const locale of relevantLocales) {
|
|
5793
|
+
const content = snapshot.survey.translations?.[locale]?.itemTranslations?.[item.id]?.[contentKey];
|
|
5794
|
+
if (!content) continue;
|
|
5795
|
+
const text = getContentPlainText(content).trim();
|
|
5796
|
+
if (text) values[locale] = text.slice(0, MAX_VISIBLE_TEXT_CHARS);
|
|
5797
|
+
}
|
|
5798
|
+
const sourceText = values[sourceLocale];
|
|
5799
|
+
if (!sourceText || Object.keys(values).length < 2) continue;
|
|
5800
|
+
const normalizedSource = sourceText.toLocaleLowerCase();
|
|
5801
|
+
const score = [...sourceTokens].reduce((total, token) => total + (normalizedSource.includes(token) ? 1 : 0), 0);
|
|
5802
|
+
allExamples.push({
|
|
5590
5803
|
itemId: item.id,
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5804
|
+
contentKey,
|
|
5805
|
+
values,
|
|
5806
|
+
score,
|
|
5807
|
+
order: itemIndex
|
|
5594
5808
|
});
|
|
5595
|
-
if (typeof option.id === "string" && option.embeddedForm) {
|
|
5596
|
-
for (const group of getGroups(item, option.id)) for (const field of records(group.fields)) if (typeof field.id === "string" && !translationText(context, sourceLocale, item.id, `embeddedForms.${option.id}.fields.${field.id}.label`)) add({
|
|
5597
|
-
code: "missing-field-label",
|
|
5598
|
-
severity: "warning",
|
|
5599
|
-
category: "content",
|
|
5600
|
-
itemId: item.id,
|
|
5601
|
-
locale: sourceLocale,
|
|
5602
|
-
contentKey: `embeddedForms.${option.id}.fields.${field.id}.label`,
|
|
5603
|
-
message: `Embedded field "${field.id}" has no visible label.`
|
|
5604
|
-
});
|
|
5605
|
-
}
|
|
5606
5809
|
}
|
|
5810
|
+
});
|
|
5811
|
+
allExamples.sort((left, right) => right.score - left.score || left.order - right.order);
|
|
5812
|
+
result.translation = {
|
|
5813
|
+
examples: allExamples.slice(0, MAX_TRANSLATION_EXAMPLES).map(({ itemId, contentKey, values }) => ({
|
|
5814
|
+
itemId,
|
|
5815
|
+
contentKey,
|
|
5816
|
+
values
|
|
5817
|
+
})),
|
|
5818
|
+
exampleCount: allExamples.length,
|
|
5819
|
+
examplesTruncated: allExamples.length > MAX_TRANSLATION_EXAMPLES
|
|
5820
|
+
};
|
|
5821
|
+
}
|
|
5822
|
+
return result;
|
|
5823
|
+
};
|
|
5824
|
+
//#endregion
|
|
5825
|
+
//#region src/engine/task-result-cache.ts
|
|
5826
|
+
const DEFAULT_TTL_MS = 900 * 1e3;
|
|
5827
|
+
const DEFAULT_MAX_ENTRIES = 64;
|
|
5828
|
+
/** Small in-memory LRU cache for deterministic, snapshot-scoped one-shot results. */
|
|
5829
|
+
var SurveyAssistantTaskResultCache = class {
|
|
5830
|
+
entries = /* @__PURE__ */ new Map();
|
|
5831
|
+
ttlMs;
|
|
5832
|
+
maxEntries;
|
|
5833
|
+
now;
|
|
5834
|
+
constructor(options = {}) {
|
|
5835
|
+
this.ttlMs = Math.max(0, options.ttlMs ?? DEFAULT_TTL_MS);
|
|
5836
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
5837
|
+
this.now = options.now ?? Date.now;
|
|
5838
|
+
}
|
|
5839
|
+
get(key) {
|
|
5840
|
+
const entry = this.entries.get(key);
|
|
5841
|
+
if (!entry) return void 0;
|
|
5842
|
+
if (entry.expiresAt <= this.now()) {
|
|
5843
|
+
this.entries.delete(key);
|
|
5844
|
+
return;
|
|
5607
5845
|
}
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
code: "form-without-groups",
|
|
5612
|
-
severity: "error",
|
|
5613
|
-
category: "content",
|
|
5614
|
-
itemId: item.id,
|
|
5615
|
-
message: "Form item has no field groups."
|
|
5616
|
-
});
|
|
5617
|
-
for (const group of groups) for (const field of records(group.fields)) if (typeof field.id === "string" && !translationText(context, sourceLocale, item.id, `fields.${field.id}.label`)) add({
|
|
5618
|
-
code: "missing-field-label",
|
|
5619
|
-
severity: "warning",
|
|
5620
|
-
category: "content",
|
|
5621
|
-
itemId: item.id,
|
|
5622
|
-
locale: sourceLocale,
|
|
5623
|
-
contentKey: `fields.${field.id}.label`,
|
|
5624
|
-
message: `Form field "${field.id}" has no visible label.`
|
|
5625
|
-
});
|
|
5626
|
-
}
|
|
5846
|
+
this.entries.delete(key);
|
|
5847
|
+
this.entries.set(key, entry);
|
|
5848
|
+
return entry.value;
|
|
5627
5849
|
}
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
severity: "warning",
|
|
5634
|
-
category: "assets",
|
|
5635
|
-
message: `Image asset "${assetId}" has no alternative text.`,
|
|
5636
|
-
suggestion: "Add concise alt text, or an empty alt value when the image is purely decorative."
|
|
5850
|
+
set(key, value) {
|
|
5851
|
+
this.entries.delete(key);
|
|
5852
|
+
this.entries.set(key, {
|
|
5853
|
+
expiresAt: this.now() + this.ttlMs,
|
|
5854
|
+
value
|
|
5637
5855
|
});
|
|
5856
|
+
while (this.entries.size > this.maxEntries) {
|
|
5857
|
+
const oldestKey = this.entries.keys().next().value;
|
|
5858
|
+
if (oldestKey === void 0) break;
|
|
5859
|
+
this.entries.delete(oldestKey);
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
clear() {
|
|
5863
|
+
this.entries.clear();
|
|
5638
5864
|
}
|
|
5639
|
-
const limited = findings.slice(0, options.maxFindings ?? 200);
|
|
5640
|
-
return {
|
|
5641
|
-
summary: limited.reduce((acc, entry) => {
|
|
5642
|
-
acc[entry.severity] += 1;
|
|
5643
|
-
acc.total += 1;
|
|
5644
|
-
return acc;
|
|
5645
|
-
}, {
|
|
5646
|
-
error: 0,
|
|
5647
|
-
warning: 0,
|
|
5648
|
-
info: 0,
|
|
5649
|
-
total: 0
|
|
5650
|
-
}),
|
|
5651
|
-
metrics: {
|
|
5652
|
-
itemCount: context.inspection.itemCount,
|
|
5653
|
-
localeCount: locales.length,
|
|
5654
|
-
responseSlotCount: context.inspection.responseSlots.length,
|
|
5655
|
-
assetCount: Object.keys(assets).length,
|
|
5656
|
-
itemTypes
|
|
5657
|
-
},
|
|
5658
|
-
findings: limited,
|
|
5659
|
-
qualityReviewPrinciples: QUALITY_PRINCIPLES,
|
|
5660
|
-
note: "Technical findings are deterministic. Quality principles are a structured review rubric; apply them with human/model judgment and domain context, and do not present them as psychometric validation."
|
|
5661
|
-
};
|
|
5662
5865
|
};
|
|
5663
5866
|
//#endregion
|
|
5664
|
-
export { deriveResponseSlotTypes as $, getRawSurvey$1 as A, resolveItemSummary as B, clampLimit as C, formFieldGroupLayoutSchema as D, formFieldGroupDetailsSchema as E, mapKeyIndexById as F, summarizeKeyIndexEntry as G, summarizeFocusItemNode as H, mapOutlineById as I, translationEntrySchema as J, summarizeOutlineNode as K, mapRawItemsById as L, itemConfigDetailsSchema as M, itemMatchesQuery as N, formSelectOptionDetailsSchema as O, itemSummarySchema as P, createSurveyAssistantEngineContext as Q, normalizeText as R, choiceOptionDetailsSchema as S, formFieldDetailsSchema as T, summarizeItemById as U, sliceWithCursor as V, summarizeKeyIndexEntries as W, inferExpressionReturnType as X, unique as Y, validateExpression as Z, buildConfigSearchText as _, MAX_SURVEY_ASSISTANT_OPERATIONS_PER_PROPOSAL as a, buildItemTranslationEntries as b, buildSurveyDocumentVersion as c, normalizeSurveyChangeOperations as d, buildExpressionMutationPatches as f, MAX_LIMIT as g, DEFAULT_LIMIT as h,
|
|
5867
|
+
export { deriveResponseSlotTypes as $, getRawSurvey$1 as A, resolveItemSummary as B, clampLimit as C, formFieldGroupLayoutSchema as D, formFieldGroupDetailsSchema as E, mapKeyIndexById as F, summarizeKeyIndexEntry as G, summarizeFocusItemNode as H, mapOutlineById as I, translationEntrySchema as J, summarizeOutlineNode as K, mapRawItemsById as L, itemConfigDetailsSchema as M, itemMatchesQuery as N, formSelectOptionDetailsSchema as O, itemSummarySchema as P, createSurveyAssistantEngineContext as Q, normalizeText as R, choiceOptionDetailsSchema as S, formFieldDetailsSchema as T, summarizeItemById as U, sliceWithCursor as V, summarizeKeyIndexEntries as W, inferExpressionReturnType as X, unique as Y, validateExpression as Z, buildConfigSearchText as _, MAX_SURVEY_ASSISTANT_OPERATIONS_PER_PROPOSAL as a, buildItemTranslationEntries as b, buildSurveyDocumentVersion as c, normalizeSurveyChangeOperations as d, buildExpressionMutationPatches as f, MAX_LIMIT as g, DEFAULT_LIMIT as h, auditSurvey as i, getRawSurveyItems as j, getKeyIndexEntries as k, createSurveyChangeSet as l, expressionSchema as m, buildSurveyAssistantTaskContext as n, MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT as o, expressionMutationSchema as p, summarizeRawItem as q, TurnSurveyDraft as r, applyValidatedSurveyOperationsToDraft as s, SurveyAssistantTaskResultCache as t, validateSurveyOperations as u, buildEmbeddedFormDetails as v, findDirectChildrenByFullKey as w, buildSurveyTranslations as x, buildItemConfigDetails as y, parseCursor as z };
|
|
5665
5868
|
|
|
5666
|
-
//# sourceMappingURL=engine-
|
|
5869
|
+
//# sourceMappingURL=engine-CWTuN3o2.mjs.map
|