```ts
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.");
  }
  if (!Array.isArray(answers)) {
    throw new Error("Answers must be a list.");
  }

  const codeTally: { [code: string]: number } = {};
  const looseAnswers: string[] = [];
  const seenPhrases = new Set<string>();

  for (const rule of rules) {
    if (typeof rule !== 'object' || rule === null) {
      throw new Error("Each rule must be a mapping.");
    }
    const code = rule.code;
    const phrase = rule.phrase;

    if (typeof code !== 'string' || code.trim() === '') {
      throw new Error("Code must be a non-empty string.");
    }
    if (typeof phrase !== 'string' || !/^[a-z0-9]+(\s[a-z0-9]+)*$/.test(phrase)) {
      throw new Error("Phrase must be one or more runs of lowercase letters and digits joined by single spaces.");
    }
    if (seenPhrases.has(phrase)) {
      throw new Error("Two rules cannot share the same phrase.");
    }
    seenPhrases.add(phrase);
  }

  const tidyAnswer = (answer: string): string => {
    return answer.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
  };

  for (const answer of answers) {
    if (typeof answer !== 'string') {
      throw new Error("Each answer must be a string.");
    }
    const tidied = tidyAnswer(answer);
    let coded = false;

    for (const rule of rules) {
      const code = rule.code as string;
      const phrase = rule.phrase as string;

      if (tidied.includes(phrase)) {
        codeTally[code] = (codeTally[code] || 0) + 1;
        coded = true;
        break;
      }
    }

    if (!coded) {
      looseAnswers.push(tidied);
    }
  }

  const tally = Object.entries(codeTally).map(([code, count]) => ({ code, count }));

  return { tally, loose: looseAnswers };
}
```