// pii-detector/core.ts — PII detection in spec text (SPEC-030)
import { createRequire } from 'node:module';
import { stripNonContractText } from '../text-signal-boundaries.js';
const require = createRequire(import.meta.url);
export const patterns = require('../../config/pii-patterns.json');
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
/**
 * Normalizes a field name for vocabulary lookup (lowercase, remove separators).
 */
function normalizeField(name) {
    return name.toLowerCase().replace(/[_\-\s.]/g, '');
}
/**
 * Determines if a field name matches any PII vocabulary term.
 * Returns the matched category or null.
 */
function matchVocabulary(normalizedField) {
    for (const [category, terms] of Object.entries(patterns.vocabulary)) {
        for (const term of terms) {
            if (normalizedField === term || normalizedField.includes(term)) {
                return category;
            }
        }
    }
    return null;
}
/**
 * Returns true if the model context is in the blacklist (non-user models like Product, Order).
 * In blacklisted model contexts, standard-looking field names like "name" are NOT PII.
 */
function isBlacklistedContext(modelContext) {
    const lower = modelContext.toLowerCase();
    return patterns.modelContextBlacklist.some((b) => lower.includes(b.toLowerCase()));
}
function isWhitelistedContext(modelContext) {
    const lower = modelContext.toLowerCase();
    return patterns.modelContextWhitelist.some((w) => lower.includes(w.toLowerCase()));
}
function hasFieldCue(contextWindow) {
    return /\b(field|fields|column|columns|property|properties|attribute|schema|model|entity|table|class|interface)\b/i.test(contextWindow);
}
function hasCollectionCue(contextWindow) {
    return /\b(collect|store|process|persist|save|record|provide|must have|includes?|contains?)\b/i.test(contextWindow);
}
/**
 * Determines if a detected PII field is a GDPR Art. 9 special category.
 */
function isSensitiveSpecial(category) {
    return category === 'sensitive-special' || category === 'health' || category === 'biometric';
}
/**
 * Determines if a field can be anonymized based on its category.
 */
function canAnonymize(category) {
    return category !== 'biometric' && category !== 'identity';
}
/**
 * Determines if a field can be pseudonymized based on its category.
 */
function canPseudonymize(category) {
    return category !== 'sensitive-special';
}
/**
 * Tries to infer model/entity context from surrounding text.
 */
function inferModelContext(contextWindow) {
    const modelMatch = /(?:model|entity|table|schema|class|interface)\s+["']?(\w+)["']?/i.exec(contextWindow);
    if (modelMatch?.[1]) {
        return modelMatch[1];
    }
    for (const w of patterns.modelContextWhitelist) {
        if (contextWindow.toLowerCase().includes(w.toLowerCase())) {
            return w;
        }
    }
    for (const b of patterns.modelContextBlacklist) {
        if (contextWindow.toLowerCase().includes(b.toLowerCase())) {
            return b;
        }
    }
    return '';
}
/**
 * Extracts candidate field names and model contexts from spec text.
 * Looks for patterns like "field X", "column X", the X field, model.X, etc.
 */
function extractCandidates(text) {
    const candidates = [];
    const cleaned = stripNonContractText(text);
    const lower = cleaned.toLowerCase();
    /* v8 ignore start */
    const words = cleaned.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) ?? [];
    /* v8 ignore stop */
    for (const word of words) {
        if (word.length < 3) {
            continue;
        }
        if (matchVocabulary(normalizeField(word)) === null) {
            continue;
        }
        const wordIndex = lower.indexOf(word.toLowerCase());
        const contextWindow = cleaned.substring(Math.max(0, wordIndex - 100), wordIndex + 100);
        const context = inferModelContext(contextWindow);
        if (isWhitelistedContext(context) ||
            (hasFieldCue(contextWindow) && !isBlacklistedContext(contextWindow)) ||
            (hasCollectionCue(contextWindow) && isWhitelistedContext(contextWindow))) {
            candidates.push({ field: word, context });
        }
    }
    return candidates;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Detects PII fields in a spec's title, description, and criteria text.
 *
 * Business rule: field `name` in a `Product` model is NOT PII,
 * but `name` in a `User` model IS PII.
 */
export function detectPIIInSpec(specTitle, specDescription, criteria) {
    const fullText = `${specTitle} ${specDescription} ${criteria}`;
    const candidates = extractCandidates(fullText);
    const found = [];
    const seen = new Set();
    for (const { field, context } of candidates) {
        const normalized = normalizeField(field);
        const category = matchVocabulary(normalized);
        if (!category) {
            continue;
        }
        if (context && isBlacklistedContext(context)) {
            continue;
        }
        const key = `${normalized}:${context.toLowerCase()}`;
        if (seen.has(key)) {
            continue;
        }
        seen.add(key);
        found.push({
            fieldName: field,
            modelContext: context || 'unknown',
            category,
            isSensitiveSpecial: isSensitiveSpecial(category),
            canBeAnonymized: canAnonymize(category),
            canBePseudonymized: canPseudonymize(category),
        });
    }
    const sensitiveSpecialCount = found.filter((f) => f.isSensitiveSpecial).length;
    const hasSensitiveSpecial = sensitiveSpecialCount > 0;
    const recommendedLegalBasis = hasSensitiveSpecial ? 'consent' : 'contract';
    const requiresConsentFlow = hasSensitiveSpecial || found.some((f) => f.category === 'behavioral');
    const requiresRightToErasure = found.length > 0;
    const requiresAuditTrail = found.length > 0;
    return {
        hasPII: found.length > 0,
        fields: found,
        sensitiveSpecialCount,
        recommendedLegalBasis,
        requiresConsentFlow,
        requiresRightToErasure,
        requiresAuditTrail,
    };
}
//# sourceMappingURL=core.js.map