Files
worldmonitor/scripts/_prediction-scoring.mjs
Elie Habib fe67111dc9 feat: harness engineering P0 - linting, testing, architecture docs (#1587)
* feat: harness engineering P0 - linting, testing, architecture docs

Add foundational infrastructure for agent-first development:

- AGENTS.md: agent entry point with progressive disclosure to deeper docs
- ARCHITECTURE.md: 12-section system reference with source-file refs and ownership rule
- Biome 2.4.7 linter with project-tuned rules, CI workflow (lint-code.yml)
- Architectural boundary lint enforcing forward-only dependency direction (lint-boundaries.mjs)
- Unit test CI workflow (test.yml), all 1083 tests passing
- Fixed 9 pre-existing test failures (bootstrap sync, deploy-config headers, globe parity, redis mocks, geometry URL, import.meta.env null safety)
- Fixed 12 architectural boundary violations (types moved to proper layers)
- Added 3 missing cache tier entries in gateway.ts
- Synced cache-keys.ts with bootstrap.js
- Renamed docs/architecture.mdx to "Design Philosophy" with cross-references
- Deprecated legacy docs/Docs_To_Review/ARCHITECTURE.md
- Harness engineering roadmap tracking doc

* fix: address PR review feedback on harness-engineering-p0

- countries-geojson.test.mjs: skip gracefully when CDN unreachable
  instead of failing CI on network issues
- country-geometry-overrides.test.mts: relax timing assertion
  (250ms -> 2000ms) for constrained CI environments
- lint-boundaries.mjs: implement the documented api/ boundary check
  (was documented but missing, causing false green)

* fix(lint): scan api/ .ts files in boundary check

The api/ boundary check only scanned .js/.mjs files, missing the 25
sebuf RPC .ts edge functions. Now scans .ts files with correct rules:
- Legacy .js: fully self-contained (no server/ or src/ imports)
- RPC .ts: may import server/ and src/generated/ (bundled at deploy),
  but blocks imports from src/ application code

* fix(lint): detect import() type expressions in boundary lint

- Move AppContext back to app/app-context.ts (aggregate type that
  references components/services/utils belongs at the top, not types/)
- Move HappyContentCategory and TechHQ to types/ (simple enums/interfaces)
- Boundary lint now catches import('@/layer') expressions, not just
  from '@/layer' imports
- correlation-engine imports of AppContext marked boundary-ignore
  (type-only imports of top-level aggregate)
2026-03-14 21:29:21 +04:00

83 lines
2.7 KiB
JavaScript

import predictionTags from './data/prediction-tags.json' with { type: 'json' };
export const EXCLUDE_KEYWORDS = predictionTags.excludeKeywords;
export const MEME_PATTERNS = [
/\b(lebron|kanye|oprah|swift|rogan|dwayne|kardashian|cardi\s*b)\b/i,
/\b(alien|ufo|zombie|flat earth)\b/i,
];
export const REGION_PATTERNS = {
america: /\b(us|u\.s\.|united states|america|trump|biden|congress|federal reserve|canada|mexico|brazil)\b/i,
eu: /\b(europe|european|eu|nato|germany|france|uk|britain|macron|ecb)\b/i,
mena: /\b(middle east|iran|iraq|syria|israel|palestine|gaza|saudi|yemen|houthi|lebanon)\b/i,
asia: /\b(china|japan|korea|india|taiwan|xi jinping|asean)\b/i,
latam: /\b(latin america|brazil|argentina|venezuela|colombia|chile)\b/i,
africa: /\b(africa|nigeria|south africa|ethiopia|sahel|kenya)\b/i,
oceania: /\b(australia|new zealand)\b/i,
};
export function isExcluded(title) {
const lower = title.toLowerCase();
return EXCLUDE_KEYWORDS.some(kw => lower.includes(kw));
}
export function isMemeCandidate(title, yesPrice) {
if (yesPrice >= 15) return false;
return MEME_PATTERNS.some(p => p.test(title));
}
export function tagRegions(title) {
return Object.entries(REGION_PATTERNS)
.filter(([, re]) => re.test(title))
.map(([region]) => region);
}
export function parseYesPrice(market) {
try {
const prices = JSON.parse(market.outcomePrices || '[]');
if (prices.length >= 1) {
const p = parseFloat(prices[0]);
if (!Number.isNaN(p) && p >= 0 && p <= 1) return +(p * 100).toFixed(1);
}
} catch {}
return null;
}
export function shouldInclude(m, relaxed = false) {
const minPrice = relaxed ? 5 : 10;
const maxPrice = relaxed ? 95 : 90;
if (m.yesPrice < minPrice || m.yesPrice > maxPrice) return false;
if (m.volume < 5000) return false;
if (isExcluded(m.title)) return false;
if (isMemeCandidate(m.title, m.yesPrice)) return false;
return true;
}
export function scoreMarket(m) {
const uncertainty = 1 - (2 * Math.abs(m.yesPrice - 50) / 100);
const vol = Math.log10(Math.max(m.volume, 1)) / Math.log10(10_000_000);
return (uncertainty * 0.6) + (Math.min(vol, 1) * 0.4);
}
export function isExpired(endDate) {
if (!endDate) return false;
const ms = Date.parse(endDate);
return Number.isFinite(ms) && ms < Date.now();
}
export function filterAndScore(candidates, tagFilter, limit = 25) {
let filtered = candidates.filter(m => !isExpired(m.endDate));
if (tagFilter) filtered = filtered.filter(tagFilter);
let result = filtered.filter(m => shouldInclude(m));
if (result.length < 15) {
result = filtered.filter(m => shouldInclude(m, true));
}
return result
.map(m => ({ ...m, regions: tagRegions(m.title) }))
.sort((a, b) => scoreMarket(b) - scoreMarket(a))
.slice(0, limit);
}