```ts
/**
 * Tidies an answer by lowering its case and turning every run of characters that are neither letters nor digits into one space,
 * then dropping any space left at either end. A rule takes a tidied answer when the rule's phrase words sit inside it as a consecutive
 * run of whole words.
 * @param rules - Array of mappings with "code" (non-empty string) and "phrase" (one or more runs of lowercase letters and digits joined by single spaces).
 * @param answers - List of written-in answers to be tidied and checked against the rules.
 * @returns A mapping with "tally", one entry per distinct code in the order the codes first appear among the rules,
 * each a mapping with "code" and "count" (how many answers that code took), and "loose", the tidied text of every uncoded answer
 * in the order the answers arrived, repeats and all.
 */
export function codeOpenAnswers(rules: Array<Record<string, unknown>>, answers: string[]): Record<string, unknown> {
    if (!Array.isArray(rules) || rules.length === 0) throw new Error('Rules must be a non-empty list');
    for (const rule of rules) {
        if (typeof rule !== 'object' || rule === null) throw new Error('Each rule must be a mapping');
        if (typeof rule.code !== 'string' || rule.code.trim() === '') throw new Error('Code must be a non-empty string');
        const phraseWords = rule.phrase.split(/\s+/);
        for (const word of phraseWords) {
            if (!/^[a-z0-9]+$/.test(word)) throw new Error('Phrase must consist only of lowercase letters and digits');
        }
    }

    const tally: Record<string, { code: string; count: number }> = {};
    const loose: string[] = [];

    for (const answer of answers) {
        if (typeof answer !== 'string') throw new Error('Each answer must be a string');

        const tidiedAnswer = answer.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();

        let matchedCode = null;
        for (const rule of rules) {
            if (tidiedAnswer.includes(rule.phrase)) {
                matchedCode = rule.code;
                break;
            }
        }

        if (matchedCode) {
            tally[matchedCode] = (tally[matchedCode] || { code: matchedCode, count: 0 });
            tally[matchedCode].count++;
        } else {
            loose.push(tidiedAnswer);
        }
    }

    return { tally, loose };
}
```