@case-framework/survey-core 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/editor.d.mts.map +1 -1
- package/build/editor.mjs.map +1 -1
- package/build/index.d.mts +48 -3
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +167 -11
- package/build/index.mjs.map +1 -1
- package/build/package.json +1 -1
- package/package.json +1 -1
package/build/index.mjs
CHANGED
|
@@ -547,34 +547,81 @@ function buildItemExpression(item, definitions, expressionId, params) {
|
|
|
547
547
|
}
|
|
548
548
|
//#endregion
|
|
549
549
|
//#region src/expressions/reference-integrity.ts
|
|
550
|
+
const ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY = {
|
|
551
|
+
component: false,
|
|
552
|
+
item: true,
|
|
553
|
+
survey: false
|
|
554
|
+
};
|
|
555
|
+
const ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY = {
|
|
556
|
+
"display-condition": true,
|
|
557
|
+
"disabled-condition": false,
|
|
558
|
+
validation: false,
|
|
559
|
+
"prefill-condition": false,
|
|
560
|
+
"prefill-source": false,
|
|
561
|
+
"template-value": false
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* Display-condition cycles are advisory because responses may be populated before visibility is
|
|
565
|
+
* evaluated, including through runtime prefills that are unavailable to static survey analysis.
|
|
566
|
+
* Concrete expression integrity failures remain blocking. This exhaustive map is the source of
|
|
567
|
+
* truth copied onto every analyzer-produced diagnostic.
|
|
568
|
+
*/
|
|
569
|
+
const SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY = {
|
|
570
|
+
"invalid-expression": {
|
|
571
|
+
category: "reference-integrity",
|
|
572
|
+
disposition: "blocking"
|
|
573
|
+
},
|
|
574
|
+
"unknown-response-ref": {
|
|
575
|
+
category: "reference-integrity",
|
|
576
|
+
disposition: "blocking"
|
|
577
|
+
},
|
|
578
|
+
"expression-reference-type-mismatch": {
|
|
579
|
+
category: "reference-integrity",
|
|
580
|
+
disposition: "blocking"
|
|
581
|
+
},
|
|
582
|
+
"unknown-reference-value": {
|
|
583
|
+
category: "reference-integrity",
|
|
584
|
+
disposition: "blocking"
|
|
585
|
+
},
|
|
586
|
+
"display-condition-cycle": {
|
|
587
|
+
category: "display-dependency",
|
|
588
|
+
disposition: "advisory"
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const createExpressionReferenceDiagnostic = (diagnostic) => ({
|
|
592
|
+
...diagnostic,
|
|
593
|
+
...SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY[diagnostic.code]
|
|
594
|
+
});
|
|
550
595
|
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
551
596
|
const isJsonExpression = (value) => isRecord$1(value) && typeof value.type === "string";
|
|
552
597
|
const collectItemExpressionLocations = (item) => {
|
|
553
598
|
const locations = [];
|
|
554
|
-
const add = (expression, location, surface) => {
|
|
599
|
+
const add = (expression, location, ownerScope, surface) => {
|
|
555
600
|
if (!isJsonExpression(expression)) return;
|
|
556
601
|
locations.push({
|
|
557
602
|
expression,
|
|
558
603
|
location,
|
|
559
604
|
ownerItemId: item.id,
|
|
560
605
|
ownerItemKey: item.key,
|
|
606
|
+
ownerScope,
|
|
561
607
|
surface
|
|
562
608
|
});
|
|
563
609
|
};
|
|
564
|
-
add(item.displayConditions?.root, "displayConditions.root", "display-condition");
|
|
565
|
-
for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "display-condition");
|
|
566
|
-
for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "disabled-condition");
|
|
567
|
-
for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "validation");
|
|
610
|
+
add(item.displayConditions?.root, "displayConditions.root", "item", "display-condition");
|
|
611
|
+
for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "component", "display-condition");
|
|
612
|
+
for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "component", "disabled-condition");
|
|
613
|
+
for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "item", "validation");
|
|
568
614
|
for (const [prefillIndex, prefill] of (item.prefills ?? []).entries()) {
|
|
569
615
|
const prefillId = prefill.id || String(prefillIndex);
|
|
570
|
-
add(prefill.when, `prefills.${prefillId}.when`, "prefill-condition");
|
|
571
|
-
if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "prefill-source");
|
|
616
|
+
add(prefill.when, `prefills.${prefillId}.when`, "item", "prefill-condition");
|
|
617
|
+
if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "item", "prefill-source");
|
|
572
618
|
}
|
|
573
619
|
return locations;
|
|
574
620
|
};
|
|
575
621
|
const collectSurveyExpressionLocations = (survey) => [...survey.surveyItems.flatMap(collectItemExpressionLocations), ...Object.entries(survey.templateValues ?? {}).flatMap(([templateKey, templateValue]) => templateValue.expression ? [{
|
|
576
622
|
expression: templateValue.expression,
|
|
577
623
|
location: `templateValues.${templateKey}.expression`,
|
|
624
|
+
ownerScope: "survey",
|
|
578
625
|
surface: "template-value"
|
|
579
626
|
}] : [])];
|
|
580
627
|
const candidateRefs = (unknownRef, availableRefs) => {
|
|
@@ -594,6 +641,76 @@ const candidateRefs = (unknownRef, availableRefs) => {
|
|
|
594
641
|
});
|
|
595
642
|
return (sameItemAndMethod.length > 0 ? sameItemAndMethod : availableRefs).slice(0, 5);
|
|
596
643
|
};
|
|
644
|
+
const findCyclicItemDisplayDependencyEdges = ({ edges, itemIds }) => {
|
|
645
|
+
const adjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
646
|
+
const reverseAdjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
647
|
+
for (const edge of edges) {
|
|
648
|
+
adjacency.get(edge.fromItemId)?.add(edge.toItemId);
|
|
649
|
+
reverseAdjacency.get(edge.toItemId)?.add(edge.fromItemId);
|
|
650
|
+
}
|
|
651
|
+
const finishOrder = [];
|
|
652
|
+
const visited = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const startItemId of itemIds) {
|
|
654
|
+
if (visited.has(startItemId)) continue;
|
|
655
|
+
const stack = [{
|
|
656
|
+
expanded: false,
|
|
657
|
+
itemId: startItemId
|
|
658
|
+
}];
|
|
659
|
+
while (stack.length > 0) {
|
|
660
|
+
const current = stack.pop();
|
|
661
|
+
if (!current) break;
|
|
662
|
+
if (current.expanded) {
|
|
663
|
+
finishOrder.push(current.itemId);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (visited.has(current.itemId)) continue;
|
|
667
|
+
visited.add(current.itemId);
|
|
668
|
+
stack.push({
|
|
669
|
+
expanded: true,
|
|
670
|
+
itemId: current.itemId
|
|
671
|
+
});
|
|
672
|
+
const dependencies = [...adjacency.get(current.itemId) ?? []];
|
|
673
|
+
for (let index = dependencies.length - 1; index >= 0; index -= 1) {
|
|
674
|
+
const dependencyId = dependencies[index];
|
|
675
|
+
if (visited.has(dependencyId)) continue;
|
|
676
|
+
stack.push({
|
|
677
|
+
expanded: false,
|
|
678
|
+
itemId: dependencyId
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const componentByItemId = /* @__PURE__ */ new Map();
|
|
684
|
+
const componentMembers = /* @__PURE__ */ new Map();
|
|
685
|
+
for (const startItemId of finishOrder.reverse()) {
|
|
686
|
+
if (componentByItemId.has(startItemId)) continue;
|
|
687
|
+
const componentId = componentMembers.size;
|
|
688
|
+
const members = [];
|
|
689
|
+
const stack = [startItemId];
|
|
690
|
+
componentByItemId.set(startItemId, componentId);
|
|
691
|
+
while (stack.length > 0) {
|
|
692
|
+
const itemId = stack.pop();
|
|
693
|
+
if (!itemId) break;
|
|
694
|
+
members.push(itemId);
|
|
695
|
+
for (const dependencyOwnerId of reverseAdjacency.get(itemId) ?? []) {
|
|
696
|
+
if (componentByItemId.has(dependencyOwnerId)) continue;
|
|
697
|
+
componentByItemId.set(dependencyOwnerId, componentId);
|
|
698
|
+
stack.push(dependencyOwnerId);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
componentMembers.set(componentId, members.sort());
|
|
702
|
+
}
|
|
703
|
+
return edges.flatMap((edge) => {
|
|
704
|
+
const componentId = componentByItemId.get(edge.fromItemId);
|
|
705
|
+
if (componentId === void 0 || componentId !== componentByItemId.get(edge.toItemId)) return [];
|
|
706
|
+
const cycleItemIds = componentMembers.get(componentId) ?? [];
|
|
707
|
+
if (edge.fromItemId !== edge.toItemId && cycleItemIds.length < 2) return [];
|
|
708
|
+
return [{
|
|
709
|
+
edge,
|
|
710
|
+
cycleItemIds
|
|
711
|
+
}];
|
|
712
|
+
});
|
|
713
|
+
};
|
|
597
714
|
const inferExpressionType = (expression, slots) => {
|
|
598
715
|
if (!expression) return void 0;
|
|
599
716
|
if (expression.type === ExpressionType.Const) return expression.value?.type;
|
|
@@ -668,6 +785,9 @@ const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
|
|
|
668
785
|
const slots = new Map(responseSlots.map((slot) => [slot.ref, slot]));
|
|
669
786
|
const availableRefs = responseSlots.map((slot) => slot.ref);
|
|
670
787
|
const diagnostics = [];
|
|
788
|
+
const surveyItemIds = survey.surveyItems.map((item) => item.id);
|
|
789
|
+
const surveyItemIdSet = new Set(surveyItemIds);
|
|
790
|
+
const itemDisplayDependencyEdges = [];
|
|
671
791
|
for (const location of collectSurveyExpressionLocations(survey)) {
|
|
672
792
|
let parsed;
|
|
673
793
|
try {
|
|
@@ -684,6 +804,12 @@ const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
|
|
|
684
804
|
}
|
|
685
805
|
for (const ref of parsed?.responseVariableRefs ?? []) {
|
|
686
806
|
const reference = ref.toString();
|
|
807
|
+
if (location.ownerItemId && ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY[location.ownerScope] && ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY[location.surface] && surveyItemIdSet.has(ref.itemId)) itemDisplayDependencyEdges.push({
|
|
808
|
+
fromItemId: location.ownerItemId,
|
|
809
|
+
location,
|
|
810
|
+
reference,
|
|
811
|
+
toItemId: ref.itemId
|
|
812
|
+
});
|
|
687
813
|
if (!slots.has(reference)) diagnostics.push({
|
|
688
814
|
code: "unknown-response-ref",
|
|
689
815
|
message: `${location.location} references unknown response slot "${reference}".`,
|
|
@@ -701,7 +827,21 @@ const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
|
|
|
701
827
|
slots
|
|
702
828
|
});
|
|
703
829
|
}
|
|
704
|
-
|
|
830
|
+
for (const { cycleItemIds, edge } of findCyclicItemDisplayDependencyEdges({
|
|
831
|
+
edges: itemDisplayDependencyEdges,
|
|
832
|
+
itemIds: surveyItemIds
|
|
833
|
+
})) {
|
|
834
|
+
const isSelfReference = edge.fromItemId === edge.toItemId;
|
|
835
|
+
diagnostics.push({
|
|
836
|
+
code: "display-condition-cycle",
|
|
837
|
+
message: isSelfReference ? `${edge.location.location} for item "${edge.fromItemId}" references its own response "${edge.reference}", creating a circular display dependency.` : `${edge.location.location} for item "${edge.fromItemId}" references item "${edge.toItemId}" within a circular display dependency among ${cycleItemIds.map((itemId) => `"${itemId}"`).join(", ")}.`,
|
|
838
|
+
location: edge.location.location,
|
|
839
|
+
ownerItemId: edge.location.ownerItemId,
|
|
840
|
+
ownerItemKey: edge.location.ownerItemKey,
|
|
841
|
+
reference: edge.reference
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return diagnostics.map(createExpressionReferenceDiagnostic);
|
|
705
845
|
};
|
|
706
846
|
const expressionReferenceDiagnosticKey = (diagnostic) => [
|
|
707
847
|
diagnostic.code,
|
|
@@ -710,10 +850,21 @@ const expressionReferenceDiagnosticKey = (diagnostic) => [
|
|
|
710
850
|
diagnostic.reference ?? "",
|
|
711
851
|
diagnostic.referenceValue ?? ""
|
|
712
852
|
].join("\0");
|
|
713
|
-
const
|
|
853
|
+
const findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories = ({ after, before }) => {
|
|
714
854
|
const existing = new Set(before.map(expressionReferenceDiagnosticKey));
|
|
715
855
|
return after.filter((diagnostic) => !existing.has(expressionReferenceDiagnosticKey(diagnostic)));
|
|
716
856
|
};
|
|
857
|
+
const isBlockingSurveyExpressionReferenceDiagnostic = (diagnostic) => {
|
|
858
|
+
switch (diagnostic.disposition) {
|
|
859
|
+
case "blocking": return true;
|
|
860
|
+
case "advisory": return false;
|
|
861
|
+
default: return true;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
const findIntroducedBlockingExpressionReferenceDiagnostics = ({ after, before }) => findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories({
|
|
865
|
+
after,
|
|
866
|
+
before
|
|
867
|
+
}).filter(isBlockingSurveyExpressionReferenceDiagnostic);
|
|
717
868
|
const replaceResponseVariableReferences = (value, replacements) => {
|
|
718
869
|
let changed = false;
|
|
719
870
|
const visit = (current) => {
|
|
@@ -1937,7 +2088,12 @@ const applyAdd = (document, path, value) => {
|
|
|
1937
2088
|
return document;
|
|
1938
2089
|
}
|
|
1939
2090
|
if (!isRecord(parent)) throw new JsonPatchError(`JSON pointer "${path}" parent is not an object or array.`, "invalid-target-parent", path);
|
|
1940
|
-
parent
|
|
2091
|
+
Object.defineProperty(parent, key, {
|
|
2092
|
+
configurable: true,
|
|
2093
|
+
enumerable: true,
|
|
2094
|
+
value: structuredCloneMethod(value),
|
|
2095
|
+
writable: true
|
|
2096
|
+
});
|
|
1941
2097
|
return document;
|
|
1942
2098
|
};
|
|
1943
2099
|
const applyRemove = (document, path) => {
|
|
@@ -2369,6 +2525,6 @@ function generateCodebook(survey, options) {
|
|
|
2369
2525
|
};
|
|
2370
2526
|
}
|
|
2371
2527
|
//#endregion
|
|
2372
|
-
export { AndExpressionEditor, CURRENT_SURVEY_SCHEMA, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstExpression, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, ContextVariableExpression, ContextVariableType, CtxCustomExpressionEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DurationUnits, EXPORT_COLUMN_SLOT_SEPARATOR, EqExpressionEditor, Expression, ExpressionEditor, ExpressionEvaluator, ExpressionType, FunctionExpression, FunctionExpressionNames, GroupItemCore, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, PageBreakItemCore, ReferenceUsageType, ReservedSurveyItemTypes, ResponseItem, ResponseVariableEditor, ResponseVariableExpression, SURVEY_EDITOR_ITEM_COLORS, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEngineCore, SurveyEventTypes, SurveyItemCore, SurveyItemKey, SurveyItemPrefillApplyMode, SurveyItemPrefillTargetType, SurveyItemResponse, SurveyItemTranslations, SurveyResponse, SurveyResponseExporter, SurveyTranslations, TemplateDefTypes, ValueReference, ValueReferenceMethod, ValueType, analyzeSurveyExpressionReferences, and, applyJsonPatch, assertResponseValue, buildItemExpression, builtInItemCoreRegistry, collectSurveyExpressionLocations, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, createFullRegistry, createItemCore, createItemTypeDefinitionRegistry, createRichTextContent, createSeededRandom, ctx_custom_expression, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, decodeJsonPointerSegment, deserializeSurveyItemPrefill, deserializeTemplateValue, deserializeTemplateValues, encodeJsonPointerSegment, eq, escapeCsvCell, exportSingleResponseSlot, expressionReferenceDiagnosticKey,
|
|
2528
|
+
export { AndExpressionEditor, CURRENT_SURVEY_SCHEMA, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstExpression, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, ContextVariableExpression, ContextVariableType, CtxCustomExpressionEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DurationUnits, EXPORT_COLUMN_SLOT_SEPARATOR, EqExpressionEditor, Expression, ExpressionEditor, ExpressionEvaluator, ExpressionType, FunctionExpression, FunctionExpressionNames, GroupItemCore, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, PageBreakItemCore, ReferenceUsageType, ReservedSurveyItemTypes, ResponseItem, ResponseVariableEditor, ResponseVariableExpression, SURVEY_EDITOR_ITEM_COLORS, SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEngineCore, SurveyEventTypes, SurveyItemCore, SurveyItemKey, SurveyItemPrefillApplyMode, SurveyItemPrefillTargetType, SurveyItemResponse, SurveyItemTranslations, SurveyResponse, SurveyResponseExporter, SurveyTranslations, TemplateDefTypes, ValueReference, ValueReferenceMethod, ValueType, analyzeSurveyExpressionReferences, and, applyJsonPatch, assertResponseValue, buildItemExpression, builtInItemCoreRegistry, collectSurveyExpressionLocations, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, createFullRegistry, createItemCore, createItemTypeDefinitionRegistry, createRichTextContent, createSeededRandom, ctx_custom_expression, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, decodeJsonPointerSegment, deserializeSurveyItemPrefill, deserializeTemplateValue, deserializeTemplateValues, encodeJsonPointerSegment, eq, escapeCsvCell, exportSingleResponseSlot, expressionReferenceDiagnosticKey, findIntroducedBlockingExpressionReferenceDiagnostics, findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories, flattenTree, generateCodebook, generateCodingKey, generateId, getAssetUsagesFromContent, getContentPlainText, getItemExpressionDefinition, getPlainTextFromRichTextContent, getValueAtJsonPointer, gt, gte, hasRenderableRichTextBlock, hasRenderableRichTextContent, in_range, initValueForType, isBlockingSurveyExpressionReferenceDiagnostic, isBuiltInItemType, isContentEmpty, isLegacyItemGroupComponent, isLegacySurveyGroupItem, isResponseValue, lt, lte, max, min, or, parseJsonPointer, prefillTargetsEqual, replaceResponseVariableReferences, responseValueReferenceIds, response_boolean, response_date, response_date_array, response_number, response_number_array, response_string, response_string_array, serializeSurveyItemPrefill, serializeTemplateValue, serializeTemplateValues, serializeToCsv, shuffleArray, shuffleIndices, str_eq, str_list_contains, structuredCloneMethod, sum, toItemTypeDefinitionRegistry, validateLocale };
|
|
2373
2529
|
|
|
2374
2530
|
//# sourceMappingURL=index.mjs.map
|