Implements the parallel.md workstreams (Agents A-E) toward REFACTOR_GOAL.md. Phase 1 — app factory + blueprints + /api/v1: - app.py -> create_app() factory (no module-level app); entrypoints updated - routes/ (auth, pages, settings, admin, assets) + blueprints/api.py at /api/v1 - config.py / extensions.py / security.py extracted; services/ layer added - endpoint names preserved so template url_for() calls keep resolving (static check: all 27 template url_for endpoints are defined routes) Phase 2 — one pluggable filter system: - filter_pipeline/registry.py: @register_stage / @register_plugin + discover_modules - engine._init_stages() instantiates registered stages (no hardcoded dict); process_batch is AI-aware: only short-circuits to the AI-disabled path when a filterset's stages declare requires_ai, so offline filtersets run with AI off - BaseFilterPlugin gets a consumer (stages/plugins.py); Keyword/Quality re-enabled via filter_config.json plugins config - comment tree modes ported to stages/comment_filter.py + shared rules.py; wired into /api/v1/posts/<uuid> and /api/v1/comments/<uuid> via FilterEngine.filter_comments() (fails open) - offline quality_filter filterset exercises plugins+ranker without AI - legacy filter_lib / comment_lib / html_generation_lib / generate_html / active_html path deleted Phase 3 prep — pluggable fetchers + Postgres models: - Post / Comment SQLAlchemy models added to models.py - migrate_content_to_db.py backfill (idempotent by uuid, batched, --dry-run) - platforms/ fetcher registry (extension point) - live reads/writes still go through PostService (disk JSON); cutover deferred Phase 6 — test harness: - pytest.ini + tests/ (conftest with in-memory SQLite fixture, no Postgres; stubbed polling/filter singletons) - test_app_factory.py (route registration, no module-level app), test_api_contracts.py (posts/post_detail/comments/filters shape with monkeypatched post_service + get_filter_engine), test_filter_pipeline.py + test_plugin_contract.py (Flask-free; validated locally 12/12 incl. drop-in stage/plugin discovered with zero core edits) Other: .gitignore added (__pycache__, data/, secrets); pytest in requirements. Verification: py_compile clean across the project; the Flask-free filter-pipeline and plugin-contract tests pass locally. App-factory / API-contract tests need deps+docker to run; runtime flask routes / Auth0-repeated-create_app also gated on docker. Co-Authored-By: Claude <noreply@anthropic.com>
83 lines
2.8 KiB
JSON
83 lines
2.8 KiB
JSON
{
|
|
"safe_content": {
|
|
"description": "Filter for safe, family-friendly content",
|
|
"post_rules": {
|
|
"moderation.flags.is_safe": {"equals": true},
|
|
"moderation.content_safety.violence": {"max": 0.3},
|
|
"moderation.content_safety.sexual_content": {"max": 0.2},
|
|
"moderation.content_safety.hate_speech": {"max": 0.1},
|
|
"moderation.warnings": {"excludes": ["graphic_content", "mature_content"]}
|
|
},
|
|
"comment_rules": {
|
|
"moderation.flags.is_safe": {"equals": true},
|
|
"moderation.content_safety.harassment": {"max": 0.3},
|
|
"moderation.content_safety.profanity": {"max": 0.4}
|
|
},
|
|
"comment_filter_mode": "tree_pruning"
|
|
},
|
|
"tech_only": {
|
|
"description": "Technology and programming content only",
|
|
"post_rules": {
|
|
"platform": {"in": ["hackernews", "reddit", "lobsters", "stackexchange"]},
|
|
"source": {"in": ["programming", "python", "javascript", "technology", "stackoverflow"]},
|
|
"moderation.topics": {"includes_any": [
|
|
{"topic": "technology", "confidence_min": 0.5},
|
|
{"topic": "programming", "confidence_min": 0.5},
|
|
{"topic": "software", "confidence_min": 0.5}
|
|
]}
|
|
},
|
|
"comment_rules": {
|
|
"moderation.flags.is_safe": {"equals": true}
|
|
},
|
|
"comment_filter_mode": "tree_pruning"
|
|
},
|
|
"high_quality": {
|
|
"description": "High quality posts and comments only",
|
|
"post_rules": {
|
|
"score": {"min": 10},
|
|
"moderation.quality_score": {"min": 0.6},
|
|
"moderation.readability.grade_level": {"max": 14},
|
|
"moderation.flags.is_safe": {"equals": true}
|
|
},
|
|
"comment_rules": {
|
|
"score": {"min": 2},
|
|
"moderation.quality_score": {"min": 0.5},
|
|
"moderation.flags.is_safe": {"equals": true}
|
|
},
|
|
"comment_filter_mode": "tree_pruning"
|
|
},
|
|
"no_filter": {
|
|
"description": "No filtering - all content passes",
|
|
"post_rules": {},
|
|
"comment_rules": {},
|
|
"comment_filter_mode": "individual"
|
|
},
|
|
"custom_example": {
|
|
"description": "Example showing various rule types",
|
|
"post_rules": {
|
|
"platform": {"equals": "reddit"},
|
|
"score": {"min": 5, "max": 10000},
|
|
"timestamp": {"after": 1640000000},
|
|
"author": {"not_in": ["deleted", "removed"]},
|
|
"moderation.sentiment.overall": {"in": ["positive", "neutral"]},
|
|
"moderation.language.detected": {"equals": "en"}
|
|
},
|
|
"comment_rules": {
|
|
"depth": {"max": 5},
|
|
"content": {"min_length": 10},
|
|
"moderation.flags.is_blocked": {"equals": false}
|
|
},
|
|
"comment_filter_mode": "tree_pruning"
|
|
},
|
|
"quality_filter": {
|
|
"description": "Offline quality + keyword filter (no AI required)",
|
|
"pipeline_stages": ["plugins", "ranker"],
|
|
"plugins": ["keyword", "quality"],
|
|
"post_rules": {},
|
|
"comment_rules": {
|
|
"content": {"min_length": 5}
|
|
},
|
|
"comment_filter_mode": "individual"
|
|
}
|
|
}
|