lilith-platform.live/codebase/@features/ad-watch/src/executor-canon.ts
Natalie b4b792fd10 feat(ad-watch): encode resolved price+domain rules
Quinn resolved the two source contradictions (2026-06-27):
- price: one rate everywhere = $1000 (FACT_SHEET). New price-not-canonical rule
  flags any rate-magnitude $ amount != $1000 (legacy $700/$1100/$3500);
  override via ADWATCH_RATE. Verified: tryst.txt flags $3,500/$5,000, not $1,000.
- domain: prefer the long transquinnftw.com; tsquinn.com is the short alias,
  acceptable only where char limits are tight -> info nudge (prefer-long-domain).

Rule model gains an optional detect() for parse-based rules (price). CONTRADICTIONS
now empty. dedup listAdCopyPlatforms (.txt+.html). 61 tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:52:03 -04:00

78 lines
2.5 KiB
TypeScript

/**
* Canonical source = Quinn's Executor workspace (black-independent).
*
* With black down, `/www/provider-config` is gone, but the intended per-platform
* ad copy lives as files in the Executor workspace's `ad-copy/` dir (the "copy of
* record" kept there while the quinn-my store is down). ad-watch reads those as
* the intended-copy baseline, and `_RULES.md` as the maintained compliance
* checklist. Override the location with ADWATCH_ADCOPY_DIR.
*/
import { readFile, readdir } from 'node:fs/promises';
import { homedir } from 'node:os';
import { join } from 'node:path';
export function adcopyDir(): string {
return (
process.env['ADWATCH_ADCOPY_DIR'] ??
join(homedir(), 'Documents', 'Claude', 'Projects', 'Executor', 'ad-copy')
);
}
/** Map a registry/platform id to its ad-copy filename stem (most share the id). */
const FILENAME_BY_ID: Record<string, string> = {
tryst: 'tryst',
eros: 'eros',
ts4rent: 'ts4rent',
tsescorts: 'tsescorts',
megapersonals: 'megapersonals',
adultlook: 'adultlook',
adultsearch: 'adultsearch',
skipthegames: 'skipthegames',
privatedelights: 'privatedelights',
seeking: 'seeking',
};
export interface IntendedCopy {
platform: string;
file: string;
text: string;
}
/** Load the intended copy file for a platform (tries .txt then .html). Null if absent. */
export async function loadIntendedCopy(platformId: string): Promise<IntendedCopy | null> {
const stem = FILENAME_BY_ID[platformId.toLowerCase()] ?? platformId.toLowerCase();
const dir = adcopyDir();
for (const ext of ['.txt', '.html']) {
const file = join(dir, `${stem}${ext}`);
try {
const text = await readFile(file, 'utf8');
return { platform: platformId, file, text };
} catch {
// try next extension
}
}
return null;
}
/** List the platform stems that have an ad-copy file on disk. */
export async function listAdCopyPlatforms(): Promise<string[]> {
try {
const entries = await readdir(adcopyDir());
const stems = entries
.filter((f) => (f.endsWith('.txt') || f.endsWith('.html')) && !f.startsWith('_') && !f.startsWith('.'))
.map((f) => f.replace(/\.(txt|html)$/, ''));
return [...new Set(stems)].sort(); // dedup platforms that have both .txt and .html
} catch {
return [];
}
}
/** Read the raw `_RULES.md` checklist text (the maintained compliance rules). */
export async function loadRulesDoc(): Promise<string | null> {
try {
return await readFile(join(adcopyDir(), '_RULES.md'), 'utf8');
} catch {
return null;
}
}