diff --git a/chat/reports.mjs b/chat/reports.mjs
new file mode 100644
index 0000000..08b2fb6
--- /dev/null
+++ b/chat/reports.mjs
@@ -0,0 +1,210 @@
+import { randomBytes } from 'crypto';
+import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, copyFileSync, renameSync } from 'fs';
+import { dirname, join } from 'path';
+import { REPORTS_DIR, REPORTS_META_FILE } from '../lib/config.mjs';
+
+// ---- Persistence ----
+
+function loadMeta() {
+ try {
+ if (!existsSync(REPORTS_META_FILE)) return [];
+ return JSON.parse(readFileSync(REPORTS_META_FILE, 'utf8'));
+ } catch {
+ return [];
+ }
+}
+
+function saveMeta(reports) {
+ const dir = dirname(REPORTS_META_FILE);
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
+ const tmp = REPORTS_META_FILE + '.tmp.' + process.pid;
+ writeFileSync(tmp, JSON.stringify(reports, null, 2), 'utf8');
+ renameSync(tmp, REPORTS_META_FILE);
+}
+
+function ensureReportsDir() {
+ if (!existsSync(REPORTS_DIR)) mkdirSync(REPORTS_DIR, { recursive: true });
+}
+
+// ---- HTML Validation (hard test) ----
+
+/**
+ * Validate an HTML file before accepting it as a report.
+ * Throws descriptive errors if validation fails.
+ */
+function validateHtml(filePath) {
+ const html = readFileSync(filePath, 'utf-8');
+
+ // Check 1: File not empty
+ if (!html.trim()) {
+ throw new Error('HTML file is empty');
+ }
+
+ // Check 2: Basic HTML structure tags
+ if (!/<(html|body|div|article|section|h[1-6]|p|table|ul|ol)/i.test(html)) {
+ throw new Error('HTML file lacks basic structure tags (expected at least one of: html, body, div, article, section, h1-h6, p, table, ul, ol)');
+ }
+
+ // Check 3: Simple tag balance check for critical tags
+ const tagErrors = checkTagBalance(html);
+ if (tagErrors.length > 0) {
+ throw new Error(`HTML structure errors:\n${tagErrors.join('\n')}`);
+ }
+
+ // Check 4: Minimum text content
+ const textContent = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
+ if (textContent.length < 50) {
+ throw new Error(`HTML content too short (${textContent.length} chars of text, minimum 50)`);
+ }
+
+ return { charCount: html.length, textLength: textContent.length };
+}
+
+/**
+ * Simple tag balance checker. Returns an array of error strings.
+ * Only checks block-level / important tags — not self-closing or inline.
+ */
+function checkTagBalance(html) {
+ const errors = [];
+ // Remove comments, scripts, styles, and self-closing tags before checking
+ let cleaned = html
+ .replace(//g, '')
+ .replace(/
+