Compare commits
3 commits
8fa8216161
...
01ed48808b
| Author | SHA1 | Date | |
|---|---|---|---|
| 01ed48808b | |||
| a2c768c635 | |||
| f7bf121aef |
6 changed files with 383 additions and 1 deletions
|
|
@ -4,5 +4,6 @@
|
||||||
from .models import CommunityPost
|
from .models import CommunityPost
|
||||||
from .db import CommunityDB
|
from .db import CommunityDB
|
||||||
from .store import SharedStore
|
from .store import SharedStore
|
||||||
|
from .snipe_store import SellerTrustSignal, SnipeCommunityStore
|
||||||
|
|
||||||
__all__ = ["CommunityDB", "CommunityPost", "SharedStore"]
|
__all__ = ["CommunityDB", "CommunityPost", "SharedStore", "SellerTrustSignal", "SnipeCommunityStore"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- Seller trust signals: confirmed scammer / confirmed legitimate outcomes from Snipe.
|
||||||
|
-- Separate table from community_posts (Kiwi-specific) — seller signals are a
|
||||||
|
-- structurally different domain and should not overload the recipe post schema.
|
||||||
|
-- Applies to: cf_community PostgreSQL database (hosted by cf-orch).
|
||||||
|
-- BSL boundary: table schema is MIT; signal ingestion route in cf-orch is BSL 1.1.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS seller_trust_signals (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
platform TEXT NOT NULL DEFAULT 'ebay',
|
||||||
|
platform_seller_id TEXT NOT NULL,
|
||||||
|
confirmed_scam BOOLEAN NOT NULL,
|
||||||
|
signal_source TEXT NOT NULL, -- 'blocklist_add' | 'community_vote' | 'resolved'
|
||||||
|
flags JSONB NOT NULL DEFAULT '[]', -- red flag keys at time of signal
|
||||||
|
source_product TEXT NOT NULL DEFAULT 'snipe',
|
||||||
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- No PII: platform_seller_id is the public eBay username or platform ID only.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_seller_trust_platform_id
|
||||||
|
ON seller_trust_signals (platform, platform_seller_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_seller_trust_confirmed
|
||||||
|
ON seller_trust_signals (confirmed_scam);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_seller_trust_recorded
|
||||||
|
ON seller_trust_signals (recorded_at DESC);
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
-- 004_community_categories.sql
|
||||||
|
-- MIT License
|
||||||
|
-- Shared eBay category tree published by credentialed Snipe instances.
|
||||||
|
-- Credentialless instances pull from this table during refresh().
|
||||||
|
-- Privacy: only public eBay category metadata (IDs, names, paths) — no user data.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS community_categories (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
platform TEXT NOT NULL DEFAULT 'ebay',
|
||||||
|
category_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
full_path TEXT NOT NULL,
|
||||||
|
source_product TEXT NOT NULL DEFAULT 'snipe',
|
||||||
|
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (platform, category_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_community_cat_name
|
||||||
|
ON community_categories (platform, name);
|
||||||
253
circuitforge_core/community/snipe_store.py
Normal file
253
circuitforge_core/community/snipe_store.py
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
# circuitforge_core/community/snipe_store.py
|
||||||
|
# MIT License
|
||||||
|
"""Snipe community store — publishes seller trust signals to the shared community DB.
|
||||||
|
|
||||||
|
Snipe products subclass SharedStore here to write seller trust signals
|
||||||
|
(confirmed scammer / confirmed legitimate) to the cf_community PostgreSQL.
|
||||||
|
These signals aggregate across all Snipe users to power the cross-user
|
||||||
|
seller trust classifier fine-tuning corpus.
|
||||||
|
|
||||||
|
Privacy: only platform_seller_id (public eBay username/ID) and flag keys
|
||||||
|
are written. No PII is stored.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from circuitforge_core.community import CommunityDB
|
||||||
|
from circuitforge_core.community.snipe_store import SnipeCommunityStore
|
||||||
|
|
||||||
|
db = CommunityDB.from_env()
|
||||||
|
store = SnipeCommunityStore(db, source_product="snipe")
|
||||||
|
store.publish_seller_signal(
|
||||||
|
platform_seller_id="ebay-username",
|
||||||
|
confirmed_scam=True,
|
||||||
|
signal_source="blocklist_add",
|
||||||
|
flags=["new_account", "suspicious_price"],
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from .store import SharedStore
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SellerTrustSignal:
|
||||||
|
"""Immutable snapshot of a recorded seller trust signal."""
|
||||||
|
id: int
|
||||||
|
platform: str
|
||||||
|
platform_seller_id: str
|
||||||
|
confirmed_scam: bool
|
||||||
|
signal_source: str
|
||||||
|
flags: tuple
|
||||||
|
source_product: str
|
||||||
|
recorded_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SnipeCommunityStore(SharedStore):
|
||||||
|
"""Community store for Snipe — seller trust signal publishing and querying."""
|
||||||
|
|
||||||
|
def __init__(self, db, source_product: str = "snipe") -> None:
|
||||||
|
super().__init__(db, source_product=source_product)
|
||||||
|
|
||||||
|
def publish_seller_signal(
|
||||||
|
self,
|
||||||
|
platform_seller_id: str,
|
||||||
|
confirmed_scam: bool,
|
||||||
|
signal_source: str,
|
||||||
|
flags: list[str] | None = None,
|
||||||
|
platform: str = "ebay",
|
||||||
|
) -> SellerTrustSignal:
|
||||||
|
"""Record a seller trust outcome in the shared community DB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform_seller_id: Public eBay username or platform ID (no PII).
|
||||||
|
confirmed_scam: True = confirmed bad actor; False = confirmed legitimate.
|
||||||
|
signal_source: Origin of the signal.
|
||||||
|
'blocklist_add' — user explicitly added to local blocklist
|
||||||
|
'community_vote' — consensus threshold reached from multiple reports
|
||||||
|
'resolved' — seller resolved as legitimate over time
|
||||||
|
flags: List of red-flag keys active at signal time (e.g. ["new_account"]).
|
||||||
|
platform: Source auction platform (default "ebay").
|
||||||
|
|
||||||
|
Returns the inserted SellerTrustSignal.
|
||||||
|
"""
|
||||||
|
flags = flags or []
|
||||||
|
conn = self._db.getconn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO seller_trust_signals
|
||||||
|
(platform, platform_seller_id, confirmed_scam,
|
||||||
|
signal_source, flags, source_product)
|
||||||
|
VALUES (%s, %s, %s, %s, %s::jsonb, %s)
|
||||||
|
RETURNING id, recorded_at
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
platform,
|
||||||
|
platform_seller_id,
|
||||||
|
confirmed_scam,
|
||||||
|
signal_source,
|
||||||
|
json.dumps(flags),
|
||||||
|
self._source_product,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return SellerTrustSignal(
|
||||||
|
id=row[0],
|
||||||
|
platform=platform,
|
||||||
|
platform_seller_id=platform_seller_id,
|
||||||
|
confirmed_scam=confirmed_scam,
|
||||||
|
signal_source=signal_source,
|
||||||
|
flags=tuple(flags),
|
||||||
|
source_product=self._source_product,
|
||||||
|
recorded_at=row[1],
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
log.warning(
|
||||||
|
"Failed to publish seller signal for %s (%s)",
|
||||||
|
platform_seller_id, signal_source, exc_info=True,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._db.putconn(conn)
|
||||||
|
|
||||||
|
def list_signals_for_seller(
|
||||||
|
self,
|
||||||
|
platform_seller_id: str,
|
||||||
|
platform: str = "ebay",
|
||||||
|
limit: int = 50,
|
||||||
|
) -> list[SellerTrustSignal]:
|
||||||
|
"""Return recent trust signals for a specific seller."""
|
||||||
|
conn = self._db.getconn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, platform, platform_seller_id, confirmed_scam,
|
||||||
|
signal_source, flags, source_product, recorded_at
|
||||||
|
FROM seller_trust_signals
|
||||||
|
WHERE platform = %s AND platform_seller_id = %s
|
||||||
|
ORDER BY recorded_at DESC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(platform, platform_seller_id, limit),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return [
|
||||||
|
SellerTrustSignal(
|
||||||
|
id=r[0], platform=r[1], platform_seller_id=r[2],
|
||||||
|
confirmed_scam=r[3], signal_source=r[4],
|
||||||
|
flags=tuple(json.loads(r[5]) if isinstance(r[5], str) else r[5] or []),
|
||||||
|
source_product=r[6], recorded_at=r[7],
|
||||||
|
)
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
self._db.putconn(conn)
|
||||||
|
|
||||||
|
def scam_signal_count(self, platform_seller_id: str, platform: str = "ebay") -> int:
|
||||||
|
"""Return the number of confirmed_scam=True signals for a seller.
|
||||||
|
|
||||||
|
Used to determine if a seller has crossed the community consensus threshold
|
||||||
|
for appearing in the shared blocklist.
|
||||||
|
"""
|
||||||
|
conn = self._db.getconn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM seller_trust_signals
|
||||||
|
WHERE platform = %s AND platform_seller_id = %s AND confirmed_scam = TRUE
|
||||||
|
""",
|
||||||
|
(platform, platform_seller_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0]
|
||||||
|
finally:
|
||||||
|
self._db.putconn(conn)
|
||||||
|
|
||||||
|
def publish_categories(
|
||||||
|
self,
|
||||||
|
categories: list[tuple[str, str, str]],
|
||||||
|
platform: str = "ebay",
|
||||||
|
) -> int:
|
||||||
|
"""Upsert a batch of eBay leaf categories into the shared community table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
categories: List of (category_id, name, full_path) tuples.
|
||||||
|
platform: Source auction platform (default "ebay").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of rows upserted.
|
||||||
|
"""
|
||||||
|
if not categories:
|
||||||
|
return 0
|
||||||
|
conn = self._db.getconn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO community_categories
|
||||||
|
(platform, category_id, name, full_path, source_product)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
ON CONFLICT (platform, category_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
full_path = EXCLUDED.full_path,
|
||||||
|
source_product = EXCLUDED.source_product,
|
||||||
|
published_at = NOW()
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
(platform, cid, name, path, self._source_product)
|
||||||
|
for cid, name, path in categories
|
||||||
|
],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return len(categories)
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
log.warning(
|
||||||
|
"Failed to publish %d categories to community store",
|
||||||
|
len(categories), exc_info=True,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._db.putconn(conn)
|
||||||
|
|
||||||
|
def fetch_categories(
|
||||||
|
self,
|
||||||
|
platform: str = "ebay",
|
||||||
|
limit: int = 500,
|
||||||
|
) -> list[tuple[str, str, str]]:
|
||||||
|
"""Fetch community-contributed eBay categories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: Source auction platform (default "ebay").
|
||||||
|
limit: Maximum rows to return.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (category_id, name, full_path) tuples ordered by name.
|
||||||
|
"""
|
||||||
|
conn = self._db.getconn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT category_id, name, full_path
|
||||||
|
FROM community_categories
|
||||||
|
WHERE platform = %s
|
||||||
|
ORDER BY name
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(platform, limit),
|
||||||
|
)
|
||||||
|
return [(row[0], row[1], row[2]) for row in cur.fetchall()]
|
||||||
|
finally:
|
||||||
|
self._db.putconn(conn)
|
||||||
1
docs/plausible.js
Normal file
1
docs/plausible.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(function(){var s=document.createElement("script");s.defer=true;s.dataset.domain="docs.circuitforge.tech,circuitforge.tech";s.dataset.api="https://analytics.circuitforge.tech/api/event";s.src="https://analytics.circuitforge.tech/js/script.js";document.head.appendChild(s);})();
|
||||||
82
mkdocs.yml
Normal file
82
mkdocs.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
site_name: circuitforge-core
|
||||||
|
site_description: Shared scaffold for CircuitForge products — modules, conventions, and developer reference.
|
||||||
|
site_author: Circuit Forge LLC
|
||||||
|
site_url: https://docs.circuitforge.tech/cf-core
|
||||||
|
repo_url: https://git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core
|
||||||
|
repo_name: Circuit-Forge/circuitforge-core
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
- scheme: default
|
||||||
|
primary: deep purple
|
||||||
|
accent: purple
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- scheme: slate
|
||||||
|
primary: deep purple
|
||||||
|
accent: purple
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
- navigation.top
|
||||||
|
- navigation.sections
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
- content.code.copy
|
||||||
|
- content.code.annotate
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting Started:
|
||||||
|
- Installation: getting-started/installation.md
|
||||||
|
- Using in a Product: getting-started/using-in-product.md
|
||||||
|
- Module Reference:
|
||||||
|
- Overview: modules/index.md
|
||||||
|
- db: modules/db.md
|
||||||
|
- llm: modules/llm.md
|
||||||
|
- tiers: modules/tiers.md
|
||||||
|
- config: modules/config.md
|
||||||
|
- hardware: modules/hardware.md
|
||||||
|
- documents: modules/documents.md
|
||||||
|
- affiliates: modules/affiliates.md
|
||||||
|
- preferences: modules/preferences.md
|
||||||
|
- tasks: modules/tasks.md
|
||||||
|
- manage: modules/manage.md
|
||||||
|
- resources: modules/resources.md
|
||||||
|
- text: modules/text.md
|
||||||
|
- stt: modules/stt.md
|
||||||
|
- tts: modules/tts.md
|
||||||
|
- pipeline: modules/pipeline.md
|
||||||
|
- vision: modules/vision.md
|
||||||
|
- wizard: modules/wizard.md
|
||||||
|
- Developer Guide:
|
||||||
|
- Adding a Module: developer/adding-module.md
|
||||||
|
- Editable Install Pattern: developer/editable-install.md
|
||||||
|
- BSL vs MIT Boundaries: developer/licensing.md
|
||||||
|
|
||||||
|
extra_javascript:
|
||||||
|
- plausible.js
|
||||||
Loading…
Reference in a new issue