Source

services/investigation.builder.js

import { InvestigationStatus } from "@typez/investigation/enums";
import { getLogger } from "@utils/asyncLocalStorage";
import { hasArrayValue, hasNumberValue, hasStringValue } from "@utils/hasValue";
/**
 * InvestigationBuilder
 * @category Services
 */
export class InvestigationBuilder {
    /**
     * Field mappings for string editable fields
     * @category Services
     */
    static STRING_FIELD_MAPPINGS = [
        { dtoField: "title", investigationField: "title" },
        { dtoField: "curriculum", investigationField: "curriculum" },
        { dtoField: "unitNumberAndTitle", investigationField: "unitNumberAndTitle" },
        { dtoField: "grade", investigationField: "grade" },
        { dtoField: "lessonNumberAndTitle", investigationField: "lessonNumberAndTitle" },
        { dtoField: "objectives", investigationField: "objectives" },
        { dtoField: "ngss", investigationField: "ngss" },
        { dtoField: "analyticalFacts", investigationField: "analyticalFacts" },
        { dtoField: "goals", investigationField: "goals" },
        { dtoField: "day", investigationField: "day" },
    ];
    /**
     * Field mappings for number editable fields
     */
    static NUMBER_FIELD_MAPPINGS = [{ dtoField: "day", investigationField: "day" }];
    /**
     * Field mappings for array editable fields
     */
    static ARRAY_FIELD_MAPPINGS = [{ dtoField: "objects", investigationField: "objects" }];
    /**
     * Builds investigation data for creation with global options.
     * @category Services
     * @param {ICreateInvestigationDto} createDto - The validated request data.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation (default: false).
     * @param {InvestigationStatus} investigationStatus - The investigation status (default: DRAFT_INCOMPLETE).
     * @returns {Partial<IInvestigation>} A clean investigation object ready for database storage.
     */
    static build(createDto, aiAssistantFlow = false, investigationStatus = InvestigationStatus.DRAFT_INCOMPLETE) {
        const builder = {
            // Lifecycle & locking fields (always included)
            status: investigationStatus,
            isLocked: false,
            lockedBy: null,
            lockedAt: null,
        };
        // Build editable fields only if they have values
        this.buildStringFields(builder, createDto, aiAssistantFlow);
        // this.buildNumberFields(builder, createDto, aiAssistantFlow);
        this.buildStepsField(builder, createDto, aiAssistantFlow);
        this.buildArrayFields(builder, createDto, aiAssistantFlow);
        this.buildMetadata(builder, createDto, aiAssistantFlow);
        builder.lastChangeWithAI = createDto.lastChangeWithAI ?? true;
        return builder;
    }
    /**
     * Creates MongoDB update fields for setting contradiction properties.
     * @category Services
     * @param {string} fieldPath - The MongoDB dot notation path (e.g., "steps.0.title", "objects.1.name").
     * @param {IDetectedContradiction} contradiction - The detected contradiction object.
     * @returns {Record<string, unknown>} An object containing MongoDB update fields for setting the contradiction.
     */
    static createContradictionSetFields(fieldPath, contradiction) {
        return {
            [`${fieldPath}.isContradicting`]: true,
            [`${fieldPath}.targetFieldName`]: contradiction.targetFieldName,
            [`${fieldPath}.contradictionReason`]: contradiction.contradictionReasoning,
            [`${fieldPath}.updatedAt`]: new Date(),
        };
    }
    /**
     * Creates MongoDB update fields for resetting contradiction properties.
     * @category Services
     * @param {string} fieldPath - The MongoDB dot notation path (e.g., "steps.0.title", "objects.1.name").
     * @returns {Record<string, unknown>} An object containing MongoDB update fields for resetting contradictions.
     */
    static createContradictionResetFields(fieldPath) {
        return {
            [`${fieldPath}.isContradicting`]: false,
            [`${fieldPath}.targetFieldName`]: null,
            [`${fieldPath}.contradictionReason`]: null,
            [`${fieldPath}.updatedAt`]: new Date(),
        };
    }
    /**
     * Checks for contradictions in array fields of an investigation.
     * @category Services
     * @param {IInvestigationModel} investigation - The investigation to check for contradictions.
     * @param {IDetectedContradiction[] | null | undefined} detectedContradiction - The detected contradictions.
     * @returns {Record<string, unknown>} An object containing MongoDB update fields for setting or resetting contradictions.
     */
    static checkArrayFieldContradiction(investigation, detectedContradiction) {
        const updateFields = {};
        // Check steps for contradictions
        if (investigation.steps && investigation.steps.length > 0) {
            investigation.steps.forEach((step, stepIndex) => {
                const stepFields = ["title", "descriptionEn", "desiredOutcome", "alternativeOutcome"];
                stepFields.forEach((fieldName) => {
                    const field = step[fieldName];
                    const contradictionKey = `steps[${stepIndex}].${fieldName}`;
                    // Check if this field has a contradiction in the new detection
                    const newContradiction = detectedContradiction?.find((contradiction) => contradiction.fieldName === contradictionKey);
                    if (newContradiction) {
                        // New contradiction detected - set contradiction properties
                        const fieldPath = `steps.${stepIndex}.${fieldName}`;
                        const setFields = this.createContradictionSetFields(fieldPath, newContradiction);
                        Object.assign(updateFields, setFields);
                    }
                    else if (field?.isContradicting) {
                        // Field was contradicting but no longer is - reset it
                        const fieldPath = `steps.${stepIndex}.${fieldName}`;
                        const resetFields = this.createContradictionResetFields(fieldPath);
                        Object.assign(updateFields, resetFields);
                    }
                });
            });
        }
        // Check objects for contradictions
        if (investigation.objects && investigation.objects.length > 0) {
            investigation.objects.forEach((object, objectIndex) => {
                const objectFields = ["name"];
                objectFields.forEach((fieldName) => {
                    const field = object[fieldName];
                    const contradictionKey = `objects[${objectIndex}].${fieldName}`;
                    // Check if this field has a contradiction in the new detection
                    const newContradiction = detectedContradiction?.find((contradiction) => contradiction.fieldName === contradictionKey);
                    if (newContradiction) {
                        // New contradiction detected - set contradiction properties
                        const fieldPath = `objects.${objectIndex}.${fieldName}`;
                        const setFields = this.createContradictionSetFields(fieldPath, newContradiction);
                        Object.assign(updateFields, setFields);
                    }
                    else if (field?.isContradicting) {
                        // Field was contradicting but no longer is - reset it
                        const fieldPath = `objects.${objectIndex}.${fieldName}`;
                        const resetFields = this.createContradictionResetFields(fieldPath);
                        Object.assign(updateFields, resetFields);
                    }
                });
            });
        }
        return updateFields;
    }
    /**
     * Builds investigation update data for contradiction detection.
     * @category Services
     * @param {IAssistantContradictionDetectionFormat} detectedContradictionResult - An array of detected contradictions to apply to investigation fields.
     * @param {IInvestigationModel} investigation - The current investigation to update.
     * @returns {Record<string, unknown>} A MongoDB update object with contradiction flags and metadata for affected fields.
     */
    static buildUpdateContradiction(detectedContradictionResult, investigation) {
        const logger = getLogger();
        logger.debug({
            investigationId: investigation._id,
            hasDetectedContradiction: detectedContradictionResult.isContradictionDetected,
            contradictionsCount: detectedContradictionResult.detectedContradiction?.length || 0,
            detectedContradictions: detectedContradictionResult.detectedContradiction,
        }, "Starting buildUpdateContradiction");
        const updateFields = {};
        // Update metadata.dateModified to current date
        updateFields["metadata.dateModified"] = new Date();
        const fieldsKeysForUpdate = [
            "title",
            "curriculum",
            "unitNumberAndTitle",
            "grade",
            "lessonNumberAndTitle",
            "objectives",
            "ngss",
            "analyticalFacts",
            "goals",
            "day",
        ];
        if (!detectedContradictionResult.isContradictionDetected) {
            for (const fieldName of fieldsKeysForUpdate) {
                // For array fields (steps, objects), we don't check isContradicting on the array itself
                // For other fields, check if they're not contradicting to skip unnecessary updates
                const fieldValue = investigation[fieldName];
                const wasContradicting = fieldValue?.isContradicting;
                if (wasContradicting) {
                    // reset if in the past the field was contradicting !
                    logger.debug({ investigationId: investigation._id, fieldName }, "Field was contradicting, creating reset fields");
                    const resetFields = this.createContradictionResetFields(fieldName);
                    Object.assign(updateFields, resetFields);
                }
            }
            const arrayUpdateFields = this.checkArrayFieldContradiction(investigation, detectedContradictionResult.detectedContradiction);
            Object.assign(updateFields, arrayUpdateFields);
            return updateFields;
        }
        //Found contradictions!
        for (const fieldName of fieldsKeysForUpdate) {
            const contradiction = detectedContradictionResult.detectedContradiction?.find((contradiction) => contradiction.fieldName === fieldName);
            if (contradiction) {
                // Use dot notation for MongoDB updates
                updateFields[`${fieldName}.isContradicting`] = true;
                updateFields[`${fieldName}.targetFieldName`] = contradiction.targetFieldName;
                updateFields[`${fieldName}.contradictionReason`] = contradiction.contradictionReasoning;
                updateFields[`${fieldName}.updatedAt`] = new Date();
            }
            else {
                // Only reset if the field was previously contradicting
                const fieldValue = investigation[fieldName];
                if (fieldValue?.isContradicting) {
                    logger.debug({ investigationId: investigation._id, fieldName }, "Field was contradicting, creating reset fields");
                    const resetFields = this.createContradictionResetFields(fieldName);
                    Object.assign(updateFields, resetFields);
                }
            }
        }
        // Handle array fields (steps and objects) for contradictions
        const arrayUpdateFields = this.checkArrayFieldContradiction(investigation, detectedContradictionResult.detectedContradiction);
        Object.assign(updateFields, arrayUpdateFields);
        return updateFields;
    }
    /**
     * Builds investigation update data for the new API format.
     * @category Services
     * @param {IUpdateInvestigationDto} updateDto - The update data in the new format, including string/number fields and step updates.
     * @param {boolean} isAiGenerated - Whether the update is AI-generated, which sets aiGeneratedValue fields.
     * @param {IInvestigationModel | null} existingInvestigation - Optional existing investigation to preserve values when fields are null.
     * @returns {MongoUpdateFields} A MongoDB update object containing field paths for direct database updates.
     */
    static buidlUpdate(updateDto, isAiGenerated = false, existingInvestigation = null) {
        const updateFields = {};
        // Handle string fields - update the value field of EditableFieldSchema
        const primitiveFields = [
            // string fields
            "title",
            "curriculum",
            "unitNumberAndTitle",
            "grade",
            "lessonNumberAndTitle",
            "objectives",
            "ngss",
            "analyticalFacts",
            "goals",
            // number fields
            "day",
        ];
        for (const field of primitiveFields) {
            const fieldValue = updateDto[field];
            if (fieldValue !== undefined) {
                if (typeof fieldValue === "string" || typeof fieldValue === "number") {
                    updateFields[`${field}.value`] = fieldValue;
                    if (isAiGenerated) {
                        updateFields[`${field}.aiGeneratedValue`] = fieldValue;
                    }
                }
                else if (typeof fieldValue === "object" && !Array.isArray(fieldValue)) {
                    Object.entries(fieldValue).forEach(([key, value]) => {
                        updateFields[`${field}.${key}`] = value;
                    });
                }
                updateFields[`${field}.updatedAt`] = new Date();
            }
        }
        // Handle objects array updates
        if (updateDto.objects !== undefined) {
            // Merge with existing objects to preserve values when fields are null
            const existingObjects = existingInvestigation?.objects || [];
            updateFields.objects = updateDto.objects.map((obj, index) => this.mapObjectToMongoFormat(obj, isAiGenerated, existingObjects[index] || null));
        }
        // Handle steps array replacement (supports deletions when fewer steps are sent)
        if (updateDto.steps !== undefined) {
            // Merge with existing steps to preserve values when fields are null
            const existingSteps = existingInvestigation?.steps || [];
            updateFields.steps = this.buildEditableStepsFromSimpleSteps(updateDto.steps ?? [], isAiGenerated, existingSteps);
        }
        // Update metadata.dateModified to current date
        updateFields["metadata.dateModified"] = new Date();
        return updateFields;
    }
    /**
     * Creates a Vector3 EditableField structure.
     * @category Services
     * @param {object} vector - The vector data containing x, y, and z coordinates.
     * @returns {IEditableField<number> | null} A Vector3 object with an EditableField structure, or `null` if not applicable.
     */
    static createVector3EditableField(vector) {
        if (!vector)
            return null;
        return {
            x: this.createEditableField(vector.x || 0, false),
            y: this.createEditableField(vector.y || 0, false),
            z: this.createEditableField(vector.z || 0, false),
        };
    }
    /**
     * Maps an API object format to a MongoDB schema format.
     * Supports both simple values and {value, aiGeneratedValue} format.
     * Preserves existing values when update fields are null/undefined.
     * @category Services
     * @param {IUpdateObjectDto} obj - The object in API format.
     * @param {boolean} isAiGenerated - Whether the update is AI-generated, which sets aiGeneratedValue fields.
     * @param {IObjectBase | null} existingObject - Optional existing object to preserve values from.
     * @returns {IObjectBase} The object in MongoDB format with an EditableField structure.
     */
    static mapObjectToMongoFormat(obj, isAiGenerated = false, existingObject = null) {
        // Extract name payload (supports both {value, aiGeneratedValue} and simple string format)
        const namePayload = this.extractEditablePayload(obj.name);
        const objectIdPayload = this.extractEditablePayload(obj.objectId);
        // Preserve existing values if new values are null/undefined
        const nameValue = namePayload.value ?? existingObject?.name?.value ?? null;
        const nameAiGeneratedValue = namePayload.aiGeneratedValue !== undefined
            ? namePayload.aiGeneratedValue
            : isAiGenerated
                ? (namePayload.aiGeneratedValue ?? existingObject?.name?.aiGeneratedValue ?? null)
                : null;
        const objectIdValue = objectIdPayload.value ?? existingObject?.objectId?.value ?? null;
        const objectIdAiGeneratedValue = objectIdPayload.aiGeneratedValue !== undefined
            ? objectIdPayload.aiGeneratedValue
            : isAiGenerated
                ? (namePayload.aiGeneratedValue ?? existingObject?.objectId?.aiGeneratedValue ?? null)
                : null;
        // Preserve position/rotation if not provided
        const position = obj.position !== undefined && obj.position !== null
            ? this.createVector3EditableField(obj.position)
            : existingObject?.position || {
                x: this.createEditableField(0, false),
                y: this.createEditableField(0, false),
                z: this.createEditableField(0, false),
            };
        const rotation = obj.rotation !== undefined && obj.rotation !== null
            ? this.createVector3EditableField(obj.rotation)
            : existingObject?.rotation || {
                x: this.createEditableField(0, false),
                y: this.createEditableField(0, false),
                z: this.createEditableField(0, false),
            };
        const sizeValue = obj.size !== undefined ? obj.size : (existingObject?.size?.value ?? null);
        return {
            name: this.createEditableField(nameValue, isAiGenerated, nameAiGeneratedValue),
            objectId: this.createEditableField(objectIdValue, isAiGenerated, objectIdAiGeneratedValue),
            position: position || {
                x: this.createEditableField(0, false),
                y: this.createEditableField(0, false),
                z: this.createEditableField(0, false),
            },
            rotation: rotation || {
                x: this.createEditableField(0, false),
                y: this.createEditableField(0, false),
                z: this.createEditableField(0, false),
            },
            size: this.createEditableField(sizeValue, false),
        };
    }
    /**
     * Builds editable string fields for an investigation.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
     */
    static buildStringFields(builder, dto, aiAssistantFlow) {
        this.buildEditableFieldsWithMapping(builder, dto, this.STRING_FIELD_MAPPINGS, (value) => hasStringValue(value), aiAssistantFlow);
    }
    /**
     * Builds editable number fields for an investigation.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
     */
    static buildNumberFields(builder, dto, aiAssistantFlow) {
        this.buildEditableFieldsWithMapping(builder, dto, this.NUMBER_FIELD_MAPPINGS, (value) => hasNumberValue(value), aiAssistantFlow);
    }
    /**
     * Builds the `steps` field for an investigation, with special handling for an array of `IEditableField<IStep>`.
     * Converts simple steps from API input into EditableField-wrapped steps for database storage.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data, including steps.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
     */
    static buildStepsField(builder, dto, aiAssistantFlow) {
        if (hasArrayValue(dto.steps)) {
            builder.steps = this.buildEditableStepsFromSimpleSteps(dto.steps, aiAssistantFlow);
        }
    }
    /**
     * Builds editable array fields for an investigation.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data, including arrays.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
     */
    static buildArrayFields(builder, dto, aiAssistantFlow) {
        // Handle objects specially - convert ISimpleObject[] to IObject[]
        if (dto.objects && hasArrayValue(dto.objects)) {
            builder.objects = this.convertSimpleObjectsToEditableObjects(dto.objects, aiAssistantFlow);
        }
        // Handle other array fields with generic mapping
        const otherArrayMappings = this.ARRAY_FIELD_MAPPINGS.filter((mapping) => mapping.dtoField !== "objects");
        if (otherArrayMappings.length > 0) {
            this.buildEditableFieldsWithMapping(builder, dto, otherArrayMappings, (value) => hasArrayValue(value), aiAssistantFlow);
        }
    }
    /**
     * Converts simple step definitions into editable step models.
     * Supports both simple values and {value, aiGeneratedValue} format.
     * Preserves existing values when update fields are null/undefined.
     * @param {Array} simpleSteps - Steps received from API (can be IStep[] or new format with {value, aiGeneratedValue}).
     * @param {boolean} aiAssistantFlow - Whether the data should be marked as AI-generated.
     * @param {IInvestigationBaseStepModel[]} existingSteps - Optional existing steps to preserve values from.
     * @returns {IInvestigationBaseStepModel[]} Editable steps ready for persistence.
     */
    static buildEditableStepsFromSimpleSteps(simpleSteps, aiAssistantFlow, existingSteps = []) {
        if (!Array.isArray(simpleSteps)) {
            return [];
        }
        return simpleSteps.map((simpleStep, index) => this.createEditableStep(simpleStep ?? null, aiAssistantFlow, existingSteps[index] || null));
    }
    /**
     * Extracts value and aiGeneratedValue from a payload that can be either a simple value or {value, aiGeneratedValue} object.
     * @param {unknown} input - The input value (can be simple value or object with value/aiGeneratedValue).
     * @returns {object} Object with value, aiGeneratedValue, and hasAiGeneratedValue flag.
     */
    static extractEditablePayload(input) {
        if (input && typeof input === "object" && !Array.isArray(input)) {
            const candidate = input;
            if ("value" in candidate || "aiGeneratedValue" in candidate) {
                return {
                    value: candidate.value,
                    aiGeneratedValue: candidate.aiGeneratedValue,
                    hasAiGeneratedValue: Object.prototype.hasOwnProperty.call(candidate, "aiGeneratedValue"),
                };
            }
        }
        return {
            value: input,
            hasAiGeneratedValue: false,
        };
    }
    /**
     * Creates an editable step model from a simple step.
     * Supports both simple values and {value, aiGeneratedValue} format for backward compatibility.
     * Preserves existing values when update fields are null/undefined.
     * @param {object | null} simpleStep - Step data from the API payload.
     * @param {boolean} aiAssistantFlow - Whether the data should be marked as AI-generated.
     * @param {IInvestigationBaseStepModel | null} existingStep - Optional existing step to preserve values from.
     * @returns {IInvestigationBaseStepModel} Editable step structure.
     */
    static createEditableStep(simpleStep, aiAssistantFlow, existingStep = null) {
        const wrappedStep = {};
        // Helper to create editable field from payload (supports both formats)
        // Preserves existing values when new values are null/undefined
        const createField = (fieldValue, fieldName, defaultVal = null) => {
            const payload = this.extractEditablePayload(fieldValue);
            const existingField = existingStep?.[fieldName];
            // Use new value if provided, otherwise preserve existing value, otherwise use default
            const value = payload.value !== undefined && payload.value !== null
                ? payload.value
                : (existingField?.value ?? defaultVal);
            // Use new aiGeneratedValue if explicitly provided, otherwise preserve existing, otherwise null
            const aiGeneratedValue = payload.hasAiGeneratedValue
                ? payload.aiGeneratedValue
                : (payload.value ?? existingField?.aiGeneratedValue ?? null);
            return this.createEditableField(value, aiAssistantFlow, aiGeneratedValue);
        };
        if (simpleStep?.title !== undefined || existingStep?.title) {
            wrappedStep.title = createField(simpleStep?.title, "title", "");
        }
        if (simpleStep?.desiredOutcome !== undefined || existingStep?.desiredOutcome) {
            wrappedStep.desiredOutcome = createField(simpleStep?.desiredOutcome, "desiredOutcome", "");
        }
        if (simpleStep?.alternativeOutcome !== undefined || existingStep?.alternativeOutcome) {
            wrappedStep.alternativeOutcome = createField(simpleStep?.alternativeOutcome, "alternativeOutcome", "");
        }
        if (simpleStep?.descriptionEn !== undefined || existingStep?.descriptionEn) {
            wrappedStep.descriptionEn = createField(simpleStep?.descriptionEn, "descriptionEn", "");
        }
        wrappedStep.skippable = createField(simpleStep?.skippable, "skippable", true);
        wrappedStep.skippable_after_marking = createField(simpleStep?.skippable_after_marking, "skippable_after_marking", true);
        return wrappedStep;
    }
    /**
     * Converts an array of `ISimpleObject` to an array of `IObjectBase` with an EditableField structure.
     * @category Services
     * @param {ISimpleObject[]} simpleObjects - The array of simple objects to convert.
     * @param {boolean} aiAssistantFlow - Whether this conversion is part of an AI-assisted creation.
     * @returns {IObjectBase[]} An array of `IObjectBase` objects with EditableField-wrapped properties.
     */
    static convertSimpleObjectsToEditableObjects(simpleObjects, aiAssistantFlow) {
        return simpleObjects.map((simpleObject) => ({
            name: this.createEditableField(simpleObject.name, aiAssistantFlow),
            objectId: this.createEditableField(simpleObject.objectId, aiAssistantFlow),
            position: {
                x: this.createEditableField(simpleObject.position?.x ?? 0, aiAssistantFlow),
                y: this.createEditableField(simpleObject.position?.y ?? 0, aiAssistantFlow),
                z: this.createEditableField(simpleObject.position?.z ?? 0, aiAssistantFlow),
            },
            rotation: {
                x: this.createEditableField(simpleObject.rotation?.x ?? 0, aiAssistantFlow),
                y: this.createEditableField(simpleObject.rotation?.y ?? 0, aiAssistantFlow),
                z: this.createEditableField(simpleObject.rotation?.z ?? 0, aiAssistantFlow),
            },
            size: this.createEditableField(simpleObject.size ?? 1, aiAssistantFlow),
        }));
    }
    /**
     * Type-safe helper to build editable fields using field mappings.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
     * @param {Array<IFieldMapping>} fieldMappings - An array of mappings between DTO fields and investigation fields.
     * @param {Function} hasValueFunction - A function to check if a value is valid for inclusion.
     * @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
     */
    static buildEditableFieldsWithMapping(builder, dto, fieldMappings, hasValueFunction, aiAssistantFlow) {
        for (const mapping of fieldMappings) {
            const value = dto[mapping.dtoField];
            if (hasValueFunction(value)) {
                // Use the mapped investigation field name for type safety
                builder[mapping.investigationField] = this.createEditableField(value, aiAssistantFlow);
            }
        }
    }
    /**
     * Builds metadata and initializes observers for an investigation.
     * @category Services
     * @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
     * @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
     * @param {boolean} _aiAssistantFlow - Indicates if this is part of an AI-assisted creation (not used in this method).
     */
    static buildMetadata(builder, dto, _aiAssistantFlow) {
        // Observers (always included, even if empty !)
        // Metadata (always included!)
        builder.metadata = {
            dateOfDevelopment: null,
            dateOfDevelopmentDelivery: null,
            dateOfPublishing: null,
            author: dto?.author || null,
            editors: [],
            views: 0,
            sourceInvestigation: null,
            dateOfCreation: new Date(),
            dateModified: new Date(),
        };
    }
    /**
     * Creates a standard EditableField structure based on `aiAssistantFlow`.
     * @category Services
     * @param {unknown} value - The value to store in the EditableField.
     * @param {boolean} aiAssistantFlow - Whether this is part of an AI-assisted creation.
     * @param {unknown | null} aiGeneratedValue - Optional AI-generated value to store separately.
     * @returns {IEditableField<unknown>} An EditableField object containing the provided value.
     */
    static createEditableField(value, aiAssistantFlow, aiGeneratedValue = null) {
        if (aiAssistantFlow) {
            // AI-assisted flow: humanEdited = false, aiEditable = true, aiGeneratedValue = value (or provided)
            // TODO think if it is appropriate to save aiGeneratedValue for the first time while creation !
            return {
                value,
                aiGeneratedValue: aiGeneratedValue !== null ? aiGeneratedValue : value, // Use provided or same as current value for revert functionality
                humanEdited: false,
                aiEditable: true,
                isContradicting: false,
                contradictionReason: null,
                updatedBy: null,
            };
        }
        else {
            // Human-created flow: humanEdited = true, aiEditable = false, aiGeneratedValue = provided value or null
            return {
                value,
                aiGeneratedValue: aiGeneratedValue ?? null,
                humanEdited: true,
                aiEditable: false,
                isContradicting: false,
                contradictionReason: null,
                updatedBy: null,
            };
        }
    }
}