Compare commits

...

3 commits

Author SHA1 Message Date
01ed48808b feat: add Plausible analytics to docs
Some checks failed
CI / test (push) Has been cancelled
Mirror / mirror (push) Has been cancelled
2026-04-16 21:16:04 -07:00
a2c768c635 feat: add community_categories table + SnipeCommunityStore publish/fetch methods
Migration 004 creates community_categories (platform, category_id, name,
full_path, source_product, published_at) with a unique constraint on
(platform, category_id) and a name index.

SnipeCommunityStore gains publish_categories() (upsert batch) and
fetch_categories() (ordered by name, configurable limit) to support
Snipe's community category federation.
2026-04-14 11:38:34 -07:00
f7bf121aef feat(community): SnipeCommunityStore + seller_trust_signals migration
- Add 003_seller_trust_signals.sql: dedicated table for Snipe seller trust
  outcomes (confirmed scammer / confirmed legitimate). Separate from the
  Kiwi recipe post schema — seller signals are a different domain.
- Add SnipeCommunityStore(SharedStore): publish_seller_signal(),
  list_signals_for_seller(), scam_signal_count() methods.
- Export SnipeCommunityStore + SellerTrustSignal from community __init__.
  No PII stored: only platform_seller_id (public username) + flag keys.
2026-04-14 08:28:12 -07:00
6 changed files with 383 additions and 1 deletions

View file

@ -4,5 +4,6 @@
from .models import CommunityPost
from .db import CommunityDB
from .store import SharedStore
from .snipe_store import SellerTrustSignal, SnipeCommunityStore
__all__ = ["CommunityDB", "CommunityPost", "SharedStore"]
__all__ = ["CommunityDB", "CommunityPost", "SharedStore", "SellerTrustSignal", "SnipeCommunityStore"]

View file

@ -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);

View file

@ -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);

View 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
View 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
View 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