"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAiAgentReviewItemFingerprint = exports.getAiAgentReviewItemFingerprintScope = exports.aiAgentReviewClassifierJudgeOutputSchema = exports.aiAgentJudgeProjectContextEntrySchema = exports.shouldReopenReviewItem = exports.getAiAgentConfigSnapshotHash = void 0;
const zod_1 = require("zod");
const projectContext_1 = require("./projectContext");
const normalizeForHash = (value) => {
    if (Array.isArray(value)) {
        return value
            .map(normalizeForHash)
            .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
    }
    if (value && typeof value === 'object') {
        return Object.keys(value)
            .sort()
            .reduce((acc, key) => {
            const normalized = normalizeForHash(value[key]);
            if (normalized !== undefined) {
                acc[key] = normalized;
            }
            return acc;
        }, {});
    }
    return value;
};
const hashStringToBase36 = (input, modulus, base) => {
    let hash = 0;
    for (let i = 0; i < input.length; i += 1) {
        hash = (hash * base + input.charCodeAt(i)) % modulus;
    }
    return hash.toString(36).padStart(8, '0');
};
const hashCanonicalJson = (input) => [
    hashStringToBase36(input, 2147483647, 31),
    hashStringToBase36(input, 2147483629, 37),
].join('');
const getAiAgentConfigSnapshotHash = (snapshot) => {
    const canonicalJson = JSON.stringify(normalizeForHash(snapshot));
    return `ai_agent_review_config_snapshot:${hashCanonicalJson(canonicalJson)}`;
};
exports.getAiAgentConfigSnapshotHash = getAiAgentConfigSnapshotHash;
// A recurring finding reopens a closed item so its "fixed → regressed" history
// stays on one card — except items dismissed as expected behavior, which stay
// dismissed rather than clawing back open on every recurrence.
const shouldReopenReviewItem = (status, dismissedReason) => {
    if (status === 'resolved') {
        return true;
    }
    if (status === 'dismissed') {
        return dismissedReason !== 'expected_behavior';
    }
    return false;
};
exports.shouldReopenReviewItem = shouldReopenReviewItem;
const aiAgentConfigurationSettingSchema = zod_1.z.enum([
    'instructions',
    'knowledge_documents',
    'data_access',
    'self_improvement',
    'sql_mode',
    'mcp_servers',
    'explore_tags',
    'space_access',
    'user_or_group_access',
    'unknown',
]);
const aiAgentJudgeTargetRefSchema = zod_1.z.object({
    type: zod_1.z.enum([
        'model',
        'explore',
        'join',
        'dimension',
        'metric',
        'additional_dimension',
        'required_filter',
        'ai_hint',
        'agent',
        'agent_config',
        'product_capability',
        'runtime',
    ]),
    label: zod_1.z.string(),
    modelName: zod_1.z.string().nullable(),
    fieldName: zod_1.z.string().nullable(),
    setting: aiAgentConfigurationSettingSchema.nullable(),
    key: zod_1.z.string().nullable(),
});
// Structured project_context living-document entry the judge emits for
// project_context findings. `id` is required when op === 'update' (the entry
// it reviewed was already injected into the turn, so it can reference the id).
exports.aiAgentJudgeProjectContextEntrySchema = zod_1.z
    .object({
    op: zod_1.z.enum(['create', 'update']),
    id: zod_1.z.string().min(1).nullable(),
    kind: zod_1.z.enum(projectContext_1.projectContextEntryKinds),
    content: zod_1.z.string(),
    terms: zod_1.z.array(zod_1.z.string()),
    objects: zod_1.z.array(zod_1.z.string()),
})
    .superRefine((entry, ctx) => {
    if (entry.op === 'update' && !entry.id) {
        ctx.addIssue({
            code: zod_1.z.ZodIssueCode.custom,
            message: 'id is required when op is update',
            path: ['id'],
        });
    }
});
// Signals that describe a healthy turn — mutually exclusive with promotion.
const NOT_A_FAILURE_SIGNALS = new Set([
    'normal_refinement',
    'output_shape_correction',
    'new_question',
    'acceptance_or_continuation',
]);
exports.aiAgentReviewClassifierJudgeOutputSchema = zod_1.z
    .object({
    signal: zod_1.z.enum([
        'normal_refinement',
        'implicit_correction',
        'explicit_dispute',
        'retry_after_failure',
        'output_shape_correction',
        'new_question',
        'acceptance_or_continuation',
        'product_capability_request',
        'human_intervention',
        'ambiguous',
    ]),
    implicitSignalSources: zod_1.z.array(zod_1.z.enum([
        'next_user_correction',
        'next_user_dispute',
        'next_user_retry',
        'output_shape_correction',
        'tool_error',
        'assistant_no_answer',
        'product_capability_request',
        'human_intervention',
    ])),
    confidence: zod_1.z.enum(['low', 'medium', 'high']),
    promotedToFinding: zod_1.z.boolean(),
    promotionReason: zod_1.z.string().nullable(),
    matchedExistingItemKey: zod_1.z.string().nullable(),
    primaryRootCause: zod_1.z.enum([
        'semantic_layer',
        'project_context',
        'agent_configuration',
        'product_capability',
        'runtime_reliability',
        'feedback_quality',
        'not_a_failure',
        'ambiguous',
    ]),
    secondaryRootCauses: zod_1.z.array(zod_1.z.enum([
        'semantic_layer',
        'project_context',
        'agent_configuration',
        'product_capability',
        'runtime_reliability',
        'feedback_quality',
        'not_a_failure',
        'ambiguous',
    ])),
    subcategories: zod_1.z.array(zod_1.z.string()),
    fixTargets: zod_1.z.array(zod_1.z.enum([
        'semantic_yaml_patch',
        'project_context_rule',
        'agent_configuration_change',
        'dbt_modeling_ticket',
        'semantic_layer_ticket',
        'product_capability_ticket',
        'runtime_reliability_ticket',
        'feedback_needed',
        'no_action',
    ])),
    targetRefs: zod_1.z.array(aiAgentJudgeTargetRefSchema),
    agentConfigurationSettings: zod_1.z.array(aiAgentConfigurationSettingSchema),
    ownerType: zod_1.z.enum([
        'semantic_layer_owner',
        'agent_admin',
        'product',
        'support',
        'unknown',
    ]),
    evidenceExcerpts: zod_1.z.array(zod_1.z.object({
        source: zod_1.z.enum([
            'user_prompt',
            'assistant_answer',
            'next_user_prompt',
            'conversation_context',
            'tool_call',
            'tool_result',
            'agent_config',
        ]),
        text: zod_1.z.string(),
        redacted: zod_1.z.boolean(),
    })),
    recommendation: zod_1.z
        .object({
        actionType: zod_1.z.enum([
            'update_semantic_yaml',
            'update_agent_instructions',
            'add_knowledge_document',
            'enable_data_access',
            'enable_sql_mode',
            'enable_self_improvement',
            'configure_mcp_server',
            'adjust_explore_tags',
            'update_access',
            'route_to_product_work',
            'request_more_evidence',
            'no_action',
        ]),
        title: zod_1.z.string(),
        rationale: zod_1.z.string(),
        targetRefs: zod_1.z.array(aiAgentJudgeTargetRefSchema),
    })
        .nullable(),
    reviewItem: zod_1.z.object({
        title: zod_1.z.string(),
        description: zod_1.z.string(),
    }),
    projectContextEntry: exports.aiAgentJudgeProjectContextEntrySchema.nullable(),
})
    .superRefine((output, ctx) => {
    if (output.promotedToFinding &&
        output.primaryRootCause === 'not_a_failure') {
        ctx.addIssue({
            code: zod_1.z.ZodIssueCode.custom,
            message: 'promotedToFinding must be false when primaryRootCause is not_a_failure',
            path: ['promotedToFinding'],
        });
    }
    if (output.promotedToFinding &&
        NOT_A_FAILURE_SIGNALS.has(output.signal)) {
        ctx.addIssue({
            code: zod_1.z.ZodIssueCode.custom,
            message: `promotedToFinding must be false when signal is ${output.signal}; promoted findings need a failure signal`,
            path: ['signal'],
        });
    }
    if (output.promotedToFinding &&
        output.recommendation?.actionType === 'no_action') {
        ctx.addIssue({
            code: zod_1.z.ZodIssueCode.custom,
            message: 'promoted findings must carry an actionable recommendation, not no_action',
            path: ['recommendation', 'actionType'],
        });
    }
});
const requireScopeValue = (value, field, rootCause) => {
    if (!value) {
        throw new Error(`${field} is required for ${rootCause} fingerprints`);
    }
    return value;
};
const getAiAgentReviewItemFingerprintScope = (input) => {
    switch (input.primaryRootCause) {
        case 'semantic_layer':
        case 'project_context':
            return {
                type: 'project',
                organizationUuid: input.organizationUuid,
                projectUuid: requireScopeValue(input.projectUuid, 'projectUuid', input.primaryRootCause),
            };
        case 'agent_configuration':
            return {
                type: 'agent',
                organizationUuid: input.organizationUuid,
                agentUuid: requireScopeValue(input.agentUuid, 'agentUuid', input.primaryRootCause),
            };
        default:
            return input.projectUuid
                ? {
                    type: 'project',
                    organizationUuid: input.organizationUuid,
                    projectUuid: input.projectUuid,
                }
                : {
                    type: 'organization',
                    organizationUuid: input.organizationUuid,
                };
    }
};
exports.getAiAgentReviewItemFingerprintScope = getAiAgentReviewItemFingerprintScope;
// Only runtime_reliability is a true per-conversation incident.
const MODE_A_INCIDENT_ROOT_CAUSES = new Set([
    'runtime_reliability',
]);
// Durable project objects whose dedup identity is the affected object, across
// threads. agent_configuration / product_capability already dedup cross-thread
// on their own scope via the fallback, so they are intentionally excluded.
const MODE_B_OBJECT_ROOT_CAUSES = new Set([
    'semantic_layer',
    'project_context',
]);
// Field-level refs identify the actual broken object; model/explore/join are
// context the judge names inconsistently across turns. We key on the field leaf
// names (ignoring modelName) so the same broken field collapses even when the
// judge varies the model or the surrounding context refs.
const FIELD_LEVEL_REF_TYPES = new Set([
    'metric',
    'dimension',
    'additional_dimension',
    'required_filter',
    'ai_hint',
]);
// Lightdash field ids are `{model}_{field}`, and the judge uses the qualified
// and unqualified forms interchangeably. Canonicalize to the unqualified field
// so `orders_total_order_amount` and `total_order_amount` are one object.
const stripModelPrefix = (name, modelName) => name.startsWith(`${modelName}_`) ? name.slice(modelName.length + 1) : name;
const getRefLeafName = (ref) => {
    switch (ref.type) {
        case 'metric':
            return stripModelPrefix(ref.metricName, ref.modelName);
        case 'dimension':
        case 'additional_dimension':
            return stripModelPrefix(ref.dimensionName, ref.modelName);
        case 'required_filter':
            return stripModelPrefix(ref.fieldName, ref.modelName);
        case 'ai_hint':
            return ref.targetName;
        case 'explore':
            return ref.exploreName;
        case 'join':
            return ref.joinName;
        case 'model':
            return ref.modelName;
        default:
            return null;
    }
};
// Identity is the PRIMARY object the finding is about — the first field-level
// ref. The judge lists the broken object first and varies the rest (related
// metrics it suggests, surrounding explores), so keying on the whole set splits
// findings that are really about the same object. Falls back to the first ref
// of any kind, then empty.
const getNormalizedObjectKey = (input) => {
    const primaryRef = input.targetRefs.find((ref) => FIELD_LEVEL_REF_TYPES.has(ref.type)) ??
        input.targetRefs[0];
    const leaf = primaryRef ? getRefLeafName(primaryRef) : null;
    return leaf ? leaf.trim().toLowerCase() : '';
};
const getAiAgentReviewItemFingerprintPayload = (input) => {
    // Mode A — incident: one conversation went sideways. Collapse every
    // runtime finding in the thread onto one item; which object each retry
    // touched, and the exact error, are noise.
    if (MODE_A_INCIDENT_ROOT_CAUSES.has(input.primaryRootCause) &&
        input.threadUuid) {
        return {
            kind: 'incident',
            threadUuid: input.threadUuid,
            primaryRootCause: input.primaryRootCause,
        };
    }
    // Mode B — object: a durable project object that recurs across threads.
    // Identity is the normalized object from targetRefs, not the LLM's phrasing.
    if (MODE_B_OBJECT_ROOT_CAUSES.has(input.primaryRootCause)) {
        return {
            kind: 'object',
            scope: (0, exports.getAiAgentReviewItemFingerprintScope)(input),
            primaryRootCause: input.primaryRootCause,
            normalizedObject: getNormalizedObjectKey(input),
        };
    }
    return {
        kind: 'scope',
        scope: (0, exports.getAiAgentReviewItemFingerprintScope)(input),
        primaryRootCause: input.primaryRootCause,
        subcategories: input.subcategories,
        fixTargets: input.fixTargets,
        targetRefs: input.targetRefs,
        agentConfigurationSettings: input.agentConfigurationSettings,
        capabilityKey: input.capabilityKey,
    };
};
const getAiAgentReviewItemFingerprint = (input) => {
    const canonicalJson = JSON.stringify(normalizeForHash(getAiAgentReviewItemFingerprintPayload(input)));
    return `ai_agent_review_item:${hashCanonicalJson(canonicalJson)}`;
};
exports.getAiAgentReviewItemFingerprint = getAiAgentReviewItemFingerprint;
//# sourceMappingURL=aiAgentReviewClassifierTypes.js.map