```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[] = [];

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

    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.");
    }
  });

  const phrases = rules.map((rule) => rule.phrase as string);
  if (new Set(phrases).size !== phrases.length) {
    throw new Error("Two rules cannot share a phrase.");
  }

  answers.forEach((answer) => {
    if (typeof answer !== 'string') {
      throw new Error("Each answer must be a string.");
    }

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

    rules.some((rule) => {
      const code = rule.code as string;
      const phrase = rule.phrase as string;

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

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

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

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