@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/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
- return diagnostics;
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 findIntroducedExpressionReferenceDiagnostics = ({ after, before }) => {
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[key] = structuredCloneMethod(value);
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, findIntroducedExpressionReferenceDiagnostics, flattenTree, generateCodebook, generateCodingKey, generateId, getAssetUsagesFromContent, getContentPlainText, getItemExpressionDefinition, getPlainTextFromRichTextContent, getValueAtJsonPointer, gt, gte, hasRenderableRichTextBlock, hasRenderableRichTextContent, in_range, initValueForType, 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 };
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